Repository navigation
fix: ignore an ACK whose CSeq does not match the server INVITE - #1
Merged
Merged
Conversation
ACKs for 2xx are routed to the server INVITE transaction per dialog (waiting_ack), so a delayed ACK of an earlier re-INVITE on the same dialog reaches the transaction of the current re-INVITE. Any ACK used to move it to Confirmed: Timer G stopped retransmitting the 2xx, the waiting_ack entry was removed so the real ACK lost its route, and the stale ACK was handed to the TU as if it acknowledged this INVITE. An ACK carries the CSeq number of the INVITE it acknowledges (RFC 3261 §17.1.1.3, §13.2.2.4). Ignore an ACK with any other CSeq number before touching the transaction state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Assessment
main(5958064):cargo test --lib test_server_invite_ack→ 1 failed:an ACK with CSeq 1 must not be delivered by the CSeq 2 INVITE transaction.cargo test→ 331 lib + 65 doc passed;cargo fmt --all -- --checkclean; clippy warning set identical to main.waiting_ackholds one entry per dialog (the N+1 entry overwrote N before this change too, so this is not a regression; proper routing needs a CSeq-aware key). 1 Minor (test naming follows the existing Timer G implementation rather than RFC 6026Accepted) waived to stay consistent with the current code.The bug
A server INVITE transaction in
CompletedorConfirmedmoves toConfirmedon any ACK (transaction.rs, theCompleted | Confirmed if req.method == Method::Ackarm). It never checks the ACK's CSeq.An ACK for a 2xx starts a new transaction (new branch), so the endpoint routes it per dialog through
waiting_ack(endpoint.rs,on_received_message). That means a delayed ACK of an earlier re-INVITE on the same dialog reaches the transaction of the current re-INVITE. That transaction then:Confirmedand stops Timer G, so a lost 2xx is never retransmitted and the UAC never gets the answer;waiting_ackentry (theConfirmedtransition does this since the waiting_ack leak fix), so the real ACK can no longer be routed and is dropped;We hit this in production with back-to-back re-INVITEs over UDP.
RFC references
So an ACK with any other CSeq number does not acknowledge this INVITE.
Reproduction
New test
src/transaction/tests/test_server_invite_ack.rs::test_server_invite_ignores_ack_with_other_csequses real UDP sockets and the endpoint serve loop. It follows the same pattern astest_server_invite_drop.rs.CSeq: 2on an established dialog, and the server replies 200 (stateCompleted, Timer G armed).CSeq: 1 ACKon the same dialog: a delayed ACK of the previous re-INVITE.Completed, Timer G is still armed, thewaiting_ackroute is still there, and the 200 is retransmitted.CSeq: 2 ACK. The test asserts it is delivered, the state isConfirmed, Timer G is stopped andwaiting_ackis empty.On current
mainthe test fails at step 3:The fix
In that arm, compare the ACK's CSeq number with
self.original's. On a mismatch, log at debug level and returnNonebefore touching any state. There is no transition, Timer G keeps running and nothing goes to the TU. This is a 16-line change insrc/transaction/transaction.rs, plus the test.Compatibility / risk
TransactionKey::from_requestrejects it first.cargo build,cargo test(331 lib + 65 doc;mainhas 330 lib) andcargo fmt --all -- --check.cargo clippy --all-targetsshows no new warnings. Its one error (never_loopintests/test_endpoint.rs) already fails the same way onmain.Out of scope, and left unchanged to keep this minimal:
CSeq: 2 INVITEis still accepted, as it is today.on_received_message'sfinished_transactionsACK path (for a transaction the TU already dropped) removeswaiting_ackwithout a CSeq check. That path absorbs the ACK silently either way, so it has no effect on retransmission.Happy to follow up on either.
Relation to restsend#128 (RFC 6026 Accepted state)
restsend#128 does not address this. It keeps the same unchecked
Completed | ConfirmedACK arm, and its newAcceptedarm also forwards any ACK routed to it to the TU.The two changes overlap textually in
on_received_request, so whichever lands second needs a small rebase. They are compatible: with restsend#128, the same CSeq-number guard should also go in theAcceptedarm, since Accepted is where the 2xx ACKs land. In that arm a stale ACK can't stop retransmissions, but it would still reach the TU and be taken for the real ACK. I'm glad to rebase either way.🤖 Generated with Claude Code
https://claude.ai/code/session_01L1Gu5CifqgBjASbxYmJ6mr