Skip to content

fix(transaction): answer a CANCEL after a final response with its own 200 - #199

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

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

Conversation

@tgeorge06

Copy link
Copy Markdown
Contributor

Fixes #198.

Thanks to @beija-wq for the report. The analysis and the suggested fix in the issue are exact; the reproduction program prints not reproduced with this PR (REPRODUCED - 2/2 on main).

Problem

A server-side CANCEL has the INVITE's transaction key (src/transaction/key.rs:41-43), so it lands on the INVITE transaction while it lives and on its finished_transactions entry after it ended. Neither answers it correctly once the INVITE has a final response:

  • In Accepted (a 2xx waiting for its ACK, RFC 6026, feat(transaction): RFC 6026 Accepted state + Timer L/M (adopted from #128) #164) and in Confirmed (a non-2xx already ACKed), Transaction::on_received_request takes the _ arm and answers 481 (src/transaction/transaction.rs:867-884 at current main, 6fd9414). Only Trying/Proceeding and Completed are handled (lines 863-866). 0.6.x answered 200 here.
  • Once the transaction has ended (the ACK of a 2xx ends Accepted, line 945; a timer ends Confirmed, Completed or Accepted; or the TU dropped it), EndpointInner::on_received_message finds the finished_transactions entry under the CANCEL's key and re-sends the cached INVITE response as the answer (src/transaction/endpoint.rs:412-419). The CANCEL gets a 200 OK with CSeq: 1 INVITE (or a copy of a 480, or of a 180 when the TU dropped an unanswered INVITE), its client transaction never completes, and the UAC retransmits it until Timer F.

Expected

RFC 3261 §9.2:

If the UAS did not find a matching transaction for the CANCEL according to the procedure above, it SHOULD respond to the CANCEL with a 481 (Call Leg/Transaction Does Not Exist).

If the transaction for the original request still exists, the behavior of the UAS on receiving a CANCEL request depends on whether it has already sent a final response for the original request. If it has, the CANCEL request has no effect on the processing of the original request, no effect on any session state, and no effect on the responses generated for the original request.

Regardless of the method of the original request, as long as the CANCEL matched an existing transaction, the UAS answers the CANCEL request itself with a 200 (OK) response. This response is constructed following the procedures described in Section 8.2.6 noting that the To tag of the response to the CANCEL and the To tag in the response to the original request SHOULD be the same.

RFC 6026 §7.1 keeps the server INVITE transaction alive in "Accepted" after a 2xx; it does not mention CANCEL, so the §9.2 rule for a transaction with a final response applies.

Fix

  • transaction.rs: Accepted and Confirmed join Completed. The CANCEL gets 200, is not passed to the TU, and the INVITE transaction is not touched (the 2xx keeps being retransmitted until its ACK, which still ends the transaction).
  • endpoint.rs, finished_transactions path: a CANCEL is never answered with the cached message. If the cached message is a final response it gets 200. If it is a provisional (the TU dropped the INVITE without answering it), it falls through to the existing "no transaction" arm and gets 481, as with no INVITE at all. INVITE retransmissions and ACKs take the cached path exactly as before.
  • Both answers carry the To header of the INVITE's last response (new EndpointInner::make_cancel_response, src/transaction/message.rs), so the 200 to the CANCEL has the To tag of the INVITE's final response. The live-transaction path does the same in Proceeding (the To of the last provisional). The 481 arm of the endpoint now goes through a small shared reply_cancel helper, with the same make_response, before_send(.., None) and Via destination as before.

+63 / -23 lines outside the test, over endpoint.rs, transaction.rs and message.rs.

Unchanged on purpose:

Contract / coverage

What a CANCEL for a server INVITE is answered with (UDP and TCP alike):

INVITE state when the CANCEL arrives before after
Trying / Proceeding 200 CANCEL, CANCEL to the TU (487) same, To of the last provisional
Completed (3xx-6xx sent) 200 CANCEL same, To of the final response
Confirmed (3xx-6xx ACKed) 481 CANCEL 200 CANCEL
Accepted (2xx sent, no ACK yet) 481 CANCEL 200 CANCEL, 2xx still retransmitted until the ACK
ended after a final response (2xx ACKed, TU dropped it, Confirmed timer) copy of the INVITE's final response (CSeq: 1 INVITE) 200 CANCEL
ended with only a provisional (TU dropped it) copy of the provisional (CSeq: 1 INVITE) 481 CANCEL
no INVITE 481 CANCEL same

In no row does the CANCEL reach the TU or change the INVITE transaction once a final response was sent.

Tests

src/transaction/tests/test_server_cancel.rs: test_cancel_answers_by_invite_state, a raw UDP or TCP peer against an endpoint, one row per line of the table above, over both transports. Every row sends the CANCEL twice (a retransmission; in Proceeding the second one after the 487). It checks the code and CSeq of every response the peer gets, that only genuine INVITE responses carry CSeq: INVITE (the 487, and the final response retransmitted while the transaction lives), the To tag of each 200 to a CANCEL, that the CANCEL never reaches the TU after a final response and leaves the transaction state alone, and that in Accepted the ACK still ends the transaction after the CANCEL.

On main (6fd9414) it fails on the first row:

assertion `left == right` failed: accepted (tcp: false): [("481 1 CANCEL", None), ("481 1 CANCEL", None), ("200 1 INVITE", Some("5CQ8vipD"))]

With the assertions relaxed to print each row, main gives: accepted 481 CANCEL, acked 200 INVITE twice, confirmed 481 CANCEL, ended after a 480 480 INVITE twice, ended after a 180 180 INVITE twice, on UDP and TCP; the Proceeding, Completed and no-INVITE rows match. Without the To copy, the To-tag assertions fail. The test passed 10 runs in a row.

The issue's reproduction program (UDP, default EndpointOption, the dialog layer's usual UAS loop):

main:    X (before ACK): CANCEL answered with ["SIP/2.0 481 Call/Transaction Does Not Exist [CSeq: 1 CANCEL]"]; dialog state: WaitAck
         Y (after ACK): CANCEL answered with ["SIP/2.0 200 OK [CSeq: 1 INVITE]"]; dialog state: Confirmed
this PR: X (before ACK): CANCEL answered with ["SIP/2.0 200 OK [CSeq: 1 CANCEL]"]; dialog state: WaitAck
         Y (after ACK): CANCEL answered with ["SIP/2.0 200 OK [CSeq: 1 CANCEL]"]; dialog state: Confirmed

Checks

  • cargo test --features bench: 421 lib tests passed, 0 failed (420 on main plus the new one), and 65 doc tests passed. Plain cargo test passes too.
  • cargo check --no-default-features --features platform-embassy: no warnings, as on main.
  • cargo clippy --features bench --all-targets: the same output as on main, none in the changed code. (On main it stops at a clippy::never_loop error in src/dialog/tests/test_refer_notify.rs:98, unrelated to this PR; with -A clippy::never_loop the warnings are the same as on main.)
  • cargo fmt --check: clean.

… 200

A CANCEL shares the server INVITE's transaction key. In Accepted (a 2xx
awaiting its ACK, RFC 6026) and in Confirmed it was answered 481, and
once the INVITE transaction had ended the cached INVITE response was
replayed as the answer to the CANCEL.

Answer it 200 with CSeq CANCEL in Accepted and Confirmed, as in
Completed, without passing it to the TU, and in the finished_transactions
path when the cached response is final. An INVITE dropped with only a
provisional, or no INVITE at all, still gets 481. The 200 carries the To
of the INVITE's last response (RFC 3261 9.2).
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 CANCEL for an INVITE that already got its 2xx is answered with 481 (0.7.x), or with a copy of the INVITE's 200

1 participant