//go:build linux package netenv import ( "bufio" "encoding/binary" "encoding/hex" "fmt" "net" "os" "strconv" "strings" ) // selectPhysicalDefaultInterfaces finds the best physical adapter per IP family // that carries the default route, excluding all virtual and tunnel interfaces. // // Physical detection: the kernel creates /sys/class/net//device only for // adapters bound to a real hardware driver. Virtual interfaces (tun, tap, // bridge, veth, wireguard) never have this entry — this is the most reliable // VPN-exclusion signal available without elevated privileges. // // IPv4 routes: /proc/net/route — always readable without root; provides // destination, mask, and metric (decimal) for every IPv4 route. // // IPv6 routes: /proc/net/ipv6_route — same access requirements; provides // destination, prefix length, next hop, and metric (hex) for every IPv6 route. func selectPhysicalDefaultInterfaces() (*net.Interface, *net.Interface, error) { type candidate struct { name string metric uint32 } var v4candidates, v6candidates []candidate // --- IPv4: read /proc/net/route --- // Columns: Iface Dest Gateway Flags RefCnt Use Metric Mask MTU Window IRTT // Dest and Mask are 4-byte values in 8 hex chars, little-endian. Metric is decimal. f4, err := os.Open("/proc/net/route") if err != nil { return nil, nil, fmt.Errorf("reading IPv4 routing table: %w", err) } defer f4.Close() //nolint:errcheck scanner4 := bufio.NewScanner(f4) scanner4.Scan() // skip header for scanner4.Scan() { fields := strings.Fields(scanner4.Text()) if len(fields) < 8 { continue } dest, err := hex.DecodeString(fields[1]) if err != nil || len(dest) != 4 { continue } mask, err := hex.DecodeString(fields[7]) if err != nil || len(mask) != 4 { continue } // Default route: 0.0.0.0/0 if binary.LittleEndian.Uint32(dest) != 0 || binary.LittleEndian.Uint32(mask) != 0 { continue } name := fields[0] if !isSysfsPhysical(name) { continue } // Metric column is decimal. metric, err := strconv.ParseUint(fields[6], 10, 32) if err != nil { continue } v4candidates = append(v4candidates, candidate{name, uint32(metric)}) } if err := scanner4.Err(); err != nil { return nil, nil, fmt.Errorf("scanning IPv4 routing table: %w", err) } // --- IPv6: read /proc/net/ipv6_route --- // Columns: dest destpfxlen src srcpfxlen nexthop metric refcnt use flags iface // All addresses are 32 hex chars (no colons). Metric is hex. Iface is last. f6, err := os.Open("/proc/net/ipv6_route") if err == nil { defer f6.Close() //nolint:errcheck scanner6 := bufio.NewScanner(f6) for scanner6.Scan() { fields := strings.Fields(scanner6.Text()) if len(fields) < 10 { continue } // Default route: destination = ::/0 if fields[0] != "00000000000000000000000000000000" { continue } pfxLen, err := strconv.ParseUint(fields[1], 16, 8) if err != nil || pfxLen != 0 { continue } // Skip on-link entries that have no actual gateway. if fields[4] == "00000000000000000000000000000000" { continue } name := fields[len(fields)-1] if !isSysfsPhysical(name) { continue } // Metric is hex in ipv6_route (unlike decimal in /proc/net/route). metric, err := strconv.ParseUint(fields[5], 16, 32) if err != nil { continue } v6candidates = append(v6candidates, candidate{name, uint32(metric)}) } // IPv6 scanner errors are non-fatal — leave result.IPv6 as nil. } // If /proc/net/ipv6_route is absent, IPv6 is not configured; that is not an error. // Pick the lowest-metric candidate per family that also has a routable address, // confirming DHCP/SLAAC has completed and the interface is actively communicating. var ipv4Iface, ipv6Iface *net.Interface var bestV4Metric, bestV6Metric uint32 for _, c := range v4candidates { iface, err := net.InterfaceByName(c.name) if err != nil || !hasRoutableIPv4(iface) { continue } if ipv4Iface == nil || c.metric < bestV4Metric { ipv4Iface = iface bestV4Metric = c.metric } } for _, c := range v6candidates { iface, err := net.InterfaceByName(c.name) if err != nil || !hasRoutableIPv6(iface) { continue } if ipv6Iface == nil || c.metric < bestV6Metric { ipv6Iface = iface bestV6Metric = c.metric } } return ipv4Iface, ipv6Iface, nil } // isSysfsPhysical reports whether the named interface is backed by a real // hardware driver. The kernel creates /sys/class/net//device only for // adapters bound to an actual device driver (PCI/USB Ethernet, wireless card). // Virtual interfaces — tun, tap, bridge, veth, wireguard, loopback — never // have this sysfs entry. func isSysfsPhysical(name string) bool { _, err := os.Stat("/sys/class/net/" + name + "/device") return err == nil }