Files
newt/network/localendpoints.go
2026-07-16 14:42:26 -04:00

165 lines
4.9 KiB
Go

package network
import (
"net"
"regexp"
"sort"
"strconv"
"github.com/fosrl/newt/logger"
)
// Interface name patterns used to rank candidate local endpoints. Interfaces
// matching physicalInterfacePatterns are tried first, interfaces matching
// virtualInterfacePatterns (container/VPN/hypervisor bridges and the like)
// are tried last, and everything else falls in between.
var (
physicalInterfacePatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)^eth\d+$`),
regexp.MustCompile(`(?i)^en\d+$`),
regexp.MustCompile(`(?i)^eno\d+$`),
regexp.MustCompile(`(?i)^ens\d+$`),
regexp.MustCompile(`(?i)^enp\d+s\d+`),
regexp.MustCompile(`(?i)^wlan\d*$`),
regexp.MustCompile(`(?i)^wlp\d+s\d+`),
regexp.MustCompile(`(?i)^wl\d+$`),
regexp.MustCompile(`(?i)ethernet`),
regexp.MustCompile(`(?i)wi-?fi`),
regexp.MustCompile(`(?i)wireless`),
}
virtualInterfacePatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)docker`),
regexp.MustCompile(`(?i)podman`),
regexp.MustCompile(`(?i)^veth`),
regexp.MustCompile(`(?i)^virbr`),
regexp.MustCompile(`(?i)vmnet`),
regexp.MustCompile(`(?i)vboxnet`),
regexp.MustCompile(`(?i)virtualbox`),
regexp.MustCompile(`(?i)^vbox`),
regexp.MustCompile(`(?i)vmware`),
regexp.MustCompile(`(?i)hyper-?v`),
regexp.MustCompile(`(?i)vethernet`),
regexp.MustCompile(`(?i)npcap`),
regexp.MustCompile(`(?i)^tun\d*$`),
regexp.MustCompile(`(?i)^tap\d*$`),
regexp.MustCompile(`(?i)^wg\d*$`),
regexp.MustCompile(`(?i)^utun\d*$`),
regexp.MustCompile(`(?i)zerotier`),
regexp.MustCompile(`(?i)^zt`),
regexp.MustCompile(`(?i)tailscale`),
regexp.MustCompile(`(?i)^ppp\d*$`),
regexp.MustCompile(`(?i)bridge`),
regexp.MustCompile(`(?i)^br-`),
regexp.MustCompile(`(?i)^br\d+$`),
regexp.MustCompile(`(?i)^cni`),
regexp.MustCompile(`(?i)flannel`),
regexp.MustCompile(`(?i)weave`),
regexp.MustCompile(`(?i)kube`),
regexp.MustCompile(`(?i)isatap`),
regexp.MustCompile(`(?i)teredo`),
regexp.MustCompile(`(?i)bluetooth`),
regexp.MustCompile(`(?i)^awdl\d*$`),
regexp.MustCompile(`(?i)^llw\d*$`),
regexp.MustCompile(`(?i)p2p`),
}
)
const (
scorePhysical = 0
scoreUnknown = 10
scoreVirtual = 20
)
// interfaceScore ranks an interface name by how likely it is to be a
// real, usable network interface. Lower scores are tried first.
func interfaceScore(name string) int {
for _, re := range physicalInterfacePatterns {
if re.MatchString(name) {
return scorePhysical
}
}
for _, re := range virtualInterfacePatterns {
if re.MatchString(name) {
return scoreVirtual
}
}
return scoreUnknown
}
// GetLocalEndpoints returns "ip:port" strings (bracketed for IPv6, e.g.
// "[fe80::1]:51820") for every usable, non-loopback IP address bound to a
// network interface on this host. The list is ordered with interfaces most
// likely to be a genuine host network (wired/Wi-Fi) first, and interfaces
// that are typically synthetic (Docker, VPN tunnels, hypervisor bridges,
// etc.) last, so callers should try the results roughly in order.
//
// excludeInterface, if non-empty, is skipped entirely - this is normally the
// name of our own WireGuard/TUN interface, whose address is the tunnel IP
// and not a useful endpoint to advertise.
//
// If interfaces cannot be enumerated (e.g. insufficient OS permissions),
// an info message is logged and an empty slice is returned.
func GetLocalEndpoints(port uint16, excludeInterface string) []string {
ifaces, err := net.Interfaces()
if err != nil {
logger.Info("Unable to enumerate local network interfaces, localEndpoints will not be reported: %v", err)
return nil
}
type candidate struct {
score int
ip string
}
var candidates []candidate
for _, iface := range ifaces {
if excludeInterface != "" && iface.Name == excludeInterface {
continue
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
logger.Debug("Unable to read addresses for interface %s: %v", iface.Name, err)
continue
}
baseScore := interfaceScore(iface.Name)
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() || ip.IsUnspecified() {
continue
}
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
// Link-local addresses (169.254.0.0/16, fe80::/10) aren't
// routable off the local segment, so they're never a
// reachable endpoint for a peer.
continue
}
candidates = append(candidates, candidate{score: baseScore, ip: ip.String()})
}
}
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].score < candidates[j].score
})
portStr := strconv.Itoa(int(port))
endpoints := make([]string, 0, len(candidates))
for _, c := range candidates {
endpoints = append(endpoints, net.JoinHostPort(c.ip, portStr))
}
return endpoints
}