Skip to content

feat(sip): M7 CP1 — wire-correct digest challenge handling on origination (M-f) - #157

Merged
ryanmurf merged 1 commit into
masterfrom
feat/m7-cp1-wire-correct-challenge
Jul 17, 2026
Merged

feat(sip): M7 CP1 — wire-correct digest challenge handling on origination (M-f)#157
ryanmurf merged 1 commit into
masterfrom
feat/m7-cp1-wire-correct-challenge

Conversation

@ryanmurf

Copy link
Copy Markdown
Owner

M7 CP1 — wire-correct digest challenge handling on origination (M-f)

Origination could not authenticate through a challenging carrier: on a 401/407
to our INVITE rustisk sent no credentials (the outbound authenticator had no
production caller
) and failed the call. This lands all of M-f.

What changed

  • New client transaction on retry (authenticator.rs): the credentialed
    retry now rewrites the top Via to a fresh branch (was reused — the M-f
    defect), plus the existing CSeq increment + Authorization.
  • Real CSeq + route-set/Contact targeting (session/mod.rs): build_ack
    (2xx) and build_cancel carry the INVITE's actual CSeq (were hardcoded 1);
    build_ack/build_bye route through the dialog route set (Record-Route →
    Route, Request-URI = refreshed Contact); in_dialog_next_hop resolves the
    first route hop. build_auth_retry_invite answers the challenge and adopts the
    retry as the live INVITE.
  • Bounded retry orchestration (event_handler.rs): 401/407 → bounded
    credentialed retry (MAX_OUTBOUND_AUTH_ATTEMPTS = 2) as a new INVITE
    transaction; the 2xx ACK and in-dialog BYE go to the route-set/Contact target,
    not the response source. (The non-2xx ACK is already emitted by the M4
    transaction layer — verified receiver-side, not duplicated.)
  • outbound_auth endpoint credential (pjsip_config.rs, channel_driver.rs).

Receiver-side proof (isolated offline-carrier harness)

tests/cp1-origination-challenge/run.sh — 3 containers on an --internal
network: rustisk, a carrier core (challenges + answers), and a separate
route target T
(Contact/Record-Route target). All asserts are on the carrier's
captured datagrams, never a rustisk TX log. GREEN verdict:

(a) 401 challenge ACKed (same branch), no retransmit:   PASS
(b) retry INVITE new-branch + CSeq++ + valid digest:    PASS
      first INVITE branch:  z9hG4bK34c3426edd394a12
      retry INVITE branch:  z9hG4bKdfc7a5deeef0e7b9 (cseq=2 valid=yes)
(c) 2xx ACK + BYE land on route target T, not core:     PASS
(d) in-dialog CSeq real (ACK=2, BYE=3), not hardcoded 1:PASS

Core capture shows ACK-CHALLENGE (branch = INVITE branch), then RETRY-INVITE … branch=<new> cseq=2 auth=yes valid=yes prev_branch=<old>. Target capture shows
ACK-2XX-AT-TARGET … cseq=2 ACK and BYE-AT-TARGET … cseq=3 BYE — both at T,
none at core.

Decisive REDs (each captured by reverting the specific fix)

  • (b) branch reuse — disable the Via-branch rewrite → harness: retry INVITE branch: z9hG4bKc417… == first INVITE branch z9hG4bKc417…(b) FAIL.
  • (c) route set ignored — send the 2xx ACK to the response source instead of
    the route hop → harness: route target never saw the 2xx ACK(c) FAIL
    (ACK/BYE land back on the core).
  • (d) hardcoded CSeq — revert to "1 ACK"/"1 CANCEL" → unit tests:
    left: "1 ACK", right: "2 ACK" and left: "1 CANCEL", right: "2 CANCEL"
    FAIL.

Tests

cargo test --workspace --exclude pjsip-shim and cargo clippy --workspace --exclude pjsip-shim -- -D warnings green. Rust 1.97.0.

Note: the harness dialplan uses Wait(2) to hold the leg through the
challenge→retry→200 exchange (the app-runs-before-answer timing is CP3's
concern; this CP proves signaling wire-correctness only).

…tion (M-f)

Today an origination INVITE that gets a 401/407 was never answered with
credentials: `OutboundAuthenticator::create_authenticated_request` had no
production caller, and the challenge was treated as a plain call failure. Fix
all of M-f so rustisk can originate through a challenging carrier, proven
RECEIVER-SIDE against an offline carrier UAS.

Changes:
- authenticator: the credentialed retry is now a NEW client transaction — the
  top Via branch is rewritten to a fresh branch (was reused: the M-f defect),
  alongside the existing CSeq increment + Authorization. `replace_via_branch`
  preserves rport/transport/sent-by.
- session: `build_auth_retry_invite` answers a 401/407 and adopts the retry as
  the live INVITE; `build_ack` (2xx) and `build_cancel` now carry the INVITE's
  REAL CSeq (were hardcoded 1); `build_ack`/`build_bye` target the dialog route
  set (Record-Route -> Route headers, Request-URI = refreshed Contact) via
  loose/strict routing; `in_dialog_next_hop` resolves the first route hop.
- event_handler: on a 401/407 to our INVITE, bounded credentialed retry
  (`MAX_OUTBOUND_AUTH_ATTEMPTS`) as a new INVITE transaction; the 2xx ACK and
  in-dialog BYE are sent to the route-set/Contact target, not the response
  source. The non-2xx ACK itself is already emitted by the transaction layer.
- pjsip_config / channel_driver: new `outbound_auth` endpoint field; the
  resolved credential rides on the outbound session.

Proofs: session cp1_tests (RED-capable: revert to CSeq 1 -> "1 ACK"!="2 ACK");
authenticator branch test; and tests/cp1-origination-challenge — an isolated
offline-carrier harness that verifies, receiver-side, (a) the 401 is ACKed
(same branch), (b) the retry carries a new branch + CSeq++ + a valid digest,
(c) the 2xx ACK and BYE land on the route-set/Contact target (a SEPARATE
address) not the core, and (d) the in-dialog CSeq is real (ACK=2, BYE=3).
@ryanmurf
ryanmurf merged commit 4db7a8d into master Jul 17, 2026
3 checks passed
@ryanmurf
ryanmurf deleted the feat/m7-cp1-wire-correct-challenge branch July 17, 2026 12:36
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