Skip to content

Commit 0b90c42

Browse files
committed
fix(tunnel): improve ICMP proxy interface selection
On systems with Docker or other container runtimes, the ICMP proxy was selecting virtual bridge interfaces (br-*, docker*, veth*) instead of physical interfaces (eth*, enp*, ens*, wlan*). This change: - Adds interface filtering to exclude known virtual/bridge interfaces - Prioritizes physical interfaces when selecting ICMP source addresses - Changes fallback dial target from 192.168.0.1 to 8.8.8.8 for better default route detection - Applies the same filtering to IPv6 source selection The existing --icmpv4-src and --icmpv6-src flags continue to work as manual overrides. Fixes #1546
1 parent d7c62ae commit 0b90c42

File tree

2 files changed

+235
-1
lines changed

2 files changed

+235
-1
lines changed

cmd/cloudflared/tunnel/configuration.go

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,14 @@ func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Ad
417417
return netip.Addr{}, fmt.Errorf("expect IPv4, but %s is IPv6", userDefinedSrc)
418418
}
419419

420-
addr, err := findLocalAddr(net.ParseIP("192.168.0.1"), 53)
420+
// First try to find an IP from a preferred physical interface,
421+
// avoiding virtual/bridge interfaces (Docker, etc.)
422+
if addr := findPreferredIP(true, logger); addr.IsValid() {
423+
return addr, nil
424+
}
425+
426+
// Fall back to dialing a public IP to determine the default route interface
427+
addr, err := findLocalAddr(net.ParseIP("8.8.8.8"), 53)
421428
if err != nil {
422429
addr = netip.IPv4Unspecified()
423430
logger.Debug().Err(err).Msgf("Failed to determine the IPv4 for this machine. It will use %s to send/listen for ICMPv4 echo", addr)
@@ -430,6 +437,125 @@ type interfaceIP struct {
430437
ip net.IP
431438
}
432439

440+
// virtualInterfacePrefixes are prefixes for virtual/bridge interfaces that should be deprioritized
441+
var virtualInterfacePrefixes = []string{
442+
"br-", // Docker bridge
443+
"docker", // Docker
444+
"veth", // Virtual ethernet (containers)
445+
"virbr", // libvirt bridge
446+
"vboxnet", // VirtualBox
447+
"vmnet", // VMware
448+
"lxcbr", // LXC bridge
449+
"lxdbr", // LXD bridge
450+
"cni", // Kubernetes CNI
451+
"flannel", // Flannel overlay
452+
"cali", // Calico
453+
"weave", // Weave
454+
"podman", // Podman
455+
}
456+
457+
// physicalInterfacePrefixes are prefixes for physical interfaces that should be prioritized
458+
var physicalInterfacePrefixes = []string{
459+
"eth", // Traditional ethernet (Linux)
460+
"enp", // Systemd predictable naming (PCI)
461+
"ens", // Systemd predictable naming (slot)
462+
"eno", // Systemd predictable naming (onboard)
463+
"wlan", // Traditional wireless (Linux)
464+
"wlp", // Systemd predictable naming (wireless PCI)
465+
"en", // macOS/BSD ethernet and wireless
466+
}
467+
468+
// isVirtualInterface returns true if the interface name matches a known virtual/bridge interface pattern
469+
func isVirtualInterface(name string) bool {
470+
for _, prefix := range virtualInterfacePrefixes {
471+
if strings.HasPrefix(name, prefix) {
472+
return true
473+
}
474+
}
475+
return false
476+
}
477+
478+
// isPhysicalInterface returns true if the interface name matches a known physical interface pattern
479+
func isPhysicalInterface(name string) bool {
480+
for _, prefix := range physicalInterfacePrefixes {
481+
if strings.HasPrefix(name, prefix) {
482+
return true
483+
}
484+
}
485+
return false
486+
}
487+
488+
// findPreferredIP returns an IP address from a preferred physical interface.
489+
// It prioritizes interfaces matching physical patterns and excludes virtual/bridge interfaces.
490+
// Returns zero value if no suitable interface is found.
491+
func findPreferredIP(wantIPv4 bool, logger *zerolog.Logger) netip.Addr {
492+
interfaces, err := net.Interfaces()
493+
if err != nil {
494+
return netip.Addr{}
495+
}
496+
497+
var fallbackIP netip.Addr
498+
for _, iface := range interfaces {
499+
// Skip interfaces that are down or loopback
500+
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
501+
continue
502+
}
503+
504+
addrs, err := iface.Addrs()
505+
if err != nil {
506+
continue
507+
}
508+
509+
for _, addr := range addrs {
510+
ipnet, ok := addr.(*net.IPNet)
511+
if !ok {
512+
continue
513+
}
514+
515+
ip := ipnet.IP
516+
parsedIP, err := netip.ParseAddr(ip.String())
517+
if err != nil {
518+
continue
519+
}
520+
521+
// Check IP version match
522+
if wantIPv4 && !parsedIP.Is4() {
523+
continue
524+
}
525+
if !wantIPv4 && !parsedIP.Is6() {
526+
continue
527+
}
528+
529+
// Skip link-local addresses for IPv4
530+
if wantIPv4 && ip.IsLinkLocalUnicast() {
531+
continue
532+
}
533+
534+
// For IPv6, skip if it's link-local and we're looking for a routable address
535+
// (link-local is fine as a fallback)
536+
isLinkLocal := ip.IsLinkLocalUnicast()
537+
538+
// Skip virtual interfaces
539+
if isVirtualInterface(iface.Name) {
540+
continue
541+
}
542+
543+
// Prefer physical interfaces
544+
if isPhysicalInterface(iface.Name) && !isLinkLocal {
545+
logger.Debug().Msgf("Selected %s from physical interface %s for ICMP proxy", parsedIP, iface.Name)
546+
return parsedIP
547+
}
548+
549+
// Store as fallback if we haven't found one yet
550+
if !fallbackIP.IsValid() && !isLinkLocal {
551+
fallbackIP = parsedIP
552+
}
553+
}
554+
}
555+
556+
return fallbackIP
557+
}
558+
433559
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
434560
if userDefinedSrc != "" {
435561
addr, err := netip.ParseAddr(userDefinedSrc)
@@ -454,6 +580,11 @@ func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src n
454580

455581
interfacesWithIPv6 := make([]interfaceIP, 0)
456582
for _, interf := range interfaces {
583+
// Skip virtual/bridge interfaces
584+
if isVirtualInterface(interf.Name) {
585+
continue
586+
}
587+
457588
interfaceAddrs, err := interf.Addrs()
458589
if err != nil {
459590
continue
@@ -490,6 +621,17 @@ func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src n
490621
}
491622
}
492623

624+
// Prefer physical interfaces when selecting from available IPv6 interfaces
625+
for _, interf := range interfacesWithIPv6 {
626+
if isPhysicalInterface(interf.name) {
627+
addr, err := netip.ParseAddr(interf.ip.String())
628+
if err == nil {
629+
return addr, interf.name, nil
630+
}
631+
}
632+
}
633+
634+
// Fall back to any non-virtual interface with IPv6
493635
for _, interf := range interfacesWithIPv6 {
494636
addr, err := netip.ParseAddr(interf.ip.String())
495637
if err == nil {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package tunnel
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestIsVirtualInterface(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
expected bool
13+
}{
14+
// Virtual interfaces that should be filtered
15+
{"br-1744e4cf9e20", true},
16+
{"docker0", true},
17+
{"docker1", true},
18+
{"veth1234abc", true},
19+
{"virbr0", true},
20+
{"vboxnet0", true},
21+
{"vmnet1", true},
22+
{"lxcbr0", true},
23+
{"lxdbr0", true},
24+
{"cni0", true},
25+
{"flannel.1", true},
26+
{"cali1234", true},
27+
{"weave", true},
28+
{"podman0", true},
29+
30+
// Physical interfaces that should not be filtered
31+
{"eth0", false},
32+
{"enp6s0", false},
33+
{"ens192", false},
34+
{"eno1", false},
35+
{"wlan0", false},
36+
{"wlp3s0", false},
37+
{"lo", false},
38+
{"bond0", false},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
result := isVirtualInterface(tt.name)
44+
assert.Equal(t, tt.expected, result, "isVirtualInterface(%q)", tt.name)
45+
})
46+
}
47+
}
48+
49+
func TestIsPhysicalInterface(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
expected bool
53+
}{
54+
// Physical interfaces that should be prioritized (Linux)
55+
{"eth0", true},
56+
{"eth1", true},
57+
{"enp6s0", true},
58+
{"enp0s25", true},
59+
{"ens192", true},
60+
{"ens33", true},
61+
{"eno1", true},
62+
{"eno2", true},
63+
{"wlan0", true},
64+
{"wlan1", true},
65+
{"wlp3s0", true},
66+
{"wlp2s0", true},
67+
68+
// Physical interfaces (macOS)
69+
{"en0", true},
70+
{"en1", true},
71+
{"en5", true},
72+
73+
// Non-physical interfaces
74+
{"lo", false},
75+
{"lo0", false},
76+
{"docker0", false},
77+
{"br-abc123", false},
78+
{"veth1234", false},
79+
{"bond0", false},
80+
{"tun0", false},
81+
{"tap0", false},
82+
{"utun0", false},
83+
{"bridge0", false},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
result := isPhysicalInterface(tt.name)
89+
assert.Equal(t, tt.expected, result, "isPhysicalInterface(%q)", tt.name)
90+
})
91+
}
92+
}

0 commit comments

Comments
 (0)