Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/dialog/invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,19 @@ impl DialogLayer {
self.inner
.dialogs
.insert(new_dialog_id.to_string(), Dialog::Invite(dialog.clone()));

// RFC 6026 §7.2: the client transaction is in Accepted
// with Timer M (64*T1) armed. Keep receiving until it
// ends so retransmitted 2xx keep being re-ACKed and
// forked 2xx are observed; abandoning the transaction
// here would leave it (and its timers) in the
// endpoint's table and silently stop the re-ACKs.
if let Some(mut tx) = guard.invite_tx.take() {
crate::platform::spawn(async move {
while tx.receive().await.is_some() {}
debug!(id = %new_dialog_id, "accepted transaction drained (Timer M expired)");
});
}
}
_ => {}
}
Expand Down Expand Up @@ -756,6 +769,16 @@ impl DialogLayer {
inner
.dialogs
.insert(new_id.to_string(), Dialog::Invite(dialog_clone.clone()));

// RFC 6026 §7.2: keep draining the Accepted transaction
// until Timer M ends it (re-ACKs retransmitted 2xx,
// observes forked 2xx, and detaches it from the
// endpoint's table). See do_invite for the rationale.
let confirmed_id = new_id.clone();
crate::platform::spawn(async move {
while tx.receive().await.is_some() {}
debug!(id = %confirmed_id, "accepted transaction drained (Timer M expired)");
});
}
}
Err(e) => debug!(%id0, error = %e, "async invite failed"),
Expand Down
15 changes: 14 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,20 @@
//!
//! * **RFC 3261** - SIP: Session Initiation Protocol (core specification)
//! * **RFC 3581** - Symmetric Response Routing (rport)
//! * **RFC 6026** - Correct Transaction Handling for 2xx Responses to INVITE
//! * **RFC 6026** - Correct Transaction Handling for 2xx Responses to INVITE.
//! Server `Accepted` state + Timer L (§7.1); client `Accepted` state + Timer M
//! (§7.2). Two documented deviations keep the rsipstack 0.5.x wire behavior
//! inside the new state machine. Client deviation: the transaction auto-ACKs
//! a 2xx (and re-ACKs every retransmitted/forked 2xx) while parked in
//! `Accepted`; strict §7.2 + RFC 3261 §17.1.1.3 place that responsibility on
//! the TU — set `EndpointOption::auto_ack_2xx = false` for strict TU-owned
//! ACKs (proxy mode: the 2xx then terminates the transaction at once).
//! Server deviation: the transaction keeps Timer G (T1, doubling to T2,
//! RFC 3261 §13.3.1.4) while in `Accepted` and stops it when the ACK
//! arrives, because the rsipstack dialog layer does not retransmit 2xx
//! itself. Dialog users must keep receiving the client transaction until
//! Timer M ends it (`DialogLayer::do_invite`/`do_invite_async` do this);
//! the absorption window delivers every retransmitted/forked 2xx to the TU.
//!
//! ## Performance
//!
Expand Down
85 changes: 65 additions & 20 deletions src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,37 +43,48 @@ pub type TransactionSender = UnboundedSender<Transaction>;
///
/// `TransactionState` represents the various states a SIP transaction can be in
/// during its lifecycle. These states implement the transaction state machines
/// defined in RFC 3261 for both client and server transactions.
/// defined in RFC 3261 for both client and server transactions, with the RFC 6026
/// `Accepted` state for INVITE 2xx response handling.
///
/// # States
///
/// * `Nothing` - Initial state for client transactions created
/// * `Calling` - Initial state for client transactions when request is sent or received
/// * `Trying` - Request has been sent/received, waiting for response/processing
/// * `Proceeding` - Provisional response received/sent (1xx except 100 Trying)
/// * `Completed` - Final response received/sent, waiting for ACK (INVITE) or cleanup
/// * `Confirmed` - ACK received/sent for INVITE transactions
/// * `Accepted` - INVITE transaction received/sent a 2xx final response (RFC 6026 §7.1/§7.2);
/// server transaction waits for ACKs and absorbs 2xx retransmissions from the TU until
/// Timer L fires; client transaction absorbs 2xx retransmissions from the server until
/// Timer M fires.
/// * `Completed` - Final non-2xx response received/sent (RFC 3261 §17), waiting for ACK (server INVITE)
/// or response retransmissions (client). For INVITE 2xx, see `Accepted` (RFC 6026 supersedes RFC 3261 §17.2.1 paragraph 4 / §17.1.1.2).
/// * `Confirmed` - ACK received/sent for INVITE non-2xx (3xx-6xx) transactions
/// * `Terminated` - Transaction has completed and is being cleaned up
///
/// # State Transitions
///
/// ## Client Non-INVITE Transaction
/// ## Client Non-INVITE Transaction (RFC 3261 §17.1.2)
/// ```text
/// Nothing → Calling → Trying → Proceeding → Completed → Terminated
/// ```
///
/// ## Client INVITE Transaction
/// ## Client INVITE Transaction (RFC 3261 §17.1.1 + RFC 6026 §7.2)
/// ```text
/// Nothing → Calling → Trying → Proceeding → Completed → Terminated
/// ↓
/// Confirmed → Terminated
/// Nothing → Calling → Trying → Proceeding ──2xx──→ Accepted ──Timer M──→ Terminated
/// │
/// └──3xx-6xx──→ Completed → Confirmed → Terminated
/// ```
///
/// ## Server INVITE Transaction (RFC 3261 §17.2.1 + RFC 6026 §7.1)
/// ```text
/// Calling → Trying → Proceeding ──2xx──→ Accepted ──Timer L──→ Terminated
/// │
/// └──3xx-6xx──→ Completed ──ACK──→ Confirmed → Terminated
/// ```
///
/// ## Server Transactions
/// ## Server Non-INVITE Transaction (RFC 3261 §17.2.2)
/// ```text
/// Calling → Trying → Proceeding → Completed → Terminated
/// ↓
/// Confirmed → Terminated (INVITE only)
/// ```
///
/// # Examples
Expand All @@ -87,17 +98,28 @@ pub type TransactionSender = UnboundedSender<Transaction>;
/// TransactionState::Calling => println!("Request sent"),
/// TransactionState::Trying => println!("Request sent/received"),
/// TransactionState::Proceeding => println!("Provisional response"),
/// TransactionState::Completed => println!("Final response"),
/// TransactionState::Accepted => println!("2xx accepted; waiting for Timer L/M"),
/// TransactionState::Completed => println!("Final non-2xx response"),
/// TransactionState::Confirmed => println!("ACK received/sent"),
/// TransactionState::Terminated => println!("Transaction complete"),
/// // `#[non_exhaustive]`: downstream code MUST keep a wildcard arm to
/// // remain forward-compatible with future state additions.
/// _ => println!("future RFC-extension state"),
/// }
/// ```
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TransactionState {
Nothing,
Calling,
Trying,
Proceeding,
/// RFC 6026 §7.1 (server) / §7.2 (client): an INVITE transaction has sent or received
/// a 2xx final response. The server absorbs 2xx retransmissions from the TU and waits
/// for ACK until Timer L (= 64*T1) fires; the client absorbs server-retransmitted 2xx
/// responses until Timer M (= 64*T1) fires. Replaces the RFC 3261 §17.1.1.2 / §17.2.1
/// behaviour that incorrectly routed 2xx through the `Completed` state.
Accepted,
Completed,
Confirmed,
Terminated,
Expand All @@ -110,6 +132,7 @@ impl core::fmt::Display for TransactionState {
TransactionState::Calling => write!(f, "Calling"),
TransactionState::Trying => write!(f, "Trying"),
TransactionState::Proceeding => write!(f, "Proceeding"),
TransactionState::Accepted => write!(f, "Accepted"),
TransactionState::Completed => write!(f, "Completed"),
TransactionState::Confirmed => write!(f, "Confirmed"),
TransactionState::Terminated => write!(f, "Terminated"),
Expand Down Expand Up @@ -186,21 +209,22 @@ impl core::fmt::Display for TransactionType {
/// SIP Transaction Timers
///
/// `TransactionTimer` represents the various timers used in SIP transactions
/// as defined in RFC 3261. These timers ensure reliable message delivery
/// and proper transaction cleanup.
/// as defined in RFC 3261 and RFC 6026. These timers ensure reliable message
/// delivery and proper transaction cleanup.
///
/// # Timer Types
///
/// * `TimerA` - Retransmission timer for client transactions (unreliable transport)
/// * `TimerB` - Transaction timeout timer for client transactions
/// * `TimerC` - Proceeding timeout for client INVITE
/// * `TimerD` - Wait timer for response retransmissions (client)
/// * `TimerE` - Retransmission timer for non-INVITE server transactions
/// * `TimerF` - Transaction timeout timer for non-INVITE server transactions
/// * `TimerK` - Wait timer for ACK (server INVITE transactions)
/// * `TimerG` - Retransmission timer for INVITE server transactions
/// * `TimerK` - Wait timer for ACK (server INVITE non-2xx) / cleanup (client non-INVITE)
/// * `TimerG` - Retransmission timer for INVITE server transactions (non-2xx only per RFC 6026 §7.1)
/// * `TimerL` - RFC 6026 §7.1 server INVITE Accepted-state timer (64*T1)
/// * `TimerM` - RFC 6026 §7.2 client INVITE Accepted-state timer (64*T1)
/// * `TimerCleanup` - Internal cleanup timer for transaction removal
///
/// # Timer Values (RFC 3261)
/// # Timer Values (RFC 3261 + RFC 6026)
///
/// * T1 = 500ms (RTT estimate)
/// * T2 = 4s (maximum retransmit interval)
Expand All @@ -212,8 +236,10 @@ impl core::fmt::Display for TransactionType {
/// * Timer D: 32 seconds for unreliable, 0 for reliable transports
/// * Timer E: starts at T1, doubles up to T2
/// * Timer F: 64*T1 (32 seconds)
/// * Timer G: starts at T1, doubles up to T2
/// * Timer G: starts at T1, doubles up to T2 (non-2xx final response retransmits only)
/// * Timer K: T4 for unreliable, 0 for reliable transports
/// * **Timer L: 64*T1 (32 seconds) — RFC 6026 §7.1**
/// * **Timer M: 64*T1 (32 seconds) — RFC 6026 §7.2**
///
/// # Examples
///
Expand Down Expand Up @@ -245,6 +271,9 @@ impl core::fmt::Display for TransactionType {
/// TransactionTimer::TimerB(key) => {
/// println!("Transaction {} timed out", key);
/// },
/// // `#[non_exhaustive]`: downstream code MUST keep a wildcard arm to
/// // remain forward-compatible with future timer additions (e.g. RFC
/// // extensions like Timer L / Timer M / future RFC variants).
/// _ => {}
/// }
/// # Ok(())
Expand All @@ -258,13 +287,25 @@ impl core::fmt::Display for TransactionType {
/// * Cancelled when leaving states or receiving responses
/// * Fire events that drive state machine transitions
/// * Handle retransmissions and timeouts
#[non_exhaustive]
pub enum TransactionTimer {
TimerA(TransactionKey, Duration),
TimerB(TransactionKey),
TimerC(TransactionKey),
TimerD(TransactionKey),
TimerK(TransactionKey),
TimerG(TransactionKey, Duration),
/// RFC 6026 §7.1: server INVITE Accepted-state timer. Fires once at 64*T1
/// after a 2xx final response is sent, then transitions the transaction to
/// Terminated. Absorbs ACKs for the 2xx and late 2xx retransmissions from
/// the TU. Replaces the RFC 3261 §17.2.1 Timer K (T4) usage for 2xx
/// responses, which was too short to handle proxy-chain ACK fan-in.
TimerL(TransactionKey),
/// RFC 6026 §7.2: client INVITE Accepted-state timer. Fires once at 64*T1
/// after a 2xx final response is received, then transitions the
/// transaction to Terminated. Absorbs server-retransmitted 2xx responses
/// (the TU is responsible for the ACK).
TimerM(TransactionKey),
TimerCleanup(TransactionKey),
}

Expand All @@ -277,6 +318,8 @@ impl TransactionTimer {
TransactionTimer::TimerD(key) => key,
TransactionTimer::TimerG(key, _) => key,
TransactionTimer::TimerK(key) => key,
TransactionTimer::TimerL(key) => key,
TransactionTimer::TimerM(key) => key,
TransactionTimer::TimerCleanup(key) => key,
}
}
Expand All @@ -295,6 +338,8 @@ impl core::fmt::Display for TransactionTimer {
write!(f, "TimerG: {} {}", key, duration.as_millis())
}
TransactionTimer::TimerK(key) => write!(f, "TimerK: {}", key),
TransactionTimer::TimerL(key) => write!(f, "TimerL: {}", key),
TransactionTimer::TimerM(key) => write!(f, "TimerM: {}", key),
TransactionTimer::TimerCleanup(key) => write!(f, "TimerCleanup: {}", key),
}
}
Expand Down
37 changes: 21 additions & 16 deletions src/transaction/tests/test_auto_ack_2xx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ async fn test_default_auto_acks_2xx_once() -> Result<()> {
let mut tx = Transaction::new_client(key, invite, inner.clone(), None);
tx.send().await?;

// The 2xx reaches the TU exactly once ...
// The 2xx reaches the TU and the transaction enters Accepted with
// Timer M armed (RFC 6026 §7.2; the auto-ACK is the documented
// rsipstack 0.5.x deviation).
let first = tokio::time::timeout(Duration::from_secs(2), tx.receive())
.await
.expect("timeout waiting for the 200")
Expand All @@ -173,18 +175,21 @@ async fn test_default_auto_acks_2xx_once() -> Result<()> {
}
other => panic!("expected the 200 OK, got {other}"),
}
// ... a retransmitted 200 OK must not reach the TU again: after the
// auto-ACK the transaction terminates and the TU channel closes, so
// receive() returns None instead of ever repeating the 2xx.
let next = tokio::time::timeout(Duration::from_millis(1200), tx.receive()).await;
assert!(
matches!(next, Ok(None)),
"the TU channel must close after the auto-ACK (no duplicate 2xx), got {next:?}"
);
// RFC 3261 §17.1.1.2: the client INVITE transaction terminates
// immediately after ACKing the 2xx; retransmissions are absorbed
// below the TU (finished_transactions replays the stored ACK).
assert_eq!(tx.state, TransactionState::Terminated);
assert_eq!(tx.state, TransactionState::Accepted);

// A retransmitted 200 OK is delivered to the TU again (§7.2) and
// re-ACKed below the TU.
let second = tokio::time::timeout(Duration::from_millis(1200), tx.receive())
.await
.expect("timeout waiting for the retransmitted 200")
.expect("transaction ended before the retransmitted 200");
match second {
SipMessage::Response(ref resp) => {
assert_eq!(resp.status_code.code(), 200);
}
other => panic!("expected the retransmitted 200 OK, got {other}"),
}
assert_eq!(tx.state, TransactionState::Accepted);
Ok::<_, crate::Error>(())
};

Expand Down Expand Up @@ -212,10 +217,10 @@ async fn test_default_auto_acks_2xx_once() -> Result<()> {
"the ACK must carry the 2xx's To tag, got {ack}"
);

// Each retransmitted 200 OK is absorbed with a re-ACK (the endpoint
// replays the stored ACK), never forwarded to the TU.
// Each retransmitted 200 OK is re-ACKed (RFC 6026 §7.2 absorption with
// the documented auto-ACK deviation).
let re_ack = next_wire_event(&mut peer, Duration::from_millis(1100)).await;
let re_ack = re_ack.expect("a retransmitted 200 OK must be re-ACKed (absorbed)");
let re_ack = re_ack.expect("a retransmitted 200 OK must be re-ACKed");
assert!(
re_ack.contains("CSeq: 1 ACK"),
"the re-ACK must carry the INVITE's CSeq, got {re_ack}"
Expand Down
Loading
Loading