Skip to content

Commit be0305e

Browse files
committed
TUN-6741: ICMP proxy tries to listen on specific IPv4 & IPv6 when possible
If it cannot determine the correct interface IP, it will fallback to all interfaces. This commit also introduces the icmpv4-src and icmpv6-src flags
1 parent 3449ea3 commit be0305e

22 files changed

+262
-109
lines changed

cmd/cloudflared/tunnel/configuration.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io/ioutil"
77
mathRand "math/rand"
8+
"net"
9+
"net/netip"
810
"os"
911
"path/filepath"
1012
"strings"
@@ -20,6 +22,7 @@ import (
2022

2123
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
2224
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
25+
"github.com/cloudflare/cloudflared/packet"
2326

2427
"github.com/cloudflare/cloudflared/config"
2528
"github.com/cloudflare/cloudflared/connection"
@@ -384,6 +387,12 @@ func prepareTunnelConfig(
384387
NeedPQ: needPQ,
385388
PQKexIdx: pqKexIdx,
386389
}
390+
packetConfig, err := newPacketConfig(c, log)
391+
if err != nil {
392+
log.Warn().Err(err).Msg("ICMP proxy feature is disabled")
393+
} else {
394+
tunnelConfig.PacketConfig = packetConfig
395+
}
387396
orchestratorConfig := &orchestration.Config{
388397
Ingress: &ingressRules,
389398
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
@@ -453,3 +462,147 @@ func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err err
453462
}
454463
return
455464
}
465+
466+
func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*packet.GlobalRouterConfig, error) {
467+
ipv4Src, err := determineICMPv4Src(c.String("icmpv4-src"), logger)
468+
if err != nil {
469+
return nil, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy")
470+
}
471+
logger.Info().Msgf("ICMP proxy will use %s as source for IPv4", ipv4Src)
472+
473+
ipv6Src, zone, err := determineICMPv6Src(c.String("icmpv6-src"), logger, ipv4Src)
474+
if err != nil {
475+
return nil, errors.Wrap(err, "failed to determine IPv6 source address for ICMP proxy")
476+
}
477+
if zone != "" {
478+
logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone)
479+
} else {
480+
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
481+
}
482+
483+
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger)
484+
if err != nil {
485+
return nil, err
486+
}
487+
return &packet.GlobalRouterConfig{
488+
ICMPRouter: icmpRouter,
489+
IPv4Src: ipv4Src,
490+
IPv6Src: ipv6Src,
491+
Zone: zone,
492+
}, nil
493+
}
494+
495+
func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) {
496+
if userDefinedSrc != "" {
497+
addr, err := netip.ParseAddr(userDefinedSrc)
498+
if err != nil {
499+
return netip.Addr{}, err
500+
}
501+
if addr.Is4() {
502+
return addr, nil
503+
}
504+
return netip.Addr{}, fmt.Errorf("expect IPv4, but %s is IPv6", userDefinedSrc)
505+
}
506+
507+
addr, err := findLocalAddr(net.ParseIP("192.168.0.1"), 53)
508+
if err != nil {
509+
addr = netip.IPv4Unspecified()
510+
logger.Debug().Err(err).Msgf("Failed to determine the IPv4 for this machine. It will use %s to send/listen for ICMPv4 echo", addr)
511+
}
512+
return addr, nil
513+
}
514+
515+
type interfaceIP struct {
516+
name string
517+
ip net.IP
518+
}
519+
520+
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
521+
if userDefinedSrc != "" {
522+
userDefinedIP, zone, _ := strings.Cut(userDefinedSrc, "%")
523+
addr, err := netip.ParseAddr(userDefinedIP)
524+
if err != nil {
525+
return netip.Addr{}, "", err
526+
}
527+
if addr.Is6() {
528+
return addr, zone, nil
529+
}
530+
return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc)
531+
}
532+
533+
// Loop through all the interfaces, the preference is
534+
// 1. The interface where ipv4Src is in
535+
// 2. Interface with IPv6 address
536+
// 3. Unspecified interface
537+
538+
interfaces, err := net.Interfaces()
539+
if err != nil {
540+
return netip.IPv6Unspecified(), "", nil
541+
}
542+
543+
interfacesWithIPv6 := make([]interfaceIP, 0)
544+
for _, interf := range interfaces {
545+
interfaceAddrs, err := interf.Addrs()
546+
if err != nil {
547+
continue
548+
}
549+
550+
foundIPv4SrcInterface := false
551+
for _, interfaceAddr := range interfaceAddrs {
552+
if ipnet, ok := interfaceAddr.(*net.IPNet); ok {
553+
ip := ipnet.IP
554+
if ip.Equal(ipv4Src.AsSlice()) {
555+
foundIPv4SrcInterface = true
556+
}
557+
if ip.To4() == nil {
558+
interfacesWithIPv6 = append(interfacesWithIPv6, interfaceIP{
559+
name: interf.Name,
560+
ip: ip,
561+
})
562+
}
563+
}
564+
}
565+
// Found the interface of ipv4Src. Loop through the addresses to see if there is an IPv6
566+
if foundIPv4SrcInterface {
567+
for _, interfaceAddr := range interfaceAddrs {
568+
if ipnet, ok := interfaceAddr.(*net.IPNet); ok {
569+
ip := ipnet.IP
570+
if ip.To4() == nil {
571+
addr, err := netip.ParseAddr(ip.String())
572+
if err == nil {
573+
return addr, interf.Name, nil
574+
}
575+
}
576+
}
577+
}
578+
}
579+
}
580+
581+
for _, interf := range interfacesWithIPv6 {
582+
addr, err := netip.ParseAddr(interf.ip.String())
583+
if err == nil {
584+
return addr, interf.name, nil
585+
}
586+
}
587+
logger.Debug().Err(err).Msgf("Failed to determine the IPv6 for this machine. It will use %s to send/listen for ICMPv6 echo", netip.IPv6Unspecified())
588+
589+
return netip.IPv6Unspecified(), "", nil
590+
}
591+
592+
// FindLocalAddr tries to dial UDP and returns the local address picked by the OS
593+
func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
594+
udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{
595+
IP: dst,
596+
Port: port,
597+
})
598+
if err != nil {
599+
return netip.Addr{}, err
600+
}
601+
defer udpConn.Close()
602+
localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String())
603+
if err != nil {
604+
return netip.Addr{}, err
605+
}
606+
localAddr := localAddrPort.Addr()
607+
return localAddr, nil
608+
}

