-
Notifications
You must be signed in to change notification settings - Fork 45
Description
While I know this issue belongs to rsip, not rsipstack. But rsip is inactive for 4 years now π«€ . Perhaps a fork is in due.
I've used claude to dig into the problem. Will create a fork and verify that this is indeed what's affecting me - because when I try to call specific sip endpoint - audio is not coming through, while everything works fine for other endpoints.
Summary
The URI parameter tokenizer uses is_token() to parse parameter values, but this character set does not include : (colon). This causes parameter values like du=sip:95.143.188.49:5060 to be silently truncated to du=sip, with everything after the first colon lost. Subsequent parameters (;did=893.d6d1) are also silently dropped.
This is a data-loss bug that breaks SIP call routing in production β specifically, ACK messages constructed from parsed Record-Route headers fail to reach the endpoint because the proxy's internal routing parameter (du) is destroyed.
Root cause
In src/common/uri/param/mod.rs, lines 169-173:
let (rem, (_, name, value)) = tuple((
tag(";"),
take_while(I::is_token),
opt(map(tuple((tag("="), take_while(I::is_token))), |t| t.1)),
))(part)Where is_token is defined in src/lib.rs:
fn is_token(c: u8) -> bool {
is_alphanumeric(c) || "-.!%*_+`'~".contains(char::from(c))
}The colon (:) is not in this set. Per RFC 3261 Β§25.1 (ABNF grammar), URI parameter values allow paramchar, which includes param-unreserved:
param-unreserved = "[" / "]" / "/" / ":" / "&" / "+" / "$"
The colon is explicitly permitted in SIP URI parameter values. The code even has a comment acknowledging this: `//rfc3261 includes other chars as well, needs fixing..
Reproduction
use rsip::headers::{RecordRoute, ToTypedHeader};
use rsip::headers::typed::RecordRoute as TypedRecordRoute;
// Kamailio Record-Route header with du= containing a SIP URI
let input = "<sip:82.202.218.130;lr=on;ftag=d4nwJ0jF;du=sip:95.143.188.49:5060;did=893.d6d1>";
let rr = RecordRoute::from(input);
let typed: TypedRecordRoute = rr.typed().unwrap();
let uri = &typed.uris()[0];
println!("{}", uri);
// Expected: sip:82.202.218.130;lr=on;ftag=d4nwJ0jF;du=sip:95.143.188.49:5060;did=893.d6d1
// Actual: sip:82.202.218.130;lr=on;ftag=d4nwJ0jF;du=sipTested output:
=== Test 1: Record-Route with du=sip:host:port ===
Input: <sip:82.202.218.130;lr=on;ftag=d4nwJ0jF;du=sip:95.143.188.49:5060;did=893.d6d1>
Output: <sip:82.202.218.130;lr=on;ftag=d4nwJ0jF;du=sip>
URI params:
Other(OtherParam("lr"), Some(OtherParamValue("on")))
Other(OtherParam("ftag"), Some(OtherParamValue("d4nwJ0jF")))
Other(OtherParam("du"), Some(OtherParamValue("sip")))
^^^ truncated! lost ":95.143.188.49:5060"
=== Round-trip data loss ===
Input: <sip:82.202.218.130;lr=on;ftag=abc;du=sip:10.0.0.1:5060;did=123.456>
Output: <sip:82.202.218.130;lr=on;ftag=abc;du=sip>
Match: DATA LOSS!
Three things go wrong:
du=sip:95.143.188.49:5060β truncated todu=sip;did=893.d6d1β silently dropped (the unparsed:95.143.188.49:5060consumes the rest)- No error returned β the parse "succeeds" with corrupted data
Real-world impact
Kamailio (and other SIP proxies) encode internal routing information in Record-Route URI parameters:
Record-Route: <sip:82.202.218.130;lr=on;ftag=d4nwJ0jF;du=sip:95.143.188.49:5060;did=893.d6d1>
Record-Route: <sip:31.129.43.67:32222;lr=on;ftag=d4nwJ0jF>
When a UAC constructs an ACK for a 2xx response, it must parse these Record-Route headers, reverse them, and include them as Route headers. The truncated du=sip causes the downstream proxy to misroute the ACK β the endpoint never receives it, 200 OK retransmissions continue indefinitely, and the call eventually fails.
This is a production call-routing failure affecting any call path where an intermediate proxy uses du= parameters containing SIP URIs (standard Kamailio nathelper/dialog behavior).
Suggested fix
Add the RFC 3261 param-unreserved characters to the is_token set, or create a separate is_paramchar function for use in the parameter value parser:
// RFC 3261 Β§25.1: paramchar = param-unreserved / unreserved / escaped
// param-unreserved = "[" / "]" / "/" / ":" / "&" / "+" / "$"
fn is_paramchar(c: u8) -> bool {
is_token(c) || b"[]/:&+$".contains(&c)
}Then in the parameter tokenizer, use is_paramchar for the value (not the name):
let (rem, (_, name, value)) = tuple((
tag(";"),
take_while(I::is_token), // name: token chars
opt(map(tuple((tag("="), take_while(I::is_paramchar))), |t| t.1)), // value: paramchar
))(part)Note: the parameter name should continue using is_token β only the value needs the expanded character set.