diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index b38f79da98a..a7d5bb10c59 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -417,7 +417,14 @@ func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Ad return netip.Addr{}, fmt.Errorf("expect IPv4, but %s is IPv6", userDefinedSrc) } - addr, err := findLocalAddr(net.ParseIP("192.168.0.1"), 53) + // First try to find an IP from a preferred physical interface, + // avoiding virtual/bridge interfaces (Docker, etc.) + if addr := findPreferredIP(true, logger); addr.IsValid() { + return addr, nil + } + + // Fall back to dialing a public IP to determine the default route interface + addr, err := findLocalAddr(net.ParseIP("8.8.8.8"), 53) if err != nil { addr = netip.IPv4Unspecified() 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 { ip net.IP } +// virtualInterfacePrefixes are prefixes for virtual/bridge interfaces that should be deprioritized +var virtualInterfacePrefixes = []string{ + "br-", // Docker bridge + "docker", // Docker + "veth", // Virtual ethernet (containers) + "virbr", // libvirt bridge + "vboxnet", // VirtualBox + "vmnet", // VMware + "lxcbr", // LXC bridge + "lxdbr", // LXD bridge + "cni", // Kubernetes CNI + "flannel", // Flannel overlay + "cali", // Calico + "weave", // Weave + "podman", // Podman +} + +// physicalInterfacePrefixes are prefixes for physical interfaces that should be prioritized +var physicalInterfacePrefixes = []string{ + "eth", // Traditional ethernet (Linux) + "enp", // Systemd predictable naming (PCI) + "ens", // Systemd predictable naming (slot) + "eno", // Systemd predictable naming (onboard) + "wlan", // Traditional wireless (Linux) + "wlp", // Systemd predictable naming (wireless PCI) + "en", // macOS/BSD ethernet and wireless +} + +// isVirtualInterface returns true if the interface name matches a known virtual/bridge interface pattern +func isVirtualInterface(name string) bool { + for _, prefix := range virtualInterfacePrefixes { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} + +// isPhysicalInterface returns true if the interface name matches a known physical interface pattern +func isPhysicalInterface(name string) bool { + for _, prefix := range physicalInterfacePrefixes { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} + +// findPreferredIP returns an IP address from a preferred physical interface. +// It prioritizes interfaces matching physical patterns and excludes virtual/bridge interfaces. +// Returns zero value if no suitable interface is found. +func findPreferredIP(wantIPv4 bool, logger *zerolog.Logger) netip.Addr { + interfaces, err := net.Interfaces() + if err != nil { + return netip.Addr{} + } + + var fallbackIP netip.Addr + for _, iface := range interfaces { + // Skip interfaces that are down or loopback + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + + addrs, err := iface.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + continue + } + + ip := ipnet.IP + parsedIP, err := netip.ParseAddr(ip.String()) + if err != nil { + continue + } + + // Check IP version match + if wantIPv4 && !parsedIP.Is4() { + continue + } + if !wantIPv4 && !parsedIP.Is6() { + continue + } + + // Skip link-local addresses for IPv4 + if wantIPv4 && ip.IsLinkLocalUnicast() { + continue + } + + // For IPv6, skip if it's link-local and we're looking for a routable address + // (link-local is fine as a fallback) + isLinkLocal := ip.IsLinkLocalUnicast() + + // Skip virtual interfaces + if isVirtualInterface(iface.Name) { + continue + } + + // Prefer physical interfaces + if isPhysicalInterface(iface.Name) && !isLinkLocal { + logger.Debug().Msgf("Selected %s from physical interface %s for ICMP proxy", parsedIP, iface.Name) + return parsedIP + } + + // Store as fallback if we haven't found one yet + if !fallbackIP.IsValid() && !isLinkLocal { + fallbackIP = parsedIP + } + } + } + + return fallbackIP +} + func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) { if userDefinedSrc != "" { addr, err := netip.ParseAddr(userDefinedSrc) @@ -454,6 +580,11 @@ func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src n interfacesWithIPv6 := make([]interfaceIP, 0) for _, interf := range interfaces { + // Skip virtual/bridge interfaces + if isVirtualInterface(interf.Name) { + continue + } + interfaceAddrs, err := interf.Addrs() if err != nil { continue @@ -490,6 +621,17 @@ func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src n } } + // Prefer physical interfaces when selecting from available IPv6 interfaces + for _, interf := range interfacesWithIPv6 { + if isPhysicalInterface(interf.name) { + addr, err := netip.ParseAddr(interf.ip.String()) + if err == nil { + return addr, interf.name, nil + } + } + } + + // Fall back to any non-virtual interface with IPv6 for _, interf := range interfacesWithIPv6 { addr, err := netip.ParseAddr(interf.ip.String()) if err == nil { diff --git a/cmd/cloudflared/tunnel/interface_filter_test.go b/cmd/cloudflared/tunnel/interface_filter_test.go new file mode 100644 index 00000000000..d1f0ec663f1 --- /dev/null +++ b/cmd/cloudflared/tunnel/interface_filter_test.go @@ -0,0 +1,92 @@ +package tunnel + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsVirtualInterface(t *testing.T) { + tests := []struct { + name string + expected bool + }{ + // Virtual interfaces that should be filtered + {"br-1744e4cf9e20", true}, + {"docker0", true}, + {"docker1", true}, + {"veth1234abc", true}, + {"virbr0", true}, + {"vboxnet0", true}, + {"vmnet1", true}, + {"lxcbr0", true}, + {"lxdbr0", true}, + {"cni0", true}, + {"flannel.1", true}, + {"cali1234", true}, + {"weave", true}, + {"podman0", true}, + + // Physical interfaces that should not be filtered + {"eth0", false}, + {"enp6s0", false}, + {"ens192", false}, + {"eno1", false}, + {"wlan0", false}, + {"wlp3s0", false}, + {"lo", false}, + {"bond0", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isVirtualInterface(tt.name) + assert.Equal(t, tt.expected, result, "isVirtualInterface(%q)", tt.name) + }) + } +} + +func TestIsPhysicalInterface(t *testing.T) { + tests := []struct { + name string + expected bool + }{ + // Physical interfaces that should be prioritized (Linux) + {"eth0", true}, + {"eth1", true}, + {"enp6s0", true}, + {"enp0s25", true}, + {"ens192", true}, + {"ens33", true}, + {"eno1", true}, + {"eno2", true}, + {"wlan0", true}, + {"wlan1", true}, + {"wlp3s0", true}, + {"wlp2s0", true}, + + // Physical interfaces (macOS) + {"en0", true}, + {"en1", true}, + {"en5", true}, + + // Non-physical interfaces + {"lo", false}, + {"lo0", false}, + {"docker0", false}, + {"br-abc123", false}, + {"veth1234", false}, + {"bond0", false}, + {"tun0", false}, + {"tap0", false}, + {"utun0", false}, + {"bridge0", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isPhysicalInterface(tt.name) + assert.Equal(t, tt.expected, result, "isPhysicalInterface(%q)", tt.name) + }) + } +}