Skip to main content

fips_core/transport/tor/
address.rs

1use crate::transport::{TransportAddr, TransportError};
2use std::net::SocketAddr;
3
4// ============================================================================
5// Tor Address Types
6// ============================================================================
7
8/// Tor-specific address type for SOCKS5 CONNECT.
9#[derive(Clone, Debug)]
10pub enum TorAddr {
11    /// .onion hidden service address (hostname, port).
12    Onion(String, u16),
13    /// Clearnet address routed through Tor (IP, port).
14    Clearnet(SocketAddr),
15    /// Clearnet hostname routed through Tor (hostname, port).
16    /// Passed as hostname to SOCKS5 so Tor resolves it — avoids local
17    /// DNS leaks and is compatible with SafeSocks 1.
18    ClearnetHostname(String, u16),
19}
20
21/// Parse a TransportAddr string into a TorAddr.
22///
23/// If the address contains ".onion:", parse as an onion address.
24/// If it parses as a numeric IP:port, use Clearnet.
25/// Otherwise, treat as a clearnet hostname:port for Tor-side DNS resolution.
26pub(super) fn parse_tor_addr(addr: &TransportAddr) -> Result<TorAddr, TransportError> {
27    let s = addr.as_str().ok_or_else(|| {
28        TransportError::InvalidAddress("Tor address must be a valid UTF-8 string".into())
29    })?;
30
31    if s.contains(".onion:") {
32        // Parse "hostname.onion:port"
33        let (host, port_str) = s.rsplit_once(':').ok_or_else(|| {
34            TransportError::InvalidAddress(format!("invalid onion address: {}", s))
35        })?;
36        let port: u16 = port_str.parse().map_err(|_| {
37            TransportError::InvalidAddress(format!("invalid port in onion address: {}", s))
38        })?;
39        Ok(TorAddr::Onion(host.to_string(), port))
40    } else if let Ok(socket_addr) = s.parse::<SocketAddr>() {
41        // Numeric IP:port
42        Ok(TorAddr::Clearnet(socket_addr))
43    } else {
44        // Hostname:port — pass through SOCKS5 for Tor-side DNS resolution
45        let (host, port_str) = s.rsplit_once(':').ok_or_else(|| {
46            TransportError::InvalidAddress(format!("invalid address (expected host:port): {}", s))
47        })?;
48        let port: u16 = port_str
49            .parse()
50            .map_err(|_| TransportError::InvalidAddress(format!("invalid port: {}", s)))?;
51        if !host.contains('.') {
52            return Err(TransportError::InvalidAddress(format!(
53                "hostname must be fully qualified (contain a dot): {}",
54                host
55            )));
56        }
57        Ok(TorAddr::ClearnetHostname(host.to_string(), port))
58    }
59}
60
61// ============================================================================
62// Helpers
63// ============================================================================
64
65/// Validate that a string is in host:port format.
66pub(super) fn validate_host_port(addr: &str, field_name: &str) -> Result<(), TransportError> {
67    if addr.parse::<SocketAddr>().is_ok() {
68        return Ok(());
69    }
70    // Not a raw IP:port — check it's at least host:port format
71    let parts: Vec<&str> = addr.rsplitn(2, ':').collect();
72    if parts.len() != 2 || parts[0].parse::<u16>().is_err() || parts[1].is_empty() {
73        return Err(TransportError::StartFailed(format!(
74            "invalid {} '{}': expected host:port or IP:port",
75            field_name, addr
76        )));
77    }
78    Ok(())
79}