Skip to content

Commit 880be95

Browse files
committed
fix: also fix create_app_caller_media_bridge overriding caller_pc to RTP
The same bug pattern existed in create_app_caller_media_bridge: when caller_is_webrtc=false, it overrode bridge.caller_pc() to RTP mode. When the call was later transferred from IVR to a WebRTC agent, the existing bridge was reused and caller_pc() (WebRTC side) generated an RTP/AVP SDP without DTLS fingerprint, causing setRemoteDescription failure on the agent. Add test_ivr_to_webrtc_transfer_callee_sdp_has_dtls_fingerprint to verify the IVR→transfer scenario.
1 parent a6e9b03 commit 880be95

1 file changed

Lines changed: 108 additions & 16 deletions

File tree

src/proxy/proxy_call/sip_session.rs

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4041,22 +4041,6 @@ impl SipSession {
40414041
bridge_builder = bridge_builder.with_rtp_port_range(start, end);
40424042
}
40434043

4044-
if !caller_is_webrtc {
4045-
let app_caller_mode = Self::sdp_transport_mode(caller_offer);
4046-
let caller_config = rustrtc::RtcConfiguration {
4047-
transport_mode: app_caller_mode,
4048-
rtp_start_port: self.context.dialplan.media.rtp_start_port,
4049-
rtp_end_port: self.context.dialplan.media.rtp_end_port,
4050-
enable_latching: self.context.dialplan.media.enable_latching,
4051-
probation_max_packets: self.context.dialplan.media.probation_max_packets,
4052-
external_ip: self.context.dialplan.media.external_ip.clone(),
4053-
bind_ip: self.context.dialplan.media.bind_ip.clone(),
4054-
cname: Some(self.server.rtc_cname.clone()),
4055-
..Default::default()
4056-
};
4057-
bridge_builder = bridge_builder.with_caller_config(caller_config);
4058-
}
4059-
40604044
if !caller_is_webrtc && caller_offer.contains("a=group:BUNDLE") {
40614045
bridge_builder = bridge_builder
40624046
.with_rtp_sdp_compatibility(rustrtc::config::SdpCompatibilityMode::Standard)
@@ -11516,6 +11500,114 @@ mod tests {
1151611500
}
1151711501
}
1151811502

11503+
#[tokio::test]
11504+
async fn test_ivr_to_webrtc_transfer_callee_sdp_has_dtls_fingerprint() {
11505+
use crate::call::{DialDirection, Dialplan, TransactionCookie};
11506+
use crate::proxy::proxy_call::test_util::tests::MockMediaPeer;
11507+
use crate::proxy::tests::common::{
11508+
create_test_request, create_test_server, create_transaction,
11509+
};
11510+
11511+
let (server, _) = create_test_server().await;
11512+
let request = create_test_request(
11513+
rsipstack::sip::Method::Invite,
11514+
"alice",
11515+
None,
11516+
"rustpbx.com",
11517+
None,
11518+
);
11519+
let original_request = request.clone();
11520+
let (tx, _) = create_transaction(request).await;
11521+
let (state_tx, _state_rx) = mpsc::unbounded_channel();
11522+
let server_dialog = server
11523+
.dialog_layer
11524+
.get_or_create_server_invite(&tx, state_tx, None, None)
11525+
.expect("failed to create server dialog");
11526+
11527+
let context = CallContext {
11528+
session_id: "test-ivr-to-webrtc".to_string(),
11529+
dialplan: Arc::new(Dialplan::new(
11530+
"test-ivr-to-webrtc".to_string(),
11531+
original_request,
11532+
DialDirection::Inbound,
11533+
)),
11534+
cookie: TransactionCookie::default(),
11535+
start_time: Instant::now(),
11536+
original_caller: "sip:alice@rustpbx.com".to_string(),
11537+
original_callee: "sip:ivr@rustpbx.com".to_string(),
11538+
max_forwards: 70,
11539+
created_at: chrono::Utc::now().to_rfc3339(),
11540+
metadata: None,
11541+
};
11542+
11543+
let caller_peer = Arc::new(MockMediaPeer::new());
11544+
let callee_peer = Arc::new(MockMediaPeer::new());
11545+
let (mut session, _handle, _cmd_rx) = SipSession::new(
11546+
server.clone(),
11547+
CancellationToken::new(),
11548+
None,
11549+
context,
11550+
server_dialog,
11551+
true, // use_media_proxy = true → Anchored
11552+
caller_peer,
11553+
callee_peer,
11554+
);
11555+
11556+
// RTP caller SDP (no ICE/DTLS)
11557+
session.media.caller_offer = Some(
11558+
concat!(
11559+
"v=0\r\n",
11560+
"o=alice 1 1 IN IP4 192.0.2.10\r\n",
11561+
"s=Talk\r\n",
11562+
"c=IN IP4 192.0.2.10\r\n",
11563+
"t=0 0\r\n",
11564+
"m=audio 40000 RTP/AVP 0 8 101\r\n",
11565+
"a=rtpmap:0 PCMU/8000\r\n",
11566+
"a=rtpmap:8 PCMA/8000\r\n",
11567+
"a=rtpmap:101 telephone-event/8000\r\n",
11568+
"a=sendrecv\r\n",
11569+
)
11570+
.to_string(),
11571+
);
11572+
11573+
// Step 1: IVR - create app caller media bridge (simulates early media / queue)
11574+
let answer = session
11575+
.prepare_app_caller_media_bridge()
11576+
.await
11577+
.expect("app caller bridge answer should be prepared");
11578+
assert!(answer.contains("RTP/AVP"));
11579+
assert!(session.media.media_bridge.is_some());
11580+
assert!(session.media.caller_answer_uses_media_bridge);
11581+
assert!(!session.media.callee_offer_uses_media_bridge);
11582+
11583+
// Step 2: Transfer to WebRTC agent - create callee track reusing the bridge
11584+
let sdp = session
11585+
.create_callee_track(true)
11586+
.await
11587+
.expect("create_callee_track should succeed for IVR->WebRTC transfer");
11588+
11589+
// The SDP sent to the WebRTC agent MUST contain DTLS fingerprint and ICE
11590+
assert!(
11591+
sdp.contains("a=fingerprint"),
11592+
"IVR->WebRTC callee SDP must contain DTLS fingerprint, got: {}",
11593+
sdp
11594+
);
11595+
assert!(
11596+
sdp.contains("a=ice-ufrag"),
11597+
"IVR->WebRTC callee SDP must contain ICE ufrag, got: {}",
11598+
sdp
11599+
);
11600+
assert!(
11601+
sdp.contains("UDP/TLS/RTP/SAVPF"),
11602+
"IVR->WebRTC callee SDP must use UDP/TLS/RTP/SAVPF profile, got: {}",
11603+
sdp
11604+
);
11605+
11606+
if let Some(bridge) = session.media.media_bridge.take() {
11607+
bridge.stop().await;
11608+
}
11609+
}
11610+
1151911611
#[tokio::test]
1152011612
async fn test_sip_session_handle() {
1152111613
use crate::call::runtime::SessionId;

0 commit comments

Comments
 (0)