Skip to content

Sync onto upstream 0.7.3 (2dfa0d1) - #21

Merged
tgeorge06 merged 28 commits into
mainfrom
sync/upstream-2dfa0d1
Oct 8, 2026
Merged

tgeorge06 merged 28 commits into
mainfrom
sync/upstream-2dfa0d1

Conversation

@tgeorge06

Copy link
Copy Markdown
Owner

Sync the fork onto upstream 0.7.3 (2dfa0d1, crates.io). 0.7.3 contains every fix the fork offered upstream, so upstream's version replaces the fork's variant for:

row upstream fork variant dropped
R2 restsend#147 (R19 kept, see below)
R3 restsend#148 transition under the lock
R21 restsend#183 the take_dialog guard variant. Upstream's guard handles a removed dialog too; it re-sends the CANCEL (same branch) in Trying/Early, so the peer sees a CANCEL retransmission
R22b restsend#178 WARNs without the SIP message
R23 (client half) restsend#176 get_client_dialog_by_call_id UAC only. The fork's UAS-only check in get_or_create_server_invite is dropped: rcx routes in-dialog requests through match_dialog / get_dialog
R24 (part) restsend#180 a BYE ends the dialog whatever its transaction returns. A UAC bye() that fails now returns the error (fork returned Ok)
R27 restsend#174 Confirmed carries the 2xx its ACK confirms

