Skip to main content

sipx_transport/
nat.rs

1//! `received` and `rport` — what makes SIP work through a NAT (RFC 3261 §18.2.1, RFC 3581).
2//!
3//! A client behind a NAT puts its private address in the `Via` sent-by, because that is all it
4//! knows. A response sent there goes nowhere. The server therefore records where the request
5//! *actually* came from, and the client's NAT pinhole is still open on that address and port.
6//!
7//! The edits here are surgical: only the topmost hop is touched, and within it only the two
8//! parameters in question. The hops below belong to other elements and go back out exactly as
9//! they arrived.
10
11use std::net::SocketAddr;
12
13use bytes::Bytes;
14use sipx_sip::headers::{Via, first_hop_end};
15use sipx_sip::{Header, HeaderName, Host, Request};
16
17/// Add `received` and, if asked, `rport` to the topmost `Via` of a received request.
18///
19/// Returns whether anything changed.
20pub fn apply_received_and_rport(request: &mut Request, source: SocketAddr) -> bool {
21    let Some(header) = request.headers.get(&HeaderName::Via) else {
22        return false;
23    };
24    let value = header.value().into_owned();
25    let hop_end = first_hop_end(&value);
26    let hop = value.get(..hop_end).unwrap_or(&value);
27    let Ok(via) = Via::parse_one(hop) else {
28        return false;
29    };
30
31    let mut updated_hop = hop.to_vec();
32    let mut changed = false;
33
34    // RFC 3581: an empty `rport` is the client asking which port we saw. The answer replaces
35    // the empty parameter — appending a second `rport` would leave the first one, and a
36    // reader taking the first occurrence would learn nothing.
37    if matches!(via.rport(), Some(None))
38        && let Some((start, end)) = param_span(&updated_hop, b"rport")
39    {
40        let replacement = format!(";rport={}", source.port());
41        updated_hop.splice(start..end, replacement.into_bytes());
42        changed = true;
43    }
44
45    // RFC 3261 §18.2.1: record the source when it differs from what the sender claimed. A
46    // hostname sent-by always counts as differing, because comparing it would mean resolving
47    // it — and the whole point is that the sender may be wrong about where it is.
48    //
49    // RFC 3581 §4 goes further for a sender that asked for `rport`: `received` is added
50    // "even if it is identical to the value of the sent-by component". Without it the
51    // response is routed by sent-by, at the sent-by port — which is the very thing `rport`
52    // was sent to correct.
53    let sent_by_matches = match &via.host {
54        Host::Ip(ip) => *ip == source.ip(),
55        Host::Name(_) => false,
56    };
57    let asked_for_rport = matches!(via.rport(), Some(None));
58    if (asked_for_rport || !sent_by_matches) && via.received().is_none() {
59        let addition = format!(";received={}", source.ip());
60        // Before the parameters, not after: `received` conventionally sits next to the
61        // sent-by, and inserting at a parameter boundary keeps the value well-formed however
62        // many parameters follow.
63        let insert_at =
64            param_span(&updated_hop, b"branch").map_or(updated_hop.len(), |(start, _)| start);
65        updated_hop.splice(insert_at..insert_at, addition.into_bytes());
66        changed = true;
67    }
68
69    if !changed {
70        return false;
71    }
72
73    let mut rebuilt = Vec::with_capacity(value.len() + 32);
74    rebuilt.extend_from_slice(&updated_hop);
75    rebuilt.extend_from_slice(value.get(hop_end..).unwrap_or(&[]));
76    replace_top_via(request, Bytes::from(rebuilt));
77    true
78}
79
80/// The span of `;name` or `;name=value` within one via-parm, quote-aware.
81pub(crate) fn param_span(hop: &[u8], name: &[u8]) -> Option<(usize, usize)> {
82    let mut i = 0usize;
83    while i < hop.len() {
84        match hop.get(i) {
85            Some(b'"') => {
86                i = quoted_end(hop, i)?;
87            }
88            Some(b';') => {
89                let start = i;
90                let mut j = i + 1;
91                while matches!(hop.get(j), Some(b' ' | b'\t')) {
92                    j += 1;
93                }
94                let name_start = j;
95                while hop
96                    .get(j)
97                    .is_some_and(|b| !matches!(b, b';' | b'=' | b' ' | b'\t'))
98                {
99                    j += 1;
100                }
101                let found = hop.get(name_start..j).unwrap_or(&[]);
102                // Skip an `=value`, which may itself be quoted.
103                let mut end = j;
104                while matches!(hop.get(end), Some(b' ' | b'\t')) {
105                    end += 1;
106                }
107                if hop.get(end) == Some(&b'=') {
108                    end += 1;
109                    while matches!(hop.get(end), Some(b' ' | b'\t')) {
110                        end += 1;
111                    }
112                    if hop.get(end) == Some(&b'"') {
113                        end = quoted_end(hop, end)?;
114                    } else {
115                        while hop.get(end).is_some_and(|&b| b != b';') {
116                            end += 1;
117                        }
118                    }
119                }
120                if found.eq_ignore_ascii_case(name) {
121                    return Some((start, end));
122                }
123                i = end;
124            }
125            Some(_) => i += 1,
126            None => break,
127        }
128    }
129    None
130}
131
132fn quoted_end(input: &[u8], at: usize) -> Option<usize> {
133    let mut i = at + 1;
134    while let Some(&b) = input.get(i) {
135        match b {
136            b'\\' if i + 1 < input.len() => i += 2,
137            b'"' => return Some(i + 1),
138            _ => i += 1,
139        }
140    }
141    None
142}
143
144/// Replace the first `Via` header, keeping every other header where it was.
145///
146/// Was a rebuild of the whole collection — a fresh `Headers` and a clone of every header, to
147/// change one — because that was the only thing the API allowed. `remove_first` plus `push_front`
148/// says the same thing in two operations and clones nothing.
149fn replace_top_via(request: &mut Request, value: Bytes) {
150    let Ok(header) = Header::build(HeaderName::Via, value) else {
151        return;
152    };
153    // Only when there is one to replace. The one caller today has already read the top `Via`, so
154    // this cannot fire from there — it is the function's contract rather than a live guard, and it
155    // is here because "replace" and "add" differ in a way that matters: the topmost `Via` is where
156    // the response goes, so adding one to a request that had none redirects the answer.
157    if request.headers.remove_first(&HeaderName::Via).is_some() {
158        request.headers.push_front(header);
159    }
160}
161
162#[cfg(test)]
163#[allow(
164    clippy::unwrap_used,
165    clippy::expect_used,
166    clippy::panic,
167    clippy::indexing_slicing
168)]
169mod tests {
170    use super::*;
171    use sipx_sip::{Limits, Message, parse_datagram};
172
173    fn request_with(via: &str) -> Request {
174        let text = format!(
175            "OPTIONS sip:a@b.com SIP/2.0\r\n\
176             Via: {via}\r\n\
177             To: <sip:a@b.com>\r\n\
178             From: <sip:c@d.net>;tag=1\r\n\
179             Call-ID: x@y\r\n\
180             CSeq: 1 OPTIONS\r\n\
181             Max-Forwards: 70\r\n\
182             Content-Length: 0\r\n\r\n"
183        );
184        match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
185            Message::Request(r) => r,
186            Message::Response(_) => panic!("a request"),
187        }
188    }
189
190    fn source() -> SocketAddr {
191        "203.0.113.9:41234".parse().expect("valid")
192    }
193
194    fn top_via(request: &Request) -> String {
195        String::from_utf8_lossy(&request.headers.value(&HeaderName::Via).expect("a Via"))
196            .into_owned()
197    }
198
199    fn parsed_via(request: &Request) -> Via {
200        request
201            .headers
202            .typed::<Via>()
203            .expect("a Via")
204            .expect("it parses")
205    }
206
207    #[test]
208    fn received_is_added_when_the_source_differs_from_the_sent_by() {
209        let mut request = request_with("SIP/2.0/UDP 10.0.0.5:5060;branch=z9hG4bKx");
210        assert!(apply_received_and_rport(&mut request, source()));
211        assert_eq!(
212            parsed_via(&request).received().map(<[u8]>::to_vec),
213            Some(b"203.0.113.9".to_vec())
214        );
215    }
216
217    #[test]
218    fn received_is_not_added_when_the_sent_by_is_already_right() {
219        let mut request = request_with("SIP/2.0/UDP 203.0.113.9:41234;branch=z9hG4bKx");
220        assert!(!apply_received_and_rport(&mut request, source()));
221        assert!(!top_via(&request).contains("received"));
222    }
223
224    /// RFC 3581: an empty `rport` is a question, and the answer replaces it. Appending a
225    /// second `rport` instead would leave the empty one first, where every reader looks.
226    #[test]
227    fn an_empty_rport_is_replaced_not_duplicated() {
228        let mut request = request_with("SIP/2.0/UDP 10.0.0.5:5060;rport;branch=z9hG4bKx");
229        assert!(apply_received_and_rport(&mut request, source()));
230
231        let via = top_via(&request);
232        assert_eq!(via.matches("rport").count(), 1, "exactly one rport: {via}");
233        assert_eq!(
234            parsed_via(&request).rport().flatten().map(<[u8]>::to_vec),
235            Some(b"41234".to_vec())
236        );
237        assert_eq!(
238            parsed_via(&request).branch().map(<[u8]>::to_vec),
239            Some(b"z9hG4bKx".to_vec()),
240            "the branch must survive the surgery"
241        );
242    }
243
244    /// RFC 3581 §4 is explicit that asking for `rport` also asks for `received`: the server
245    /// "MUST insert a `received` parameter containing the source IP address that the request
246    /// came from, even if it is identical to the value of the `sent-by` component". Omitting
247    /// it when the addresses agree leaves the response to be routed by sent-by, which is the
248    /// port `rport` exists to correct.
249    #[test]
250    fn rport_brings_received_with_it_even_when_the_sent_by_matches() {
251        let mut request = request_with("SIP/2.0/UDP 203.0.113.9:41234;rport;branch=z9hG4bKx");
252        assert!(apply_received_and_rport(&mut request, source()));
253        assert_eq!(
254            parsed_via(&request).rport().flatten().map(<[u8]>::to_vec),
255            Some(b"41234".to_vec())
256        );
257        assert_eq!(
258            parsed_via(&request).received().map(<[u8]>::to_vec),
259            Some(b"203.0.113.9".to_vec())
260        );
261    }
262
263    #[test]
264    fn an_absent_rport_is_not_invented() {
265        let mut request = request_with("SIP/2.0/UDP 10.0.0.5:5060;branch=z9hG4bKx");
266        apply_received_and_rport(&mut request, source());
267        assert!(!top_via(&request).contains("rport"));
268    }
269
270    #[test]
271    fn an_rport_that_already_has_a_value_is_left_alone() {
272        let mut request = request_with("SIP/2.0/UDP 203.0.113.9:5060;rport=9999;branch=z9hG4bKx");
273        apply_received_and_rport(&mut request, source());
274        assert_eq!(
275            parsed_via(&request).rport().flatten().map(<[u8]>::to_vec),
276            Some(b"9999".to_vec())
277        );
278    }
279
280    /// The hops below the top one belong to other elements and must be left exactly alone.
281    #[test]
282    fn only_the_topmost_hop_is_touched() {
283        let mut request = request_with(
284            "SIP/2.0/UDP 10.0.0.5:5060;rport;branch=z9hG4bK1, SIP/2.0/UDP 192.0.2.7:5060;branch=z9hG4bK2",
285        );
286        assert!(apply_received_and_rport(&mut request, source()));
287        let via = top_via(&request);
288        assert!(
289            via.ends_with("SIP/2.0/UDP 192.0.2.7:5060;branch=z9hG4bK2"),
290            "the second hop is untouched: {via}"
291        );
292        assert_eq!(via.matches("received").count(), 1);
293        assert_eq!(
294            parsed_via(&request).rport().flatten().map(<[u8]>::to_vec),
295            Some(b"41234".to_vec()),
296            "the top hop is the one that got the answer"
297        );
298    }
299
300    #[test]
301    fn every_other_header_keeps_its_place() {
302        let mut request = request_with("SIP/2.0/UDP 10.0.0.5:5060;branch=z9hG4bKx");
303        let before: Vec<String> = request
304            .headers
305            .iter()
306            .map(|h| h.name().to_string())
307            .collect();
308        apply_received_and_rport(&mut request, source());
309        let after: Vec<String> = request
310            .headers
311            .iter()
312            .map(|h| h.name().to_string())
313            .collect();
314        assert_eq!(before, after);
315    }
316
317    #[test]
318    fn parameter_spans_are_found_regardless_of_position_and_quoting() {
319        let hop = br#"SIP/2.0/UDP h;a=1;rport;note="x;y";branch=z"#;
320        let (start, end) = param_span(hop, b"rport").expect("rport is there");
321        assert_eq!(&hop[start..end], b";rport");
322        let (start, end) = param_span(hop, b"branch").expect("branch is there");
323        assert_eq!(&hop[start..end], b";branch=z");
324        let (start, end) = param_span(hop, b"note").expect("note is there");
325        assert_eq!(&hop[start..end], br#";note="x;y""#);
326        assert!(param_span(hop, b"absent").is_none());
327    }
328
329    /// `replace_top_via` replaces and does not add.
330    ///
331    /// Its one caller today has already read a `Via`, so it cannot reach this — which is exactly
332    /// why the property is worth pinning here rather than left to that caller's shape. Adding a
333    /// `Via` to a request that had none redirects the response, and the second caller is how that
334    /// bug arrives.
335    #[test]
336    fn replacing_the_top_via_on_a_request_without_one_adds_nothing() {
337        let text = "OPTIONS sip:a@b.com SIP/2.0\r\n\
338             To: <sip:a@b.com>\r\n\
339             From: <sip:c@d.net>;tag=1\r\n\
340             Call-ID: x@y\r\n\
341             CSeq: 1 OPTIONS\r\n\
342             Max-Forwards: 70\r\n\
343             Content-Length: 0\r\n\r\n";
344        let mut request = match parse_datagram(Bytes::from(text.to_owned()), &Limits::datagram())
345            .expect("parses")
346        {
347            Message::Request(r) => r,
348            Message::Response(_) => panic!("a request"),
349        };
350        assert_eq!(request.headers.count(&HeaderName::Via), 0);
351
352        replace_top_via(
353            &mut request,
354            Bytes::from_static(b"SIP/2.0/UDP invented.example"),
355        );
356
357        assert_eq!(
358            request.headers.count(&HeaderName::Via),
359            0,
360            "a request with no Via must not acquire one; the topmost Via is where the response goes"
361        );
362    }
363}