From 4013cd9d56fa64ec38eabb97a4f35ec3b1dae85d Mon Sep 17 00:00:00 2001 From: Tung-Yang Li Date: Sun, 6 Sep 2026 00:07:03 +0800 Subject: [PATCH 1/5] Fix REGISTER's Via header ignoring the target's actual transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registration::register() built its Via header via `self.endpoint.get_via(None, None)` before the request's actual transport had been resolved. get_via's `None` fallback always uses the endpoint's first-bound listener — in any client that binds UDP upfront (the common case for a register/dial-only role, no inbound listener needed) that's the UDP transport, regardless of what `;transport=` the target URI actually asked for. Symptom: registering against a `sip:host;transport=tcp` target correctly dials a real TCP connection and sends the REGISTER over it, but the message's own Via header claims `SIP/2.0/UDP` with the client's unrelated UDP transport's address. A spec-compliant server receiving a request whose declared Via transport doesn't match the connection it actually arrived on is free to treat that as malformed and drop it silently — confirmed against a real deployment (FreeSWITCH/sofia-sip): TCP handshake completes, the REGISTER is received intact and logged, but the server never responds — no error, nothing in its own logs beyond the raw bytes arriving. From the client's side that's indistinguishable from "the server doesn't support TCP", which is what it looks like until you compare the exact bytes received against what a spec-correct request looks like. Fix: resolve (and, for TCP/TLS/WS/WSS, lazily dial+cache — the same lookup Transaction::send() would perform anyway, so this doesn't add a second real connection attempt on the happy path) the target's connection first, and build Via from that connection's real local SipAddr instead. A target with no `;transport=` param still falls through to the existing bound UDP listener exactly as before (see TransportLayerInner::lookup's `first_udp` fallback), so this is safe to apply unconditionally rather than gating it on transport type. Falls back to the old `get_via(None, None)` behavior if the early lookup itself fails, rather than surfacing the error twice — the identical lookup happens again inside Transaction::send() regardless, so a real failure (unreachable target, DNS failure, etc.) still surfaces normally through the existing error path. cargo test --lib: 269/269 passing, no regressions. Co-Authored-By: Claude Sonnet 5 --- src/dialog/registration.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/dialog/registration.rs b/src/dialog/registration.rs index 6a85c922..d8169905 100644 --- a/src/dialog/registration.rs +++ b/src/dialog/registration.rs @@ -373,7 +373,35 @@ impl Registration { } .with_tag(make_tag()); - let via = self.endpoint.get_via(None, None)?; + // Resolve (and, for TCP/TLS/WS/WSS, lazily dial+cache) the connection + // this REGISTER will actually go out on *before* building the Via + // header, and build Via from that connection's real local address + // instead of always falling back to the endpoint's first-bound + // transport (get_via(None, None) below). Without this, a request + // targeting `;transport=tcp` still gets a Via that claims the + // endpoint's default UDP transport — physically sent over TCP, but + // self-describing as UDP inside the SIP headers. A spec-compliant + // server can, and in the wild does (FreeSWITCH's sofia-sip), treat + // that mismatch as reason enough to silently drop the request: no + // response, no error, nothing distinguishable in the server's own + // logs from the request never having arrived — the exact symptom + // this fixes. + // + // `lookup` already correctly falls through to the existing bound + // UDP listener for a target with no explicit `;transport=` param + // (see `TransportLayerInner::lookup`'s `first_udp` fallback), so + // this is safe to do unconditionally rather than only for TCP/TLS. + let via = match SipAddr::try_from(&server) { + Ok(target_addr) => { + match self.endpoint.transport_layer.lookup(&target_addr, None).await { + Ok((connection, _resolved)) => { + self.endpoint.get_via(Some(connection.get_addr().clone()), None)? + } + Err(_) => self.endpoint.get_via(None, None)?, + } + } + Err(_) => self.endpoint.get_via(None, None)?, + }; // Contact address selection priority: // 1. Explicitly set self.contact (if caller set it) From 053a111b92d276b44e39a0ba0c73fdcc5f604ed1 Mon Sep 17 00:00:00 2001 From: yeoleobun Date: Mon, 14 Sep 2026 11:53:51 +0800 Subject: [PATCH 2/5] fix: reuse effective REGISTER connection for Via and sending --- src/dialog/registration.rs | 87 +++++++------- src/dialog/tests/mod.rs | 1 + src/dialog/tests/test_registration.rs | 163 ++++++++++++++++++++++++++ src/transaction/endpoint.rs | 2 +- 4 files changed, 206 insertions(+), 47 deletions(-) create mode 100644 src/dialog/tests/test_registration.rs diff --git a/src/dialog/registration.rs b/src/dialog/registration.rs index d8169905..8b01c052 100644 --- a/src/dialog/registration.rs +++ b/src/dialog/registration.rs @@ -373,35 +373,41 @@ impl Registration { } .with_tag(make_tag()); - // Resolve (and, for TCP/TLS/WS/WSS, lazily dial+cache) the connection - // this REGISTER will actually go out on *before* building the Via - // header, and build Via from that connection's real local address - // instead of always falling back to the endpoint's first-bound - // transport (get_via(None, None) below). Without this, a request - // targeting `;transport=tcp` still gets a Via that claims the - // endpoint's default UDP transport — physically sent over TCP, but - // self-describing as UDP inside the SIP headers. A spec-compliant - // server can, and in the wild does (FreeSWITCH's sofia-sip), treat - // that mismatch as reason enough to silently drop the request: no - // response, no error, nothing distinguishable in the server's own - // logs from the request never having arrived — the exact symptom - // this fixes. - // - // `lookup` already correctly falls through to the existing bound - // UDP listener for a target with no explicit `;transport=` param - // (see `TransportLayerInner::lookup`'s `first_udp` fallback), so - // this is safe to do unconditionally rather than only for TCP/TLS. - let via = match SipAddr::try_from(&server) { - Ok(target_addr) => { - match self.endpoint.transport_layer.lookup(&target_addr, None).await { - Ok((connection, _resolved)) => { - self.endpoint.get_via(Some(connection.get_addr().clone()), None)? - } - Err(_) => self.endpoint.get_via(None, None)?, - } + // Choose the same destination as the transaction before building Via. + // A registration proxy overrides the registrar without changing the URI. + let proxy_destination = self.outbound_proxy.map(|proxy| { + let mut dest = SipAddr::from(proxy); + if let Some(Param::Transport(t)) = server + .params + .iter() + .find(|p| matches!(p, Param::Transport(_))) + { + dest.r#type = Some(*t); } - Err(_) => self.endpoint.get_via(None, None)?, + dest + }); + let target = match &proxy_destination { + Some(dest) => Ok(dest.clone()), + None => match self.endpoint.locator.as_ref() { + Some(locator) => locator.locate(&server).await, + None => SipAddr::try_from(&server), + }, }; + let resolved = match target { + Ok(target) => self + .endpoint + .transport_layer + .lookup(&target, None) + .await + .ok(), + Err(_) => None, + }; + let via = self.endpoint.get_via( + resolved + .as_ref() + .map(|(connection, _)| connection.get_addr().clone()), + None, + )?; // Contact address selection priority: // 1. Explicitly set self.contact (if caller set it) @@ -460,25 +466,14 @@ impl Registration { } let key = TransactionKey::from_request(&request, TransactionRole::Client)?; - let mut tx = Transaction::new_client(key, request, self.endpoint.clone(), None); - - // Override transport destination if outbound proxy is configured. - // This keeps the domain in SIP headers (Request-URI, From, To) while - // sending all packets to the pinned proxy IP for NAT consistency. - if let Some(proxy) = &self.outbound_proxy { - let mut dest = SipAddr::from(*proxy); - // Inherit transport type from the request URI (e.g., TCP) - if let Some(Param::Transport(t)) = tx - .original - .uri() - .params - .iter() - .find(|p| matches!(p, Param::Transport(_))) - { - dest.r#type = Some(*t); - } - tx.destination = Some(dest); - } + // Reuse the connection that supplied Via, including for authentication retries. + // On lookup failure, retain the existing transaction retry/timeout behavior. + let (connection, destination) = match resolved { + Some((connection, destination)) => (Some(connection), Some(destination)), + None => (None, proxy_destination), + }; + let mut tx = Transaction::new_client(key, request, self.endpoint.clone(), connection); + tx.destination = destination; tx.send().await?; let mut auth_sent = false; diff --git a/src/dialog/tests/mod.rs b/src/dialog/tests/mod.rs index 2982103f..27b1ca21 100644 --- a/src/dialog/tests/mod.rs +++ b/src/dialog/tests/mod.rs @@ -5,5 +5,6 @@ mod test_dialog_layer; mod test_dialog_states; mod test_prack; mod test_refer; +mod test_registration; mod test_server_dialog; mod test_sub_pub; diff --git a/src/dialog/tests/test_registration.rs b/src/dialog/tests/test_registration.rs new file mode 100644 index 00000000..6e8f5de8 --- /dev/null +++ b/src/dialog/tests/test_registration.rs @@ -0,0 +1,163 @@ +use crate::dialog::authenticate::Credential; +use crate::dialog::registration::Registration; +use crate::sip::{prelude::*, SipMessage, StatusCode, Transport, Uri}; +use crate::transaction::endpoint::TargetLocator; +use crate::transport::stream::{SipCodec, SipCodecType}; +use crate::transport::{udp::UdpConnection, SipAddr, TransportLayer}; +use crate::EndpointBuilder; +use async_trait::async_trait; +use futures::{SinkExt, StreamExt}; +use tokio::net::{TcpListener, UdpSocket}; +use tokio::time::{timeout, Duration}; +use tokio_util::codec::Framed; +use tokio_util::sync::CancellationToken; + +struct RegistrationLocator(SipAddr); + +#[async_trait] +impl TargetLocator for RegistrationLocator { + async fn locate(&self, _uri: &Uri) -> crate::Result { + Ok(self.0.clone()) + } +} + +#[tokio::test] +async fn test_register_via_matches_tcp_connection() { + for mode in ["direct", "proxy", "locator"] { + let token = CancellationToken::new(); + let tl = TransportLayer::new(token.clone()); + let udp = UdpConnection::create_connection( + "127.0.0.1:0".parse().unwrap(), + None, + Some(token.clone()), + ) + .await + .unwrap(); + tl.add_transport(udp.into()); + let mut builder = EndpointBuilder::new(); + builder + .with_cancel_token(token.clone()) + .with_transport_layer(tl); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + // Keep a separate registrar listening to detect any unwanted direct dial. + let registrar = TcpListener::bind("127.0.0.1:0").await.unwrap(); + if mode == "locator" { + let mut dest = SipAddr::from(listener.local_addr().unwrap()); + dest.r#type = Some(Transport::Tcp); + builder.with_target_locator(Box::new(RegistrationLocator(dest))); + } + let endpoint = builder.build(); + let target = if mode != "direct" { + registrar.local_addr().unwrap() + } else { + listener.local_addr().unwrap() + }; + let uri: Uri = format!("sip:{};transport=tcp", target).try_into().unwrap(); + let mut registration = Registration::new( + endpoint.inner.clone(), + Some(Credential { + username: "alice".into(), + password: "secret".into(), + realm: Some("test".into()), + }), + ); + if mode == "proxy" { + registration.outbound_proxy = Some(listener.local_addr().unwrap()); + } + let server = async { + let (stream, peer) = listener.accept().await.unwrap(); + let mut framed = Framed::new(stream, SipCodec::new()); + for status in [StatusCode::Unauthorized, StatusCode::OK] { + let request = match framed.next().await.unwrap().unwrap() { + SipCodecType::Message(SipMessage::Request(request)) => request, + other => panic!("unexpected message: {}", other), + }; + assert_eq!(request.uri, uri); + let via = request.via_header().unwrap().typed().unwrap(); + assert_eq!(via.transport, Transport::Tcp); + assert_eq!(via.uri.host_with_port, peer.into()); + if status == StatusCode::OK { + assert!(request.authorization_header().is_some()); + } + let mut response = endpoint.inner.make_response(&request, status.clone(), None); + if status == StatusCode::Unauthorized { + response.headers.push( + crate::sip::headers::WwwAuthenticate::new( + "Digest realm=\"test\", nonce=\"test-nonce\", algorithm=MD5, qop=\"auth\"", + ) + .into(), + ); + } + framed.send(SipMessage::Response(response)).await.unwrap(); + } + }; + let client = async { + let response = registration.register(uri.clone(), Some(300)).await.unwrap(); + assert_eq!(response.status_code, StatusCode::OK); + }; + let exchange = async { + tokio::join!(server, client); + }; + tokio::select! { + _ = endpoint.serve() => panic!("endpoint stopped"), + result = timeout(Duration::from_secs(5), exchange) => result.unwrap(), + } + assert!(timeout(Duration::from_millis(50), registrar.accept()) + .await + .is_err()); + assert!(timeout(Duration::from_millis(50), listener.accept()) + .await + .is_err()); + token.cancel(); + } +} + +#[tokio::test] +async fn test_register_udp_unchanged() { + let token = CancellationToken::new(); + let tl = TransportLayer::new(token.clone()); + let udp = UdpConnection::create_connection( + "127.0.0.1:0".parse().unwrap(), + None, + Some(token.clone()), + ) + .await + .unwrap(); + tl.add_transport(udp.into()); + let endpoint = EndpointBuilder::new() + .with_cancel_token(token.clone()) + .with_transport_layer(tl) + .build(); + let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let uri: Uri = format!("sip:{}", socket.local_addr().unwrap()) + .try_into() + .unwrap(); + let mut registration = Registration::new(endpoint.inner.clone(), None); + let server = async { + let mut buf = [0u8; 4096]; + let (len, peer) = socket.recv_from(&mut buf).await.unwrap(); + let request: crate::sip::Request = std::str::from_utf8(&buf[..len]) + .unwrap() + .try_into() + .unwrap(); + let via = request.via_header().unwrap().typed().unwrap(); + assert_eq!(via.transport, Transport::Udp); + assert_eq!(via.uri.host_with_port, peer.into()); + let response = endpoint.inner.make_response(&request, StatusCode::OK, None); + socket.send_to(&response.to_bytes(), peer).await.unwrap(); + }; + let client = async { + assert_eq!( + registration.register(uri, Some(300)).await.unwrap().status_code, + StatusCode::OK, + ); + }; + let exchange = async { + tokio::join!(server, client); + }; + tokio::select! { + _ = endpoint.serve() => panic!("endpoint stopped"), + result = timeout(Duration::from_secs(5), exchange) => result.unwrap(), + } + token.cancel(); +} diff --git a/src/transaction/endpoint.rs b/src/transaction/endpoint.rs index 973c620a..15bf025d 100644 --- a/src/transaction/endpoint.rs +++ b/src/transaction/endpoint.rs @@ -109,7 +109,7 @@ pub struct EndpointInner { #[allow(dead_code)] timer_interval: Duration, pub(super) message_inspector: Option>, - pub(super) locator: Option>, + pub(crate) locator: Option>, pub(super) transport_inspector: Option>, pub option: EndpointOption, } From 7c3643c8971ed171aa5e1e01c09fdc69d11fdbd7 Mon Sep 17 00:00:00 2001 From: yeoleobun Date: Mon, 14 Sep 2026 14:10:41 +0800 Subject: [PATCH 3/5] test: remove added registration regression tests --- src/dialog/tests/mod.rs | 1 - src/dialog/tests/test_registration.rs | 163 -------------------------- 2 files changed, 164 deletions(-) delete mode 100644 src/dialog/tests/test_registration.rs diff --git a/src/dialog/tests/mod.rs b/src/dialog/tests/mod.rs index 27b1ca21..2982103f 100644 --- a/src/dialog/tests/mod.rs +++ b/src/dialog/tests/mod.rs @@ -5,6 +5,5 @@ mod test_dialog_layer; mod test_dialog_states; mod test_prack; mod test_refer; -mod test_registration; mod test_server_dialog; mod test_sub_pub; diff --git a/src/dialog/tests/test_registration.rs b/src/dialog/tests/test_registration.rs deleted file mode 100644 index 6e8f5de8..00000000 --- a/src/dialog/tests/test_registration.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::dialog::authenticate::Credential; -use crate::dialog::registration::Registration; -use crate::sip::{prelude::*, SipMessage, StatusCode, Transport, Uri}; -use crate::transaction::endpoint::TargetLocator; -use crate::transport::stream::{SipCodec, SipCodecType}; -use crate::transport::{udp::UdpConnection, SipAddr, TransportLayer}; -use crate::EndpointBuilder; -use async_trait::async_trait; -use futures::{SinkExt, StreamExt}; -use tokio::net::{TcpListener, UdpSocket}; -use tokio::time::{timeout, Duration}; -use tokio_util::codec::Framed; -use tokio_util::sync::CancellationToken; - -struct RegistrationLocator(SipAddr); - -#[async_trait] -impl TargetLocator for RegistrationLocator { - async fn locate(&self, _uri: &Uri) -> crate::Result { - Ok(self.0.clone()) - } -} - -#[tokio::test] -async fn test_register_via_matches_tcp_connection() { - for mode in ["direct", "proxy", "locator"] { - let token = CancellationToken::new(); - let tl = TransportLayer::new(token.clone()); - let udp = UdpConnection::create_connection( - "127.0.0.1:0".parse().unwrap(), - None, - Some(token.clone()), - ) - .await - .unwrap(); - tl.add_transport(udp.into()); - let mut builder = EndpointBuilder::new(); - builder - .with_cancel_token(token.clone()) - .with_transport_layer(tl); - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - // Keep a separate registrar listening to detect any unwanted direct dial. - let registrar = TcpListener::bind("127.0.0.1:0").await.unwrap(); - if mode == "locator" { - let mut dest = SipAddr::from(listener.local_addr().unwrap()); - dest.r#type = Some(Transport::Tcp); - builder.with_target_locator(Box::new(RegistrationLocator(dest))); - } - let endpoint = builder.build(); - let target = if mode != "direct" { - registrar.local_addr().unwrap() - } else { - listener.local_addr().unwrap() - }; - let uri: Uri = format!("sip:{};transport=tcp", target).try_into().unwrap(); - let mut registration = Registration::new( - endpoint.inner.clone(), - Some(Credential { - username: "alice".into(), - password: "secret".into(), - realm: Some("test".into()), - }), - ); - if mode == "proxy" { - registration.outbound_proxy = Some(listener.local_addr().unwrap()); - } - let server = async { - let (stream, peer) = listener.accept().await.unwrap(); - let mut framed = Framed::new(stream, SipCodec::new()); - for status in [StatusCode::Unauthorized, StatusCode::OK] { - let request = match framed.next().await.unwrap().unwrap() { - SipCodecType::Message(SipMessage::Request(request)) => request, - other => panic!("unexpected message: {}", other), - }; - assert_eq!(request.uri, uri); - let via = request.via_header().unwrap().typed().unwrap(); - assert_eq!(via.transport, Transport::Tcp); - assert_eq!(via.uri.host_with_port, peer.into()); - if status == StatusCode::OK { - assert!(request.authorization_header().is_some()); - } - let mut response = endpoint.inner.make_response(&request, status.clone(), None); - if status == StatusCode::Unauthorized { - response.headers.push( - crate::sip::headers::WwwAuthenticate::new( - "Digest realm=\"test\", nonce=\"test-nonce\", algorithm=MD5, qop=\"auth\"", - ) - .into(), - ); - } - framed.send(SipMessage::Response(response)).await.unwrap(); - } - }; - let client = async { - let response = registration.register(uri.clone(), Some(300)).await.unwrap(); - assert_eq!(response.status_code, StatusCode::OK); - }; - let exchange = async { - tokio::join!(server, client); - }; - tokio::select! { - _ = endpoint.serve() => panic!("endpoint stopped"), - result = timeout(Duration::from_secs(5), exchange) => result.unwrap(), - } - assert!(timeout(Duration::from_millis(50), registrar.accept()) - .await - .is_err()); - assert!(timeout(Duration::from_millis(50), listener.accept()) - .await - .is_err()); - token.cancel(); - } -} - -#[tokio::test] -async fn test_register_udp_unchanged() { - let token = CancellationToken::new(); - let tl = TransportLayer::new(token.clone()); - let udp = UdpConnection::create_connection( - "127.0.0.1:0".parse().unwrap(), - None, - Some(token.clone()), - ) - .await - .unwrap(); - tl.add_transport(udp.into()); - let endpoint = EndpointBuilder::new() - .with_cancel_token(token.clone()) - .with_transport_layer(tl) - .build(); - let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); - let uri: Uri = format!("sip:{}", socket.local_addr().unwrap()) - .try_into() - .unwrap(); - let mut registration = Registration::new(endpoint.inner.clone(), None); - let server = async { - let mut buf = [0u8; 4096]; - let (len, peer) = socket.recv_from(&mut buf).await.unwrap(); - let request: crate::sip::Request = std::str::from_utf8(&buf[..len]) - .unwrap() - .try_into() - .unwrap(); - let via = request.via_header().unwrap().typed().unwrap(); - assert_eq!(via.transport, Transport::Udp); - assert_eq!(via.uri.host_with_port, peer.into()); - let response = endpoint.inner.make_response(&request, StatusCode::OK, None); - socket.send_to(&response.to_bytes(), peer).await.unwrap(); - }; - let client = async { - assert_eq!( - registration.register(uri, Some(300)).await.unwrap().status_code, - StatusCode::OK, - ); - }; - let exchange = async { - tokio::join!(server, client); - }; - tokio::select! { - _ = endpoint.serve() => panic!("endpoint stopped"), - result = timeout(Duration::from_secs(5), exchange) => result.unwrap(), - } - token.cancel(); -} From 487b5c57dfab832e56d69c217c89d02dcde8dfcd Mon Sep 17 00:00:00 2001 From: yeoleobun Date: Mon, 14 Sep 2026 16:31:23 +0800 Subject: [PATCH 4/5] revert: remove follow-up changes to REGISTER Via fix --- src/dialog/registration.rs | 87 ++++++++++++++++++++----------------- src/transaction/endpoint.rs | 2 +- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/dialog/registration.rs b/src/dialog/registration.rs index 8b01c052..d8169905 100644 --- a/src/dialog/registration.rs +++ b/src/dialog/registration.rs @@ -373,41 +373,35 @@ impl Registration { } .with_tag(make_tag()); - // Choose the same destination as the transaction before building Via. - // A registration proxy overrides the registrar without changing the URI. - let proxy_destination = self.outbound_proxy.map(|proxy| { - let mut dest = SipAddr::from(proxy); - if let Some(Param::Transport(t)) = server - .params - .iter() - .find(|p| matches!(p, Param::Transport(_))) - { - dest.r#type = Some(*t); + // Resolve (and, for TCP/TLS/WS/WSS, lazily dial+cache) the connection + // this REGISTER will actually go out on *before* building the Via + // header, and build Via from that connection's real local address + // instead of always falling back to the endpoint's first-bound + // transport (get_via(None, None) below). Without this, a request + // targeting `;transport=tcp` still gets a Via that claims the + // endpoint's default UDP transport — physically sent over TCP, but + // self-describing as UDP inside the SIP headers. A spec-compliant + // server can, and in the wild does (FreeSWITCH's sofia-sip), treat + // that mismatch as reason enough to silently drop the request: no + // response, no error, nothing distinguishable in the server's own + // logs from the request never having arrived — the exact symptom + // this fixes. + // + // `lookup` already correctly falls through to the existing bound + // UDP listener for a target with no explicit `;transport=` param + // (see `TransportLayerInner::lookup`'s `first_udp` fallback), so + // this is safe to do unconditionally rather than only for TCP/TLS. + let via = match SipAddr::try_from(&server) { + Ok(target_addr) => { + match self.endpoint.transport_layer.lookup(&target_addr, None).await { + Ok((connection, _resolved)) => { + self.endpoint.get_via(Some(connection.get_addr().clone()), None)? + } + Err(_) => self.endpoint.get_via(None, None)?, + } } - dest - }); - let target = match &proxy_destination { - Some(dest) => Ok(dest.clone()), - None => match self.endpoint.locator.as_ref() { - Some(locator) => locator.locate(&server).await, - None => SipAddr::try_from(&server), - }, + Err(_) => self.endpoint.get_via(None, None)?, }; - let resolved = match target { - Ok(target) => self - .endpoint - .transport_layer - .lookup(&target, None) - .await - .ok(), - Err(_) => None, - }; - let via = self.endpoint.get_via( - resolved - .as_ref() - .map(|(connection, _)| connection.get_addr().clone()), - None, - )?; // Contact address selection priority: // 1. Explicitly set self.contact (if caller set it) @@ -466,14 +460,25 @@ impl Registration { } let key = TransactionKey::from_request(&request, TransactionRole::Client)?; - // Reuse the connection that supplied Via, including for authentication retries. - // On lookup failure, retain the existing transaction retry/timeout behavior. - let (connection, destination) = match resolved { - Some((connection, destination)) => (Some(connection), Some(destination)), - None => (None, proxy_destination), - }; - let mut tx = Transaction::new_client(key, request, self.endpoint.clone(), connection); - tx.destination = destination; + let mut tx = Transaction::new_client(key, request, self.endpoint.clone(), None); + + // Override transport destination if outbound proxy is configured. + // This keeps the domain in SIP headers (Request-URI, From, To) while + // sending all packets to the pinned proxy IP for NAT consistency. + if let Some(proxy) = &self.outbound_proxy { + let mut dest = SipAddr::from(*proxy); + // Inherit transport type from the request URI (e.g., TCP) + if let Some(Param::Transport(t)) = tx + .original + .uri() + .params + .iter() + .find(|p| matches!(p, Param::Transport(_))) + { + dest.r#type = Some(*t); + } + tx.destination = Some(dest); + } tx.send().await?; let mut auth_sent = false; diff --git a/src/transaction/endpoint.rs b/src/transaction/endpoint.rs index 15bf025d..973c620a 100644 --- a/src/transaction/endpoint.rs +++ b/src/transaction/endpoint.rs @@ -109,7 +109,7 @@ pub struct EndpointInner { #[allow(dead_code)] timer_interval: Duration, pub(super) message_inspector: Option>, - pub(crate) locator: Option>, + pub(super) locator: Option>, pub(super) transport_inspector: Option>, pub option: EndpointOption, } From cd7771b64483a85886e06217f38e71b87754f6f3 Mon Sep 17 00:00:00 2001 From: yeoleobun Date: Mon, 14 Sep 2026 16:31:34 +0800 Subject: [PATCH 5/5] style: format REGISTER Via transport fix --- src/dialog/registration.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/dialog/registration.rs b/src/dialog/registration.rs index d8169905..a2acd7f1 100644 --- a/src/dialog/registration.rs +++ b/src/dialog/registration.rs @@ -393,10 +393,15 @@ impl Registration { // this is safe to do unconditionally rather than only for TCP/TLS. let via = match SipAddr::try_from(&server) { Ok(target_addr) => { - match self.endpoint.transport_layer.lookup(&target_addr, None).await { - Ok((connection, _resolved)) => { - self.endpoint.get_via(Some(connection.get_addr().clone()), None)? - } + match self + .endpoint + .transport_layer + .lookup(&target_addr, None) + .await + { + Ok((connection, _resolved)) => self + .endpoint + .get_via(Some(connection.get_addr().clone()), None)?, Err(_) => self.endpoint.get_via(None, None)?, } }