Still fork-only (rcx needs them until its ELIMINATE PRs land): R15, R16, R17, R18, R19 (no Early notified for a 1xx to an in-dialog request), R20, R22a, R24 (a UAS notifies Terminated before sending its BYE), R25, R28 (Response::wire_reason, #20), and RECURSIVECX.md.

Tests

  • cargo test --features bench: 427 + 65 passed. cargo test: 426 + 65 passed.
  • cargo check --no-default-features --features platform-embassy: clean.
  • clippy --all-targets --features bench: same warnings as upstream 0.7.3, none new.
  • rustfmt --check on every file that differs from 2dfa0d1: clean.
  • Upstream's test_warn_logs now expects the WebSocket receive log at DEBUG (R22a). test_in_dialog_provisional asserts R19 on upstream's file.
  • rcx (vector-ventures/recursivecx) compiles unchanged against this branch; 783 targeted rcx tests pass (originate, transfer/REFER, renegotiation, session timer, CANCEL, BYE, the RWI trunk and queue ring-accept e2e suites).
  • I ran the removed fork tests against this base before deleting them. test_late_reinvite_ack passes, and so do the taken-dialog races except the "no second CANCEL" assertion (R21, dropped on purpose). The R23 server lookup test and the UAC BYE Ok test fail, as expected for the dropped behavior.

Fork diff (git diff --stat): vs 2dfa0d1, 50 files +2186/-2094 before, 38 files +1505/-78 after.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L1Gu5CifqgBjASbxYmJ6mr

tgeorge06 and others added 28 commits October 6, 2026 09:31
send_dialog_request moved the dialog to Early on every non-100
provisional, including responses to our own re-INVITE or UPDATE on an
established dialog. A 183 to a session-refresh re-INVITE therefore
regressed a Confirmed dialog to Early for good (the final 200 does not
restore it): bye() is then refused outside Confirmed and hangup()
falls through to a CANCEL of the long-completed INVITE, which times
out, so the call can no longer be torn down.

RFC 3261 §12 has a dialog move from early to confirmed and never back,
and a provisional to a mid-dialog request does not create early state.
Only take the Early transition while the dialog is still in a
pre-confirmation state (Calling / Trying / Early). On a confirmed
dialog the provisional is still notified as before, so callers keep
seeing it, but the stored state stays Confirmed. The initial INVITE
path (process_invite) is unchanged, and reliable provisionals to a
re-INVITE are still PRACKed.

Adds tests driving a raw UDP peer: the initial INVITE's 183 still
reports Early, while a 100/183/200 to an in-dialog re-INVITE or UPDATE
keeps the dialog Confirmed, the 183 is still notified, and BYE
succeeds.
DialogInner::transition sent the new state to subscribers before
deciding whether to apply it, so ignored transitions were still
notified: a second Terminated when two teardown paths race (a local BYE
completing while the peer's BYE is handled), and a WaitAck on an
already confirmed dialog. The late-update check added in 0.7.0 reads
the state under a lock it then releases, so a concurrent transition can
still slip a notification past it.

Decide under the state lock, update the state, then send the
notification while still holding the lock, so every notification
describes a transition that happened and notifications follow the
order of state changes. Updates arriving after Terminated, including
the event-only variants (Updated/Notify/Info/Options/Refer), stay
dropped as in 0.7.0; while the dialog is live, event-only variants are
sent as before.

Adds an end-to-end test over UDP through DialogLayer: the peer sends an
INFO, then a BYE before the application answers it; once the INFO is
answered, no Confirmed may follow the Terminated notification.
…d (RFC 3515)

handle_refer left the dialog without the closing return_to_confirmed
that handle_message/handle_notify have: answering a REFER (usually 202)
never re-surfaced a Confirmed event, so TU-side logic waiting on it —
e.g. an active-call bot's pending 100 Trying NOTIFY for the implicit
refer subscription — starved, and the referrer saw the 202 but no
NOTIFY ever. The stored dialog state was always correct (in-dialog
request events do not change it); only the notification was missing.

Adds a regression test that fails without the fix: answer the REFER,
expect a Confirmed event and a working notify_refer.
… ends it

Since restsend#169 the matching ACK terminates an Accepted server INVITE
transaction, and cleanup() took last_response before the dialog built
DialogState::Confirmed from it. Confirmed carried Response::default()
(no CSeq) for the initial INVITE and every re-INVITE, so a TU could not
tell which INVITE it confirms.

The server INVITE transaction now keeps last_response and hands a copy
to finished_transactions.
Since the unified `Dialog::Invite(InviteDialog)` (0.6.0) the lookup
matches every INVITE dialog with the Call-ID, so a B2BUA that keeps the
Call-ID across its legs gets the inbound (UAS) leg back as a client
dialog. Check the role, as the documentation says ("client-side INVITE
dialogs (UAC)") and as `Dialog::ClientInvite` did in 0.5.x.
Some WARN records carried a whole SIP message, with the From/To/Contact
URIs, display names and any body:

- the dialog layer's "failed to send request" (send_prack_request and
  send_dialog_request) printed the full request;
- the WebSocket transport's "Error parsing SIP message" printed the raw
  text frame;
- the "bye skipped" WARN of ClientInviteDialog, InviteDialog and
  ServerInviteDialog printed the dialog state with Debug, which for
  Early, WaitAck and Confirmed includes the whole response; the returned
  error did the same.

The send failure now logs the method at WARN and the request at DEBUG.
The WebSocket parse failure logs the frame length; the frame is already
logged at INFO when it is received. The BYE paths use the Display of
DialogState (dialog id and state name).
RFC 3261 §15.1.1: the session ends once the BYE is passed to the client
transaction, and the dialog ends on a 481, a 408 or no response.
`bye_with_headers` notified `Terminated` only after the BYE transaction
returned Ok. When it returned an error (the target locator fails, or a
401/407 to the BYE cannot be answered) the dialog stayed `Confirmed`, and
callers that remove the dialog on `Terminated` kept it forever.

`DialogInner::send_bye`, used by `InviteDialog` and both deprecated
wrappers, notifies `Terminated` (`UacBye` / `UasBye`, as before) whatever
the transaction returns and still returns its error.
- platform::tls: TlsConnector/TlsStream/TcpStream traits (poll-based,
  no_std-safe) + TlsClientConfig + process-wide connector registry
- transport::tls::TlsConnection::connect consults the registered
  connector first (embedded backends: embassy-net + embedded-tls etc.,
  the connector owns the TCP dial); without one, the built-in rustls
  path is unchanged
- client halves are boxed so rustls and seam streams share one
  TlsConnectionInner::Client representation
- e2e test: a registered connector bypasses rustls entirely
- readme: no_std/embedded support notes
…rly-regression

fix: keep confirmed dialogs confirmed on 1xx to in-dialog requests
…ter-terminated

fix: notify dialog state only for transitions that are applied
…the-final-response

fix(transaction): keep a server INVITE's final response after the ACK ends it
…pect-role

fix(dialog): get_client_dialog_by_call_id returns only UAC dialogs
…sip-message

fix: keep whole SIP messages out of WARN logs
…g-whatever-the-outcome

fix(dialog): a BYE ends the dialog even when its transaction fails
Every 2xx to the INVITE with a new To tag establishes its own dialog.
The transaction already re-ACKs a forked 2xx with its own tag and
remote target (restsend#172), but the Accepted-window drainer dropped it: no
dialog was created for the branch and no BYE was ever sent, so the
forked callee kept retransmitting its 2xx until it gave up.

The drainer now inspects every message the client INVITE transaction
delivers after confirmation: a 2xx whose To tag differs from the
confirmed dialog's is ended with a BYE built from that response's own
Contact (remote target) and Record-Route (route set), CSeq continuing
the forked dialog's. The confirmed dialog's state is untouched, no
dialog is registered for the branch, and no state is notified.
fix(dialog): BYE a forked 2xx's dialog (RFC 3261 §13.2.2.4)
When the `do_invite` future is dropped mid-INVITE, `DialogGuardForUnconfirmed`
finds the dialog by removing it from the `DialogLayer` and did nothing when it
was not there. An application that called `remove_dialog` before dropping the
future (for example on its own hangup) therefore got no `Terminated`, no
CANCEL after a provisional, and no BYE for a 2xx that answered the abandoned
INVITE, leaving the callee in a session.

The guard now keeps the INVITE's dialog and falls back to it when the layer
entry is gone, until `process_invite` returns. A dialog that is still
registered is handled exactly as before.
…ixes

1. A provisional response to an in-dialog request was still notified
   through the raw state_sender when the dialog had already terminated
   (the else arm of the restsend#147 fix bypassed transition's Terminated
   guard). A 1xx racing a peer BYE could notify Early after Terminated,
   breaking the nothing-after-Terminated contract. The fallback now
   checks is_terminated first.

2. The forked-2xx drainer sent one BYE per received 2xx, so a forked
   2xx retransmission in flight before its ACK landed produced duplicate
   BYEs. The drainer now tracks the forked tags it already ended and
   sends one BYE per branch.

Both regressions are pinned by tests that fail without the fixes:
test_provisional_after_terminated_is_not_notified and the retransmission
window of test_forked_2xx_is_byed_without_touching_the_confirmed_dialog.
…rd-after-remove-dialog

fix(dialog): end a dropped INVITE whose dialog was already removed
Follow-up to restsend#183. The removed-dialog variant of the drop guard is only
exercised with a crossing 2xx in the InviteOkFirst ordering. Two gaps:

- A CANCEL that wins the race (487 to the INVITE) with the dialog already
  removed: run_cancel_answered_487 now takes a provisional code and a
  remove flag, so test_removed_cancel_answered_487_sends_no_bye covers
  Early (180) and Trying (100) — CANCEL, ACK of the 487, no BYE, exactly
  one Terminated(UacCancel), never Confirmed. The pre-existing test gains
  the same Terminated/Confirmed assertions.
- The CancelOkFirst wire ordering (200 to the CANCEL before the 2xx) with
  the dialog removed:
  test_removed_dialog_2xx_after_the_cancel_response_is_acked_and_byed.
…InviteDialog

The deprecated ClientInviteDialog has its own handle_reinvite, which
missed the teardown that InviteDialog and ServerInviteDialog apply
(RFC 3261 13.3.1.4): when the callee re-INVITEs a UAC dialog handled
through the wrapper and never ACKs the 2xx, the 2xx was retransmitted
until 64*T1 and then nothing happened. The dialog stayed Confirmed,
with no event and no BYE.

Track the 2xx and the ACK as the other two handlers do and call
end_session_without_ack, so the dialog ends with
TerminatedReason::Timeout and a BYE.
…k fails at once

When the target locator fails, Transaction::send returns before the
transaction enters Calling, so no timer is armed and nothing is ever
sent. A server dialog then looks for a dial-back address; with none it
went on to wait in tx.receive() for a response that cannot come, and
BYE, re-INVITE, INFO and every other in-dialog request never returned.

Return the send error in that case, as a client dialog already does.
The dial-back retry is unchanged.
…ceeding

A non-INVITE client transaction runs Timer F as Timer B, and on_timer
handled Timer B only in Calling and Trying. After a provisional other
than 100 (e.g. 180 to a BYE) the transaction is in Proceeding, Timer F
was ignored there, and a request answered with one 1xx and then nothing
never ended. RFC 3261 section 17.1.2.2: if Timer F fires in Proceeding,
the TU must be informed of a timeout and the transaction terminated.

Handle Timer B for a non-INVITE client in Proceeding like Timer C: a
local 408 to the TU, which terminates the transaction.
…og-unacked-reinvite

fix(dialog): end the session on a never-ACKed re-INVITE 2xx in ClientInviteDialog
…without-route-fails-fast

fix(dialog): a server in-dialog request with no route and no dial-back fails at once
fix(transaction): Timer F ends a non-INVITE client transaction in Proceeding
0.7.3 contains every fix the fork offered upstream. Upstream's version
replaces the fork's for:

- R2 (restsend#147): a 1xx to an in-dialog request does not regress Confirmed.
- R3 (restsend#148): only applied dialog transitions are notified.
- R21 (restsend#183): a dropped INVITE whose dialog was removed still ends. The
  fork's take_dialog variant (no second CANCEL) is gone.
- R22b (restsend#178): WARN logs without the SIP message.
- R23 client half (restsend#176): get_client_dialog_by_call_id returns UAC
  dialogs only. The fork's UAS-only check in get_or_create_server_invite
  is dropped.
- R24 (restsend#180): a BYE ends the dialog whatever its transaction returns.
- R27 (restsend#174): Confirmed carries the 2xx its ACK confirms.

Still fork-only: R15, R16, R17, R18, R19 (no Early notified for an
in-dialog 1xx), R20, R22a, R24 (a UAS notifies Terminated before its
BYE), R25, R28 (wire_reason), RECURSIVECX.md.

Upstream's test_warn_logs now expects the WebSocket receive log at DEBUG
(R22a). Fork tests for dropped rows are removed (test_late_reinvite_ack,
the taken-dialog races, the role-typed lookup test, the UAC BYE test).
@tgeorge06
tgeorge06 merged commit 70a4341 into main Oct 8, 2026
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.

2 participants