Skip to content

fix: ACK and BYE a 2xx that crosses the CANCEL of a dropped INVITE - #159

Open
tgeorge06 wants to merge 1 commit into
restsend:mainfrom
tgeorge06:fix/2xx-crossing-cancel
Open

tgeorge06 wants to merge 1 commit into
restsend:mainfrom
tgeorge06:fix/2xx-crossing-cancel

Conversation

@tgeorge06

Copy link
Copy Markdown

Fixes #158.

Problem

Dropping the do_invite future after a provisional response cancels the INVITE: DialogGuardForUnconfirmed sends CANCEL and waits 2 s for the INVITE's final response, then drops the transaction. When the callee answers while the CANCEL is in flight, the 2xx is ACKed (by the transaction, or after the window by the detached transaction), but nothing sends a BYE, and the application no longer has a dialog to BYE. The callee stays in a confirmed call on dead air.

Spec

  • RFC 3261 §9.1: a CANCEL has no effect on a request that already got a final response; the client transaction is kept until the final response, up to 64*T1 after the CANCEL.
  • RFC 3261 §13.2.2.4 / §15: the UAC ACKs every 2xx and ends an unwanted session with BYE.
  • RFC 5407 §3.1.2: the CANCEL/2xx race.

Fix

  • DialogGuardForUnconfirmed::drop: keep the INVITE transaction past the 2 s window until its final response, with one absolute 64*T1 deadline measured from the cancel. The transaction ACKs the 2xx as before.
  • On a 2xx, send a BYE built from the 2xx's To tag, route set and Contact (InviteDialog::bye_2xx_after_cancel). The 2xx remote-target update is moved out of process_invite into update_remote_target_from_2xx, shared by both paths.
  • Terminated(UacCancel) is still emitted at the same point, and no Confirmed is emitted for the abandoned call. 487 and other final responses are unchanged.

No public API change.

Contract / coverage

  • Role: UAC only, and only the dropped-do_invite path. An explicit InviteDialog::cancel() while do_invite still runs does not go through this path and is unchanged: the crossing 2xx is returned to the caller, which owns the confirmed dialog.
  • Initial INVITE only: re-INVITEs do not go through DialogGuardForUnconfirmed.
  • Transport: the change is above the transaction layer and does not depend on the transport; tests use UDP, where response retransmission applies.
  • Automatic ACK (3ea35cd): an INVITE sent by do_invite carries one Via, so its 2xx is still ACKed by the transaction; the multi-Via (proxied) path added in 3ea35cd is not involved.
  • Retransmitted 2xx: ACKed again and does not start a second BYE (tested).
  • Timers: waiting stops at the final response or at 64*T1 after the cancel, whichever is first.

Tests

New src/dialog/tests/test_cancel_2xx_race.rs (raw UDP peer, public API): 180, drop the do_invite future, CANCEL, then the 200 to the INVITE (a) before the CANCEL's 200, (b) after it, (c) after the 2 s window. Each case checks that the ACK and the BYE carry the INVITE's Call-ID, our From tag and the 2xx's To tag, that the BYE uses a new CSeq and targets the 2xx Contact, that a retransmitted 2xx is re-ACKed without a second BYE, and that the state stream shows exactly one Terminated(UacCancel) and no Confirmed.

  • test_2xx_before_cancel_response_is_acked_and_byed
  • test_2xx_after_cancel_response_is_acked_and_byed
  • test_2xx_after_cancel_settle_window_is_acked_and_byed
  • test_cancel_answered_487_sends_no_bye (guard: 487 is ACKed, no BYE)

Checks (on main @ 3ea35cd)

  • Red: with the src/ change reverted and the new test kept, cargo test --lib test_cancel_2xx_race → 3 failed (timeout waiting for BYE), 1 passed (the 487 guard).
  • Green: cargo test → 336 lib + 65 doc passed, 0 failed. rustfmt --check clean on the changed files.

Out of scope

@Object905

Copy link
Copy Markdown

that seem to came up here miuda-ai/active-call#125 too

@tgeorge06

Copy link
Copy Markdown
Author

Thanks, yes, that's the same RFC 3261 §9.1 race. active-call#125 handles it in the application; this PR fixes it inside rsipstack's do_invite drop path (ACK + BYE the crossing 2xx), so applications get it without their own handling.

Dropping the do_invite future while the call is ringing cancels the
INVITE. The guard waited up to 2 s for the INVITE's final response and
then dropped the transaction, but a 2xx found in that window was only
logged, and a 2xx arriving later was ACKed by the detached transaction
without anything ending the session. Either way the callee kept a
confirmed dialog on dead air until it hung up.

A CANCEL that crosses a 2xx has no effect on the INVITE; the UAC must
ACK the 2xx and send a BYE (RFC 3261 §9.1, §15; RFC 5407 §3.1.2).
Keep the INVITE transaction after the settle window until its final
response (up to 64*T1) so a 2xx is ACKed, and send a BYE built from the
2xx's To tag, route set and Contact. Terminated(UacCancel) is still
reported at the same point, and no Confirmed state is emitted for the
abandoned call.
@tgeorge06
tgeorge06 force-pushed the fix/2xx-crossing-cancel branch from 31647d4 to 9cf1a0d Compare October 5, 2026 18:07
@tgeorge06

Copy link
Copy Markdown
Author

Rebased onto 0.7.0.

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.

A 2xx that crosses the CANCEL of a dropped INVITE is never BYE'd (RFC 3261 §9.1, §15)

2 participants