dns_intercept: don't write a GP sibling on failed NRPT handback

A failed handback restored ctrld's rule via addNRPTCatchAllRule, which
also writes the GP-path rule whenever another GP rule exists - leaving
CtrldCatchAll beside the administrator's catch-all. Restore only the
local store while that child is still present.
This commit is contained in:
Cuong Manh Le committed 2026-09-28 16:45:45 +07:00
1 parent 799f14e639
commit d1690545f9
5 files changed
+586 -92

No files matched your search

@@ -38,10 +38,11 @@ func newInterceptTestProg(t *testing.T) (*prog, *wfpState, *fakeNRPTOps) {
// would have hit the host's real policy.
func assertNoNRPTSideEffects(t *testing.T, f *fakeNRPTOps) {
t.Helper()
add, remove, signal, _ := f.counts()
if add != 0 || remove != 0 || signal != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, want 0/0/0: this path must not write NRPT policy",
add, remove, signal)
add, restoreLocal := f.writes()
_, remove, signal, _ := f.counts()
if add != 0 || restoreLocal != 0 || remove != 0 || signal != 0 {
t.Errorf("addRule = %d, restoreLocalRule = %d, removeRule = %d, signal = %d, want 0/0/0/0: this path must not write NRPT policy",
add, restoreLocal, remove, signal)
}
if flush := f.flushCount(); flush != 0 {
t.Errorf("flush calls = %d, want 0: this path must not flush the resolver cache", flush)
+121 -30
View File
@@ -399,6 +399,11 @@ const (
// (Get-DnsClientNrptPolicy returns empty). This is a deterministic GUID
// so we can reliably find and clean up our own rule.
nrptDirectRuleName = `{B2E9A3C1-7F4D-4A8E-9D6B-5C1E0F3A2B8D}`
// nrptDirectRulePath is the full path of ctrld's local-store catch-all: the
// primary key, written on every start and the only one a failed handback puts
// back. Named once so the writers, the reader and the remover cannot drift.
nrptDirectRulePath = nrptDirectKey + `\` + nrptDirectRuleName
)
func (p *prog) nrptListenerIP() string {
@@ -522,7 +527,7 @@ func findConflictingGPCatchAll(listenerIP string) (string, string) {
// - After GP writes, call RefreshPolicyEx to activate.
func addNRPTCatchAllRule(listenerIP string) error {
// Always write to local/direct service store path.
if err := writeNRPTRule(nrptDirectKey+`\`+nrptDirectRuleName, listenerIP); err != nil {
if err := writeNRPTRule(nrptDirectRulePath, listenerIP); err != nil {
return fmt.Errorf("failed to write NRPT local path rule: %w", err)
}
@@ -541,6 +546,41 @@ func addNRPTCatchAllRule(listenerIP string) error {
return nil
}
// restoreNRPTLocalCatchAllRule writes ctrld's catch-all to the local/direct store and
// touches nothing else. It is the only writer ctrld may use while an administrator's
// catch-all is on disk: the handback whose probe found no route, and startup when it must
// install ctrld's own rule beside external policy.
//
// addNRPTCatchAllRule cannot be used there. It writes ctrld's GP-path rule whenever any
// other GP rule exists, and the administrator's catch-all under test is exactly such a
// rule - so "restoring" through it plants CtrldCatchAll beside that child: the second GP
// catch-all that an administrator did not write.
//
// The contract this file keeps, which is narrower than a restore:
//
// - ctrld does not write its GP rule next to an administrator catch-all.
// - This writes back the local GUID key, and only that key.
// - Every other key removeNRPTCatchAllRule deleted stays deleted: the legacy local name
// and ctrld's GP CtrldCatchAll. A GP sibling left behind by an earlier run does not
// come back, so the store after this write is not the store the transition started
// from.
// - What comes back is ownership, not delivery. The branch is reached because the
// post-removal probe failed, and while GP policy is in effect the DNS Client can
// ignore the local key entirely. Nor is the withheld sibling knowably useless:
// isMatchingGPNRPTRule reads only Name and GenericDNSServers, so a child it accepts
// can still be one the DNS Client skips for want of Comment, DisplayName or
// IPSECCARestriction - in which case ctrld's GP rule was the route.
//
// The cost of that contract is that there may be no DNS until the administrator's rule is
// corrected. ctrld logs a warning, keeps ownership, and re-tests the external child after
// nrptHandbackRetryInterval.
func restoreNRPTLocalCatchAllRule(listenerIP string) error {
if err := writeNRPTRule(nrptDirectRulePath, listenerIP); err != nil {
return fmt.Errorf("failed to restore NRPT local path rule: %w", err)
}
return nil
}
// otherGPRulesExist checks if non-ctrld NRPT rules exist in the GP path.
// When other software (IT policy, VPN, MDM) has GP rules, DNS Client enters
// "GP mode" and ignores ALL local-path rules.
@@ -568,6 +608,12 @@ func otherGPRulesExist() bool {
//
// Do not leave an empty GP parent behind: Windows treats the parent key itself
// as the policy store boundary, so an empty key can still hide local-path rules.
// removeCtrldGPRule deletes ctrld's GP-store catch-all and nothing else: not the parent
// key, which an administrator rule still occupies, and not the local store.
func removeCtrldGPRule() {
registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseKey+`\`+nrptRuleName)
}
func cleanGPPath() bool {
// Delete our specific rule.
registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseKey+`\`+nrptRuleName)
@@ -635,7 +681,7 @@ func writeNRPTRule(keyPath, listenerIP string) error {
// the empty parent on stop, we ensure a clean slate for the next start.
func removeNRPTCatchAllRule() error {
// Remove our GUID-named rule from local/direct path.
if err := registry.DeleteKey(registry.LOCAL_MACHINE, nrptDirectKey+`\`+nrptDirectRuleName); err != nil {
if err := registry.DeleteKey(registry.LOCAL_MACHINE, nrptDirectRulePath); err != nil {
if err != registry.ErrNotExist {
return fmt.Errorf("failed to delete NRPT local rule: %w", err)
}
@@ -665,7 +711,7 @@ func deleteEmptyParentKey(keyPath string) {
// in either the local or GP path.
func nrptCatchAllRuleExists() bool {
for _, path := range []string{
nrptDirectKey + `\` + nrptDirectRuleName,
nrptDirectRulePath,
nrptBaseKey + `\` + nrptRuleName,
} {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.QUERY_VALUE)
@@ -877,7 +923,20 @@ func (p *prog) startDNSInterceptLocked() error {
owner, _ = state.nrptPolicyOwner()
if owner == nrptRuleOwnerNone {
if ops.ruleExists() {
exists := ops.ruleExists()
if exists && ops.findGPRule(listenerIP) != "" {
// An earlier run left ctrld's GP-store catch-all beside an administrator
// catch-all, and adopting would inherit it: nothing downstream removes it.
// Every handback from here stops at the pre-probe while that rule is
// unproven, the monitor calls removeRule only inside a handback, and the
// orphan sweep runs at stop - so the second catch-all would stand for the
// whole process lifetime, through a DNS outage. Drop it before adopting,
// then re-read: ruleExists is also true for the sibling alone, and a store
// holding nothing else must fall through to the writer below.
ops.dropGPRule()
exists = ops.ruleExists()
}
if exists {
// A rule from an earlier run already points at this listener. Adopt it rather
// than writing a second time: with a GP child present, addNRPTCatchAllRule
// would also write ctrld's GP-path sibling.
@@ -885,10 +944,22 @@ func (p *prog) startDNSInterceptLocked() error {
mainLog.Load().Info().Str("listener", listenerIP).
Msg("DNS intercept: adopting the ctrld NRPT catch-all already present from an earlier run")
} else {
// Which writer is allowed here is decided the same way the handback decides
// it: with an administrator catch-all on disk, addNRPTCatchAllRule would
// place ctrld's GP rule beside it, so only the local key may be written.
//
// This is not a hypothetical branch. The sibling cleanup above falls into it
// whenever the store held nothing but ctrld's GP key - having just deleted
// that key, the full writer would put it straight back, next to the very
// rule it was removed from.
write := ops.addRule
if ops.findGPRule(listenerIP) != "" {
write = ops.restoreLocalRule
}
if ops.cleanParent() {
ops.signal()
}
if err := ops.addRule(listenerIP); err != nil {
if err := write(listenerIP); err != nil {
return fmt.Errorf("dns intercept: failed to add NRPT catch-all rule: %w", err)
}
logNRPTParentKeyState("post-write")
@@ -2085,12 +2156,18 @@ func gpCatchAllConflictBlocksFallback(state *wfpState, reason string) bool {
// they cause. Tests substitute it to drive a transition - probe outcomes, registry
// state, concurrency - without touching the host's registry or DNS Client.
type nrptOps struct {
probe func(state *wfpState) bool
ruleExists func() bool
addRule func(listenerIP string) error
removeRule func() error
signal func()
findGPRule func(listenerIP string) string
probe func(state *wfpState) bool
ruleExists func() bool
addRule func(listenerIP string) error
// restoreLocalRule re-writes only ctrld's local-store catch-all, leaving the GP
// store exactly as found. See restoreNRPTLocalCatchAllRule.
restoreLocalRule func(listenerIP string) error
removeRule func() error
signal func()
findGPRule func(listenerIP string) string
// dropGPRule deletes ctrld's GP-store catch-all, leaving the administrator's rule
// and the local store alone. See removeCtrldGPRule.
dropGPRule func()
gpRuleMatches func(ruleName, listenerIP string) bool
gpConflicts func(state *wfpState, reason string) bool
loopback func(state *wfpState) error
@@ -2109,20 +2186,22 @@ func (p *prog) nrptOps() nrptOps {
return *nrptOpsForTest
}
return nrptOps{
probe: p.probeNRPT,
ruleExists: nrptCatchAllRuleExists,
addRule: addNRPTCatchAllRule,
removeRule: removeNRPTCatchAllRule,
signal: signalNRPTChange,
findGPRule: findMatchingGPNRPTRule,
gpRuleMatches: gpNRPTRuleMatches,
gpConflicts: p.gpCatchAllConflictBlocksFallbackOps,
loopback: p.activateLoopbackWFPProtect,
wait: p.interceptWait,
flush: flushDNSCache,
parentEmpty: nrptParentKeyEmpty,
cleanParent: cleanEmptyNRPTParent,
startWFP: p.startWFPFilters,
probe: p.probeNRPT,
ruleExists: nrptCatchAllRuleExists,
addRule: addNRPTCatchAllRule,
restoreLocalRule: restoreNRPTLocalCatchAllRule,
removeRule: removeNRPTCatchAllRule,
signal: signalNRPTChange,
findGPRule: findMatchingGPNRPTRule,
dropGPRule: removeCtrldGPRule,
gpRuleMatches: gpNRPTRuleMatches,
gpConflicts: p.gpCatchAllConflictBlocksFallbackOps,
loopback: p.activateLoopbackWFPProtect,
wait: p.interceptWait,
flush: flushDNSCache,
parentEmpty: nrptParentKeyEmpty,
cleanParent: cleanEmptyNRPTParent,
startWFP: p.startWFPFilters,
}
}
@@ -2284,10 +2363,22 @@ func (p *prog) nrptHandbackToExternal(state *wfpState, ruleName, reason string)
}
// The original child is still exact but cannot carry DNS, or external policy is gone
// altogether: ctrld's route is the one that has to come back. This only restores the
// state the transition started from, so it creates no new sibling.
if err := ops.addRule(state.listenerIP); err != nil {
// ctrld's route has to come back. Class decides the writer:
//
// gpChildGone the namespace is free, so the full writer is correct - it may
// place ctrld's GP rule where unrelated GP rules would otherwise
// hide the local store, or drop an empty parent to leave GP mode.
// anything else an external catch-all is still on disk, so only the local key
// may be written. See restoreNRPTLocalCatchAllRule.
//
// The local writer is the default rather than the exception: reaching the full writer
// with any external catch-all present writes the sibling that must never exist, so a
// class added later must opt in to it deliberately.
restore := ops.restoreLocalRule
if class == gpChildGone {
restore = ops.addRule
}
if err := restore(state.listenerIP); err != nil {
mainLog.Load().Error().Err(err).Str("rule", ruleName).
Msg("DNS intercept: handback probe failed and the ctrld NRPT rule could not be restored; the health monitor will retry")
state.setNRPTPolicyOwner(nrptRuleOwnerNone, "")
@@ -2300,7 +2391,7 @@ func (p *prog) nrptHandbackToExternal(state *wfpState, ruleName, reason string)
Msg("DNS intercept: GP-managed catch-all disappeared during the handback probe; restored the ctrld fallback and kept ctrld ownership")
} else {
mainLog.Load().Warn().Str("rule", ruleName).Str("reason", reason).
Msg("DNS intercept: GP-managed catch-all did not carry DNS without ctrld's rule; restored the ctrld fallback and kept ctrld ownership")
Msg("DNS intercept: GP-managed catch-all could not be proved to carry DNS without ctrld's rule; wrote back ctrld's local-store key without writing a GP sibling, and kept ctrld ownership")
}
return nrptHandbackKeptCtrld
}
+443 -51
View File
@@ -28,11 +28,23 @@ type fakeNRPTOps struct {
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
ctrldRule bool // whether ctrld's own catch-all exists in the local store
// ctrldGPRule is whether ctrld's own catch-all also exists in the GP store. The two
// stores are modelled separately because that is where the duplicate lives: the
// production writer adds ctrld's GP rule whenever any other GP rule is present, so a
// fake with a single "ctrld rule exists" flag cannot see a sibling being written
// beside an administrator's catch-all.
ctrldGPRule bool
// otherGPRules models GP rules that are not catch-alls. Production
// otherGPRulesExist is true for any GP rule at all, so the full writer places
// ctrld's GP rule for these too - a shape a gpRule-only fake cannot express.
otherGPRules bool
dropGPCalls int
existsCalls int
addCalls int
restoreLocalCalls int
removeCalls int
signalCalls int
addErr error
removeErr error
@@ -74,7 +86,9 @@ func (f *fakeNRPTOps) ops() *nrptOps {
f.mu.Lock()
defer f.mu.Unlock()
f.existsCalls++
return f.ctrldRule
// nrptCatchAllRuleExists reads the local GUID key and the GP CtrldCatchAll
// key, so a store holding only the sibling still reports "a rule exists".
return f.ctrldRule || f.ctrldGPRule
},
addRule: func(string) error {
f.mu.Lock()
@@ -82,6 +96,9 @@ func (f *fakeNRPTOps) ops() *nrptOps {
f.addCalls++
if err == nil {
f.ctrldRule = true
// Mirror addNRPTCatchAllRule: with any other GP rule present it also
// writes ctrld's GP-path rule, otherwise it cleans the GP path.
f.ctrldGPRule = f.gpRule != "" || f.gpConflict || f.otherGPRules
}
f.mu.Unlock()
if beforeAdd != nil {
@@ -89,6 +106,16 @@ func (f *fakeNRPTOps) ops() *nrptOps {
}
return err
},
restoreLocalRule: func(string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.restoreLocalCalls++
if f.addErr != nil {
return f.addErr
}
f.ctrldRule = true // local store only; the GP store is left as found
return nil
},
removeRule: func() error {
f.mu.Lock()
defer f.mu.Unlock()
@@ -97,6 +124,7 @@ func (f *fakeNRPTOps) ops() *nrptOps {
return f.removeErr
}
f.ctrldRule = false
f.ctrldGPRule = false
return nil
},
signal: func() {
@@ -104,6 +132,12 @@ func (f *fakeNRPTOps) ops() *nrptOps {
defer f.mu.Unlock()
f.signalCalls++
},
dropGPRule: func() {
f.mu.Lock()
defer f.mu.Unlock()
f.dropGPCalls++
f.ctrldGPRule = false // the local store and the administrator's rule are untouched
},
findGPRule: func(string) string {
f.mu.Lock()
defer f.mu.Unlock()
@@ -191,6 +225,37 @@ func (f *fakeNRPTOps) hasCtrldRule() bool {
return f.ctrldRule
}
// hasCtrldGPSibling reports whether ctrld's own rule is sitting in the GP store, which
// beside an administrator's catch-all is the second GP catch-all that an administrator
// did not write.
func (f *fakeNRPTOps) hasCtrldGPSibling() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.ctrldGPRule
}
// writes reports every way this file can put ctrld's catch-all back, so a "nothing was
// written" assertion cannot pass by checking one writer while the other ran.
func (f *fakeNRPTOps) writes() (add, restoreLocal int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.addCalls, f.restoreLocalCalls
}
// dropGPCount is how many times ctrld's GP-store key was deleted on its own.
func (f *fakeNRPTOps) dropGPCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.dropGPCalls
}
// restoreLocalCount is how many times the local-store-only restore ran.
func (f *fakeNRPTOps) restoreLocalCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.restoreLocalCalls
}
// 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.
@@ -215,6 +280,8 @@ type fakeNRPTSeed struct {
gpConflict bool
parentEmpty bool
ctrldRule bool
ctrldGPRule bool
otherGPRules bool
addErr error
removeErr error
}
@@ -229,6 +296,8 @@ func (f *fakeNRPTOps) configure(seed fakeNRPTSeed) {
f.gpConflict = seed.gpConflict
f.parentEmpty = seed.parentEmpty
f.ctrldRule = seed.ctrldRule
f.ctrldGPRule = seed.ctrldGPRule
f.otherGPRules = seed.otherGPRules
f.addErr = seed.addErr
f.removeErr = seed.removeErr
}
@@ -306,7 +375,15 @@ func newHandbackTestProg(t *testing.T) (*prog, *wfpState, *fakeNRPTOps) {
// 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.
//
// How it comes back matters just as much, and is the #576 owned-lab regression: the
// administrator's child is still on disk, so restoring through addNRPTCatchAllRule leaves
// CtrldCatchAll sitting in the GP store beside it - a machine that started with one GP
// catch-all ends the failed handback with two, both naming the same listener. Only the
// local store may be rewritten here.
func TestHandbackRestoresFallbackWhenGPCannotRoute(t *testing.T) {
// The lab fixture: ctrld holds only its local rule, and the GP store holds only the
// administrator's exact catch-all - ctrld has no GP rule to begin with.
f := &fakeNRPTOps{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true}
installFakeNRPTOps(t, f)
@@ -323,12 +400,49 @@ func TestHandbackRestoresFallbackWhenGPCannotRoute(t *testing.T) {
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.restoreLocalCount() != 1 {
t.Errorf("restoreLocalRule calls = %d, want 1: the ctrld fallback must be restored after the probe fails", f.restoreLocalCount())
}
if add != 0 {
t.Errorf("addRule calls = %d, want 0: the full writer adds ctrld's GP rule beside the administrator's child", add)
}
if !f.hasCtrldRule() {
t.Error("ctrld's NRPT rule is missing after a failed handback; the host has no working route")
}
if f.hasCtrldGPSibling() {
t.Error("the failed handback left ctrld's rule in the GP store beside the administrator's catch-all")
}
}
// TestHandbackRestoreDropsAPreexistingGPSibling covers the machine that is already in the
// forbidden state: ctrld wrote its GP rule while only unrelated GP rules existed, and an
// administrator catch-all arrived afterwards. The failed handback must not preserve that
// duplicate on the way out.
//
// It does not, and gets there without a special case. removeRule takes all of ctrld's keys
// for the probe - the local GUID key, the legacy local name and the GP CtrldCatchAll - and
// the local-only write puts back just the first, so the store converges to a single
// catch-all, the administrator's. What is asserted is the shape of the store, not that DNS
// is flowing: this branch exists because the probe failed.
func TestHandbackRestoreDropsAPreexistingGPSibling(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{
probeResults: []bool{true, false},
gpRule: "{GP-RULE}",
ctrldRule: true,
ctrldGPRule: true, // the duplicate this run inherited
})
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "test"); got != nrptHandbackKeptCtrld {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
if f.hasCtrldGPSibling() {
t.Error("the inherited GP sibling survived the handback; the store still holds two catch-alls")
}
if !f.hasCtrldRule() {
t.Error("ctrld's local-store rule was not restored; the transition dropped ctrld's own key instead of putting it back")
}
}
// TestHandbackAcceptsGPWhenItRoutesWithoutCtrld is the other half of the contract: when
@@ -349,8 +463,8 @@ func TestHandbackAcceptsGPWhenItRoutesWithoutCtrld(t *testing.T) {
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 remove != 1 || add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("removeRule = %d, addRule = %d, restoreLocalRule = %d, want 1/0/0: ctrld's rule must be removed and not restored", remove, add, f.restoreLocalCount())
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is still installed beside the adopted GP catch-all")
@@ -381,8 +495,9 @@ func TestIneffectiveGPRuleTriggersNoSignalling(t *testing.T) {
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 add != 0 || remove != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, restoreLocalRule = %d, removeRule = %d, want 0/0/0: external policy must be left untouched",
add, f.restoreLocalCount(), remove)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerGroupPolicy {
t.Errorf("owner = %v, want nrptRuleOwnerGroupPolicy: ctrld must not seize a namespace an administrator owns", owner)
@@ -544,9 +659,9 @@ func TestOwnedRecoveryDefersToIneffectiveExternalPolicy(t *testing.T) {
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 add != 0 || remove != 0 || signal != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, restoreLocalRule = %d, want 0/0/0/0: owned recovery must stop once external policy owns the namespace",
add, remove, signal, f.restoreLocalCount())
}
if f.hasCtrldRule() {
t.Error("owned recovery recreated ctrld's catch-all beside the administrator's rule")
@@ -601,8 +716,12 @@ func TestHandbackRestoresFallbackWhenGPChildDisappearsMidProbe(t *testing.T) {
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)
// A freed namespace is the one case the full writer is still correct for: with no
// external catch-all left to sit beside, it may place ctrld's GP rule where unrelated
// GP rules would otherwise hide the local store, or drop an empty parent to leave GP
// mode. Narrowing this case to a local-only write would leave ctrld invisible there.
if add, remove, _, _ := f.counts(); add != 1 || remove != 1 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, restoreLocalRule = %d, removeRule = %d, want 1/0/1", add, f.restoreLocalCount(), remove)
}
}
@@ -629,8 +748,8 @@ func TestHandbackWritesNoSiblingWhenGPChildTurnsConflicting(t *testing.T) {
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 add, restoreLocal := f.writes(); add != 0 || restoreLocal != 0 {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/0: restoring here writes a competing rule beside administrator policy", add, restoreLocal)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is installed beside a GP catch-all that targets another resolver")
@@ -659,8 +778,8 @@ func TestHandbackWithoutCtrldRuleClassifiesAfterFailedProbe(t *testing.T) {
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)
if add, remove, signal, _ := f.counts(); add != 0 || remove != 0 || signal != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, restoreLocalRule = %d, want 0/0/0/0", add, remove, signal, f.restoreLocalCount())
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerNone || ruleName != "" {
@@ -686,8 +805,8 @@ func TestHandbackGivesReplacementChildItsOwnPass(t *testing.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)
if add, remove, signal, _ := f.counts(); add != 0 || remove != 0 || signal != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, removeRule = %d, signal = %d, restoreLocalRule = %d, want 0/0/0/0", add, remove, signal, f.restoreLocalCount())
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{NEW-GP-RULE}" {
@@ -720,8 +839,9 @@ func TestHandbackWritesNoSiblingWhenReplacementChildTakesOver(t *testing.T) {
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 add, remove, _, _ := f.counts(); add != 0 || remove != 1 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, restoreLocalRule = %d, removeRule = %d, want 0/0/1: ctrld's rule must not be restored beside a replacement catch-all",
add, f.restoreLocalCount(), remove)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is installed beside an administrator catch-all that took the namespace")
@@ -749,8 +869,8 @@ func TestHandbackAdoptsReplacementChildThatRoutes(t *testing.T) {
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)
if add, restoreLocal := f.writes(); add != 0 || restoreLocal != 0 {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/0", add, restoreLocal)
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{NEW-GP-RULE}" {
@@ -788,8 +908,8 @@ func TestHandbackChurnFailsSafeToCurrentExternalOwner(t *testing.T) {
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)
if add, restoreLocal := f.writes(); add != 0 || restoreLocal != 0 {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/0: no sibling write while the GP store is churning", add, restoreLocal)
}
owner, ruleName := state.nrptPolicyOwner()
if owner != nrptRuleOwnerGroupPolicy || ruleName != "{GP-RULE-3}" {
@@ -817,8 +937,8 @@ func TestHandbackChurnFailsSafeToConflict(t *testing.T) {
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 add, _, _, _ := f.counts(); add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, restoreLocalRule = %d, want 0/0", add, f.restoreLocalCount())
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerNone {
t.Errorf("owner = %v, want nrptRuleOwnerNone", owner)
@@ -878,8 +998,8 @@ func TestActivationWritesNoSiblingWhileGPStoreChurns(t *testing.T) {
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 add != 0 || signal != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, signal = %d, restoreLocalRule = %d, want 0/0/0: ctrld must not write beside the administrator's catch-all", add, signal, f.restoreLocalCount())
}
if remove != 0 {
t.Errorf("removeRule calls = %d, want 0: there was no ctrld rule to remove", remove)
@@ -905,9 +1025,9 @@ func TestStopLeavesGPManagedPolicyAlone(t *testing.T) {
}
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 remove != 0 || signal != 0 || add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("removeRule = %d, signal = %d, addRule = %d, restoreLocalRule = %d, want 0/0/0/0: shutdown must not touch externally owned NRPT policy",
remove, signal, add, f.restoreLocalCount())
}
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())
@@ -935,8 +1055,8 @@ func TestStopRemovesOrphanWhileLeavingGPPolicy(t *testing.T) {
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 add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/0", add, f.restoreLocalCount())
}
if f.hasCtrldRule() {
t.Error("the orphaned ctrld rule survived shutdown; a later GP removal would activate it")
@@ -958,8 +1078,8 @@ func TestRemoveOrphanedCtrldNRPTRule(t *testing.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 remove != 1 || signal != 1 || add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("removeRule = %d, signal = %d, addRule = %d, restoreLocalRule = %d, want 1/1/0/0", remove, signal, add, f.restoreLocalCount())
}
if f.hasCtrldRule() {
t.Error("the ctrld rule is still installed")
@@ -974,9 +1094,9 @@ func TestRemoveOrphanedCtrldNRPTRule(t *testing.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)
if remove != 0 || signal != 0 || add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("removeRule = %d, signal = %d, addRule = %d, restoreLocalRule = %d, want 0/0/0/0: nothing to sweep must cost no registry writes and no signalling",
remove, signal, add, f.restoreLocalCount())
}
})
}
@@ -1073,8 +1193,8 @@ func TestHealLadderStopsWhenGPAppearsBeforeTwoPhase(t *testing.T) {
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 add != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/0: the ladder must not recreate ctrld's rule beside a new GP catch-all", add, f.restoreLocalCount())
}
if !f.hasCtrldRule() {
t.Error("ctrld's rule was deleted after an administrator catch-all appeared")
@@ -1100,8 +1220,8 @@ func TestHealLadderCleansEmptyGPParentBeforeRetrying(t *testing.T) {
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)
if add, remove, _, _ := f.counts(); add != 0 || remove != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, removeRule = %d, restoreLocalRule = %d, want 0/0/0: the probe passed after the cleanup, so no recovery was needed", add, remove, f.restoreLocalCount())
}
}
@@ -1185,8 +1305,8 @@ func TestPhaseTwoRevalidatesOwnershipAfterWaitingForTheLock(t *testing.T) {
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 add, restoreLocal := f.writes(); add != 0 || restoreLocal != 0 {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/0: phase two re-added ctrld's rule beside a GP catch-all that took ownership while it waited for the lock", add, restoreLocal)
}
if f.hasCtrldRule() {
t.Error("ctrld's rule is installed beside the administrator's catch-all")
@@ -1267,8 +1387,9 @@ func TestStartupReportsFailureWhenExternalPolicyNeverRoutes(t *testing.T) {
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)
if add, remove, signal, _ := f.counts(); add != 0 || remove != 0 || signal != 0 || f.restoreLocalCount() != 0 {
t.Errorf("addRule = %d, restoreLocalRule = %d, removeRule = %d, signal = %d, want 0/0/0/0: no owned fallback may be written beside an administrator catch-all",
add, f.restoreLocalCount(), remove, signal)
}
// Recovery must stay live so the rule can be re-tested.
@@ -1307,3 +1428,274 @@ func TestFakeNRPTOpsPreconditionDetectsBypass(t *testing.T) {
t.Error("a fake that is not the installed one passed the precondition")
}
}
// startupInterceptMode puts the globals startDNSInterceptLocked reads into dns mode with a
// single loopback listener, and puts them back afterwards.
func startupInterceptMode(t *testing.T) {
t.Helper()
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
}
// TestHandbackAbortsWhenTheLocalWriteFails covers the write failure on the put-back path.
// Nothing else exercises it: every other test lets restoreLocalRule succeed.
//
// The rule is gone by then - removeRule ran before the probe - so a failed put-back leaves
// the host with no ctrld route at all. What the transition owes is an accurate report:
// ownership goes back to none so the health monitor treats it as unowned and retries, and
// no signal is sent for a change that did not happen.
func TestHandbackAbortsWhenTheLocalWriteFails(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{
probeResults: []bool{true, false},
gpRule: "{GP-RULE}",
ctrldRule: true,
addErr: errors.New("registry write failed"),
})
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "local write fails"); got != nrptHandbackAborted {
t.Errorf("nrptHandbackToExternal() = %v, want nrptHandbackAborted", got)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerNone {
t.Errorf("NRPT owner = %v, want nrptRuleOwnerNone: a failed put-back owns nothing", owner)
}
if f.restoreLocalCount() != 1 {
t.Errorf("restoreLocalRule calls = %d, want 1", f.restoreLocalCount())
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0: the failing writer must not be retried through the full writer", add)
}
if f.hasCtrldRule() {
t.Error("ctrld's local rule reads as present after the write that was supposed to create it failed")
}
if f.hasCtrldGPSibling() {
t.Error("a failed put-back left ctrld's rule in the GP store")
}
}
// TestHandbackKeepsCtrldWhenTheStoreReturnsToTheInitialRule covers the spent-budget branch
// of externalAfterRemoval when the churn ends where it began.
//
// The three existing churn tests all end somewhere else - a third rule, a rule targeting
// another resolver, or a free namespace - so none of them reaches this line with the
// original rule name still current. That combination selects the writer: the class is
// gpChildSameExact, the administrator's catch-all is on disk, and only the local key may
// be written.
func TestHandbackKeepsCtrldWhenTheStoreReturnsToTheInitialRule(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{
probeResults: []bool{true, false, false},
gpRule: "{GP-RULE}",
ctrldRule: true,
})
f.onProbe = func(call int) {
switch call {
case 2:
f.setGPRule("{GP-RULE-2}", false)
case 3:
f.setGPRule("{GP-RULE}", false) // back to the rule the transition started on
}
}
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "churn returns to the initial rule"); got != nrptHandbackKeptCtrld {
t.Errorf("nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerCtrld {
t.Errorf("NRPT owner = %v, want nrptRuleOwnerCtrld", owner)
}
if f.restoreLocalCount() != 1 {
t.Errorf("restoreLocalRule calls = %d, want 1", f.restoreLocalCount())
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0: the spent-budget path must not write a sibling either", add)
}
if f.hasCtrldGPSibling() {
t.Error("the spent-budget path left ctrld's rule in the GP store beside the administrator's catch-all")
}
}
// TestGPChildGoneWritesTheGPRuleWhenOtherGPRulesRemain is the counterpart to the
// no-sibling tests: with the administrator's catch-all gone but unrelated GP rules still
// present, the full writer is correct and must place ctrld's GP rule, or DNS Client's GP
// mode hides the local store and ctrld routes nothing.
func TestGPChildGoneWritesTheGPRuleWhenOtherGPRulesRemain(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{
probeResults: []bool{true, false},
gpRule: "{GP-RULE}",
ctrldRule: true,
otherGPRules: true, // unrelated GP policy that keeps DNS Client in GP mode
})
f.onProbe = func(call int) {
if call == 2 {
f.setGPRule("", false) // the administrator's catch-all disappears mid-probe
}
}
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "child gone with other GP rules"); got != nrptHandbackKeptCtrld {
t.Errorf("nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
if add, _, _, _ := f.counts(); add != 1 {
t.Errorf("addRule calls = %d, want 1: a free namespace is what the full writer is for", add)
}
if f.restoreLocalCount() != 0 {
t.Errorf("restoreLocalRule calls = %d, want 0", f.restoreLocalCount())
}
if !f.hasCtrldGPSibling() {
t.Error("ctrld's GP rule is missing while unrelated GP rules keep DNS Client in GP mode; the local store is hidden and nothing routes")
}
}
// TestStartupKeepsCtrldRuleWhenExternalPolicyCannotRoute is the owned-lab regression as
// startup runs it: a service start with ctrld's rule from an earlier run and a matching
// administrator catch-all that cannot carry DNS alone.
//
// The existing startup test has no ctrld rule and a single failing probe, so it never
// reaches the put-back. This one does, and the assertion that matters is which writer ran.
//
// Ownership is deliberately not asserted: startup launches nrptProbeAndHeal, whose own
// handback is throttled and whose undecided branch moves the owner to Group Policy within
// seconds, so any owner assertion here is a race.
func TestStartupKeepsCtrldRuleWhenExternalPolicyCannotRoute(t *testing.T) {
startupInterceptMode(t)
p, _, f := newHandbackTestProg(t)
p.cfg = &cfg
p.dnsInterceptState = nil // startup publishes its own state
f.configure(fakeNRPTSeed{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true})
if err := p.startDNSInterceptLocked(); err != nil {
t.Fatalf("startDNSInterceptLocked() = %v, want nil: ctrld's own route was put back, so startup succeeded", err)
}
if f.restoreLocalCount() != 1 {
t.Errorf("restoreLocalRule calls = %d, want 1: startup must put back only the local key", f.restoreLocalCount())
}
if add, _, _, _ := f.counts(); add != 0 {
t.Errorf("addRule calls = %d, want 0: the full writer would place ctrld's GP rule beside the administrator's", add)
}
if !f.hasCtrldRule() {
t.Error("ctrld's local rule is missing after startup; the host has no route")
}
if f.hasCtrldGPSibling() {
t.Error("startup left ctrld's rule in the GP store beside the administrator's catch-all")
}
_ = p.stopDNSIntercept()
}
// TestStartupDropsAnInheritedGPSibling covers the machine that boots already holding the
// forbidden pair: ctrld's GP rule from an earlier run beside an administrator catch-all.
//
// Adoption used to inherit it. Nothing downstream removes it: every handback from here
// stops at the pre-probe while the administrator's rule stays unproven, the monitor calls
// removeRule only inside a handback, and the orphan sweep runs at stop. The second
// catch-all would therefore stand for the life of the process, through the very outage the
// operator is trying to diagnose.
//
// The GP-key-only case is the one that proves the cleanup is not self-defeating. Dropping
// the sibling empties the store of ctrld keys, so startup has to write its rule after all -
// and the full writer would put the sibling straight back, beside the rule it was just
// removed from.
func TestStartupDropsAnInheritedGPSibling(t *testing.T) {
for _, tc := range []struct {
name string
ctrldRule bool
// wantRestoreLocal is whether startup had to write ctrld's rule itself, which it
// may only ever do into the local store while an administrator rule is present.
wantRestoreLocal int
}{
{name: "local and GP keys", ctrldRule: true, wantRestoreLocal: 0},
{name: "GP key only", ctrldRule: false, wantRestoreLocal: 1},
} {
t.Run(tc.name, func(t *testing.T) {
startupInterceptMode(t)
p, _, f := newHandbackTestProg(t)
p.cfg = &cfg
p.dnsInterceptState = nil
f.configure(fakeNRPTSeed{
probeResults: []bool{false}, // the pre-probe never answers, so the handback decides nothing
gpRule: "{GP-RULE}",
ctrldRule: tc.ctrldRule,
ctrldGPRule: true,
})
err := p.startDNSInterceptLocked()
if f.hasCtrldGPSibling() {
t.Error("startup left ctrld's rule in the GP store beside the administrator's catch-all; nothing later removes it")
}
if f.dropGPCount() != 1 {
t.Errorf("dropGPRule calls = %d, want 1", f.dropGPCount())
}
if add, restoreLocal := f.writes(); add != 0 || restoreLocal != tc.wantRestoreLocal {
t.Errorf("addRule calls = %d, restoreLocalRule calls = %d, want 0/%d: the full writer re-creates the sibling",
add, restoreLocal, tc.wantRestoreLocal)
}
if !f.hasCtrldRule() {
t.Error("no ctrld rule after startup; the host has no route")
}
if err == nil {
_ = p.stopDNSIntercept()
}
})
}
}
// TestHandbackPutBackSettlesToGroupPolicyOwnership pins the registry and ownership state a
// failed handback actually leaves behind, which is what QA will find on the machine.
//
// After the put-back the health monitor keeps running: its handback is throttled, so the
// heal reaches deferToExternalCatchAll undecided and records Group Policy ownership. The
// end state is ctrld's local key on disk, the administrator's rule on disk, no sibling,
// and no further writes - and stop then removes the local key as an orphan.
func TestHandbackPutBackSettlesToGroupPolicyOwnership(t *testing.T) {
p, state, f := newHandbackTestProg(t)
f.configure(fakeNRPTSeed{probeResults: []bool{true, false}, gpRule: "{GP-RULE}", ctrldRule: true})
state.setNRPTPolicyOwner(nrptRuleOwnerCtrld, "")
if got := p.nrptHandbackToExternal(state, "{GP-RULE}", "probe finds no route"); got != nrptHandbackKeptCtrld {
t.Fatalf("nrptHandbackToExternal() = %v, want nrptHandbackKeptCtrld", got)
}
_, _, signalBefore, _ := f.counts()
restoreBefore := f.restoreLocalCount()
p.nrptProbeAndHeal(state)
if owner, _ := state.nrptPolicyOwner(); owner != nrptRuleOwnerGroupPolicy {
t.Errorf("NRPT owner = %v, want nrptRuleOwnerGroupPolicy: the throttled heal ends undecided and defers", owner)
}
addAfter, _, signalAfter, _ := f.counts()
// Absolute, not "unchanged": the whole sequence must never reach the full writer,
// so a first call that had already happened would be a defect too.
if addAfter != 0 {
t.Errorf("addRule calls = %d, want 0: neither the handback nor the heal may write a sibling", addAfter)
}
if signalAfter != signalBefore {
t.Errorf("signal calls = %d, want %d: nothing changed, so nothing is signalled", signalAfter, signalBefore)
}
if f.restoreLocalCount() != restoreBefore {
t.Errorf("restoreLocalRule calls = %d, want %d: the rule is already back", f.restoreLocalCount(), restoreBefore)
}
if !f.hasCtrldRule() {
t.Error("ctrld's local key is missing; the put-back did not survive the heal")
}
if f.hasCtrldGPSibling() {
t.Error("the heal wrote ctrld's rule into the GP store")
}
_, removeBefore, _, _ := f.counts()
_ = p.stopDNSIntercept()
if _, removeAfter, _, _ := f.counts(); removeAfter != removeBefore+1 {
t.Errorf("removeRule calls = %d, want %d: stop must sweep the local key it left on disk", removeAfter, removeBefore+1)
}
if f.hasCtrldRule() {
t.Error("stop left ctrld's local key behind")
}
}