Repository navigation
A CANCEL for an INVITE that already got its 2xx is answered with 481 (0.7.x), or with a copy of the INVITE's 200 #198
Description
Found while integrating rsipstack in a SIP application; same reporter as #192.
Versions affected.
- After the ACK: the CANCEL is answered with a copy of the INVITE's 200, on
0.7.3 (latest) (5/5 runs), 0.6.12 and 0.6.10 (5/5 runs each). - While the 2xx awaits its ACK: 481 on 0.7.3 (5/5 runs). On 0.6.12 and
0.6.10 the answer is the correct200 OKwithCSeq: 1 CANCEL(5/5), so
this half is a regression that came with the Accepted state (feat(transaction): RFC 6026 Accepted state + Timer L/M (adopted from #128) #164).
Environment. rsipstack 0.7.3, the latest version on crates.io, default features. Rust 1.98.1 stable, tokio 1.53 (multi-thread runtime). Linux 6.12 x86_64, 4 CPUs. UDP over loopback only: everything on 127.0.0.1, every port assigned by the OS. Default EndpointOption (T1 = 500 ms, T2 = 4 s, T4 = 5 s).
Where. Line numbers are from the 0.7.3 sources.
- A server-side CANCEL's transaction key is the INVITE's key
(src/transaction/key.rs:41-43). - In Accepted,
Transaction::on_received_requestfalls into the_arm and
answers481 Call/Transaction Does Not Exist
(src/transaction/transaction.rs:834-857). OnlyProceeding/Trying
(forward, then 200) andCompleted(200) are handled. - After the ACK, the INVITE transaction is in
finished_transactions.
on_received_messagefinds it under the CANCEL's key and re-sends the
INVITE's last response ("For INVITE retransmissions, re-send the last
response",src/transaction/endpoint.rs:399-419). The CANCEL itself gets
no response.
How to run.
- Call X: INVITE,
accept(), then a CANCEL (same branch as the INVITE)
before the ACK. - Call Y: INVITE,
accept(), ACK, and a CANCEL 200 ms later.
Reproduction program (one self-contained file, 322 lines) and how to run it
It uses only rsipstack's public API plus a raw tokio::net::UdpSocket peer
on 127.0.0.1. It prints what it observes and exits with 1 when the problem
reproduces, 0 when it does not.
Cargo.toml (replace the [dependencies] section):
[dependencies]
rsipstack = "=0.7.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time", "sync"] }
tokio-util = "0.7"src/main.rs:
//! Issue F: a CANCEL for an INVITE that already got its 2xx is answered with
//! 481 (before the ACK, 0.7.x) or with a copy of the INVITE's 200 (after the
//! ACK). RFC 3261 §9.2: the UAS answers the CANCEL itself (200, or 481 if it
//! matches nothing), with `CSeq: n CANCEL`.
//!
//! Call X: CANCEL while the 2xx is not yet ACKed.
//! Call Y: CANCEL 200 ms after the ACK.
//!
//! Usage: cargo run --release
#![allow(dead_code)] // the harness at the bottom is shared by several issues
use rsipstack::dialog::dialog::DialogState;
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
/// A CANCEL with the INVITE's branch, Call-ID, From, To and CSeq number.
fn cancel_for(peer: &Peer, uas: SocketAddr, call_id: &str) -> String {
let me = peer.addr;
format!(
"CANCEL sip:bob@{uas} SIP/2.0\r\n\
Via: SIP/2.0/UDP {me};branch=z9hG4bK-{call_id}-inv;rport\r\n\
Max-Forwards: 70\r\n\
From: <sip:alice@{me}>;tag=a1\r\n\
To: <sip:bob@{uas}>\r\n\
Call-ID: {call_id}\r\n\
CSeq: 1 CANCEL\r\n\
Content-Length: 0\r\n\r\n"
)
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), BoxError> {
println!(
"== issue F, late CANCEL | {} | 127.0.0.1 only",
rsipstack::VERSION
);
let stack = Stack::start().await?;
let mut calls = stack.uas_loop()?;
let uas = stack.addr;
let mut bad = 0;
for (call_id, ack_first) in [("f-x", false), ("f-y", true)] {
let peer = Peer::bind().await?;
peer.send(uas, &peer.invite(uas, call_id)).await?;
let mut call = calls.recv().await.ok_or("no call")?;
call.spawn_handle();
tokio::time::sleep(Duration::from_millis(20)).await;
call.dialog.accept(None, None)?;
let ok = peer
.recv_until(Duration::from_secs(2), |r| r.is_response(200, "INVITE"))
.await
.ok_or("no 200")?;
if ack_first {
peer.send(uas, &peer.ack_for(uas, &ok)).await?;
tokio::time::sleep(Duration::from_millis(200)).await;
}
peer.send(uas, &cancel_for(&peer, uas, call_id)).await?;
// Replies within 300 ms (call X's next 2xx retransmission is at 500 ms).
let mut got = Vec::new();
let deadline = Instant::now() + Duration::from_millis(300);
while let Some(r) = peer
.recv(deadline.saturating_duration_since(Instant::now()))
.await
{
got.push(format!(
"{} [CSeq: {}]",
r.first_line(),
header(&r.text, "CSeq").unwrap_or_default()
));
}
let label = if ack_first {
"Y (after ACK)"
} else {
"X (before ACK)"
};
println!(
"{label}: CANCEL answered with {got:?}; dialog state: {}",
state_name(&call.dialog.state())
);
let answered_200 = got
.iter()
.any(|l| l.starts_with("SIP/2.0 200") && l.ends_with("CANCEL]"));
if !answered_200 || got.iter().any(|l| l.ends_with("INVITE]")) {
bad += 1;
}
if !ack_first {
peer.send(uas, &peer.ack_for(uas, &ok)).await?;
}
}
verdict(
bad > 0,
&format!("{bad}/2 CANCELs not answered 200 to the CANCEL"),
)
}
// ---- harness: the stack under test and a raw UDP peer, 127.0.0.1 only ----
type BoxError = Box<dyn std::error::Error + Send + Sync>;
fn verdict(reproduced: bool, what: &str) -> ! {
let tag = if reproduced {
"REPRODUCED"
} else {
"not reproduced"
};
println!("RESULT: {tag} - {what}");
std::process::exit(i32::from(reproduced))
}
/// The stack under test: one UDP transport on 127.0.0.1:0, default options.
struct Stack {
endpoint: rsipstack::transaction::Endpoint,
dialogs: std::sync::Arc<rsipstack::dialog::dialog_layer::DialogLayer>,
addr: SocketAddr,
cancel: tokio_util::sync::CancellationToken,
}
impl Stack {
async fn start() -> Result<Stack, BoxError> {
let cancel = tokio_util::sync::CancellationToken::new();
let tl = rsipstack::transport::TransportLayer::new(cancel.child_token());
let udp = rsipstack::transport::udp::UdpConnection::create_connection(
"127.0.0.1:0".parse()?,
None,
Some(cancel.child_token()),
)
.await?;
let addr = udp.get_addr().get_socketaddr()?;
tl.add_transport(udp.into());
let endpoint = rsipstack::EndpointBuilder::new()
.with_user_agent("repro")
.with_transport_layer(tl)
.with_cancel_token(cancel.child_token())
.build();
let inner = endpoint.inner.clone();
tokio::spawn(async move { inner.serve().await });
let dialogs = std::sync::Arc::new(rsipstack::dialog::dialog_layer::DialogLayer::new(
endpoint.inner.clone(),
));
Ok(Stack {
endpoint,
dialogs,
addr,
cancel,
})
}
}
impl Drop for Stack {
fn drop(&mut self) {
self.cancel.cancel();
}
}
/// A new server INVITE dialog, its state receiver and its (not yet handled)
/// transaction.
struct UasCall {
dialog: rsipstack::dialog::invite_dialog::InviteDialog,
states: UnboundedReceiver<DialogState>,
tx: Option<rsipstack::transaction::Transaction>,
}
impl UasCall {
fn spawn_handle(&mut self) {
if let Some(mut tx) = self.tx.take() {
let mut d = self.dialog.clone();
tokio::spawn(async move { d.handle(&mut tx).await });
}
}
}
impl Stack {
/// The usual UAS loop: in-dialog requests go to `match_dialog` + `handle()`;
/// each new INVITE gets `get_or_create_server_invite` and is handed over.
fn uas_loop(&self) -> Result<UnboundedReceiver<UasCall>, BoxError> {
use rsipstack::sip::prelude::HeadersExt;
let mut incoming = self.endpoint.incoming_transactions()?;
let dl = self.dialogs.clone();
let (call_tx, call_rx) = unbounded_channel();
tokio::spawn(async move {
while let Some(mut tx) = incoming.recv().await {
let to_tag = tx
.original
.to_header()
.ok()
.and_then(|h| h.tag().ok().flatten());
if to_tag.is_some() {
if let Some(mut d) = dl.match_dialog(&tx) {
tokio::spawn(async move { d.handle(&mut tx).await });
}
} else if tx.original.method == rsipstack::sip::Method::Invite {
let (state_tx, states) = unbounded_channel();
if let Ok(dialog) = dl.get_or_create_server_invite(&tx, state_tx, None, None) {
let _ = call_tx.send(UasCall {
dialog,
states,
tx: Some(tx),
});
}
}
}
});
Ok(call_rx)
}
}
fn state_name(s: &DialogState) -> String {
match s {
DialogState::Calling(_) => "Calling".into(),
DialogState::Trying(_) => "Trying".into(),
DialogState::Early(..) => "Early".into(),
DialogState::WaitAck(..) => "WaitAck".into(),
DialogState::Confirmed(..) => "Confirmed".into(),
DialogState::Terminated(_, r) => format!("Terminated({r:?})"),
_ => "Other".into(),
}
}
/// The far end: a raw UDP socket that writes and reads SIP text by hand.
struct Peer {
sock: tokio::net::UdpSocket,
addr: SocketAddr,
}
/// One datagram received by the peer.
struct Rx {
text: String,
from: SocketAddr,
}
impl Rx {
fn first_line(&self) -> &str {
self.text.lines().next().unwrap_or("")
}
fn is_request(&self, method: &str) -> bool {
self.first_line().starts_with(&format!("{method} "))
}
fn is_response(&self, code: u16, cseq_method: &str) -> bool {
self.first_line().starts_with(&format!("SIP/2.0 {code} "))
&& header(&self.text, "CSeq").is_some_and(|c| c.ends_with(cseq_method))
}
}
/// The value of the first header `name` (full name, case-insensitive).
fn header(text: &str, name: &str) -> Option<String> {
let prefix = format!("{}:", name.to_ascii_lowercase());
text.split("\r\n")
.find(|l| l.to_ascii_lowercase().starts_with(&prefix))
.map(|l| l[prefix.len()..].trim().to_string())
}
impl Peer {
async fn bind() -> Result<Peer, BoxError> {
let sock = tokio::net::UdpSocket::bind("127.0.0.1:0").await?;
let addr = sock.local_addr()?;
Ok(Peer { sock, addr })
}
async fn send(&self, to: SocketAddr, text: &str) -> Result<(), BoxError> {
self.sock.send_to(text.as_bytes(), to).await?;
Ok(())
}
async fn recv(&self, wait: Duration) -> Option<Rx> {
let mut buf = vec![0u8; 65536];
let (n, from) = tokio::time::timeout(wait, self.sock.recv_from(&mut buf))
.await
.ok()?
.ok()?;
Some(Rx {
text: String::from_utf8_lossy(&buf[..n]).into_owned(),
from,
})
}
/// The first datagram matching `pred` within `wait`; others are dropped.
async fn recv_until(&self, wait: Duration, pred: impl Fn(&Rx) -> bool) -> Option<Rx> {
let deadline = Instant::now() + wait;
loop {
let rx = self
.recv(deadline.saturating_duration_since(Instant::now()))
.await?;
if pred(&rx) {
return Some(rx);
}
}
}
}
impl Peer {
/// An out-of-dialog INVITE from this peer to `uas`.
fn invite(&self, uas: SocketAddr, call_id: &str) -> String {
let me = self.addr;
format!(
"INVITE sip:bob@{uas} SIP/2.0\r\n\
Via: SIP/2.0/UDP {me};branch=z9hG4bK-{call_id}-inv;rport\r\n\
Max-Forwards: 70\r\n\
From: <sip:alice@{me}>;tag=a1\r\n\
To: <sip:bob@{uas}>\r\n\
Call-ID: {call_id}\r\n\
CSeq: 1 INVITE\r\n\
Contact: <sip:alice@{me}>\r\n\
Content-Length: 0\r\n\r\n"
)
}
/// The ACK for a 2xx (new branch, To copied from the 2xx).
fn ack_for(&self, uas: SocketAddr, ok: &Rx) -> String {
let me = self.addr;
let to = header(&ok.text, "To").unwrap_or_default();
let call_id = header(&ok.text, "Call-ID").unwrap_or_default();
format!(
"ACK sip:bob@{uas} SIP/2.0\r\n\
Via: SIP/2.0/UDP {me};branch=z9hG4bK-{call_id}-ack;rport\r\n\
Max-Forwards: 70\r\n\
From: <sip:alice@{me}>;tag=a1\r\n\
To: {to}\r\n\
Call-ID: {call_id}\r\n\
CSeq: 1 ACK\r\n\
Content-Length: 0\r\n\r\n"
)
}
}Run:
cargo new repro && cd repro # then paste the two blocks above
cargo run --releaseObserved.
== issue F, late CANCEL | rsipstack/0.7.3 | 127.0.0.1 only
X (before ACK): CANCEL answered with ["SIP/2.0 481 Call/Transaction Does Not Exist [CSeq: 1 CANCEL]"]; dialog state: WaitAck
Y (after ACK): CANCEL answered with ["SIP/2.0 200 OK [CSeq: 1 INVITE]"]; dialog state: Confirmed
RESULT: REPRODUCED - 2/2 CANCELs not answered 200 to the CANCEL
On 0.6.12 and 0.6.10, line X is SIP/2.0 200 OK [CSeq: 1 CANCEL].
Expected. RFC 3261 §9.2: a CANCEL that matches a transaction is answered
by the UAS with a 200 to the CANCEL itself, whatever the state of the
INVITE. If nothing matches, the answer is 481. Either way the response
carries CSeq: n CANCEL, and the CANCEL has no effect on an INVITE that
already has a final response. A copy of the INVITE's 200 sent in response to
a CANCEL is a response to the wrong request: the UAC's CANCEL transaction
never completes, and it retransmits until Timer F.
Suggested fix.
- Treat
AcceptedlikeCompletedin the CANCEL branch: answer 200. - In the
finished_transactionspath, replay the cached response only for
INVITE retransmissions. For a CANCEL, answer 200 (the transaction did
exist) or 481.
Related. #164 (RFC 6026 Accepted state), where the 481 half started.
Activity