use super::*;
use crate::transport::{Transport, packet_channel};
fn make_config() -> TorConfig {
TorConfig {
socks5_addr: Some("127.0.0.1:19050".to_string()),
..Default::default()
}
}
#[test]
fn test_parse_tor_addr_onion() {
let addr = TransportAddr::from_string("abcdef1234567890.onion:2121");
let tor_addr = parse_tor_addr(&addr).unwrap();
match tor_addr {
TorAddr::Onion(host, port) => {
assert_eq!(host, "abcdef1234567890.onion");
assert_eq!(port, 2121);
}
_ => panic!("expected Onion variant"),
}
}
#[test]
fn test_parse_tor_addr_clearnet() {
let addr = TransportAddr::from_string("192.168.1.1:8080");
let tor_addr = parse_tor_addr(&addr).unwrap();
match tor_addr {
TorAddr::Clearnet(socket_addr) => {
assert_eq!(
socket_addr,
"192.168.1.1:8080".parse::<SocketAddr>().unwrap()
);
}
_ => panic!("expected Clearnet variant"),
}
}
#[test]
fn test_parse_tor_addr_clearnet_hostname() {
let addr = TransportAddr::from_string("peer1.example.com:2121");
let tor_addr = parse_tor_addr(&addr).unwrap();
match tor_addr {
TorAddr::ClearnetHostname(host, port) => {
assert_eq!(host, "peer1.example.com");
assert_eq!(port, 2121);
}
_ => panic!("expected ClearnetHostname variant"),
}
}
#[test]
fn test_parse_tor_addr_invalid() {
let addr = TransportAddr::from_string("localhost:2121");
assert!(parse_tor_addr(&addr).is_err());
let addr = TransportAddr::from_string("not-a-valid-address");
assert!(parse_tor_addr(&addr).is_err());
let addr = TransportAddr::from_string("example.com:notaport");
assert!(parse_tor_addr(&addr).is_err());
}
#[test]
fn test_config_defaults() {
let config = TorConfig::default();
assert_eq!(config.mode(), "socks5");
assert_eq!(config.socks5_addr(), "127.0.0.1:9050");
assert_eq!(config.connect_timeout_ms(), 120000);
assert_eq!(config.mtu(), 1400);
assert_eq!(config.advertised_port(), 443);
}
#[test]
fn test_advertised_port_override() {
let config = TorConfig {
advertised_port: Some(9001),
..Default::default()
};
assert_eq!(config.advertised_port(), 9001);
}
#[test]
fn test_advert_address_round_trips_through_parser() {
let onion = "mwvj6q3pnsiaky7i6wg5s42xlfurt5uqr3qzckrlw2graa2ugcgwhiqd.onion";
let cfg = TorConfig::default();
let advertised = format!("{}:{}", onion, cfg.advertised_port());
let parsed = parse_tor_addr(&TransportAddr::from_string(&advertised)).unwrap();
match parsed {
TorAddr::Onion(host, port) => {
assert_eq!(host, onion);
assert_eq!(port, 443);
}
other => panic!("expected Onion variant, got {:?}", other),
}
assert!(parse_tor_addr(&TransportAddr::from_string(onion)).is_err());
}
#[tokio::test]
async fn test_start_stop() {
let (tx, _rx) = packet_channel(32);
let mut transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
transport.start_async().await.unwrap();
assert_eq!(transport.state(), TransportState::Up);
transport.stop_async().await.unwrap();
assert_eq!(transport.state(), TransportState::Down);
}
#[tokio::test]
async fn test_double_start_fails() {
let (tx, _rx) = packet_channel(32);
let mut transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
transport.start_async().await.unwrap();
assert!(transport.start_async().await.is_err());
}
#[tokio::test]
async fn test_stop_not_started_fails() {
let (tx, _rx) = packet_channel(32);
let mut transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
assert!(transport.stop_async().await.is_err());
}
#[tokio::test]
async fn test_send_not_started() {
let (tx, _rx) = packet_channel(32);
let transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
let addr = TransportAddr::from_string("127.0.0.1:2121");
let result = transport.send_async(&addr, &[0u8; 10]).await;
assert!(result.is_err());
}
#[test]
fn test_transport_type() {
let (tx, _rx) = packet_channel(32);
let transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
let tt = transport.transport_type();
assert_eq!(tt.name, "tor");
assert!(tt.connection_oriented);
assert!(tt.reliable);
}
#[test]
fn test_sync_methods_return_not_supported() {
let (tx, _rx) = packet_channel(32);
let mut transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
assert!(transport.start().is_err());
assert!(transport.stop().is_err());
let addr = TransportAddr::from_string("127.0.0.1:2121");
assert!(transport.send(&addr, &[0u8; 10]).is_err());
}
#[test]
fn test_accept_connections_false() {
let (tx, _rx) = packet_channel(32);
let transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
assert!(!transport.accept_connections());
}
#[test]
fn test_discover_returns_empty() {
let (tx, _rx) = packet_channel(32);
let transport = TorTransport::new(TransportId::new(1), None, make_config(), tx);
assert!(transport.discover().unwrap().is_empty());
}
#[tokio::test]
async fn test_invalid_socks5_addr_start_fails() {
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
socks5_addr: Some("not-a-socket-addr".to_string()),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
assert!(transport.start_async().await.is_err());
}
#[tokio::test]
async fn test_unsupported_mode_start_fails() {
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("embedded".to_string()),
socks5_addr: Some("127.0.0.1:9050".to_string()),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
assert!(transport.start_async().await.is_err());
}
use crate::config::TcpConfig;
use crate::transport::tcp::TcpTransport;
use mock_socks5::MockSocks5Server;
const MSG1_WIRE_SIZE: usize = 114;
const MSG1_PAYLOAD_LEN: u16 = (MSG1_WIRE_SIZE - 4) as u16;
fn build_msg1_frame() -> Vec<u8> {
let mut frame = vec![0xAA; MSG1_WIRE_SIZE];
frame[0] = 0x01; frame[1] = 0x00; frame[2..4].copy_from_slice(&MSG1_PAYLOAD_LEN.to_le_bytes());
frame
}
fn build_direct_fsp_frame(counter: u64, payload_len: u16) -> Vec<u8> {
let mut frame =
vec![
0u8;
crate::proto::fsp_wire::FSP_HEADER_SIZE + payload_len as usize + crate::noise::TAG_SIZE
];
frame[0] = crate::proto::fsp_wire::FSP_PHASE_ESTABLISHED;
frame[1] = crate::proto::fsp_wire::FSP_FLAG_DIRECT_TRANSPORT;
frame[2..4].copy_from_slice(&payload_len.to_le_bytes());
frame[4..12].copy_from_slice(&counter.to_le_bytes());
frame
}
async fn tcp_pair() -> (TcpStream, TcpStream) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let client = TcpStream::connect(listener.local_addr().unwrap())
.await
.unwrap();
let (server, _) = listener.accept().await.unwrap();
(client, server)
}
#[tokio::test]
async fn test_send_recv_via_socks5() {
let (dest_tx, mut dest_rx) = packet_channel(32);
let dest_config = TcpConfig {
bind_addr: Some("127.0.0.1:0".to_string()),
..Default::default()
};
let mut dest = TcpTransport::new(TransportId::new(100), None, dest_config, dest_tx);
dest.start_async().await.unwrap();
let dest_addr = dest.local_addr().unwrap();
let mock = MockSocks5Server::new(dest_addr).await.unwrap();
let proxy_addr = mock.addr();
let _proxy_handle = mock.spawn();
let (tor_tx, _tor_rx) = packet_channel(32);
let tor_config = TorConfig {
socks5_addr: Some(proxy_addr.to_string()),
mtu: Some(220),
..Default::default()
};
let mut tor = TorTransport::new(TransportId::new(200), None, tor_config, tor_tx);
tor.start_async().await.unwrap();
let direct = build_direct_fsp_frame(7, 300);
let frame = build_msg1_frame();
let target = TransportAddr::from_string(&dest_addr.to_string());
tor.send_async(&target, &frame).await.unwrap();
let received = tokio::time::timeout(Duration::from_secs(5), dest_rx.recv())
.await
.expect("timeout waiting for FMP frame")
.expect("channel closed");
assert_eq!(received.data.as_slice(), frame.as_slice());
let following = build_direct_fsp_frame(8, 7);
tor.send_async(&target, &direct).await.unwrap();
tor.send_async(&target, &following).await.unwrap();
let mut received_frames = Vec::new();
for _ in 0..2 {
let received = tokio::time::timeout(Duration::from_secs(5), dest_rx.recv())
.await
.expect("timeout waiting for packet")
.expect("channel closed");
received_frames.push(received.data.as_slice().to_vec());
}
assert_eq!(received_frames, [direct, following]);
tor.stop_async().await.unwrap();
dest.stop_async().await.unwrap();
}
#[tokio::test]
async fn inbound_receive_loop_bounds_an_incomplete_first_frame() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let _client = TcpStream::connect(listener.local_addr().unwrap())
.await
.unwrap();
let (server, peer_addr) = listener.accept().await.unwrap();
let (reader, writer) = server.into_split();
let io = Arc::new(StreamConnectionIo::new(writer));
let (packet_tx, _packet_rx) = packet_channel(1);
let pool = Arc::new(Mutex::new(HashMap::new()));
let stats = Arc::new(TorStats::new());
tokio::time::timeout(
Duration::from_secs(1),
tasks::tor_receive_loop(
reader,
tasks::TorReceiveContext {
transport_id: TransportId::new(201),
remote_addr: TransportAddr::from_string(&peer_addr.to_string()),
packet_tx,
pool: pool.clone(),
stats: stats.clone(),
first_frame_timeout: Some(Duration::from_millis(30)),
direction: Direction::Inbound,
io,
},
),
)
.await
.expect("incomplete inbound Tor frame should be bounded");
assert_eq!(stats.snapshot().recv_errors, 1);
assert!(pool.lock().await.is_empty());
}
#[tokio::test]
async fn stale_receive_cleanup_preserves_successor_generation_and_gauge() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let old_client = TcpStream::connect(listener.local_addr().unwrap())
.await
.unwrap();
let (old_server, _) = listener.accept().await.unwrap();
let (old_reader, old_writer) = old_server.into_split();
let old_io = Arc::new(StreamConnectionIo::new(old_writer));
let successor_client = TcpStream::connect(listener.local_addr().unwrap())
.await
.unwrap();
let (successor_server, _) = listener.accept().await.unwrap();
let (_successor_reader, successor_writer) = successor_server.into_split();
let successor_io = Arc::new(StreamConnectionIo::new(successor_writer));
let successor_task = tokio::spawn(std::future::pending::<()>());
let addr = TransportAddr::from_string("stale-tor-generation.test:443");
let pool = Arc::new(Mutex::new(HashMap::new()));
pool.lock().await.insert(
addr.clone(),
TorConnection {
io: successor_io.clone(),
recv_task: successor_task,
direction: Direction::Outbound,
},
);
let stats = Arc::new(TorStats::new());
stats.record_pool_outbound_added();
let (packet_tx, _packet_rx) = packet_channel(1);
drop(old_client);
tasks::tor_receive_loop(
old_reader,
tasks::TorReceiveContext {
transport_id: TransportId::new(202),
remote_addr: addr.clone(),
packet_tx,
pool: pool.clone(),
stats: stats.clone(),
first_frame_timeout: None,
direction: Direction::Inbound,
io: old_io,
},
)
.await;
let mut pool = pool.lock().await;
let successor = pool.get(&addr).expect("stale cleanup removed successor");
assert!(Arc::ptr_eq(&successor.io, &successor_io));
assert_eq!(stats.snapshot().pool_inbound, 0);
assert_eq!(stats.snapshot().pool_outbound, 1);
let successor = pool.remove(&addr).unwrap();
successor.recv_task.abort();
drop(pool);
drop(successor_client);
}
#[tokio::test]
async fn background_promotion_keeps_open_and_replaces_closed_generation_once() {
let (packet_tx, _packet_rx) = packet_channel(1);
let transport = TorTransport::new(TransportId::new(203), None, make_config(), packet_tx);
let addr = TransportAddr::from_string("promotion-tor-generation.test:443");
let (_existing_peer, existing_stream) = tcp_pair().await;
let (_existing_reader, existing_writer) = existing_stream.into_split();
let existing_io = Arc::new(StreamConnectionIo::new(existing_writer));
transport.pool.lock().await.insert(
addr.clone(),
TorConnection {
io: existing_io.clone(),
recv_task: tokio::spawn(std::future::pending::<()>()),
direction: Direction::Outbound,
},
);
transport.stats.record_pool_outbound_added();
let (_discarded_peer, discarded_candidate) = tcp_pair().await;
{
let mut pool = transport.pool.lock().await;
transport.promote_connection_in_pool(&mut pool, &addr, discarded_candidate);
assert!(Arc::ptr_eq(&pool.get(&addr).unwrap().io, &existing_io));
}
assert_eq!(transport.stats.snapshot().pool_outbound, 1);
assert_eq!(
transport.connection_state_sync(&addr),
ConnectionState::Connected
);
existing_io.mark_closed();
let (_replacement_peer, replacement_candidate) = tcp_pair().await;
{
let mut pool = transport.pool.lock().await;
transport.promote_connection_in_pool(&mut pool, &addr, replacement_candidate);
let replacement = &pool.get(&addr).unwrap().io;
assert!(!Arc::ptr_eq(replacement, &existing_io));
assert!(!replacement.is_closed());
}
assert_eq!(transport.stats.snapshot().pool_outbound, 1);
assert_eq!(transport.stats.snapshot().connections_established, 1);
assert_eq!(
transport.connection_state_sync(&addr),
ConnectionState::Connected
);
transport.close_connection_async(&addr).await;
assert_eq!(transport.stats.snapshot().pool_outbound, 0);
}
#[tokio::test]
async fn test_socks5_proxy_down() {
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
socks5_addr: Some("127.0.0.1:19999".to_string()),
connect_timeout_ms: Some(2000),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
transport.start_async().await.unwrap();
let addr = TransportAddr::from_string("192.168.1.1:2121");
let result = transport.send_async(&addr, &build_msg1_frame()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_connect_timeout() {
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
socks5_addr: Some("192.0.2.1:9050".to_string()),
connect_timeout_ms: Some(500),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
transport.start_async().await.unwrap();
let addr = TransportAddr::from_string("10.0.0.1:2121");
let result = transport.send_async(&addr, &build_msg1_frame()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn completed_background_dial_waits_for_pool_then_promotes_once() {
let (packet_tx, _packet_rx) = packet_channel(1);
let transport = TorTransport::new(TransportId::new(204), None, make_config(), packet_tx);
let remote = TransportAddr::from_string("completed-tor-background.test:443");
let (_peer, candidate) = tcp_pair().await;
let task = tokio::spawn(async move { Ok::<_, TransportError>(candidate) });
while !task.is_finished() {
tokio::task::yield_now().await;
}
transport
.connecting
.lock()
.await
.insert(remote.clone(), ConnectingEntry { task });
let occupied_pool = transport.pool.lock().await;
assert_eq!(
transport.connection_state_sync(&remote),
ConnectionState::Connecting
);
assert_eq!(transport.connecting.lock().await.len(), 1);
drop(occupied_pool);
assert_eq!(
transport.connection_state_sync(&remote),
ConnectionState::Connected
);
assert!(transport.connecting.lock().await.is_empty());
assert_eq!(transport.pool.lock().await.len(), 1);
assert_eq!(transport.stats.snapshot().pool_outbound, 1);
transport.close_connection_async(&remote).await;
assert_eq!(transport.stats.snapshot().pool_outbound, 0);
}
#[tokio::test]
async fn test_close_connection() {
let (dest_tx, _dest_rx) = packet_channel(32);
let dest_config = TcpConfig {
bind_addr: Some("127.0.0.1:0".to_string()),
..Default::default()
};
let mut dest = TcpTransport::new(TransportId::new(100), None, dest_config, dest_tx);
dest.start_async().await.unwrap();
let dest_addr = dest.local_addr().unwrap();
let mock = MockSocks5Server::new(dest_addr).await.unwrap();
let proxy_addr = mock.addr();
let _proxy_handle = mock.spawn();
let (tor_tx, _tor_rx) = packet_channel(32);
let tor_config = TorConfig {
socks5_addr: Some(proxy_addr.to_string()),
..Default::default()
};
let mut tor = TorTransport::new(TransportId::new(200), None, tor_config, tor_tx);
tor.start_async().await.unwrap();
let target = TransportAddr::from_string(&dest_addr.to_string());
tor.send_async(&target, &build_msg1_frame()).await.unwrap();
{
let pool = tor.pool.lock().await;
assert_eq!(pool.len(), 1);
}
assert_eq!(tor.stats.snapshot().pool_outbound, 1);
assert_eq!(tor.stats.snapshot().pool_inbound, 0);
tor.close_connection_async(&target).await;
{
let pool = tor.pool.lock().await;
assert_eq!(pool.len(), 0);
}
assert_eq!(tor.stats.snapshot().pool_outbound, 0);
tor.stop_async().await.unwrap();
dest.stop_async().await.unwrap();
}
use mock_control::MockTorControlServer;
#[tokio::test]
async fn test_control_port_start_stop() {
let mock = MockTorControlServer::start().await;
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("control_port".to_string()),
socks5_addr: Some("127.0.0.1:19050".to_string()),
control_addr: Some(mock.addr().to_string()),
control_auth: Some("password:testpass".to_string()),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
transport.start_async().await.unwrap();
assert_eq!(transport.state(), TransportState::Up);
assert!(transport.onion_address().is_none());
assert!(!transport.accept_connections());
transport.stop_async().await.unwrap();
}
#[tokio::test]
async fn test_config_defaults_phase2() {
let config = TorConfig::default();
assert_eq!(config.control_addr(), "/run/tor/control");
assert_eq!(config.control_auth(), "cookie");
assert_eq!(config.cookie_path(), "/var/run/tor/control.authcookie");
assert_eq!(config.max_inbound_connections(), 64);
}
use crate::config::DirectoryServiceConfig;
use tempfile::TempDir;
#[test]
fn test_directory_service_config_defaults() {
let config = DirectoryServiceConfig::default();
assert_eq!(
config.hostname_file(),
"/var/lib/tor/fips_onion_service/hostname"
);
assert_eq!(config.bind_addr(), "127.0.0.1:8443");
}
#[tokio::test]
async fn test_directory_mode_start_stop() {
let dir = TempDir::new().unwrap();
let hostname_path = dir.path().join("hostname");
std::fs::write(
&hostname_path,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2.onion\n",
)
.unwrap();
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("directory".to_string()),
socks5_addr: Some("127.0.0.1:19050".to_string()),
directory_service: Some(DirectoryServiceConfig {
hostname_file: Some(hostname_path.to_str().unwrap().to_string()),
bind_addr: Some("127.0.0.1:0".to_string()),
}),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
transport.start_async().await.unwrap();
assert_eq!(transport.state(), TransportState::Up);
assert_eq!(
transport.onion_address(),
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2.onion"),
);
assert!(transport.accept_connections());
transport.stop_async().await.unwrap();
assert_eq!(transport.state(), TransportState::Down);
}
#[tokio::test]
async fn test_directory_mode_missing_hostname_file() {
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("directory".to_string()),
socks5_addr: Some("127.0.0.1:19050".to_string()),
directory_service: Some(DirectoryServiceConfig {
hostname_file: Some("/nonexistent/hostname".to_string()),
bind_addr: Some("127.0.0.1:0".to_string()),
}),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
let result = transport.start_async().await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("hostname"));
}
#[tokio::test]
async fn test_directory_mode_invalid_hostname() {
let dir = TempDir::new().unwrap();
let hostname_path = dir.path().join("hostname");
std::fs::write(&hostname_path, "not-an-onion-address\n").unwrap();
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("directory".to_string()),
socks5_addr: Some("127.0.0.1:19050".to_string()),
directory_service: Some(DirectoryServiceConfig {
hostname_file: Some(hostname_path.to_str().unwrap().to_string()),
bind_addr: Some("127.0.0.1:0".to_string()),
}),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
let result = transport.start_async().await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("invalid onion address"));
}
#[tokio::test]
async fn test_directory_mode_accept_inbound() {
let dir = TempDir::new().unwrap();
let hostname_path = dir.path().join("hostname");
std::fs::write(
&hostname_path,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2.onion\n",
)
.unwrap();
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("directory".to_string()),
socks5_addr: Some("127.0.0.1:19050".to_string()),
directory_service: Some(DirectoryServiceConfig {
hostname_file: Some(hostname_path.to_str().unwrap().to_string()),
bind_addr: Some("127.0.0.1:0".to_string()),
}),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
transport.start_async().await.unwrap();
assert!(transport.accept_connections());
transport.stop_async().await.unwrap();
}
#[tokio::test]
async fn test_socks5_mode_rejects_directory_service_config() {
let (tx, _rx) = packet_channel(32);
let config = TorConfig {
mode: Some("socks5".to_string()),
socks5_addr: Some("127.0.0.1:9050".to_string()),
directory_service: Some(DirectoryServiceConfig::default()),
..Default::default()
};
let mut transport = TorTransport::new(TransportId::new(1), None, config, tx);
let result = transport.start_async().await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("directory"));
}