Skip to content

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

Closed

Description

@beija-wq

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 correct 200 OK with CSeq: 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_request falls into the _ arm and
    answers 481 Call/Transaction Does Not Exist
    (src/transaction/transaction.rs:834-857). Only Proceeding/Trying
    (forward, then 200) and Completed (200) are handled.
  • After the ACK, the INVITE transaction is in finished_transactions.
    on_received_message finds 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 --release

Observed.

== 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 Accepted like Completed in the CANCEL branch: answer 200.
  • In the finished_transactions path, 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions