diff --git a/src/dialog/invitation.rs b/src/dialog/invitation.rs index 45179d5e..c8125dc8 100644 --- a/src/dialog/invitation.rs +++ b/src/dialog/invitation.rs @@ -136,6 +136,11 @@ pub struct InviteOption { pub destination: Option, pub content_type: Option, pub offer: Option>, + /// Local Contact. On stream transports its host and port are normally + /// replaced with the outgoing connection's local address (and the scheme + /// becomes `sips` over TLS). A Contact that already carries a `transport` + /// parameter is treated as deliberately built by the caller, for example + /// from the address a registrar reported behind NAT, and is sent unchanged. pub contact: crate::sip::Uri, pub credential: Option, pub headers: Option>, @@ -421,7 +426,17 @@ impl DialogLayer { request.headers.push(route.clone().into()); } - let contact = if let Some(ref addr) = transport_addr { + // A Contact that names its transport was built deliberately by the + // caller (e.g. with the public address learned from a REGISTER + // response behind NAT); keep it rather than rewriting it to the local + // socket address. + let caller_built_contact = opt + .contact + .params + .iter() + .any(|p| matches!(p, crate::sip::Param::Transport(_))); + let contact = if let Some(addr) = transport_addr.as_ref().filter(|_| !caller_built_contact) + { let mut uri = opt.contact.clone(); uri.host_with_port = addr.addr.clone(); if !uri diff --git a/src/dialog/tests/test_dialog_layer.rs b/src/dialog/tests/test_dialog_layer.rs index cec219bf..205c3fd2 100644 --- a/src/dialog/tests/test_dialog_layer.rs +++ b/src/dialog/tests/test_dialog_layer.rs @@ -639,6 +639,54 @@ async fn test_make_invite_request_with_tls_transport_uses_sips_scheme() -> crate Ok(()) } +#[cfg(feature = "rustls")] +#[tokio::test] +async fn test_make_invite_request_keeps_caller_built_contact() -> crate::Result<()> { + let token = CancellationToken::new(); + let tl = TransportLayer::new(token.child_token()); + + let tls_addr = "192.168.1.10:5061".parse()?; + let tls_listener = TlsListenerConnection::new(tls_addr, None, TlsConfig::default()).await?; + tl.add_transport(crate::transport::SipConnection::TlsListener(tls_listener)); + + let endpoint = EndpointBuilder::new() + .with_user_agent("rsipstack-test") + .with_transport_layer(tl) + .build(); + let dialog_layer = DialogLayer::new(endpoint.inner.clone()); + + let destination = SipAddr { + r#type: Some(Transport::Tls), + addr: HostWithPort { + host: crate::sip::Host::IpAddr(std::net::IpAddr::V4(std::net::Ipv4Addr::new( + 10, 0, 0, 1, + ))), + port: Some(5061.into()), + }, + }; + + // A Contact built from the address a registrar reported behind NAT. + let public_contact = crate::sip::Uri::try_from("sip:alice@203.0.113.7:40000;transport=tls")?; + let opt = crate::dialog::invitation::InviteOption { + caller: crate::sip::Uri::try_from("sip:alice@example.com")?, + callee: crate::sip::Uri::try_from("sip:bob@example.com")?, + contact: public_contact.clone(), + destination: Some(destination), + ..Default::default() + }; + + let request = dialog_layer.make_invite_request(&opt)?; + let contact = request.contact_header()?.typed()?; + + assert_eq!( + contact.uri, public_contact, + "a Contact with an explicit transport should be sent unchanged" + ); + assert_eq!(contact.uri.scheme, Some(crate::sip::Scheme::Sip)); + + Ok(()) +} + #[tokio::test] async fn test_make_invite_request_preloads_service_route() -> crate::Result<()> { let token = CancellationToken::new();