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
80 changes: 50 additions & 30 deletions src/dialog/invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,28 +200,40 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
};

match client_dialog.state() {
// CANCEL cannot be sent before a provisional response. Dropping the
// INVITE transaction here also removes its retransmission timers.
DialogState::Calling(_) => {
drop(self.invite_tx.take());
let _ = client_dialog.inner.transition(DialogState::Terminated(
client_dialog.id(),
TerminatedReason::UacCancel,
));
debug!(id = %client_dialog.id(), "dialog terminated before provisional response");
}
DialogState::Terminated(_, _) => {}
DialogState::Trying(_) | DialogState::Early(_, _) => {
let Some(mut invite_tx) = self.invite_tx.take() else {
state @ (DialogState::Calling(_)
| DialogState::Trying(_)
| DialogState::Early(_, _)) => {
// CANCEL cannot be sent before a provisional response (RFC 3261
// §9.1): a Calling dialog ends now, its INVITE is CANCELed once
// a provisional arrives.
let calling = matches!(state, DialogState::Calling(_));
let invite_tx = self.invite_tx.take();
if calling || invite_tx.is_none() {
let _ = client_dialog.inner.transition(DialogState::Terminated(
client_dialog.id(),
TerminatedReason::UacCancel,
));
}
let Some(mut invite_tx) = invite_tx else {
return;
};

debug!(%self.id, "unconfirmed dialog dropped, cancelling it");
let _handle = crate::platform::spawn(async move {
if calling {
invite_tx.stop_retransmissions();
let window = invite_tx.endpoint_inner.option.t1x64;
let first = wait_response(&mut invite_tx, window, false).await;
if !first
.as_ref()
.is_some_and(|r| r.status_code.kind() == StatusCodeKind::Provisional)
{
drop(invite_tx);
bye_if_2xx(&client_dialog, first).await;
return;
}
}
let started = crate::platform::Instant::now();
let final_window = invite_tx.endpoint_inner.option.t1x64;
let mut timeout =
Expand All @@ -231,7 +243,7 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
let mut cancel_done = false;
let mut final_response = None;
// Boxed so it can be dropped before the wait below.
let mut cancel = Box::pin(client_dialog.cancel());
let mut cancel = Box::pin(client_dialog.send_cancel());

use crate::platform::select::Which3;
loop {
Expand Down Expand Up @@ -280,11 +292,13 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
}

drop(cancel);
let _ = client_dialog.inner.transition(DialogState::Terminated(
client_dialog.id(),
TerminatedReason::UacCancel,
));
debug!(id = %client_dialog.id(), "dialog terminated");
if !calling {
let _ = client_dialog.inner.transition(DialogState::Terminated(
client_dialog.id(),
TerminatedReason::UacCancel,
));
debug!(id = %client_dialog.id(), "dialog terminated");
}

// The callee may still answer after the CANCEL: keep the
// INVITE transaction until its final response (up to
Expand All @@ -294,22 +308,16 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
let elapsed = crate::platform::Instant::now()
.checked_duration_since(started)
.unwrap_or_default();
final_response = wait_final_response(
final_response = wait_response(
&mut invite_tx,
final_window.saturating_sub(elapsed),
true,
)
.await;
}
// Closing the command channel terminates the dialog loop.
drop(invite_tx);
if let Some(resp) = final_response {
if resp.status_code.kind() == StatusCodeKind::Successful {
info!(id = %client_dialog.id(), "2xx after CANCEL, sending BYE");
if let Err(e) = client_dialog.bye_2xx_after_cancel(&resp).await {
warn!(id = %client_dialog.id(), error = %e, "BYE after CANCEL failed");
}
}
}
bye_if_2xx(&client_dialog, final_response).await;
});
}
DialogState::Confirmed(_, _) => {
Expand All @@ -324,15 +332,17 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
}
}

/// Wait up to `limit` for the INVITE transaction's final response.
async fn wait_final_response(
/// Wait up to `limit` for the INVITE transaction's first response, or its
/// first final one.
async fn wait_response(
invite_tx: &mut Transaction,
limit: core::time::Duration,
final_only: bool,
) -> Option<Response> {
let wait = async {
while let Some(msg) = invite_tx.receive().await {
if let SipMessage::Response(resp) = msg {
if resp.status_code.kind() != StatusCodeKind::Provisional {
if !final_only || resp.status_code.kind() != StatusCodeKind::Provisional {
return Some(resp);
}
}
Expand All @@ -342,6 +352,16 @@ async fn wait_final_response(
crate::platform::timeout(limit, wait).await.ok().flatten()
}

/// End the session a 2xx to an abandoned INVITE established.
async fn bye_if_2xx(client_dialog: &InviteDialog, resp: Option<Response>) {
if let Some(resp) = resp.filter(|r| r.status_code.kind() == StatusCodeKind::Successful) {
info!(id = %client_dialog.id(), "2xx after CANCEL, sending BYE");
if let Err(e) = client_dialog.bye_2xx_after_cancel(&resp).await {
warn!(id = %client_dialog.id(), error = %e, "BYE after CANCEL failed");
}
}
}

pub type InviteAsyncResult = Result<(DialogId, Option<Response>)>;

impl DialogLayer {
Expand Down
5 changes: 5 additions & 0 deletions src/dialog/invite_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ impl InviteDialog {
{
return Ok(());
}
self.send_cancel().await
}

/// Send the CANCEL for the initial INVITE, whatever the dialog state.
pub(super) async fn send_cancel(&self) -> Result<()> {
debug!(id = %self.id(), "sending cancel request");
let mut cancel_request = self.inner.initial_request.lock().clone();
let invite_seq = cancel_request.cseq_header()?.seq()?;
Expand Down
79 changes: 79 additions & 0 deletions src/dialog/tests/test_cancel_2xx_race.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,82 @@ async fn test_cancel_answered_487_sends_no_bye() -> crate::Result<()> {
token.cancel();
Ok(())
}

/// Dropped before any response: Terminated(UacCancel) is reported at once and
/// nothing is sent while no provisional has arrived (RFC 3261 §9.1). The first
/// response then gets a CANCEL (180), or an ACK and a BYE (200).
async fn run_dropped_before_provisional(first: u16) -> crate::Result<()> {
let token = CancellationToken::new();
let Uac {
dialog_layer,
option,
peer,
} = setup(&token).await?;
let wait = Duration::from_secs(2);

let (state_sender, mut states) = unbounded_channel();
let invite = tokio::spawn(async move { dialog_layer.do_invite(option, state_sender).await });
let (inv, uac) = recv_request(&peer, Method::Invite, wait).await;
invite.abort();
let _ = invite.await;
let terminated = wait_for_state(&mut states, "Terminated", Duration::from_millis(200), |s| {
matches!(s, DialogState::Terminated(_, _))
})
.await;
assert!(matches!(
terminated,
DialogState::Terminated(_, TerminatedReason::UacCancel)
));
// Over twice T1: not even an INVITE retransmission.
let mut buf = vec![0u8; 4096];
let silent = tokio::time::timeout(Duration::from_millis(1100), peer.recv_from(&mut buf)).await;
assert!(silent.is_err(), "nothing may be sent before a provisional");

match first {
180 => {
reply(&peer, uac, &inv, 180, "Ringing").await;
let (cancel, _) = recv_request(&peer, Method::Cancel, wait).await;
assert_eq!(cancel.cseq_header()?.seq()?, inv.cseq_header()?.seq()?);
reply(&peer, uac, &cancel, 200, "OK").await;
reply(&peer, uac, &inv, 487, "Request Terminated").await;
}
200 => reply(&peer, uac, &inv, 200, "OK").await,
_ => reply(&peer, uac, &inv, first, "Busy Here").await,
}
let (ack, _) = recv_request(&peer, Method::Ack, wait).await;
assert_in_dialog(&ack, &inv, "ACK");
if first == 200 {
let (bye, _) = recv_request(&peer, Method::Bye, wait).await;
assert_in_dialog(&bye, &inv, "BYE");
reply(&peer, uac, &bye, 200, "OK").await;
} else {
assert_no_bye(
&peer,
Duration::from_millis(500),
"a call that was not answered",
)
.await;
}
assert!(
!std::iter::from_fn(|| states.try_recv().ok())
.any(|s| matches!(s, DialogState::Terminated(..) | DialogState::Confirmed(..))),
"no second Terminated and no Confirmed"
);
token.cancel();
Ok(())
}

#[tokio::test]
async fn test_dropped_before_provisional_is_cancelled_after_the_180() -> crate::Result<()> {
run_dropped_before_provisional(180).await
}

#[tokio::test]
async fn test_dropped_before_provisional_2xx_is_acked_and_byed() -> crate::Result<()> {
run_dropped_before_provisional(200).await
}

#[tokio::test]
async fn test_dropped_before_provisional_final_failure_is_acked_only() -> crate::Result<()> {
run_dropped_before_provisional(486).await
}
Loading