//! Types and traits for converting objects to addresses which //! Tor can connect to. use crate::err::ErrorDetail; use std::fmt::Display; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::str::FromStr; use thiserror::Error; use tor_basic_utils::StrExt; #[cfg(feature = "onion-client")] use tor_hscrypto::pk::{HsId, HSID_ONION_SUFFIX}; /// Fake plastic imitation of some of the `tor-hs*` functionality #[cfg(not(feature = "onion-client"))] pub(crate) mod hs_dummy { use super::*; use tor_error::internal; use void::Void; /// Parsed hidden service identity - uninhabited, since not supported #[derive(Debug, Clone)] pub(crate) struct HsId(pub(crate) Void); impl PartialEq for HsId { fn eq(&self, _other: &Self) -> bool { void::unreachable(self.0) } } impl Eq for HsId {} /// Duplicates `tor-hscrypto::pk::HSID_ONION_SUFFIX`, ah well pub(crate) const HSID_ONION_SUFFIX: &str = ".onion"; /// Must not be used other than for actual `.onion` addresses impl FromStr for HsId { type Err = ErrorDetail; fn from_str(s: &str) -> Result { if !s.ends_with(HSID_ONION_SUFFIX) { return Err(internal!("non-.onion passed to dummy HsId::from_str").into()); } Err(ErrorDetail::OnionAddressNotSupported) } } } #[cfg(not(feature = "onion-client"))] use hs_dummy::*; // ---------------------------------------------------------------------- /// An object that can be converted to a [`TorAddr`] with a minimum of risk. /// /// Typically, this trait will be implemented for a hostname or service name. /// /// Don't implement this trait for IP addresses and similar types; instead, /// implement [`DangerouslyIntoTorAddr`] for those. (The trouble with accepting /// IP addresses is that, in order to get an IP address, most programs will do a /// local hostname lookup, which will leak the target address to the DNS /// resolver. The `DangerouslyIntoTorAddr` trait provides a contract for careful /// programs to say, "I have gotten this IP address from somewhere safe." This /// trait is for name-based addressing and similar, which _usually_ gets its /// addresses from a safer source.) /// /// [*See also: the `TorAddr` documentation.*](TorAddr) /// /// # Design note /// /// We use a separate trait here, instead of using `Into` or /// `TryInto`, because `IntoTorAddr` implies additional guarantees /// relating to privacy risk. The separate trait alerts users that something /// tricky is going on here, and encourages them to think twice before /// implementing `IntoTorAddr` for their own types. pub trait IntoTorAddr { /// Try to make a [`TorAddr`] to represent connecting to this /// address. fn into_tor_addr(self) -> Result; } /// An object that can be converted to a [`TorAddr`], but which it /// might be risky to get in the first place if you're hoping for /// anonymity. /// /// For example, you can use this trait to convert a [`SocketAddr`] /// into a [`TorAddr`], and it's safe to do that conversion. But /// where did you get the [`SocketAddr`] in the first place? If it /// comes from a local DNS lookup, then you have leaked the address /// you were resolving to your DNS resolver, and probably your ISP. /// /// [*See also: the `TorAddr` documentation.*](TorAddr) pub trait DangerouslyIntoTorAddr { /// Try to make a [`TorAddr`] to represent connecting to `self`. /// /// By calling this function, the caller asserts that `self` was /// obtained from some secure, private mechanism, and **not** from a local /// DNS lookup or something similar. fn into_tor_addr_dangerously(self) -> Result; } /// An address object that you can connect to over the Tor network. /// /// When you're making a connection with Tor, you shouldn't do your DNS /// lookups locally: that would leak your target address to your DNS server. /// Instead, it's better to use a combination of a hostname and a port /// directly. /// /// The preferred way to create a `TorAddr` is via the [`IntoTorAddr`] trait, /// using a hostname and a port (or a string containing a hostname and a /// port). It's also okay to use an IP and Port there, but only if they come /// from some source _other than_ a local DNS lookup. /// /// In order to discourage local hostname lookups, the functions that /// construct a [`TorAddr`] from [`IpAddr`], [`SocketAddr`], and so /// forth are labeled as "dangerous". /// /// # Examples /// /// Making a `TorAddr` from various "safe" sources: /// /// ```rust /// # use anyhow::Result; /// # fn main() -> Result<()> { /// use arti_client::IntoTorAddr; /// /// let example_from_tuple = ("example.com", 80).into_tor_addr()?; /// let example_from_string = "example.com:80".into_tor_addr()?; /// /// assert_eq!(example_from_tuple, example_from_string); /// # Ok(()) /// # } /// ``` /// /// Making a `TorAddr` from an IP address and port: /// /// > **Warning:** This example is only safe because we're not doing a DNS lookup; rather, the /// > intent is to connect to a hardcoded IP address. /// > If you're using [`DangerouslyIntoTorAddr`], pay careful attention to where your IP addresses /// > are coming from, and whether there's a risk of information leakage. /// /// ```rust /// # use anyhow::Result; /// # fn main() -> Result<()> { /// use arti_client::DangerouslyIntoTorAddr; /// use std::net::{IpAddr, SocketAddr}; /// /// let quad_one_dns: SocketAddr = "1.1.1.1:53".parse()?; /// let addr_from_socketaddr = quad_one_dns.into_tor_addr_dangerously()?; /// /// let quad_one_ip: IpAddr = "1.1.1.1".parse()?; /// let addr_from_tuple = (quad_one_ip, 53).into_tor_addr_dangerously()?; /// /// assert_eq!(addr_from_socketaddr, addr_from_tuple); /// # Ok(()) /// # } /// ``` #[derive(Debug, Clone, Eq, PartialEq)] pub struct TorAddr { /// The target host. host: Host, /// The target port number. port: u16, } /// How to make a stream to this `TorAddr`? /// /// This is a separate type, returned from `address.rs` to `client.rs`, /// so that we can test our "how to make a connection" logic and policy, /// in isolation, without a whole Tor client. #[derive(Debug, PartialEq, Eq)] pub(crate) enum StreamInstructions { /// Create an exit circuit suitable for port, and then make a stream to `hostname` Exit { /// Hostname hostname: String, /// Port port: u16, }, /// Create a hidden service connection to hsid, and then make a stream to `hostname` /// /// `HsId`, and therefore this variant, is uninhabited, unless the feature is enabled Hs { /// The target hidden service hsid: HsId, /// The hostname (used for subdomains, sent to the peer) hostname: String, /// Port port: u16, }, } /// How to resolve this Tor host address into IP address(es) #[derive(PartialEq, Eq, Debug)] pub(crate) enum ResolveInstructions { /// Create an exit circuit without port restrictions, and ask the exit Exit(String), /// Simply return this Return(Vec), } impl TorAddr { /// Construct a TorAddr from its constituent parts, rejecting it if the /// port is zero. fn new(host: Host, port: u16) -> Result { if port == 0 { Err(TorAddrError::BadPort) } else { Ok(TorAddr { host, port }) } } /// Construct a `TorAddr` from any object that implements /// [`IntoTorAddr`]. pub fn from(addr: A) -> Result { addr.into_tor_addr() } /// Construct a `TorAddr` from any object that implements /// [`DangerouslyIntoTorAddr`]. /// /// See [`DangerouslyIntoTorAddr`] for an explanation of why the /// style of programming supported by this function is dangerous /// to use. pub fn dangerously_from(addr: A) -> Result { addr.into_tor_addr_dangerously() } /// Return true if this is an IP address (rather than a hostname). pub fn is_ip_address(&self) -> bool { matches!(&self.host, Host::Ip(_)) } /// Get instructions for how to make a stream to this address pub(crate) fn into_stream_instructions(self) -> Result { // TODO enforcement of the config should go here, not separately let port = self.port; Ok(match self.host { Host::Hostname(hostname) => StreamInstructions::Exit { hostname, port }, Host::Ip(ip) => StreamInstructions::Exit { hostname: ip.to_string(), port, }, Host::Onion(onion) => { // The HS is identified by the last two domain name components let rhs = onion .rmatch_indices('.') .nth(1) .map(|(i, _)| i + 1) .unwrap_or(0); let rhs = &onion[rhs..]; let hsid = rhs.parse()?; StreamInstructions::Hs { hsid, port, hostname: onion, } } }) } /// Get instructions for how to make a stream to this address pub(crate) fn into_resolve_instructions(self) -> Result { // TODO enforcement of the config should go here, not separately Ok(match self.host { Host::Hostname(hostname) => ResolveInstructions::Exit(hostname), Host::Ip(ip) => ResolveInstructions::Return(vec![ip]), Host::Onion(_) => return Err(ErrorDetail::OnionAddressResolveRequest), }) } /// Return true if the `host` in this address is local. fn is_local(&self) -> bool { self.host.is_local() } /// Give an error if this address doesn't conform to the rules set in /// `cfg`. pub(crate) fn enforce_config( &self, cfg: &crate::config::ClientAddrConfig, ) -> Result<(), ErrorDetail> { if !cfg.allow_local_addrs && self.is_local() { return Err(ErrorDetail::LocalAddress); } if let Host::Hostname(addr) = &self.host { if !is_valid_hostname(addr) { // This ought not to occur, because it violates Host's invariant return Err(ErrorDetail::InvalidHostname); } if addr.ends_with_ignore_ascii_case(HSID_ONION_SUFFIX) { // This ought not to occur, because it violates Host's invariant return Err(ErrorDetail::OnionAddressNotSupported); } } Ok(()) } } impl std::fmt::Display for TorAddr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.host { Host::Ip(IpAddr::V6(addr)) => write!(f, "[{}]:{}", addr, self.port), _ => write!(f, "{}:{}", self.host, self.port), } } } /// An error created while making or using a [`TorAddr`]. #[derive(Debug, Error, Clone, Eq, PartialEq)] #[non_exhaustive] // TODO Should implement ErrorKind pub enum TorAddrError { /// Tried to parse a string that can never be interpreted as a valid host. #[error("String can never be a valid hostname")] InvalidHostname, /// Tried to parse a string as an `address:port`, but it had no port. #[error("No port found in string")] NoPort, /// Tried to parse a port that wasn't a valid nonzero `u16`. #[error("Could not parse port")] BadPort, /// Tried to parse a `.onion` domain that could not be decoded #[cfg(feature = "onion-client")] #[error("Invalid or corrupted `.onion` address")] // TODO HS should we include the original HsIdParseError? // However, this struct wants to be Eq and HsIdParseError contains a tor_bytes::Error // which contains various other stuff that's not Eq. We need a policy about whether // errors are always Eq (or maybe PartialEq). // TODO HS the ErrorKind should delegate to HsIdParseError's BadOnion, } /// A host that Tor can connect to: either a hostname or an IP address. // // We use `String` in here, and pass that directly to (for example) // `HsId::from_str`, or `begin_stream`. // In theory we could use a couple of newtypes or something, but // * The stringly-typed `HsId::from_str` call (on a string known to end `.onion`) // appears precisely in `into_stream_instructions` which knows what it's doing; // * The stringly-typed .onion domain name must be passed in the // StreamInstructions so that we can send it to the HS for its vhosting. #[derive(Clone, Debug, Eq, PartialEq)] enum Host { /// A hostname. /// /// This variant should never be used if the `Ip` /// variant could be used instead. /// Ie, it must not be a stringified IP address. /// /// Likewise, this variant must *not* be used for a `.onion` address. /// Even if we have `.onion` support compiled out, we use the `Onion` variant for that. /// /// But, this variant might *not* be on the public internet. /// For example, it might be `localhost`. Hostname(String), /// An IP address. Ip(IpAddr), /// The address of a hidden service (`.onion` service). /// /// We haven't validated that the base32 makes any kind of sense, yet. /// We do that when we try to connect. Onion(String), } impl FromStr for Host { type Err = TorAddrError; fn from_str(s: &str) -> Result { if s.ends_with_ignore_ascii_case(".onion") && is_valid_hostname(s) { Ok(Host::Onion(s.to_owned())) } else if let Ok(ip_addr) = s.parse() { Ok(Host::Ip(ip_addr)) } else if is_valid_hostname(s) { // TODO(nickm): we might someday want to reject some kinds of bad // hostnames here, rather than when we're about to connect to them. // But that would be an API break, and maybe not what people want. // Maybe instead we should have a method to check whether a hostname // is "bad"? Not sure; we'll need to decide the right behavior here. Ok(Host::Hostname(s.to_owned())) } else { Err(TorAddrError::InvalidHostname) } } } impl Host { /// Return true if this address is one that is "internal": that is, /// relative to the particular host that is resolving it. fn is_local(&self) -> bool { match self { Host::Hostname(name) => name.eq_ignore_ascii_case("localhost"), // TODO: use is_global once it's stable. Host::Ip(IpAddr::V4(ip)) => ip.is_loopback() || ip.is_private(), Host::Ip(IpAddr::V6(ip)) => ip.is_loopback(), Host::Onion(_) => false, } } } impl std::fmt::Display for Host { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Host::Hostname(s) => Display::fmt(s, f), Host::Ip(ip) => Display::fmt(ip, f), Host::Onion(onion) => Display::fmt(onion, f), } } } impl IntoTorAddr for TorAddr { fn into_tor_addr(self) -> Result { Ok(self) } } impl IntoTorAddr for &A { fn into_tor_addr(self) -> Result { self.clone().into_tor_addr() } } impl IntoTorAddr for &str { fn into_tor_addr(self) -> Result { if let Ok(sa) = SocketAddr::from_str(self) { TorAddr::new(Host::Ip(sa.ip()), sa.port()) } else { let (host, port) = self.rsplit_once(':').ok_or(TorAddrError::NoPort)?; let host = host.parse()?; let port = port.parse().map_err(|_| TorAddrError::BadPort)?; TorAddr::new(host, port) } } } impl IntoTorAddr for String { fn into_tor_addr(self) -> Result { self[..].into_tor_addr() } } impl FromStr for TorAddr { type Err = TorAddrError; fn from_str(s: &str) -> Result { s.into_tor_addr() } } impl IntoTorAddr for (&str, u16) { fn into_tor_addr(self) -> Result { let (host, port) = self; let host = host.parse()?; TorAddr::new(host, port) } } impl IntoTorAddr for (String, u16) { fn into_tor_addr(self) -> Result { let (host, port) = self; (&host[..], port).into_tor_addr() } } impl DangerouslyIntoTorAddr for &T { fn into_tor_addr_dangerously(self) -> Result { self.clone().into_tor_addr_dangerously() } } impl DangerouslyIntoTorAddr for (IpAddr, u16) { fn into_tor_addr_dangerously(self) -> Result { let (addr, port) = self; TorAddr::new(Host::Ip(addr), port) } } impl DangerouslyIntoTorAddr for (Ipv4Addr, u16) { fn into_tor_addr_dangerously(self) -> Result { let (addr, port) = self; TorAddr::new(Host::Ip(addr.into()), port) } } impl DangerouslyIntoTorAddr for (Ipv6Addr, u16) { fn into_tor_addr_dangerously(self) -> Result { let (addr, port) = self; TorAddr::new(Host::Ip(addr.into()), port) } } impl DangerouslyIntoTorAddr for SocketAddr { fn into_tor_addr_dangerously(self) -> Result { let (addr, port) = (self.ip(), self.port()); (addr, port).into_tor_addr_dangerously() } } impl DangerouslyIntoTorAddr for SocketAddrV4 { fn into_tor_addr_dangerously(self) -> Result { let (addr, port) = (self.ip(), self.port()); (*addr, port).into_tor_addr_dangerously() } } impl DangerouslyIntoTorAddr for SocketAddrV6 { fn into_tor_addr_dangerously(self) -> Result { let (addr, port) = (self.ip(), self.port()); (*addr, port).into_tor_addr_dangerously() } } /// Check whether `hostname` is a valid hostname or not. /// /// (Note that IPv6 addresses don't follow these rules.) fn is_valid_hostname(hostname: &str) -> bool { hostname_validator::is_valid(hostname) } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] //! use super::*; #[test] fn validate_hostname() { // Valid hostname tests assert!(is_valid_hostname("torproject.org")); assert!(is_valid_hostname("Tor-Project.org")); assert!(is_valid_hostname("example.onion")); assert!(is_valid_hostname("some.example.onion")); // Invalid hostname tests assert!(!is_valid_hostname("-torproject.org")); assert!(!is_valid_hostname("_torproject.org")); assert!(!is_valid_hostname("tor_project1.org")); assert!(!is_valid_hostname("iwanna$money.org")); } #[test] fn validate_addr() { use crate::err::ErrorDetail; fn val(addr: A) -> Result { let toraddr = addr.into_tor_addr()?; toraddr.enforce_config(&Default::default())?; Ok(toraddr) } assert!(val("[2001:db8::42]:20").is_ok()); assert!(val(("2001:db8::42", 20)).is_ok()); assert!(val(("198.151.100.42", 443)).is_ok()); assert!(val("198.151.100.42:443").is_ok()); assert!(val("www.torproject.org:443").is_ok()); assert!(val(("www.torproject.org", 443)).is_ok()); assert!(val("example.onion:80").is_ok()); assert!(val(("example.onion", 80)).is_ok()); assert!(matches!( val("-foobar.net:443"), Err(ErrorDetail::InvalidHostname) )); assert!(matches!( val("www.torproject.org"), Err(ErrorDetail::Address(TorAddrError::NoPort)) )); assert!(matches!( val("192.168.0.1:80"), Err(ErrorDetail::LocalAddress) )); match val("eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443") { Ok(TorAddr { host: Host::Onion(_), .. }) => {} x => panic!("{x:?}"), } } #[test] fn local_addrs() { fn is_local_hostname(s: &str) -> bool { let h: Host = s.parse().unwrap(); h.is_local() } assert!(is_local_hostname("localhost")); assert!(is_local_hostname("loCALHOST")); assert!(is_local_hostname("127.0.0.1")); assert!(is_local_hostname("::1")); assert!(is_local_hostname("192.168.0.1")); assert!(!is_local_hostname("www.example.com")); } #[test] fn is_ip_address() { fn ip(s: &str) -> bool { TorAddr::from(s).unwrap().is_ip_address() } assert!(ip("192.168.0.1:80")); assert!(ip("[::1]:80")); assert!(ip("[2001:db8::42]:65535")); assert!(!ip("example.com:80")); assert!(!ip("example.onion:80")); } #[test] fn stream_instructions() { use StreamInstructions as SI; fn sap(s: &str) -> Result { TorAddr::from(s).unwrap().into_stream_instructions() } assert_eq!( sap("[2001:db8::42]:9001").unwrap(), SI::Exit { hostname: "2001:db8::42".to_owned(), port: 9001 }, ); assert_eq!( sap("example.com:80").unwrap(), SI::Exit { hostname: "example.com".to_owned(), port: 80 }, ); { let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad"; let onion = format!("sss1234.www.{}.onion", b32); let got = sap(&format!("{}:443", onion)); #[cfg(feature = "onion-client")] assert_eq!( got.unwrap(), SI::Hs { hsid: format!("{}.onion", b32).parse().unwrap(), hostname: onion, port: 443, } ); #[cfg(not(feature = "onion-client"))] assert!(matches!(got, Err(ErrorDetail::OnionAddressNotSupported))); } } #[test] fn resolve_instructions() { use ResolveInstructions as RI; fn sap(s: &str) -> Result { TorAddr::from(s).unwrap().into_resolve_instructions() } assert_eq!( sap("[2001:db8::42]:9001").unwrap(), RI::Return(vec!["2001:db8::42".parse().unwrap()]), ); assert_eq!( sap("example.com:80").unwrap(), RI::Exit("example.com".to_owned()), ); assert!(matches!( sap("example.onion:80"), Err(ErrorDetail::OnionAddressResolveRequest), )); } #[test] fn bad_ports() { assert_eq!( TorAddr::from("www.example.com:squirrel"), Err(TorAddrError::BadPort) ); assert_eq!( TorAddr::from("www.example.com:0"), Err(TorAddrError::BadPort) ); } #[test] fn convert_safe() { fn check(a: A, s: &str) { let a1 = TorAddr::from(a).unwrap(); let a2 = s.parse().unwrap(); assert_eq!(a1, a2); assert_eq!(&a1.to_string(), s); } check(("www.example.com", 8000), "www.example.com:8000"); check( TorAddr::from(("www.example.com", 8000)).unwrap(), "www.example.com:8000", ); check( TorAddr::from(("www.example.com", 8000)).unwrap(), "www.example.com:8000", ); check("[2001:db8::0042]:9001".to_owned(), "[2001:db8::42]:9001"); check(("2001:db8::0042".to_owned(), 9001), "[2001:db8::42]:9001"); } #[test] fn convert_dangerous() { fn check(a: A, s: &str) { let a1 = TorAddr::dangerously_from(a).unwrap(); let a2 = TorAddr::from(s).unwrap(); assert_eq!(a1, a2); assert_eq!(&a1.to_string(), s); } let ip: IpAddr = "203.0.133.6".parse().unwrap(); let ip4: Ipv4Addr = "203.0.133.7".parse().unwrap(); let ip6: Ipv6Addr = "2001:db8::42".parse().unwrap(); let sa: SocketAddr = "203.0.133.8:80".parse().unwrap(); let sa4: SocketAddrV4 = "203.0.133.8:81".parse().unwrap(); let sa6: SocketAddrV6 = "[2001:db8::43]:82".parse().unwrap(); // This tests impl DangerouslyIntoTorAddr for &T #[allow(clippy::needless_borrow)] check(&(ip, 443), "203.0.133.6:443"); check((ip, 443), "203.0.133.6:443"); check((ip4, 444), "203.0.133.7:444"); check((ip6, 445), "[2001:db8::42]:445"); check(sa, "203.0.133.8:80"); check(sa4, "203.0.133.8:81"); check(sa6, "[2001:db8::43]:82"); } }