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
This commit is contained in:
parent
d7c62aed71
commit
0b90c422c4
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue