use super::common::*;
use super::*;
use crate::onion::OnionServiceName;
#[tokio::test]
async fn test_onion_exit_lookup_uses_dedicated_exit_registry() -> Result<()> {
let processor = prepare_processor().await;
let relay_only = prepare_processor().await;
let exit = prepare_processor().await;
let relay_descriptor = relay_only.online_node_descriptor_at(get_epoch_ms())?;
let exit_descriptor = onion_exit_descriptor_for_processor(&exit, "web", get_epoch_ms())?;
processor
.storage_store(Processor::online_node_registry_entry(vec![
relay_descriptor,
])?)
.await?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![
exit_descriptor.clone()
])?)
.await?;
let exits = processor.lookup_onion_exits("web", false).await?;
assert_eq!(exits, vec![exit_descriptor]);
assert!(exits.iter().all(OnionExitDescriptor::verify_signature));
assert!(!exits
.iter()
.any(|descriptor| descriptor.did == relay_only.did()));
Ok(())
}
#[tokio::test]
async fn test_onion_exit_lookup_preserves_distinct_services_for_same_did() -> Result<()> {
let processor = prepare_processor().await;
let exit = prepare_processor().await;
let now_ms = get_epoch_ms();
let older_web = onion_exit_descriptor_for_processor(&exit, "web", now_ms)?;
let newer_ssh = onion_exit_descriptor_for_processor(&exit, "ssh", now_ms + 1)?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![
older_web, newer_ssh,
])?)
.await?;
assert_eq!(processor.lookup_onion_exits("web", false).await?.len(), 1);
assert_eq!(processor.lookup_onion_exits("ssh", false).await?.len(), 1);
assert_eq!(processor.lookup_onion_exits("", false).await?.len(), 2);
Ok(())
}
#[tokio::test]
async fn test_onion_route_builder_uses_presence_relays_without_exit_descriptor() -> Result<()> {
let processor = prepare_processor().await;
let first_relay = prepare_processor().await;
let second_relay = prepare_processor().await;
let exit = prepare_processor().await;
let now_ms = get_epoch_ms();
let exit_descriptor = onion_exit_descriptor_for_processor(&exit, "web", now_ms)?;
processor
.storage_store(Processor::online_node_registry_entry(vec![
online_relay_descriptor_for_processor(&first_relay, now_ms)?,
online_relay_descriptor_for_processor(&second_relay, now_ms)?,
exit.online_node_descriptor_at(now_ms)?,
])?)
.await?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![
exit_descriptor.clone()
])?)
.await?;
let route = processor
.build_onion_route("web".to_string(), 3, false)
.await?;
assert_eq!(route.hops().len(), 3);
assert_eq!(route.exit_did(), exit.did());
assert_eq!(route.hops().last().copied(), Some(exit.did()));
assert!(route.hops().contains(&first_relay.did()));
assert!(route.hops().contains(&second_relay.did()));
assert_eq!(route.exit(), &exit_descriptor);
Ok(())
}
#[tokio::test]
async fn test_onion_proxy_route_uses_protocol_service_class() -> Result<()> {
let processor = prepare_processor().await;
let tcp_exit = prepare_processor().await;
let https_exit = prepare_processor().await;
let now_ms = get_epoch_ms();
let tcp_exit_descriptor = onion_exit_descriptor_for_processor(&tcp_exit, "tcp", now_ms)?;
let https_exit_descriptor = onion_exit_descriptor_for_processor(&https_exit, "https", now_ms)?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![
tcp_exit_descriptor,
https_exit_descriptor,
])?)
.await?;
let target = OnionProxyTarget::parse_authority("example.com:443")?;
let tcp_route = processor
.build_onion_proxy_route(OnionProxyConfig::tcp_connect(1, false), target.clone())
.await?;
let https_route = processor
.build_onion_proxy_route(OnionProxyConfig::https_proxy(1, false), target)
.await?;
assert_eq!(tcp_route.exit_service(), "tcp");
assert_eq!(tcp_route.exit_did(), tcp_exit.did());
assert_eq!(https_route.exit_service(), "https");
assert_eq!(https_route.exit_did(), https_exit.did());
Ok(())
}
#[tokio::test]
async fn test_onion_route_accepts_https_service_over_tcp_transport() -> Result<()> {
let processor = prepare_processor().await;
let exit = prepare_processor().await;
let descriptor = onion_exit_descriptor_for_processor_with_service(
&exit,
OnionExitService::new("https", OnionExitTransport::Tcp)?,
get_epoch_ms(),
{
let mut policy = onion_policy(&["example.com:443"], &[])?;
policy.max_circuits = 8;
policy.max_streams_per_circuit = 2;
policy.max_bytes_per_minute = 4096;
policy
},
)?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![descriptor])?)
.await?;
let route = processor
.build_onion_route("https".to_string(), 1, false)
.await?;
assert_eq!(route.exit_did(), exit.did());
assert_eq!(route.service(), "https");
Ok(())
}
#[tokio::test]
async fn test_onion_proxy_route_accepts_https_service_over_tcp_transport() -> Result<()> {
let processor = prepare_processor().await;
let exit = prepare_processor().await;
let descriptor = onion_exit_descriptor_for_processor_with_service(
&exit,
OnionExitService::new("https", OnionExitTransport::Tcp)?,
get_epoch_ms(),
{
let mut policy = onion_policy(&["example.com:443"], &[])?;
policy.max_circuits = 8;
policy.max_streams_per_circuit = 2;
policy.max_bytes_per_minute = 4096;
policy
},
)?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![descriptor])?)
.await?;
let target = OnionProxyTarget::parse_authority("example.com:443")?;
let route = processor
.build_onion_proxy_route(OnionProxyConfig::https_proxy(1, false), target)
.await?;
assert_eq!(route.exit_did(), exit.did());
assert_eq!(route.exit_service(), "https");
assert_eq!(route.exit_transport(), OnionExitTransport::Tcp);
Ok(())
}
#[tokio::test]
async fn test_tcp_connect_route_rejects_browser_https_exit_descriptor() -> Result<()> {
let processor = prepare_processor().await;
let browser_exit = prepare_processor().await;
let descriptor = onion_exit_descriptor_for_processor_with_node_type_service(
&browser_exit,
OnlineNodeType::Browser,
OnionExitService::https(),
get_epoch_ms(),
{
let mut policy = onion_policy(&["example.com:443"], &[])?;
policy.max_circuits = 8;
policy.max_streams_per_circuit = 2;
policy.max_bytes_per_minute = 4096;
policy
},
)?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![descriptor])?)
.await?;
let target = OnionProxyTarget::parse_authority("example.com:443")?;
let error = processor
.build_onion_proxy_route(
OnionProxyConfig::tcp_connect_service(OnionServiceName::https(), 1, false)?,
target,
)
.await
.err()
.ok_or_else(|| Error::InvalidConfig("expected route failure".to_string()))?;
assert!(matches!(
error,
Error::OnionRouteError(OnionRouteError::NoExitForProxyProtocol { service, protocol })
if service == "https" && protocol == "tcp-connect"
));
Ok(())
}
#[tokio::test]
async fn test_onion_proxy_route_filters_exits_by_target_policy() -> Result<()> {
let processor = prepare_processor().await;
let allowed_exit = prepare_processor().await;
let denied_exit = prepare_processor().await;
let now_ms = get_epoch_ms();
let allowed_descriptor =
onion_exit_descriptor_for_processor_with_policy(&allowed_exit, "https", now_ms, {
let mut policy = onion_policy(&["example.com:443"], &[])?;
policy.max_circuits = 8;
policy.max_streams_per_circuit = 2;
policy.max_bytes_per_minute = 4096;
policy
})?;
let denied_descriptor =
onion_exit_descriptor_for_processor_with_policy(&denied_exit, "https", now_ms, {
let mut policy = onion_policy(&["example.com:443"], &["example.com:443"])?;
policy.max_circuits = 8;
policy.max_streams_per_circuit = 2;
policy.max_bytes_per_minute = 4096;
policy
})?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![
denied_descriptor,
allowed_descriptor,
])?)
.await?;
let target = OnionProxyTarget::parse_authority("example.com:443")?;
let route = processor
.build_onion_proxy_route(OnionProxyConfig::https_proxy(1, false), target)
.await?;
assert_eq!(route.exit_did(), allowed_exit.did());
Ok(())
}
#[tokio::test]
async fn test_onion_proxy_route_reports_policy_denied_target() -> Result<()> {
let processor = prepare_processor().await;
let denied_exit = prepare_processor().await;
let now_ms = get_epoch_ms();
let denied_descriptor =
onion_exit_descriptor_for_processor_with_policy(&denied_exit, "https", now_ms, {
let mut policy = onion_policy(&["other.example.com:443"], &[])?;
policy.max_circuits = 8;
policy.max_streams_per_circuit = 2;
policy.max_bytes_per_minute = 4096;
policy
})?;
processor
.storage_store(Processor::onion_exit_registry_entry(vec![
denied_descriptor,
])?)
.await?;
let target = OnionProxyTarget::parse_authority("example.com:443")?;
let error = processor
.build_onion_proxy_route(OnionProxyConfig::https_proxy(1, false), target)
.await
.err()
.ok_or_else(|| Error::InvalidConfig("expected route failure".to_string()))?;
assert!(matches!(
error,
Error::OnionRouteError(OnionRouteError::NoExitAllowsTarget { service, target })
if service == "https" && target == "example.com:443"
));
Ok(())
}