Skip to content

The Firewalla stops being a DNS server: Unbound off and its fallback upstream moved to the resolver pair through the WAN DNS field — the per-network Primary DNS repoint rejected because it is a DHCPv4 option with no write surface on nine of eleven legs, a public secondary rejected because it imports a second authority for the same names, and every appliance reading recorded once with a date rather than gated

Date: 2026-09-09 Status: Accepted Deciders: Sean Brandt

A note on dates before anything else, because this record mixes two clocks. This ADR is authored on 2026-09-09 local (EDT). Several readings it cites are stamped 2026-09-10 UTC — the Unbound toggle landed at Sep 9 22:39 EDT, which is 2026-09-10 02:39 UTC. Both stamps describe the same evening. Where a reading is quoted with a date, that date is the one the recording plan wrote, and the runbook record it points at carries the same stamp.

The Firewalla Gold has been this estate’s DNS server. Phases 01–03 built a resolver pair (sandiego/portland, Blocky in front of PowerDNS, floating on 192.168.20.5) and moved DHCP to Kea; this phase takes the appliance out of the query path. Six facts govern what that exit could be, and a reader arriving cold will not have any of them.

THE APPLIANCE WAS NEVER THE CLIENT-FACING RESOLVER SOURCE, and discovering that changed the size of the whole problem. ansible/roles/kea/templates/kea-dhcp4.conf.j2:723-727 sets DHCP option 6 domain-name-servers to {{ leg.vip }} with always-send, defaulted true at ansible/roles/kea/defaults/main.yml:425. Kea serves the nine legs, so every DHCP-leased client on them was already being handed its own leg VIP and never 192.168.20.1. The per-network DNS value on the appliance governs only where the appliance itself forwards queries that arrive at it. The largest population the exit was feared for was never in the blast radius.

NINE OF THE ELEVEN INTERFACE KEYS HAD NO FIELD TO SET. FireRouter’s config.dns payload carries eleven interface keys, nine of them reading useNameserversFromWAN: true. Measured 2026-09-09 (04-03): the per-network Primary DNS Server is a DHCPv4 option, the DHCPv4 form is its only write surface, and that form is not rendered on a leg whose DHCPv4 server is off. config.dhcp holds exactly two keysbond0.3100 (Guest) and bond0.3200 (Telework) — which are exactly the two legs carrying a real nameservers value and exactly the two Phase 03 was able to set. The operator worked the nine legs in the app’s Edit view and found no DNS section on any of them. There is no Secondary field either: ABSENT, not PRESENT-BUT-EMPTY. See docs/operations/dns.md § The appliance leaves the DNS path — the 04-03 capture, and the SUPERSEDED 2026-09-09 (04-04) block on step 2.

EIGHT OF THOSE NINE LEGS SHARE ONE RESOLVER FILE, AND THE WAN DNS FIELD IS WHAT WRITES IT. Measured 2026-09-09 (04-RESEARCH F-4, re-read by 04-04): eight of the nine WAN-pointed legs’ dnsmasq resolv-file is a symlink to a single file, /home/pi/.router/run/eth1.resolv.conf, which FireRouter writes from interface.phy.eth1.nameservers — the WAN Primary/Secondary DNS field, which the operator confirmed on screen is editable. /etc/resolv.conf chains onto the same file through default.resolv.conf. So the impossible per-network repoint and the planned APPL-03 change were the same write, reached one layer down. One editable field did what nine missing ones could not, and it also moved the appliance’s own resolution, which the per-network repoint would not have done. Guest and Telework carry real files rather than symlinks, so their per-leg VIPs survive by construction rather than by anyone remembering to preserve them. tun_fwvpn has a config.dns key, no materialised file and no dnsmasq process, so no write of any kind reaches it.

THE WAN DNS VALUES WERE HAND-SET, NOT ISP-LEARNED. Measured 2026-09-09 (04-04, Q5): the live Verizon FiOS lease on eth1 hands option domain-name-servers 71.252.0.12,71.242.0.12 and the TMobile lease on eth2 hands 192.168.12.1; both materialised files read 9.9.9.9 / 1.1.1.1. Two different ISPs, two different offered pairs, one identical materialised pair — and FireRouter’s applyDnsSettings() branches on a non-empty nameservers array with the vendor’s own comment saying specified nameservers supersede those assigned by DHCP. The 9.9.9.9/1.1.1.1 pair was a hand-set override with a real before-value, which is the opposite of what the phase’s own locked decision D-09 asserted.

