arti_client/
address.rs

1//! Types and traits for converting objects to addresses which
2//! Tor can connect to.
3
4use crate::err::ErrorDetail;
5use crate::StreamPrefs;
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;
11
12#[cfg(feature = "onion-service-client")]
13use tor_hscrypto::pk::{HsId, HSID_ONION_SUFFIX};
14
15/// Fake plastic imitation of some of the `tor-hs*` functionality
16#[cfg(not(feature = "onion-service-client"))]
17pub(crate) mod hs_dummy {
18    use super::*;
19    use tor_error::internal;
20    use void::Void;
21
22    /// Parsed hidden service identity - uninhabited, since not supported
23    #[derive(Debug, Clone)]
24    pub(crate) struct HsId(pub(crate) Void);
25
26    impl PartialEq for HsId {
27        fn eq(&self, _other: &Self) -> bool {
28            void::unreachable(self.0)
29        }
30    }
31    impl Eq for HsId {}
32
33    /// Duplicates `tor-hscrypto::pk::HSID_ONION_SUFFIX`, ah well
34    pub(crate) const HSID_ONION_SUFFIX: &str = ".onion";
35
36    /// Must not be used other than for actual `.onion` addresses
37    impl FromStr for HsId {
38        type Err = ErrorDetail;
39
40        fn from_str(s: &str) -> Result<Self, Self::Err> {
41            if !s.ends_with(HSID_ONION_SUFFIX) {
42                return Err(internal!("non-.onion passed to dummy HsId::from_str").into());
43            }
44
45            Err(ErrorDetail::OnionAddressNotSupported)
46        }
47    }
48}
49#[cfg(not(feature = "onion-service-client"))]
50use hs_dummy::*;
51
52// ----------------------------------------------------------------------
53
54/// An object that can be converted to a [`TorAddr`] with a minimum of risk.
55///
56/// Typically, this trait will be implemented for a hostname or service name.
57///
58/// Don't implement this trait for IP addresses and similar types; instead,
59/// implement [`DangerouslyIntoTorAddr`] for those.  (The trouble with accepting
60/// IP addresses is that, in order to get an IP address, most programs will do a
61/// local hostname lookup, which will leak the target address to the DNS
62/// resolver. The `DangerouslyIntoTorAddr` trait provides a contract for careful
63/// programs to say, "I have gotten this IP address from somewhere safe."  This
64/// trait is for name-based addressing and similar, which _usually_ gets its
65/// addresses from a safer source.)
66///
67/// [*See also: the `TorAddr` documentation.*](TorAddr)
68///
69/// # Design note
70///
71/// We use a separate trait here, instead of using `Into<TorAddr>` or
72/// `TryInto<TorAddr>`, because `IntoTorAddr` implies additional guarantees
73/// relating to privacy risk.  The separate trait alerts users that something
74/// tricky is going on here, and encourages them to think twice before
75/// implementing `IntoTorAddr` for their own types.
76pub trait IntoTorAddr {
77    /// Try to make a [`TorAddr`] to represent connecting to this
78    /// address.
79    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError>;
80}
81
82/// An object that can be converted to a [`TorAddr`], but which it
83/// might be risky to get in the first place if you're hoping for
84/// anonymity.
85///
86/// For example, you can use this trait to convert a [`SocketAddr`]
87/// into a [`TorAddr`], and it's safe to do that conversion.  But
88/// where did you get the [`SocketAddr`] in the first place?  If it
89/// comes from a local DNS lookup, then you have leaked the address
90/// you were resolving to your DNS resolver, and probably your ISP.
91///
92/// [*See also: the `TorAddr` documentation.*](TorAddr)
93pub trait DangerouslyIntoTorAddr {
94    /// Try to make a [`TorAddr`] to represent connecting to `self`.
95    ///
96    /// By calling this function, the caller asserts that `self` was
97    /// obtained from some secure, private mechanism, and **not** from a local
98    /// DNS lookup or something similar.
99    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError>;
100}
101
102/// An address object that you can connect to over the Tor network.
103///
104/// When you're making a connection with Tor, you shouldn't do your DNS
105/// lookups locally: that would leak your target address to your DNS server.
106/// Instead, it's better to use a combination of a hostname and a port
107/// directly.
108///
109/// The preferred way to create a `TorAddr` is via the [`IntoTorAddr`] trait,
110/// using a hostname and a port (or a string containing a hostname and a
111/// port).  It's also okay to use an IP and Port there, but only if they come
112/// from some source _other than_ a local DNS lookup.
113///
114/// In order to discourage local hostname lookups, the functions that
115/// construct a [`TorAddr`] from [`IpAddr`], [`SocketAddr`], and so
116/// forth are labeled as "dangerous".
117///
118/// # Examples
119///
120/// Making a `TorAddr` from various "safe" sources:
121///
122/// ```rust
123/// # use anyhow::Result;
124/// # fn main() -> Result<()> {
125/// use arti_client::IntoTorAddr;
126///
127/// let example_from_tuple = ("example.com", 80).into_tor_addr()?;
128/// let example_from_string = "example.com:80".into_tor_addr()?;
129///
130/// assert_eq!(example_from_tuple, example_from_string);
131/// # Ok(())
132/// # }
133/// ```
134///
135/// Making a `TorAddr` from an IP address and port:
136///
137/// > **Warning:** This example is only safe because we're not doing a DNS lookup; rather, the
138/// > intent is to connect to a hardcoded IP address.
139/// > If you're using [`DangerouslyIntoTorAddr`], pay careful attention to where your IP addresses
140/// > are coming from, and whether there's a risk of information leakage.
141///
142/// ```rust
143/// # use anyhow::Result;
144/// # fn main() -> Result<()> {
145/// use arti_client::DangerouslyIntoTorAddr;
146/// use std::net::{IpAddr, SocketAddr};
147///
148/// let quad_one_dns: SocketAddr = "1.1.1.1:53".parse()?;
149/// let addr_from_socketaddr = quad_one_dns.into_tor_addr_dangerously()?;
150///
151/// let quad_one_ip: IpAddr = "1.1.1.1".parse()?;
152/// let addr_from_tuple = (quad_one_ip, 53).into_tor_addr_dangerously()?;
153///
154/// assert_eq!(addr_from_socketaddr, addr_from_tuple);
155/// # Ok(())
156/// # }
157/// ```
158#[derive(Debug, Clone, Eq, PartialEq)]
159pub struct TorAddr {
160    /// The target host.
161    host: Host,
162    /// The target port number.
163    port: u16,
164}
165
166/// How to make a stream to this `TorAddr`?
167///
168/// This is a separate type, returned from `address.rs` to `client.rs`,
169/// so that we can test our "how to make a connection" logic and policy,
170/// in isolation, without a whole Tor client.
171#[derive(Debug, PartialEq, Eq)]
172pub(crate) enum StreamInstructions {
173    /// Create an exit circuit suitable for port, and then make a stream to `hostname`
174    Exit {
175        /// Hostname
176        hostname: String,
177        /// Port
178        port: u16,
179    },
180    /// Create a hidden service connection to hsid, and then make a stream to `hostname`
181    ///
182    /// `HsId`, and therefore this variant, is uninhabited, unless the feature is enabled
183    Hs {
184        /// The target hidden service
185        hsid: HsId,
186        /// The hostname (used for subdomains, sent to the peer)
187        hostname: String,
188        /// Port
189        port: u16,
190    },
191}
192
193/// How to resolve this Tor host address into IP address(es)
194#[derive(PartialEq, Eq, Debug)]
195pub(crate) enum ResolveInstructions {
196    /// Create an exit circuit without port restrictions, and ask the exit
197    Exit(String),
198    /// Simply return this
199    Return(Vec<IpAddr>),
200}
201
202impl TorAddr {
203    /// Construct a TorAddr from its constituent parts, rejecting it if the
204    /// port is zero.
205    fn new(host: Host, port: u16) -> Result<Self, TorAddrError> {
206        if port == 0 {
207            Err(TorAddrError::BadPort)
208        } else {
209            Ok(TorAddr { host, port })
210        }
211    }
212
213    /// Construct a `TorAddr` from any object that implements
214    /// [`IntoTorAddr`].
215    pub fn from<A: IntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
216        addr.into_tor_addr()
217    }
218    /// Construct a `TorAddr` from any object that implements
219    /// [`DangerouslyIntoTorAddr`].
220    ///
221    /// See [`DangerouslyIntoTorAddr`] for an explanation of why the
222    /// style of programming supported by this function is dangerous
223    /// to use.
224    pub fn dangerously_from<A: DangerouslyIntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
225        addr.into_tor_addr_dangerously()
226    }
227
228    /// Return true if this is an IP address (rather than a hostname).
229    pub fn is_ip_address(&self) -> bool {
230        matches!(&self.host, Host::Ip(_))
231    }
232
233    /// Get instructions for how to make a stream to this address
234    pub(crate) fn into_stream_instructions(
235        self,
236        cfg: &crate::config::ClientAddrConfig,
237        prefs: &StreamPrefs,
238    ) -> Result<StreamInstructions, ErrorDetail> {
239        self.enforce_config(cfg, prefs)?;
240
241        let port = self.port;
242        Ok(match self.host {
243            Host::Hostname(hostname) => StreamInstructions::Exit { hostname, port },
244            Host::Ip(ip) => StreamInstructions::Exit {
245                hostname: ip.to_string(),
246                port,
247            },
248            Host::Onion(onion) => {
249                // The HS is identified by the last two domain name components
250                let rhs = onion
251                    .rmatch_indices('.')
252                    .nth(1)
253                    .map(|(i, _)| i + 1)
254                    .unwrap_or(0);
255                let rhs = &onion[rhs..];
256                let hsid = rhs.parse()?;
257                StreamInstructions::Hs {
258                    hsid,
259                    port,
260                    hostname: onion,
261                }
262            }
263        })
264    }
265
266    /// Get instructions for how to make a stream to this address
267    pub(crate) fn into_resolve_instructions(
268        self,
269        cfg: &crate::config::ClientAddrConfig,
270        prefs: &StreamPrefs,
271    ) -> Result<ResolveInstructions, ErrorDetail> {
272        // We defer enforcing the config until we see if this is a .onion,
273        // in which case it's always doomed and we want to return *our* error,
274        // not any problem with the configuration or preferences.
275        // But we must *calculate* the error now because instructions consumes self.
276        let enforce_config_result = self.enforce_config(cfg, prefs);
277
278        // This IEFE is so that any use of `return` doesn't bypass
279        // checking the enforce_config result
280        let instructions = (move || {
281            Ok(match self.host {
282                Host::Hostname(hostname) => ResolveInstructions::Exit(hostname),
283                Host::Ip(ip) => ResolveInstructions::Return(vec![ip]),
284                Host::Onion(_) => return Err(ErrorDetail::OnionAddressResolveRequest),
285            })
286        })()?;
287
288        let () = enforce_config_result?;
289
290        Ok(instructions)
291    }
292
293    /// Return true if the `host` in this address is local.
294    fn is_local(&self) -> bool {
295        self.host.is_local()
296    }
297
298    /// Give an error if this address doesn't conform to the rules set in
299    /// `cfg`.
300    fn enforce_config(
301        &self,
302        cfg: &crate::config::ClientAddrConfig,
303        #[allow(unused_variables)] // will only be used in certain configurations
304        prefs: &StreamPrefs,
305    ) -> Result<(), ErrorDetail> {
306        if !cfg.allow_local_addrs && self.is_local() {
307            return Err(ErrorDetail::LocalAddress);
308        }
309
310        if let Host::Hostname(addr) = &self.host {
311            if !is_valid_hostname(addr) {
312                // This ought not to occur, because it violates Host's invariant
313                return Err(ErrorDetail::InvalidHostname);
314            }
315            if addr.ends_with_ignore_ascii_case(HSID_ONION_SUFFIX) {
316                // This ought not to occur, because it violates Host's invariant
317                return Err(ErrorDetail::OnionAddressNotSupported);
318            }
319        }
320
321        if let Host::Onion(_name) = &self.host {
322            cfg_if::cfg_if! {
323                if #[cfg(feature = "onion-service-client")] {
324                    if !prefs.connect_to_onion_services.as_bool().unwrap_or(cfg.allow_onion_addrs) {
325                        return Err(ErrorDetail::OnionAddressDisabled);
326                    }
327                } else {
328                    return Err(ErrorDetail::OnionAddressNotSupported);
329                }
330            }
331        }
332
333        Ok(())
334    }
335}
336
337impl std::fmt::Display for TorAddr {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        match self.host {
340            Host::Ip(IpAddr::V6(addr)) => write!(f, "[{}]:{}", addr, self.port),
341            _ => write!(f, "{}:{}", self.host, self.port),
342        }
343    }
344}
345
346/// An error created while making or using a [`TorAddr`].
347//
348// NOTE: Unlike ErrorDetail, this is a `pub` enum: Do not make breaking changes
349// to it, or expose lower-level errors in it, without careful consideration!
350#[derive(Debug, Error, Clone, Eq, PartialEq)]
351#[non_exhaustive]
352// TODO Should implement ErrorKind
353pub enum TorAddrError {
354    /// Tried to parse a string that can never be interpreted as a valid host.
355    #[error("String can never be a valid hostname")]
356    InvalidHostname,
357    /// Tried to parse a string as an `address:port`, but it had no port.
358    #[error("No port found in string")]
359    NoPort,
360    /// Tried to parse a port that wasn't a valid nonzero `u16`.
361    #[error("Could not parse port")]
362    BadPort,
363}
364
365/// A host that Tor can connect to: either a hostname or an IP address.
366//
367// We use `String` in here, and pass that directly to (for example)
368// `HsId::from_str`, or `begin_stream`.
369// In theory we could use a couple of newtypes or something, but
370//  * The stringly-typed `HsId::from_str` call (on a string known to end `.onion`)
371//    appears precisely in `into_stream_instructions` which knows what it's doing;
372//  * The stringly-typed .onion domain name must be passed in the
373//    StreamInstructions so that we can send it to the HS for its vhosting.
374#[derive(Clone, Debug, Eq, PartialEq)]
375enum Host {
376    /// A hostname.
377    ///
378    /// This variant should never be used if the `Ip`
379    /// variant could be used instead.
380    /// Ie, it must not be a stringified IP address.
381    ///
382    /// Likewise, this variant must *not* be used for a `.onion` address.
383    /// Even if we have `.onion` support compiled out, we use the `Onion` variant for that.
384    ///
385    /// But, this variant might *not* be on the public internet.
386    /// For example, it might be `localhost`.
387    Hostname(String),
388    /// An IP address.
389    Ip(IpAddr),
390    /// The address of a hidden service (`.onion` service).
391    ///
392    /// We haven't validated that the base32 makes any kind of sense, yet.
393    /// We do that when we try to connect.
394    Onion(String),
395}
396
397impl FromStr for Host {
398    type Err = TorAddrError;
399    fn from_str(s: &str) -> Result<Host, TorAddrError> {
400        if s.ends_with_ignore_ascii_case(".onion") && is_valid_hostname(s) {
401            Ok(Host::Onion(s.to_owned()))
402        } else if let Ok(ip_addr) = s.parse() {
403            Ok(Host::Ip(ip_addr))
404        } else if is_valid_hostname(s) {
405            // TODO(nickm): we might someday want to reject some kinds of bad
406            // hostnames here, rather than when we're about to connect to them.
407            // But that would be an API break, and maybe not what people want.
408            // Maybe instead we should have a method to check whether a hostname
409            // is "bad"? Not sure; we'll need to decide the right behavior here.
410            Ok(Host::Hostname(s.to_owned()))
411        } else {
412            Err(TorAddrError::InvalidHostname)
413        }
414    }
415}
416
417impl Host {
418    /// Return true if this address is one that is "internal": that is,
419    /// relative to the particular host that is resolving it.
420    fn is_local(&self) -> bool {
421        match self {
422            Host::Hostname(name) => name.eq_ignore_ascii_case("localhost"),
423            // TODO: use is_global once it's stable, perhaps.
424            // NOTE: Contrast this with is_sufficiently_private in tor-hsproxy,
425            // which has a different purpose. Also see #1159.
426            // The purpose of _this_ test is to find addresses that cannot
427            // meaningfully be connected to over Tor, and that the exit
428            // will not accept.
429            Host::Ip(IpAddr::V4(ip)) => ip.is_loopback() || ip.is_private(),
430            Host::Ip(IpAddr::V6(ip)) => ip.is_loopback(),
431            Host::Onion(_) => false,
432        }
433    }
434}
435
436impl std::fmt::Display for Host {
437    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        match self {
439            Host::Hostname(s) => Display::fmt(s, f),
440            Host::Ip(ip) => Display::fmt(ip, f),
441            Host::Onion(onion) => Display::fmt(onion, f),
442        }
443    }
444}
445
446impl IntoTorAddr for TorAddr {
447    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
448        Ok(self)
449    }
450}
451
452impl<A: IntoTorAddr + Clone> IntoTorAddr for &A {
453    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
454        self.clone().into_tor_addr()
455    }
456}
457
458impl IntoTorAddr for &str {
459    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
460        if let Ok(sa) = SocketAddr::from_str(self) {
461            TorAddr::new(Host::Ip(sa.ip()), sa.port())
462        } else {
463            let (host, port) = self.rsplit_once(':').ok_or(TorAddrError::NoPort)?;
464            let host = host.parse()?;
465            let port = port.parse().map_err(|_| TorAddrError::BadPort)?;
466            TorAddr::new(host, port)
467        }
468    }
469}
470
471impl IntoTorAddr for String {
472    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
473        self[..].into_tor_addr()
474    }
475}
476
477impl FromStr for TorAddr {
478    type Err = TorAddrError;
479    fn from_str(s: &str) -> Result<Self, TorAddrError> {
480        s.into_tor_addr()
481    }
482}
483
484impl IntoTorAddr for (&str, u16) {
485    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
486        let (host, port) = self;
487        let host = host.parse()?;
488        TorAddr::new(host, port)
489    }
490}
491
492impl IntoTorAddr for (String, u16) {
493    fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
494        let (host, port) = self;
495        (&host[..], port).into_tor_addr()
496    }
497}
498
499impl<T: DangerouslyIntoTorAddr + Clone> DangerouslyIntoTorAddr for &T {
500    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
501        self.clone().into_tor_addr_dangerously()
502    }
503}
504
505impl DangerouslyIntoTorAddr for (IpAddr, u16) {
506    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
507        let (addr, port) = self;
508        TorAddr::new(Host::Ip(addr), port)
509    }
510}
511
512impl DangerouslyIntoTorAddr for (Ipv4Addr, u16) {
513    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
514        let (addr, port) = self;
515        TorAddr::new(Host::Ip(addr.into()), port)
516    }
517}
518
519impl DangerouslyIntoTorAddr for (Ipv6Addr, u16) {
520    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
521        let (addr, port) = self;
522        TorAddr::new(Host::Ip(addr.into()), port)
523    }
524}
525
526impl DangerouslyIntoTorAddr for SocketAddr {
527    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
528        let (addr, port) = (self.ip(), self.port());
529        (addr, port).into_tor_addr_dangerously()
530    }
531}
532
533impl DangerouslyIntoTorAddr for SocketAddrV4 {
534    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
535        let (addr, port) = (self.ip(), self.port());
536        (*addr, port).into_tor_addr_dangerously()
537    }
538}
539
540impl DangerouslyIntoTorAddr for SocketAddrV6 {
541    fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
542        let (addr, port) = (self.ip(), self.port());
543        (*addr, port).into_tor_addr_dangerously()
544    }
545}
546
547/// Check whether `hostname` is a valid hostname or not.
548///
549/// (Note that IPv6 addresses don't follow these rules.)
550fn is_valid_hostname(hostname: &str) -> bool {
551    hostname_validator::is_valid(hostname)
552}
553
554#[cfg(test)]
555mod test {
556    // @@ begin test lint list maintained by maint/add_warning @@
557    #![allow(clippy::bool_assert_comparison)]
558    #![allow(clippy::clone_on_copy)]
559    #![allow(clippy::dbg_macro)]
560    #![allow(clippy::mixed_attributes_style)]
561    #![allow(clippy::print_stderr)]
562    #![allow(clippy::print_stdout)]
563    #![allow(clippy::single_char_pattern)]
564    #![allow(clippy::unwrap_used)]
565    #![allow(clippy::unchecked_duration_subtraction)]
566    #![allow(clippy::useless_vec)]
567    #![allow(clippy::needless_pass_by_value)]
568    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
569    use super::*;
570
571    /// Make a `StreamPrefs` with `.onion` enabled, if cfg-enabled
572    fn mk_stream_prefs() -> StreamPrefs {
573        let prefs = crate::StreamPrefs::default();
574
575        #[cfg(feature = "onion-service-client")]
576        let prefs = {
577            let mut prefs = prefs;
578            prefs.connect_to_onion_services(tor_config::BoolOrAuto::Explicit(true));
579            prefs
580        };
581
582        prefs
583    }
584
585    #[test]
586    fn validate_hostname() {
587        // Valid hostname tests
588        assert!(is_valid_hostname("torproject.org"));
589        assert!(is_valid_hostname("Tor-Project.org"));
590        assert!(is_valid_hostname("example.onion"));
591        assert!(is_valid_hostname("some.example.onion"));
592
593        // Invalid hostname tests
594        assert!(!is_valid_hostname("-torproject.org"));
595        assert!(!is_valid_hostname("_torproject.org"));
596        assert!(!is_valid_hostname("tor_project1.org"));
597        assert!(!is_valid_hostname("iwanna$money.org"));
598    }
599
600    #[test]
601    fn validate_addr() {
602        use crate::err::ErrorDetail;
603        fn val<A: IntoTorAddr>(addr: A) -> Result<TorAddr, ErrorDetail> {
604            let toraddr = addr.into_tor_addr()?;
605            toraddr.enforce_config(&Default::default(), &mk_stream_prefs())?;
606            Ok(toraddr)
607        }
608
609        assert!(val("[2001:db8::42]:20").is_ok());
610        assert!(val(("2001:db8::42", 20)).is_ok());
611        assert!(val(("198.151.100.42", 443)).is_ok());
612        assert!(val("198.151.100.42:443").is_ok());
613        assert!(val("www.torproject.org:443").is_ok());
614        assert!(val(("www.torproject.org", 443)).is_ok());
615
616        // When HS disabled, tested elsewhere, see: stream_instructions, prefs_onion_services
617        #[cfg(feature = "onion-service-client")]
618        {
619            assert!(val("example.onion:80").is_ok());
620            assert!(val(("example.onion", 80)).is_ok());
621
622            match val("eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443") {
623                Ok(TorAddr {
624                    host: Host::Onion(_),
625                    ..
626                }) => {}
627                x => panic!("{x:?}"),
628            }
629        }
630
631        assert!(matches!(
632            val("-foobar.net:443"),
633            Err(ErrorDetail::InvalidHostname)
634        ));
635        assert!(matches!(
636            val("www.torproject.org"),
637            Err(ErrorDetail::Address(TorAddrError::NoPort))
638        ));
639
640        assert!(matches!(
641            val("192.168.0.1:80"),
642            Err(ErrorDetail::LocalAddress)
643        ));
644        assert!(matches!(
645            val(TorAddr::new(Host::Hostname("foo@bar".to_owned()), 553).unwrap()),
646            Err(ErrorDetail::InvalidHostname)
647        ));
648        assert!(matches!(
649            val(TorAddr::new(Host::Hostname("foo.onion".to_owned()), 80).unwrap()),
650            Err(ErrorDetail::OnionAddressNotSupported)
651        ));
652    }
653
654    #[test]
655    fn local_addrs() {
656        fn is_local_hostname(s: &str) -> bool {
657            let h: Host = s.parse().unwrap();
658            h.is_local()
659        }
660
661        assert!(is_local_hostname("localhost"));
662        assert!(is_local_hostname("loCALHOST"));
663        assert!(is_local_hostname("127.0.0.1"));
664        assert!(is_local_hostname("::1"));
665        assert!(is_local_hostname("192.168.0.1"));
666
667        assert!(!is_local_hostname("www.example.com"));
668    }
669
670    #[test]
671    fn is_ip_address() {
672        fn ip(s: &str) -> bool {
673            TorAddr::from(s).unwrap().is_ip_address()
674        }
675
676        assert!(ip("192.168.0.1:80"));
677        assert!(ip("[::1]:80"));
678        assert!(ip("[2001:db8::42]:65535"));
679        assert!(!ip("example.com:80"));
680        assert!(!ip("example.onion:80"));
681    }
682
683    #[test]
684    fn stream_instructions() {
685        use StreamInstructions as SI;
686
687        fn sap(s: &str) -> Result<StreamInstructions, ErrorDetail> {
688            TorAddr::from(s)
689                .unwrap()
690                .into_stream_instructions(&Default::default(), &mk_stream_prefs())
691        }
692
693        assert_eq!(
694            sap("[2001:db8::42]:9001").unwrap(),
695            SI::Exit {
696                hostname: "2001:db8::42".to_owned(),
697                port: 9001
698            },
699        );
700        assert_eq!(
701            sap("example.com:80").unwrap(),
702            SI::Exit {
703                hostname: "example.com".to_owned(),
704                port: 80
705            },
706        );
707
708        {
709            let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad";
710            let onion = format!("sss1234.www.{}.onion", b32);
711            let got = sap(&format!("{}:443", onion));
712
713            #[cfg(feature = "onion-service-client")]
714            assert_eq!(
715                got.unwrap(),
716                SI::Hs {
717                    hsid: format!("{}.onion", b32).parse().unwrap(),
718                    hostname: onion,
719                    port: 443,
720                }
721            );
722
723            #[cfg(not(feature = "onion-service-client"))]
724            assert!(matches!(got, Err(ErrorDetail::OnionAddressNotSupported)));
725        }
726    }
727
728    #[test]
729    fn resolve_instructions() {
730        use ResolveInstructions as RI;
731
732        fn sap(s: &str) -> Result<ResolveInstructions, ErrorDetail> {
733            TorAddr::from(s)
734                .unwrap()
735                .into_resolve_instructions(&Default::default(), &Default::default())
736        }
737
738        assert_eq!(
739            sap("[2001:db8::42]:9001").unwrap(),
740            RI::Return(vec!["2001:db8::42".parse().unwrap()]),
741        );
742        assert_eq!(
743            sap("example.com:80").unwrap(),
744            RI::Exit("example.com".to_owned()),
745        );
746        assert!(matches!(
747            sap("example.onion:80"),
748            Err(ErrorDetail::OnionAddressResolveRequest),
749        ));
750    }
751
752    #[test]
753    fn bad_ports() {
754        assert_eq!(
755            TorAddr::from("www.example.com:squirrel"),
756            Err(TorAddrError::BadPort)
757        );
758        assert_eq!(
759            TorAddr::from("www.example.com:0"),
760            Err(TorAddrError::BadPort)
761        );
762    }
763
764    #[test]
765    fn prefs_onion_services() {
766        use crate::err::ErrorDetailDiscriminants;
767        use tor_error::{ErrorKind, HasKind as _};
768        use ErrorDetailDiscriminants as EDD;
769        use ErrorKind as EK;
770
771        #[allow(clippy::redundant_closure)] // for symmetry with prefs_of, below, and clarity
772        let prefs_def = || StreamPrefs::default();
773
774        let addr: TorAddr = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443"
775            .parse()
776            .unwrap();
777
778        fn map(
779            got: Result<impl Sized, ErrorDetail>,
780        ) -> Result<(), (ErrorDetailDiscriminants, ErrorKind)> {
781            got.map(|_| ())
782                .map_err(|e| (ErrorDetailDiscriminants::from(&e), e.kind()))
783        }
784
785        let check_stream = |prefs, expected| {
786            let got = addr
787                .clone()
788                .into_stream_instructions(&Default::default(), &prefs);
789            assert_eq!(map(got), expected, "{prefs:?}");
790        };
791        let check_resolve = |prefs| {
792            let got = addr
793                .clone()
794                .into_resolve_instructions(&Default::default(), &prefs);
795            // This should be OnionAddressResolveRequest no matter if .onion is compiled in or enabled.
796            // Since compiling it in, or enabling it, won't help.
797            let expected = Err((EDD::OnionAddressResolveRequest, EK::NotImplemented));
798            assert_eq!(map(got), expected, "{prefs:?}");
799        };
800
801        cfg_if::cfg_if! {
802            if #[cfg(feature = "onion-service-client")] {
803                use tor_config::BoolOrAuto as B;
804                let prefs_of = |yn| {
805                    let mut prefs = StreamPrefs::default();
806                    prefs.connect_to_onion_services(yn);
807                    prefs
808                };
809                check_stream(prefs_def(), Ok(()));
810                check_stream(prefs_of(B::Auto), Ok(()));
811                check_stream(prefs_of(B::Explicit(true)), Ok(()));
812                check_stream(prefs_of(B::Explicit(false)), Err((EDD::OnionAddressDisabled, EK::ForbiddenStreamTarget)));
813
814                check_resolve(prefs_def());
815                check_resolve(prefs_of(B::Auto));
816                check_resolve(prefs_of(B::Explicit(true)));
817                check_resolve(prefs_of(B::Explicit(false)));
818            } else {
819                check_stream(prefs_def(), Err((EDD::OnionAddressNotSupported, EK::FeatureDisabled)));
820
821                check_resolve(prefs_def());
822            }
823        }
824    }
825
826    #[test]
827    fn convert_safe() {
828        fn check<A: IntoTorAddr>(a: A, s: &str) {
829            let a1 = TorAddr::from(a).unwrap();
830            let a2 = s.parse().unwrap();
831            assert_eq!(a1, a2);
832            assert_eq!(&a1.to_string(), s);
833        }
834
835        check(("www.example.com", 8000), "www.example.com:8000");
836        check(
837            TorAddr::from(("www.example.com", 8000)).unwrap(),
838            "www.example.com:8000",
839        );
840        check(
841            TorAddr::from(("www.example.com", 8000)).unwrap(),
842            "www.example.com:8000",
843        );
844        let addr = "[2001:db8::0042]:9001".to_owned();
845        check(&addr, "[2001:db8::42]:9001");
846        check(addr, "[2001:db8::42]:9001");
847        check(("2001:db8::0042".to_owned(), 9001), "[2001:db8::42]:9001");
848        check(("example.onion", 80), "example.onion:80");
849    }
850
851    #[test]
852    fn convert_dangerous() {
853        fn check<A: DangerouslyIntoTorAddr>(a: A, s: &str) {
854            let a1 = TorAddr::dangerously_from(a).unwrap();
855            let a2 = TorAddr::from(s).unwrap();
856            assert_eq!(a1, a2);
857            assert_eq!(&a1.to_string(), s);
858        }
859
860        let ip: IpAddr = "203.0.133.6".parse().unwrap();
861        let ip4: Ipv4Addr = "203.0.133.7".parse().unwrap();
862        let ip6: Ipv6Addr = "2001:db8::42".parse().unwrap();
863        let sa: SocketAddr = "203.0.133.8:80".parse().unwrap();
864        let sa4: SocketAddrV4 = "203.0.133.8:81".parse().unwrap();
865        let sa6: SocketAddrV6 = "[2001:db8::43]:82".parse().unwrap();
866
867        // This tests impl DangerouslyIntoTorAddr for &T
868        #[allow(clippy::needless_borrow)]
869        #[allow(clippy::needless_borrows_for_generic_args)]
870        check(&(ip, 443), "203.0.133.6:443");
871        check((ip, 443), "203.0.133.6:443");
872        check((ip4, 444), "203.0.133.7:444");
873        check((ip6, 445), "[2001:db8::42]:445");
874        check(sa, "203.0.133.8:80");
875        check(sa4, "203.0.133.8:81");
876        check(sa6, "[2001:db8::43]:82");
877    }
878}