# 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 = new HELD `var.vr1_dc0_uplink_cidr` (no default; NetBox office1-netbox apex).
- `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). OPNsense WAN re-addresses into the vcloud ISP /24 (edge config, D-113 --
HELD). `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. **HELD:** assign `vr1_dc0_uplink_cidr` (vcloud ISP /24) in office1-netbox; set OPNsense WAN static in
   that /24 (edge config). `tofu plan` requires the tfvar.
3. Confirm the `uplink`/`br-vr1-dc0-wan` interface names on first boot (enp2s0-vs-ensX naming trap).

## Revert (per item)

- `git rm -r opentofu/modules/wan-bridge`; revert `opentofu/main.tf` + `variables.tf` (drop
  `vr1_dc0_uplink` + `vr1_dc0_uplink_cidr` + 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. 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`.
