Skip to content

test(sip): container-restart REGISTER acceptance harness + full FS regression (M6 CP5, B4) - #155

Merged
ryanmurf merged 2 commits into
masterfrom
test/m6-cp5-container-restart-register-harness
Jul 17, 2026
Merged

test(sip): container-restart REGISTER acceptance harness + full FS regression (M6 CP5, B4)#155
ryanmurf merged 2 commits into
masterfrom
test/m6-cp5-container-restart-register-harness

Conversation

@ryanmurf

Copy link
Copy Markdown
Owner

M6 CP5 (B4) — container-restart REGISTER acceptance + full FS regression

Two parts: (1) build the outstanding CP5/B4 acceptance — an on-demand,
isolated-Docker
harness that proves dynamic authenticated REGISTER routing
follows a real container restart to a new IP, receiver-side; (2) run the full
isolated FreeSWITCH harness to confirm M6 regressed nothing.

PR #154 landed only the in-process mechanism proof
(crates/asterisk-integration-tests/tests/e2e_dynamic_register.rs). Its literal
plan acceptance — a bridge container restarted so it comes back with a new
container IP
on an isolated Docker network, then a call routing to the new IP —
was not built. This adds it as tests/cp5-register-restart/.

Part 1 — the harness (tests/cp5-register-restart/run.sh)