THE WAN HEALTH CHECK digs AT WHATEVER THE WAN DNS NAMES, AND A FAILED DNS TEST MARKS THE WAN INACTIVE. Read from this box’s firmware, 2026-09-09 (04-05): :1668 gates the test behind if (active && dnsTestEnabled) and :1677 then sets active = false, so the verdict is carrier AND ping AND dns and the ping test cannot rescue a WAN whose DNS test failed. The test cannot be disabled from the app. dnsTestEnabled is the only off switch (:1582, hasOwnProperty(...) ? ... : true — absence is not off) and no app control exposes it; clearing dnsTestDomain falls through validator.isFQDN to defaultDnsTestDomain = "github.com" (sensors/wan_conn_check_sensor.js:80). Clearing the nameserver list entirely marks the WAN inactive by itself (:1683-1686). That is why pointing the WAN DNS at the pair makes the pair a dependency of the appliance’s internet-up determination, and why that could not be avoided by a setting.

THE APPLIANCE PUBLISHES NAMES NOBODY DECLARED, AND THE SUFFIX IT PUBLISHES THEM UNDER IS NOT WHAT THIS PAGE SAID SINCE JULY. Measured 2026-09-10 (04-08): local:domain:suffix is wireless.lan.fzymgc.house, not the fzymgc.house apex the runbook had asserted since July. 115 per-MAC files carry 269 lines carrying 97 distinct names, and zero of the 97 sit directly in the apex — all 97 sit under the three-label subdomain. The apex-pollution shape the page described is not the current state; it is one redis-cli set away from being the current state, and that threshold is recorded in both directions rather than as reassurance. local:domain:no_forward — the landmine that writes server-uhigh=/<suffix>/127.0.0.1#33333 and blackholes the whole suffix in front of unbound — measured unset, corroborated by zero server-uhigh hits across the appliance’s three dnsmasq trees.

The appliance stops resolving .house for anyone, including itself, and becomes a forwarder to the resolver pair. Performed in this order, and the order is the whole mitigation:

  1. Repoint every appliance-side consumer at 192.168.20.5 and recreate its container. Four Ansible-managed pins (ansible/roles/router-vault-unseal/defaults/main.yml:58, ansible/roles/router-tailscale/defaults/main.yml:54, ansible/roles/otel-collector/defaults/main.yml:269 consumed at both dns: sites), held by an offline render gate confirmed RED before GREEN.
  2. Repoint the appliance’s fallback upstream at the resolver pair — setting (b), the WAN Primary/Secondary DNS, on BOTH WANs. Set on both or a failover reverts it silently.
  3. Repoint the k3s CoreDNS fzymgc.house forward (argocd/app-configs/coredns-custom/coredns-custom.yaml:63) as a GitOps commit.
  4. Turn Unbound — setting (d) — off. The only appliance feature this procedure switches off. The redirect chain is untouched and nothing is deleted from the box.
  5. Read the box’s own resolution back from an SSH session on it, expecting a change from not-working to working.

Steps 1–3 are each independently safe and independently reversible. Step 4 is the one that must not run early, and the rule is stated here in the same words the runbook uses so that one procedure is not described two ways by two documents edited apart: step 4 must not run before step 2.

Step 2’s CONTENT was replaced and its POSITION and OBLIGATION were not, which is why that rule survived the correction unchanged. The step that originally stood at position 2 instructed a per-network repoint of setting (a) that is structurally unperformable; its successor — the WAN lever — discharges the identical obligation, making the appliance’s fallback upstream the resolver pair before Unbound stops overriding every leg. If step 4 runs first, dnsmasq’s fallback on every WAN-pointed leg is Quad9, which knows no internal name, so every .house name dies at the appliance in one move — and the box’s own telemetry collector resolves otel-gateway.fzymgc.house through that same chain, so the box goes dark at the same instant. There is no alarm that would catch it faster than the ordering prevents it.