cmd/cloudflared/tunnel/subcommands.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,16 @@ var (
176176
Usage: "Base64 encoded secret to set for the tunnel. The decoded secret must be at least 32 bytes long. If not specified, a random 32-byte secret will be generated.",
177177
EnvVars: []string{"TUNNEL_CREATE_SECRET"},
178178
}
179+
icmpv4SrcFlag = &cli.StringFlag{
180+
Name: "icmpv4-src",
181+
Usage: "Source address to send/receive ICMPv4 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to 0.0.0.0.",
182+
EnvVars: []string{"TUNNEL_ICMPV4_SRC"},
183+
}
184+
icmpv6SrcFlag = &cli.StringFlag{
185+
Name: "icmpv6-src",
186+
Usage: "Source address and the interface name to send/receive ICMPv6 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to ::.",
187+
EnvVars: []string{"TUNNEL_ICMPV6_SRC"},
188+
}
179189
)
180190

181191
func buildCreateCommand() *cli.Command {
@@ -613,6 +623,8 @@ func buildRunCommand() *cli.Command {
613623
selectProtocolFlag,
614624
featuresFlag,
615625
tunnelTokenFlag,
626+
icmpv4SrcFlag,
627+
icmpv6SrcFlag,
616628
}
617629
flags = append(flags, configureProxyFlags(false)...)
618630
return &cli.Command{

component-tests/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
METRICS_PORT = 51000
22
MAX_RETRIES = 5
33
BACKOFF_SECS = 7
4+
MAX_LOG_LINES = 50
45

56
PROXY_DNS_PORT = 9053
67

component-tests/test_logging.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44

5+
from constants import MAX_LOG_LINES
56
from util import start_cloudflared, wait_tunnel_ready, send_requests
67

78
# Rolling logger rotate log files after 1 MB
@@ -11,14 +12,24 @@
1112

1213

1314
def assert_log_to_terminal(cloudflared):
14-
stderr = cloudflared.stderr.read(1500)
15-
assert expect_message.encode() in stderr, f"{stderr} doesn't contain {expect_message}"
15+
for _ in range(0, MAX_LOG_LINES):
16+
line = cloudflared.stderr.readline()
17+
if not line:
18+
break
19+
if expect_message.encode() in line:
20+
return
21+
raise Exception(f"terminal log doesn't contain {expect_message}")
1622

1723

1824
def assert_log_in_file(file):
1925
with open(file, "r") as f:
20-
log = f.read(2000)
21-
assert expect_message in log, f"{log} doesn't contain {expect_message}"
26+
for _ in range(0, MAX_LOG_LINES):
27+
line = f.readline()
28+
if not line:
29+
break
30+
if expect_message in line:
31+
return
32+
raise Exception(f"log file doesn't contain {expect_message}")
2233

2334

2435
def assert_json_log(file):

connection/quic.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,22 @@ func NewQUICConnection(
6565
connOptions *tunnelpogs.ConnectionOptions,
6666
controlStreamHandler ControlStreamHandler,
6767
logger *zerolog.Logger,
68-
icmpRouter packet.ICMPRouter,
68+
packetRouterConfig *packet.GlobalRouterConfig,
6969
) (*QUICConnection, error) {
7070
session, err := quic.DialAddr(edgeAddr.String(), tlsConfig, quicConfig)
7171
if err != nil {
7272
return nil, &EdgeQuicDialError{Cause: err}
7373
}
7474

7575
sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity)
76-
var (
77-
datagramMuxer quicpogs.BaseDatagramMuxer
78-
pr *packet.Router
79-
)
80-
if icmpRouter != nil {
81-
datagramMuxerV2 := quicpogs.NewDatagramMuxerV2(session, logger, sessionDemuxChan)
82-
pr = packet.NewRouter(datagramMuxerV2, &returnPipe{muxer: datagramMuxerV2}, icmpRouter, logger)
83-
datagramMuxer = datagramMuxerV2
84-
} else {
85-
datagramMuxer = quicpogs.NewDatagramMuxer(session, logger, sessionDemuxChan)
86-
}
76+
datagramMuxer := quicpogs.NewDatagramMuxerV2(session, logger, sessionDemuxChan)
8777
sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan)
8878

79+
var pr *packet.Router
80+
if packetRouterConfig != nil {
81+
pr = packet.NewRouter(packetRouterConfig, datagramMuxer, &returnPipe{muxer: datagramMuxer}, logger)
82+
}
83+
8984
return &QUICConnection{
9085
session: session,
9186
orchestrator: orchestrator,

connection/quic_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,8 +583,12 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
583583
close(sessionDone)
584584
}()
585585

586-
// Send a message to the quic session on edge side, it should be deumx to this datagram session
587-
muxedPayload := append(payload, sessionID[:]...)
586+
// Send a message to the quic session on edge side, it should be deumx to this datagram v2 session
587+
muxedPayload, err := quicpogs.SuffixSessionID(sessionID, payload)
588+
require.NoError(t, err)
589+
muxedPayload, err = quicpogs.SuffixType(muxedPayload, quicpogs.DatagramTypeUDP)
590+
require.NoError(t, err)
591+
588592
err = edgeQUICSession.SendMessage(muxedPayload)
589593
require.NoError(t, err)
590594

ingress/icmp_darwin.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,12 @@ func (snf echoFunnelID) String() string {
113113
return strconv.FormatUint(uint64(snf), 10)
114114
}
115115

116-
func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
117-
conn, err := newICMPConn(listenIP)
116+
func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
117+
conn, err := newICMPConn(listenIP, zone)
118118
if err != nil {
119119
return nil, err
120120
}
121+
logger.Info().Msgf("Created ICMP proxy listening on %s", conn.LocalAddr())
121122
return &icmpProxy{
122123
srcFunnelTracker: packet.NewFunnelTracker(),
123124
echoIDTracker: newEchoIDTracker(),

ingress/icmp_generic.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ func (ip *icmpProxy) Serve(ctx context.Context) error {
2626
return errICMPProxyNotImplemented
2727
}
2828

29-
func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
29+
func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
3030
return nil, errICMPProxyNotImplemented
3131
}

ingress/icmp_linux.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,27 @@ import (
2424
type icmpProxy struct {
2525
srcFunnelTracker *packet.FunnelTracker
2626
listenIP netip.Addr
27+
ipv6Zone string
2728
logger *zerolog.Logger
2829
idleTimeout time.Duration
2930
}
3031

31-
func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
32-
if err := testPermission(listenIP); err != nil {
32+
func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
33+
if err := testPermission(listenIP, zone); err != nil {
3334
return nil, err
3435
}
3536
return &icmpProxy{
3637
srcFunnelTracker: packet.NewFunnelTracker(),
3738
listenIP: listenIP,
39+
ipv6Zone: zone,
3840
logger: logger,
3941
idleTimeout: idleTimeout,
4042
}, nil
4143
}
4244

43-
func testPermission(listenIP netip.Addr) error {
45+
func testPermission(listenIP netip.Addr, zone string) error {
4446
// Opens a non-privileged ICMP socket. On Linux the group ID of the process needs to be in ping_group_range
45-
conn, err := newICMPConn(listenIP)
47+
conn, err := newICMPConn(listenIP, zone)
4648
if err != nil {
4749
// TODO: TUN-6715 check if cloudflared is in ping_group_range if the check failed. If not log instruction to
4850
// change the group ID
@@ -63,10 +65,11 @@ func (ip *icmpProxy) Request(pk *packet.ICMP, responder packet.FunnelUniPipe) er
6365
}
6466
newConnChan := make(chan *icmp.PacketConn, 1)
6567
newFunnelFunc := func() (packet.Funnel, error) {
66-
conn, err := newICMPConn(ip.listenIP)
68+
conn, err := newICMPConn(ip.listenIP, ip.ipv6Zone)
6769
if err != nil {
6870
return nil, errors.Wrap(err, "failed to open ICMP socket")
6971
}
72+
ip.logger.Debug().Msgf("Opened ICMP socket listen on %s", conn.LocalAddr())
7073
newConnChan <- conn
7174
localUDPAddr, ok := conn.LocalAddr().(*net.UDPAddr)
7275
if !ok {

ingress/icmp_posix.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import (
1616
)
1717

1818
// Opens a non-privileged ICMP socket on Linux and Darwin
19-
func newICMPConn(listenIP netip.Addr) (*icmp.PacketConn, error) {
20-
network := "udp6"
19+
func newICMPConn(listenIP netip.Addr, zone string) (*icmp.PacketConn, error) {
2120
if listenIP.Is4() {
22-
network = "udp4"
21+
return icmp.ListenPacket("udp4", listenIP.String())
2322
}
24-
return icmp.ListenPacket(network, listenIP.String())
23+
listenAddr := listenIP.String()
24+
if zone != "" {
25+
listenAddr = listenAddr + "%" + zone
26+
}
27+
return icmp.ListenPacket("udp6", listenAddr)
2528
}
2629

2730
func netipAddr(addr net.Addr) (netip.Addr, bool) {

0 commit comments

Comments
 (0)