From 9f326c57c0857bd7c5a7e77ca17191f610648fa4 Mon Sep 17 00:00:00 2001 From: Ivan Shapovalov Date: Tue, 4 Nov 2025 16:48:54 +0100 Subject: [PATCH 1/2] net/netmon: add support for `$TS_{ONLY,AVOID}_INTERFACES` Add a pair of knobs to selectively allow/disallow Tailscale to send data over specific interfaces. Fixes #1552. Signed-off-by: Ivan Shapovalov --- net/netmon/state.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/net/netmon/state.go b/net/netmon/state.go index 27e3524e8..e97c43325 100644 --- a/net/netmon/state.go +++ b/net/netmon/state.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/netip" + "path" "runtime" "slices" "sort" @@ -28,6 +29,14 @@ import ( // same interface and subnet. var forceAllIPv6Endpoints = envknob.RegisterBool("TS_DEBUG_FORCE_ALL_IPV6_ENDPOINTS") +// avoidInterfaces is a debug/power-user knob to exclude specific interfaces from consideration +// when gathering endpoints. +var avoidInterfaces = envknob.RegisterString("TS_AVOID_INTERFACES") + +// Likewise, onlyInterfaces can be set to specify the _only_ interfaces Tailscale is allowed to +// consider when gathering endpoints. +var onlyInterfaces = envknob.RegisterString("TS_ONLY_INTERFACES") + // LoginEndpointForProxyDetermination is the URL used for testing // which HTTP proxy the system should use. var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/" @@ -35,6 +44,20 @@ var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/" func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 } func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 } +func matchInterfaceName(name string, patterns string) bool { + patternsList := strings.Split(patterns, ",") + for _, pattern := range patternsList { + pattern = strings.TrimSpace(pattern) + if pattern == "" { + continue + } + if matched, _ := path.Match(pattern, name); matched { + return true + } + } + return false +} + func isProblematicInterface(nif *net.Interface) bool { name := nif.Name // Don't try to send disco/etc packets over zerotier; they effectively @@ -47,6 +70,17 @@ func isProblematicInterface(nif *net.Interface) bool { return false } +func isAllowedInterface(nif *net.Interface) bool { + name := nif.Name + if onlyInterfaces() != "" && !matchInterfaceName(name, onlyInterfaces()) { + return false + } + if avoidInterfaces() != "" && matchInterfaceName(name, avoidInterfaces()) { + return false + } + return true +} + // LocalAddresses returns the machine's IP addresses, separated by // whether they're loopback addresses. If there are no regular addresses // it will return any IPv4 linklocal or IPv6 unique local addresses because we @@ -66,6 +100,10 @@ func LocalAddresses() (regular, loopback []netip.Addr, err error) { // send Tailscale traffic over. continue } + if !isAllowedInterface(stdIf) { + // Skip interfaces that the user does not want to use with Tailscale. + continue + } ifcIsLoopback := isLoopback(stdIf) addrs, err := iface.Addrs() From 679002272decccca48cb03a58fc7721835ee058d Mon Sep 17 00:00:00 2001 From: Ivan Shapovalov Date: Tue, 4 Nov 2025 16:49:50 +0100 Subject: [PATCH 2/2] net/netmon: add support for `$TS_AVOID_PREFIX` In addition to the previous commit, add a knob to selectively disallow Tailscale to use specific addresses as endpoints. Fixes #1552. Signed-off-by: Ivan Shapovalov --- net/netmon/state.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/net/netmon/state.go b/net/netmon/state.go index e97c43325..d9f646f86 100644 --- a/net/netmon/state.go +++ b/net/netmon/state.go @@ -37,6 +37,10 @@ var avoidInterfaces = envknob.RegisterString("TS_AVOID_INTERFACES") // consider when gathering endpoints. var onlyInterfaces = envknob.RegisterString("TS_ONLY_INTERFACES") +// In the same vein, avoidPrefix is a debug/power-user knob to exclude any addresses within specific +// prefixes from being used as endpoints. This is a more granular version of $TS_AVOID_INTERFACES. +var avoidPrefix = envknob.RegisterString("TS_AVOID_PREFIX") + // LoginEndpointForProxyDetermination is the URL used for testing // which HTTP proxy the system should use. var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/" @@ -58,6 +62,25 @@ func matchInterfaceName(name string, patterns string) bool { return false } +func matchIP(ip netip.Addr, prefixes string) bool { + prefixList := strings.Split(prefixes, ",") + for _, arg := range prefixList { + arg = strings.TrimSpace(arg) + if arg == "" { + continue + } + prefix, err := netip.ParsePrefix(arg) + if err != nil { + continue + } + if prefix.Contains(ip) { + return true + } + } + return false + +} + func isProblematicInterface(nif *net.Interface) bool { name := nif.Name // Don't try to send disco/etc packets over zerotier; they effectively @@ -81,6 +104,13 @@ func isAllowedInterface(nif *net.Interface) bool { return true } +func isAllowedAddress(ip netip.Addr) bool { + if avoidPrefix() != "" && matchIP(ip, avoidPrefix()) { + return false + } + return true +} + // LocalAddresses returns the machine's IP addresses, separated by // whether they're loopback addresses. If there are no regular addresses // it will return any IPv4 linklocal or IPv6 unique local addresses because we @@ -119,6 +149,10 @@ func LocalAddresses() (regular, loopback []netip.Addr, err error) { continue } ip = ip.Unmap() + if !isAllowedAddress(ip) { + // Skip addresses that the user does not want to use with Tailscale. + continue + } // TODO(apenwarr): don't special case cgNAT. // In the general wireguard case, it might // very well be something we can route to