|
|
|
|
@ -7,10 +7,11 @@
|
|
|
|
|
package netmon
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/netip"
|
|
|
|
|
"runtime"
|
|
|
|
|
"slices"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
@ -45,12 +46,15 @@ type osMon interface {
|
|
|
|
|
// until the osMon is closed. After a Close, the returned
|
|
|
|
|
// error is ignored.
|
|
|
|
|
Receive() (message, error)
|
|
|
|
|
|
|
|
|
|
// IsInterestingInterface reports whether the provided interface should
|
|
|
|
|
// be considered for network change events.
|
|
|
|
|
IsInterestingInterface(iface string) bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsInterestingInterface is the function used to determine whether
|
|
|
|
|
// a given interface name is interesting enough to pay attention to
|
|
|
|
|
// for network change monitoring purposes.
|
|
|
|
|
//
|
|
|
|
|
// If nil, all interfaces are considered interesting.
|
|
|
|
|
var IsInterestingInterface func(Interface, []netip.Prefix) bool
|
|
|
|
|
|
|
|
|
|
// Monitor represents a monitoring instance.
|
|
|
|
|
type Monitor struct {
|
|
|
|
|
logf logger.Logf
|
|
|
|
|
@ -62,10 +66,6 @@ type Monitor struct {
|
|
|
|
|
stop chan struct{} // closed on Stop
|
|
|
|
|
static bool // static Monitor that doesn't actually monitor
|
|
|
|
|
|
|
|
|
|
// Things that must be set early, before use,
|
|
|
|
|
// and not change at runtime.
|
|
|
|
|
tsIfName string // tailscale interface name, if known/set ("tailscale0", "utun3", ...)
|
|
|
|
|
|
|
|
|
|
mu syncs.Mutex // guards all following fields
|
|
|
|
|
cbs set.HandleSet[ChangeFunc]
|
|
|
|
|
ifState *State
|
|
|
|
|
@ -78,6 +78,7 @@ type Monitor struct {
|
|
|
|
|
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
|
|
|
|
|
tsIfName string // tailscale interface name, if known/set ("tailscale0", "utun3", ...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ChangeFunc is a callback function registered with Monitor that's called when the
|
|
|
|
|
@ -85,6 +86,11 @@ type Monitor struct {
|
|
|
|
|
type ChangeFunc func(*ChangeDelta)
|
|
|
|
|
|
|
|
|
|
// ChangeDelta describes the difference between two network states.
|
|
|
|
|
//
|
|
|
|
|
// Use NewChangeDelta to construct a delta and compute the cached fields.
|
|
|
|
|
//
|
|
|
|
|
// TODO (barnstar): make new and old (and netmon.State) private once all consumers are updated
|
|
|
|
|
// to use the accessor methods.
|
|
|
|
|
type ChangeDelta struct {
|
|
|
|
|
// Old is the old interface state, if known.
|
|
|
|
|
// It's nil if the old state is unknown.
|
|
|
|
|
@ -96,21 +102,221 @@ type ChangeDelta struct {
|
|
|
|
|
// Do not mutate it.
|
|
|
|
|
New *State
|
|
|
|
|
|
|
|
|
|
// Major is our legacy boolean of whether the network changed in some major
|
|
|
|
|
// way.
|
|
|
|
|
//
|
|
|
|
|
// Deprecated: do not remove. As of 2023-08-23 we're in a renewed effort to
|
|
|
|
|
// remove it and ask specific qustions of ChangeDelta instead. Look at Old
|
|
|
|
|
// and New (or add methods to ChangeDelta) instead of using Major.
|
|
|
|
|
Major bool
|
|
|
|
|
|
|
|
|
|
// TimeJumped is whether there was a big jump in wall time since the last
|
|
|
|
|
// time we checked. This is a hint that a mobile sleeping device might have
|
|
|
|
|
// time we checked. This is a hint that a sleeping device might have
|
|
|
|
|
// come out of sleep.
|
|
|
|
|
TimeJumped bool
|
|
|
|
|
|
|
|
|
|
// TODO(bradfitz): add some lazy cached fields here as needed with methods
|
|
|
|
|
// on *ChangeDelta to let callers ask specific questions
|
|
|
|
|
// The tailscale interface name, e.g. "tailscale0", "utun3", etc. Not all
|
|
|
|
|
// platforms know this or set it. Copied from netmon.Monitor.tsIfName.
|
|
|
|
|
TailscaleIfaceName string
|
|
|
|
|
|
|
|
|
|
DefaultRouteInterface string
|
|
|
|
|
|
|
|
|
|
// Computed Fields
|
|
|
|
|
|
|
|
|
|
DefaultInterfaceChanged bool // whether default route interface changed
|
|
|
|
|
IsLessExpensive bool // whether new state's default interface is less expensive than old.
|
|
|
|
|
HasPACOrProxyConfigChanged bool // whether PAC/HTTP proxy config changed
|
|
|
|
|
InterfaceIPsChanged bool // whether any interface IPs changed in a meaningful way
|
|
|
|
|
AvailableProtocolsChanged bool // whether we have seen a change in available IPv4/IPv6
|
|
|
|
|
DefaultInterfaceMaybeViable bool // whether the default interface is potentially viable (has usable IPs, is up and is not the tunnel itself)
|
|
|
|
|
IsInitialState bool // whether this is the initial state (old == nil, new != nil)
|
|
|
|
|
|
|
|
|
|
// RebindLikelyRequired combines the various fields above to report whether this change likely requires us
|
|
|
|
|
// to rebind sockets. This is a very conservative estimate and covers a number ofcases where a rebind
|
|
|
|
|
// may not be strictly necessary. Consumers of the ChangeDelta should consider checking the individual fields
|
|
|
|
|
// above or the state of their sockets.
|
|
|
|
|
RebindLikelyRequired bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewChangeDelta builds a ChangeDelta and eagerly computes the cached fields.
|
|
|
|
|
// 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, tsIfName string, forceViability bool) ChangeDelta {
|
|
|
|
|
cd := ChangeDelta{
|
|
|
|
|
Old: old,
|
|
|
|
|
New: new,
|
|
|
|
|
TimeJumped: timeJumped,
|
|
|
|
|
TailscaleIfaceName: tsIfName,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cd.New == nil {
|
|
|
|
|
return cd
|
|
|
|
|
} else if cd.Old == nil {
|
|
|
|
|
cd.DefaultInterfaceChanged = cd.New.DefaultRouteInterface != ""
|
|
|
|
|
cd.IsLessExpensive = false
|
|
|
|
|
cd.HasPACOrProxyConfigChanged = true
|
|
|
|
|
cd.InterfaceIPsChanged = true
|
|
|
|
|
cd.IsInitialState = true
|
|
|
|
|
} else {
|
|
|
|
|
cd.AvailableProtocolsChanged = (cd.Old.HaveV4 != cd.New.HaveV4) || (cd.Old.HaveV6 != cd.New.HaveV6)
|
|
|
|
|
cd.DefaultInterfaceChanged = cd.Old.DefaultRouteInterface != cd.New.DefaultRouteInterface
|
|
|
|
|
cd.IsLessExpensive = cd.Old.IsExpensive && !cd.New.IsExpensive
|
|
|
|
|
cd.HasPACOrProxyConfigChanged = (cd.Old.PAC != cd.New.PAC) || (cd.Old.HTTPProxy != cd.New.HTTPProxy)
|
|
|
|
|
cd.InterfaceIPsChanged = cd.isInterestingIntefaceChange()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the default route interface is populated, but it's not up this event signifies that we're in
|
|
|
|
|
// the process of tearing it down. Rebinds are going to fail so it's flappy to try.
|
|
|
|
|
cd.DefaultRouteInterface = new.DefaultRouteInterface
|
|
|
|
|
defIf := new.Interface[cd.DefaultRouteInterface]
|
|
|
|
|
|
|
|
|
|
// The default interface is not viable if is down or is the Tailscale interface itself.
|
|
|
|
|
if !forceViability && (!defIf.IsUp() || cd.DefaultRouteInterface == tsIfName) {
|
|
|
|
|
cd.DefaultInterfaceMaybeViable = false
|
|
|
|
|
} else {
|
|
|
|
|
cd.DefaultInterfaceMaybeViable = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute rebind requirement. The default interface needs to be viable and
|
|
|
|
|
// one of the other conditions needs to be true.
|
|
|
|
|
cd.RebindLikelyRequired = (cd.Old == nil ||
|
|
|
|
|
cd.TimeJumped ||
|
|
|
|
|
cd.DefaultInterfaceChanged ||
|
|
|
|
|
cd.InterfaceIPsChanged ||
|
|
|
|
|
cd.IsLessExpensive ||
|
|
|
|
|
cd.HasPACOrProxyConfigChanged ||
|
|
|
|
|
cd.AvailableProtocolsChanged) &&
|
|
|
|
|
cd.DefaultInterfaceMaybeViable
|
|
|
|
|
|
|
|
|
|
return cd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InterfaceIPAppeared reports whether the given IP address exists on any interface
|
|
|
|
|
// in the old state, but not in the new state.
|
|
|
|
|
func (cd *ChangeDelta) InterfaceIPDisppeared(ip netip.Addr) bool {
|
|
|
|
|
if cd.Old == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if cd.New == nil && cd.Old.HasIP(ip) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return cd.New.HasIP(ip) && !cd.Old.HasIP(ip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InterfaceIPDisappeared reports whether the given IP address existed on any interface
|
|
|
|
|
// in the old state, but not in the new state.
|
|
|
|
|
func (cd *ChangeDelta) InterfaceIPDisappeared(ip netip.Addr) bool {
|
|
|
|
|
return !cd.New.HasIP(ip) && cd.Old.HasIP(ip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AnyInterfaceUp reports whether any interfaces are up in the new state.
|
|
|
|
|
func (cd *ChangeDelta) AnyInterfaceUp() bool {
|
|
|
|
|
if cd.New == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, ifi := range cd.New.Interface {
|
|
|
|
|
if ifi.IsUp() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isInterestingIntefaceChange reports whether any interfaces have changed in a meaninful 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).
|
|
|
|
|
func (cd *ChangeDelta) isInterestingIntefaceChange() bool {
|
|
|
|
|
if cd.Old == nil && cd.New == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If either side is nil treat as changed.
|
|
|
|
|
if cd.Old == nil || cd.New == nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare interfaces in both directions. Old to new and new to old.
|
|
|
|
|
|
|
|
|
|
for iname, oldInterface := range cd.Old.Interface {
|
|
|
|
|
if iname == cd.TailscaleIfaceName {
|
|
|
|
|
// Ignore changes in the Tailscale interface itself.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
oldIps := filterRoutableIPs(cd.Old.InterfaceIPs[iname])
|
|
|
|
|
if IsInterestingInterface != nil && !IsInterestingInterface(oldInterface, oldIps) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Old interfaces with no routable addresses are not interesting
|
|
|
|
|
if len(oldIps) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The old interface doesn't exist in the new interface set and it has
|
|
|
|
|
// a global unicast IP. That's considered a change from the perspective
|
|
|
|
|
// of anything that may have been bound to it. If it didn't have a global
|
|
|
|
|
// unicast IP, it's not interesting.
|
|
|
|
|
newInterface, ok := cd.New.Interface[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
newIps, ok := cd.New.InterfaceIPs[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
newIps = filterRoutableIPs(newIps)
|
|
|
|
|
|
|
|
|
|
if !oldInterface.Equal(newInterface) || !prefixesEqual(oldIps, newIps) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for iname, newInterface := range cd.New.Interface {
|
|
|
|
|
if iname == cd.TailscaleIfaceName {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
newIps := filterRoutableIPs(cd.New.InterfaceIPs[iname])
|
|
|
|
|
if IsInterestingInterface != nil && !IsInterestingInterface(newInterface, newIps) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New interfaces with no routable addresses are not interesting
|
|
|
|
|
if len(newIps) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldInterface, ok := cd.Old.Interface[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldIps, ok := cd.Old.InterfaceIPs[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
// Redundant but we can't dig up the "old" IPs for this interface.
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
oldIps = filterRoutableIPs(oldIps)
|
|
|
|
|
|
|
|
|
|
// The interface's IPs, Name, MTU, etc have changed. This is definitely interesting.
|
|
|
|
|
if !newInterface.Equal(oldInterface) || !prefixesEqual(oldIps, newIps) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func filterRoutableIPs(addrs []netip.Prefix) []netip.Prefix {
|
|
|
|
|
var filtered []netip.Prefix
|
|
|
|
|
for _, pfx := range addrs {
|
|
|
|
|
a := pfx.Addr()
|
|
|
|
|
// Skip link-local multicast addresses.
|
|
|
|
|
if a.IsLinkLocalMulticast() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isUsableV4(a) || isUsableV6(a) {
|
|
|
|
|
filtered = append(filtered, pfx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("Filtered: %v\n", filtered)
|
|
|
|
|
return filtered
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New instantiates and starts a monitoring instance.
|
|
|
|
|
@ -174,9 +380,17 @@ func (m *Monitor) interfaceStateUncached() (*State, error) {
|
|
|
|
|
// This must be called only early in tailscaled startup before the monitor is
|
|
|
|
|
// used.
|
|
|
|
|
func (m *Monitor) SetTailscaleInterfaceName(ifName string) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
m.tsIfName = ifName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Monitor) TailscaleInterfaceName() string {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
return m.tsIfName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GatewayAndSelfIP returns the current network's default gateway, and
|
|
|
|
|
// the machine's default IP for that gateway.
|
|
|
|
|
//
|
|
|
|
|
@ -344,18 +558,7 @@ func (m *Monitor) pump() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isInterestingInterface reports whether the provided interface should be
|
|
|
|
|
// considered when checking for network state changes.
|
|
|
|
|
// The ips parameter should be the IPs of the provided interface.
|
|
|
|
|
func (m *Monitor) isInterestingInterface(i Interface, ips []netip.Prefix) bool {
|
|
|
|
|
if !m.om.IsInterestingInterface(i.Name) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// debounce calls the callback function with a delay between events
|
|
|
|
|
// / debounce calls the callback function with a delay between events
|
|
|
|
|
// and exits when a stop is issued.
|
|
|
|
|
func (m *Monitor) debounce() {
|
|
|
|
|
defer m.goroutines.Done()
|
|
|
|
|
@ -376,7 +579,10 @@ func (m *Monitor) debounce() {
|
|
|
|
|
select {
|
|
|
|
|
case <-m.stop:
|
|
|
|
|
return
|
|
|
|
|
case <-time.After(250 * time.Millisecond):
|
|
|
|
|
// 1s is reasonable debounce time for network changes. Events such as undocking a laptop
|
|
|
|
|
// or roaming onto wifi will often generate multiple events in quick succession as interfaces
|
|
|
|
|
// flap. We want to avoid spamming consumers of these events.
|
|
|
|
|
case <-time.After(1000 * time.Millisecond):
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -403,34 +609,18 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delta := ChangeDelta{
|
|
|
|
|
Old: oldState,
|
|
|
|
|
New: newState,
|
|
|
|
|
TimeJumped: timeJumped,
|
|
|
|
|
}
|
|
|
|
|
delta := NewChangeDelta(oldState, newState, timeJumped, m.tsIfName, false)
|
|
|
|
|
|
|
|
|
|
delta.Major = m.IsMajorChangeFrom(oldState, newState)
|
|
|
|
|
if delta.Major {
|
|
|
|
|
if delta.RebindLikelyRequired {
|
|
|
|
|
m.gwValid = false
|
|
|
|
|
|
|
|
|
|
if s1, s2 := oldState.String(), delta.New.String(); s1 == s2 {
|
|
|
|
|
m.logf("[unexpected] network state changed, but stringification didn't: %v", s1)
|
|
|
|
|
m.logf("[unexpected] old: %s", jsonSummary(oldState))
|
|
|
|
|
m.logf("[unexpected] new: %s", jsonSummary(newState))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m.ifState = newState
|
|
|
|
|
// See if we have a queued or new time jump signal.
|
|
|
|
|
if timeJumped {
|
|
|
|
|
m.resetTimeJumpedLocked()
|
|
|
|
|
if !delta.Major {
|
|
|
|
|
// Only log if it wasn't an interesting change.
|
|
|
|
|
m.logf("time jumped (probably wake from sleep); synthesizing major change event")
|
|
|
|
|
delta.Major = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
metricChange.Add(1)
|
|
|
|
|
if delta.Major {
|
|
|
|
|
if delta.RebindLikelyRequired {
|
|
|
|
|
metricChangeMajor.Add(1)
|
|
|
|
|
}
|
|
|
|
|
if delta.TimeJumped {
|
|
|
|
|
@ -442,107 +632,24 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsMajorChangeFrom reports whether the transition from s1 to s2 is
|
|
|
|
|
// a "major" change, where major roughly means it's worth tearing down
|
|
|
|
|
// a bunch of connections and rebinding.
|
|
|
|
|
//
|
|
|
|
|
// TODO(bradiftz): tigten this definition.
|
|
|
|
|
func (m *Monitor) IsMajorChangeFrom(s1, s2 *State) bool {
|
|
|
|
|
if s1 == nil && s2 == nil {
|
|
|
|
|
// reports whether a and b contain the same set of prefixes regardless of order.
|
|
|
|
|
func prefixesEqual(a, b []netip.Prefix) bool {
|
|
|
|
|
if len(a) != len(b) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if s1 == nil || s2 == nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if s1.HaveV6 != s2.HaveV6 ||
|
|
|
|
|
s1.HaveV4 != s2.HaveV4 ||
|
|
|
|
|
s1.IsExpensive != s2.IsExpensive ||
|
|
|
|
|
s1.DefaultRouteInterface != s2.DefaultRouteInterface ||
|
|
|
|
|
s1.HTTPProxy != s2.HTTPProxy ||
|
|
|
|
|
s1.PAC != s2.PAC {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
for iname, i := range s1.Interface {
|
|
|
|
|
if iname == m.tsIfName {
|
|
|
|
|
// Ignore changes in the Tailscale interface itself.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ips := s1.InterfaceIPs[iname]
|
|
|
|
|
if !m.isInterestingInterface(i, ips) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
i2, ok := s2.Interface[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ips2, ok := s2.InterfaceIPs[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if !i.Equal(i2) || !prefixesMajorEqual(ips, ips2) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Iterate over s2 in case there is a field in s2 that doesn't exist in s1
|
|
|
|
|
for iname, i := range s2.Interface {
|
|
|
|
|
if iname == m.tsIfName {
|
|
|
|
|
// Ignore changes in the Tailscale interface itself.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ips := s2.InterfaceIPs[iname]
|
|
|
|
|
if !m.isInterestingInterface(i, ips) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
i1, ok := s1.Interface[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ips1, ok := s1.InterfaceIPs[iname]
|
|
|
|
|
if !ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if !i.Equal(i1) || !prefixesMajorEqual(ips, ips1) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// prefixesMajorEqual reports whether a and b are equal after ignoring
|
|
|
|
|
// boring things like link-local, loopback, and multicast addresses.
|
|
|
|
|
func prefixesMajorEqual(a, b []netip.Prefix) bool {
|
|
|
|
|
// trim returns a subslice of p with link local unicast,
|
|
|
|
|
// loopback, and multicast prefixes removed from the front.
|
|
|
|
|
trim := func(p []netip.Prefix) []netip.Prefix {
|
|
|
|
|
for len(p) > 0 {
|
|
|
|
|
a := p[0].Addr()
|
|
|
|
|
if a.IsLinkLocalUnicast() || a.IsLoopback() || a.IsMulticast() {
|
|
|
|
|
p = p[1:]
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
a = trim(a)
|
|
|
|
|
b = trim(b)
|
|
|
|
|
if len(a) == 0 || len(b) == 0 {
|
|
|
|
|
return len(a) == 0 && len(b) == 0
|
|
|
|
|
}
|
|
|
|
|
if a[0] != b[0] {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
a, b = a[1:], b[1:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
aa := make([]netip.Prefix, len(a))
|
|
|
|
|
bb := make([]netip.Prefix, len(b))
|
|
|
|
|
copy(aa, a)
|
|
|
|
|
copy(bb, b)
|
|
|
|
|
|
|
|
|
|
func jsonSummary(x any) any {
|
|
|
|
|
j, err := json.Marshal(x)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
less := func(x, y netip.Prefix) int {
|
|
|
|
|
return x.Addr().Compare(y.Addr())
|
|
|
|
|
}
|
|
|
|
|
return j
|
|
|
|
|
|
|
|
|
|
slices.SortFunc(aa, less)
|
|
|
|
|
slices.SortFunc(bb, less)
|
|
|
|
|
return slices.Equal(aa, bb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func wallTime() time.Time {
|
|
|
|
|
|