From 4013cd9d56fa64ec38eabb97a4f35ec3b1dae85d Mon Sep 17 00:00:00 2001 From: Tung-Yang Li Date: Sun, 6 Sep 2026 00:07:03 +0800 Subject: [PATCH] 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)