The result, measured rather than inferred. sandiego.fzymgc.house at 192.168.20.1 went from NOERROR / ANSWER: 0 to the pair’s five A records exactly (192.168.20.6, .24.6, .40.6, .200.6, .218.6) — a set no local tier, no cache of a public answer and no redirect can manufacture. The public control example.com was unchanged at ANSWER: 2 in the same breath. Unbound off is evidenced positively: ~/.firewalla/config/dnsmasq/unbound.conf is gone, because UnboundPlugin unlinks it rather than commenting it out, and the absence was asserted only after a positive control that ran first (366 .conf files still listing, both sibling files still present). The mechanism is visible on disk: unbound_system.conf survived and still carries mac-address-tag=%FF:FF:FF:FF:FF:FF$unbound, but the server= line that tag selects no longer exists. The tag still selects; there is nothing left to select.

The error shape this phase produced, and it is worth more than the change

Section titled “The error shape this phase produced, and it is worth more than the change”

A true measurement about STORAGE was read as if it were a measurement about EDITABILITY. A data model was read as a UI contract.

The measurement was real: config.dns.<iface> persists after a leg’s DHCPv4 server goes off, and the API returns eleven interface keys. The inference was not: that eleven keys are eleven settable fields. Nine of them are inert defaults no screen exposes. Only a write attempt could have found this, and this phase found it by attempting the write and failing — after building a plan, a wave and a safety argument on top of it.

The rule it yields. Before planning a write against a value read from an API, establish the write surface separately: which screen, which form, and whether that form renders on this object. A read proves a value exists in a store. It proves nothing about whether anything can change it.

The warning sign, so it is recognisable next time. A plan whose step reads set field X on N objects, where N was counted from a read. The count is the tell. It is arithmetic over a store presented as a work estimate over a surface, and the two are different populations that happen to agree when a system’s data model and its UI are the same shape — which is exactly the assumption that is never stated because nobody notices making it.

The same shape in a second disguise, because one instance reads as bad luck

Section titled “The same shape in a second disguise, because one instance reads as bad luck”

Locked decision D-09 ruled APPL-03 confirm-only: “WAN addressing on this appliance is DHCP, so the WAN Primary/Secondary DNS are DHCP-derived and there is nothing to configure and no manually-set before-value to capture.” Measured false three ways (Context, fourth paragraph).

"dhcp": true in that object describes WAN addressing. WAN DNS is a separate field in the same object. The property of one field was read across onto another because the data model presents them together — which is the storage-versus-editability error wearing different clothes: a structural adjacency in a payload read as a semantic one.

And a third, which is the cleanest of them

Section titled “And a third, which is the cleanest of them”

The suffix correction in 04-08 is the same failure at documentation scale. docs/operations/dns.md asserted local:domain:suffix was fzymgc.house and had asserted it since July; nobody re-read the key. The correction does not adjust a number — it changes what the whole writers section means, because “the appliance pollutes the apex with names nobody declared” and “the appliance publishes 97 names under a three-label subdomain that is not a delegated zone” are different findings with different consequences. Its companion figure went stale in its denominator rather than its numerator: the 114 MAC files against 85 DHCP leases comparison re-measured as 115 against 11, because the legs left the appliance for Kea. That cell was wrong for roughly six weeks and would have told a triage reader the two writers were within 35% of each other when the moving half was off by a factor of ten.

Two independent methods converged here, and the convergence is worth more than either reading alone. 04-07 settled the provenance of the appliance’s local .house answers by observing what stopped answering when Unbound went off: grafana.fzymgc.house and mcp-gw.fzymgc.house both went NXDOMAIN at the instant the tagged upstream vanished, so both were served through Unbound rather than from a local addn-hosts tier. 04-08 reached the same conclusion from the other direction and without looking for it: both are apex names (<name>.fzymgc.house) and the auto-publishing writer publishes under wireless.lan.fzymgc.house, so neither could have come from that writer. Two methods, no shared assumption, same answer.

The through-line across all three, stated as an observation with its evidence rather than as a maxim. Every prediction this phase made about the appliance’s internals failed — the per-network field’s editability, the WAN DNS’s provenance, the DNS test’s disableability, the published suffix, the aa flag’s meaning. Every prediction it made about our own configuration held — the Kea option-6 value, the CoreDNS forward, the container pins, the render gate, the resolver pair’s answers. The asymmetry is not luck: our own configuration is in the repository and can be read before it is asserted, and the appliance’s is not.

The identity decision: both WAN slots name members of one entity

Section titled “The identity decision: both WAN slots name members of one entity”

Recorded rather than re-derived; the full argument is 04-05-PLAN.md’s <assumption_delta_decision> block. The WAN DNS field takes an ordered array, and the decision was which entity the two slots name. Both name members of the resolver pair — Primary 192.168.20.5 (the floating identity), Secondary 192.168.20.6 (sandiego, a named member of that same entity).

