Repository navigation
lib.rs claims RFC 6026 but transaction layer skips Accepted/Timer L/Timer M (§4, §7.1, §7.2, §8.5) #127
Description
Hey, I'm using rsipstack 0.5.12 for a SIP↔Telegram voice bridge and ran into something while auditing the transaction layer. Wanted to surface it before I assume the worst.
src/lib.rs:211 says:
//! * **RFC 6026** - Correct Transaction Handling for 2xx Responses to INVITE
But reading the transaction code, I don't see RFC 6026 actually implemented — and the gap looks like several explicit MUSTs in the spec. Quick rundown:
1. No Accepted state
RFC 6026 §4 (Summary): "It adds a state to the INVITE server state machine to absorb retransmissions of the INVITE after a 2xx response has been sent." §7.1 introduces this state by name.
src/transaction/mod.rs:84-92:
pub enum TransactionState {
Nothing, Calling, Trying, Proceeding,
Completed, Confirmed, Terminated,
}2. Server INVITE 2xx goes to Completed, not Accepted
RFC 6026 §7.1: "If the SIP element's TU issues a 2xx response for this transaction while the state machine is in the 'Proceeding' state, the state machine MUST transition to the 'Accepted' state and set Timer L to 64*T1…"
§8.5 restates this as the replacement for RFC 3261 §17.2.1 paragraph 4: "…The server transaction MUST then transition to the 'Accepted' state."
src/transaction/transaction.rs:348-357:
let new_state = match response.status_code.kind() {
StatusCodeKind::Provisional => match response.status_code {
StatusCode::Trying => TransactionState::Trying,
_ => TransactionState::Proceeding,
},
_ => match self.transaction_type {
TransactionType::ServerInvite => TransactionState::Completed,
_ => TransactionState::Terminated,
},
};2xx and non-2xx final responses both end up in Completed.
3. Server transaction retransmits 2xx via Timer G
RFC 6026 §7.1: "The server transaction MUST NOT generate 2xx retransmissions on its own." §8.5 reinforces: "It is not retransmitted by the server transaction; retransmissions of 2xx responses are handled by the TU."
src/transaction/transaction.rs:744-770 — when Timer G fires in Completed state, it resends last_response. For a 2xx, this is exactly what §7.1 forbids:
TransactionState::Completed => {
if let TransactionTimer::TimerG(key, duration) = timer {
if let Some(last_response) = &self.last_response {
if let Some(connection) = &self.connection {
// ...inspector pass-through elided...
connection.send(last_response, ...).await?;
}
}
// restart Timer G with an upper limit
let duration = (duration * 2).min(self.endpoint_inner.option.t1x64);
// ...
}
}4. No Timer L; effective ACK window is ~T4 (≈5s), not 64*T1 (≈32s)
RFC 6026 §7.1: "set Timer L to 64*T1, where T1 is the round-trip time estimate" and "Timer L reflects the amount of time the server transaction could receive 2xx responses for retransmission from the TU while it is waiting to receive an ACK."
src/transaction/mod.rs:249-257 — no Timer L variant:
pub enum TransactionTimer {
TimerA(...), TimerB(...), TimerC(...), TimerD(...),
TimerK(...), TimerG(...), TimerCleanup(...),
}src/transaction/transaction.rs:837-877 (entry to Completed for ServerInvite) starts Timer G, Timer K (= T4, default 5s), and Timer D (= 64*T1, default 32s); lines 771-780 transition to Terminated on either Timer K or Timer D. Timer K fires first, so the effective wait for an ACK after sending a 2xx is ~5s rather than RFC 6026's 32s.
5. Client INVITE 2xx skips Accepted + Timer M
RFC 6026 §7.2: "If a 2xx response is received while the client INVITE state machine is in the 'Calling' or 'Proceeding' states, it MUST transition to the 'Accepted' state, pass the 2xx response to the TU, and set Timer M to 64*T1." §8.4 restates this as the replacement for RFC 3261 §17.1.1.2.
src/transaction/transaction.rs:660-666:
_ => {
if self.transaction_type == TransactionType::ClientInvite {
TransactionState::Completed
} else {
TransactionState::Terminated
}
}ClientInvite goes to Completed on any non-provisional response, including 2xx. No Timer M anywhere in the codebase.
6. No tests reference RFC 6026
grep -rn '6026\|Accepted state\|TimerL\|TimerM' src/transaction/tests/ → no hits across mod.rs, test_client.rs, test_endpoint.rs, test_provisional_responses.rs, test_server.rs, test_transaction_states.rs.
Question: is RFC 6026 intended to be supported, with the current state machine being a deliberate design choice (e.g. relying on waiting_ack for 2xx-ACK routing instead of the spec-prescribed flow)? Or is the lib.rs claim aspirational?
For my project (UAS only, direct PBX → bridge over UDP, no proxy chain, no forking, no outbound INVITE), the current behavior is functionally OK — waiting_ack resolves the ACKs and Timer G handles 2xx retransmits within the ~5s Timer K window. So I'm not blocked. I just want my own audit trail to agree with reality.
If you'd like a PR adding Accepted / TimerL / TimerM and a strict-RFC-6026 SIPp scenario, happy to scope it. Otherwise it would help to soften the lib.rs wording.
Thanks for rsipstack — clean dependency to work with otherwise.
Activity
shenjinti commentedon Jun 17, 2026
Thanks, you're completely right.
RFC-6026 not implemented yet. The comment is aspirational.
A PR would be very welcome.
fix(transaction): pass every 2xx in Accepted to TU per RFC 6026 §7.2 …
docs(lib): clarify RFC 6026 client-side auto-ACK retention (13/N)
feat(transaction): mark TransactionState + TransactionTimer #[non_exh…
refactor(transaction): tighten Accepted timer dispatch (15/N)
fix(transaction): log auto-ACK failures instead of silent .ok() (16/N)
docs(transaction): cancel-safety rustdoc on async public APIs (17/N)
feat(transaction): add Accepted state for RFC 6026 §7.1/§7.2 (1/N)
feat(transaction): add TimerL + TimerM variants for RFC 6026 (2/N)
feat(transaction): allow RFC 6026 edges in can_transition matrix (3/N)
feat(transaction): add timer_l + timer_m fields on Transaction (4/N)
feat(transaction): Accepted-state entry handler per RFC 6026 §7.1/§7.…
feat(transaction): on_timer Accepted dispatch + Completed 2xx guard (…
feat(transaction): server-side RFC 6026 2xx routing + ACK handling (7/N)
feat(transaction): client-side RFC 6026 2xx routing + send_ack split …
test(transaction): RFC 6026 conformance tests for Accepted state (9/N)
docs(lib): expand RFC 6026 standards-compliance claim (10/N)
fix(transaction): pass every 2xx in Accepted to TU per RFC 6026 §7.2 …
docs(lib): clarify RFC 6026 client-side auto-ACK retention (13/N)
feat(transaction): mark TransactionState + TransactionTimer #[non_exh…
refactor(transaction): tighten Accepted timer dispatch (15/N)
fix(transaction): log auto-ACK failures instead of silent .ok() (16/N)
docs(transaction): cancel-safety rustdoc on async public APIs (17/N)
docs(transaction): correct rustdoc and comment accuracy on touched AP…
refactor(transaction): drop dead wildcard arm in send_ack post-send d…
style: cargo fmt cleanup on rebased F1 hunk (20/N)
andrico21 commentedon Jun 17, 2026
PR #128 opened: implements RFC 6026 §7.1-§7.2 across 20 atomic commits, rebased onto current
main(8928174).Validation:
cargo test --lib247/0 +cargo test --doc65/0 +cargo fmt --checkclean. Pre-existing 66 clippy errors onmainare baseline; touched files contribute zero new clippy regressions.A few notes worth flagging up front:
TransactionStateandTransactionTimerare marked#[non_exhaustive]- itself a breaking change. Recommend targeting 0.6.0.lib.rs::Standards Compliance. Removal is a candidate for a follow-up PR that migrates 2xx-ACK responsibility to the dialog layer.Happy to iterate on review comments. Thanks for the welcome, @shenjinti.
fix: avoid automatic ACKs for proxied INVITE 2xx responses