Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+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
|
||||
}
|
||||
Reference in new issue
Block a user