Scrape and send local endpoints

This commit is contained in:
Owen
2026-07-15 17:45:39 -04:00
parent 6d8aca9c7c
commit e21d608bd8
3 changed files with 183 additions and 8 deletions
+164
View File
@@ -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
}
+8
View File
@@ -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
+11 -8
View File
@@ -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