Isolated --internal network. rustisk (fixed IP, kept out of the dynamic pool
via --ip-range) + a sip-bridge container. A and B are read at runtime
(docker inspect), never hardcoded. Every INVITE is verified receiver-side
(the datagram's arrival on the bridge/sentinel), never a rustisk TX log.

  • GREEN-A — bridge digest-REGISTERs (401 → digest → 200) advertising IP
    A; Dial(PJSIP/bridge) INVITE arrives at A (captured on the bridge).
  • restart — the bridge container is destroyed; a sentinel container
    seizes the vacated A (so a stale-route INVITE has a receiver and Docker
    can't hand A back); a fresh bridge comes up with a new IP B (B ≠ A,
    enforced) and re-REGISTERs from B.
  • GREEN-BDial(PJSIP/bridge) INVITE follows to B (captured on the
    new bridge) and not to stale A (sentinel silent). rustisk holds both
    the stale-A and fresh-B bindings live, so Registrar::best_contact's
    newest-wins is genuinely load-bearing — not a trivial single-binding route.
  • RED (captured) — routing is defeated by a static contact pinned to A
    (bridge_pinned, whose AoR is contact = sip:bridge@A:5060 and never
    re-registers). Dial(PJSIP/bridge_pinned) misroutes to stale A, the sentinel
    catches the INVITE, and the follow-to-B assertion goes RED. This negative
    control proves the A-detection is real (the harness can fail). The
    complementary best_contact oldest-wins RED is covered in-process by
    e2e_dynamic_register.rs.

Captured proof (representative run, A/B assigned at runtime)

bridge IP A (pre-restart, runtime-read): 10.252.68.33
bridge IP B (post-restart, runtime-read): 10.252.68.34
A != B: yes

GREEN-A (INVITE arrives at registered A):        PASS
GREEN-B (INVITE follows to B, stale A silent):   PASS
RED     (static-pin defeats routing -> A caught): PASS_AS_RED

--- bridge receiver-side INVITE captures ---
INVITE role=bridge own=10.252.68.33 src=10.252.68.2:15060 ruri=sip:bridge@10.252.68.33:5060 ...
INVITE role=bridge own=10.252.68.34 src=10.252.68.2:15060 ruri=sip:bridge@10.252.68.34:5060 ...
--- sentinel (stale-A) INVITE captures ---
STRAY_INVITE role=sentinel own=10.252.68.33 src=10.252.68.2:15060 ruri=sip:bridge@10.252.68.33:5060 ...   # RED
--- rustisk registrar binding events ---
INFO Contact registered aor=bridge contact=sip:bridge@10.252.68.33:5060 expires=3600   # stale A
INFO Handled REGISTER status=200
INFO Contact registered aor=bridge contact=sip:bridge@10.252.68.34:5060 expires=3600   # fresh B (both live)
INFO Handled REGISTER status=200

The INVITE follows A → B receiver-side; the RED (static pin) misroutes to
stale A and is caught. This is test infrastructure only — no product code
changes — so merging it is safe.

Part 2 — full isolated FreeSWITCH regression (master @ 8a3cc2d)

tests/freeswitch-pin-gate/run.sh (full case), on the same rustisk build. All
sub-cases PASS — no M6 regression.

Area Result
Startup fail-closed (absent + invalid mounted secret) PASS / PASS
PIN gate REJECTED (REJECTED_B_SIDE delta=0; TX/RX voice + DTMF) PASS
PIN gate GRANTED (GRANTED_BRIDGE M2 two-way; TX/RX voice + DTMF) PASS
M3 no-input absolute deadline PASS
Bundle-A AMI positive control (Newexten/PinGate + VarSet/PINGATESTATUS) PASS
SIP-only pcap PASS
M1 listen-only (RTPVoiceFramesRx=0), Dial-timeout CANCEL PASS / PASS
M2 two-way + DTMF A→B, ingress hygiene (wrong-source/wrong-pt/malformed/unstable-SSRC), pattern injection, BYE-silent + deadline-silent teardown PASS
M4 dropped-200, late-replay, dropped-ACK Timer-H, dropped-BYE, 180-silence Timer-B, forged-dialog, concurrency-10, abandon-200 race, BYE-final-481, BYE-final-dropped Timer-F PASS (all)
M5 500-call exact-baseline soak (every registry restored to 0…) PASS
Bundle-B ZERO_HIT_SECRET_AUDIT (both patterns, concatenated + per-digit; every artifact; AMI live) PASS

Constraints honored

  • Isolated Docker only — no touch to the live voice stack / Helm / k8s / trunk /
    Chime / Mumble. Not wired into CI (on-demand).
  • Every container/network reaped on exit (host-PID kill for tron's
    userns-remapped docker, where docker rm -f/stop hang).
  • Throwaway random test PIN, mounted read-only, shredded on exit; the real PIN
    never appears in code, logs, or evidence.
  • cargo clippy --workspace --exclude pjsip-shim -- -D warnings and
    cargo test --workspace --exclude pjsip-shim both green (no Rust changed).

Repin Agent added 2 commits July 17, 2026 05:31
…ess (M6 CP5, B4)

On-demand harness (tests/cp5-register-restart/) proving rustisk's dynamic
authenticated REGISTER end-to-end with a REAL bridge container restart on an
isolated --internal Docker network — the literal CP5/B4 acceptance that PR #154
landed only as an in-process mechanism proof.

A sip-bridge container digest-REGISTERs (401->digest->200) advertising its
Docker-assigned IP A; Dial(PJSIP/bridge) lands an INVITE at A (captured
receiver-side on the bridge). The bridge container is destroyed, a sentinel
seizes the vacated A, and a fresh bridge comes up with a new IP B (B!=A,
enforced) and re-REGISTERs. A subsequent Dial FOLLOWS to B receiver-side and
NOT to stale A (sentinel silent). Both stale-A and fresh-B bindings stay live,
so best_contact's newest-wins is genuinely load-bearing. A/B are read at
runtime, never hardcoded.

RED negative control (captured): routing is defeated by a static contact pinned
to A (bridge_pinned); the Dial misroutes to stale A, the sentinel catches it,
and the follow-to-B assertion goes RED — proving the A-detection is real.

Reaps every container/network on exit (host-PID kill for tron's userns-remapped
docker); generates a throwaway test PIN, shredded on exit; never touches the
live stack. Not wired into CI (on-demand).
- sip_agent.py: a 200 to the UNAUTHENTICATED REGISTER is now a hard failure
  (exit 3), not success — a regression that bound an unauthenticated contact
  would otherwise green-light the digest-auth proof.
- run.sh: validate CP5_CASE up front (all|green|red) so a typo cannot skip the
  case-gated assertions and still exit 0.
- pjsip.conf/run.sh: give bridge_pinned a distinct contact user (sip:pinned@A)
  and correlate the RED capture on that Request-URI, so no stray 'bridge'
  datagram can false-pass RED.
- run.sh: harden reap_container (retry host-PID acquisition, timeout-wrap every
  docker op) and raise a loud CLEANUP WARNING on any leaked container/network.
- ami_originate.py: require the Originate-specific 'successfully queued'; a bare
  'Success' (also emitted by Login) no longer counts as Originate success.
- README: correct 'shred' wording and document the distinct pinned user.
@ryanmurf

Copy link
Copy Markdown
Owner Author

External adversarial review: codex (gpt-5.5, xhigh). It confirmed the receiver-side A/B captures are genuinely distinct, B != A is enforced, and the RFC2617 qop=auth digest matches rustisk's verifier — and flagged 5 false-green-hardening issues, all applied in 681fac6:

  1. (P1) sip_agent.py accepted an unauthenticated-200 REGISTER as success → now a hard failure (an auth regression can no longer green-light the digest-auth proof).
  2. (P1) an invalid CP5_CASE silently skipped the case-gated assertions and exited 0 → now validated up front.
  3. (P2) RED could consume a stray bridge datagram → bridge_pinned now uses a distinct sip:pinned@A contact, and RED correlates on that Request-URI.
  4. (P2) reap could leak on a transiently-empty host PID → retry PID acquisition, timeout-wrap every docker op, loud leak alarm.
  5. (P2) AMI Originate matched Login's Success → now requires the Originate-specific successfully queued.

Harness re-run green after the fixes (GREEN-A/GREEN-B pass, RED captured); Docker reaped.

@ryanmurf
ryanmurf merged commit 1ec0f37 into master Jul 17, 2026
3 checks passed
@ryanmurf
ryanmurf deleted the test/m6-cp5-container-restart-register-harness branch July 17, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant