Files
ctrld/cmd/cli/network_transition_review_test.go
T

131 lines
5.3 KiB
Go

package cli
import (
"context"
"net"
"sync/atomic"
"testing"
"time"
"tailscale.com/net/netmon"
"github.com/Control-D-Inc/ctrld"
)
func TestNetmonMinorAndWakeSnapshotReconciliation(t *testing.T) {
for _, timeJump := range []bool{false, true} {
t.Run(map[bool]string{false: "minor", true: "time_jump"}[timeJump], func(t *testing.T) {
sourceTestGlobals(t)
mon := netmon.NewStatic()
// NewStatic has no monitoring resources to close.
old := mon.InterfaceState()
next := *old
// An identical snapshot is non-major; NewStatic has no OS monitor for IsMajorChangeFrom.
major := false
networkChangeValidInterfacesFn = func(context.Context) map[string]struct{} { return map[string]struct{}{} }
calls := 0
networkChangeIgnoredInterceptFn = func(*prog, *netmon.ChangeDelta, time.Time) { calls++ }
p := sourceTestProg(&prog{cfg: &ctrld.Config{}, dnsInterceptState: &interceptStateStub{}})
delta := &netmon.ChangeDelta{Monitor: mon, Old: old, New: &next, TimeJumped: timeJump}
//lint:ignore SA1019 This fixture reproduces netmon v1.74.0's legacy wake flag.
delta.Major = timeJump
p.handleNetworkChange(context.Background(), delta, major)
if calls != 1 {
t.Fatalf("real netmon non-major delta discarded: ignored-path calls=%d transition ID=%d", calls, p.networkTransitionGen.Load())
}
})
}
}
func TestIgnoredDeltaPreservesAcceptedRecovery(t *testing.T) {
sourceTestGlobals(t)
dnsIntercept = false
oldRecovery := handleRecoveryForTransitionFn
t.Cleanup(func() { handleRecoveryForTransitionFn = oldRecovery })
calls := make(chan uint64, 2)
handleRecoveryForTransitionFn = func(_ *prog, _ RecoveryReason, id uint64) { calls <- id }
networkChangeReconcileFn = (*prog).reconcileNetworkChange // Real accepted-path wiring, with recovery stubbed below.
p := sourceTestProg(&prog{cfg: &ctrld.Config{}})
a := sourceTestState("en0", true, "192.0.2.20/24")
p.handleNetworkChange(context.Background(), &netmon.ChangeDelta{Old: sourceTestState("en0", true, "192.0.2.10/24"), New: a}, true)
b := sourceTestState("en0", true, "192.0.2.20/24")
p.handleNetworkChange(context.Background(), &netmon.ChangeDelta{Old: a, New: b}, false)
select {
case <-calls:
case <-time.After(1200 * time.Millisecond):
t.Fatal("ignored delta canceled accepted recovery; no replacement was scheduled")
}
}
func TestIgnoredDeltaPreservesValidPendingSource(t *testing.T) {
sourceTestGlobals(t)
dnsIntercept = false
ctrld.SetDefaultLocalIPv4(context.Background(), nil)
ctrld.SetDefaultLocalIPv6(context.Background(), nil)
entered, release, done := make(chan struct{}), make(chan struct{}), make(chan struct{})
networkChangeDefaultRouteIPFn = func(*prog) string { close(entered); <-release; return "192.0.2.20" }
reconciled := make(chan uint64, 2)
networkChangeReconcileFn = func(_ *prog, _ context.Context, id uint64) { reconciled <- id }
p := sourceTestProg(&prog{cfg: &ctrld.Config{}})
a := sourceTestState("en0", true, "192.0.2.20/24")
go func() {
defer close(done)
p.handleNetworkChange(context.Background(), &netmon.ChangeDelta{Old: sourceTestState("en0", true, "192.0.2.10/24"), New: a}, true)
}()
<-entered
p.handleNetworkChange(context.Background(), &netmon.ChangeDelta{Old: a, New: sourceTestState("en0", true, "192.0.2.20/24")}, false)
close(release)
<-done
if !ctrld.GetDefaultLocalIPv4().Equal(net.ParseIP("192.0.2.20")) || len(reconciled) != 1 {
t.Fatalf("valid source discarded after ignored delta: source=%v reconciliations=%d", ctrld.GetDefaultLocalIPv4(), len(reconciled))
}
}
func TestNewAcceptedDeltaSupersedesPendingAcceptedSource(t *testing.T) {
sourceTestGlobals(t)
dnsIntercept = false
entered, release, done := make(chan struct{}), make(chan struct{}), make(chan struct{})
var lookups atomic.Int32
networkChangeDefaultRouteIPFn = func(*prog) string {
if lookups.Add(1) == 1 {
close(entered)
<-release
return "192.0.2.20"
}
return ""
}
reconciled := make(chan uint64, 2)
networkChangeReconcileFn = func(_ *prog, _ context.Context, id uint64) { reconciled <- id }
p := sourceTestProg(&prog{cfg: &ctrld.Config{}})
a := sourceTestState("en0", true, "192.0.2.20/24")
go func() {
defer close(done)
p.handleNetworkChange(context.Background(), &netmon.ChangeDelta{Old: sourceTestState("en0", true, "192.0.2.10/24"), New: a}, true)
}()
<-entered
p.handleNetworkChange(context.Background(), &netmon.ChangeDelta{Old: a, New: sourceTestState("en0", true, "192.0.2.30/24")}, true)
close(release)
<-done
if !ctrld.GetDefaultLocalIPv4().Equal(net.ParseIP("192.0.2.30")) || len(reconciled) != 1 || <-reconciled != 2 {
t.Fatal("older accepted delta replaced the newer source or recovery")
}
}
func TestSourceCommitUsesCurrentMajorEpoch(t *testing.T) {
mon := netmon.NewStatic()
latest := mon.InterfaceState()
minor := *latest
p := sourceTestProg(&prog{networkSourceState: &minor, networkSourceEpoch: latest})
delta := &netmon.ChangeDelta{Monitor: mon, Old: latest, New: &minor}
if p.sourceCommitState(delta) != &minor {
t.Fatal("discarded the observed minor update within the current epoch")
}
p.networkSourceEpoch = &minor
if p.sourceCommitState(delta) != latest {
t.Fatal("did not refresh from a newer major epoch")
}
if networkSnapshotCurrent(&netmon.ChangeDelta{Monitor: mon, Old: &minor, New: &minor}, false, latest) {
t.Fatal("accepted a minor callback from an obsolete major epoch")
}
}