ipn/ipnlocal, net/netmon: remove ChangeDelta.New and Old

updates tailscale/corp#33891

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
jonathan/netmon-changedelta
Jonathan Nobels 3 days ago
parent b57e778618
commit db7c05aa59

@ -143,7 +143,7 @@ func changeDeltaWatcher(ec *eventbus.Client, ctx context.Context, dump func(st *
return return
} }
log.Printf("Network monitor fired. New state:") log.Printf("Network monitor fired. New state:")
dump(delta.New) dump(delta.CurrentState())
} }
} }
} }

@ -560,10 +560,15 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
b.e.SetStatusCallback(b.setWgengineStatus) b.e.SetStatusCallback(b.setWgengineStatus)
b.interfaceState = netMon.InterfaceState() b.interfaceState = netMon.InterfaceState()
// Call our linkChange code once with the current state. // Call our linkChange code once with the current state.
// Following changes are triggered via the eventbus. // Following changes are triggered via the eventbus.
cd := netmon.NewChangeDelta(nil, b.interfaceState, false, netMon.TailscaleInterfaceName(), false) cd, err := netmon.NewChangeDelta(nil, b.interfaceState, false, netMon.TailscaleInterfaceName(), false)
b.linkChange(&cd) if err != nil {
b.logf("[unexpected] setting initial netmon state failed: %v", err)
} else {
b.linkChange(cd)
}
if buildfeatures.HasPeerAPIServer { if buildfeatures.HasPeerAPIServer {
if tunWrap, ok := b.sys.Tun.GetOK(); ok { if tunWrap, ok := b.sys.Tun.GetOK(); ok {
@ -962,7 +967,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
b.interfaceState = delta.New b.interfaceState = delta.CurrentState()
b.pauseOrResumeControlClientLocked() b.pauseOrResumeControlClientLocked()
prefs := b.pm.CurrentPrefs() prefs := b.pm.CurrentPrefs()
@ -996,7 +1001,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
// If the local network configuration has changed, our filter may // If the local network configuration has changed, our filter may
// need updating to tweak default routes. // need updating to tweak default routes.
b.updateFilterLocked(prefs) b.updateFilterLocked(prefs)
updateExitNodeUsageWarning(prefs, delta.New, b.health) updateExitNodeUsageWarning(prefs, delta.CurrentState(), b.health)
if buildfeatures.HasPeerAPIServer { if buildfeatures.HasPeerAPIServer {
cn := b.currentNode() cn := b.currentNode()

@ -6,38 +6,43 @@ package netmon
import ( import (
"context" "context"
"sync" "sync"
"time"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/eventbus" "tailscale.com/util/eventbus"
) )
const cooldownSeconds = 300
// LinkChangeLogLimiter returns a new [logger.Logf] that logs each unique // LinkChangeLogLimiter returns a new [logger.Logf] that logs each unique
// format string to the underlying logger only once per major LinkChange event. // format string to the underlying logger only once per major LinkChange event
// with a cooldownSeconds second cooldown.
// //
// The logger stops tracking seen format strings when the provided context is // The logger stops tracking seen format strings when the provided context is
// done. // done.
func LinkChangeLogLimiter(ctx context.Context, logf logger.Logf, nm *Monitor) logger.Logf { func LinkChangeLogLimiter(ctx context.Context, logf logger.Logf, nm *Monitor) logger.Logf {
var formatSeen sync.Map // map[string]bool var formatLastSeen sync.Map // map[string]int64
sub := eventbus.SubscribeFunc(nm.b, func(cd ChangeDelta) {
// If we're in a major change or a time jump, clear the seen map. sub := eventbus.SubscribeFunc(nm.b, func(cd *ChangeDelta) {
if cd.RebindLikelyRequired || cd.TimeJumped { // Any link changes that are flagged as likely require a rebind are
formatSeen.Clear() // interesting enough that we should log them.
if cd.RebindLikelyRequired {
formatLastSeen = sync.Map{}
} }
}) })
context.AfterFunc(ctx, sub.Close) context.AfterFunc(ctx, sub.Close)
return func(format string, args ...any) { return func(format string, args ...any) {
// We only store 'true' in the map, so if it's present then it // get the current timestamp
// means we've already logged this format string. now := time.Now().Unix()
_, loaded := formatSeen.LoadOrStore(format, true) lastSeen, ok := formatLastSeen.Load(format)
if loaded { if ok {
// TODO(andrew-d): we may still want to log this // if we've seen this format string within the last hour, skip logging
// message every N minutes (1x/hour?) even if it's been if now-lastSeen.(int64) < cooldownSeconds {
// seen, so that debugging doesn't require searching return
// back in the logs for an unbounded amount of time. }
//
// See: https://github.com/tailscale/tailscale/issues/13145
return
} }
// update the last seen timestamp for this format string
formatLastSeen.Store(format, now)
logf(format, args...) logf(format, args...)
} }

@ -64,7 +64,14 @@ func syncTestLinkChangeLogLimiter(t *testing.T) {
// InjectEvent doesn't work because it's not a major event, so we // InjectEvent doesn't work because it's not a major event, so we
// instead inject the event ourselves. // instead inject the event ourselves.
injector := eventbustest.NewInjector(t, bus) injector := eventbustest.NewInjector(t, bus)
eventbustest.Inject(injector, NewChangeDelta(nil, nil, true, "", false)) cd, err := NewChangeDelta(nil, &State{}, true, "tailscale0", true)
if err != nil {
t.Fatal(err)
}
if cd.RebindLikelyRequired != true {
t.Fatalf("expected RebindLikelyRequired to be true, got false")
}
eventbustest.Inject(injector, cd)
synctest.Wait() synctest.Wait()
logf("hello %s", "world") logf("hello %s", "world")

@ -9,6 +9,7 @@ package netmon
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"net/netip" "net/netip"
"runtime" "runtime"
"slices" "slices"
@ -88,19 +89,14 @@ type ChangeFunc func(*ChangeDelta)
// ChangeDelta describes the difference between two network states. // ChangeDelta describes the difference between two network states.
// //
// Use NewChangeDelta to construct a delta 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.
// Do not mutate it. old *State
Old *State
// New is the new network state. // New is the new network state.
// It is always non-nil. // It is always non-nil.
// Do not mutate it. new *State
New *State
// TimeJumped is whether there was a big jump in wall time since the last // 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 // time we checked. This is a hint that a sleeping device might have
@ -130,30 +126,36 @@ type ChangeDelta struct {
RebindLikelyRequired bool RebindLikelyRequired bool
} }
// 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. // NewChangeDelta builds a ChangeDelta and eagerly computes the cached fields.
// forceViability, if true, forces DefaultInterfaceMaybeViable to be true regardless of the // forceViability, if true, forces DefaultInterfaceMaybeViable to be true regardless of the
// actual state of the default interface. This is useful in testing. // actual state of the default interface. This is useful in testing.
func NewChangeDelta(old, new *State, timeJumped bool, tsIfName string, forceViability bool) ChangeDelta { func NewChangeDelta(old, new *State, timeJumped bool, tsIfName string, forceViability bool) (*ChangeDelta, error) {
cd := ChangeDelta{ cd := ChangeDelta{
Old: old, old: old,
New: new, new: new,
TimeJumped: timeJumped, TimeJumped: timeJumped,
TailscaleIfaceName: tsIfName, TailscaleIfaceName: tsIfName,
} }
if cd.New == nil { if cd.new == nil {
return cd log.Printf("[unexpected] NewChangeDelta called with nil new state")
} else if cd.Old == nil { return nil, errors.New("new state cannot be nil")
cd.DefaultInterfaceChanged = cd.New.DefaultRouteInterface != "" } else if cd.old == nil && cd.new != nil {
cd.DefaultInterfaceChanged = cd.new.DefaultRouteInterface != ""
cd.IsLessExpensive = false cd.IsLessExpensive = false
cd.HasPACOrProxyConfigChanged = true cd.HasPACOrProxyConfigChanged = true
cd.InterfaceIPsChanged = true cd.InterfaceIPsChanged = true
cd.IsInitialState = 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()
} }
@ -171,7 +173,7 @@ func NewChangeDelta(old, new *State, timeJumped bool, tsIfName string, forceViab
// 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. // one of the other conditions needs to be true.
cd.RebindLikelyRequired = (cd.Old == nil || cd.RebindLikelyRequired = (cd.old == nil ||
cd.TimeJumped || cd.TimeJumped ||
cd.DefaultInterfaceChanged || cd.DefaultInterfaceChanged ||
cd.InterfaceIPsChanged || cd.InterfaceIPsChanged ||
@ -180,38 +182,38 @@ func NewChangeDelta(old, new *State, timeJumped bool, tsIfName string, forceViab
cd.AvailableProtocolsChanged) && cd.AvailableProtocolsChanged) &&
cd.DefaultInterfaceMaybeViable cd.DefaultInterfaceMaybeViable
return cd return &cd, nil
} }
// StateDesc returns a description of the old and new states for logging // StateDesc returns a description of the old and new states for logging
func (cd *ChangeDelta) StateDesc() string { func (cd *ChangeDelta) StateDesc() string {
return fmt.Sprintf("old: %v new: %v", cd.Old, cd.New) return fmt.Sprintf("old: %v new: %v", cd.old, cd.new)
} }
// InterfaceIPAppeared reports whether the given IP address exists on any interface // InterfaceIPAppeared reports whether the given IP address exists on any interface
// in the old state, but not in the new state. // in the old state, but not in the new state.
func (cd *ChangeDelta) InterfaceIPDisppeared(ip netip.Addr) bool { func (cd *ChangeDelta) InterfaceIPDisppeared(ip netip.Addr) bool {
if cd.Old == nil { if cd.old == nil {
return false return false
} }
if cd.New == nil && cd.Old.HasIP(ip) { if cd.new == nil && cd.old.HasIP(ip) {
return true return true
} }
return cd.New.HasIP(ip) && !cd.Old.HasIP(ip) return cd.new.HasIP(ip) && !cd.old.HasIP(ip)
} }
// InterfaceIPDisappeared reports whether the given IP address existed on any interface // InterfaceIPDisappeared reports whether the given IP address existed on any interface
// in the old state, but not in the new state. // in the old state, but not in the new state.
func (cd *ChangeDelta) InterfaceIPDisappeared(ip netip.Addr) bool { func (cd *ChangeDelta) InterfaceIPDisappeared(ip netip.Addr) bool {
return !cd.New.HasIP(ip) && cd.Old.HasIP(ip) return !cd.new.HasIP(ip) && cd.old.HasIP(ip)
} }
// AnyInterfaceUp reports whether any interfaces are up in the new state. // AnyInterfaceUp reports whether any interfaces are up in the new state.
func (cd *ChangeDelta) AnyInterfaceUp() bool { func (cd *ChangeDelta) AnyInterfaceUp() bool {
if cd.New == nil { if cd.new == nil {
return false return false
} }
for _, ifi := range cd.New.Interface { for _, ifi := range cd.new.Interface {
if ifi.IsUp() { if ifi.IsUp() {
return true return true
} }
@ -223,23 +225,23 @@ func (cd *ChangeDelta) AnyInterfaceUp() bool {
// 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 { if cd.old == nil && cd.new == nil {
return false 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
} }
// Compare interfaces in both directions. Old to new and new to old. // Compare interfaces in both directions. Old to new and new to old.
for iname, oldInterface := range cd.Old.Interface { for iname, oldInterface := range cd.old.Interface {
if iname == cd.TailscaleIfaceName { if iname == cd.TailscaleIfaceName {
// Ignore changes in the Tailscale interface itself. // Ignore changes in the Tailscale interface itself.
continue continue
} }
oldIps := filterRoutableIPs(cd.Old.InterfaceIPs[iname]) oldIps := filterRoutableIPs(cd.old.InterfaceIPs[iname])
if IsInterestingInterface != nil && !IsInterestingInterface(oldInterface, oldIps) { if IsInterestingInterface != nil && !IsInterestingInterface(oldInterface, oldIps) {
continue continue
} }
@ -253,11 +255,11 @@ func (cd *ChangeDelta) isInterestingIntefaceChange() bool {
// 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]
if !ok { if !ok {
return true return true
} }
newIps, ok := cd.New.InterfaceIPs[iname] newIps, ok := cd.new.InterfaceIPs[iname]
if !ok { if !ok {
return true return true
} }
@ -268,11 +270,11 @@ func (cd *ChangeDelta) isInterestingIntefaceChange() bool {
} }
} }
for iname, newInterface := range cd.New.Interface { for iname, newInterface := range cd.new.Interface {
if iname == cd.TailscaleIfaceName { if iname == cd.TailscaleIfaceName {
continue continue
} }
newIps := filterRoutableIPs(cd.New.InterfaceIPs[iname]) newIps := filterRoutableIPs(cd.new.InterfaceIPs[iname])
if IsInterestingInterface != nil && !IsInterestingInterface(newInterface, newIps) { if IsInterestingInterface != nil && !IsInterestingInterface(newInterface, newIps) {
continue continue
} }
@ -282,12 +284,12 @@ func (cd *ChangeDelta) isInterestingIntefaceChange() bool {
continue continue
} }
oldInterface, ok := cd.Old.Interface[iname] oldInterface, ok := cd.old.Interface[iname]
if !ok { if !ok {
return true return true
} }
oldIps, ok := cd.Old.InterfaceIPs[iname] oldIps, ok := cd.old.InterfaceIPs[iname]
if !ok { if !ok {
// Redundant but we can't dig up the "old" IPs for this interface. // Redundant but we can't dig up the "old" IPs for this interface.
return true return true
@ -315,7 +317,6 @@ func filterRoutableIPs(addrs []netip.Prefix) []netip.Prefix {
filtered = append(filtered, pfx) filtered = append(filtered, pfx)
} }
} }
fmt.Printf("Filtered: %v\n", filtered)
return filtered return filtered
} }
@ -609,7 +610,11 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
return return
} }
delta := NewChangeDelta(oldState, newState, timeJumped, m.tsIfName, false) delta, err := NewChangeDelta(oldState, newState, timeJumped, m.tsIfName, false)
if err != nil {
m.logf("[unexpected] error creating ChangeDelta: %v", err)
return
}
if delta.RebindLikelyRequired { if delta.RebindLikelyRequired {
m.gwValid = false m.gwValid = false
@ -626,9 +631,9 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
if delta.TimeJumped { if delta.TimeJumped {
metricChangeTimeJump.Add(1) metricChangeTimeJump.Add(1)
} }
m.changed.Publish(delta) m.changed.Publish(*delta)
for _, cb := range m.cbs { for _, cb := range m.cbs {
go cb(&delta) go cb(delta)
} }
} }

@ -139,7 +139,7 @@ func TestMonitorMode(t *testing.T) {
n := 0 n := 0
mon.RegisterChangeCallback(func(d *ChangeDelta) { mon.RegisterChangeCallback(func(d *ChangeDelta) {
n++ n++
t.Logf("cb: changed=%v, ifSt=%v", d.RebindLikelyRequired, d.New) t.Logf("cb: changed=%v, ifSt=%v", d.RebindLikelyRequired, d.CurrentState())
}) })
mon.Start() mon.Start()
<-done <-done
@ -150,7 +150,7 @@ func TestMonitorMode(t *testing.T) {
mon.Start() mon.Start()
eventbustest.Expect(tw, func(event *ChangeDelta) (bool, error) { eventbustest.Expect(tw, func(event *ChangeDelta) (bool, error) {
n++ n++
t.Logf("cb: changed=%v, ifSt=%v", event.RebindLikelyRequired, event.New) t.Logf("cb: changed=%v, ifSt=%v", event.RebindLikelyRequired, event.CurrentState())
return false, nil // Return false, indicating we wanna look for more events return false, nil // Return false, indicating we wanna look for more events
}) })
t.Logf("%v events", n) t.Logf("%v events", n)
@ -159,16 +159,13 @@ func TestMonitorMode(t *testing.T) {
// tests (*ChangeDelta).RebindRequired // tests (*ChangeDelta).RebindRequired
func TestRebindRequired(t *testing.T) { func TestRebindRequired(t *testing.T) {
// s1 cannot be nil by definition
tests := []struct { tests := []struct {
name string name string
s1, s2 *State s1, s2 *State
tsIfName string tsIfName string
want bool want bool
}{ }{
{
name: "eq_nil",
want: false,
},
{ {
name: "nil_mix", name: "nil_mix",
s2: new(State), s2: new(State),
@ -498,7 +495,10 @@ func TestRebindRequired(t *testing.T) {
} }
} }
cd := NewChangeDelta(tt.s1, tt.s2, false, tt.tsIfName, true) cd, err := NewChangeDelta(tt.s1, tt.s2, false, tt.tsIfName, true)
if err != nil {
t.Fatalf("NewChangeDelta error: %v", err)
}
_ = cd // in case we need it later _ = cd // in case we need it later
if got := cd.RebindLikelyRequired; got != tt.want { if got := cd.RebindLikelyRequired; got != tt.want {
t.Errorf("RebindRequired = %v; want %v", got, tt.want) t.Errorf("RebindRequired = %v; want %v", got, tt.want)

@ -274,7 +274,7 @@ func setNetMon(netMon *netmon.Monitor) {
if !delta.RebindLikelyRequired { if !delta.RebindLikelyRequired {
return return
} }
state := delta.New state := delta.CurrentState()
ifName := state.DefaultRouteInterface ifName := state.DefaultRouteInterface
if ifName == "" { if ifName == "" {
return return

Loading…
Cancel
Save