Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+3
-4
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
+85
-110
@@ -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
@@ -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
|
||||
|
||||
|
||||
Generated
Vendored
+3
-3
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
+2
-2
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
+123
-49
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netmon
|
||||
|
||||
+1
-1
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user