Move hostname enforcement into TorAddr.

This commit is contained in:
Nick Mathewson 2021-10-18 14:32:05 -04:00
parent 5ae433c747
commit 0750199a8c
2 changed files with 132 additions and 97 deletions

View File

@ -82,6 +82,33 @@ impl TorAddr {
let port = self.port;
(host, port)
}
/// 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::ClientConfig,
) -> Result<(), crate::Error> {
if !cfg.allow_local_addrs && self.is_local() {
return Err(crate::Error::LocalAddress);
}
if let Host::Hostname(addr) = &self.host {
if !is_valid_hostname(addr) {
return Err(crate::Error::InvalidHostname);
}
if addr.to_lowercase().ends_with(".onion") {
return Err(crate::Error::OnionAddressNotSupported);
}
}
Ok(())
}
}
impl std::fmt::Display for TorAddr {
@ -130,6 +157,19 @@ impl FromStr for Host {
}
}
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(),
}
}
}
impl std::fmt::Display for Host {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
@ -244,3 +284,92 @@ impl DangerouslyIntoTorAddr for SocketAddrV6 {
(*addr, port).into_tor_addr_dangerously()
}
}
/// Check whether `hostname` is a valid hostname or not.
///
/// TODO: Check whether the rules given here are in fact the same rules
/// as Tor follows, and whether they conform to anything.
fn is_valid_hostname(hostname: &str) -> bool {
/// Check if we have the valid characters for a hostname
fn is_valid_char(byte: u8) -> bool {
((b'a'..=b'z').contains(&byte))
|| ((b'A'..=b'Z').contains(&byte))
|| ((b'0'..=b'9').contains(&byte))
|| byte == b'-'
|| byte == b'.'
}
/// Check if we look like an IPv6 address
fn is_ipv6_str(addr: &str) -> bool {
if let Ok(ip) = IpAddr::from_str(addr) {
ip.is_ipv6()
} else {
false
}
}
!(hostname.bytes().any(|byte| !is_valid_char(byte))
|| hostname.ends_with('-')
|| hostname.starts_with('-')
|| hostname.ends_with('.')
|| hostname.starts_with('.')
|| hostname.is_empty())
|| is_ipv6_str(hostname)
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn validate_hostname() {
// Valid hostname tests
assert!(is_valid_hostname("torproject.org"));
assert!(is_valid_hostname("Tor-Project.org"));
// 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"));
assert!(!is_valid_hostname(""));
}
#[test]
fn validate_addr() {
fn ok<A: IntoTorAddr>(addr: A) -> bool {
if let Ok(toraddr) = addr.into_tor_addr() {
toraddr.enforce_config(&Default::default()).is_ok()
} else {
false
}
}
assert!(ok("[2001:db8::42]:20"));
assert!(ok(("2001:db8::42", 20)));
assert!(ok(("128.66.0.42", 443)));
assert!(ok("128.66.0.42:443"));
assert!(ok("www.torproject.org:443"));
assert!(ok(("www.torproject.org", 443)));
assert!(!ok("-foobar.net:443"));
assert!(!ok("www.torproject.org"));
}
#[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"));
}
}

View File

@ -201,18 +201,9 @@ impl<R: Runtime> TorClient<R> {
flags: Option<ConnectPrefs>,
) -> Result<DataStream> {
let addr = target.into_tor_addr()?;
addr.enforce_config(&self.clientcfg)?;
let (addr, port) = addr.into_string_and_port();
if addr.to_lowercase().ends_with(".onion") {
return Err(Error::OnionAddressNotSupported);
}
if !is_valid_hostname(&addr) {
return Err(Error::InvalidHostname);
}
if !self.clientcfg.allow_local_addrs && is_local_hostname(&addr) {
return Err(Error::LocalAddress);
}
let flags = flags.unwrap_or_default();
let exit_ports = [flags.wrap_target_port(port)];
let circ = self.get_or_launch_exit_circ(&exit_ports, &flags).await?;
@ -236,12 +227,8 @@ impl<R: Runtime> TorClient<R> {
hostname: &str,
flags: Option<ConnectPrefs>,
) -> Result<Vec<IpAddr>> {
if hostname.to_lowercase().ends_with(".onion") {
return Err(Error::OnionAddressNotSupported);
}
if !is_valid_hostname(hostname) {
return Err(Error::InvalidHostname);
}
let addr = (hostname, 0).into_tor_addr()?;
addr.enforce_config(&self.clientcfg)?;
let flags = flags.unwrap_or_default();
let circ = self.get_or_launch_exit_circ(&[], &flags).await?;
@ -325,54 +312,6 @@ impl<R: Runtime> TorClient<R> {
}
}
/// Validate if we are an valid hostname or not
fn is_valid_hostname(hostname: &str) -> bool {
/// Check if we have the valid characters for a hostname
fn is_valid_char(byte: u8) -> bool {
((b'a'..=b'z').contains(&byte))
|| ((b'A'..=b'Z').contains(&byte))
|| ((b'0'..=b'9').contains(&byte))
|| byte == b'-'
|| byte == b'.'
}
/// Check if we look like an IPv6 address
fn is_ipv6_str(addr: &str) -> bool {
if let Ok(ip) = IpAddr::from_str(addr) {
ip.is_ipv6()
} else {
false
}
}
!(hostname.bytes().any(|byte| !is_valid_char(byte))
|| hostname.ends_with('-')
|| hostname.starts_with('-')
|| hostname.ends_with('.')
|| hostname.starts_with('.')
|| hostname.is_empty())
|| is_ipv6_str(hostname)
}
/// Return true if `addr` refers to a local address.
fn is_local_hostname(addr: &str) -> bool {
if addr.eq_ignore_ascii_case("localhost") {
true
} else if let Ok(ip) = IpAddr::from_str(addr) {
is_internal_ip(&ip)
} else {
false
}
}
/// Check if the IP is internal
fn is_internal_ip(addr: &IpAddr) -> bool {
// TODO: Use is_global() when it is stable
match addr {
IpAddr::V4(ip) => ip.is_loopback() || ip.is_private(),
IpAddr::V6(ip) => ip.is_loopback(),
}
}
/// Whenever a [`DirEvent::NewConsensus`] arrives on `events`, update
/// `circmgr` with the consensus parameters from `dirmgr`.
///
@ -489,36 +428,3 @@ impl<R: Runtime> Drop for TorClient<R> {
}
}
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
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("72.0.227.52"));
assert!(is_valid_hostname("2600::1"));
// 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"));
assert!(!is_valid_hostname(""));
}
#[test]
fn local_addrs() {
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"));
}
}