//go:build linux package ingress import ( "context" "fmt" "net" "net/netip" "sync" "sync/atomic" "time" "github.com/google/gopacket/layers" "github.com/pkg/errors" "github.com/rs/zerolog" "golang.org/x/net/icmp" "github.com/cloudflare/cloudflared/packet" ) // The request echo ID is rewritten to the port of the socket. The kernel uses the reply echo ID to demultiplex // We can open a socket for each source so multiple sources requesting the same destination doesn't collide type icmpProxy struct { srcToFlowTracker *srcToFlowTracker listenIP netip.Addr logger *zerolog.Logger shutdownC chan struct{} } func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger) (ICMPProxy, error) { if err := testPermission(listenIP); err != nil { return nil, err } return &icmpProxy{ srcToFlowTracker: newSrcToConnTracker(), listenIP: listenIP, logger: logger, shutdownC: make(chan struct{}), }, nil } func testPermission(listenIP netip.Addr) error { // Opens a non-privileged ICMP socket. On Linux the group ID of the process needs to be in ping_group_range // For more information, see https://man7.org/linux/man-pages/man7/icmp.7.html and https://lwn.net/Articles/422330/ conn, err := newICMPConn(listenIP) if err != nil { // TODO: TUN-6715 check if cloudflared is in ping_group_range if the check failed. If not log instruction to // change the group ID return err } // This conn is only to test if cloudflared has permission to open this type of socket conn.Close() return nil } func (ip *icmpProxy) Request(pk *packet.ICMP, responder packet.FlowResponder) error { if pk == nil { return errPacketNil } switch body := pk.Message.Body.(type) { case *icmp.Echo: return ip.sendICMPEchoRequest(pk, body, responder) default: return fmt.Errorf("sending ICMP %s is not implemented", pk.Type) } } func (ip *icmpProxy) Serve(ctx context.Context) error { <-ctx.Done() close(ip.shutdownC) return ctx.Err() } func (ip *icmpProxy) sendICMPEchoRequest(pk *packet.ICMP, echo *icmp.Echo, responder packet.FlowResponder) error { icmpFlow, ok := ip.srcToFlowTracker.get(pk.Src) if ok { return icmpFlow.send(pk) } conn, err := newICMPConn(ip.listenIP) if err != nil { return err } flow := packet.Flow{ Src: pk.Src, Dst: pk.Dst, Responder: responder, } icmpFlow = newICMPFlow(conn, &flow, uint16(echo.ID), ip.logger) go func() { defer ip.srcToFlowTracker.delete(pk.Src) if err := icmpFlow.serve(ip.shutdownC, defaultCloseAfterIdle); err != nil { ip.logger.Debug().Err(err).Uint16("flowID", icmpFlow.echoID).Msg("flow terminated") } }() ip.srcToFlowTracker.set(pk.Src, icmpFlow) return icmpFlow.send(pk) } type srcIPFlowID netip.Addr func (sifd srcIPFlowID) Type() string { return "srcIP" } func (sifd srcIPFlowID) String() string { return netip.Addr(sifd).String() } type srcToFlowTracker struct { lock sync.RWMutex // srcIPToConn tracks source IP to ICMP connection srcToFlow map[netip.Addr]*icmpFlow } func newSrcToConnTracker() *srcToFlowTracker { return &srcToFlowTracker{ srcToFlow: make(map[netip.Addr]*icmpFlow), } } func (sft *srcToFlowTracker) get(srcIP netip.Addr) (*icmpFlow, bool) { sft.lock.RLock() defer sft.lock.RUnlock() flow, ok := sft.srcToFlow[srcIP] return flow, ok } func (sft *srcToFlowTracker) set(srcIP netip.Addr, flow *icmpFlow) { sft.lock.Lock() defer sft.lock.Unlock() sft.srcToFlow[srcIP] = flow } func (sft *srcToFlowTracker) delete(srcIP netip.Addr) { sft.lock.Lock() defer sft.lock.Unlock() delete(sft.srcToFlow, srcIP) } type icmpFlow struct { conn *icmp.PacketConn flow *packet.Flow echoID uint16 // last active unix time. Unit is seconds lastActive int64 logger *zerolog.Logger } func newICMPFlow(conn *icmp.PacketConn, flow *packet.Flow, echoID uint16, logger *zerolog.Logger) *icmpFlow { return &icmpFlow{ conn: conn, flow: flow, echoID: echoID, lastActive: time.Now().Unix(), logger: logger, } } func (f *icmpFlow) serve(shutdownC chan struct{}, closeAfterIdle time.Duration) error { errC := make(chan error) go func() { errC <- f.listenResponse() }() checkIdleTicker := time.NewTicker(closeAfterIdle) defer f.conn.Close() defer checkIdleTicker.Stop() for { select { case err := <-errC: return err case <-shutdownC: return nil case <-checkIdleTicker.C: now := time.Now().Unix() lastActive := atomic.LoadInt64(&f.lastActive) if now > lastActive+int64(closeAfterIdle.Seconds()) { return errFlowInactive } } } } func (f *icmpFlow) send(pk *packet.ICMP) error { f.updateLastActive() // For IPv4, the pseudoHeader is not used because the checksum is always calculated var pseudoHeader []byte = nil serializedMsg, err := pk.Marshal(pseudoHeader) if err != nil { return errors.Wrap(err, "Failed to encode ICMP message") } // The address needs to be of type UDPAddr when conn is created without priviledge _, err = f.conn.WriteTo(serializedMsg, &net.UDPAddr{ IP: pk.Dst.AsSlice(), }) return err } func (f *icmpFlow) listenResponse() error { buf := make([]byte, mtu) encoder := packet.NewEncoder() for { n, src, err := f.conn.ReadFrom(buf) if err != nil { return err } f.updateLastActive() if err := f.handleResponse(encoder, src, buf[:n]); err != nil { f.logger.Err(err).Str("dst", src.String()).Msg("Failed to handle ICMP response") continue } } } func (f *icmpFlow) handleResponse(encoder *packet.Encoder, from net.Addr, rawPacket []byte) error { // TODO: TUN-6654 Check for IPv6 msg, err := icmp.ParseMessage(int(layers.IPProtocolICMPv4), rawPacket) if err != nil { return err } echo, ok := msg.Body.(*icmp.Echo) if !ok { return fmt.Errorf("received unexpected icmp type %s from non-privileged ICMP socket", msg.Type) } addrPort, err := netip.ParseAddrPort(from.String()) if err != nil { return err } icmpPacket := packet.ICMP{ IP: &packet.IP{ Src: addrPort.Addr(), Dst: f.flow.Src, Protocol: layers.IPProtocol(msg.Type.Protocol()), }, Message: &icmp.Message{ Type: msg.Type, Code: msg.Code, Body: &icmp.Echo{ ID: int(f.echoID), Seq: echo.Seq, Data: echo.Data, }, }, } serializedPacket, err := encoder.Encode(&icmpPacket) if err != nil { return errors.Wrap(err, "Failed to encode ICMP message") } if err := f.flow.Responder.SendPacket(serializedPacket); err != nil { return errors.Wrap(err, "Failed to send packet to the edge") } return nil } func (f *icmpFlow) updateLastActive() { atomic.StoreInt64(&f.lastActive, time.Now().Unix()) }