Skip to main content

sipx_transport/
resolve.rs

1//! RFC 3263: turning a SIP URI into an ordered list of places to try.
2//!
3//! The RFC's procedure is short to state and easy to get subtly wrong. What matters:
4//!
5//! - An IP literal or an explicit port means no lookup at all. The URI has already answered.
6//! - NAPTR chooses the *transport*; SRV chooses the *host and port*; A/AAAA chooses the
7//!   address. Skipping a stage changes which deployments are reachable.
8//! - `sips:` restricts the candidates to TLS. Falling back to UDP because TLS was unavailable
9//!   would silently downgrade a request the user asked to be secure.
10//! - The result is a *list*. One candidate failing is normal, and the request has not failed
11//!   until the list is exhausted.
12//!
13//! DNS itself is behind a trait: tests use a fixture and never touch a resolver.
14
15use std::net::{IpAddr, SocketAddr};
16
17use sipx_sip::{Host, Uri, UriTransport};
18
19use crate::target::{Target, TransportKind};
20
21/// A NAPTR record, reduced to what RFC 3263 uses.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Naptr {
24    /// Lower is preferred.
25    pub order: u16,
26    /// Lower is preferred, within an order.
27    pub preference: u16,
28    /// `SIP+D2U`, `SIPS+D2T` and friends.
29    pub service: String,
30    /// The SRV name to look up next.
31    pub replacement: String,
32}
33
34/// An SRV record.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Srv {
37    /// Lower is preferred.
38    pub priority: u16,
39    /// Relative share within a priority (RFC 2782).
40    pub weight: u16,
41    /// The port to use.
42    pub port: u16,
43    /// The host to resolve.
44    pub target: String,
45}
46
47/// What a resolver must be able to answer.
48///
49/// A trait rather than a concrete DNS client so that the selection logic — which is where the
50/// bugs live — is testable without a network.
51pub trait Resolver: Send + Sync {
52    /// NAPTR records for a domain.
53    fn naptr(&self, domain: &str) -> Vec<Naptr>;
54    /// SRV records for a name.
55    fn srv(&self, name: &str) -> Vec<Srv>;
56    /// Addresses for a host.
57    fn addresses(&self, host: &str) -> Vec<IpAddr>;
58}
59
60/// A source of randomness for RFC 2782 weighted selection.
61///
62/// Injectable so the distribution is testable with a fixed seed; a test that cannot pin the
63/// randomness can only assert that selection did *something*.
64pub trait Rng: Send + Sync {
65    /// A value in `0..=max`.
66    fn below(&mut self, max: u32) -> u32;
67}
68
69/// The thread RNG.
70#[derive(Debug, Default)]
71pub struct OsRng;
72
73impl Rng for OsRng {
74    fn below(&mut self, max: u32) -> u32 {
75        if max == 0 {
76            return 0;
77        }
78        rand::Rng::random_range(&mut rand::rng(), 0..=max)
79    }
80}
81
82/// A deterministic RNG for tests: a linear congruential generator with a fixed seed.
83#[derive(Debug)]
84pub struct SeededRng(u64);
85
86impl SeededRng {
87    /// A generator with the given seed.
88    #[must_use]
89    pub fn new(seed: u64) -> Self {
90        Self(seed)
91    }
92}
93
94impl Rng for SeededRng {
95    fn below(&mut self, max: u32) -> u32 {
96        // Numerical Recipes' constants. Adequate for choosing among SRV records; this is not
97        // used for anything security-relevant, which is why `new_branch` does not use it.
98        self.0 = self
99            .0
100            .wrapping_mul(6_364_136_223_846_793_005)
101            .wrapping_add(1_442_695_040_888_963_407);
102        if max == 0 {
103            return 0;
104        }
105        // Inclusive of both ends, as the trait says: a generator that never returns 0 cannot
106        // select the first record of a group, which is where RFC 2782 puts the zero-weight
107        // ones — the skew would be invisible in production and wrong only under test.
108        u32::try_from((self.0 >> 33) % (u64::from(max) + 1)).unwrap_or(0)
109    }
110}
111
112/// Which transports a scheme permits.
113fn permitted(uri: &Uri) -> Vec<TransportKind> {
114    if uri.scheme().is_secure() {
115        // A `sips:` URI is a request for TLS. Falling back to UDP because TLS was unavailable
116        // would silently downgrade exactly the thing the scheme asked for.
117        vec![TransportKind::Tls, TransportKind::Wss, TransportKind::Quic]
118    } else {
119        vec![
120            TransportKind::Udp,
121            TransportKind::Tcp,
122            TransportKind::Tls,
123            TransportKind::Ws,
124            TransportKind::Wss,
125            TransportKind::Quic,
126        ]
127    }
128}
129
130/// The transport to use when no NAPTR or SRV record narrows it down.
131///
132/// For a `sips:` URI the `transport` parameter names the transport carried *under* TLS
133/// (RFC 3261 §26.2.2 and Table 1), so `transport=tcp` asks for TLS over TCP rather than for
134/// cleartext TCP. Taking the parameter at face value is a downgrade on exactly the paths that
135/// never reach the SRV stage where the scheme filter is applied — an IP literal, an explicit
136/// port, and the bare A-record last resort.
137///
138/// `None` means the URI names nothing reachable: RFC 3261 defines no TLS over UDP, so a
139/// `sips:` URI asking for UDP has no secure candidate, and inventing a cleartext one is the
140/// single answer that is wrong.
141fn default_transport(uri: &Uri) -> Option<TransportKind> {
142    uri.selected_transport()
143        .ok()
144        .map(|transport| match transport {
145            UriTransport::Udp => TransportKind::Udp,
146            UriTransport::Tcp => TransportKind::Tcp,
147            UriTransport::Tls => TransportKind::Tls,
148            UriTransport::Ws => TransportKind::Ws,
149            UriTransport::Wss => TransportKind::Wss,
150            UriTransport::Quic => TransportKind::Quic,
151        })
152}
153
154/// Map a NAPTR service field to a transport (RFC 3263 §4.1).
155fn service_transport(service: &str) -> Option<TransportKind> {
156    match service.to_ascii_uppercase().as_str() {
157        "SIP+D2U" => Some(TransportKind::Udp),
158        "SIP+D2T" => Some(TransportKind::Tcp),
159        "SIPS+D2T" => Some(TransportKind::Tls),
160        "SIP+D2W" => Some(TransportKind::Ws),
161        "SIPS+D2W" => Some(TransportKind::Wss),
162        "SIPS+D2Q" => Some(TransportKind::Quic),
163        _ => None,
164    }
165}
166
167/// The conventional SRV prefix for a transport.
168fn srv_prefix(transport: TransportKind) -> &'static str {
169    match transport {
170        TransportKind::Udp => "_sip._udp.",
171        TransportKind::Tcp => "_sip._tcp.",
172        TransportKind::Tls => "_sips._tcp.",
173        TransportKind::Ws => "_sip._ws.",
174        TransportKind::Wss => "_sips._wss.",
175        TransportKind::Quic => "_sips._quic.",
176    }
177}
178
179/// Resolve a URI to an ordered list of candidates.
180///
181/// The list is tried in order; a transport failure moves to the next. The request has not
182/// failed until every candidate has.
183pub fn resolve<R: Resolver + ?Sized, G: Rng + ?Sized>(
184    uri: &Uri,
185    resolver: &R,
186    rng: &mut G,
187) -> Vec<Target> {
188    // Every secure or WebSocket candidate carries the name from the URI, and resolution is
189    // exactly why. Clear WS needs it for HTTP authority even though it verifies no certificate.
190    //
191    // Without this, a `sips:` URI resolved through NAPTR and SRV arrives at an address with
192    // nothing attached, and the certificate ends up checked against whatever that address or
193    // SRV target happens to be called. That is the failure `docs/specs/sip-tls.md` §3.3 exists
194    // to prevent: the check still runs, the handshake still succeeds, and whoever can influence
195    // DNS has chosen which certificate is acceptable.
196    let identity = match uri.host() {
197        Some(Host::Name(name)) => String::from_utf8_lossy(name.as_bytes()).into_owned(),
198        Some(Host::Ip(ip)) => ip.to_string(),
199        None => String::new(),
200    };
201    let named_authority = matches!(uri.host(), Some(Host::Name(_)));
202    candidates(uri, resolver, rng)
203        .into_iter()
204        .map(|target| match target.transport {
205            TransportKind::Tls | TransportKind::Wss | TransportKind::Quic => {
206                target.verifying(&identity)
207            }
208            TransportKind::Ws if named_authority => target.verifying(&identity),
209            _ => target,
210        })
211        .collect()
212}
213
214/// Where a URI's addresses come from, before the verification name is attached.
215fn candidates<R: Resolver + ?Sized, G: Rng + ?Sized>(
216    uri: &Uri,
217    resolver: &R,
218    rng: &mut G,
219) -> Vec<Target> {
220    let allowed = permitted(uri);
221    let Some(default_transport) = default_transport(uri) else {
222        return Vec::new();
223    };
224
225    // §4.2: an IP literal, or an explicit port, ends the procedure. The URI has answered.
226    if let Some(Host::Ip(ip)) = uri.host() {
227        let port = uri
228            .port()
229            .unwrap_or_else(|| default_transport.default_port());
230        return vec![Target::new(SocketAddr::new(*ip, port), default_transport)];
231    }
232
233    let Some(Host::Name(name)) = uri.host() else {
234        return Vec::new();
235    };
236    let domain = String::from_utf8_lossy(name.as_bytes()).into_owned();
237
238    if let Some(port) = uri.port() {
239        // A port was given, so no SRV lookup — but the name still has to become an address.
240        return resolver
241            .addresses(&domain)
242            .into_iter()
243            .map(|ip| Target::new(SocketAddr::new(ip, port), default_transport))
244            .collect();
245    }
246
247    // An explicit `transport=` parameter skips NAPTR: the caller has already chosen. What it
248    // chose is `default_transport`, which has already resolved the parameter against the
249    // scheme rather than trusting it verbatim.
250    let transports: Vec<(TransportKind, String)> = if uri.transport().is_some() {
251        vec![(
252            default_transport,
253            format!("{}{domain}", srv_prefix(default_transport)),
254        )]
255    } else {
256        naptr_transports(&domain, resolver, &allowed)
257    };
258
259    let mut targets = Vec::new();
260    for (transport, srv_name) in transports {
261        if !allowed.contains(&transport) {
262            continue;
263        }
264        let records = resolver.srv(&srv_name);
265        if records.is_empty() {
266            continue;
267        }
268        for srv in order_srv(records, rng) {
269            for ip in resolver.addresses(&srv.target) {
270                targets.push(Target::new(SocketAddr::new(ip, srv.port), transport));
271            }
272        }
273    }
274
275    if !targets.is_empty() {
276        return targets;
277    }
278
279    // §4.2 last resort: no NAPTR, no SRV — resolve the name and use the default port.
280    resolver
281        .addresses(&domain)
282        .into_iter()
283        .map(|ip| {
284            Target::new(
285                SocketAddr::new(ip, default_transport.default_port()),
286                default_transport,
287            )
288        })
289        .collect()
290}
291
292/// NAPTR lookup, reduced to an ordered list of (transport, SRV name).
293///
294/// When there are no NAPTR records the RFC says to try the SRV names directly, in an order of
295/// the implementation's choosing among the transports it supports.
296fn naptr_transports<R: Resolver + ?Sized>(
297    domain: &str,
298    resolver: &R,
299    allowed: &[TransportKind],
300) -> Vec<(TransportKind, String)> {
301    let mut records = resolver.naptr(domain);
302    if records.is_empty() {
303        return allowed
304            .iter()
305            .filter(|t| {
306                !matches!(
307                    t,
308                    TransportKind::Ws | TransportKind::Wss | TransportKind::Quic
309                )
310            })
311            .map(|&t| (t, format!("{}{domain}", srv_prefix(t))))
312            .collect();
313    }
314
315    // Order first, then preference — both ascending, both "lower is better".
316    records.sort_by_key(|r| {
317        (
318            r.order,
319            r.preference,
320            r.service.eq_ignore_ascii_case("SIPS+D2Q"),
321        )
322    });
323    records
324        .into_iter()
325        .filter_map(|record| {
326            let transport = service_transport(&record.service)?;
327            Some((transport, record.replacement))
328        })
329        .collect()
330}
331
332/// Order SRV records: priority ascending, and within a priority the RFC 2782 weighted shuffle.
333fn order_srv<G: Rng + ?Sized>(mut records: Vec<Srv>, rng: &mut G) -> Vec<Srv> {
334    records.sort_by_key(|r| r.priority);
335
336    let mut ordered = Vec::with_capacity(records.len());
337    let mut rest = records;
338    while !rest.is_empty() {
339        let priority = rest.first().map_or(0, |r| r.priority);
340        let mut group: Vec<Srv> = Vec::new();
341        let mut remainder: Vec<Srv> = Vec::new();
342        for record in rest {
343            if record.priority == priority {
344                group.push(record);
345            } else {
346                remainder.push(record);
347            }
348        }
349        ordered.extend(weighted_shuffle(group, rng));
350        rest = remainder;
351    }
352    ordered
353}
354
355/// RFC 2782's selection: pick with probability proportional to weight, repeatedly.
356///
357/// The RFC's own wording — running sum, pick a random number in `0..=total`, take the first
358/// entry whose running sum is at least that number. A weight of 0 is legal and means "only if
359/// nothing else is available", which falls out of the arithmetic rather than needing a case.
360fn weighted_shuffle<G: Rng + ?Sized>(mut group: Vec<Srv>, rng: &mut G) -> Vec<Srv> {
361    // RFC 2782: "all those with weight 0 are placed at the beginning of the list". Left where
362    // they arrived, an earlier non-zero record always satisfies the running-sum test first and
363    // a zero-weight record is chosen with probability exactly zero — not the "very small"
364    // chance the RFC intends, which is what keeps a spare server in rotation at all.
365    group.sort_by_key(|record| record.weight != 0);
366
367    let mut ordered = Vec::with_capacity(group.len());
368    while !group.is_empty() {
369        let total: u32 = group.iter().map(|r| u32::from(r.weight)).sum();
370        let pick = rng.below(total);
371
372        let mut running = 0u32;
373        let mut chosen = group.len().saturating_sub(1);
374        for (index, record) in group.iter().enumerate() {
375            running += u32::from(record.weight);
376            if running >= pick {
377                chosen = index;
378                break;
379            }
380        }
381        if chosen < group.len() {
382            ordered.push(group.remove(chosen));
383        }
384    }
385    ordered
386}
387
388#[cfg(test)]
389#[allow(
390    clippy::unwrap_used,
391    clippy::expect_used,
392    clippy::panic,
393    clippy::indexing_slicing
394)]
395mod tests {
396    use super::*;
397    use std::collections::HashMap;
398
399    #[derive(Debug, Default)]
400    struct Fixture {
401        naptr: HashMap<String, Vec<Naptr>>,
402        srv: HashMap<String, Vec<Srv>>,
403        addresses: HashMap<String, Vec<IpAddr>>,
404    }
405
406    impl Fixture {
407        fn with_address(mut self, host: &str, addr: &str) -> Self {
408            self.addresses
409                .entry(host.to_owned())
410                .or_default()
411                .push(addr.parse().expect("a valid address"));
412            self
413        }
414
415        fn with_srv(mut self, name: &str, records: Vec<Srv>) -> Self {
416            self.srv.insert(name.to_owned(), records);
417            self
418        }
419
420        fn with_naptr(mut self, domain: &str, records: Vec<Naptr>) -> Self {
421            self.naptr.insert(domain.to_owned(), records);
422            self
423        }
424    }
425
426    impl Resolver for Fixture {
427        fn naptr(&self, domain: &str) -> Vec<Naptr> {
428            self.naptr.get(domain).cloned().unwrap_or_default()
429        }
430        fn srv(&self, name: &str) -> Vec<Srv> {
431            self.srv.get(name).cloned().unwrap_or_default()
432        }
433        fn addresses(&self, host: &str) -> Vec<IpAddr> {
434            self.addresses.get(host).cloned().unwrap_or_default()
435        }
436    }
437
438    fn uri(text: &str) -> Uri {
439        Uri::parse(bytes::Bytes::from(text.to_owned())).expect("a valid URI")
440    }
441
442    fn srv(priority: u16, weight: u16, port: u16, target: &str) -> Srv {
443        Srv {
444            priority,
445            weight,
446            port,
447            target: target.to_owned(),
448        }
449    }
450
451    /// §4.2: an IP literal has already answered the question.
452    #[test]
453    fn an_ip_literal_short_circuits_resolution() {
454        let targets = resolve(
455            &uri("sip:192.0.2.10:5080"),
456            &Fixture::default(),
457            &mut SeededRng::new(1),
458        );
459        assert_eq!(targets.len(), 1);
460        assert_eq!(targets[0].addr.to_string(), "192.0.2.10:5080");
461        assert_eq!(targets[0].transport, TransportKind::Udp);
462    }
463
464    #[test]
465    fn an_ip_literal_without_a_port_uses_the_transport_default() {
466        let targets = resolve(
467            &uri("sips:192.0.2.10"),
468            &Fixture::default(),
469            &mut SeededRng::new(1),
470        );
471        assert_eq!(targets[0].addr.port(), 5061);
472        assert_eq!(targets[0].transport, TransportKind::Tls);
473    }
474
475    /// An explicit port means no SRV lookup — but the name still has to be resolved.
476    #[test]
477    fn an_explicit_port_skips_srv_but_not_the_address_lookup() {
478        let fixture = Fixture::default()
479            .with_address("example.com", "192.0.2.20")
480            .with_srv("_sip._udp.example.com", vec![srv(1, 1, 9999, "wrong.com")]);
481        let targets = resolve(
482            &uri("sip:example.com:5080"),
483            &fixture,
484            &mut SeededRng::new(1),
485        );
486        assert_eq!(targets.len(), 1);
487        assert_eq!(
488            targets[0].addr.to_string(),
489            "192.0.2.20:5080",
490            "the SRV port must not override an explicit one"
491        );
492    }
493
494    #[test]
495    fn naptr_chooses_the_transport_and_srv_the_port() {
496        let fixture = Fixture::default()
497            .with_naptr(
498                "example.com",
499                vec![
500                    Naptr {
501                        order: 20,
502                        preference: 10,
503                        service: "SIP+D2U".to_owned(),
504                        replacement: "_sip._udp.example.com".to_owned(),
505                    },
506                    Naptr {
507                        order: 10,
508                        preference: 10,
509                        service: "SIP+D2T".to_owned(),
510                        replacement: "_sip._tcp.example.com".to_owned(),
511                    },
512                ],
513            )
514            .with_srv(
515                "_sip._tcp.example.com",
516                vec![srv(1, 0, 5060, "tcp.example.com")],
517            )
518            .with_srv(
519                "_sip._udp.example.com",
520                vec![srv(1, 0, 5060, "udp.example.com")],
521            )
522            .with_address("tcp.example.com", "192.0.2.30")
523            .with_address("udp.example.com", "192.0.2.31");
524
525        let targets = resolve(&uri("sip:example.com"), &fixture, &mut SeededRng::new(1));
526        assert_eq!(
527            targets[0].transport,
528            TransportKind::Tcp,
529            "order 10 is preferred over order 20"
530        );
531        assert_eq!(targets[0].addr.to_string(), "192.0.2.30:5060");
532        assert_eq!(targets[1].transport, TransportKind::Udp);
533    }
534
535    #[test]
536    fn a_sips_d2q_naptr_record_selects_quic_explicitly() {
537        let resolver = Fixture::default()
538            .with_naptr(
539                "example.com",
540                vec![Naptr {
541                    order: 10,
542                    preference: 10,
543                    service: "SIPS+D2Q".to_owned(),
544                    replacement: "_sips._quic.example.com".to_owned(),
545                }],
546            )
547            .with_srv(
548                "_sips._quic.example.com",
549                vec![Srv {
550                    priority: 0,
551                    weight: 0,
552                    port: 5071,
553                    target: "quic.example.com".to_owned(),
554                }],
555            )
556            .with_address("quic.example.com", "192.0.2.44");
557        let targets = resolve(
558            &uri("sips:alice@example.com"),
559            &resolver,
560            &mut SeededRng::new(1),
561        );
562
563        assert_eq!(targets.len(), 1);
564        assert_eq!(targets[0].transport, TransportKind::Quic);
565        assert_eq!(targets[0].addr.to_string(), "192.0.2.44:5071");
566        assert_eq!(targets[0].verify_as.as_deref(), Some("example.com"));
567    }
568
569    #[test]
570    fn tls_wins_an_equal_naptr_choice_over_the_experimental_quic_mapping() {
571        let resolver = Fixture::default()
572            .with_naptr(
573                "example.com",
574                vec![
575                    Naptr {
576                        order: 10,
577                        preference: 10,
578                        service: "SIPS+D2Q".to_owned(),
579                        replacement: "_sips._quic.example.com".to_owned(),
580                    },
581                    Naptr {
582                        order: 10,
583                        preference: 10,
584                        service: "SIPS+D2T".to_owned(),
585                        replacement: "_sips._tcp.example.com".to_owned(),
586                    },
587                ],
588            )
589            .with_srv(
590                "_sips._quic.example.com",
591                vec![srv(0, 0, 5061, "quic.example.com")],
592            )
593            .with_srv(
594                "_sips._tcp.example.com",
595                vec![srv(0, 0, 5061, "tls.example.com")],
596            )
597            .with_address("quic.example.com", "192.0.2.44")
598            .with_address("tls.example.com", "192.0.2.45");
599        let targets = resolve(
600            &uri("sips:alice@example.com"),
601            &resolver,
602            &mut SeededRng::new(1),
603        );
604
605        assert_eq!(targets.len(), 2);
606        assert_eq!(targets[0].transport, TransportKind::Tls);
607        assert_eq!(targets[1].transport, TransportKind::Quic);
608    }
609
610    #[test]
611    fn sips_without_an_explicit_quic_naptr_record_does_not_try_quic() {
612        let resolver = Fixture::default()
613            .with_srv(
614                "_sips._quic.example.com",
615                vec![srv(0, 0, 5061, "quic.example.com")],
616            )
617            .with_address("quic.example.com", "192.0.2.44")
618            .with_address("example.com", "192.0.2.45");
619        let targets = resolve(
620            &uri("sips:alice@example.com"),
621            &resolver,
622            &mut SeededRng::new(1),
623        );
624
625        assert_eq!(targets.len(), 1);
626        assert_eq!(targets[0].transport, TransportKind::Tls);
627        assert_eq!(targets[0].addr.to_string(), "192.0.2.45:5061");
628    }
629
630    /// A `sips:` URI is a request for TLS. Falling back to UDP because TLS was unavailable
631    /// would silently downgrade exactly what the scheme asked for.
632    #[test]
633    fn sips_never_yields_a_cleartext_candidate() {
634        let fixture = Fixture::default()
635            .with_naptr(
636                "secure.example",
637                vec![
638                    Naptr {
639                        order: 10,
640                        preference: 10,
641                        service: "SIP+D2U".to_owned(),
642                        replacement: "_sip._udp.secure.example".to_owned(),
643                    },
644                    Naptr {
645                        order: 20,
646                        preference: 10,
647                        service: "SIPS+D2T".to_owned(),
648                        replacement: "_sips._tcp.secure.example".to_owned(),
649                    },
650                ],
651            )
652            .with_srv(
653                "_sip._udp.secure.example",
654                vec![srv(1, 0, 5060, "plain.secure.example")],
655            )
656            .with_srv(
657                "_sips._tcp.secure.example",
658                vec![srv(1, 0, 5061, "tls.secure.example")],
659            )
660            .with_address("plain.secure.example", "192.0.2.40")
661            .with_address("tls.secure.example", "192.0.2.41");
662
663        let targets = resolve(
664            &uri("sips:secure.example"),
665            &fixture,
666            &mut SeededRng::new(1),
667        );
668        assert!(!targets.is_empty(), "TLS is available and must be found");
669        for target in &targets {
670            assert!(
671                matches!(target.transport, TransportKind::Tls | TransportKind::Wss),
672                "sips must not yield {:?}",
673                target.transport
674            );
675        }
676    }
677
678    /// The name a certificate must be valid for is the one in the URI, and it survives
679    /// resolution. Without this a `sips:` URI arrives at an address with nothing attached, the
680    /// certificate is checked against whatever the SRV target or the address happens to be
681    /// called, and whoever can influence DNS chooses which certificate is acceptable — the
682    /// handshake still succeeds and the check has become decorative (`sip-tls.md` §3.3).
683    #[test]
684    fn a_secure_candidate_carries_the_uri_host_not_the_resolved_one() {
685        let fixture = Fixture::default()
686            .with_naptr(
687                "secure.example",
688                vec![Naptr {
689                    order: 10,
690                    preference: 10,
691                    service: "SIPS+D2T".to_owned(),
692                    replacement: "_sips._tcp.secure.example".to_owned(),
693                }],
694            )
695            .with_srv(
696                "_sips._tcp.secure.example",
697                // A SRV target with a different name entirely, which is normal: SRV exists so
698                // the service can live somewhere other than the domain it serves.
699                vec![srv(1, 0, 5061, "edge-07.hosting.example")],
700            )
701            .with_address("edge-07.hosting.example", "192.0.2.41");
702
703        let targets = resolve(
704            &uri("sips:secure.example"),
705            &fixture,
706            &mut SeededRng::new(1),
707        );
708        assert!(!targets.is_empty(), "a candidate must be found");
709        for target in &targets {
710            assert_eq!(
711                target.verify_as.as_deref(),
712                Some("secure.example"),
713                "not the SRV target and not the address"
714            );
715        }
716    }
717
718    /// A cleartext candidate carries nothing: there is no certificate, so an identity here
719    /// would only be something for a later reader to mistake for one.
720    #[test]
721    fn a_cleartext_candidate_carries_no_identity() {
722        let fixture = Fixture::default()
723            .with_address("plain.example", "192.0.2.50")
724            .with_naptr("plain.example", Vec::new());
725
726        for target in resolve(&uri("sip:plain.example"), &fixture, &mut SeededRng::new(1)) {
727            assert!(target.verify_as.is_none(), "{target:?}");
728        }
729    }
730
731    /// RFC 3261 §26.2.2 and Table 1: in a `sips:` URI the transport parameter names the
732    /// transport carried *under* TLS, so `transport=tcp` means TLS over TCP. Reading it as
733    /// cleartext TCP downgrades the one thing the scheme was used to ask for, and it does so
734    /// on the paths that never reach the SRV stage where the sips filter lives: an IP literal,
735    /// an explicit port, and the bare A-record last resort.
736    #[test]
737    fn sips_with_a_transport_parameter_stays_secure() {
738        let literal = resolve(
739            &uri("sips:192.0.2.1;transport=tcp"),
740            &Fixture::default(),
741            &mut SeededRng::new(1),
742        );
743        assert_eq!(literal.len(), 1);
744        assert_eq!(literal[0].transport, TransportKind::Tls);
745        assert_eq!(literal[0].addr.to_string(), "192.0.2.1:5061");
746
747        let fixture = Fixture::default().with_address("secure.example", "192.0.2.42");
748        let last_resort = resolve(
749            &uri("sips:secure.example;transport=tcp"),
750            &fixture,
751            &mut SeededRng::new(1),
752        );
753        assert_eq!(last_resort.len(), 1);
754        assert_eq!(last_resort[0].transport, TransportKind::Tls);
755        assert_eq!(last_resort[0].addr.to_string(), "192.0.2.42:5061");
756        assert_eq!(last_resort[0].verify_as.as_deref(), Some("secure.example"));
757
758        let with_port = resolve(
759            &uri("sips:secure.example:9999;transport=tcp"),
760            &fixture,
761            &mut SeededRng::new(1),
762        );
763        assert_eq!(with_port.len(), 1);
764        assert_eq!(with_port[0].transport, TransportKind::Tls);
765        assert_eq!(with_port[0].addr.to_string(), "192.0.2.42:9999");
766    }
767
768    /// RFC 3261 defines no TLS over UDP, so a `sips:` URI asking for it names nothing that can
769    /// be reached securely. No candidate is the honest answer; a cleartext one is not.
770    #[test]
771    fn sips_over_udp_yields_nothing_rather_than_cleartext() {
772        let fixture = Fixture::default().with_address("secure.example", "192.0.2.42");
773        let targets = resolve(
774            &uri("sips:secure.example;transport=udp"),
775            &fixture,
776            &mut SeededRng::new(1),
777        );
778        assert!(targets.is_empty(), "{targets:?}");
779    }
780
781    /// And when TLS is *not* available, the answer is no candidates — not a downgrade.
782    #[test]
783    fn sips_with_no_tls_available_yields_nothing_rather_than_downgrading() {
784        let fixture = Fixture::default()
785            .with_naptr(
786                "plain.example",
787                vec![Naptr {
788                    order: 10,
789                    preference: 10,
790                    service: "SIP+D2U".to_owned(),
791                    replacement: "_sip._udp.plain.example".to_owned(),
792                }],
793            )
794            .with_srv(
795                "_sip._udp.plain.example",
796                vec![srv(1, 0, 5060, "host.plain.example")],
797            );
798        let targets = resolve(&uri("sips:plain.example"), &fixture, &mut SeededRng::new(1));
799        assert!(targets.is_empty());
800    }
801
802    #[test]
803    fn an_explicit_transport_parameter_skips_naptr() {
804        let fixture = Fixture::default()
805            .with_naptr(
806                "example.com",
807                vec![Naptr {
808                    order: 10,
809                    preference: 10,
810                    service: "SIP+D2U".to_owned(),
811                    replacement: "_sip._udp.example.com".to_owned(),
812                }],
813            )
814            .with_srv(
815                "_sip._tcp.example.com",
816                vec![srv(1, 0, 5060, "t.example.com")],
817            )
818            .with_address("t.example.com", "192.0.2.50");
819
820        let targets = resolve(
821            &uri("sip:example.com;transport=tcp"),
822            &fixture,
823            &mut SeededRng::new(1),
824        );
825        assert_eq!(targets.len(), 1);
826        assert_eq!(targets[0].transport, TransportKind::Tcp);
827    }
828
829    #[test]
830    fn priority_is_absolute_and_weight_only_orders_within_it() {
831        let fixture = Fixture::default()
832            .with_srv(
833                "_sip._udp.example.com",
834                vec![
835                    srv(20, 100, 5060, "low.example.com"),
836                    srv(10, 1, 5060, "high.example.com"),
837                ],
838            )
839            .with_address("low.example.com", "192.0.2.60")
840            .with_address("high.example.com", "192.0.2.61");
841
842        for seed in 0..20 {
843            let targets = resolve(&uri("sip:example.com"), &fixture, &mut SeededRng::new(seed));
844            assert_eq!(
845                targets[0].addr.ip().to_string(),
846                "192.0.2.61",
847                "priority 10 always precedes priority 20, whatever the weights"
848            );
849        }
850    }
851
852    /// RFC 2782 weighted selection: over many draws, the share of first-picks should track the
853    /// weights. With 10 and 90 the split should be near one in ten.
854    #[test]
855    fn srv_weighted_selection_matches_rfc2782_distribution() {
856        let records = vec![
857            srv(1, 10, 5060, "light.example"),
858            srv(1, 90, 5060, "heavy.example"),
859        ];
860
861        let mut light_first: i32 = 0;
862        let draws: i32 = 4000;
863        for seed in 0..u64::try_from(draws).unwrap_or(0) {
864            let mut rng = SeededRng::new(seed);
865            let ordered = weighted_shuffle(records.clone(), &mut rng);
866            if ordered.first().map(|r| r.target.as_str()) == Some("light.example") {
867                light_first += 1;
868            }
869        }
870
871        let share = f64::from(light_first) / f64::from(draws);
872        assert!(
873            (0.05..0.16).contains(&share),
874            "a weight of 10 against 90 should win about a tenth of the time, got {share}"
875        );
876    }
877
878    /// A weight of 0 is legal and means "only if nothing else is available".
879    #[test]
880    fn a_zero_weight_record_is_still_reachable() {
881        let records = vec![
882            srv(1, 0, 5060, "spare.example"),
883            srv(1, 100, 5060, "main.example"),
884        ];
885        let ordered = weighted_shuffle(records, &mut SeededRng::new(7));
886        assert_eq!(ordered.len(), 2, "every record appears exactly once");
887        assert!(ordered.iter().any(|r| r.target == "spare.example"));
888    }
889
890    /// RFC 2782 requires weight-0 records to be moved to the front of the group before the
891    /// running-sum walk. Left where they arrived, an earlier non-zero record always satisfies
892    /// `running >= pick` first, so a zero-weight target is chosen with probability exactly
893    /// zero rather than the "very small" one the RFC describes — it becomes reachable only
894    /// once every other record in its priority has been consumed.
895    #[test]
896    fn a_zero_weight_record_listed_last_is_still_sometimes_chosen_first() {
897        let records = vec![
898            srv(1, 100, 5060, "main.example"),
899            srv(1, 0, 5060, "spare.example"),
900        ];
901        let chosen_first = (0..200).filter(|&seed| {
902            let ordered = weighted_shuffle(records.clone(), &mut SeededRng::new(seed));
903            ordered.first().is_some_and(|r| r.target == "spare.example")
904        });
905        assert!(
906            chosen_first.count() > 0,
907            "a zero-weight record must retain a small chance of being picked first"
908        );
909    }
910
911    /// Every record must survive the shuffle. Losing one silently removes a server from
912    /// rotation, which is the kind of bug that shows up as capacity that is never used.
913    #[test]
914    fn the_shuffle_is_a_permutation() {
915        let records = vec![
916            srv(1, 1, 5060, "a"),
917            srv(1, 2, 5060, "b"),
918            srv(1, 3, 5060, "c"),
919            srv(1, 0, 5060, "d"),
920        ];
921        for seed in 0..50 {
922            let ordered = weighted_shuffle(records.clone(), &mut SeededRng::new(seed));
923            let mut names: Vec<&str> = ordered.iter().map(|r| r.target.as_str()).collect();
924            names.sort_unstable();
925            assert_eq!(names, vec!["a", "b", "c", "d"]);
926        }
927    }
928
929    /// §4.2's last resort: no NAPTR, no SRV, just an A record and the default port.
930    #[test]
931    fn a_bare_a_record_is_the_last_resort() {
932        let fixture = Fixture::default().with_address("simple.example", "192.0.2.70");
933        let targets = resolve(&uri("sip:simple.example"), &fixture, &mut SeededRng::new(1));
934        assert_eq!(targets.len(), 1);
935        assert_eq!(targets[0].addr.to_string(), "192.0.2.70:5060");
936        assert_eq!(targets[0].transport, TransportKind::Udp);
937    }
938
939    #[test]
940    fn a_name_that_resolves_to_nothing_yields_no_candidates() {
941        let targets = resolve(
942            &uri("sip:nowhere.example"),
943            &Fixture::default(),
944            &mut SeededRng::new(1),
945        );
946        assert!(targets.is_empty());
947    }
948
949    /// Multiple addresses for one SRV target are all candidates — falling through them is the
950    /// point of returning a list.
951    #[test]
952    fn every_address_of_a_target_becomes_a_candidate() {
953        let fixture = Fixture::default()
954            .with_srv(
955                "_sip._udp.example.com",
956                vec![srv(1, 0, 5060, "multi.example.com")],
957            )
958            .with_address("multi.example.com", "192.0.2.80")
959            .with_address("multi.example.com", "192.0.2.81");
960        let targets = resolve(&uri("sip:example.com"), &fixture, &mut SeededRng::new(1));
961        assert_eq!(targets.len(), 2);
962    }
963}