The rejected alternative was Primary 192.168.20.5, Secondary 9.9.9.9, and it was rejected on three grounds, not on outage arithmetic:

  • It imports a second authority with contradictory semantics for the same names. fzymgc.house is a real Cloudflare-hosted public zone; it answers those names NODATA (measured over DoH, 2026-09-09). Both answers are definitive, both are cacheable, and dnsmasq caches whichever it gets.
  • It bypasses a control this estate deliberately built. Blocky’s per-client view withholds the apex from untrusted legs — the subject of 2026-08-28-blocky-per-client-view-withholds-the-whole-apex. A second upstream answering .house outside that view is an access-control regression wearing the costume of resilience.
  • It fires on a transient blip — a VRRP failover, a Blocky restart, one dropped packet past the timeout — and the result is cached and served to every client on that leg for the negative TTL. A SERVFAIL or a timeout would have been strictly better, because neither is cacheable as a fact about the name.

The consequence of the choice is ACCEPTED COST #1, not a new cost. With both slots naming the pair and the DNS connectivity test undisableable, a whole-pair outage can make the appliance conclude both WANs are inactive. During a whole-pair outage the estate has no DNS regardless of what the appliance concludes about its WANs. What this decision added is the reach of a ruling made 2026-08-28, not a new risk.

wg0 and wg_ap: chosen, then measured forced, then chosen again through a different control

Section titled “wg0 and wg_ap: chosen, then measured forced, then chosen again through a different control”

Recording only the endpoint loses the lesson; recording only the middle leaves a false statement in the file. The arc is three steps and it needs all three.

  1. CHOSEN. The milestone accepted the cost of VPN clients losing the apex, deliberately, and planned to preserve it by repointing those two legs’ per-network DNS. A decision priced in advance.
  2. MEASURED FORCED. 04-03 worked the app and found no field exists to repoint a VPN leg’s DNS — the per-network Primary DNS is a DHCPv4 option and these legs run no appliance DHCP server. On the evidence available then, the loss was correctly recorded as forced: made by the appliance’s write surface, not by a decision this project took.
  3. CHOSEN AGAIN, THROUGH A DIFFERENT CONTROL. Both legs read the WAN resolver file, which 04-05’s lever writes. Measured after the toggle: both went from *** Can't find sandiego.fzymgc.house: No answer to the pair’s five A records, and both kept the apex (id.fzymgc.house192.168.20.145). The outcome the milestone wanted is reached — not through the field that does not exist, but one layer down at the layer that is actually writable.

The outcome returns from FORCED to CHOSEN. Anyone reading step 2 alone would carry a false statement about this estate; anyone reading step 3 alone would not know that the appliance’s write surface can force a milestone-level outcome and that the recovery came from looking one layer down rather than from a workaround.

The refutation of the on-box belief that host networking makes Docker ignore dns:

Section titled “The refutation of the on-box belief that host networking makes Docker ignore dns:”

An unmanaged container’s resolver file on this appliance carries the comment “Required because network_mode: host ignores the docker-compose dns: directive”. Measured false on this box, 2026-09-09 (04-01, 04-02). Every managed container’s own generated resolver file — read at ResolvConfPath, not the host’s /etc/resolv.conf — carries Docker Engine’s # Overrides: [nameservers] marker, which is Docker stating in its own output that the dns: key won. Host networking does not suppress it.

The same measurement supplied a positive discriminator for the unmanaged case: vector’s resolver file carries no Overrides marker where every other container’s does, which is what proves its pin arrives through a bind mount rather than a compose key. An absence is only evidence when something else present makes it one.

The refuted belief still sits in a comment on the appliance, and there is no repository file to correct — the role that wrote it was retired. That is precisely why it belongs in a document a future reader will find, rather than only in a commit that fixes something.

Reproduced as the split it is rather than flattened into one confidence.

Measured. The fall-back verdict, on a five-record answer only the pair can produce. Unbound off, positively, by an unlinked file with a 366-file positive control. The mechanism, from a surviving tag whose target is gone. The appliance’s own resolution, broken → working. The Guest leak closed (id.fzymgc.house at 192.168.79.1: 192.168.20.145 → NXDOMAIN, matching the Guest VIP exactly). Four container pins present and still resolving, both halves read, because a pin that is present and no longer resolving is exactly the failure the toggle can produce. DNS Booster off for sandiego and portland, from configuration and from enforcement independently, each behind its own read control.

