Skip to content

nta: ACK and BYE an orphan 2xx over the connection the INVITE was sent on - #346

Open
renny678 wants to merge 1 commit into
freeswitch:masterfrom
renny678:fix-orphan-2xx-ackbye-ws
Open

renny678 wants to merge 1 commit into
freeswitch:masterfrom
renny678:fix-orphan-2xx-ackbye-ws

Conversation

@renny678

Copy link
Copy Markdown

Problem

FreeSWITCH (mod_sofia) calls a SIP WebSocket client (JsSIP in a browser, auto-answer) and cancels the call a few tens of milliseconds later. When the client's 200 OK crosses the CANCEL, nta treats it as an orphan 2xx and tries to ACK and BYE it, but the ACK cannot be sent (sofia-sip 1.13.18):

nta.c:9551 outgoing_recv() nta: Orphan 200 Ok send ACK&BYE 0x7f6bc0070ca0
nta.c:10235 outgoing_resolve() nta(0x7f6bc005cb60): transport ws is not supported
nta.c:9932 outgoing_reply() nta(0x7f6bc005cb60): responding 503 DNS Error to ACK!

The client gets neither the ACK nor the BYE. It keeps retransmitting the 200 OK (RFC 3261 section 13.3.1.4: "If the server retransmits the 2xx response for 64*T1 seconds without receiving an ACK, the dialog is confirmed, but the session SHOULD be terminated.") and, still being in a call, rejects every new INVITE with 486 Busy Here for those 32 seconds.

Root cause

nta_msg_ackbye() sends the ACK and BYE as stateless requests without a transport, so they are routed by the request URI taken from the Contact of the 2xx. A SIP WebSocket client cannot learn its own transport address and puts a random .invalid host in Via and Contact. RFC 7118 Appendix B.1: "The Contact URI provided by SIP UAs requesting (and receiving) Outbound support is not used for routing requests to those UAs, thus it is safe to set a random domain in the Contact URI hostport." Such a client is reachable only over the WebSocket connection it opened.

Fix

In both orphan-2xx paths of outgoing_recv(), give the ACK and BYE the transport of the original INVITE client transaction (NTATAG_TPORT(orq->orq_tport)).

  • outgoing_create() sends over that transport only when it is a secondary transport, i.e. an established connection (TCP, TLS, WS, WSS). For a primary transport such as UDP it only takes the protocol name from it, so routing does not change there.
  • A cancelled INVITE transaction keeps its transport reference until outgoing_free(), so orq_tport is either valid or NULL here. When it is NULL, nothing changes.
  • nta_msg_ackbye() is public API, so it keeps its signature and calls a static helper that takes the transport.

Testing

FreeSWITCH 1.11.3 with sofia-sip 1.13.18 (the changed code is identical on master), JsSIP in Chromium over WSS, with a 120 ms one-way delay added between the browser and FreeSWITCH (the round trip where we first saw the problem was about 240 ms). 54 originates, each cancelled 20-80 ms after it was sent, then a probe originate about every 0.5 s:

without patch with patch
probes rejected with 486 after the cancels 51 1
time until the client accepted calls again about 27 s under 1 s
"transport ws is not supported" in the log 11 0

With the patch, the client received the ACK and BYE in all 15 collisions. The one 486 was an INVITE that arrived while the ACK and BYE were still on the way.

We have been running this patch in production since 2026-10-11.

🤖 Generated with Claude Code

…t on

When a 2xx arrives for an INVITE client transaction that was already
cancelled, outgoing_recv() calls nta_msg_ackbye(), which routes the ACK
and BYE by the Contact of the 2xx. A SIP WebSocket client (RFC 7118)
puts a random ".invalid" host there, so the ACK fails with "transport ws
is not supported". The client keeps retransmitting the 2xx for 64*T1
and rejects new INVITEs with 486 meanwhile.

Send the ACK and BYE over the transport of the original INVITE. It is
used only when it is an established connection (TCP, TLS, WS, WSS), so
routing over UDP is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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