This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+3 -4
View File
@@ -1,11 +1,10 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Common code for FreeBSD. This might also work on other
// BSD systems (e.g. OpenBSD) but has not been tested.
// Common code for FreeBSD and OpenBSD.
// Not used on iOS or macOS. See defaultroute_darwin.go.
//go:build freebsd
//go:build freebsd || openbsd
package netmon
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin || ios
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
+85 -110
View File
@@ -1,13 +1,11 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
import (
"bytes"
"log"
"net/netip"
"os/exec"
"sync/atomic"
"go4.org/mem"
@@ -19,6 +17,7 @@ import (
var (
lastKnownDefaultRouteIfName syncs.AtomicValue[string]
lastKnownDefaultGateway syncs.AtomicValue[string]
)
var procNetRoutePath = "/proc/net/route"
@@ -42,123 +41,99 @@ ens18 00000000 0100000A 0003 0 0 0 00000000
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
*/
func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
if procNetRouteErr.Load() {
// If we failed to read /proc/net/route previously, don't keep trying.
return likelyHomeRouterIPHelper()
}
lineNum := 0
var f []mem.RO
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
lineNum++
if lineNum == 1 {
// Skip header line.
continue
}
if lineNum > maxProcNetRouteRead {
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
break
if gwStr := lastKnownDefaultGateway.Load(); gwStr != "" {
if ip, err := netip.ParseAddr(gwStr); err == nil {
return ip, netip.Addr{}, true
}
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with
// this route to short-circuit finding the IP associated with
// this gateway. This isn't fatal if it fails.
if len(f) > 0 && !disableLikelyHomeRouterIPSelf() {
ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
// Ensure this is the same interface
if !f[0].EqualString(ni.Name) {
return
}
// Fall back to /proc/net/route for early startup before
// NetworkChangeCallback has fired, or if the cached gateway
// was empty (e.g., cellular with no private gateway).
if !procNetRouteErr.Load() {
lineNum := 0
var f []mem.RO
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
// Find the first IPv4 address and use it.
for _, pfx := range pfxs {
if addr := pfx.Addr(); addr.Is4() {
myIP = addr
break
lineNum++
if lineNum == 1 {
// Skip header line.
continue
}
if lineNum > maxProcNetRouteRead {
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
break
}
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with
// this route to short-circuit finding the IP associated with
// this gateway. This isn't fatal if it fails.
if len(f) > 0 && !disableLikelyHomeRouterIPSelf() {
ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
// Ensure this is the same interface
if !f[0].EqualString(ni.Name) {
return
}
}
})
}
return ret, myIP, true
}
if lineNum >= maxProcNetRouteRead {
// If we went over our line limit without finding an answer, assume
// we're a big fancy Linux router (or at least not a home system)
// and set the error bit so we stop trying this in the future (and wasting CPU).
// See https://github.com/tailscale/tailscale/issues/7621.
//
// Remember that "likelyHomeRouterIP" exists purely to find the port
// mapping service (UPnP, PMP, PCP) often present on a home router. If we hit
// the route (line) limit without finding an answer, we're unlikely to ever
// find one in the future.
procNetRouteErr.Store(true)
// Find the first IPv4 address and use it.
for _, pfx := range pfxs {
if addr := pfx.Addr(); addr.Is4() {
myIP = addr
break
}
}
})
}
return ret, myIP, true
}
if lineNum >= maxProcNetRouteRead {
// If we went over our line limit without finding an answer, assume
// we're a big fancy Linux router (or at least not a home system)
// and set the error bit so we stop trying this in the future (and wasting CPU).
// See https://github.com/tailscale/tailscale/issues/7621.
//
// Remember that "likelyHomeRouterIP" exists purely to find the port
// mapping service (UPnP, PMP, PCP) often present on a home router. If we hit
// the route (line) limit without finding an answer, we're unlikely to ever
// find one in the future.
procNetRouteErr.Store(true)
}
}
return netip.Addr{}, netip.Addr{}, false
}
// Android apps don't have permission to read /proc/net/route, at
// least on Google devices and the Android emulator.
func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) {
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
out, err := cmd.StdoutPipe()
if err != nil {
return
// UpdateLastKnownDefaultGateway is called by libtailscale in the Android app when
// the connectivity manager provides an updated default gateway IP from LinkProperties.
func UpdateLastKnownDefaultGateway(ipStr string) {
if old := lastKnownDefaultGateway.Swap(ipStr); old != ipStr {
log.Printf("defaultgateway: update from Android, gateway = %s (was %s)", ipStr, old)
}
if err := cmd.Start(); err != nil {
log.Printf("interfaces: running /system/bin/ip: %v", err)
return
}
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
for lr := range lineiter.Reader(out) {
line, err := lr.Value()
if err != nil {
break
}
const pfx = "default via "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
continue
}
line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ')
if sp == -1 {
continue
}
ipb := line[:sp]
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
ret = ip
log.Printf("interfaces: found Android default route %v", ip)
}
}
cmd.Process.Kill()
cmd.Wait()
return ret, netip.Addr{}, ret.IsValid()
}
// UpdateLastKnownDefaultRouteInterface is called by libtailscale in the Android app when
+2 -2
View File
@@ -1,10 +1,10 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Common code for FreeBSD and Darwin. This might also work on other
// BSD systems (e.g. OpenBSD) but has not been tested.
//go:build darwin || freebsd
//go:build darwin || freebsd || openbsd
package netmon
@@ -1,9 +1,9 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// This might work on other BSDs, but only tested on FreeBSD.
// FreeBSD and OpenBSD routing table functions.
//go:build freebsd
//go:build freebsd || openbsd
package netmon
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
+2 -2
View File
@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux && !windows && !darwin && !freebsd && !android
//go:build !linux && !windows && !darwin && !freebsd && !android && !openbsd
package netmon
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
+123 -49
View File
@@ -1,9 +1,9 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package monitor provides facilities for monitoring network
// interface and route changes. It primarily exists to know when
// portable devices move between different networks.
// Package netmon provides facilities for monitoring network interface and
// route changes. It primarily exists to know when portable devices move
// between different networks.
package netmon
import (
@@ -13,6 +13,7 @@ import (
"net/netip"
"runtime"
"slices"
"strings"
"sync"
"time"
@@ -31,6 +32,15 @@ import (
// us check the wall time sooner than this.
const pollWallTimeInterval = 15 * time.Second
// majorTimeJumpThreshold is the minimum sleep duration that warrants
// treating a time jump as a major event requiring socket rebinding,
// even if the interface state appears unchanged. After a long sleep,
// NAT mappings are likely stale and DHCP leases may have expired
// (the renewal happens after wake, so local state may not yet reflect it).
// Short sleeps (e.g., macOS DarkWake maintenance cycles of ~55s) should
// not trigger rebinding if the network state is unchanged.
const majorTimeJumpThreshold = 10 * time.Minute
// message represents a message returned from an osMon.
type message interface {
// Ignore is whether we should ignore this message.
@@ -67,18 +77,18 @@ type Monitor struct {
stop chan struct{} // closed on Stop
static bool // static Monitor that doesn't actually monitor
mu syncs.Mutex // guards all following fields
cbs set.HandleSet[ChangeFunc]
ifState *State
gwValid bool // whether gw and gwSelfIP are valid
gw netip.Addr // our gateway's IP
gwSelfIP netip.Addr // our own IP address (that corresponds to gw)
started bool
closed bool
goroutines sync.WaitGroup
wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick
lastWall time.Time
timeJumped bool // whether we need to send a changed=true after a big time jump
mu syncs.Mutex // guards all following fields
cbs set.HandleSet[ChangeFunc]
ifState *State
gwValid bool // whether gw and gwSelfIP are valid
gw netip.Addr // our gateway's IP
gwSelfIP netip.Addr // our own IP address (that corresponds to gw)
started bool
closed bool
goroutines sync.WaitGroup
wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick
lastWall time.Time
jumpDuration time.Duration // wall-clock time elapsed during detected time jump; 0 if no time jump observed since reset
}
// ChangeFunc is a callback function registered with Monitor that's called when the
@@ -97,10 +107,12 @@ type ChangeDelta struct {
// It is always non-nil.
new *State
// TimeJumped is whether there was a big jump in wall time since the last
// time we checked. This is a hint that a sleeping device might have
// come out of sleep.
TimeJumped bool
// JumpDuration is non-zero when a wall-clock time jump was detected,
// indicating the machine likely just woke from sleep. It is approximately
// how long the machine was asleep (the wall-clock delta since the last
// check, not an exact sleep measurement). Use TimeJumped() to check
// whether a time jump occurred.
JumpDuration time.Duration
DefaultRouteInterface string
@@ -121,19 +133,28 @@ type ChangeDelta struct {
RebindLikelyRequired bool
}
// TimeJumped reports whether a wall-clock time jump was detected,
// indicating the machine likely just woke from sleep. When true,
// JumpDuration contains the approximate duration.
func (cd *ChangeDelta) TimeJumped() bool {
return cd.JumpDuration > 0
}
// CurrentState returns the current (new) state after the change.
func (cd *ChangeDelta) CurrentState() *State {
return cd.new
}
// NewChangeDelta builds a ChangeDelta and eagerly computes the cached fields.
// jumpDuration, if non-zero, indicates a wall-clock time jump was detected
// (the machine likely woke from sleep) and is the approximate duration of the jump.
// forceViability, if true, forces DefaultInterfaceMaybeViable to be true regardless of the
// actual state of the default interface. This is useful in testing.
func NewChangeDelta(old, new *State, timeJumped bool, forceViability bool) (*ChangeDelta, error) {
func NewChangeDelta(old, new *State, jumpDuration time.Duration, forceViability bool) (*ChangeDelta, error) {
cd := ChangeDelta{
old: old,
new: new,
TimeJumped: timeJumped,
old: old,
new: new,
JumpDuration: jumpDuration,
}
if cd.new == nil {
@@ -165,10 +186,18 @@ func NewChangeDelta(old, new *State, timeJumped bool, forceViability bool) (*Cha
cd.DefaultInterfaceMaybeViable = true
}
// Compute rebind requirement. The default interface needs to be viable and
// Compute rebind requirement. The default interface needs to be viable and
// one of the other conditions needs to be true.
//
// Short time jumps (e.g., macOS DarkWake maintenance cycles of ~55s) are
// excluded — if the network state is unchanged after a brief sleep, there's
// no reason to rebind. However, a major time jump (over majorTimeJumpThreshold)
// warrants a rebind even if the local state looks the same, because NAT
// mappings are likely stale and DHCP leases may have changed (the renewal
// happens after wake, so local state may not yet reflect it).
majorTimeJump := cd.JumpDuration >= majorTimeJumpThreshold
cd.RebindLikelyRequired = (cd.old == nil ||
cd.TimeJumped ||
majorTimeJump ||
cd.DefaultInterfaceChanged ||
cd.InterfaceIPsChanged ||
cd.IsLessExpensive ||
@@ -181,7 +210,39 @@ func NewChangeDelta(old, new *State, timeJumped bool, forceViability bool) (*Cha
// StateDesc returns a description of the old and new states for logging.
func (cd *ChangeDelta) StateDesc() string {
return fmt.Sprintf("old: %v new: %v", cd.old, cd.new)
var sb strings.Builder
fmt.Fprintf(&sb, "old: %v new: %v", cd.old, cd.new)
if cd.old != nil && cd.new != nil {
if diff := cd.old.InterfaceDiff(cd.new); diff != "" {
fmt.Fprintf(&sb, " diff: %s", diff)
}
}
if cd.RebindLikelyRequired {
var reasons []string
if cd.old == nil {
reasons = append(reasons, "initial-state")
}
if cd.TimeJumped() {
reasons = append(reasons, fmt.Sprintf("time-jumped(%v)", cd.JumpDuration.Round(time.Second)))
}
if cd.DefaultInterfaceChanged {
reasons = append(reasons, "default-if-changed")
}
if cd.InterfaceIPsChanged {
reasons = append(reasons, "ips-changed")
}
if cd.IsLessExpensive {
reasons = append(reasons, "less-expensive")
}
if cd.HasPACOrProxyConfigChanged {
reasons = append(reasons, "pac-proxy-changed")
}
if cd.AvailableProtocolsChanged {
reasons = append(reasons, "protocols-changed")
}
fmt.Fprintf(&sb, " rebind-reason=[%s]", strings.Join(reasons, ","))
}
return sb.String()
}
// InterfaceIPDisappeared reports whether the given IP address exists on any interface
@@ -193,7 +254,7 @@ func (cd *ChangeDelta) InterfaceIPDisappeared(ip netip.Addr) bool {
if cd.new == nil && cd.old.HasIP(ip) {
return true
}
return cd.new.HasIP(ip) && !cd.old.HasIP(ip)
return cd.old.HasIP(ip) && !cd.new.HasIP(ip)
}
// AnyInterfaceUp reports whether any interfaces are up in the new state.
@@ -201,17 +262,12 @@ func (cd *ChangeDelta) AnyInterfaceUp() bool {
if cd.new == nil {
return false
}
for _, ifi := range cd.new.Interface {
if ifi.IsUp() {
return true
}
}
return false
return cd.new.AnyInterfaceUp()
}
// isInterestingInterfaceChange reports whether any interfaces have changed in a meaningful way.
// This excludes interfaces that are not interesting per IsInterestingInterface and
// filters out changes to interface IPs that that are uninteresting (e.g. link-local addresses).
// filters out changes to interface IPs that are uninteresting (e.g. link-local addresses).
func (cd *ChangeDelta) isInterestingInterfaceChange() bool {
// If there is no old state, everything is considered changed.
if cd.old == nil {
@@ -250,7 +306,12 @@ func (cd *ChangeDelta) isInterestingInterfaceChange() bool {
}
newIps = filterRoutableIPs(newIps)
if !oldInterface.Equal(newInterface) || !prefixesEqual(oldIps, newIps) {
// Only consider routable IP changes and up/down state transitions
// as interesting. Transient metadata changes (Flags like FlagRunning,
// MTU, etc.) should not trigger a major link change, as they cause
// false "major" events on macOS and Windows when the OS notifies us
// of interface changes that don't affect connectivity.
if oldInterface.IsUp() != newInterface.IsUp() || !prefixesEqual(oldIps, newIps) {
return true
}
}
@@ -282,8 +343,8 @@ func (cd *ChangeDelta) isInterestingInterfaceChange() bool {
}
oldIps = filterRoutableIPs(oldIps)
// The interface's IPs, Name, MTU, etc have changed. This is definitely interesting.
if !newInterface.Equal(oldInterface) || !prefixesEqual(oldIps, newIps) {
// Only consider routable IP changes and up/down state transitions.
if newInterface.IsUp() != oldInterface.IsUp() || !prefixesEqual(oldIps, newIps) {
return true
}
}
@@ -319,12 +380,10 @@ func New(bus *eventbus.Bus, logf logger.Logf) (*Monitor, error) {
lastWall: wallTime(),
}
m.changed = eventbus.Publish[ChangeDelta](m.b)
st, err := m.interfaceStateUncached()
if err != nil {
return nil, err
}
m.ifState = st
// Subscribe to OS change notifications before taking the initial
// snapshot, so a change between the two is queued rather than lost.
var err error
m.om, err = newOSMon(bus, logf, m)
if err != nil {
return nil, err
@@ -333,6 +392,13 @@ func New(bus *eventbus.Bus, logf logger.Logf) (*Monitor, error) {
return nil, errors.New("newOSMon returned nil, nil")
}
st, err := m.interfaceStateUncached()
if err != nil {
m.om.Close()
return nil, err
}
m.ifState = st
return m, nil
}
@@ -357,6 +423,12 @@ func (m *Monitor) InterfaceState() *State {
return m.ifState
}
// ProbeLocks acquires and releases the monitor's internal mutex.
func (m *Monitor) ProbeLocks() {
m.mu.Lock()
m.mu.Unlock()
}
func (m *Monitor) interfaceStateUncached() (*State, error) {
return getState(tsIfProps.tsIfName())
}
@@ -579,7 +651,8 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
return
}
delta, err := NewChangeDelta(oldState, newState, timeJumped, false)
jumpDuration := m.jumpDuration
delta, err := NewChangeDelta(oldState, newState, jumpDuration, false)
if err != nil {
m.logf("[unexpected] error creating ChangeDelta: %v", err)
return
@@ -592,12 +665,13 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
// See if we have a queued or new time jump signal.
if timeJumped {
m.resetTimeJumpedLocked()
m.logf("time jump detected (slept %v), probably wake from sleep", jumpDuration.Round(time.Second))
}
metricChange.Add(1)
if delta.RebindLikelyRequired {
metricChangeMajor.Add(1)
}
if delta.TimeJumped {
if delta.TimeJumped() {
metricChangeTimeJump.Add(1)
}
m.changed.Publish(*delta)
@@ -659,14 +733,14 @@ func (m *Monitor) checkWallTimeAdvanceLocked() bool {
panic("unreachable") // if callers are correct
}
now := wallTime()
if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 {
m.timeJumped = true // it is reset by debounce.
if elapsed := now.Sub(m.lastWall); elapsed > pollWallTimeInterval*3/2 {
m.jumpDuration = elapsed
}
m.lastWall = now
return m.timeJumped
return m.jumpDuration != 0
}
// resetTimeJumpedLocked consumes the signal set by checkWallTimeAdvanceLocked.
func (m *Monitor) resetTimeJumpedLocked() {
m.timeJumped = false
m.jumpDuration = 0
}
+12 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
@@ -150,6 +150,17 @@ func (m *darwinRouteMon) skipInterfaceAddrMessage(msg *route.InterfaceAddrMessag
}
func (m *darwinRouteMon) skipRouteMessage(msg *route.RouteMessage) bool {
// RTM_MISS fires on every failed route lookup (no matching entry in the
// routing table). It scales with traffic volume, not network-state
// changes, and is never the leading signal for a topology change: route
// withdrawals emit RTM_DELETE synchronously before any subsequent lookup
// can miss. Letting these through causes netmon to report spurious
// link changes, which trigger a re-STUN/netcheck loop when a probe
// destination is unreachable (e.g. IPv6 DERP probes on a network with
// no IPv6 default route), as each failed probe emits another RTM_MISS.
if msg.Type == unix.RTM_MISS {
return true
}
if ip := ipOfAddr(addrType(msg.Addrs, unix.RTAX_DST)); ip.IsLinkLocalUnicast() {
// Skip those like:
// dst = fe80::b476:66ff:fe30:c8f6%15
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build (!linux && !freebsd && !windows && !darwin) || android
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !darwin
+108 -8
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
@@ -41,7 +41,12 @@ func isProblematicInterface(nif *net.Interface) bool {
// DoS each other by doing traffic amplification, both of them
// preferring/trying to use each other for transport. See:
// https://github.com/tailscale/tailscale/issues/1208
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
// TODO(https://github.com/tailscale/tailscale/issues/18824): maybe exclude
// "WireGuard tunnel 0" as well on Windows (NetBird), but the name seems too
// generic where there is not a platform standard (on Linux wt0 is at least
// explicitly different from the WireGuard conventional default of wg0).
if strings.HasPrefix(name, "zt") || name == "wt0" /* NetBird */ ||
(runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
return true
}
return false
@@ -413,7 +418,7 @@ func (s *State) HasIP(ip netip.Addr) bool {
}
for _, pv := range s.InterfaceIPs {
for _, p := range pv {
if p.Contains(ip) {
if p.Addr() == ip {
return true
}
}
@@ -438,6 +443,91 @@ func (a Interface) Equal(b Interface) bool {
return true
}
// InterfaceDiff returns a human-readable summary of the differences between s
// and s2 that would cause Equal to return false. It returns "" if the states
// are equal. This is useful for debugging false link change events where the
// State.String() output looks identical but Equal() returns false because it
// checks fields not shown in String() (like interface Flags, MTU, HardwareAddr).
func (s *State) InterfaceDiff(s2 *State) string {
if s == nil && s2 == nil {
return ""
}
if s == nil {
return "old=nil"
}
if s2 == nil {
return "new=nil"
}
var diffs []string
if s.HaveV6 != s2.HaveV6 {
diffs = append(diffs, fmt.Sprintf("HaveV6: %v->%v", s.HaveV6, s2.HaveV6))
}
if s.HaveV4 != s2.HaveV4 {
diffs = append(diffs, fmt.Sprintf("HaveV4: %v->%v", s.HaveV4, s2.HaveV4))
}
if s.IsExpensive != s2.IsExpensive {
diffs = append(diffs, fmt.Sprintf("IsExpensive: %v->%v", s.IsExpensive, s2.IsExpensive))
}
if s.DefaultRouteInterface != s2.DefaultRouteInterface {
diffs = append(diffs, fmt.Sprintf("DefaultRoute: %q->%q", s.DefaultRouteInterface, s2.DefaultRouteInterface))
}
if s.HTTPProxy != s2.HTTPProxy {
diffs = append(diffs, fmt.Sprintf("HTTPProxy: %q->%q", s.HTTPProxy, s2.HTTPProxy))
}
if s.PAC != s2.PAC {
diffs = append(diffs, fmt.Sprintf("PAC: %q->%q", s.PAC, s2.PAC))
}
if len(s.Interface) != len(s2.Interface) {
diffs = append(diffs, fmt.Sprintf("numInterfaces: %d->%d", len(s.Interface), len(s2.Interface)))
}
if len(s.InterfaceIPs) != len(s2.InterfaceIPs) {
diffs = append(diffs, fmt.Sprintf("numInterfaceIPs: %d->%d", len(s.InterfaceIPs), len(s2.InterfaceIPs)))
}
for iname, i := range s.Interface {
i2, ok := s2.Interface[iname]
if !ok {
diffs = append(diffs, fmt.Sprintf("if %s: removed", iname))
continue
}
if !i.Equal(i2) {
if i.Interface != nil && i2.Interface != nil {
if i.Flags != i2.Flags {
diffs = append(diffs, fmt.Sprintf("if %s flags: %v->%v", iname, i.Flags, i2.Flags))
}
if i.MTU != i2.MTU {
diffs = append(diffs, fmt.Sprintf("if %s MTU: %d->%d", iname, i.MTU, i2.MTU))
}
if i.Index != i2.Index {
diffs = append(diffs, fmt.Sprintf("if %s index: %d->%d", iname, i.Index, i2.Index))
}
if !bytes.Equal([]byte(i.HardwareAddr), []byte(i2.HardwareAddr)) {
diffs = append(diffs, fmt.Sprintf("if %s hwaddr: %v->%v", iname, i.HardwareAddr, i2.HardwareAddr))
}
}
if i.Desc != i2.Desc {
diffs = append(diffs, fmt.Sprintf("if %s desc: %q->%q", iname, i.Desc, i2.Desc))
}
}
}
for iname := range s2.Interface {
if _, ok := s.Interface[iname]; !ok {
diffs = append(diffs, fmt.Sprintf("if %s: added", iname))
}
}
for iname, vv := range s.InterfaceIPs {
vv2 := s2.InterfaceIPs[iname]
if !slices.Equal(vv, vv2) {
diffs = append(diffs, fmt.Sprintf("ips %s: %v->%v", iname, vv, vv2))
}
}
for iname := range s2.InterfaceIPs {
if _, ok := s.InterfaceIPs[iname]; !ok {
diffs = append(diffs, fmt.Sprintf("ips %s: added %v", iname, s2.InterfaceIPs[iname]))
}
}
return strings.Join(diffs, "; ")
}
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
@@ -736,6 +826,14 @@ func RegisterInterfaceGetter(getInterfaces func() ([]Interface, error)) {
altNetInterfaces = getInterfaces
}
// HookInterfacesFallback is a hook for the androidbin feature to
// provide a last-resort interface list when the OS denies
// net.Interfaces to the process, as Android does to app UIDs. It is
// consulted only when no getter was registered with
// RegisterInterfaceGetter and net.Interfaces failed; if it returns an
// error, the net.Interfaces error is returned instead.
var HookInterfacesFallback feature.Hook[func() ([]Interface, error)]
// InterfaceList is a list of interfaces on the machine.
type InterfaceList []Interface
@@ -755,6 +853,11 @@ func netInterfaces() ([]Interface, error) {
}
ifs, err := net.Interfaces()
if err != nil {
if f, ok := HookInterfacesFallback.GetOk(); ok {
if ret, err := f(); err == nil {
return ret, nil
}
}
return nil, err
}
ret := make([]Interface, len(ifs))
@@ -807,11 +910,8 @@ func (m *Monitor) HasCGNATInterface() (bool, error) {
if hasCGNATInterface || !i.IsUp() || isTailscaleInterface(i.Name, pfxs) {
return
}
for _, pfx := range pfxs {
if cgnatRange.Overlaps(pfx) {
hasCGNATInterface = true
break
}
if slices.ContainsFunc(pfxs, cgnatRange.Overlaps) {
hasCGNATInterface = true
}
})
if err != nil {