Inferred, and left inferred. That what wg0/wg_ap clients received before the toggle came from the appliance’s local tiers rather than from Unbound recursing. It is marked INFERRED on the runbook and was not upgraded. Being wrong about it changes nothing already decided: the post-toggle behaviour is measured on both legs, and this claim is about a state that no longer exists.

Recorded PROVENANCE UNKNOWN, in those words. DNS Booster is off for both devices — but the appliance runs a background loop-detector that can turn it off with no operator having touched it, and nothing in either half of the reading distinguishes a value the appliance set from a value a person set. Off because someone decided and off because firmware decided are different facts and only one is durable. APPL-04 asks that it be off for these two devices as devices, and it is; the ADR does not claim more.

An instrument that failed, kept because the failure is the finding. The aa flag was proposed as a provenance discriminator — an authoritative answer implying a local tier. It failed twice: aa was set on an answer that demonstrably came from the pair by fall-back, and it was not stable across two reads of the same name at the same server 71 seconds apart. A flag that differs between two identical reads discriminates nothing. What replaced it is stronger: provenance settled by what stopped answering.

And an error in classification, kept for the same reason. mcp-gw.fzymgc.house went NXDOMAIN estate-wide at the toggle, was classified a REGRESSION, and the tail was halted. The halt was correct — the check was named in advance and it fired. The classification was falsified the same day: no configured MCP client references the name; tf/keycloak/mcp_kubernetes.tf uses it as an OIDC included_custom_audience, an identifier compared inside a token claim and never resolved; and the two tools/litellm-verify/ references name it as the surface being migrated away from. The lesson, stated plainly: a grep for a name is not a measurement of a dependency on it. References were counted as consumers without asking what each one does with the string. mcp-gw sits in the same class as grafana — a name with no declared source whose consumers have already left — and its disappearance is this milestone’s intended outcome. The WINDOWS entry was closed as FALSIFIED, not as fixed and not deleted.

The residues carried out of this phase, each with an owner

Section titled “The residues carried out of this phase, each with an owner”
  • router-hosts-vault-agent’s certificate. Still pinned at 192.168.20.1, needs vault.fzymgc.house to renew its mTLS material, hard expiry 2026-09-26, and the renewal path has no alarm — a stalled renewal looks exactly like a healthy one until something fails to connect. Deferred to Phase 06, and the deferral is defensible by non-occurrence: because step 2 ran before step 4, the container never lost .house resolution at any point, measured directly in the post-toggle container readback. That defensibility is conditional, and the condition is the ordering — a later reader who finds the ordering broken should know the acceptance no longer holds. No detector was built.
  • vector’s aggregator failure. Unrelated to DNS and deliberately unrepaired. Its entire configuration names no DNS name at all (API bind, cAdvisor scrape, aggregator sink — all address literals over 45 lines), so its DNS residue is closed by measurement: it could resolve nothing and behave identically. What measurement uncovered instead is that it is Up 4 weeks (healthy) and failing continuously against 192.168.20.155:6000 — 37 retry warnings in ten minutes with No route to host. The sink is an IP literal and the error is routing, not resolution. Filed to the retirement window; a phase about the DNS path must not acquire a telemetry repair.
  • The appliance-published names with no declared source. 97 names under wireless.lan.fzymgc.house, plus grafana.fzymgc.house and mcp-gw.fzymgc.house measured from the other direction. wireless.lan.fzymgc.house is not a delegated subdomain in the authoritative tier. Flagged to Phase 10 as a CLOSE-03 input, not solved here.
  • The three name-registration levers, documented and unpulled under D-10: the local_domain feature flag, the local:domain:suffix value, and per-network searchDomains/localDomains. Pulling a lever against unmanaged vendor state to change nothing observable is cost with no benefit now that clients point at the pair. local:domain:no_forward is documented immediately beside them rather than in a footnote, because a reader who has just been told the suffix key is adjustable is the reader most likely to reach for the key next to it.
  • Three agentgateway *-gw listener hostnames NXDOMAIN on the pair (mcp-gw, llm-gw, openrouter-gw), published by a HostMapping CR rather than by external-dns’s gateway-httproute source. Flagged to whoever owns the agentgateway retirement. No detector, no assertion, no standing check.
  • lan.fzymgc.house and service.fzymgc.house — two of the 21 names declared in tf/router-hosts/hosts.tf that resolve on neither resolver. Search-domain constructs, not records anyone resolves. Flagged to Phase 07, which owns tf/router-hosts/ retirement.

