From acb014e23e2166412cf1f8ac91325a96fb896a25 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 1 Sep 2026 17:23:35 -0400 Subject: [PATCH] Add support for local endpoint interface allowlist --- clients/clients.go | 56 ++++++++++++++++++++------------------- config.go | 47 +++++++++++++++++++++++++++----- network/localendpoints.go | 19 ++++++++++++- newt/clients.go | 1 + newt/config.go | 1 + 5 files changed, 89 insertions(+), 35 deletions(-) diff --git a/clients/clients.go b/clients/clients.go index 73f6549..dc2d3c1 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -98,20 +98,21 @@ type PeerReading struct { } type WireGuardService struct { - interfaceName string - mtu int - client *websocket.Client - config WgConfig - key wgtypes.Key - newtId string - lastReadings map[string]PeerReading - mu sync.Mutex - Port uint16 - host string - serverPubKey string - token string - stopGetConfig func() - pendingConfigChainId string + interfaceName string + localEndpointInterfaces []string + mtu int + client *websocket.Client + config WgConfig + key wgtypes.Key + newtId string + lastReadings map[string]PeerReading + mu sync.Mutex + Port uint16 + host string + serverPubKey string + token string + stopGetConfig func() + pendingConfigChainId string // Netstack fields tun tun.Device tnet *netstack2.Net @@ -154,7 +155,7 @@ func generateChainId() string { return hex.EncodeToString(b) } -func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool) (*WireGuardService, error) { +func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool, localEndpointInterfaces []string) (*WireGuardService, error) { key, err := wgtypes.GeneratePrivateKey() if err != nil { return nil, fmt.Errorf("failed to generate private key: %v", err) @@ -195,17 +196,18 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string dnsAddrs := []netip.Addr{netip.MustParseAddr(dns)} service := &WireGuardService{ - interfaceName: interfaceName, - mtu: mtu, - client: wsClient, - key: key, - newtId: newtId, - host: host, - lastReadings: make(map[string]PeerReading), - Port: port, - dns: dnsAddrs, - sharedBind: sharedBind, - useNativeInterface: useNativeInterface, + interfaceName: interfaceName, + localEndpointInterfaces: localEndpointInterfaces, + mtu: mtu, + client: wsClient, + key: key, + newtId: newtId, + host: host, + lastReadings: make(map[string]PeerReading), + Port: port, + dns: dnsAddrs, + sharedBind: sharedBind, + useNativeInterface: useNativeInterface, } // Create the holepunch manager @@ -532,7 +534,7 @@ func (s *WireGuardService) LoadRemoteConfig() error { "publicKey": s.key.PublicKey().String(), "port": s.Port, "chainId": chainId, - "localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName), + "localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName, s.localEndpointInterfaces), }, 2*time.Second) logger.Debug("Requesting WireGuard configuration from remote server") diff --git a/config.go b/config.go index 59f69eb..ab88137 100644 --- a/config.go +++ b/config.go @@ -53,11 +53,12 @@ type fileSettings struct { MTU *int `json:"mtu"` Port *int `json:"port"` - UseNativeInterface *bool `json:"native"` - UseNativeMainInterface *bool `json:"nativeMain"` - NativeMainInterfaceName *string `json:"interfaceMain"` - NoCloud *bool `json:"noCloud"` - PreferEndpoint *string `json:"preferEndpoint"` + UseNativeInterface *bool `json:"native"` + UseNativeMainInterface *bool `json:"nativeMain"` + NativeMainInterfaceName *string `json:"interfaceMain"` + NoCloud *bool `json:"noCloud"` + PreferEndpoint *string `json:"preferEndpoint"` + LocalEndpointInterfaces []string `json:"localEndpointInterfaces"` PingInterval *string `json:"pingInterval"` PingTimeout *string `json:"pingTimeout"` @@ -294,6 +295,10 @@ func loadNewtConfig() newtpkg.Config { applyStr(&cfg.NativeMainInterfaceName, fileCfg.NativeMainInterfaceName, "interface-main", sources, sourceFile) applyBool(&cfg.NoCloud, fileCfg.NoCloud, "no-cloud", sources, sourceFile) applyStr(&cfg.PreferEndpoint, fileCfg.PreferEndpoint, "prefer-endpoint", sources, sourceFile) + if len(fileCfg.LocalEndpointInterfaces) > 0 { + cfg.LocalEndpointInterfaces = fileCfg.LocalEndpointInterfaces + sources["local-endpoint-interfaces"] = string(sourceFile) + } applyStr(&pingIntervalStr, fileCfg.PingInterval, "ping-interval", sources, sourceFile) applyStr(&pingTimeoutStr, fileCfg.PingTimeout, "ping-timeout", sources, sourceFile) @@ -351,6 +356,16 @@ func loadNewtConfig() newtpkg.Config { applyEnvBool(&cfg.UseNativeMainInterface, "USE_NATIVE_MAIN_INTERFACE", "native-main", sources) applyEnvStr(&cfg.NativeMainInterfaceName, "INTERFACE_MAIN", "interface-main", sources) applyEnvBool(&cfg.NoCloud, "NO_CLOUD", "no-cloud", sources) + if v := os.Getenv("LOCAL_ENDPOINT_INTERFACES"); v != "" { + var names []string + for _, n := range strings.Split(v, ",") { + if t := strings.TrimSpace(n); t != "" { + names = append(names, t) + } + } + cfg.LocalEndpointInterfaces = names + sources["local-endpoint-interfaces"] = string(sourceEnv) + } applyEnvStr(&pingIntervalStr, "PING_INTERVAL", "ping-interval", sources) applyEnvStr(&pingTimeoutStr, "PING_TIMEOUT", "ping-timeout", sources) @@ -416,6 +431,8 @@ func loadNewtConfig() newtpkg.Config { origTLSCert, origTLSKey, origDockerEnforce := cfg.TLSClientCert, cfg.TLSClientKey, dockerEnforceStr origHealthFile, origBlueprintFile, origProvBlueprintFile := cfg.HealthFile, cfg.BlueprintFile, cfg.ProvisioningBlueprintFile origNoCloud, origTLSPrivateKey := cfg.NoCloud, cfg.TLSPrivateKey + localEndpointInterfacesStr := strings.Join(cfg.LocalEndpointInterfaces, ",") + origLocalEndpointInterfaces := localEndpointInterfacesStr origMetrics, origOTLP, origAdminAddr := cfg.MetricsEnabled, cfg.OTLPEnabled, cfg.AdminAddr origMetricsAsync, origPprof, origRegion := cfg.MetricsAsyncBytes, cfg.PprofEnabled, cfg.Region origADKey, origADPrincipals, origADCACert := cfg.AuthDaemonKey, cfg.AuthDaemonPrincipalsFile, cfg.AuthDaemonCACertPath @@ -442,6 +459,7 @@ func loadNewtConfig() newtpkg.Config { flag.StringVar(&pingTimeoutStr, "ping-timeout", pingTimeoutStr, "Timeout for each ping (default 7s)") flag.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", udpProxyIdleTimeoutStr, "Idle timeout for UDP proxied client flows before cleanup") flag.StringVar(&cfg.PreferEndpoint, "prefer-endpoint", cfg.PreferEndpoint, "Prefer this endpoint for the connection (if set, will override the endpoint from the server)") + flag.StringVar(&localEndpointInterfacesStr, "local-endpoint-interfaces", localEndpointInterfacesStr, "Comma-separated list of network interface names to restrict reported local endpoints to (default: report all interfaces)") flag.StringVar(&cfg.ProvisioningKey, "provisioning-key", cfg.ProvisioningKey, "One-time provisioning key used to obtain a newt ID and secret from the server") flag.StringVar(&cfg.NewtName, "name", cfg.NewtName, "Name for the site created during provisioning (supports {{env.VAR}} interpolation)") flag.StringVar(&cfg.ConfigFile, "config-file", configPath, "Path to config file (overrides CONFIG_FILE env var and default location)") @@ -517,6 +535,7 @@ func loadNewtConfig() newtpkg.Config { markCLI("blueprint-file", cfg.BlueprintFile != origBlueprintFile) markCLI("provisioning-blueprint-file", cfg.ProvisioningBlueprintFile != origProvBlueprintFile) markCLI("no-cloud", cfg.NoCloud != origNoCloud) + markCLI("local-endpoint-interfaces", localEndpointInterfacesStr != origLocalEndpointInterfaces) markCLI("metrics", cfg.MetricsEnabled != origMetrics) markCLI("otlp", cfg.OTLPEnabled != origOTLP) markCLI("metrics-admin-addr", cfg.AdminAddr != origAdminAddr) @@ -538,7 +557,7 @@ func loadNewtConfig() newtpkg.Config { } if *showConfig { - printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr) + printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr, localEndpointInterfacesStr) os.Exit(0) } @@ -554,6 +573,19 @@ func loadNewtConfig() newtpkg.Config { } } + // Parse local endpoint interface allowlist (after flag.Parse so CLI takes effect) + if localEndpointInterfacesStr != "" { + var names []string + for _, n := range strings.Split(localEndpointInterfacesStr, ",") { + if t := strings.TrimSpace(n); t != "" { + names = append(names, t) + } + } + cfg.LocalEndpointInterfaces = names + } else { + cfg.LocalEndpointInterfaces = nil + } + // Parse MTU if mtuStr == "" { mtuStr = "1280" @@ -581,7 +613,7 @@ func loadNewtConfig() newtpkg.Config { } // printShowConfig prints the resolved configuration and the source of each value -func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr string) { +func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr, localEndpointInterfacesStr string) { getSource := func(key string) string { if s, ok := sources[key]; ok && s != "" { return s @@ -629,6 +661,7 @@ func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, fmt.Printf(" native-main = %v [%s]\n", cfg.UseNativeMainInterface, getSource("native-main")) fmt.Printf(" interface-main = %s [%s]\n", cfg.NativeMainInterfaceName, getSource("interface-main")) fmt.Printf(" no-cloud = %v [%s]\n", cfg.NoCloud, getSource("no-cloud")) + fmt.Printf(" local-endpoint-interfaces = %s [%s]\n", mask("local-endpoint-interfaces", localEndpointInterfacesStr), getSource("local-endpoint-interfaces")) fmt.Println("\nLogging:") fmt.Printf(" log-level = %s [%s]\n", cfg.LogLevel, getSource("log-level")) diff --git a/network/localendpoints.go b/network/localendpoints.go index 4f03b1a..3b99bad 100644 --- a/network/localendpoints.go +++ b/network/localendpoints.go @@ -98,15 +98,27 @@ func interfaceScore(name string) int { // name of our own WireGuard/TUN interface, whose address is the tunnel IP // and not a useful endpoint to advertise. // +// allowedInterfaces, if non-empty, restricts the result to only those +// interface names (an allowlist), letting callers report a single known-good +// interface instead of every candidate on the host. +// // 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 { +func GetLocalEndpoints(port uint16, excludeInterface string, allowedInterfaces []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 } + var allowedSet map[string]struct{} + if len(allowedInterfaces) > 0 { + allowedSet = make(map[string]struct{}, len(allowedInterfaces)) + for _, name := range allowedInterfaces { + allowedSet[name] = struct{}{} + } + } + type candidate struct { score int ip string @@ -117,6 +129,11 @@ func GetLocalEndpoints(port uint16, excludeInterface string) []string { if excludeInterface != "" && iface.Name == excludeInterface { continue } + if allowedSet != nil { + if _, ok := allowedSet[iface.Name]; !ok { + continue + } + } if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } diff --git a/newt/clients.go b/newt/clients.go index 73b9a03..d7b61a2 100644 --- a/newt/clients.go +++ b/newt/clients.go @@ -42,6 +42,7 @@ func (n *Newt) setupClients() { n.client, n.config.DNS, n.config.UseNativeInterface, + n.config.LocalEndpointInterfaces, ) if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) diff --git a/newt/config.go b/newt/config.go index 89d2001..671c8e7 100644 --- a/newt/config.go +++ b/newt/config.go @@ -29,6 +29,7 @@ type Config struct { NativeMainInterfaceName string NoCloud bool PreferEndpoint string + LocalEndpointInterfaces []string // Timing PingInterval time.Duration