232 lines
5.9 KiB
Go
232 lines
5.9 KiB
Go
//go:build darwin
|
|
|
|
package ingress
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
"golang.org/x/net/icmp"
|
|
|
|
"github.com/cloudflare/cloudflared/packet"
|
|
)
|
|
|
|
// TODO: TUN-6654 Extend support to IPv6
|
|
// On Darwin, a non-privileged ICMP socket can read messages from all echo IDs, so we use it for all sources.
|
|
type icmpProxy struct {
|
|
// TODO: TUN-6588 clean up flows
|
|
srcFlowTracker *packet.FlowTracker
|
|
echoIDTracker *echoIDTracker
|
|
conn *icmp.PacketConn
|
|
logger *zerolog.Logger
|
|
encoder *packet.Encoder
|
|
}
|
|
|
|
// echoIDTracker tracks which ID has been assigned. It first loops through assignment from lastAssignment to then end,
|
|
// then from the beginning to lastAssignment.
|
|
// ICMP echo are short lived. By the time an ID is revisited, it should have been released.
|
|
type echoIDTracker struct {
|
|
lock sync.RWMutex
|
|
// maps the source IP to an echo ID obtained from assignment
|
|
srcIPMapping map[netip.Addr]uint16
|
|
// assignment tracks if an ID is assigned using index as the ID
|
|
// The size of the array is math.MaxUint16 because echo ID is 2 bytes
|
|
assignment [math.MaxUint16]bool
|
|
// nextAssignment is the next number to check for assigment
|
|
nextAssignment uint16
|
|
}
|
|
|
|
func newEchoIDTracker() *echoIDTracker {
|
|
return &echoIDTracker{
|
|
srcIPMapping: make(map[netip.Addr]uint16),
|
|
}
|
|
}
|
|
|
|
func (eit *echoIDTracker) get(srcIP netip.Addr) (uint16, bool) {
|
|
eit.lock.RLock()
|
|
defer eit.lock.RUnlock()
|
|
id, ok := eit.srcIPMapping[srcIP]
|
|
return id, ok
|
|
}
|
|
|
|
func (eit *echoIDTracker) assign(srcIP netip.Addr) (uint16, bool) {
|
|
eit.lock.Lock()
|
|
defer eit.lock.Unlock()
|
|
|
|
if eit.nextAssignment == math.MaxUint16 {
|
|
eit.nextAssignment = 0
|
|
}
|
|
|
|
for i, assigned := range eit.assignment[eit.nextAssignment:] {
|
|
if !assigned {
|
|
echoID := uint16(i) + eit.nextAssignment
|
|
eit.set(srcIP, echoID)
|
|
return echoID, true
|
|
}
|
|
}
|
|
for i, assigned := range eit.assignment[0:eit.nextAssignment] {
|
|
if !assigned {
|
|
echoID := uint16(i)
|
|
eit.set(srcIP, echoID)
|
|
return echoID, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// Caller should hold the lock
|
|
func (eit *echoIDTracker) set(srcIP netip.Addr, echoID uint16) {
|
|
eit.assignment[echoID] = true
|
|
eit.srcIPMapping[srcIP] = echoID
|
|
eit.nextAssignment = echoID + 1
|
|
}
|
|
|
|
func (eit *echoIDTracker) release(srcIP netip.Addr, id uint16) bool {
|
|
eit.lock.Lock()
|
|
defer eit.lock.Unlock()
|
|
|
|
currentID, ok := eit.srcIPMapping[srcIP]
|
|
if ok && id == currentID {
|
|
delete(eit.srcIPMapping, srcIP)
|
|
eit.assignment[id] = false
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
type echoFlowID uint16
|
|
|
|
func (snf echoFlowID) Type() string {
|
|
return "echoID"
|
|
}
|
|
|
|
func (snf echoFlowID) String() string {
|
|
return strconv.FormatUint(uint64(snf), 10)
|
|
}
|
|
|
|
func newICMPProxy(listenIP net.IP, logger *zerolog.Logger) (ICMPProxy, error) {
|
|
network := "udp6"
|
|
if listenIP.To4() != nil {
|
|
network = "udp4"
|
|
}
|
|
// Opens a non-privileged ICMP socket
|
|
conn, err := icmp.ListenPacket(network, listenIP.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &icmpProxy{
|
|
srcFlowTracker: packet.NewFlowTracker(),
|
|
echoIDTracker: newEchoIDTracker(),
|
|
conn: conn,
|
|
logger: logger,
|
|
encoder: packet.NewEncoder(),
|
|
}, nil
|
|
}
|
|
|
|
func (ip *icmpProxy) Request(pk *packet.ICMP, responder packet.FlowResponder) error {
|
|
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) ListenResponse(ctx context.Context) error {
|
|
go func() {
|
|
<-ctx.Done()
|
|
ip.conn.Close()
|
|
}()
|
|
buf := make([]byte, 1500)
|
|
for {
|
|
n, src, err := ip.conn.ReadFrom(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// TODO: TUN-6654 Check for IPv6
|
|
msg, err := icmp.ParseMessage(int(layers.IPProtocolICMPv4), buf[:n])
|
|
if err != nil {
|
|
ip.logger.Error().Err(err).Str("src", src.String()).Msg("Failed to parse ICMP message")
|
|
continue
|
|
}
|
|
switch body := msg.Body.(type) {
|
|
case *icmp.Echo:
|
|
if err := ip.handleEchoResponse(msg, body); err != nil {
|
|
ip.logger.Error().Err(err).
|
|
Str("src", src.String()).
|
|
Str("flowID", echoFlowID(body.ID).String()).
|
|
Msg("Failed to handle ICMP response")
|
|
continue
|
|
}
|
|
default:
|
|
ip.logger.Warn().
|
|
Str("icmpType", fmt.Sprintf("%s", msg.Type)).
|
|
Msgf("Responding to this type of ICMP is not implemented")
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ip *icmpProxy) sendICMPEchoRequest(pk *packet.ICMP, echo *icmp.Echo, responder packet.FlowResponder) error {
|
|
echoID, ok := ip.echoIDTracker.get(pk.Src)
|
|
if !ok {
|
|
echoID, ok = ip.echoIDTracker.assign(pk.Src)
|
|
if !ok {
|
|
return fmt.Errorf("failed to assign unique echo ID")
|
|
}
|
|
flowID := echoFlowID(echoID)
|
|
flow := packet.Flow{
|
|
Src: pk.Src,
|
|
Dst: pk.Dst,
|
|
Responder: responder,
|
|
}
|
|
if replaced := ip.srcFlowTracker.Register(flowID, &flow, true); replaced {
|
|
ip.logger.Info().Str("src", flow.Src.String()).Str("dst", flow.Dst.String()).Msg("Replaced flow")
|
|
}
|
|
}
|
|
|
|
echo.ID = int(echoID)
|
|
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 = ip.conn.WriteTo(serializedMsg, &net.UDPAddr{
|
|
IP: pk.Dst.AsSlice(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (ip *icmpProxy) handleEchoResponse(msg *icmp.Message, echo *icmp.Echo) error {
|
|
flowID := echoFlowID(echo.ID)
|
|
flow, ok := ip.srcFlowTracker.Get(flowID)
|
|
if !ok {
|
|
return fmt.Errorf("flow not found")
|
|
}
|
|
icmpPacket := packet.ICMP{
|
|
IP: &packet.IP{
|
|
Src: flow.Dst,
|
|
Dst: flow.Src,
|
|
Protocol: layers.IPProtocol(msg.Type.Protocol()),
|
|
},
|
|
Message: msg,
|
|
}
|
|
serializedPacket, err := ip.encoder.Encode(&icmpPacket)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to encode ICMP message")
|
|
}
|
|
if err := flow.Responder.SendPacket(serializedPacket); err != nil {
|
|
return errors.Wrap(err, "Failed to send packet to the edge")
|
|
}
|
|
return nil
|
|
}
|