mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-27 18:39:58 +00:00
TUN-6695: Implement ICMP proxy for linux
This commit is contained in:

committed by
Chung-Ting Huang

parent
faa86ffeca
commit
fc20a22685
267
ingress/icmp_linux.go
Normal file
267
ingress/icmp_linux.go
Normal file
@@ -0,0 +1,267 @@
|
||||
//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())
|
||||
}
|
Reference in New Issue
Block a user