1use std::net::{IpAddr, SocketAddr};
16
17use sipx_sip::{Host, Uri, UriTransport};
18
19use crate::target::{Target, TransportKind};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Naptr {
24 pub order: u16,
26 pub preference: u16,
28 pub service: String,
30 pub replacement: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Srv {
37 pub priority: u16,
39 pub weight: u16,
41 pub port: u16,
43 pub target: String,
45}
46
47pub trait Resolver: Send + Sync {
52 fn naptr(&self, domain: &str) -> Vec<Naptr>;
54 fn srv(&self, name: &str) -> Vec<Srv>;
56 fn addresses(&self, host: &str) -> Vec<IpAddr>;
58}
59
60pub trait Rng: Send + Sync {
65 fn below(&mut self, max: u32) -> u32;
67}
68
69#[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#[derive(Debug)]
84pub struct SeededRng(u64);
85
86impl SeededRng {
87 #[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 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 u32::try_from((self.0 >> 33) % (u64::from(max) + 1)).unwrap_or(0)
109 }
110}
111
112fn permitted(uri: &Uri) -> Vec<TransportKind> {
114 if uri.scheme().is_secure() {
115 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
130fn 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
154fn 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
167fn 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
179pub fn resolve<R: Resolver + ?Sized, G: Rng + ?Sized>(
184 uri: &Uri,
185 resolver: &R,
186 rng: &mut G,
187) -> Vec<Target> {
188 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
214fn 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 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 return resolver
241 .addresses(&domain)
242 .into_iter()
243 .map(|ip| Target::new(SocketAddr::new(ip, port), default_transport))
244 .collect();
245 }
246
247 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 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
292fn 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 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
332fn 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
355fn weighted_shuffle<G: Rng + ?Sized>(mut group: Vec<Srv>, rng: &mut G) -> Vec<Srv> {
361 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}