Skip to content

URI parameter parser truncates values at colon β€” breaks Record-Route du= parameters from KamailioΒ #106

@Object905

Description

@Object905

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=sip

Tested 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:

  1. du=sip:95.143.188.49:5060 β†’ truncated to du=sip
  2. ;did=893.d6d1 β†’ silently dropped (the unparsed :95.143.188.49:5060 consumes the rest)
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions