use crate::StreamPrefs;
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;
use tor_error::{ErrorKind, HasKind};
#[cfg(feature = "onion-service-client")]
use tor_hscrypto::pk::{HSID_ONION_SUFFIX, HsId};
#[cfg(not(feature = "onion-service-client"))]
pub(crate) mod hs_dummy {
use super::*;
use tor_error::internal;
use void::Void;
#[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 {}
pub(crate) const HSID_ONION_SUFFIX: &str = ".onion";
impl FromStr for HsId {
type Err = ErrorDetail;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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-service-client"))]
use hs_dummy::*;
pub trait IntoTorAddr {
fn into_tor_addr(self) -> Result<TorAddr, TorAddrError>;
}
pub trait DangerouslyIntoTorAddr {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError>;
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct TorAddr {
host: Host,
port: u16,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum StreamInstructions {
Exit {
hostname: String,
port: u16,
},
Hs {
hsid: HsId,
hostname: String,
port: u16,
},
}
#[derive(PartialEq, Eq, Debug)]
pub(crate) enum ResolveInstructions {
Exit(String),
Return(Vec<IpAddr>),
}
impl TorAddr {
fn new(host: Host, port: u16) -> Result<Self, TorAddrError> {
if port == 0 {
Err(TorAddrError::BadPort)
} else {
Ok(TorAddr { host, port })
}
}
pub fn from<A: IntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
addr.into_tor_addr()
}
pub fn dangerously_from<A: DangerouslyIntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
addr.into_tor_addr_dangerously()
}
pub fn is_ip_address(&self) -> bool {
matches!(&self.host, Host::Ip(_))
}
pub fn as_ip_address(&self) -> Option<&IpAddr> {
match &self.host {
Host::Ip(a) => Some(a),
_ => None,
}
}
pub(crate) fn into_stream_instructions(
self,
cfg: &crate::config::ClientAddrConfig,
prefs: &StreamPrefs,
) -> Result<StreamInstructions, ErrorDetail> {
self.enforce_config(cfg, prefs)?;
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) => {
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,
}
}
})
}
pub(crate) fn into_resolve_instructions(
self,
cfg: &crate::config::ClientAddrConfig,
prefs: &StreamPrefs,
) -> Result<ResolveInstructions, ErrorDetail> {
let enforce_config_result = self.enforce_config(cfg, prefs);
let instructions = (move || {
Ok(match self.host {
Host::Hostname(hostname) => ResolveInstructions::Exit(hostname),
Host::Ip(ip) => ResolveInstructions::Return(vec![ip]),
Host::Onion(_) => return Err(ErrorDetail::OnionAddressResolveRequest),
})
})()?;
let () = enforce_config_result?;
Ok(instructions)
}
fn is_local(&self) -> bool {
self.host.is_local()
}
fn enforce_config(
&self,
cfg: &crate::config::ClientAddrConfig,
#[allow(unused_variables)] prefs: &StreamPrefs,
) -> 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) {
return Err(ErrorDetail::InvalidHostname);
}
if addr.ends_with_ignore_ascii_case(HSID_ONION_SUFFIX) {
return Err(ErrorDetail::OnionAddressNotSupported);
}
}
if let Host::Onion(_name) = &self.host {
cfg_if::cfg_if! {
if #[cfg(feature = "onion-service-client")] {
if !prefs.connect_to_onion_services.as_bool().unwrap_or(cfg.allow_onion_addrs) {
return Err(ErrorDetail::OnionAddressDisabled);
}
} else {
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),
}
}
}
#[derive(Debug, Error, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum TorAddrError {
#[error("String can never be a valid hostname")]
InvalidHostname,
#[error("No port found in string")]
NoPort,
#[error("Could not parse port")]
BadPort,
}
impl HasKind for TorAddrError {
fn kind(&self) -> ErrorKind {
use ErrorKind as EK;
use TorAddrError as TAE;
match self {
TAE::InvalidHostname => EK::InvalidStreamTarget,
TAE::NoPort => EK::InvalidStreamTarget,
TAE::BadPort => EK::InvalidStreamTarget,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Host {
Hostname(String),
Ip(IpAddr),
Onion(String),
}
impl FromStr for Host {
type Err = TorAddrError;
fn from_str(s: &str) -> Result<Host, TorAddrError> {
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) {
Ok(Host::Hostname(s.to_owned()))
} else {
Err(TorAddrError::InvalidHostname)
}
}
}
impl Host {
fn is_local(&self) -> bool {
match self {
Host::Hostname(name) => name.eq_ignore_ascii_case("localhost"),
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<TorAddr, TorAddrError> {
Ok(self)
}
}
impl<A: IntoTorAddr + Clone> IntoTorAddr for &A {
fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
self.clone().into_tor_addr()
}
}
impl IntoTorAddr for &str {
fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
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<TorAddr, TorAddrError> {
self[..].into_tor_addr()
}
}
impl FromStr for TorAddr {
type Err = TorAddrError;
fn from_str(s: &str) -> Result<Self, TorAddrError> {
s.into_tor_addr()
}
}
impl IntoTorAddr for (&str, u16) {
fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
let (host, port) = self;
let host = host.parse()?;
TorAddr::new(host, port)
}
}
impl IntoTorAddr for (String, u16) {
fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
let (host, port) = self;
(&host[..], port).into_tor_addr()
}
}
impl<T: DangerouslyIntoTorAddr + Clone> DangerouslyIntoTorAddr for &T {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
self.clone().into_tor_addr_dangerously()
}
}
impl DangerouslyIntoTorAddr for (IpAddr, u16) {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
let (addr, port) = self;
TorAddr::new(Host::Ip(addr), port)
}
}
impl DangerouslyIntoTorAddr for (Ipv4Addr, u16) {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
let (addr, port) = self;
TorAddr::new(Host::Ip(addr.into()), port)
}
}
impl DangerouslyIntoTorAddr for (Ipv6Addr, u16) {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
let (addr, port) = self;
TorAddr::new(Host::Ip(addr.into()), port)
}
}
impl DangerouslyIntoTorAddr for SocketAddr {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
let (addr, port) = (self.ip(), self.port());
(addr, port).into_tor_addr_dangerously()
}
}
impl DangerouslyIntoTorAddr for SocketAddrV4 {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
let (addr, port) = (self.ip(), self.port());
(*addr, port).into_tor_addr_dangerously()
}
}
impl DangerouslyIntoTorAddr for SocketAddrV6 {
fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
let (addr, port) = (self.ip(), self.port());
(*addr, port).into_tor_addr_dangerously()
}
}
fn is_valid_hostname(hostname: &str) -> bool {
hostname_validator::is_valid(hostname)
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
#[test]
fn test_error_kind() {
use tor_error::ErrorKind as EK;
assert_eq!(
TorAddrError::InvalidHostname.kind(),
EK::InvalidStreamTarget
);
assert_eq!(TorAddrError::NoPort.kind(), EK::InvalidStreamTarget);
assert_eq!(TorAddrError::BadPort.kind(), EK::InvalidStreamTarget);
}
fn mk_stream_prefs() -> StreamPrefs {
let prefs = crate::StreamPrefs::default();
#[cfg(feature = "onion-service-client")]
let prefs = {
let mut prefs = prefs;
prefs.connect_to_onion_services(tor_config::BoolOrAuto::Explicit(true));
prefs
};
prefs
}
#[test]
fn validate_hostname() {
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"));
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<A: IntoTorAddr>(addr: A) -> Result<TorAddr, ErrorDetail> {
let toraddr = addr.into_tor_addr()?;
toraddr.enforce_config(&Default::default(), &mk_stream_prefs())?;
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());
#[cfg(feature = "onion-service-client")]
{
assert!(val("example.onion:80").is_ok());
assert!(val(("example.onion", 80)).is_ok());
match val("eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443") {
Ok(TorAddr {
host: Host::Onion(_),
..
}) => {}
x => panic!("{x:?}"),
}
}
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)
));
assert!(matches!(
val(TorAddr::new(Host::Hostname("foo@bar".to_owned()), 553).unwrap()),
Err(ErrorDetail::InvalidHostname)
));
assert!(matches!(
val(TorAddr::new(Host::Hostname("foo.onion".to_owned()), 80).unwrap()),
Err(ErrorDetail::OnionAddressNotSupported)
));
}
#[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<StreamInstructions, ErrorDetail> {
TorAddr::from(s)
.unwrap()
.into_stream_instructions(&Default::default(), &mk_stream_prefs())
}
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-service-client")]
assert_eq!(
got.unwrap(),
SI::Hs {
hsid: format!("{}.onion", b32).parse().unwrap(),
hostname: onion,
port: 443,
}
);
#[cfg(not(feature = "onion-service-client"))]
assert!(matches!(got, Err(ErrorDetail::OnionAddressNotSupported)));
}
}
#[test]
fn resolve_instructions() {
use ResolveInstructions as RI;
fn sap(s: &str) -> Result<ResolveInstructions, ErrorDetail> {
TorAddr::from(s)
.unwrap()
.into_resolve_instructions(&Default::default(), &Default::default())
}
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 prefs_onion_services() {
use crate::err::ErrorDetailDiscriminants;
use ErrorDetailDiscriminants as EDD;
use ErrorKind as EK;
use tor_error::{ErrorKind, HasKind as _};
#[allow(clippy::redundant_closure)] let prefs_def = || StreamPrefs::default();
let addr: TorAddr = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443"
.parse()
.unwrap();
fn map(
got: Result<impl Sized, ErrorDetail>,
) -> Result<(), (ErrorDetailDiscriminants, ErrorKind)> {
got.map(|_| ())
.map_err(|e| (ErrorDetailDiscriminants::from(&e), e.kind()))
}
let check_stream = |prefs, expected| {
let got = addr
.clone()
.into_stream_instructions(&Default::default(), &prefs);
assert_eq!(map(got), expected, "{prefs:?}");
};
let check_resolve = |prefs| {
let got = addr
.clone()
.into_resolve_instructions(&Default::default(), &prefs);
let expected = Err((EDD::OnionAddressResolveRequest, EK::NotImplemented));
assert_eq!(map(got), expected, "{prefs:?}");
};
cfg_if::cfg_if! {
if #[cfg(feature = "onion-service-client")] {
use tor_config::BoolOrAuto as B;
let prefs_of = |yn| {
let mut prefs = StreamPrefs::default();
prefs.connect_to_onion_services(yn);
prefs
};
check_stream(prefs_def(), Ok(()));
check_stream(prefs_of(B::Auto), Ok(()));
check_stream(prefs_of(B::Explicit(true)), Ok(()));
check_stream(prefs_of(B::Explicit(false)), Err((EDD::OnionAddressDisabled, EK::ForbiddenStreamTarget)));
check_resolve(prefs_def());
check_resolve(prefs_of(B::Auto));
check_resolve(prefs_of(B::Explicit(true)));
check_resolve(prefs_of(B::Explicit(false)));
} else {
check_stream(prefs_def(), Err((EDD::OnionAddressNotSupported, EK::FeatureDisabled)));
check_resolve(prefs_def());
}
}
}
#[test]
fn convert_safe() {
fn check<A: IntoTorAddr>(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",
);
let addr = "[2001:db8::0042]:9001".to_owned();
check(&addr, "[2001:db8::42]:9001");
check(addr, "[2001:db8::42]:9001");
check(("2001:db8::0042".to_owned(), 9001), "[2001:db8::42]:9001");
check(("example.onion", 80), "example.onion:80");
}
#[test]
fn convert_dangerous() {
fn check<A: DangerouslyIntoTorAddr>(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();
#[allow(clippy::needless_borrow)]
#[allow(clippy::needless_borrows_for_generic_args)]
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");
}
}