mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-06-18 06:56:34 +00:00

A new ICMPResponder interface is introduced to provide different implementations of how the ICMP flows should return to the QUIC connection muxer. Improves usages of netip.AddrPort to leverage the embedded zone field for IPv6 addresses. Closes TUN-8640
203 lines
5.1 KiB
Go
203 lines
5.1 KiB
Go
//go:build darwin || linux
|
|
|
|
package ingress
|
|
|
|
// This file extracts logic shared by Linux and Darwin implementation if ICMPProxy.
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"sync/atomic"
|
|
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/rs/zerolog"
|
|
"golang.org/x/net/icmp"
|
|
|
|
"github.com/cloudflare/cloudflared/packet"
|
|
)
|
|
|
|
// Opens a non-privileged ICMP socket on Linux and Darwin
|
|
func newICMPConn(listenIP netip.Addr) (*icmp.PacketConn, error) {
|
|
if listenIP.Is4() {
|
|
return icmp.ListenPacket("udp4", listenIP.String())
|
|
}
|
|
return icmp.ListenPacket("udp6", listenIP.String())
|
|
}
|
|
|
|
func netipAddr(addr net.Addr) (netip.Addr, bool) {
|
|
udpAddr, ok := addr.(*net.UDPAddr)
|
|
if !ok {
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
return udpAddr.AddrPort().Addr(), true
|
|
}
|
|
|
|
type flow3Tuple struct {
|
|
srcIP netip.Addr
|
|
dstIP netip.Addr
|
|
originalEchoID int
|
|
}
|
|
|
|
// icmpEchoFlow implements the packet.Funnel interface.
|
|
type icmpEchoFlow struct {
|
|
*packet.ActivityTracker
|
|
closeCallback func() error
|
|
closed *atomic.Bool
|
|
src netip.Addr
|
|
originConn *icmp.PacketConn
|
|
responder ICMPResponder
|
|
assignedEchoID int
|
|
originalEchoID int
|
|
}
|
|
|
|
func newICMPEchoFlow(src netip.Addr, closeCallback func() error, originConn *icmp.PacketConn, responder ICMPResponder, assignedEchoID, originalEchoID int) *icmpEchoFlow {
|
|
return &icmpEchoFlow{
|
|
ActivityTracker: packet.NewActivityTracker(),
|
|
closeCallback: closeCallback,
|
|
closed: &atomic.Bool{},
|
|
src: src,
|
|
originConn: originConn,
|
|
responder: responder,
|
|
assignedEchoID: assignedEchoID,
|
|
originalEchoID: originalEchoID,
|
|
}
|
|
}
|
|
|
|
func (ief *icmpEchoFlow) Equal(other packet.Funnel) bool {
|
|
otherICMPFlow, ok := other.(*icmpEchoFlow)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if otherICMPFlow.src != ief.src {
|
|
return false
|
|
}
|
|
if otherICMPFlow.originalEchoID != ief.originalEchoID {
|
|
return false
|
|
}
|
|
if otherICMPFlow.assignedEchoID != ief.assignedEchoID {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (ief *icmpEchoFlow) Close() error {
|
|
ief.closed.Store(true)
|
|
return ief.closeCallback()
|
|
}
|
|
|
|
func (ief *icmpEchoFlow) IsClosed() bool {
|
|
return ief.closed.Load()
|
|
}
|
|
|
|
// sendToDst rewrites the echo ID to the one assigned to this flow
|
|
func (ief *icmpEchoFlow) sendToDst(dst netip.Addr, msg *icmp.Message) error {
|
|
ief.UpdateLastActive()
|
|
originalEcho, err := getICMPEcho(msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sendMsg := icmp.Message{
|
|
Type: msg.Type,
|
|
Code: msg.Code,
|
|
Body: &icmp.Echo{
|
|
ID: ief.assignedEchoID,
|
|
Seq: originalEcho.Seq,
|
|
Data: originalEcho.Data,
|
|
},
|
|
}
|
|
// For IPv4, the pseudoHeader is not used because the checksum is always calculated
|
|
var pseudoHeader []byte = nil
|
|
serializedPacket, err := sendMsg.Marshal(pseudoHeader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = ief.originConn.WriteTo(serializedPacket, &net.UDPAddr{
|
|
IP: dst.AsSlice(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// returnToSrc rewrites the echo ID to the original echo ID from the eyeball
|
|
func (ief *icmpEchoFlow) returnToSrc(reply *echoReply) error {
|
|
ief.UpdateLastActive()
|
|
reply.echo.ID = ief.originalEchoID
|
|
reply.msg.Body = reply.echo
|
|
pk := packet.ICMP{
|
|
IP: &packet.IP{
|
|
Src: reply.from,
|
|
Dst: ief.src,
|
|
Protocol: layers.IPProtocol(reply.msg.Type.Protocol()),
|
|
TTL: packet.DefaultTTL,
|
|
},
|
|
Message: reply.msg,
|
|
}
|
|
return ief.responder.ReturnPacket(&pk)
|
|
}
|
|
|
|
type echoReply struct {
|
|
from netip.Addr
|
|
msg *icmp.Message
|
|
echo *icmp.Echo
|
|
}
|
|
|
|
func parseReply(from net.Addr, rawMsg []byte) (*echoReply, error) {
|
|
fromAddr, ok := netipAddr(from)
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot convert %s to netip.Addr", from)
|
|
}
|
|
proto := layers.IPProtocolICMPv4
|
|
if fromAddr.Is6() {
|
|
proto = layers.IPProtocolICMPv6
|
|
}
|
|
msg, err := icmp.ParseMessage(int(proto), rawMsg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
echo, err := getICMPEcho(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &echoReply{
|
|
from: fromAddr,
|
|
msg: msg,
|
|
echo: echo,
|
|
}, nil
|
|
}
|
|
|
|
func toICMPEchoFlow(funnel packet.Funnel) (*icmpEchoFlow, error) {
|
|
icmpFlow, ok := funnel.(*icmpEchoFlow)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%v is not *ICMPEchoFunnel", funnel)
|
|
}
|
|
return icmpFlow, nil
|
|
}
|
|
|
|
func createShouldReplaceFunnelFunc(logger *zerolog.Logger, responder ICMPResponder, pk *packet.ICMP, originalEchoID int) func(packet.Funnel) bool {
|
|
return func(existing packet.Funnel) bool {
|
|
existingFlow, err := toICMPEchoFlow(existing)
|
|
if err != nil {
|
|
logger.Err(err).
|
|
Str("src", pk.Src.String()).
|
|
Str("dst", pk.Dst.String()).
|
|
Int("originalEchoID", originalEchoID).
|
|
Msg("Funnel of wrong type found")
|
|
return true
|
|
}
|
|
// Each quic connection should have a unique muxer.
|
|
// If the existing flow has a different muxer, there's a new quic connection where return packets should be
|
|
// routed. Otherwise, return packets will be send to the first observed incoming connection, rather than the
|
|
// most recently observed connection.
|
|
if existingFlow.responder.ConnectionIndex() != responder.ConnectionIndex() {
|
|
logger.Debug().
|
|
Str("src", pk.Src.String()).
|
|
Str("dst", pk.Dst.String()).
|
|
Int("originalEchoID", originalEchoID).
|
|
Msg("Replacing funnel with new responder")
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|