Skip to content

Commit 9d1e927

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 9d1e927

File tree

2 files changed

+226
-1
lines changed

2 files changed

+226
-1
lines changed

cmd/cloudflared/tunnel/configuration.go

Lines changed: 142 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,124 @@ 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
460+
"enp", // Systemd predictable naming (PCI)
461+
"ens", // Systemd predictable naming (slot)
462+
"eno", // Systemd predictable naming (onboard)
463+
"wlan", // Traditional wireless
464+
"wlp", // Systemd predictable naming (wireless PCI)
465+
}
466+
467+
// isVirtualInterface returns true if the interface name matches a known virtual/bridge interface pattern
468+
func isVirtualInterface(name string) bool {
469+
for _, prefix := range virtualInterfacePrefixes {
470+
if strings.HasPrefix(name, prefix) {
471+
return true
472+
}
473+
}
474+
return false
475+
}
476+
477+
// isPhysicalInterface returns true if the interface name matches a known physical interface pattern
478+
func isPhysicalInterface(name string) bool {
479+
for _, prefix := range physicalInterfacePrefixes {
480+
if strings.HasPrefix(name, prefix) {
481+
return true
482+
}
483+
}
484+
return false
485+
}
486+
487+
// findPreferredIP returns an IP address from a preferred physical interface.
488+
// It prioritizes interfaces matching physical patterns and excludes virtual/bridge interfaces.
489+
// Returns zero value if no suitable interface is found.
490+
func findPreferredIP(wantIPv4 bool, logger *zerolog.Logger) netip.Addr {
491+
interfaces, err := net.Interfaces()
492+
if err != nil {
493+
return netip.Addr{}
494+
}
495+
496+
var fallbackIP netip.Addr
497+
for _, iface := range interfaces {
498+
// Skip interfaces that are down or loopback
499+
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
500+
continue
501+
}
502+
503+
addrs, err := iface.Addrs()
504+
if err != nil {
505+
continue
506+
}
507+
508+
for _, addr := range addrs {
509+
ipnet, ok := addr.(*net.IPNet)
510+
if !ok {
511+
continue
512+
}
513+
514+
ip := ipnet.IP
515+
parsedIP, err := netip.ParseAddr(ip.String())
516+
if err != nil {
517+
continue
518+
}
519+
520+
// Check IP version match
521+
if wantIPv4 && !parsedIP.Is4() {
522+
continue
523+
}
524+
if !wantIPv4 && !parsedIP.Is6() {
525+
continue
526+
}
527+
528+
// Skip link-local addresses for IPv4
529+
if wantIPv4 && ip.IsLinkLocalUnicast() {
530+
continue
531+
}
532+
533+
// For IPv6, skip if it's link-local and we're looking for a routable address
534+
// (link-local is fine as a fallback)
535+
isLinkLocal := ip.IsLinkLocalUnicast()
536+
537+
// Skip virtual interfaces
538+
if isVirtualInterface(iface.Name) {
539+
continue
540+
}
541+
542+
// Prefer physical interfaces
543+
if isPhysicalInterface(iface.Name) && !isLinkLocal {
544+
logger.Debug().Msgf("Selected %s from physical interface %s for ICMP proxy", parsedIP, iface.Name)
545+
return parsedIP
546+
}
547+
548+
// Store as fallback if we haven't found one yet
549+
if !fallbackIP.IsValid() && !isLinkLocal {
550+
fallbackIP = parsedIP
551+
}
552+
}
553+
}
554+
555+
return fallbackIP
556+
}
557+
433558
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
434559
if userDefinedSrc != "" {
435560
addr, err := netip.ParseAddr(userDefinedSrc)
@@ -454,6 +579,11 @@ func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src n
454579

455580
interfacesWithIPv6 := make([]interfaceIP, 0)
456581
for _, interf := range interfaces {
582+
// Skip virtual/bridge interfaces
583+
if isVirtualInterface(interf.Name) {
584+
continue
585+
}
586+
457587
interfaceAddrs, err := interf.Addrs()
458588
if err != nil {
459589
continue
@@ -490,6 +620,17 @@ func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src n
490620
}
491621
}
492622

623+
// Prefer physical interfaces when selecting from available IPv6 interfaces
624+
for _, interf := range interfacesWithIPv6 {
625+
if isPhysicalInterface(interf.name) {
626+
addr, err := netip.ParseAddr(interf.ip.String())
627+
if err == nil {
628+
return addr, interf.name, nil
629+
}
630+
}
631+
}
632+
633+
// Fall back to any non-virtual interface with IPv6
493634
for _, interf := range interfacesWithIPv6 {
494635
addr, err := netip.ParseAddr(interf.ip.String())
495636
if err == nil {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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
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+
// Non-physical interfaces
69+
{"lo", false},
70+
{"docker0", false},
71+
{"br-abc123", false},
72+
{"veth1234", false},
73+
{"bond0", false},
74+
{"tun0", false},
75+
{"tap0", false},
76+
}
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
result := isPhysicalInterface(tt.name)
81+
assert.Equal(t, tt.expected, result, "isPhysicalInterface(%q)", tt.name)
82+
})
83+
}
84+
}

0 commit comments

Comments
 (0)