One ordering deviation from research, recorded rather than left to be inferred

Section titled “One ordering deviation from research, recorded rather than left to be inferred”

04-RESEARCH Open Question 1 recommends running the per-device Unbound experiment as the first task of the tail, before the WAN lever, so a negative answer costs nothing. The phase ran it after — and then retired it entirely.

Its first stated justification was measured wrong by this phase’s own baseline and is withdrawn rather than quietly replaced. That justification claimed the experiment would be unreadable pre-lever because the appliance answers .house from a local tier in front of any upstream. True of grafana.fzymgc.house; not true of the discriminator the phase actually adopted, because 04-04 recorded 192.168.20.1 returning zero records for sandiego.fzymgc.house. A deviation whose stated reason a later reader can falsify from the same page is worse than no deviation note at all, which is why the withdrawn reason is named as withdrawn.

The surviving justification is evidential rather than about risk. Pre-lever, a passing reading would have been byte-identical to 04-04’s pre-experiment baseline at the same gateway: nothing in the reply would distinguish “the scope change applied and dnsmasq fell back” from “the tap did not take effect”. The experiment would have had no positive control. Post-lever the pair’s five-record answer appears only if the scope change applied and dnsmasq fell back and the consulted file names the pair — one reading, three facts. Both orderings are defensible; this one was chosen on evidential strength, not on risk.

And then the experiment was retired without running at all, on a measurement taken after it was planned: DHCP clients were never appliance consumers, the k3s cluster had already been repointed by 04-02, and the pair already answered 19 of the 21 curated names. The asymmetry the experiment was built on — one device’s DNS for a few minutes versus every appliance-served client’s — had collapsed. No fall-back-versus-blackhole verdict was asserted from it, and none was inferred from surrounding evidence. 04-07’s toggle answered the question directly, in seconds, which vindicates the retirement.

Two honest non-results from the same tail, recorded as non-results. The local_domain feature flag’s redis home was not established — hget sys:features local_domain returned empty, which with 115 live per-MAC files means the lookup was wrong, not that the feature is off; recording “measured OFF” would have been a plausible number from a wrong query. And one plan’s verify literal was not satisfiable as written, because the page’s heading carries backticks the literal omitted; the property it protected was verified with the page’s real string instead, and the substitution was recorded rather than performed silently.

The Firewalla is third-party state this repository does not ship. It appears in none of our three IaC layers. Under the workstream’s standing constraint, the behaviour of hardware and software we do not ship may be measured once and recorded with a date, and must not become an assertion this repository keeps green. Every figure in this ADR and in the runbook records it points at is a dated reading, not a promise. There is no gate over any of it and there will not be one. No file was added under tools/dns-role-gates/tests/ by any plan in this phase; the two counted-gate constants (EXPECTED_SECTION_COUNT = 33, EXPECTED_SECTION_ANCHOR_COUNT = 267) assert what this repository’s own runbook says, never what the appliance does.

The apex threshold above is the sharpest case, and it is stated here precisely because it is prose. local:domain:suffix sits at a three-label subdomain today, so the apex is clean; one redis-cli set moves 97 names into the apex and the pollution shape returns. That is a hazard a reader should be able to recognise. It is not a thing to detect on a schedule against a box we do not own.

  • Accepted. The resolver pair is a single point of failure for the whole estate (accepted cost #1, ruled 2026-08-28), and that ruling now also reaches the appliance’s internet-up determination, because the WAN DNS connectivity test cannot be disabled from the app.
  • Accepted. Names the appliance published from its own tiers stop answering. That is the milestone’s intended outcome, not a regression; grafana was confirmed unused and mcp-gw’s references were measured to be non-resolving.
  • Gained, and not planned for. The Guest apex leak closed as a side effect: 192.168.79.1 now refuses id.fzymgc.house exactly as the Guest VIP does, because Guest carries a real resolver file naming its own leg VIP, so once the broadcast-MAC tag stopped selecting anything its dnsmasq fell through to the Guest view.
  • Unchanged. The redirect chain still holds its 18 DNAT rules. Nothing was deleted from the appliance. Every step is reversible in one field or one toggle, with the before-values captured by field name on the runbook.