Thanks for the three commits and the explicit clarification of their scope. I retested 71b70211daf9ebe25500f64d8f8634be32fd2c3d with four real Linux matrices, nine scenarios each. This follow-up focuses on functional behavior rather than style.
Confirmed fixes
- No
dns section, an empty DNS configuration, and the gateway-source-IP → blackhole case now correctly refuse takeover while system resolution remains usable. - The dirty-state fix works: real private-bus ACL denials cause another revert attempt at shutdown. After restoring the private policy, I captured successful Revert method-return replies, correlated by caller and serial—not merely the absence of an error log. With permission still denied, the retry receives an error and the per-link state disappears with the TUN on exit.
Please keep the small app/dns accessor; covering the empty-server case is justified. The real-router tests are also useful, CI-friendly regression coverage. They test decisions rather than DNS responses, but that does not negate their value.
New counterexample: mixed independent and system-resolver upstreams
Starting from the working independent-upstream configuration, add this DNS server:
{
"address": "localhost",
"domains": ["full:example.com"],
"finalQuery": true,
"skipFallback": true
}
The takeover is accepted, but example.com times out in 4/4 runs. Queries reach the DNS outbound, and logs show lookups returning to the private system stub and timing out. An unrelated domain resolves successfully before and after the failing query in the same instance.
UsesSystemResolver() returns false as soon as any upstream is non-local, whereas the domain-specific selection can still choose only localhost. This is an additional configuration case, not a claim that your no/empty-DNS fixes failed.
A conservative fix would refuse takeover when a selectable resolution path may depend on the system resolver, rather than checking whether all clients do. A MayUseSystemResolver-style check and a mixed-server regression case would make that distinction explicit; do not silently remove the user's local resolver.
Source ports: the limitation you already disclosed
I acknowledge your statement that the representative source port cannot predict the real query's port. The live consequence is reproducible: with source port 49152 → DNS and remaining TUN/53 traffic → blackhole, preflight accepts takeover; real queries from 49152 answer, while 49153 and the system resolver's actual ephemeral ports fail, in all four runs.
Could the README explicitly require a source-port-independent DNS path and clarify whether unsupported configurations should be refused? We should avoid presenting the synthetic check as a guarantee for arbitrary routing rules. I am not asking for a general Router redesign in this PR.
Scope and evidence limits
Deferring godbus is fine. Fixture duplication and missing full TUN E2E coverage are not additional observed functional failures. The remaining .Base(err).Base(cause) overwrite is a diagnostic improvement, not evidence that cleanup is skipped.
The real TUN/gVisor, DNS and resolvectl path ran inside private mount/PID namespaces with a private bus/resolved. This does not establish normal-host authorization or general leak coverage. Four seconds is the client's timeout limit.
One supplementary rollback-recovery run also had a DNS timeout before policy restoration; Revert succeeded and the next identical run answered normally. That failure is retained and unexplained—not attributed to rollback or dismissed as proven network noise.
All 36 Core instances exited 0, their TUNs disappeared automatically, and all four host-preservation audits passed.