# 2026-07-16 -- D-125: Model B per-DC ISP egress (bridge-in, single-NAT) -- resolves OBS-3's design gap; egress efficacy is a deploy-time gate (present-only)

## Context

Post-commit `5175365` (Stage-3 sweep C+D), the adversarial re-run + advisor confirmed **OBS-3** as
material once the operator ruled **"DC0 and DC1 have their own ISP connections."** Model B nesting had
severed the per-DC simulated-ISP egress: `modules/site-wan` makes `vr1-dc0-wan` a libvirt NAT that
egresses via ITS HOST's default route; under Model B that host is `vvr1-dc0` (transit-only, East-West,
SEC-010 FORWARD-dropped), so the inner NAT had nowhere to go. Operator ruled **bridge-in (single-NAT)**
over the double-NAT alternative. Governing record: **D-125** (cross-ref D-122/D-123). NO cloud mutation;
authored present-only; both roots validate. **NOT applied, NOT committed, NOT pushed.**

## What changed

**New module `opentofu/modules/wan-bridge/` (versions/variables/main.tf/outputs.tf).** A libvirt
`forward = { mode = "bridge" }` network bound to a pre-existing host bridge (`bridge = { name = ... }`),
no subnet/NAT. Sibling to `site-wan` (chosen over a `site-wan` `mode` flag so every existing NAT user --
`office1-wan`, the new `vr1-dc0-uplink` -- stays literally untouched; noted vs. the option preview's
"mode param" wording). Schema (`bridge` is an attribute-OBJECT, not a string) measured from the installed
provider, not inferred. Covered by `opentofu-validate` S3 (standalone `[PASS]`).

**OUTER root (`opentofu/main.tf` + `variables.tf`, vcloud).**
- New `module "vr1_dc0_uplink"` (`site-wan` NAT) = the SINGLE ISP NAT at vcloud level (egresses via
  vcloud's uplink). CIDR = `172.30.2.0/24` inlined -- the SAME /24 D-115 already assigned vr1-dc0's edge
  WAN (bridge-in is single-NAT; RULED, not HELD). [CORRECTED 2026-07-16: an earlier draft added a HELD
  `var.vr1_dc0_uplink_cidr` + a NetBox assignment; both removed -- D-125 adds no new NetBox object.]
- `vvr1_dc0` gains a 2nd NIC (order `[transit, uplink]` -- transit stays first so SEC-010/`--transit-if`
  keep keying on it). Netplan now declares the IP-less `uplink` port + the `br-vr1-dc0-wan` bridge
  (STP off), so the bridge exists before the inner apply. `tofu validate` Success.

**INNER root (`opentofu/vr1-dc0-substrate/main.tf`).** `module "vr1_dc0_wan"` switched
`site-wan` (NAT, `172.30.2.0/24`) -> `wan-bridge` bound to `br-vr1-dc0-wan`. OPNsense `wan_network_name`
wiring unchanged (same output). The `/24` and the OPNsense WAN `.2` are UNCHANGED (same 172.30.2.0/24,
D-115); only the libvirt realization moved to the vcloud NAT. `tofu validate` Success.

**Bootstrap (`scripts/site-headend-install.sh` node-host mode).**
- New `--uplink-if` (default `uplink`) + `--wan-bridge` (default `br-vr1-dc0-wan`).
- `node_host_check`: WAN-egress guard -- verifies the bridge exists AND the uplink is ENSLAVED (`master
  <bridge>`); same fail-open class as the SEC-010 transit-iface check. Prints the DEPLOY-TIME isolation
  gate (throwaway guest on the bridge must get a vcloud-ISP addr + ping out; FAIL => double-NAT fallback).
- `node_host_setup`: SEC-010 rationale corrected (bridge-in has no inner NAT, so the old ip_forward
  forcing no longer applies); **br_netfilter CONSTRAINT** -- keep the FORWARD-drop interface-scoped,
  never global. Verifies the WAN bridge landed (does NOT create it; netplan owns L2). Harness 49 -> 54.

**Records.** D-125 (new); `security-ledger` SEC-010 (bridge-in + br_netfilter constraint + hardened
`--check`); `model-a-fallback-plan` B->A delta item 8 + revert step + OPNsense WAN restore; `session-ledger`
(commit `5175365` + OBS-3 RESOLVED-by-D-125 + new HELD gates); `references/platform-traps.md` 1g (bridge
+ br_netfilter + nested-foreign-MAC trap).

## Verification

- `tofu validate` OUTER = Success; INNER = Success; `opentofu-validate` S3 `[PASS] modules/wan-bridge`
  (+ `site-wan` still `[PASS]`, untouched).
- `tests/site-headend-install` = 54/54.
- `repo-lint` = 0 fail (1 documented legacy WARN). All edited files ASCII/LF.

## Deploy-time gates opened (NOT closed by this change)

1. **L2-NAT of foreign MAC across the nested bridge** -- unprovable pre-apply (bridge-in's L4-boot-gate
   equivalent). Isolation-test first; FAIL => revert to double-NAT (D-125).
2. Confirm the `uplink`/`br-vr1-dc0-wan` interface names on first boot (enp2s0-vs-ensX naming trap).

(No new HELD addressing: the `/24` is the ruled `172.30.2.0/24` and the OPNsense WAN stays `.2`; D-125
adds no NetBox object. The only NetBox work for the DC substrate is D-124's transit /30 + rack IP.)

## Revert (per item)

- `git rm -r opentofu/modules/wan-bridge`; revert `opentofu/main.tf` (drop `vr1_dc0_uplink` + the 2nd
  NIC/netplan bridge); revert `opentofu/vr1-dc0-substrate/main.tf` `vr1_dc0_wan` back to `site-wan` NAT
  `172.30.2.0/24`; revert the `site-headend-install.sh` `--uplink-if`/`--wan-bridge` block + harness;
  revert the doc records. (No tfvar to drop -- the /24 is an inlined ruled literal.) This
  reverts D-125's bridge-in to the (dead-egress) committed Model B state -- the full B->A revert is
  `docs/model-a-fallback-plan.md`.
