@ -87,7 +87,10 @@ type ChangeFunc func(*ChangeDelta)
// ChangeDelta describes the difference between two network states.
// ChangeDelta describes the difference between two network states.
//
//
// Use NewChangeDelta to construct one and compute the cached fields.
// 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 {
type ChangeDelta struct {
// Old is the old interface state, if known.
// Old is the old interface state, if known.
// It's nil if the old state is unknown.
// It's nil if the old state is unknown.
@ -108,26 +111,29 @@ type ChangeDelta struct {
// platforms know this or set it. Copied from netmon.Monitor.tsIfName.
// platforms know this or set it. Copied from netmon.Monitor.tsIfName.
TailscaleIfaceName string
TailscaleIfaceName string
DefaultRouteInterface string
// Computed Fields
// Computed Fields
DefaultInterfaceChanged bool // whether default route interface changed
DefaultInterfaceChanged bool // whether default route interface changed
IsLessExpensive bool // whether new state is less expensive than old
IsLessExpensive bool // whether new state 's default interface is less expensive than old.
HasPACOrProxyConfigChanged bool // whether PAC/HTTP proxy config changed
HasPACOrProxyConfigChanged bool // whether PAC/HTTP proxy config changed
InterfaceIPsChanged bool // whether any interface IPs changed in a meaningful way
InterfaceIPsChanged bool // whether any interface IPs changed in a meaningful way
AvailableProtocolsChanged bool // whether we have seen a change in available IPv4/IPv6
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)
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
// 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 of
// to rebind sockets. This is a very conservative estimate and covers a number of cases where a rebind
// cases where a rebind is not strictly necessary. Consumers of the ChangeDelta should
// may not be strictly necessary. Consumers of the ChangeDelta should consider checking the individual fields
// use this as a hint only. If in doubt, rebind .
// above or the state of their sockets .
RebindLikelyRequired bool
RebindLikelyRequired bool
}
}
var skipRebindIfNoDefaultRouteChange = true
// NewChangeDelta builds a ChangeDelta and eagerly computes the cached fields.
// NewChangeDelta builds a ChangeDelta and eagerly computes the cached fields.
func NewChangeDelta ( old , new * State , timeJumped bool , tsIfName string ) ChangeDelta {
// 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 {
cd := ChangeDelta {
Old : old ,
Old : old ,
New : new ,
New : new ,
@ -142,29 +148,30 @@ func NewChangeDelta(old, new *State, timeJumped bool, tsIfName string) ChangeDel
cd . IsLessExpensive = false
cd . IsLessExpensive = false
cd . HasPACOrProxyConfigChanged = true
cd . HasPACOrProxyConfigChanged = true
cd . InterfaceIPsChanged = true
cd . InterfaceIPsChanged = true
cd . IsInitialState = true
} else {
} else {
cd . AvailableProtocolsChanged = cd . Old . HaveV4 != cd . New . HaveV4 || cd . Old . HaveV6 != cd . New . HaveV6
cd . AvailableProtocolsChanged = ( cd . Old . HaveV4 != cd . New . HaveV4 ) || ( cd . Old . HaveV6 != cd . New . HaveV6 )
cd . DefaultInterfaceChanged = cd . Old . DefaultRouteInterface != cd . New . DefaultRouteInterface
cd . DefaultInterfaceChanged = cd . Old . DefaultRouteInterface != cd . New . DefaultRouteInterface
cd . IsLessExpensive = cd . Old . IsExpensive && ! cd . New . IsExpensive
cd . IsLessExpensive = cd . Old . IsExpensive && ! cd . New . IsExpensive
cd . HasPACOrProxyConfigChanged = cd . Old . PAC != cd . New . PAC || cd . Old . HTTPProxy != cd . New . HTTPProxy
cd . HasPACOrProxyConfigChanged = ( cd . Old . PAC != cd . New . PAC ) || ( cd . Old . HTTPProxy != cd . New . HTTPProxy )
cd . InterfaceIPsChanged = cd . isInterestingIntefaceChange ( )
cd . InterfaceIPsChanged = cd . isInterestingIntefaceChange ( )
}
}
// If the default route interface is populated, but it's not up this event signifies that we're in
// 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.
// the process of tearing it down. Rebinds are going to fail so it's flappy to try.
defIfName : = new . DefaultRouteInterface
cd. DefaultRouteInterface = new . DefaultRouteInterface
defIf := new . Interface [ defIfNam e]
defIf := new . Interface [ cd. DefaultRouteInterfac e]
cd . DefaultInterfaceMaybeViable = true
// The default interface is not viable if is down or is the Tailscale interface itself.
// The default interface is not viable if is down or is the Tailscale interface itself.
if ! defIf. IsUp ( ) || defIfName == tsIfName {
if ! forceViability && ( ! defIf. IsUp ( ) || cd. DefaultRouteInterface == tsIfName ) {
cd . DefaultInterfaceMaybeViable = false
cd . DefaultInterfaceMaybeViable = false
} else {
cd . DefaultInterfaceMaybeViable = true
}
}
// Compute rebind requirement. A number of these checks are redundant - HaveSomeAddressChanged
// Compute rebind requirement. The default interface needs to be viable and
// subsumes InterfaceIPsChanged, IsExpensive likely does not change without a new interface
// one of the other conditions needs to be true.
// appearing, but we'll keep them all for clarity and testability.
cd . RebindLikelyRequired = ( cd . Old == nil ||
cd . RebindLikelyRequired = ( cd . New == nil || // Do we need to rebind if there is no current state?
cd . Old == nil ||
cd . TimeJumped ||
cd . TimeJumped ||
cd . DefaultInterfaceChanged ||
cd . DefaultInterfaceChanged ||
cd . InterfaceIPsChanged ||
cd . InterfaceIPsChanged ||
@ -173,18 +180,53 @@ func NewChangeDelta(old, new *State, timeJumped bool, tsIfName string) ChangeDel
cd . AvailableProtocolsChanged ) &&
cd . AvailableProtocolsChanged ) &&
cd . DefaultInterfaceMaybeViable
cd . DefaultInterfaceMaybeViable
// (barnstar) TODO: There are likely a number of optimizations we can do here to avoid
// rebinding in cases where it is not necessary but we really need to leave that to the
// upstream component. If it's sockets are happy, then it probably doesn't need to rebind,
// but it may want to if any of these fields are true.
return cd
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.
// isInterestingIntefaceChange reports whether any interfaces have changed in a meaninful way.
// This excludes interfaces that are not interesting per IsInterestingInterface and
// 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 that are uninteresting (e.g. link-local addresses).
func ( cd * ChangeDelta ) isInterestingIntefaceChange ( ) bool {
func ( cd * ChangeDelta ) isInterestingIntefaceChange ( ) bool {
if cd . Old == nil && cd . New == nil {
return false
}
// If either side is nil treat as changed.
// If either side is nil treat as changed.
if cd . Old == nil || cd . New == nil {
if cd . Old == nil || cd . New == nil {
return true
return true
@ -208,7 +250,7 @@ func (cd *ChangeDelta) isInterestingIntefaceChange() bool {
}
}
// The old interface doesn't exist in the new interface set and it has
// The old interface doesn't exist in the new interface set and it has
// a n a global unicast IP. That's considered a change from the perspective
// 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
// of anything that may have been bound to it. If it didn't have a global
// unicast IP, it's not interesting.
// unicast IP, it's not interesting.
newInterface , ok := cd . New . Interface [ iname ]
newInterface , ok := cd . New . Interface [ iname ]
@ -567,7 +609,7 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
return
return
}
}
delta := NewChangeDelta ( oldState , newState , timeJumped , m . tsIfName )
delta := NewChangeDelta ( oldState , newState , timeJumped , m . tsIfName , false )
if delta . RebindLikelyRequired {
if delta . RebindLikelyRequired {
m . gwValid = false
m . gwValid = false