test(m8a): prove the cutover mechanism — LISTENER SWAP (no NAT), isolated netns#163
Merged
Merged
Conversation
…ated netns
Selects and proves the mechanism M9 will use to switch the public UDP trunk
endpoint (tron:45070 + RTP 20000-20100) between FreeSWITCH and rustisk. The
whole proof runs INSIDE ONE isolated container network namespace
(--privileged --network none) on SYNTHETIC high ports over an internal veth
pair + child netns ("chime" -> "tron"), so packets traverse prerouting+input
exactly like the live trunk. It never touches the host nftables, host ports,
the live trunk, port 45070, the router, or the cluster. No `flush ruleset`;
the cutover table is separate and independently deletable.
Selected mechanism: LISTENER SWAP. FS trunk profile stopped -> rustisk binds
hostIP:45070 directly. Delivery is per-packet socket lookup — no NAT, no
conntrack, no redirect — so the conntrack objection is structurally absent. A
preinstalled fail-closed DROP on the guarded dport is enabled only across the
handover so the sender sees loss (SIP retransmits) not ICMP port-unreachable.
M9 invokes exactly these artifacts:
- cutover_lib.sh — the filter-only nft primitives (table up/down,
handover drop on/off). The only host-net surface.
- apply-fs-to-rustisk.sh — APPLY (FS->rustisk): drop-on, stop FS, bind rustisk,
drop-off. STOP_OLD/START_NEW injected as commands, so
the orchestration is identical in the synthetic proof
and the live cutover (M9 substitutes
`sofia profile <trunk> stop/start`). Measures window.
- rollback-rustisk-to-fs.sh — ROLLBACK (rustisk->FS), the exact mirror.
Proof (tests/m8a-cutover, run-proof.sh; RESULTS.md holds the passing transcript):
1. one fixed UDP five-tuple primed, numbered datagrams flowing continuously
(seq 1..2296) across BOTH transitions, never restarted;
2. switch FS->rustisk: single clean delivery boundary, FS through seq 745,
rustisk from 758 (12-datagram handover-drop gap), overlap_count=0 — no
split-brain / no interleaving;
3. rollback rustisk->FS under the SAME flow: reverse boundary just as clean
(rustisk through 1519, FS from 1533);
4. source-drop holds throughout both transitions for v4 AND v6 — untrusted
10.9.0.3 (v4) and fd00::3 (v6) delivered to NEITHER stand-in (0/0), a genuine
DROP in an accept-policy chain;
5. handover windows measured both directions: FS->rustisk ~47-53 ms,
rustisk->FS ~49-52 ms (mechanism floor; the live M9 window adds FS profile
stop/start + DNS re-resolve). bind-on-command (main.rs:2189-2192 / PR #65) is
NOT required on this evidence — flagged conditional, not built.
RED control (teeth): a stateful dnat/redirect lever FAILS the proof where the
listener swap passes — it rewrites the dport past the source-drop, delivering
the untrusted source to the successor (delivered_to_RUSTISK=695). Honest finding:
PLAN-v3's conntrack-persistence rollback objection did NOT reproduce on tron's
kernel (redirect reverts cleanly on rule removal); the listener swap is still
chosen for introducing no NAT/conntrack into a NAT-free path. A detector
self-test proves assert_boundary rejects overlap/interleave/missing-rollback/
untrusted-delivery and accepts a clean capture.
Synthetic ports only; live voice stack, Helm, host nft, router untouched.
Co-authored-by: Repin Agent <repin-agent@herodevs.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M8a — select + prove the cutover mechanism M9 invokes
Proves the mechanism to switch the public UDP trunk endpoint (
tron:45070+ RTP20000-20100) between FreeSWITCH and rustisk. The entire proof runs inside ONE isolated container netns (--privileged --network none) on synthetic high ports over an internal veth pair + child netns (chime → tron), so packets traverse prerouting+input like the real trunk. The host nftables, host ports, the live trunk, port 45070, the router, and the cluster were NEVER touched. Noflush ruleset.Selected mechanism: LISTENER SWAP (no NAT)
FS trunk profile stopped → rustisk binds
hostIP:45070directly. Delivery is per-packet socket lookup — no NAT, no conntrack, no redirect — so the conntrack objection is structurally absent. A preinstalled fail-closed DROP on the guarded dport is enabled only across the handover so the sender sees loss (SIP retransmits), not ICMP port-unreachable. Chosen over redirect/DNAT/router-repoint per PLAN-v3 §M8a (no new firewall rules, source allowlist unchanged,sofia profile startrollback already proven, direct bind ⇒ New-3 N/A; New-5 honored: no K8s Service).Artifacts M9 invokes (
tests/m8a-cutover/)cutover_lib.sh— filter-only nft primitives (table up/down,handover_drop_on/off). The only host-net surface.apply-fs-to-rustisk.sh— APPLY (FS→rustisk): drop-on → stop FS → bind rustisk → drop-off.STOP_OLD/START_NEWinjected as commands so the orchestration is identical here and in live M9 (substitutesofia profile <trunk> stop/start). Measures the window.rollback-rustisk-to-fs.sh— ROLLBACK (rustisk→FS), the exact mirror.run-proof.shbuilds + runs--privileged --network none+ reaps;README.md/RESULTS.mddocument it.Measured handover windows (both directions)
Floor = nft-toggle + socket close/bind + IPC. The live M9 window adds FS
sofia profile stop/start+ DNS re-resolve (feed M0a's number). bind-on-command (main.rs:2189-2192/ PR #65) is NOT required on this evidence — flagged conditional, not built.Passing transcript (steps 1–4 + RED + detector) — full in
RESULTS.mdoverlap_count=0.10.9.0.3/fd00::3delivered to neither stand-in (0/0), a genuine DROP.RED teeth + honest finding
A stateful
dnat/redirect lever fails the proof where the swap passes — it rewrites the dport past the source-drop, delivering the untrusted source to the successor (PLAN-v3's port-rewrite hazard). Honest negative result: PLAN-v3's conntrack-persistence rollback objection did not reproduce on tron's kernel (redirect reverts cleanly on rule removal); the listener swap is still chosen for introducing no NAT/conntrack into a NAT-free path. A detector self-test provesassert_boundaryrejects every crafted pathology.Test-harness only (shell/python/docs under a new
tests/dir); no Rust/product/CI-workflow code changed.