pub mod control;
pub mod stats;
mod address;
#[cfg(test)]
mod mock_control;
#[cfg(test)]
mod mock_socks5;
mod monitoring;
mod tasks;
mod trait_impl;
#[cfg(test)]
mod tests;
use super::stream_io::{StreamConnectionIo, StreamWriteError};
use super::{
ConnectionState, PacketTx, TransportAddr, TransportError, TransportId, TransportState,
};
use crate::config::TorConfig;
use crate::transport::tcp::stream::validate_stream_record;
pub use address::TorAddr;
use address::{parse_tor_addr, validate_host_port};
use control::{ControlAuth, TorControlClient, TorMonitoringInfo};
use stats::TorStats;
use tasks::{TorReceiveContext, configure_socket, tor_accept_loop, tor_receive_loop};
use futures::FutureExt;
use std::collections::HashMap;
#[cfg(test)]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::tcp::OwnedWriteHalf;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::time::Instant;
use tokio_socks::tcp::Socks5Stream;
use tracing::{debug, info, trace, warn};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Direction {
Inbound,
Outbound,
}
struct TorConnection {
io: Arc<StreamConnectionIo<OwnedWriteHalf>>,
recv_task: JoinHandle<()>,
direction: Direction,
}
type ConnectionPool = Arc<Mutex<HashMap<TransportAddr, TorConnection>>>;
fn remove_if_current(
pool: &mut HashMap<TransportAddr, TorConnection>,
addr: &TransportAddr,
io: &Arc<StreamConnectionIo<OwnedWriteHalf>>,
) -> Option<TorConnection> {
pool.get(addr)
.is_some_and(|connection| Arc::ptr_eq(&connection.io, io))
.then(|| pool.remove(addr))
.flatten()
}
fn record_pool_removed(stats: &TorStats, connection: &TorConnection) {
match connection.direction {
Direction::Inbound => stats.record_pool_inbound_removed(),
Direction::Outbound => stats.record_pool_outbound_removed(),
}
}
struct ConnectingEntry {
task: JoinHandle<Result<TcpStream, TransportError>>,
}
type ConnectingPool = Arc<Mutex<HashMap<TransportAddr, ConnectingEntry>>>;
pub struct TorTransport {
transport_id: TransportId,
name: Option<String>,
config: TorConfig,
state: TransportState,
pool: ConnectionPool,
connecting: ConnectingPool,
packet_tx: PacketTx,
stats: Arc<TorStats>,
accept_task: Option<JoinHandle<()>>,
onion_address: Option<String>,
control_client: Option<Arc<Mutex<TorControlClient>>>,
cached_monitoring: Arc<std::sync::RwLock<Option<TorMonitoringInfo>>>,
monitoring_task: Option<JoinHandle<()>>,
}
impl TorTransport {
pub fn new(
transport_id: TransportId,
name: Option<String>,
config: TorConfig,
packet_tx: PacketTx,
) -> Self {
Self {
transport_id,
name,
config,
state: TransportState::Configured,
pool: Arc::new(Mutex::new(HashMap::new())),
connecting: Arc::new(Mutex::new(HashMap::new())),
packet_tx,
stats: Arc::new(TorStats::new()),
accept_task: None,
onion_address: None,
control_client: None,
cached_monitoring: Arc::new(std::sync::RwLock::new(None)),
monitoring_task: None,
}
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn onion_address(&self) -> Option<&str> {
self.onion_address.as_deref()
}
pub fn stats(&self) -> &Arc<TorStats> {
&self.stats
}
pub fn cached_monitoring(&self) -> Option<TorMonitoringInfo> {
self.cached_monitoring.read().ok()?.clone()
}
pub fn mode(&self) -> &str {
self.config.mode()
}
pub async fn start_async(&mut self) -> Result<(), TransportError> {
if !self.state.can_start() {
return Err(TransportError::AlreadyStarted);
}
self.state = TransportState::Starting;
let socks5_addr = self.config.socks5_addr().to_string();
validate_host_port(&socks5_addr, "socks5_addr")?;
let mode = self.config.mode().to_string();
match mode.as_str() {
"socks5" => {
if self.config.directory_service.is_some() {
return Err(TransportError::StartFailed(
"directory_service config requires mode 'directory', not 'socks5'".into(),
));
}
self.state = TransportState::Up;
}
"control_port" => {
self.start_control_port_mode().await?;
}
"directory" => {
self.start_directory_mode().await?;
}
other => {
return Err(TransportError::StartFailed(format!(
"unsupported Tor mode '{}' (expected 'socks5', 'control_port', or 'directory')",
other
)));
}
}
if let Some(ref name) = self.name {
info!(
name = %name,
mode = %mode,
socks5_addr = %socks5_addr,
onion_address = ?self.onion_address,
mtu = self.config.mtu(),
"Tor transport started"
);
} else {
info!(
mode = %mode,
socks5_addr = %socks5_addr,
onion_address = ?self.onion_address,
mtu = self.config.mtu(),
"Tor transport started"
);
}
Ok(())
}
async fn start_control_port_mode(&mut self) -> Result<(), TransportError> {
let control_addr = self.config.control_addr().to_string();
if !control_addr.starts_with('/') && !control_addr.starts_with("./") {
validate_host_port(&control_addr, "control_addr")?;
}
let mut client = TorControlClient::connect(&control_addr)
.await
.map_err(|e| {
self.stats.record_control_error();
TransportError::StartFailed(format!("Tor control port: {}", e))
})?;
let auth = ControlAuth::from_config(self.config.control_auth(), self.config.cookie_path())
.map_err(|e| TransportError::StartFailed(format!("Tor auth config: {}", e)))?;
client.authenticate(&auth).await.map_err(|e| {
self.stats.record_control_error();
TransportError::StartFailed(format!("Tor authentication: {}", e))
})?;
self.control_client = Some(Arc::new(Mutex::new(client)));
self.state = TransportState::Up;
self.spawn_monitoring_task();
Ok(())
}
async fn start_directory_mode(&mut self) -> Result<(), TransportError> {
let dir_config = self.config.directory_service.clone().unwrap_or_default();
let hostname_file = dir_config.hostname_file();
let onion_addr = std::fs::read_to_string(hostname_file)
.map_err(|e| {
TransportError::StartFailed(format!(
"failed to read onion hostname from '{}': {} \
(ensure HiddenServiceDir is configured in torrc and Tor has started)",
hostname_file, e
))
})?
.trim()
.to_string();
if onion_addr.is_empty() || !onion_addr.ends_with(".onion") {
return Err(TransportError::StartFailed(format!(
"invalid onion address in '{}': '{}'",
hostname_file, onion_addr
)));
}
self.onion_address = Some(onion_addr.clone());
let bind_addr = dir_config.bind_addr();
let listener = TcpListener::bind(bind_addr).await.map_err(|e| {
TransportError::StartFailed(format!(
"failed to bind directory-mode listener on {}: {}",
bind_addr, e
))
})?;
let local_addr = listener
.local_addr()
.map_err(|e| TransportError::StartFailed(format!("failed to get local addr: {}", e)))?;
info!(
onion_address = %onion_addr,
local_addr = %local_addr,
hostname_file = %hostname_file,
"Directory-mode onion service active"
);
let transport_id = self.transport_id;
let packet_tx = self.packet_tx.clone();
let pool = self.pool.clone();
let max_inbound = self.config.max_inbound_connections();
let stats = self.stats.clone();
let accept_handle = tokio::spawn(async move {
tor_accept_loop(listener, transport_id, packet_tx, pool, max_inbound, stats).await;
});
self.accept_task = Some(accept_handle);
self.state = TransportState::Up;
if self.config.control_addr.is_some() {
self.try_connect_control_port().await;
}
Ok(())
}
async fn try_connect_control_port(&mut self) {
let control_addr = self.config.control_addr().to_string();
if !control_addr.starts_with('/')
&& !control_addr.starts_with("./")
&& let Err(e) = validate_host_port(&control_addr, "control_addr")
{
warn!(
transport_id = %self.transport_id,
error = %e,
"Tor control port address invalid, monitoring disabled"
);
return;
}
let client = match TorControlClient::connect(&control_addr).await {
Ok(c) => c,
Err(e) => {
warn!(
transport_id = %self.transport_id,
addr = %control_addr,
error = %e,
"Tor control port connect failed, monitoring disabled"
);
return;
}
};
let auth =
match ControlAuth::from_config(self.config.control_auth(), self.config.cookie_path()) {
Ok(a) => a,
Err(e) => {
warn!(
transport_id = %self.transport_id,
error = %e,
"Tor control auth config error, monitoring disabled"
);
return;
}
};
let mut client = client;
if let Err(e) = client.authenticate(&auth).await {
warn!(
transport_id = %self.transport_id,
error = %e,
"Tor control port auth failed, monitoring disabled"
);
return;
}
info!(
transport_id = %self.transport_id,
addr = %control_addr,
"Tor control port connected (monitoring enabled)"
);
self.control_client = Some(Arc::new(Mutex::new(client)));
self.spawn_monitoring_task();
}
pub async fn stop_async(&mut self) -> Result<(), TransportError> {
if !self.state.is_operational() {
return Err(TransportError::NotStarted);
}
if let Some(task) = self.accept_task.take() {
task.abort();
let _ = task.await;
debug!(
transport_id = %self.transport_id,
"Onion service accept loop stopped"
);
}
if let Some(task) = self.monitoring_task.take() {
task.abort();
let _ = task.await;
}
if let Ok(mut w) = self.cached_monitoring.write() {
*w = None;
}
self.control_client = None;
self.onion_address = None;
let mut connecting = self.connecting.lock().await;
for (addr, entry) in connecting.drain() {
entry.task.abort();
debug!(
transport_id = %self.transport_id,
remote_addr = %addr,
"Tor connect aborted (transport stopping)"
);
}
drop(connecting);
let mut pool = self.pool.lock().await;
for (addr, conn) in pool.drain() {
conn.recv_task.abort();
let _ = conn.recv_task.await;
match conn.direction {
Direction::Inbound => self.stats.record_pool_inbound_removed(),
Direction::Outbound => self.stats.record_pool_outbound_removed(),
}
debug!(
transport_id = %self.transport_id,
remote_addr = %addr,
direction = ?conn.direction,
"Tor connection closed (transport stopping)"
);
}
drop(pool);
self.state = TransportState::Down;
info!(
transport_id = %self.transport_id,
"Tor transport stopped"
);
Ok(())
}
pub async fn send_async(
&self,
addr: &TransportAddr,
data: &[u8],
) -> Result<usize, TransportError> {
if !self.state.is_operational() {
return Err(TransportError::NotStarted);
}
if let Err(error) = validate_stream_record(data) {
self.stats.record_send_error();
return Err(TransportError::SendFailed(format!(
"invalid FIPS stream record: {error}"
)));
}
let io = {
let pool = self.pool.lock().await;
pool.get(addr).map(|connection| connection.io.clone())
};
let io = match io {
Some(io) if !io.is_closed() => io,
None => {
self.connect(addr).await?
}
Some(_) => self.connect(addr).await?,
};
match io.write_record(data, None).await {
Ok(()) => {
self.stats.record_send(data.len());
trace!(
transport_id = %self.transport_id,
remote_addr = %addr,
bytes = data.len(),
"Tor packet sent"
);
Ok(data.len())
}
Err(error) => {
self.stats.record_send_error();
if error.poisons_connection() {
self.remove_failed_connection(addr, &io).await;
}
match error {
StreamWriteError::LockTimeout | StreamWriteError::WriteTimeout => {
Err(TransportError::Timeout)
}
StreamWriteError::Closed | StreamWriteError::Io(_) => {
Err(TransportError::SendFailed(error.to_string()))
}
}
}
}
}
async fn remove_failed_connection(
&self,
addr: &TransportAddr,
failed_io: &Arc<StreamConnectionIo<OwnedWriteHalf>>,
) {
let mut pool = self.pool.lock().await;
if let Some(connection) = remove_if_current(&mut pool, addr, failed_io) {
connection.recv_task.abort();
record_pool_removed(&self.stats, &connection);
}
}
async fn connect(
&self,
addr: &TransportAddr,
) -> Result<Arc<StreamConnectionIo<OwnedWriteHalf>>, TransportError> {
let tor_addr = parse_tor_addr(addr)?;
let proxy_addr = self.config.socks5_addr();
let timeout_ms = self.config.connect_timeout_ms();
debug!(
transport_id = %self.transport_id,
remote_addr = %addr,
proxy = %proxy_addr,
timeout_secs = timeout_ms / 1000,
"Connecting via Tor SOCKS5"
);
let isolation_key = addr.to_string();
let connect_start = Instant::now();
let socks_result = tokio::time::timeout(Duration::from_millis(timeout_ms), async {
match &tor_addr {
TorAddr::Onion(host, port) | TorAddr::ClearnetHostname(host, port) => {
Socks5Stream::connect_with_password(
proxy_addr,
(host.as_str(), *port),
"fips",
&isolation_key,
)
.await
}
TorAddr::Clearnet(socket_addr) => {
Socks5Stream::connect_with_password(
proxy_addr,
*socket_addr,
"fips",
&isolation_key,
)
.await
}
}
})
.await;
let stream = match socks_result {
Ok(Ok(socks_stream)) => socks_stream.into_inner(),
Ok(Err(e)) => {
self.stats.record_socks5_error();
warn!(
transport_id = %self.transport_id,
remote_addr = %addr,
error = %e,
elapsed_secs = connect_start.elapsed().as_secs(),
"Tor SOCKS5 connection failed"
);
return Err(TransportError::ConnectionRefused);
}
Err(_) => {
self.stats.record_connect_timeout();
warn!(
transport_id = %self.transport_id,
remote_addr = %addr,
timeout_secs = timeout_ms / 1000,
"Tor SOCKS5 connection timed out"
);
return Err(TransportError::Timeout);
}
};
let std_stream = stream
.into_std()
.map_err(|e| TransportError::StartFailed(format!("into_std: {}", e)))?;
configure_socket(&std_stream, &self.config)?;
let stream = TcpStream::from_std(std_stream)
.map_err(|e| TransportError::StartFailed(format!("from_std: {}", e)))?;
let mut pool_guard = self.pool.lock().await;
if let Some(existing) = pool_guard.get(addr)
&& !existing.io.is_closed()
{
return Ok(existing.io.clone());
}
if let Some(closed) = pool_guard.remove(addr) {
closed.recv_task.abort();
record_pool_removed(&self.stats, &closed);
}
let (read_half, write_half) = stream.into_split();
let io = Arc::new(StreamConnectionIo::new(write_half));
let transport_id = self.transport_id;
let packet_tx = self.packet_tx.clone();
let recv_pool = self.pool.clone();
let recv_stats = self.stats.clone();
let remote_addr = addr.clone();
let recv_io = io.clone();
let recv_task = tokio::spawn(async move {
tor_receive_loop(
read_half,
TorReceiveContext {
transport_id,
remote_addr,
packet_tx,
pool: recv_pool,
stats: recv_stats,
first_frame_timeout: None,
direction: Direction::Outbound,
io: recv_io,
},
)
.await;
});
let conn = TorConnection {
io: io.clone(),
recv_task,
direction: Direction::Outbound,
};
pool_guard.insert(addr.clone(), conn);
self.stats.record_pool_outbound_added();
drop(pool_guard);
self.stats.record_connection_established();
info!(
transport_id = %self.transport_id,
remote_addr = %addr,
elapsed_secs = connect_start.elapsed().as_secs(),
"Tor circuit established via SOCKS5"
);
Ok(io)
}
pub async fn connect_async(&self, addr: &TransportAddr) -> Result<(), TransportError> {
if !self.state.is_operational() {
return Err(TransportError::NotStarted);
}
let tor_addr = parse_tor_addr(addr)?;
let proxy_addr = self.config.socks5_addr().to_string();
let timeout_ms = self.config.connect_timeout_ms();
let transport_id = self.transport_id;
let remote_addr = addr.clone();
let config = self.config.clone();
debug!(
transport_id = %transport_id,
remote_addr = %remote_addr,
timeout_ms,
"Initiating background Tor SOCKS5 connect"
);
let isolation_key = addr.to_string();
let pool = self.pool.lock().await;
if pool
.get(addr)
.is_some_and(|connection| !connection.io.is_closed())
{
return Ok(());
}
let mut connecting = self.connecting.lock().await;
if connecting.contains_key(addr) {
return Ok(());
}
let task = tokio::spawn(async move {
let socks_result = tokio::time::timeout(Duration::from_millis(timeout_ms), async {
match &tor_addr {
TorAddr::Onion(host, port) | TorAddr::ClearnetHostname(host, port) => {
Socks5Stream::connect_with_password(
proxy_addr.as_str(),
(host.as_str(), *port),
"fips",
&isolation_key,
)
.await
}
TorAddr::Clearnet(socket_addr) => {
Socks5Stream::connect_with_password(
proxy_addr.as_str(),
*socket_addr,
"fips",
&isolation_key,
)
.await
}
}
})
.await;
let stream = match socks_result {
Ok(Ok(socks_stream)) => socks_stream.into_inner(),
Ok(Err(e)) => {
debug!(
transport_id = %transport_id,
remote_addr = %remote_addr,
error = %e,
"Background Tor SOCKS5 connect failed"
);
return Err(TransportError::ConnectionRefused);
}
Err(_) => {
debug!(
transport_id = %transport_id,
remote_addr = %remote_addr,
"Background Tor SOCKS5 connect timed out"
);
return Err(TransportError::Timeout);
}
};
let std_stream = stream
.into_std()
.map_err(|e| TransportError::StartFailed(format!("into_std: {}", e)))?;
configure_socket(&std_stream, &config)?;
let stream = TcpStream::from_std(std_stream)
.map_err(|e| TransportError::StartFailed(format!("from_std: {}", e)))?;
Ok(stream)
});
connecting.insert(addr.clone(), ConnectingEntry { task });
drop(pool);
Ok(())
}
pub fn connection_state_sync(&self, addr: &TransportAddr) -> ConnectionState {
let mut pool = match self.pool.try_lock() {
Ok(pool) => pool,
Err(_) => return ConnectionState::Connecting,
};
if pool
.get(addr)
.is_some_and(|connection| !connection.io.is_closed())
{
if let Ok(mut connecting) = self.connecting.try_lock()
&& let Some(entry) = connecting.remove(addr)
{
entry.task.abort();
}
return ConnectionState::Connected;
}
let mut connecting = match self.connecting.try_lock() {
Ok(c) => c,
Err(_) => return ConnectionState::Connecting,
};
let entry = match connecting.get_mut(addr) {
Some(e) => e,
None => return ConnectionState::None,
};
if !entry.task.is_finished() {
return ConnectionState::Connecting;
}
let addr_clone = addr.clone();
let task = connecting.remove(&addr_clone).unwrap().task;
drop(connecting);
match task.now_or_never() {
Some(Ok(Ok(stream))) => {
self.promote_connection_in_pool(&mut pool, addr, stream);
ConnectionState::Connected
}
Some(Ok(Err(e))) => ConnectionState::Failed(format!("{}", e)),
Some(Err(e)) => {
ConnectionState::Failed(format!("task failed: {}", e))
}
None => {
ConnectionState::Connecting
}
}
}
fn promote_connection_in_pool(
&self,
pool: &mut HashMap<TransportAddr, TorConnection>,
addr: &TransportAddr,
stream: TcpStream,
) {
if let Some(existing) = pool.get(addr)
&& !existing.io.is_closed()
{
return;
}
if let Some(closed) = pool.remove(addr) {
closed.recv_task.abort();
record_pool_removed(&self.stats, &closed);
}
let (read_half, write_half) = stream.into_split();
let io = Arc::new(StreamConnectionIo::new(write_half));
let transport_id = self.transport_id;
let packet_tx = self.packet_tx.clone();
let recv_pool = self.pool.clone();
let recv_stats = self.stats.clone();
let remote_addr = addr.clone();
let recv_io = io.clone();
let recv_task = tokio::spawn(async move {
tor_receive_loop(
read_half,
TorReceiveContext {
transport_id,
remote_addr,
packet_tx,
pool: recv_pool,
stats: recv_stats,
first_frame_timeout: None,
direction: Direction::Outbound,
io: recv_io,
},
)
.await;
});
let conn = TorConnection {
io,
recv_task,
direction: Direction::Outbound,
};
pool.insert(addr.clone(), conn);
self.stats.record_pool_outbound_added();
self.stats.record_connection_established();
debug!(
transport_id = %self.transport_id,
remote_addr = %addr,
"Tor connection established (background connect)"
);
}
pub async fn close_connection_async(&self, addr: &TransportAddr) {
let mut pool = self.pool.lock().await;
if let Some(conn) = pool.remove(addr) {
conn.recv_task.abort();
match conn.direction {
Direction::Inbound => self.stats.record_pool_inbound_removed(),
Direction::Outbound => self.stats.record_pool_outbound_removed(),
}
debug!(
transport_id = %self.transport_id,
remote_addr = %addr,
direction = ?conn.direction,
"Tor connection closed"
);
}
}
}