From e21d608bd81a92667a48655b2cf32076011d1a4d Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 15 Jul 2026 17:45:39 -0400 Subject: [PATCH] Scrape and send local endpoints --- network/localendpoints.go | 164 ++++++++++++++++++++++++++++++++++++++ newt/clients.go | 8 ++ newt/handlers.go | 19 +++-- 3 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 network/localendpoints.go diff --git a/network/localendpoints.go b/network/localendpoints.go new file mode 100644 index 0000000..23a2bff --- /dev/null +++ b/network/localendpoints.go @@ -0,0 +1,164 @@ +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 + scoreLinkLocal = 1000 +) + +// 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 + } + + score := baseScore + if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + score += scoreLinkLocal + } + + candidates = append(candidates, candidate{score: score, 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 +} diff --git a/newt/clients.go b/newt/clients.go index 73b9a03..a82aea0 100644 --- a/newt/clients.go +++ b/newt/clients.go @@ -6,6 +6,7 @@ import ( wgnetstack "github.com/fosrl/newt/clients" "github.com/fosrl/newt/clients/permissions" "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/network" "golang.zx2c4.com/wireguard/tun/netstack" ) @@ -102,6 +103,13 @@ func (n *Newt) clientsOnConnect() { } } +// localEndpoints returns "ip:port" candidates on this host that could +// potentially be used to reach our WireGuard listen port, ranked with the +// most likely genuine host interfaces first. +func (n *Newt) localEndpoints() []string { + return network.GetLocalEndpoints(n.config.Port, n.config.InterfaceName) +} + func (n *Newt) clientsStartDirectRelay(tunnelIP string) { if !n.ready { return diff --git a/newt/handlers.go b/newt/handlers.go index 0a448f1..1aa8a27 100644 --- a/newt/handlers.go +++ b/newt/handlers.go @@ -167,10 +167,11 @@ func (n *Newt) registerHandlers(ctx context.Context) { chainId := generateChainId() n.pendingRegisterChainId = chainId n.stopFunc = n.client.SendMessageInterval(topicWGRegister, map[string]interface{}{ - "publicKey": n.publicKey.String(), - "pingResults": pingResults, - "newtVersion": n.config.Version, - "chainId": chainId, + "publicKey": n.publicKey.String(), + "pingResults": pingResults, + "newtVersion": n.config.Version, + "chainId": chainId, + "localEndpoints": n.localEndpoints(), }, 2*time.Second) return @@ -267,10 +268,11 @@ func (n *Newt) registerHandlers(ctx context.Context) { chainId := generateChainId() n.pendingRegisterChainId = chainId n.stopFunc = n.client.SendMessageInterval(topicWGRegister, map[string]interface{}{ - "publicKey": n.publicKey.String(), - "pingResults": pingResults, - "newtVersion": n.config.Version, - "chainId": chainId, + "publicKey": n.publicKey.String(), + "pingResults": pingResults, + "newtVersion": n.config.Version, + "chainId": chainId, + "localEndpoints": n.localEndpoints(), }, 2*time.Second) logger.Debug("Sent exit node ping results to cloud for selection: pingResults=%+v", pingResults) @@ -1016,6 +1018,7 @@ func (n *Newt) registerHandlers(ctx context.Context) { "newtVersion": n.config.Version, "backwardsCompatible": true, "chainId": bcChainId, + "localEndpoints": n.localEndpoints(), }); err != nil { logger.Error("Failed to send registration message: %v", err) return err