Files
ctrld/cmd/cli/nrpt_handback_windows_test.go
T

1310 lines
50 KiB
Go

//go:build windows
package cli
import (
"errors"
"strings"
"sync"
"testing"
"time"
"github.com/Control-D-Inc/ctrld"
)
// fakeNRPTOps records the Windows side effects a transition would cause and serves
// scripted probe results, so the ownership decisions can be driven without touching the
// registry or the DNS Client.
type fakeNRPTOps struct {
mu sync.Mutex
// probeResults is consumed in order; the last value repeats once exhausted.
probeResults []bool
probeCalls int
flushCalls int
gpRule string // rule name findGPRule reports, "" for none
gpConflict bool // whether an external catch-all targets another resolver
parentEmpty bool // whether the GP parent key reads as present but empty
cleanCalls int
gpConflictCalls int
ctrldRule bool // whether ctrld's own catch-all exists
existsCalls int
addCalls int
removeCalls int
signalCalls int
addErr error
removeErr error
// beforeAdd runs inside addRule, while the transition lock is held.
beforeAdd func()
// onProbe runs after the nth probe (1-based) is answered, so a test can change the
// GP store mid-probe the way a Group Policy refresh would.
onProbe func(call int)
// onGPConflicts runs after each gpConflicts check, which is the last ops call before
// the two-phase re-add queues for the transition lock.
onGPConflicts func(call int)
// waitHook stands in for a cancellable wait: returning false models the intercept
// being retired mid-wait.
waitHook func() bool
}
func (f *fakeNRPTOps) ops() *nrptOps {
return &nrptOps{
probe: func(*wfpState) bool {
f.mu.Lock()
f.probeCalls++
call := f.probeCalls
result := false
if len(f.probeResults) > 0 {
result = f.probeResults[0]
if len(f.probeResults) > 1 {
f.probeResults = f.probeResults[1:]
}
}
onProbe := f.onProbe
f.mu.Unlock()
if onProbe != nil {
onProbe(call)
}
return result
},
ruleExists: func() bool {
f.mu.Lock()
defer f.mu.Unlock()
f.existsCalls++
return f.ctrldRule
},
addRule: func(string) error {
f.mu.Lock()
beforeAdd, err := f.beforeAdd, f.addErr
f.addCalls++
if err == nil {
f.ctrldRule = true
}
f.mu.Unlock()
if beforeAdd != nil {
beforeAdd()
}
return err
},
removeRule: func() error {
f.mu.Lock()
defer f.mu.Unlock()
f.removeCalls++
if f.removeErr != nil {
return f.removeErr
}
f.ctrldRule = false
return nil
},
signal: func() {
f.mu.Lock()
defer f.mu.Unlock()
f.signalCalls++
},
findGPRule: func(string) string {
f.mu.Lock()
defer f.mu.Unlock()
return f.gpRule
},
gpRuleMatches: func(ruleName, _ string) bool {
f.mu.Lock()
defer f.mu.Unlock()
return ruleName != "" && ruleName == f.gpRule
},
gpConflicts: func(*wfpState, string) bool {
f.mu.Lock()
f.gpConflictCalls++
call := f.gpConflictCalls
conflict, hook := f.gpConflict, f.onGPConflicts
f.mu.Unlock()
if hook != nil {
hook(call)
}
return conflict
},
loopback: func(*wfpState) error { return nil },
// No real waiting in tests; report "still live" unless a test says otherwise.
wait: func(*wfpState, time.Duration) bool {
f.mu.Lock()
hook := f.waitHook
f.mu.Unlock()
if hook != nil {
return hook()
}
return true
},
startWFP: func(*wfpState) error { return nil },
flush: func() {
f.mu.Lock()
defer f.mu.Unlock()
f.flushCalls++
},
parentEmpty: func(string) bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.parentEmpty
},
cleanParent: func() bool {
f.mu.Lock()
defer f.mu.Unlock()
f.cleanCalls++
wasEmpty := f.parentEmpty
f.parentEmpty = false
return wasEmpty
},
}
}
func (f *fakeNRPTOps) counts() (add, remove, signal, probe int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.addCalls, f.removeCalls, f.signalCalls, f.probeCalls
}
func (f *fakeNRPTOps) ruleExistsCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.existsCalls
}
func (f *fakeNRPTOps) flushCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.flushCalls
}
// setGPRule replaces what the fake GP store reports, as a policy refresh would.
func (f *fakeNRPTOps) setGPRule(ruleName string, conflicting bool) {
f.mu.Lock()
defer f.mu.Unlock()
f.gpRule = ruleName
f.gpConflict = conflicting
}
// hasCtrldRule reports whether ctrld's catch-all is currently installed.
func (f *fakeNRPTOps) hasCtrldRule() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.ctrldRule
}
// installedFakeNRPTOps is the fake currently standing in for the production NRPT
// operations, so fixtures can hand it to tests and can tell whether one is installed at
// all.
var installedFakeNRPTOps *fakeNRPTOps
// installFakeNRPTOps swaps in the fake for the duration of the test.
func installFakeNRPTOps(t *testing.T, f *fakeNRPTOps) {
t.Helper()
nrptOpsForTest = f.ops()
installedFakeNRPTOps = f
t.Cleanup(func() {
nrptOpsForTest = nil
installedFakeNRPTOps = nil
})
}
// fakeNRPTSeed is the starting state a test wants the fixture's fake to have. It is a
// separate type so the fake - which carries a mutex - is never copied.
type fakeNRPTSeed struct {
probeResults []bool
gpRule string
gpConflict bool
parentEmpty bool
ctrldRule bool
addErr error
removeErr error
}
// configure re-arms the fake the fixture installed with a test's own starting state, so
// the fixture keeps ownership of installation - and therefore of the production-call guard.
func (f *fakeNRPTOps) configure(seed fakeNRPTSeed) {
f.mu.Lock()
defer f.mu.Unlock()
f.probeResults = seed.probeResults
f.gpRule = seed.gpRule
f.gpConflict = seed.gpConflict
f.parentEmpty = seed.parentEmpty
f.ctrldRule = seed.ctrldRule
f.addErr = seed.addErr
f.removeErr = seed.removeErr
}
// requireFakeNRPTOpsInstalled proves the fake is the table production code will actually
// consult, and fails the test immediately if it is not.
//
// Identity is not enough on its own, and neither is asserting side-effect counters after
// the fact: a fixture that never installed the fake, or a seam that stopped consulting the
// override, leaves those counters at zero and every "no side effects" assertion passes
// while the real registry and signalling functions run. So this calls through
// prog.nrptOps() and requires the call to land on the fake. The probe is only made once an
// override is known to exist, so it can never reach the host itself.
func requireFakeNRPTOpsInstalled(t *testing.T, f *fakeNRPTOps) {
t.Helper()
if err := checkFakeNRPTOpsInstalled(f); err != nil {
t.Fatal(err)
}
}
// checkFakeNRPTOpsInstalled is the precondition itself, as a predicate so it can be tested
// like any other logic - see TestFakeNRPTOpsPreconditionDetectsBypass. It returns nil only
// when f is the table prog.nrptOps() hands out.
func checkFakeNRPTOpsInstalled(f *fakeNRPTOps) error {
if nrptOpsForTest == nil {
return errors.New("fake NRPT operations are not installed: production registry and signalling functions would run")
}
if installedFakeNRPTOps != f {
return errors.New("the installed fake is not the one this fixture returned")
}
before := f.ruleExistsCount()
_ = (&prog{}).nrptOps().ruleExists()
if f.ruleExistsCount() == before {
return errors.New("prog.nrptOps() did not route to the installed fake: the seam is not in effect")
}
return nil
}
// fakeNRPTOpsForTest returns the installed fake, installing a default one if the test has
// not already done so.
//
// Every fixture goes through this before publishing state, so no test can reach the
// production registry and signalling functions. That matters because those are not
// read-only: on a host where ctrld's deterministic key exists, one unfaked call can delete
// live NRPT policy and force a Group Policy refresh, a Dnscache paramchange and a cache
// flush. A clean runner is not a safety boundary.
func fakeNRPTOpsForTest(t *testing.T) *fakeNRPTOps {
t.Helper()
if installedFakeNRPTOps != nil {
return installedFakeNRPTOps
}
f := &fakeNRPTOps{}
installFakeNRPTOps(t, f)
return f
}
func newHandbackTestProg(t *testing.T) (*prog, *wfpState, *fakeNRPTOps) {
t.Helper()
// Never let a test fall through to the production NRPT operations, and prove it here
// rather than trusting that installation happened.
f := fakeNRPTOpsForTest(t)
requireFakeNRPTOpsInstalled(t, f)
state := &wfpState{stopCh: make(chan struct{}), listenerIP: "127.0.0.1"}
p := &prog{}
p.dnsInterceptState = state
return p, state, f
}
// TestHandbackRestoresFallbackWhenGPCannotRoute is the behaviour test for the handback
// contract. The first probe runs while ctrld's fallback is still installed, so that
// fallback can satisfy it; only a probe taken after ctrld's keys are gone says anything
// about the GP rule. With results [true, false] - the GP child looks fine until ctrld
// steps aside - the transition must put ctrld's rule back and keep ctrld ownership.
//
// Getting this wrong deletes the last working route and declares external ownership: in
// hard mode a machine-wide DNS outage, because WFP keeps blocking outbound DNS with
// nothing redirecting it to ctrld.
func TestHandbackRestoresFallbackWhenGPCannotRoute(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.tryAdoptMatchingGPNRPT(state); got {
t.Error("tryAdoptMatchingGPNRPT() = true for a GP rule that cannot route without ctrld's rule")
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("NRPT owner = %v, want nrptRuleOwnerCtrld: ownership must not move to a rule that failed the post-removal probe", owner)
}
add, remove, _, _ := f.counts()
if remove != 1 {
t.Errorf("removeRule calls = %d, want 1: the handback probe must run with ctrld's keys gone", remove)
}
if add != 1 {
t.Errorf("addRule calls = %d, want 1: the ctrld fallback must be restored after the probe fails", add)
}
if !f.hasCtrldRule() {
t.Error("ctrld's NRPT rule is missing after a failed handback; the host has no working route")
}
}
// TestHandbackAcceptsGPWhenItRoutesWithoutCtrld is the other half of the contract: when
// the post-removal probe passes, external policy really is carrying DNS, so ctrld hands
// ownership over and leaves its keys off the machine.
func TestHandbackAcceptsGPWhenItRoutesWithoutCtrld(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, true}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if !p.tryAdoptMatchingGPNRPT(state) {
t.Fatal("tryAdoptMatchingGPNRPT() = false for a GP rule that routes on its own")
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{GP-RULE}", owner, ruleName)
}
add, remove, _, _ := f.counts()
if remove != 1 || add != 0 {
t.Errorf("removeRule = %d, addRule = %d, want 1/0: ctrld's rule must be removed and not restored", remove, add)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is still installed beside the adopted GP catch-all")
}
}
// TestIneffectiveGPRuleTriggersNoSignalling holds ctrld to #576's contract for a
// present-but-ineffective external rule: warn and retry WFP-only, with no policy
// refresh, no Dnscache paramchange and no cache flush. signalNRPTChange is all three at
// once, so on a permanently dead GP rule a signalling loop would force machine Group
// Policy on a schedule.
func TestIneffectiveGPRuleTriggersNoSignalling(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false}, gpRule: "{GP-RULE}"}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerGroupPolicy, "{GP-RULE}")
// The verification path a health tick and the heal cycle both take.
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackUnverified {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackUnverified", got)
}
if p.healBlockedLoopbackDNS(state, "test") {
t.Error("healBlockedLoopbackDNS() = true although the probe never succeeds")
}
add, remove, signal, _ := f.counts()
if signal != 0 {
t.Errorf("signal calls = %d, want 0: ctrld must not force GP refresh, paramchange or a cache flush while external policy owns NRPT", signal)
}
if add != 0 || remove != 0 {
t.Errorf("addRule = %d, removeRule = %d, want 0/0: external policy must be left untouched", add, remove)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerGroupPolicy {
t.Errorf("owner = %v, want nrptRuleOwnerGroupPolicy: ctrld must not seize a namespace an administrator owns", owner)
}
}
// TestConcurrentFallbackActivationWritesOnce drives two health paths - the monitor and a
// delayed recheck - into activation at the same moment. Without a transition lock they
// both observe "rule missing" and both write and signal.
func TestConcurrentFallbackActivationWritesOnce(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true}}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
// Hold both callers at the barrier until each is inside activateCtrldNRPTFallback.
start := make(chan struct{})
var wg sync.WaitGroup
results := make([]bool, 2)
for i := range results {
wg.Add(1)
go func(i int) {
defer wg.Done()
<-start
results[i] = p.activateCtrldNRPTFallback(state, "concurrent test")
}(i)
}
close(start)
wg.Wait()
add, _, signal, _ := f.counts()
if add != 1 {
t.Errorf("addRule calls = %d, want 1: concurrent activations must not both write the catch-all", add)
}
if signal != 1 {
t.Errorf("signal calls = %d, want 1: the DNS Client must be signalled once per transition", signal)
}
if results[0] == results[1] {
t.Errorf("both callers reported %v; exactly one transition should report having written the rule", results[0])
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("owner = %v, want nrptRuleOwnerCtrld", owner)
}
}
// waitFor polls cond until it holds, failing the test if it never does.
func waitFor(t *testing.T, what string, cond func() bool) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for !cond() {
if time.Now().After(deadline) {
t.Fatalf("timed out waiting for %s", what)
}
time.Sleep(time.Millisecond)
}
}
// TestActivationLosingRaceWithStopWritesNothing is the interleaving a revocation check
// alone cannot catch: the activation passes its check, and the stop then revokes the
// state, removes NRPT and finishes before the write lands. Windows would be left routing
// every query to a listener that is gone.
//
// The test asserts the transition lock is load-bearing, in three steps. The barrier sits
// inside addRule, so the stop starts while a transition is mid-write; the stop is then
// shown to be unable to progress past its own NRPT cleanup while that transition holds
// the lock; and once the transition releases, the stop must actually have removed the
// rule before a later activation attempt is refused. Dropping the lock on either side -
// activation or stop cleanup - makes the second step fail, because the stop's removeRule
// then runs while the transition is still in flight.
func TestActivationLosingRaceWithStopWritesNothing(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true}}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
stopDone := make(chan struct{})
f.beforeAdd = func() {
// Runs with the transition lock held, from the test goroutine.
go func() {
defer close(stopDone)
_ = p.stopDNSIntercept()
}()
// The stop closes stopCh before it reaches the transition lock, so once that is
// closed the stop can only be waiting on this transition. Waiting on stopCh
// rather than interceptStateRevoked matters: the latter also reports the
// stop-requested flag, which is set before the stop has done anything.
waitFor(t, "the stop to revoke the intercept state", func() bool {
select {
case <-state.stopCh:
return true
default:
return false
}
})
// Give it a window in which it would visibly proceed if the lock were not held,
// then prove it did not: no NRPT removal, and the stop has not finished.
time.Sleep(100 * time.Millisecond)
if _, remove, _, _ := f.counts(); remove != 0 {
t.Errorf("stop removed NRPT (removeRule calls = %d) while a transition held the lock", remove)
}
select {
case <-stopDone:
t.Error("stop completed while an NRPT transition was still in flight")
default:
}
}
if !p.activateCtrldNRPTFallback(state, "activation racing a stop") {
t.Fatal("the in-flight activation should have completed its transition")
}
<-stopDone
// The stop ran after the transition released the lock, so its cleanup must have
// removed the rule this transition wrote.
if f.hasCtrldRule() {
t.Error("ctrld's NRPT rule survived the stop: the machine is left pointing at a listener that is gone")
}
if _, remove, _, _ := f.counts(); remove != 1 {
t.Errorf("removeRule calls = %d, want 1: the stop must clean up the rule written by the racing transition", remove)
}
if p.dnsInterceptState != nil {
t.Error("the stop did not complete after the in-flight transition released the lock")
}
// A second attempt models the other health path arriving after the stop finished.
addBefore, _, signalBefore, _ := f.counts()
if p.activateCtrldNRPTFallback(state, "activation after the stop completed") {
t.Error("activateCtrldNRPTFallback() = true after shutdown completed")
}
addAfter, _, signalAfter, _ := f.counts()
if addAfter != addBefore || signalAfter != signalBefore {
t.Errorf("addRule %d->%d, signal %d->%d: a post-shutdown transition wrote NRPT policy for a dead listener",
addBefore, addAfter, signalBefore, signalAfter)
}
if f.hasCtrldRule() {
t.Error("a post-shutdown transition re-created ctrld's NRPT rule")
}
}
// TestOwnedRecoveryDefersToIneffectiveExternalPolicy is the caller-level case for the
// ownership boundary: state says ctrld-owned, ctrld's key has disappeared, and an exact
// GP child is present but not routing.
//
// The handback records Group Policy ownership and reports Unverified. That is terminal:
// an administrator's catch-all owns the namespace whether or not it currently routes, so
// the owned-recovery flow must not go on to signal the DNS Client or delete and recreate
// ctrld's rule beside it. Only loopback WFP protect is allowed, and it touches no policy.
func TestOwnedRecoveryDefersToIneffectiveExternalPolicy(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false}, gpRule: "{GP-RULE}", ctrldRule: false}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
p.nrptProbeAndHeal(state)
add, remove, signal, _ := f.counts()
if add != 0 || remove != 0 || signal != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, want 0/0/0: owned recovery must stop once external policy owns the namespace",
add, remove, signal)
}
if f.hasCtrldRule() {
t.Error("owned recovery recreated ctrld's catch-all beside the administrator's rule")
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{GP-RULE}", owner, ruleName)
}
}
// TestHandbackAbortedWhenRuleCannotBeRemoved keeps a failed registry step from being
// read as a verdict about external policy.
func TestHandbackAbortedWhenRuleCannotBeRemoved(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{
probeResults: []bool{true, true},
gpRule: "{GP-RULE}",
ctrldRule: true,
removeErr: errors.New("access denied"),
})
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackAborted {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackAborted", got)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("owner = %v, want nrptRuleOwnerCtrld: a failed removal is not proof about external policy", owner)
}
}
// TestHandbackRestoresFallbackWhenGPChildDisappearsMidProbe covers a Group Policy refresh
// landing during the post-removal probe: the child ctrld was testing is simply gone. There
// is no external policy left to hand ownership to, so ctrld's rule has to come back.
func TestHandbackRestoresFallbackWhenGPChildDisappearsMidProbe(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
if call == 2 {
f.setGPRule("", false) // the administrator removed the catch-all
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackKeptCtrld {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("owner = %v, want nrptRuleOwnerCtrld", owner)
}
if !f.hasCtrldRule() {
t.Error("ctrld's rule was not restored after the GP child disappeared; the host has no route")
}
if add, remove, _, _ := f.counts(); add != 1 || remove != 1 {
t.Errorf("addRule = %d, removeRule = %d, want 1/1", add, remove)
}
}
// TestHandbackWritesNoSiblingWhenGPChildTurnsConflicting is the case the same-child
// re-read exists for. ctrld removes its keys to test an exact catch-all, and during the
// probe Group Policy replaces it with one that targets another resolver (or a malformed
// one). Restoring ctrld's rule would create exactly the competing sibling beside
// administrator policy that this must never write, so the rule stays off and ownership
// goes to nobody.
func TestHandbackWritesNoSiblingWhenGPChildTurnsConflicting(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
if call == 2 {
// The child now names another resolver: not a match for this listener, and
// reported as a conflict by the GP classifier.
f.setGPRule("", true)
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackConflict {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackConflict", got)
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0: restoring here writes a competing rule beside administrator policy", add)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is installed beside a GP catch-all that targets another resolver")
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerNone {
t.Errorf("owner = %v, want nrptRuleOwnerNone: ctrld owns nothing once external policy points elsewhere", owner)
}
}
// TestHandbackWithoutCtrldRuleClassifiesAfterFailedProbe covers the other short-circuit:
// with no ctrld rule to remove, a failed probe must still be classified before anything is
// recorded. A child that changed into a conflicting catch-all is not an ineffective ctrld
// candidate, and must not be recorded as one.
func TestHandbackWithoutCtrldRuleClassifiesAfterFailedProbe(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false}, gpRule: "{GP-RULE}"}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
if call == 1 {
f.setGPRule("", true)
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackConflict {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackConflict", got)
}
if add, remove, signal, _ := f.counts(); add != 0 || remove != 0 || signal != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, want 0/0/0", add, remove, signal)
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerNone || ruleName != "" {
t.Errorf("owner = %v, rule = %q, want nrptRuleOwnerNone/\"\": a conflicting child must not be recorded as ctrld's external owner", owner, ruleName)
}
}
// TestHandbackGivesReplacementChildItsOwnPass keeps an unproved child from inheriting a
// verdict that was measured against a different one. With no ctrld rule installed the
// replacement is simply probed on its own pass; failing that pass records it as external
// policy that is not routing, and writes nothing either way.
func TestHandbackGivesReplacementChildItsOwnPass(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false, false}, gpRule: "{GP-RULE}"}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
if call == 1 {
f.setGPRule("{NEW-GP-RULE}", false)
}
}
p, state, _ := newHandbackTestProg(t)
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackUnverified {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackUnverified", got)
}
if add, remove, signal, _ := f.counts(); add != 0 || remove != 0 || signal != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, want 0/0/0", add, remove, signal)
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{NEW-GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{NEW-GP-RULE}: the verdict must name the child it was measured against", owner, ruleName)
}
}
// TestHandbackWritesNoSiblingWhenReplacementChildTakesOver is the case where ctrld's rule
// was removed for the probe and a *different* exact catch-all took the namespace while it
// was off.
//
// Restoring ctrld's rule here is not a return to the status quo: the replacement was not
// there when the transition started, and addNRPTCatchAllRule writes ctrld's own GP
// catch-all whenever another GP rule exists, so the restore would plant a sibling beside
// the administrator's new rule. The replacement gets its own probe instead, and a failed
// one leaves NRPT to Group Policy with nothing of ctrld's written.
func TestHandbackWritesNoSiblingWhenReplacementChildTakesOver(t *testing.T) {
// [pre-probe true, post-removal probe false, replacement's own pass false]
f := &fakeNRPTOps{probeResults: []bool{true, false, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
if call == 2 {
f.setGPRule("{NEW-GP-RULE}", false) // a policy refresh swaps the child
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackUnverified {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackUnverified", got)
}
if add, remove, _, _ := f.counts(); add != 0 || remove != 1 {
t.Errorf("addRule = %d, removeRule = %d, want 0/1: ctrld's rule must not be restored beside a replacement catch-all", add, remove)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is installed beside an administrator catch-all that took the namespace")
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{NEW-GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{NEW-GP-RULE}", owner, ruleName)
}
}
// TestHandbackAdoptsReplacementChildThatRoutes is the same swap where the replacement does
// carry DNS on its own: it is adopted, still with no ctrld write.
func TestHandbackAdoptsReplacementChildThatRoutes(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false, true}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
if call == 2 {
f.setGPRule("{NEW-GP-RULE}", false)
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackVerified {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackVerified", got)
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0", add)
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{NEW-GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{NEW-GP-RULE}", owner, ruleName)
}
}
// TestHandbackChurnFailsSafeToCurrentExternalOwner bounds the extra pass without letting
// exhaustion become a licence to write.
//
// When the probe budget runs out with the store still changing, the transition classifies
// the store as it stands and proves no route. Reporting "undecided" instead would be
// unsafe: that disposition is not terminal external ownership, so startup and owned
// recovery fall through to writing ctrld's rule, and addNRPTCatchAllRule puts it in the GP
// path beside whichever exact catch-all is there now.
func TestHandbackChurnFailsSafeToCurrentExternalOwner(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
switch call {
case 2:
f.setGPRule("{GP-RULE-2}", false)
case 3:
f.setGPRule("{GP-RULE-3}", false)
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test")
if got != nrptHandbackUnverified {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackUnverified", got)
}
if !nrptExternalOwns(got) {
t.Error("the churn disposition is not terminal external ownership; callers would fall through and write a sibling")
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0: no sibling write while the GP store is churning", add)
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE-3}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{GP-RULE-3}: exhaustion must record the store as it stands", owner, ruleName)
}
}
// TestHandbackChurnFailsSafeToConflict covers exhaustion where the store has settled on a
// catch-all that does not target ctrld: still terminal, still no write.
func TestHandbackChurnFailsSafeToConflict(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
switch call {
case 2:
f.setGPRule("{GP-RULE-2}", false)
case 3:
f.setGPRule("", true) // now an administrator catch-all for another resolver
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackConflict {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackConflict", got)
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0", add)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerNone {
t.Errorf("owner = %v, want nrptRuleOwnerNone", owner)
}
}
// TestHandbackChurnRestoresFallbackWhenNamespaceFreed is the third exhaustion outcome: the
// store ended up with no external catch-all at all, so the namespace is free and ctrld's
// rule is the one that belongs there.
func TestHandbackChurnRestoresFallbackWhenNamespaceFreed(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
switch call {
case 2:
f.setGPRule("{GP-RULE-2}", false)
case 3:
f.setGPRule("", false) // the administrator removed the policy entirely
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackKeptCtrld {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
if !f.hasCtrldRule() {
t.Error("ctrld's rule was not restored although no external catch-all remains")
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("owner = %v, want nrptRuleOwnerCtrld", owner)
}
}
// TestActivationWritesNoSiblingWhileGPStoreChurns is the caller-level closure for the churn
// path. activateCtrldNRPTFallback is the function both startup fallback and owned recovery
// reach when ctrld's rule is missing, and it is where a non-terminal churn disposition
// would turn into addNRPTCatchAllRule writing a GP sibling beside the current catch-all.
func TestActivationWritesNoSiblingWhileGPStoreChurns(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false, false}, gpRule: "{GP-RULE}"}
installFakeNRPTOps(t, f)
f.onProbe = func(call int) {
switch call {
case 1:
f.setGPRule("{GP-RULE-2}", false)
case 2:
f.setGPRule("{GP-RULE-3}", false)
}
}
p, state, _ := newHandbackTestProg(t)
// Startup and the owner-None retry both arrive here with no ownership recorded.
state.setNRPTPolicyOwner(nrptRuleOwnerNone, "")
if p.activateCtrldNRPTFallback(state, "churn test") {
t.Error("activateCtrldNRPTFallback() = true while an external catch-all owns the namespace")
}
add, remove, signal, _ := f.counts()
if add != 0 || signal != 0 {
t.Errorf("addRule = %d, signal = %d, want 0/0: ctrld must not write beside the administrator's catch-all", add, signal)
}
if remove != 0 {
t.Errorf("removeRule calls = %d, want 0: there was no ctrld rule to remove", remove)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerGroupPolicy {
t.Errorf("owner = %v, want nrptRuleOwnerGroupPolicy: the current external catch-all owns the namespace", owner)
}
}
// TestStopLeavesGPManagedPolicyAlone is the central promise of this work: an
// administrator's catch-all is a deployment contract that must survive service stop,
// restart and uninstall. A regression that removed NRPT for every owner would delete that
// rule and take the fleet's DNS policy with it.
func TestStopLeavesGPManagedPolicyAlone(t *testing.T) {
f := &fakeNRPTOps{gpRule: "{GP-RULE}"}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerGroupPolicy, "{GP-RULE}")
if err := p.stopDNSIntercept(); err != nil {
t.Fatalf("stopDNSIntercept() = %v", err)
}
add, remove, signal, _ := f.counts()
if remove != 0 || signal != 0 || add != 0 {
t.Errorf("removeRule = %d, signal = %d, addRule = %d, want 0/0/0: shutdown must not touch externally owned NRPT policy",
remove, signal, add)
}
if f.flushCount() != 0 {
t.Errorf("flush calls = %d, want 0: no cache flush is owed for a rule ctrld does not own", f.flushCount())
}
if p.dnsInterceptState != nil {
t.Error("dnsInterceptState survived shutdown")
}
}
// TestStopRemovesOrphanWhileLeavingGPPolicy is the same shutdown with a ctrld rule left
// behind by an earlier unclean exit. External policy still stays, but the orphan must go:
// while GP mode hides the local store this stop is the last chance anything looks there.
func TestStopRemovesOrphanWhileLeavingGPPolicy(t *testing.T) {
f := &fakeNRPTOps{gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerGroupPolicy, "{GP-RULE}")
if err := p.stopDNSIntercept(); err != nil {
t.Fatalf("stopDNSIntercept() = %v", err)
}
add, remove, signal, _ := f.counts()
if remove != 1 || signal != 1 {
t.Errorf("removeRule = %d, signal = %d, want 1/1: the orphaned ctrld rule must be removed on the way out", remove, signal)
}
if add != 0 {
t.Errorf("addRule calls = %d, want 0", add)
}
if f.hasCtrldRule() {
t.Error("the orphaned ctrld rule survived shutdown; a later GP removal would activate it")
}
if f.flushCount() != 0 {
t.Errorf("flush calls = %d, want 0: the ctrld-owned branch is what owes a flush, not the GP branch", f.flushCount())
}
}
// TestRemoveOrphanedCtrldNRPTRule covers the sweep directly, including that it is a no-op
// when there is nothing of ctrld's on disk - it runs on shutdown paths where the common
// case is a clean machine.
func TestRemoveOrphanedCtrldNRPTRule(t *testing.T) {
t.Run("removes and signals when a ctrld rule exists", func(t *testing.T) {
f := &fakeNRPTOps{ctrldRule: true}
installFakeNRPTOps(t, f)
p, _, _ := newHandbackTestProg(t)
p.removeOrphanedCtrldNRPTRule("test")
add, remove, signal, _ := f.counts()
if remove != 1 || signal != 1 || add != 0 {
t.Errorf("removeRule = %d, signal = %d, addRule = %d, want 1/1/0", remove, signal, add)
}
if f.hasCtrldRule() {
t.Error("the ctrld rule is still installed")
}
})
t.Run("does nothing when no ctrld rule exists", func(t *testing.T) {
f := &fakeNRPTOps{}
installFakeNRPTOps(t, f)
p, _, _ := newHandbackTestProg(t)
p.removeOrphanedCtrldNRPTRule("test")
add, remove, signal, _ := f.counts()
if remove != 0 || signal != 0 || add != 0 {
t.Errorf("removeRule = %d, signal = %d, addRule = %d, want 0/0/0: nothing to sweep must cost no registry writes and no signalling",
remove, signal, add)
}
})
}
// TestHandbackThrottleBlocksTheSecondAttempt exercises the throttle through the
// transition, not just its accessor. Each attempt removes the live catch-all for a probe,
// so a second attempt inside the window must cost nothing at all: no probe, no removal.
func TestHandbackThrottleBlocksTheSecondAttempt(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "first"); got != nrptHandbackKeptCtrld {
t.Fatalf("first nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
_, removeAfterFirst, _, probesAfterFirst := f.counts()
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "second"); got != nrptHandbackAborted {
t.Fatalf("second nrptHandbackToExternal() = %v, want nrptHandbackAborted", got)
}
_, remove, _, probes := f.counts()
if probes != probesAfterFirst {
t.Errorf("probe calls went from %d to %d: a throttled attempt must not probe", probesAfterFirst, probes)
}
if remove != removeAfterFirst {
t.Errorf("removeRule calls went from %d to %d: a throttled attempt must not remove the live rule", removeAfterFirst, remove)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("owner = %v, want nrptRuleOwnerCtrld", owner)
}
}
// TestHealLadderRunsRetriesThenTwoPhaseRecovery drives the ctrld-owned heal cycle end to
// end: the immediate probe, the signal-and-backoff retries, then the two-phase delete and
// re-add, with the last probe finally succeeding. Each nrptTransition phase is asserted
// through its side effects, since these are its only call sites.
func TestHealLadderRunsRetriesThenTwoPhaseRecovery(t *testing.T) {
// Every probe fails until the one after the re-add.
f := &fakeNRPTOps{
probeResults: []bool{false, false, false, false, true},
ctrldRule: true,
}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
p.nrptProbeAndHeal(state)
add, remove, signal, probes := f.counts()
if remove != 1 {
t.Errorf("removeRule calls = %d, want 1: phase one of the two-phase recovery deletes ctrld's rule once", remove)
}
if add != 1 {
t.Errorf("addRule calls = %d, want 1: phase two re-adds it once", add)
}
if !f.hasCtrldRule() {
t.Error("ctrld's rule is missing after the two-phase recovery")
}
// Three backoff rounds signal, then the delete phase and the re-add phase.
if signal < 5 {
t.Errorf("signal calls = %d, want at least 5 (three retries plus both recovery phases)", signal)
}
if probes < 5 {
t.Errorf("probe calls = %d, want at least 5", probes)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("owner = %v, want nrptRuleOwnerCtrld", owner)
}
}
// TestHealLadderStopsWhenGPAppearsBeforeTwoPhase covers a Group Policy catch-all landing
// during the backoff retries. The ladder must hand back and stop before the destructive
// delete-and-re-add phase, and must not write ctrld's rule beside the new policy.
func TestHealLadderStopsWhenGPAppearsBeforeTwoPhase(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false}, ctrldRule: true}
installFakeNRPTOps(t, f)
// The administrator's rule appears while the fourth probe is in flight, which is the
// last one before the two-phase recovery.
f.onProbe = func(call int) {
if call == 4 {
f.setGPRule("{GP-RULE}", false)
}
}
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
p.nrptProbeAndHeal(state)
add, remove, _, _ := f.counts()
if remove != 0 {
t.Errorf("removeRule calls = %d, want 0: the ladder must stop before the delete half of the two-phase recovery", remove)
}
if add != 0 {
t.Errorf("addRule calls = %d, want 0: the ladder must not recreate ctrld's rule beside a new GP catch-all", add)
}
if !f.hasCtrldRule() {
t.Error("ctrld's rule was deleted after an administrator catch-all appeared")
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{GP-RULE}: the ladder must defer rather than continue", owner, ruleName)
}
}
// TestHealLadderCleansEmptyGPParentBeforeRetrying pins the empty-GP-parent shortcut: an
// empty parent key puts the DNS Client in GP mode where ctrld's local rule is invisible, so
// signalling retries cannot succeed until it is deleted.
func TestHealLadderCleansEmptyGPParentBeforeRetrying(t *testing.T) {
f := &fakeNRPTOps{probeResults: []bool{false, true}, ctrldRule: true, parentEmpty: true}
installFakeNRPTOps(t, f)
p, state, _ := newHandbackTestProg(t)
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
p.nrptProbeAndHeal(state)
if f.cleanCalls != 1 {
t.Errorf("cleanParent calls = %d, want 1: the empty GP parent must be removed before burning retries", f.cleanCalls)
}
if add, remove, _, _ := f.counts(); add != 0 || remove != 0 {
t.Errorf("addRule = %d, removeRule = %d, want 0/0: the probe passed after the cleanup, so no recovery was needed", add, remove)
}
}
// TestRepairMissingWFPRetriesHardModeWithNoEngine covers the entry point a startup WFP
// failure depends on. In hard mode a closed engine means nothing is being blocked, so the
// health monitor must keep retrying; in dns mode a closed engine is the normal state and
// must not trigger anything.
func TestRepairMissingWFPRetriesHardModeWithNoEngine(t *testing.T) {
originalRebuild, originalHard := rebuildDNSInterceptFn, hardIntercept
t.Cleanup(func() { rebuildDNSInterceptFn, hardIntercept = originalRebuild, originalHard })
var reasons []string
rebuildDNSInterceptFn = func(_ *prog, _ *wfpState, reason string) interceptRebuildResult {
reasons = append(reasons, reason)
return interceptRebuildDone
}
t.Run("hard mode retries when the engine never opened", func(t *testing.T) {
reasons = nil
hardIntercept = true
p, state, _ := newHandbackTestProg(t)
if !p.repairMissingWFP(state) {
t.Error("repairMissingWFP() = false; the monitor must hand over to the rebuilt intercept")
}
if len(reasons) != 1 {
t.Fatalf("rebuild requests = %d, want 1: hard mode with no WFP engine must be retried", len(reasons))
}
if !strings.Contains(reasons[0], "no WFP engine") {
t.Errorf("rebuild reason = %q, want it to name the missing engine", reasons[0])
}
})
t.Run("dns mode leaves a closed engine alone", func(t *testing.T) {
reasons = nil
hardIntercept = false
p, state, _ := newHandbackTestProg(t)
if p.repairMissingWFP(state) {
t.Error("repairMissingWFP() = true in dns mode; a closed engine is normal there")
}
if len(reasons) != 0 {
t.Errorf("rebuild requests = %d, want 0", len(reasons))
}
})
}
// TestPhaseTwoRevalidatesOwnershipAfterWaitingForTheLock is the ordering a pre-lock check
// cannot cover. Phase one removes ctrld's rule and releases the transition lock; the
// pre-re-add checks see no GP rule; another health path then takes the lock, hands the
// namespace to a GP catch-all that has just arrived, and records GroupPolicy. Phase two
// acquires the lock afterwards.
//
// Without a re-read inside the locked closure, phase two re-adds ctrld's rule beside the
// administrator's catch-all and stamps ctrld ownership over theirs - exactly the competing
// policy this work exists to prevent.
func TestPhaseTwoRevalidatesOwnershipAfterWaitingForTheLock(t *testing.T) {
p, state, f := newHandbackTestProg(t)
// Every probe fails, so the ladder runs to the two-phase recovery.
f.configure(fakeNRPTSeed{probeResults: []bool{false}, ctrldRule: true})
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
// The last ops call before phase two queues for the lock. Hold the lock from another
// goroutine and complete the competing transition while phase two waits for it.
f.onGPConflicts = func(call int) {
if call != 1 {
return
}
locked := make(chan struct{})
go func() {
p.nrptTransitionMu.Lock()
close(locked)
f.setGPRule("{GP-RULE}", false)
state.setNRPTPolicyOwner(nrptRuleOwnerGroupPolicy, "{GP-RULE}")
// Hold it long enough that phase two must queue behind this transition.
time.Sleep(100 * time.Millisecond)
p.nrptTransitionMu.Unlock()
}()
<-locked
}
p.nrptProbeAndHeal(state)
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0: phase two re-added ctrld's rule beside a GP catch-all that took ownership while it waited for the lock", add)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is installed beside the administrator's catch-all")
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{GP-RULE}: phase two overwrote a newer ownership transition", owner, ruleName)
}
}
// TestRetiredHealDoesNotDeleteSuccessorRule covers the other side of a rebuild. The heal
// cycle re-adds ctrld's rule, then its final settle wait observes that its own state was
// retired - while a successor intercept is already running with the same deterministic key.
//
// The key is process-global, so any cleanup from here would delete the successor's route
// and leave hard mode blocking DNS with nothing redirecting it. Teardown of the retired
// state owns that cleanup instead.
func TestRetiredHealDoesNotDeleteSuccessorRule(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{probeResults: []bool{false}, ctrldRule: true})
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
successor := &wfpState{stopCh: make(chan struct{}), listenerIP: "127.0.0.1"}
// Retire this cycle's state during the final wait - the one after the re-add, which is
// the only wait where both a removal and an add have happened - and publish a successor
// that owns the same deterministic key.
f.waitHook = func() bool {
if add, remove, _, _ := f.counts(); add == 0 || remove == 0 {
return true
}
close(state.stopCh) // a rebuild retired the old state
p.dnsInterceptState = successor
successor.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
return false
}
p.nrptProbeAndHeal(state)
if !f.hasCtrldRule() {
t.Error("the successor's NRPT rule was deleted by a retired heal cycle")
}
if owner, _ := successor.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("successor owner = %v, want nrptRuleOwnerCtrld", owner)
}
}
// TestStartupReportsFailureWhenExternalPolicyNeverRoutes is the readiness direction of the
// GP-managed contract, driven through the real startup control flow.
//
// An exact GP child is present and no probe ever reaches ctrld. Startup must keep the
// recovery state and the monitor - the rule may start routing later, and only external
// policy can fix it - must leave adapter DNS alone, and must not write an owned fallback.
// What it must not do is return success: setDNS records readiness from a nil error, so
// reporting ready here publishes "service healthy" while the DNS Client is not delivering
// queries to ctrld, and in hard mode WFP is blocking every other resolver at the same time.
func TestStartupReportsFailureWhenExternalPolicyNeverRoutes(t *testing.T) {
originalCfg, originalMode, originalIntercept, originalHard := cfg, interceptMode, dnsIntercept, hardIntercept
t.Cleanup(func() {
cfg, interceptMode, dnsIntercept, hardIntercept = originalCfg, originalMode, originalIntercept, originalHard
})
cfg = ctrld.Config{}
cfg.Listener = map[string]*ctrld.ListenerConfig{"0": {IP: "127.0.0.1", Port: 53}}
interceptMode, dnsIntercept, hardIntercept = "dns", true, false
p, _, f := newHandbackTestProg(t)
p.cfg = &cfg
p.dnsInterceptState = nil // startup publishes its own state
// An exact GP child that never answers a probe.
f.configure(fakeNRPTSeed{probeResults: []bool{false}, gpRule: "{GP-RULE}"})
err := p.startDNSInterceptLocked()
if err == nil {
t.Fatal("startDNSInterceptLocked() = nil: startup reported success while no probe reached ctrld, so setDNS would mark the service ready")
}
if !interceptFailedUnderExternalDNSPolicy(err) {
t.Errorf("err = %v; the failure must tell setDNS to leave adapter DNS untouched", err)
}
if interceptFailedWithVerifiedExternalDNS(err) {
t.Errorf("err = %v; this route was never verified, so it must not read as the verified case", err)
}
if add, remove, signal, _ := f.counts(); add != 0 || remove != 0 || signal != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, want 0/0/0: no owned fallback may be written beside an administrator catch-all", add, remove, signal)
}
// Recovery must stay live so the rule can be re-tested.
state, ok := p.dnsInterceptState.(*wfpState)
if !ok || state == nil {
t.Fatal("intercept state was not published; nothing would keep re-testing the external rule")
}
if owner, ruleName := state.nrptPolicyOwner(); owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE}" {
t.Errorf("owner = %v, rule = %q, want GroupPolicy/{GP-RULE}", owner, ruleName)
}
// Stop the monitor startup launched.
_ = p.stopDNSIntercept()
}
// TestFakeNRPTOpsPreconditionDetectsBypass guards the guard. The fixtures' promise is that
// no test can reach the production registry and signalling functions, and that promise is
// only as good as this precondition: if it stops detecting a bypass, every "no side
// effects" assertion in the Windows suite silently becomes vacuous while the real host
// policy is at risk.
func TestFakeNRPTOpsPreconditionDetectsBypass(t *testing.T) {
f := &fakeNRPTOps{}
// The exact bypass to catch: a fixture that returns a fake it never installed.
if err := checkFakeNRPTOpsInstalled(f); err == nil {
t.Error("an uninstalled fake passed the precondition; a test could fall through to production NRPT operations")
}
installFakeNRPTOps(t, f)
if err := checkFakeNRPTOpsInstalled(f); err != nil {
t.Errorf("an installed fake failed the precondition: %v", err)
}
// A different fake than the installed one must not pass either: side-effect counters
// would be read from an object nothing consults.
if err := checkFakeNRPTOpsInstalled(&fakeNRPTOps{}); err == nil {
t.Error("a fake that is not the installed one passed the precondition")
}
}