1use crate::StreamPrefs;
5use crate::err::ErrorDetail;
6use std::fmt::Display;
7use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
8use std::str::FromStr;
9use thiserror::Error;
10use tor_basic_utils::StrExt;
11use tor_error::{ErrorKind, HasKind};
12
13#[cfg(feature = "onion-service-client")]
14use tor_hscrypto::pk::{HSID_ONION_SUFFIX, HsId};
15
16#[cfg(not(feature = "onion-service-client"))]
18pub(crate) mod hs_dummy {
19 use super::*;
20 use tor_error::internal;
21 use void::Void;
22
23 #[derive(Debug, Clone)]
25 pub(crate) struct HsId(pub(crate) Void);
26
27 impl PartialEq for HsId {
28 fn eq(&self, _other: &Self) -> bool {
29 void::unreachable(self.0)
30 }
31 }
32 impl Eq for HsId {}
33
34 pub(crate) const HSID_ONION_SUFFIX: &str = ".onion";
36
37 impl FromStr for HsId {
39 type Err = ErrorDetail;
40
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 if !s.ends_with(HSID_ONION_SUFFIX) {
43 return Err(internal!("non-.onion passed to dummy HsId::from_str").into());
44 }
45
46 Err(ErrorDetail::OnionAddressNotSupported)
47 }
48 }
49}
50#[cfg(not(feature = "onion-service-client"))]
51use hs_dummy::*;
52
53pub trait IntoTorAddr {
78 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError>;
81}
82
83pub trait DangerouslyIntoTorAddr {
95 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError>;
101}
102
103#[derive(Debug, Clone, Eq, PartialEq)]
160pub struct TorAddr {
161 host: Host,
163 port: u16,
165}
166
167#[derive(Debug, PartialEq, Eq)]
173pub(crate) enum StreamInstructions {
174 Exit {
176 hostname: String,
178 port: u16,
180 },
181 Hs {
185 hsid: HsId,
187 hostname: String,
189 port: u16,
191 },
192}
193
194#[derive(PartialEq, Eq, Debug)]
196pub(crate) enum ResolveInstructions {
197 Exit(String),
199 Return(Vec<IpAddr>),
201}
202
203impl TorAddr {
204 fn new(host: Host, port: u16) -> Result<Self, TorAddrError> {
207 if port == 0 {
208 Err(TorAddrError::BadPort)
209 } else {
210 Ok(TorAddr { host, port })
211 }
212 }
213
214 pub fn from<A: IntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
217 addr.into_tor_addr()
218 }
219 pub fn dangerously_from<A: DangerouslyIntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
226 addr.into_tor_addr_dangerously()
227 }
228
229 pub fn is_ip_address(&self) -> bool {
231 matches!(&self.host, Host::Ip(_))
232 }
233
234 pub fn as_ip_address(&self) -> Option<&IpAddr> {
236 match &self.host {
237 Host::Ip(a) => Some(a),
238 _ => None,
239 }
240 }
241
242 pub(crate) fn into_stream_instructions(
244 self,
245 cfg: &crate::config::ClientAddrConfig,
246 prefs: &StreamPrefs,
247 ) -> Result<StreamInstructions, ErrorDetail> {
248 self.enforce_config(cfg, prefs)?;
249
250 let port = self.port;
251 Ok(match self.host {
252 Host::Hostname(hostname) => StreamInstructions::Exit { hostname, port },
253 Host::Ip(ip) => StreamInstructions::Exit {
254 hostname: ip.to_string(),
255 port,
256 },
257 Host::Onion(onion) => {
258 let rhs = onion
260 .rmatch_indices('.')
261 .nth(1)
262 .map(|(i, _)| i + 1)
263 .unwrap_or(0);
264 let rhs = &onion[rhs..];
265 let hsid = rhs.parse()?;
266 StreamInstructions::Hs {
267 hsid,
268 port,
269 hostname: onion,
270 }
271 }
272 })
273 }
274
275 pub(crate) fn into_resolve_instructions(
277 self,
278 cfg: &crate::config::ClientAddrConfig,
279 prefs: &StreamPrefs,
280 ) -> Result<ResolveInstructions, ErrorDetail> {
281 let enforce_config_result = self.enforce_config(cfg, prefs);
286
287 let instructions = (move || {
290 Ok(match self.host {
291 Host::Hostname(hostname) => ResolveInstructions::Exit(hostname),
292 Host::Ip(ip) => ResolveInstructions::Return(vec![ip]),
293 Host::Onion(_) => return Err(ErrorDetail::OnionAddressResolveRequest),
294 })
295 })()?;
296
297 let () = enforce_config_result?;
298
299 Ok(instructions)
300 }
301
302 fn is_local(&self) -> bool {
304 self.host.is_local()
305 }
306
307 fn enforce_config(
310 &self,
311 cfg: &crate::config::ClientAddrConfig,
312 #[allow(unused_variables)] prefs: &StreamPrefs,
314 ) -> Result<(), ErrorDetail> {
315 if !cfg.allow_local_addrs && self.is_local() {
316 return Err(ErrorDetail::LocalAddress);
317 }
318
319 if let Host::Hostname(addr) = &self.host {
320 if !is_valid_hostname(addr) {
321 return Err(ErrorDetail::InvalidHostname);
323 }
324 if addr.ends_with_ignore_ascii_case(HSID_ONION_SUFFIX) {
325 return Err(ErrorDetail::OnionAddressNotSupported);
327 }
328 }
329
330 if let Host::Onion(_name) = &self.host {
331 cfg_if::cfg_if! {
332 if #[cfg(feature = "onion-service-client")] {
333 if !prefs.connect_to_onion_services.as_bool().unwrap_or(cfg.allow_onion_addrs) {
334 return Err(ErrorDetail::OnionAddressDisabled);
335 }
336 } else {
337 return Err(ErrorDetail::OnionAddressNotSupported);
338 }
339 }
340 }
341
342 Ok(())
343 }
344}
345
346impl std::fmt::Display for TorAddr {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 match self.host {
349 Host::Ip(IpAddr::V6(addr)) => write!(f, "[{}]:{}", addr, self.port),
350 _ => write!(f, "{}:{}", self.host, self.port),
351 }
352 }
353}
354
355#[derive(Debug, Error, Clone, Eq, PartialEq)]
360#[non_exhaustive]
361pub enum TorAddrError {
362 #[error("String can never be a valid hostname")]
364 InvalidHostname,
365 #[error("No port found in string")]
367 NoPort,
368 #[error("Could not parse port")]
370 BadPort,
371}
372
373impl HasKind for TorAddrError {
374 fn kind(&self) -> ErrorKind {
375 use ErrorKind as EK;
376 use TorAddrError as TAE;
377
378 match self {
379 TAE::InvalidHostname => EK::InvalidStreamTarget,
380 TAE::NoPort => EK::InvalidStreamTarget,
381 TAE::BadPort => EK::InvalidStreamTarget,
382 }
383 }
384}
385
386#[derive(Clone, Debug, Eq, PartialEq)]
396enum Host {
397 Hostname(String),
409 Ip(IpAddr),
411 Onion(String),
416}
417
418impl FromStr for Host {
419 type Err = TorAddrError;
420 fn from_str(s: &str) -> Result<Host, TorAddrError> {
421 if s.ends_with_ignore_ascii_case(".onion") && is_valid_hostname(s) {
422 Ok(Host::Onion(s.to_owned()))
423 } else if let Ok(ip_addr) = s.parse() {
424 Ok(Host::Ip(ip_addr))
425 } else if is_valid_hostname(s) {
426 Ok(Host::Hostname(s.to_owned()))
432 } else {
433 Err(TorAddrError::InvalidHostname)
434 }
435 }
436}
437
438impl Host {
439 fn is_local(&self) -> bool {
442 match self {
443 Host::Hostname(name) => name.eq_ignore_ascii_case("localhost"),
444 Host::Ip(IpAddr::V4(ip)) => ip.is_loopback() || ip.is_private(),
451 Host::Ip(IpAddr::V6(ip)) => ip.is_loopback(),
452 Host::Onion(_) => false,
453 }
454 }
455}
456
457impl std::fmt::Display for Host {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 match self {
460 Host::Hostname(s) => Display::fmt(s, f),
461 Host::Ip(ip) => Display::fmt(ip, f),
462 Host::Onion(onion) => Display::fmt(onion, f),
463 }
464 }
465}
466
467impl IntoTorAddr for TorAddr {
468 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
469 Ok(self)
470 }
471}
472
473impl<A: IntoTorAddr + Clone> IntoTorAddr for &A {
474 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
475 self.clone().into_tor_addr()
476 }
477}
478
479impl IntoTorAddr for &str {
480 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
481 if let Ok(sa) = SocketAddr::from_str(self) {
482 TorAddr::new(Host::Ip(sa.ip()), sa.port())
483 } else {
484 let (host, port) = self.rsplit_once(':').ok_or(TorAddrError::NoPort)?;
485 let host = host.parse()?;
486 let port = port.parse().map_err(|_| TorAddrError::BadPort)?;
487 TorAddr::new(host, port)
488 }
489 }
490}
491
492impl IntoTorAddr for String {
493 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
494 self[..].into_tor_addr()
495 }
496}
497
498impl FromStr for TorAddr {
499 type Err = TorAddrError;
500 fn from_str(s: &str) -> Result<Self, TorAddrError> {
501 s.into_tor_addr()
502 }
503}
504
505impl IntoTorAddr for (&str, u16) {
506 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
507 let (host, port) = self;
508 let host = host.parse()?;
509 TorAddr::new(host, port)
510 }
511}
512
513impl IntoTorAddr for (String, u16) {
514 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
515 let (host, port) = self;
516 (&host[..], port).into_tor_addr()
517 }
518}
519
520impl<T: DangerouslyIntoTorAddr + Clone> DangerouslyIntoTorAddr for &T {
521 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
522 self.clone().into_tor_addr_dangerously()
523 }
524}
525
526impl DangerouslyIntoTorAddr for (IpAddr, u16) {
527 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
528 let (addr, port) = self;
529 TorAddr::new(Host::Ip(addr), port)
530 }
531}
532
533impl DangerouslyIntoTorAddr for (Ipv4Addr, u16) {
534 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
535 let (addr, port) = self;
536 TorAddr::new(Host::Ip(addr.into()), port)
537 }
538}
539
540impl DangerouslyIntoTorAddr for (Ipv6Addr, u16) {
541 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
542 let (addr, port) = self;
543 TorAddr::new(Host::Ip(addr.into()), port)
544 }
545}
546
547impl DangerouslyIntoTorAddr for SocketAddr {
548 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
549 let (addr, port) = (self.ip(), self.port());
550 (addr, port).into_tor_addr_dangerously()
551 }
552}
553
554impl DangerouslyIntoTorAddr for SocketAddrV4 {
555 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
556 let (addr, port) = (self.ip(), self.port());
557 (*addr, port).into_tor_addr_dangerously()
558 }
559}
560
561impl DangerouslyIntoTorAddr for SocketAddrV6 {
562 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
563 let (addr, port) = (self.ip(), self.port());
564 (*addr, port).into_tor_addr_dangerously()
565 }
566}
567
568fn is_valid_hostname(hostname: &str) -> bool {
572 hostname_validator::is_valid(hostname)
573}
574
575#[cfg(test)]
576mod test {
577 #![allow(clippy::bool_assert_comparison)]
579 #![allow(clippy::clone_on_copy)]
580 #![allow(clippy::dbg_macro)]
581 #![allow(clippy::mixed_attributes_style)]
582 #![allow(clippy::print_stderr)]
583 #![allow(clippy::print_stdout)]
584 #![allow(clippy::single_char_pattern)]
585 #![allow(clippy::unwrap_used)]
586 #![allow(clippy::unchecked_time_subtraction)]
587 #![allow(clippy::useless_vec)]
588 #![allow(clippy::needless_pass_by_value)]
589 use super::*;
591
592 #[test]
593 fn test_error_kind() {
594 use tor_error::ErrorKind as EK;
595
596 assert_eq!(
597 TorAddrError::InvalidHostname.kind(),
598 EK::InvalidStreamTarget
599 );
600 assert_eq!(TorAddrError::NoPort.kind(), EK::InvalidStreamTarget);
601 assert_eq!(TorAddrError::BadPort.kind(), EK::InvalidStreamTarget);
602 }
603
604 fn mk_stream_prefs() -> StreamPrefs {
606 let prefs = crate::StreamPrefs::default();
607
608 #[cfg(feature = "onion-service-client")]
609 let prefs = {
610 let mut prefs = prefs;
611 prefs.connect_to_onion_services(tor_config::BoolOrAuto::Explicit(true));
612 prefs
613 };
614
615 prefs
616 }
617
618 #[test]
619 fn validate_hostname() {
620 assert!(is_valid_hostname("torproject.org"));
622 assert!(is_valid_hostname("Tor-Project.org"));
623 assert!(is_valid_hostname("example.onion"));
624 assert!(is_valid_hostname("some.example.onion"));
625
626 assert!(!is_valid_hostname("-torproject.org"));
628 assert!(!is_valid_hostname("_torproject.org"));
629 assert!(!is_valid_hostname("tor_project1.org"));
630 assert!(!is_valid_hostname("iwanna$money.org"));
631 }
632
633 #[test]
634 fn validate_addr() {
635 use crate::err::ErrorDetail;
636 fn val<A: IntoTorAddr>(addr: A) -> Result<TorAddr, ErrorDetail> {
637 let toraddr = addr.into_tor_addr()?;
638 toraddr.enforce_config(&Default::default(), &mk_stream_prefs())?;
639 Ok(toraddr)
640 }
641
642 assert!(val("[2001:db8::42]:20").is_ok());
643 assert!(val(("2001:db8::42", 20)).is_ok());
644 assert!(val(("198.151.100.42", 443)).is_ok());
645 assert!(val("198.151.100.42:443").is_ok());
646 assert!(val("www.torproject.org:443").is_ok());
647 assert!(val(("www.torproject.org", 443)).is_ok());
648
649 #[cfg(feature = "onion-service-client")]
651 {
652 assert!(val("example.onion:80").is_ok());
653 assert!(val(("example.onion", 80)).is_ok());
654
655 match val("eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443") {
656 Ok(TorAddr {
657 host: Host::Onion(_),
658 ..
659 }) => {}
660 x => panic!("{x:?}"),
661 }
662 }
663
664 assert!(matches!(
665 val("-foobar.net:443"),
666 Err(ErrorDetail::InvalidHostname)
667 ));
668 assert!(matches!(
669 val("www.torproject.org"),
670 Err(ErrorDetail::Address(TorAddrError::NoPort))
671 ));
672
673 assert!(matches!(
674 val("192.168.0.1:80"),
675 Err(ErrorDetail::LocalAddress)
676 ));
677 assert!(matches!(
678 val(TorAddr::new(Host::Hostname("foo@bar".to_owned()), 553).unwrap()),
679 Err(ErrorDetail::InvalidHostname)
680 ));
681 assert!(matches!(
682 val(TorAddr::new(Host::Hostname("foo.onion".to_owned()), 80).unwrap()),
683 Err(ErrorDetail::OnionAddressNotSupported)
684 ));
685 }
686
687 #[test]
688 fn local_addrs() {
689 fn is_local_hostname(s: &str) -> bool {
690 let h: Host = s.parse().unwrap();
691 h.is_local()
692 }
693
694 assert!(is_local_hostname("localhost"));
695 assert!(is_local_hostname("loCALHOST"));
696 assert!(is_local_hostname("127.0.0.1"));
697 assert!(is_local_hostname("::1"));
698 assert!(is_local_hostname("192.168.0.1"));
699
700 assert!(!is_local_hostname("www.example.com"));
701 }
702
703 #[test]
704 fn is_ip_address() {
705 fn ip(s: &str) -> bool {
706 TorAddr::from(s).unwrap().is_ip_address()
707 }
708
709 assert!(ip("192.168.0.1:80"));
710 assert!(ip("[::1]:80"));
711 assert!(ip("[2001:db8::42]:65535"));
712 assert!(!ip("example.com:80"));
713 assert!(!ip("example.onion:80"));
714 }
715
716 #[test]
717 fn stream_instructions() {
718 use StreamInstructions as SI;
719
720 fn sap(s: &str) -> Result<StreamInstructions, ErrorDetail> {
721 TorAddr::from(s)
722 .unwrap()
723 .into_stream_instructions(&Default::default(), &mk_stream_prefs())
724 }
725
726 assert_eq!(
727 sap("[2001:db8::42]:9001").unwrap(),
728 SI::Exit {
729 hostname: "2001:db8::42".to_owned(),
730 port: 9001
731 },
732 );
733 assert_eq!(
734 sap("example.com:80").unwrap(),
735 SI::Exit {
736 hostname: "example.com".to_owned(),
737 port: 80
738 },
739 );
740
741 {
742 let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad";
743 let onion = format!("sss1234.www.{}.onion", b32);
744 let got = sap(&format!("{}:443", onion));
745
746 #[cfg(feature = "onion-service-client")]
747 assert_eq!(
748 got.unwrap(),
749 SI::Hs {
750 hsid: format!("{}.onion", b32).parse().unwrap(),
751 hostname: onion,
752 port: 443,
753 }
754 );
755
756 #[cfg(not(feature = "onion-service-client"))]
757 assert!(matches!(got, Err(ErrorDetail::OnionAddressNotSupported)));
758 }
759 }
760
761 #[test]
762 fn resolve_instructions() {
763 use ResolveInstructions as RI;
764
765 fn sap(s: &str) -> Result<ResolveInstructions, ErrorDetail> {
766 TorAddr::from(s)
767 .unwrap()
768 .into_resolve_instructions(&Default::default(), &Default::default())
769 }
770
771 assert_eq!(
772 sap("[2001:db8::42]:9001").unwrap(),
773 RI::Return(vec!["2001:db8::42".parse().unwrap()]),
774 );
775 assert_eq!(
776 sap("example.com:80").unwrap(),
777 RI::Exit("example.com".to_owned()),
778 );
779 assert!(matches!(
780 sap("example.onion:80"),
781 Err(ErrorDetail::OnionAddressResolveRequest),
782 ));
783 }
784
785 #[test]
786 fn bad_ports() {
787 assert_eq!(
788 TorAddr::from("www.example.com:squirrel"),
789 Err(TorAddrError::BadPort)
790 );
791 assert_eq!(
792 TorAddr::from("www.example.com:0"),
793 Err(TorAddrError::BadPort)
794 );
795 }
796
797 #[test]
798 fn prefs_onion_services() {
799 use crate::err::ErrorDetailDiscriminants;
800 use ErrorDetailDiscriminants as EDD;
801 use ErrorKind as EK;
802 use tor_error::{ErrorKind, HasKind as _};
803
804 #[allow(clippy::redundant_closure)] let prefs_def = || StreamPrefs::default();
806
807 let addr: TorAddr = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443"
808 .parse()
809 .unwrap();
810
811 fn map(
812 got: Result<impl Sized, ErrorDetail>,
813 ) -> Result<(), (ErrorDetailDiscriminants, ErrorKind)> {
814 got.map(|_| ())
815 .map_err(|e| (ErrorDetailDiscriminants::from(&e), e.kind()))
816 }
817
818 let check_stream = |prefs, expected| {
819 let got = addr
820 .clone()
821 .into_stream_instructions(&Default::default(), &prefs);
822 assert_eq!(map(got), expected, "{prefs:?}");
823 };
824 let check_resolve = |prefs| {
825 let got = addr
826 .clone()
827 .into_resolve_instructions(&Default::default(), &prefs);
828 let expected = Err((EDD::OnionAddressResolveRequest, EK::NotImplemented));
831 assert_eq!(map(got), expected, "{prefs:?}");
832 };
833
834 cfg_if::cfg_if! {
835 if #[cfg(feature = "onion-service-client")] {
836 use tor_config::BoolOrAuto as B;
837 let prefs_of = |yn| {
838 let mut prefs = StreamPrefs::default();
839 prefs.connect_to_onion_services(yn);
840 prefs
841 };
842 check_stream(prefs_def(), Ok(()));
843 check_stream(prefs_of(B::Auto), Ok(()));
844 check_stream(prefs_of(B::Explicit(true)), Ok(()));
845 check_stream(prefs_of(B::Explicit(false)), Err((EDD::OnionAddressDisabled, EK::ForbiddenStreamTarget)));
846
847 check_resolve(prefs_def());
848 check_resolve(prefs_of(B::Auto));
849 check_resolve(prefs_of(B::Explicit(true)));
850 check_resolve(prefs_of(B::Explicit(false)));
851 } else {
852 check_stream(prefs_def(), Err((EDD::OnionAddressNotSupported, EK::FeatureDisabled)));
853
854 check_resolve(prefs_def());
855 }
856 }
857 }
858
859 #[test]
860 fn convert_safe() {
861 fn check<A: IntoTorAddr>(a: A, s: &str) {
862 let a1 = TorAddr::from(a).unwrap();
863 let a2 = s.parse().unwrap();
864 assert_eq!(a1, a2);
865 assert_eq!(&a1.to_string(), s);
866 }
867
868 check(("www.example.com", 8000), "www.example.com:8000");
869 check(
870 TorAddr::from(("www.example.com", 8000)).unwrap(),
871 "www.example.com:8000",
872 );
873 check(
874 TorAddr::from(("www.example.com", 8000)).unwrap(),
875 "www.example.com:8000",
876 );
877 let addr = "[2001:db8::0042]:9001".to_owned();
878 check(&addr, "[2001:db8::42]:9001");
879 check(addr, "[2001:db8::42]:9001");
880 check(("2001:db8::0042".to_owned(), 9001), "[2001:db8::42]:9001");
881 check(("example.onion", 80), "example.onion:80");
882 }
883
884 #[test]
885 fn convert_dangerous() {
886 fn check<A: DangerouslyIntoTorAddr>(a: A, s: &str) {
887 let a1 = TorAddr::dangerously_from(a).unwrap();
888 let a2 = TorAddr::from(s).unwrap();
889 assert_eq!(a1, a2);
890 assert_eq!(&a1.to_string(), s);
891 }
892
893 let ip: IpAddr = "203.0.133.6".parse().unwrap();
894 let ip4: Ipv4Addr = "203.0.133.7".parse().unwrap();
895 let ip6: Ipv6Addr = "2001:db8::42".parse().unwrap();
896 let sa: SocketAddr = "203.0.133.8:80".parse().unwrap();
897 let sa4: SocketAddrV4 = "203.0.133.8:81".parse().unwrap();
898 let sa6: SocketAddrV6 = "[2001:db8::43]:82".parse().unwrap();
899
900 #[allow(clippy::needless_borrow)]
902 #[allow(clippy::needless_borrows_for_generic_args)]
903 check(&(ip, 443), "203.0.133.6:443");
904 check((ip, 443), "203.0.133.6:443");
905 check((ip4, 444), "203.0.133.7:444");
906 check((ip6, 445), "[2001:db8::42]:445");
907 check(sa, "203.0.133.8:80");
908 check(sa4, "203.0.133.8:81");
909 check(sa6, "[2001:db8::43]:82");
910 }
911}