Skip to content

test(sip): M6 CP5 (B4) — dynamic authenticated REGISTER + IP-change routing (receiver-side, in-process) - #154

Merged
ryanmurf merged 1 commit into
masterfrom
feat/m6-cp5-dynamic-authenticated-register
Jul 17, 2026
Merged

test(sip): M6 CP5 (B4) — dynamic authenticated REGISTER + IP-change routing (receiver-side, in-process)#154
ryanmurf merged 1 commit into
masterfrom
feat/m6-cp5-dynamic-authenticated-register

Conversation

@ryanmurf

Copy link
Copy Markdown
Owner

M6 CP5 (B4) — dynamic authenticated REGISTER + IP-change routing: rustisk-side receiver-side proof

Context. The sip-bridge advertises an EPHEMERAL pod IP and REGISTERs; a static contact is wrong
because a pod restart gives it a NEW IP (B4). The rustisk registrar-side mechanism for this ALREADY
EXISTS:

So a re-REGISTER from a new address updates the routing. What was missing is a committed
receiver-side regression test
of the pod-restart-new-IP property. This PR adds it.

RECEIVER-SIDE PROOF (e2e_dynamic_register.rs, real UDP sockets).

  1. the bridge digest-REGISTERs from address A (401 → digest → 200 bound);
  2. an outbound Dial to the bridge endpoint routes to A — the INVITE datagram lands on A's socket;
  3. the bridge "restarts": a NEW socket B re-REGISTERs (digest) from a new address;
  4. a subsequent Dial FOLLOWS to B — the INVITE datagram lands on B and NOT on the stale A.

The REGISTER is a genuine digest round-trip (401 challenge → create_digest_response → 200), so it
proves authenticated dynamic registration; the outbound routing is exercised through the real
ChannelDriver::request/call path (direct transport send).

CAPTURED RED. Make Registrar::best_contact oldest-wins instead of newest-wins:

thread 're_register_from_new_ip_reroutes_the_call' panicked:
after restart: the outbound INVITE must FOLLOW to the re-REGISTERed address B

(the INVITE datagram fails to reach B — receiver-side RED).

SCOPE / what remains. This is the in-process rustisk-side mechanism proof. CP5's stated
acceptance also calls for an isolated-Docker container-restart harness (a bridge container
restarted so it comes back with a NEW pod IP on an --internal Docker network, then a call that
routes to the new IP). That containerized harness is a separate proof, run out-of-band, and is NOT
included in this PR
— it is flagged for continuation. The rustisk code proven correct here is
exactly what that harness exercises.

GATES. cargo test --workspace --exclude pjsip-shim green; cargo clippy --workspace --exclude pjsip-shim -- -D warnings clean. Rust 1.97.0. No rustisk source change (test-only;
mechanism pre-exists via issue #33).

@ryanmurf

Copy link
Copy Markdown
Owner Author

Review note

This is a test-only PR — it adds e2e_dynamic_register.rs and changes NO rustisk source. The B4 mechanism it proves (resolve_endpoint_contact preferring the live registration, issue #33; Registrar::best_contact newest-wins) already exists on master. Self-review per pr-workflow: no production code changed, no auth/RLS weakening, no assertions removed or loosened, purely additive receiver-side regression coverage. External review not required for this trivial (test-only) diff; the captured RED (oldest-wins best_contact → INVITE fails to follow to B) is in the PR body.

Not included: CP5's isolated-Docker container-restart harness (bridge container restarted with a new pod IP) — flagged as the remaining CP5 acceptance step for continuation.

…eceiver-side (M6 CP5, B4)

The rustisk registrar-side B4 mechanism already exists (resolve_endpoint_contact
prefers the live registration over a static contact, issue #33; Registrar::
best_contact returns the most-recently-registered non-expired contact). This
adds the missing committed RECEIVER-SIDE regression test for the pod-restart-
new-IP property, with real UDP sockets:
  1. the bridge digest-REGISTERs from address A (401 -> digest -> 200);
  2. an outbound Dial routes to A — the INVITE datagram lands on A's socket;
  3. the bridge 'restarts': a new socket B re-REGISTERs (digest) from a new addr;
  4. a subsequent Dial FOLLOWS to B — the INVITE lands on B, NOT the stale A.

RED (captured in the PR): make best_contact oldest-wins -> after the re-REGISTER
the INVITE datagram fails to reach B ("must FOLLOW to the re-REGISTERed address
B") -> receiver-side RED.

Scope: this is the in-process rustisk-side mechanism proof. CP5's isolated-Docker
container-restart harness (bridge container restarted with a new pod IP) is a
separate acceptance run out-of-band and is NOT included here.
@ryanmurf
ryanmurf force-pushed the feat/m6-cp5-dynamic-authenticated-register branch from 18d69e5 to 0ec8912 Compare July 17, 2026 10:35
@ryanmurf
ryanmurf merged commit 8a3cc2d into master Jul 17, 2026
3 checks passed
@ryanmurf
ryanmurf deleted the feat/m6-cp5-dynamic-authenticated-register branch July 17, 2026 10:39
ryanmurf added a commit that referenced this pull request Jul 17, 2026
…gression (M6 CP5, B4) (#155)

* test(sip): isolated-Docker container-restart REGISTER acceptance harness (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).

* test(sip): harden CP5 harness against false-green (codex review)

- 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.

---------

Co-authored-by: Repin Agent <repin-agent@herodevs.dev>
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