diff --git a/src/dialog/invitation.rs b/src/dialog/invitation.rs index 54db4efe..f39c3269 100644 --- a/src/dialog/invitation.rs +++ b/src/dialog/invitation.rs @@ -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 = @@ -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 { @@ -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 @@ -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(_, _) => { @@ -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 { 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); } } @@ -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) { + 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)>; impl DialogLayer { diff --git a/src/dialog/invite_dialog.rs b/src/dialog/invite_dialog.rs index 8c15c15b..fb4af377 100644 --- a/src/dialog/invite_dialog.rs +++ b/src/dialog/invite_dialog.rs @@ -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()?; diff --git a/src/dialog/tests/test_cancel_2xx_race.rs b/src/dialog/tests/test_cancel_2xx_race.rs index b5a939bc..a95033da 100644 --- a/src/dialog/tests/test_cancel_2xx_race.rs +++ b/src/dialog/tests/test_cancel_2xx_race.rs @@ -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 +}