mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-11 04:46:35 +00:00

Remove send and return methods from Funnel interface. Users of Funnel can provide their own send and return methods without wrapper to comply with the interface. Move packet router to ingress package to avoid circular dependency
197 lines
5.9 KiB
Go
197 lines
5.9 KiB
Go
//go:build linux
|
|
|
|
package ingress
|
|
|
|
// This file implements ICMPProxy for Linux. Each (source IP, destination IP, echo ID) opens a non-privileged ICMP socket.
|
|
// The source IP of the requests are rewritten to the bind IP of the socket and echo ID rewritten to the port number of
|
|
// the socket. The kernel ensures the socket only reads replies whose echo ID matches the port number.
|
|
// For more information about the socket, see https://man7.org/linux/man-pages/man7/icmp.7.html and https://lwn.net/Articles/422330/
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
"golang.org/x/net/icmp"
|
|
|
|
"github.com/cloudflare/cloudflared/packet"
|
|
)
|
|
|
|
const (
|
|
// https://lwn.net/Articles/550551/ IPv4 and IPv6 share the same path
|
|
pingGroupPath = "/proc/sys/net/ipv4/ping_group_range"
|
|
)
|
|
|
|
var (
|
|
findGroupIDRegex = regexp.MustCompile(`\d+`)
|
|
)
|
|
|
|
type icmpProxy struct {
|
|
srcFunnelTracker *packet.FunnelTracker
|
|
listenIP netip.Addr
|
|
ipv6Zone string
|
|
logger *zerolog.Logger
|
|
idleTimeout time.Duration
|
|
}
|
|
|
|
func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
|
|
if err := testPermission(listenIP, zone, logger); err != nil {
|
|
return nil, err
|
|
}
|
|
return &icmpProxy{
|
|
srcFunnelTracker: packet.NewFunnelTracker(),
|
|
listenIP: listenIP,
|
|
ipv6Zone: zone,
|
|
logger: logger,
|
|
idleTimeout: idleTimeout,
|
|
}, nil
|
|
}
|
|
|
|
func testPermission(listenIP netip.Addr, zone string, logger *zerolog.Logger) error {
|
|
// Opens a non-privileged ICMP socket. On Linux the group ID of the process needs to be in ping_group_range
|
|
// Only check ping_group_range once for IPv4
|
|
if listenIP.Is4() {
|
|
if err := checkInPingGroup(); err != nil {
|
|
logger.Warn().Err(err).Msgf("The user running cloudflared process has a GID (group ID) that is not within ping_group_range. You might need to add that user to a group within that range, or instead update the range to encompass a group the user is already in by modifying %s. Otherwise cloudflared will not be able to ping this network", pingGroupPath)
|
|
return err
|
|
}
|
|
}
|
|
conn, err := newICMPConn(listenIP, zone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// This conn is only to test if cloudflared has permission to open this type of socket
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
|
|
func checkInPingGroup() error {
|
|
file, err := os.ReadFile(pingGroupPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
groupID := os.Getgid()
|
|
// Example content: 999 59999
|
|
found := findGroupIDRegex.FindAll(file, 2)
|
|
if len(found) == 2 {
|
|
groupMin, err := strconv.ParseInt(string(found[0]), 10, 32)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to determine minimum ping group ID")
|
|
}
|
|
groupMax, err := strconv.ParseInt(string(found[1]), 10, 32)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to determine minimum ping group ID")
|
|
}
|
|
if groupID < int(groupMin) || groupID > int(groupMax) {
|
|
return fmt.Errorf("Group ID %d is not between ping group %d to %d", groupID, groupMin, groupMax)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("did not find group range in %s", pingGroupPath)
|
|
}
|
|
|
|
func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *packetResponder) error {
|
|
originalEcho, err := getICMPEcho(pk.Message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newConnChan := make(chan *icmp.PacketConn, 1)
|
|
newFunnelFunc := func() (packet.Funnel, error) {
|
|
conn, err := newICMPConn(ip.listenIP, ip.ipv6Zone)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to open ICMP socket")
|
|
}
|
|
ip.logger.Debug().Msgf("Opened ICMP socket listen on %s", conn.LocalAddr())
|
|
newConnChan <- conn
|
|
closeCallback := func() error {
|
|
return conn.Close()
|
|
}
|
|
localUDPAddr, ok := conn.LocalAddr().(*net.UDPAddr)
|
|
if !ok {
|
|
return nil, fmt.Errorf("ICMP listener address %s is not net.UDPAddr", conn.LocalAddr())
|
|
}
|
|
echoID := localUDPAddr.Port
|
|
icmpFlow := newICMPEchoFlow(pk.Src, closeCallback, conn, responder, echoID, originalEcho.ID, packet.NewEncoder())
|
|
return icmpFlow, nil
|
|
}
|
|
funnelID := flow3Tuple{
|
|
srcIP: pk.Src,
|
|
dstIP: pk.Dst,
|
|
originalEchoID: originalEcho.ID,
|
|
}
|
|
funnel, isNew, err := ip.srcFunnelTracker.GetOrRegister(funnelID, newFunnelFunc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
icmpFlow, err := toICMPEchoFlow(funnel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isNew {
|
|
ip.logger.Debug().
|
|
Str("src", pk.Src.String()).
|
|
Str("dst", pk.Dst.String()).
|
|
Int("originalEchoID", originalEcho.ID).
|
|
Msg("New flow")
|
|
conn := <-newConnChan
|
|
go func() {
|
|
defer ip.srcFunnelTracker.Unregister(funnelID, icmpFlow)
|
|
if err := ip.listenResponse(icmpFlow, conn); err != nil {
|
|
ip.logger.Debug().Err(err).
|
|
Str("src", pk.Src.String()).
|
|
Str("dst", pk.Dst.String()).
|
|
Int("originalEchoID", originalEcho.ID).
|
|
Msg("Failed to listen for ICMP echo response")
|
|
}
|
|
}()
|
|
}
|
|
if err := icmpFlow.sendToDst(pk.Dst, pk.Message); err != nil {
|
|
return errors.Wrap(err, "failed to send ICMP echo request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ip *icmpProxy) Serve(ctx context.Context) error {
|
|
ip.srcFunnelTracker.ScheduleCleanup(ctx, ip.idleTimeout)
|
|
return ctx.Err()
|
|
}
|
|
|
|
func (ip *icmpProxy) listenResponse(flow *icmpEchoFlow, conn *icmp.PacketConn) error {
|
|
buf := make([]byte, mtu)
|
|
for {
|
|
n, from, err := conn.ReadFrom(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply, err := parseReply(from, buf[:n])
|
|
if err != nil {
|
|
ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply")
|
|
continue
|
|
}
|
|
if !isEchoReply(reply.msg) {
|
|
ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type)
|
|
continue
|
|
}
|
|
if err := flow.returnToSrc(reply); err != nil {
|
|
ip.logger.Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply")
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only linux uses flow3Tuple as FunnelID
|
|
func (ft flow3Tuple) Type() string {
|
|
return "srcIP_dstIP_echoID"
|
|
}
|
|
|
|
func (ft flow3Tuple) String() string {
|
|
return fmt.Sprintf("%s:%s:%d", ft.srcIP, ft.dstIP, ft.originalEchoID)
|
|
}
|