When a client INVITE transaction receives a 2xx final response,
Transaction::on_received_response transitions to Completed and
unconditionally calls send_ack (src/transaction/transaction.rs, the
is_completed_client_invite branch).
Per RFC 3261 section 17.1.1.2, a 2xx final response is treated differently
from non-2xx: the client INVITE transaction terminates immediately and
the ACK for a 2xx is generated by the TU (UAC core), end-to-end —
not by the transaction layer. The transaction-layer ACK is correct
only for non-2xx finals (hop-by-hop, section 17.1.1.3).
For UA applications built on the dialog layer this auto-ACK is
convenient (and the dialog layer relies on it — nothing outside the
transaction module sends ACKs). But for a proxy built on the
transaction layer (RFC 3261 section 16), it is a protocol violation
with interop consequence:
- The proxy forwards an INVITE (or in-dialog re-INVITE) downstream
via a client transaction.
- The UAS answers 200.
- rsipstack immediately emits an ACK the proxy must not send.
- The genuine end-to-end ACK from the caller then arrives and is
relayed, so the UAS receives two ACKs per 2xx.
Some stacks ignore the duplicate as a retransmission, but causes
issues with servers that mishandle the duplicate ACK after a re-INVITE,
breaking the call. There is no way to suppress the ACK from outside the
crate: send_ack is unconditional, and the MessageInspector hook
can transform a message but not cancel thesend.
Two secondary effects of the current behavior also matter for proxies:
- While the transaction sits in
Completed, retransmitted 2xx are
swallowed as duplicates instead of being passed to the TU — but a
proxy must forward 2xx retransmissions end-to-end (RFC 3261 section 16.7);
the 200-retransmit/ACK cycle is the end-to-end reliability loop.
- After termination,
cleanup() stores the ACK in
finished_transactions, so the endpoint re-sends the stray ACK on
every retransmitted 200 for the cleanup window.
Proposed fix: an opt-out flag
EndpointOption::auto_ack_2xx, default true so existing UA/dialog
behavior is fully preserved. With the flag false, a 2xx moves the client
INVITE transaction directly to Terminated with no transaction-layer ACK
and no stored ACK, so the 2xx (and its retransmissions) are the TU's to
handle — the RFC-correct proxy semantics. Non-2xx handling is
unchanged in both modes.
This patch has been tested internally and can confirm it fixes the
duplicate-ACK interop failure.
When a client INVITE transaction receives a 2xx final response,
Transaction::on_received_responsetransitions toCompletedandunconditionally calls
send_ack(src/transaction/transaction.rs, theis_completed_client_invitebranch).Per RFC 3261 section 17.1.1.2, a 2xx final response is treated differently
from non-2xx: the client INVITE transaction terminates immediately and
the ACK for a 2xx is generated by the TU (UAC core), end-to-end —
not by the transaction layer. The transaction-layer ACK is correct
only for non-2xx finals (hop-by-hop, section 17.1.1.3).
For UA applications built on the dialog layer this auto-ACK is
convenient (and the dialog layer relies on it — nothing outside the
transaction module sends ACKs). But for a proxy built on the
transaction layer (RFC 3261 section 16), it is a protocol violation
with interop consequence:
via a client transaction.
relayed, so the UAS receives two ACKs per 2xx.
Some stacks ignore the duplicate as a retransmission, but causes
issues with servers that mishandle the duplicate ACK after a re-INVITE,
breaking the call. There is no way to suppress the ACK from outside the
crate:
send_ackis unconditional, and theMessageInspectorhookcan transform a message but not cancel thesend.
Two secondary effects of the current behavior also matter for proxies:
Completed, retransmitted 2xx areswallowed as duplicates instead of being passed to the TU — but a
proxy must forward 2xx retransmissions end-to-end (RFC 3261 section 16.7);
the 200-retransmit/ACK cycle is the end-to-end reliability loop.
cleanup()stores the ACK infinished_transactions, so the endpoint re-sends the stray ACK onevery retransmitted 200 for the cleanup window.
Proposed fix: an opt-out flag
EndpointOption::auto_ack_2xx, defaulttrueso existing UA/dialogbehavior is fully preserved. With the flag
false, a 2xx moves the clientINVITE transaction directly to
Terminatedwith no transaction-layer ACKand no stored ACK, so the 2xx (and its retransmissions) are the TU's to
handle — the RFC-correct proxy semantics. Non-2xx handling is
unchanged in both modes.
This patch has been tested internally and can confirm it fixes the
duplicate-ACK interop failure.