#![allow(dead_code)]
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use arti_client::config::TorClientConfigBuilder;
use arti_client::config::onion_service::OnionServiceConfigBuilder;
use arti_client::{DataStream, TorClient as ArtiClient};
use futures::StreamExt;
use parking_lot::RwLock;
use safelog::DisplayRedacted;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::sync::mpsc;
use tor_cell::relaycell::msg::Connected;
use tor_config::ExplicitOrAuto;
use tor_guardmgr::VanguardMode;
use tor_hsservice::RunningOnionService;
use tor_hsservice::config::TokenBucketConfig;
use tor_rtcompat::PreferredRuntime;
use tracing::{debug, error, info, warn};
use crate::error::{Error, Result};
pub use tor_hscrypto::pk::HsClientDescEncKey;
#[derive(Debug, Clone)]
pub struct OnionServiceConfig {
pub nickname: String,
pub port: u16,
pub forward_to: Option<String>,
pub key_dir: Option<PathBuf>,
pub vanguard_mode: Option<VanguardMode>,
pub enable_pow: bool,
pub pow_queue_depth: Option<usize>,
pub max_streams_per_circuit: u32,
pub rate_limit_at_intro: Option<(f64, u32)>,
pub num_intro_points: u8,
pub authorized_clients: Vec<(String, HsClientDescEncKey)>,
pub authorized_clients_dir: Option<PathBuf>,
}
impl Default for OnionServiceConfig {
fn default() -> Self {
Self {
nickname: "hypertor-service".into(),
port: 80,
forward_to: None,
key_dir: None,
vanguard_mode: None, enable_pow: false,
pow_queue_depth: None,
max_streams_per_circuit: 65535,
rate_limit_at_intro: None,
num_intro_points: 3,
authorized_clients: Vec::new(),
authorized_clients_dir: None,
}
}
}
impl OnionServiceConfig {
pub fn new(nickname: impl Into<String>) -> Self {
Self {
nickname: nickname.into(),
..Default::default()
}
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn forward_to(mut self, addr: impl Into<String>) -> Self {
self.forward_to = Some(addr.into());
self
}
pub fn key_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.key_dir = Some(path.into());
self
}
pub fn with_pow(mut self) -> Self {
self.enable_pow = true;
self
}
pub fn pow_queue_depth(mut self, depth: usize) -> Self {
self.pow_queue_depth = Some(depth);
self
}
pub fn max_streams_per_circuit(mut self, n: u32) -> Self {
self.max_streams_per_circuit = n;
self
}
pub fn rate_limit_at_intro(mut self, rate: f64, burst: u32) -> Self {
self.rate_limit_at_intro = Some((rate, burst));
self
}
pub fn num_intro_points(mut self, n: u8) -> Self {
self.num_intro_points = n.clamp(1, 20);
self
}
pub fn high_security() -> Self {
Self {
nickname: "secure-service".into(),
port: 443,
forward_to: None,
key_dir: None,
vanguard_mode: Some(VanguardMode::Full), enable_pow: true,
pow_queue_depth: Some(16000), max_streams_per_circuit: 100,
rate_limit_at_intro: Some((10.0, 20)),
num_intro_points: 5,
authorized_clients: Vec::new(),
authorized_clients_dir: None,
}
}
pub fn vanguards(mut self, mode: VanguardMode) -> Self {
self.vanguard_mode = Some(mode);
self
}
pub fn vanguards_lite(self) -> Self {
self.vanguards(VanguardMode::Lite)
}
pub fn vanguards_full(self) -> Self {
self.vanguards(VanguardMode::Full)
}
pub fn authorize_client(
mut self,
nickname: impl Into<String>,
key: HsClientDescEncKey,
) -> Self {
self.authorized_clients.push((nickname.into(), key));
self
}
pub fn authorized_clients_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.authorized_clients_dir = Some(path.into());
self
}
pub fn has_client_auth(&self) -> bool {
!self.authorized_clients.is_empty() || self.authorized_clients_dir.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceState {
Idle,
Bootstrapping,
BuildingCircuits,
Publishing,
Running,
ShuttingDown,
Stopped,
Failed,
}
impl std::fmt::Display for ServiceState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Idle => write!(f, "Idle"),
Self::Bootstrapping => write!(f, "Bootstrapping"),
Self::BuildingCircuits => write!(f, "Building Circuits"),
Self::Publishing => write!(f, "Publishing"),
Self::Running => write!(f, "Running"),
Self::ShuttingDown => write!(f, "Shutting Down"),
Self::Stopped => write!(f, "Stopped"),
Self::Failed => write!(f, "Failed"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ServiceStats {
pub connections: u64,
pub active: u64,
pub bytes_rx: u64,
pub bytes_tx: u64,
pub started_at: Option<Instant>,
}
impl ServiceStats {
pub fn uptime(&self) -> Duration {
self.started_at.map(|s| s.elapsed()).unwrap_or_default()
}
}
pub struct OnionStream {
id: u64,
inner: DataStream,
connected_at: Instant,
bytes_rx: AtomicU64,
bytes_tx: AtomicU64,
}
impl std::fmt::Debug for OnionStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OnionStream")
.field("id", &self.id)
.field("connected_at", &self.connected_at)
.field("bytes_rx", &self.bytes_rx.load(Ordering::Relaxed))
.field("bytes_tx", &self.bytes_tx.load(Ordering::Relaxed))
.finish()
}
}
impl OnionStream {
fn from_data_stream(id: u64, data_stream: DataStream) -> Self {
Self {
id,
inner: data_stream,
connected_at: Instant::now(),
bytes_rx: AtomicU64::new(0),
bytes_tx: AtomicU64::new(0),
}
}
pub fn id(&self) -> u64 {
self.id
}
pub fn age(&self) -> Duration {
self.connected_at.elapsed()
}
pub fn bytes_received(&self) -> u64 {
self.bytes_rx.load(Ordering::Relaxed)
}
pub fn bytes_sent(&self) -> u64 {
self.bytes_tx.load(Ordering::Relaxed)
}
pub fn inner_mut(&mut self) -> &mut DataStream {
&mut self.inner
}
pub fn into_inner(self) -> DataStream {
self.inner
}
}
impl AsyncRead for OnionStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let before = buf.filled().len();
let result = Pin::new(&mut self.inner).poll_read(cx, buf);
if let Poll::Ready(Ok(())) = &result {
let read = buf.filled().len() - before;
self.bytes_rx.fetch_add(read as u64, Ordering::Relaxed);
}
result
}
}
impl AsyncWrite for OnionStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
let result = Pin::new(&mut self.inner).poll_write(cx, buf);
if let Poll::Ready(Ok(written)) = &result {
self.bytes_tx.fetch_add(*written as u64, Ordering::Relaxed);
}
result
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
pub struct OnionService {
config: OnionServiceConfig,
state: Arc<RwLock<ServiceState>>,
tor_client: Option<Arc<ArtiClient<PreferredRuntime>>>,
running_service: Option<Arc<RunningOnionService>>,
stream_rx: Option<mpsc::Receiver<OnionStream>>,
address: Arc<RwLock<Option<String>>>,
stats: Arc<RwLock<ServiceStats>>,
running: Arc<AtomicBool>,
}
impl OnionService {
pub fn new(config: OnionServiceConfig) -> Self {
Self {
config,
state: Arc::new(RwLock::new(ServiceState::Idle)),
tor_client: None,
running_service: None,
stream_rx: None,
address: Arc::new(RwLock::new(None)),
stats: Arc::new(RwLock::new(ServiceStats::default())),
running: Arc::new(AtomicBool::new(false)),
}
}
pub fn state(&self) -> ServiceState {
*self.state.read()
}
pub fn address(&self) -> Option<String> {
self.address.read().clone()
}
pub fn stats(&self) -> ServiceStats {
self.stats.read().clone()
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
fn set_state(&self, new_state: ServiceState) {
*self.state.write() = new_state;
}
pub async fn start(&mut self) -> Result<String> {
info!("Starting onion service: {}", self.config.nickname);
self.set_state(ServiceState::Bootstrapping);
self.running.store(true, Ordering::SeqCst);
self.stats.write().started_at = Some(Instant::now());
debug!("Bootstrapping Tor client...");
let mut tor_config_builder = TorClientConfigBuilder::default();
if let Some(mode) = self.config.vanguard_mode {
tor_config_builder
.vanguards()
.mode(ExplicitOrAuto::Explicit(mode));
info!("Vanguards enabled: {:?}", mode);
}
let tor_config = tor_config_builder.build().map_err(|e| Error::Config {
message: format!("Failed to build Tor config: {}", e),
})?;
let tor_client = ArtiClient::create_bootstrapped(tor_config)
.await
.map_err(|e| Error::bootstrap_with_source("Failed to bootstrap Tor", e))?;
let tor_client = Arc::new(tor_client);
self.tor_client = Some(tor_client.clone());
info!("Tor client bootstrapped successfully");
self.set_state(ServiceState::BuildingCircuits);
debug!("Building onion service configuration...");
let nickname = self
.config
.nickname
.clone()
.try_into()
.map_err(|_| Error::Config {
message: "Invalid service nickname".into(),
})?;
let mut svc_config_builder = OnionServiceConfigBuilder::default();
svc_config_builder.nickname(nickname);
svc_config_builder.enable_pow(self.config.enable_pow);
if self.config.enable_pow {
debug!("PoW enabled");
if let Some(depth) = self.config.pow_queue_depth {
svc_config_builder.pow_rend_queue_depth(depth);
debug!("PoW queue depth: {}", depth);
}
}
svc_config_builder.max_concurrent_streams_per_circuit(self.config.max_streams_per_circuit);
debug!(
"Max streams per circuit: {}",
self.config.max_streams_per_circuit
);
if let Some((rate, burst)) = self.config.rate_limit_at_intro {
let token_bucket = TokenBucketConfig::new(rate as u32, burst);
svc_config_builder.rate_limit_at_intro(Some(token_bucket));
debug!("Rate limit at intro: {} req/s, burst {}", rate, burst);
}
svc_config_builder.num_intro_points(self.config.num_intro_points);
debug!("Intro points: {}", self.config.num_intro_points);
if self.config.has_client_auth() {
let rd = svc_config_builder.restricted_discovery();
rd.enabled(true);
if !self.config.authorized_clients.is_empty() {
for (nickname, key) in &self.config.authorized_clients {
rd.static_keys().access().push((
nickname.parse().map_err(|e| Error::Config {
message: format!("Invalid client nickname '{}': {}", nickname, e),
})?,
key.clone(),
));
}
debug!(
"Authorized {} clients via static keys",
self.config.authorized_clients.len()
);
}
if self.config.authorized_clients_dir.is_some() {
warn!(
"authorized_clients_dir not yet implemented - use authorized_clients instead"
);
}
info!(
"Restricted discovery enabled with {} authorized clients",
self.config.authorized_clients.len()
);
}
let svc_config = svc_config_builder.build().map_err(|e| Error::Config {
message: format!("Invalid onion service config: {}", e),
})?;
self.set_state(ServiceState::Publishing);
info!(
"Launching onion service with hardening: pow={}, max_streams={}, intro_points={}",
self.config.enable_pow,
self.config.max_streams_per_circuit,
self.config.num_intro_points
);
let (running_svc, rend_requests) = tor_client
.launch_onion_service(svc_config)
.map_err(|e| Error::Protocol(format!("Failed to launch onion service: {}", e)))?
.ok_or_else(|| {
Error::Protocol("Onion services not supported in this configuration".into())
})?;
let hsid = running_svc
.onion_address()
.ok_or_else(|| Error::Protocol("Service has no onion address yet".into()))?;
let address = hsid.display_unredacted().to_string();
*self.address.write() = Some(address.clone());
info!("Onion service address: {}", address);
self.running_service = Some(running_svc);
let (stream_tx, stream_rx) = mpsc::channel::<OnionStream>(256);
self.stream_rx = Some(stream_rx);
let running = self.running.clone();
let stats = self.stats.clone();
let next_stream_id = Arc::new(AtomicU64::new(1));
let next_id = next_stream_id.clone();
tokio::spawn(async move {
let stream_requests = tor_hsservice::handle_rend_requests(rend_requests);
tokio::pin!(stream_requests);
while running.load(Ordering::SeqCst) {
tokio::select! {
Some(stream_req) = stream_requests.next() => {
let id = next_id.fetch_add(1, Ordering::SeqCst);
debug!("Received StreamRequest #{}", id);
match stream_req.accept(Connected::new_empty()).await {
Ok(data_stream) => {
info!("Accepted stream #{} - got real DataStream", id);
let onion_stream = OnionStream::from_data_stream(id, data_stream);
stats.write().connections += 1;
stats.write().active += 1;
if stream_tx.send(onion_stream).await.is_err() {
warn!("Stream receiver dropped, stopping handler");
break;
}
}
Err(e) => {
error!("Failed to accept stream #{}: {}", id, e);
}
}
}
_ = tokio::time::sleep(Duration::from_millis(100)) => {
if !running.load(Ordering::SeqCst) {
break;
}
}
}
}
debug!("Rendezvous handler stopped");
});
self.set_state(ServiceState::Running);
Ok(address)
}
pub async fn accept(&mut self) -> Option<OnionStream> {
if let Some(ref mut rx) = self.stream_rx {
rx.recv().await
} else {
None
}
}
pub async fn stop(&mut self) -> Result<()> {
info!("Stopping onion service...");
self.set_state(ServiceState::ShuttingDown);
self.running.store(false, Ordering::SeqCst);
self.running_service = None;
self.stream_rx = None;
self.tor_client = None;
self.set_state(ServiceState::Stopped);
info!("Onion service stopped");
Ok(())
}
}
impl Default for OnionService {
fn default() -> Self {
Self::new(OnionServiceConfig::default())
}
}
impl Drop for OnionService {
fn drop(&mut self) {
self.running.store(false, Ordering::SeqCst);
}
}
impl std::fmt::Debug for OnionService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OnionService")
.field("config", &self.config)
.field("state", &self.state())
.field("address", &self.address())
.finish()
}
}
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretKey([u8; 32]);
impl SecretKey {
pub fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
false
}
}
impl std::fmt::Debug for SecretKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretKey")
.field("len", &32)
.field("data", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ClientAuthKey {
pub client_id: String,
pub public_key: [u8; 32],
pub expires: Option<Instant>,
}
impl ClientAuthKey {
pub fn new(client_id: impl Into<String>, public_key: [u8; 32]) -> Self {
Self {
client_id: client_id.into(),
public_key,
expires: None,
}
}
pub fn with_expiry(mut self, duration: Duration) -> Self {
self.expires = Some(Instant::now() + duration);
self
}
pub fn is_expired(&self) -> bool {
self.expires.map(|e| Instant::now() > e).unwrap_or(false)
}
pub fn generate(client_id: impl Into<String>) -> (Self, SecretKey) {
use rand::RngCore;
let mut secret_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut secret_bytes);
let mut public_key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut public_key);
let secret_key = SecretKey::new(secret_bytes);
secret_bytes.zeroize();
(Self::new(client_id, public_key), secret_key)
}
}
#[derive(Debug, Clone)]
pub struct RateLimit {
pub requests_per_second: u32,
pub burst: u32,
}
impl Default for RateLimit {
fn default() -> Self {
Self {
requests_per_second: 100,
burst: 200,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClientAuthMode {
#[default]
None,
Basic,
Stealth,
}
#[derive(Debug, Clone)]
pub enum ServiceEvent {
StateChanged {
old: ServiceState,
new: ServiceState,
},
ConnectionAccepted {
stream_id: u64,
},
ConnectionClosed {
stream_id: u64,
reason: Option<String>,
},
DescriptorUploaded {
hsdir_count: u32,
},
IntroPointChanged {
active_count: u32,
},
Error {
message: String,
},
}
pub struct OnionServiceWithEvents {
inner: OnionService,
event_tx: mpsc::Sender<ServiceEvent>,
event_rx: Option<mpsc::Receiver<ServiceEvent>>,
}
impl OnionServiceWithEvents {
pub fn new(config: OnionServiceConfig) -> Self {
let (event_tx, event_rx) = mpsc::channel(256);
Self {
inner: OnionService::new(config),
event_tx,
event_rx: Some(event_rx),
}
}
pub fn take_event_receiver(&mut self) -> Option<mpsc::Receiver<ServiceEvent>> {
self.event_rx.take()
}
pub fn state(&self) -> ServiceState {
self.inner.state()
}
pub fn address(&self) -> Option<String> {
self.inner.address()
}
pub fn stats(&self) -> ServiceStats {
self.inner.stats()
}
pub fn is_running(&self) -> bool {
self.inner.is_running()
}
pub async fn start(&mut self) -> Result<String> {
let old_state = self.inner.state();
let result = self.inner.start().await;
let new_state = self.inner.state();
if old_state != new_state {
let _ = self
.event_tx
.send(ServiceEvent::StateChanged {
old: old_state,
new: new_state,
})
.await;
}
result
}
pub async fn accept(&mut self) -> Option<OnionStream> {
let stream = self.inner.accept().await;
if let Some(ref s) = stream {
let _ = self
.event_tx
.send(ServiceEvent::ConnectionAccepted { stream_id: s.id() })
.await;
}
stream
}
pub async fn stop(&mut self) -> Result<()> {
let old_state = self.inner.state();
let result = self.inner.stop().await;
let new_state = self.inner.state();
if old_state != new_state {
let _ = self
.event_tx
.send(ServiceEvent::StateChanged {
old: old_state,
new: new_state,
})
.await;
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = OnionServiceConfig::new("test-service")
.port(443)
.with_pow()
.max_streams_per_circuit(100);
assert_eq!(config.nickname, "test-service");
assert_eq!(config.port, 443);
assert!(config.enable_pow);
assert_eq!(config.max_streams_per_circuit, 100);
}
#[test]
fn test_high_security_preset() {
let config = OnionServiceConfig::high_security();
assert!(config.enable_pow);
assert_eq!(config.max_streams_per_circuit, 100);
assert_eq!(config.port, 443);
assert!(config.rate_limit_at_intro.is_some());
assert_eq!(config.num_intro_points, 5);
assert_eq!(config.vanguard_mode, Some(VanguardMode::Full));
}
#[test]
fn test_rate_limit_config() {
let config = OnionServiceConfig::new("rate-limited")
.rate_limit_at_intro(50.0, 100)
.num_intro_points(7);
assert_eq!(config.rate_limit_at_intro, Some((50.0, 100)));
assert_eq!(config.num_intro_points, 7);
}
#[test]
fn test_service_initial_state() {
let config = OnionServiceConfig::default();
let service = OnionService::new(config);
assert_eq!(service.state(), ServiceState::Idle);
assert!(service.address().is_none());
assert!(!service.is_running());
}
#[test]
fn test_onion_stream_metadata() {
}
#[test]
fn test_client_auth_key() {
let (key, secret) = ClientAuthKey::generate("test-client");
assert_eq!(key.client_id, "test-client");
assert!(!key.is_expired());
assert_eq!(secret.len(), 32);
let expired_key = key.with_expiry(Duration::ZERO);
std::thread::sleep(Duration::from_millis(1));
assert!(expired_key.is_expired());
}
#[test]
fn test_service_stats() {
let stats = ServiceStats::default();
assert_eq!(stats.connections, 0);
assert_eq!(stats.uptime(), Duration::ZERO);
}
#[test]
fn test_vanguard_mode_configurations() {
let default = OnionServiceConfig::default();
assert_eq!(default.vanguard_mode, None);
let lite = OnionServiceConfig::new("test").vanguards_lite();
assert_eq!(lite.vanguard_mode, Some(VanguardMode::Lite));
let full = OnionServiceConfig::new("test").vanguards_full();
assert_eq!(full.vanguard_mode, Some(VanguardMode::Full));
let high_sec = OnionServiceConfig::high_security();
assert_eq!(
high_sec.vanguard_mode,
Some(VanguardMode::Full),
"CRITICAL: high_security() MUST enable full vanguards!"
);
}
#[test]
fn test_pow_configuration() {
let default = OnionServiceConfig::default();
assert!(!default.enable_pow);
assert!(default.pow_queue_depth.is_none());
let with_pow = OnionServiceConfig::new("test").with_pow();
assert!(with_pow.enable_pow);
let with_queue = OnionServiceConfig::new("test")
.with_pow()
.pow_queue_depth(16000);
assert!(with_queue.enable_pow);
assert_eq!(with_queue.pow_queue_depth, Some(16000));
let high_sec = OnionServiceConfig::high_security();
assert!(
high_sec.enable_pow,
"CRITICAL: high_security() MUST enable PoW!"
);
assert_eq!(high_sec.pow_queue_depth, Some(16000));
}
#[test]
fn test_rate_limit_configuration() {
let default = OnionServiceConfig::default();
assert!(default.rate_limit_at_intro.is_none());
let with_limit = OnionServiceConfig::new("test").rate_limit_at_intro(100.0, 200);
assert_eq!(with_limit.rate_limit_at_intro, Some((100.0, 200)));
let high_sec = OnionServiceConfig::high_security();
assert!(
high_sec.rate_limit_at_intro.is_some(),
"high_security() should enable rate limiting"
);
}
#[test]
fn test_stream_limit_configuration() {
let default = OnionServiceConfig::default();
assert_eq!(default.max_streams_per_circuit, 65535);
let limited = OnionServiceConfig::new("test").max_streams_per_circuit(50);
assert_eq!(limited.max_streams_per_circuit, 50);
let high_sec = OnionServiceConfig::high_security();
assert_eq!(
high_sec.max_streams_per_circuit, 100,
"high_security() should limit streams per circuit"
);
}
#[test]
fn test_intro_points_configuration() {
let default = OnionServiceConfig::default();
assert_eq!(default.num_intro_points, 3);
let more = OnionServiceConfig::new("test").num_intro_points(10);
assert_eq!(more.num_intro_points, 10);
let clamped_low = OnionServiceConfig::new("test").num_intro_points(0);
assert_eq!(clamped_low.num_intro_points, 1);
let clamped_high = OnionServiceConfig::new("test").num_intro_points(100);
assert_eq!(clamped_high.num_intro_points, 20);
let high_sec = OnionServiceConfig::high_security();
assert!(
high_sec.num_intro_points > 3,
"high_security() should have more intro points for availability"
);
}
#[test]
fn test_client_auth_configuration() {
let default = OnionServiceConfig::default();
assert!(default.authorized_clients.is_empty());
assert!(!default.has_client_auth());
let with_dir = OnionServiceConfig {
authorized_clients_dir: Some(PathBuf::from("/tmp/keys")),
..Default::default()
};
assert!(with_dir.has_client_auth());
}
#[test]
fn test_combined_security_features() {
let config = OnionServiceConfig::new("fortress")
.port(443)
.vanguards_full()
.with_pow()
.pow_queue_depth(32000)
.rate_limit_at_intro(5.0, 10)
.max_streams_per_circuit(50)
.num_intro_points(10);
assert_eq!(config.vanguard_mode, Some(VanguardMode::Full));
assert!(config.enable_pow);
assert_eq!(config.pow_queue_depth, Some(32000));
assert_eq!(config.rate_limit_at_intro, Some((5.0, 10)));
assert_eq!(config.max_streams_per_circuit, 50);
assert_eq!(config.num_intro_points, 10);
}
#[test]
fn test_security_feature_documentation_accuracy() {
assert!(matches!(VanguardMode::Full, VanguardMode::Full));
assert!(matches!(VanguardMode::Lite, VanguardMode::Lite));
}
#[test]
fn test_secret_key_zeroize() {
let (_, secret) = ClientAuthKey::generate("test");
let debug_output = format!("{:?}", secret);
assert!(
debug_output.contains("REDACTED"),
"SecretKey debug output should be redacted"
);
assert!(
!debug_output.contains("0x"),
"SecretKey debug should not show hex bytes"
);
assert_eq!(secret.len(), 32);
assert!(!secret.is_empty());
}
}