šŸš€

Getting Started #

Set up your development environment and establish your first connection to Tor's control port. This guide covers prerequisites, configuration, and basic authentication methods.

Beginner Setup Authentication
1 Prerequisites

Before using stem-rs, ensure you have Tor installed and configured with a control port. The control port allows external applications to communicate with the Tor process.

šŸ”§ Tor Installation

Install Tor via your package manager or from torproject.org

āš™ļø Control Port

Enable the control port in your torrc configuration file

šŸ¦€ Rust Toolchain

Rust 1.70+ with async runtime support (Tokio)

2 Tor Configuration
torrc

Add the following lines to your torrc file to enable the control port:

/etc/tor/torrc
# Enable the control port for local connections
ControlPort 9051

# Cookie authentication (recommended for local use)
CookieAuthentication 1

# Alternative: Password authentication
# HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
šŸ’”
Security Recommendation

Cookie authentication is preferred for local connections as it doesn't require storing passwords. The cookie file is automatically created with restricted permissions.

3 Add Dependencies
Cargo.toml
Cargo.toml
[dependencies]
stem-rs = "1"
tokio = { version = "1", features = ["full"] }
4 Basic Connection
Rust

Establish a connection to Tor and authenticate. The controller automatically detects the best authentication method available.

main.rs
use stem_rs::{Controller, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    // Connect to Tor's control port
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    
    // Authenticate (auto-detects: cookie, safecookie, or password)
    controller.authenticate(None).await?;
    
    // Query Tor version to verify connection
    let version = controller.get_version().await?;
    println!("Connected to Tor {}", version);
    
    Ok(())
}
Output
Connected to Tor 0.4.8.10
5 Authentication Methods

stem-rs supports all standard Tor authentication methods:

MethodDescriptionConfiguration
SAFECOOKIEChallenge-response authentication (most secure)CookieAuthentication 1
COOKIEFile-based authentication tokenCookieAuthentication 1
HASHEDPASSWORDPassword-based authenticationHashedControlPassword ...
NULLNo authentication (not recommended)Default when no auth configured
Password Authentication
use stem_rs::{Controller, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    
    // Provide password explicitly
    controller.authenticate(Some("your_password")).await?;
    
    Ok(())
}
Unix Socket Connection
use std::path::Path;
use stem_rs::{Controller, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    // Connect via Unix domain socket (enhanced security)
    let mut controller = Controller::from_socket_file(
        Path::new("/var/run/tor/control")
    ).await?;
    
    controller.authenticate(None).await?;
    
    let pid = controller.get_pid().await?;
    println!("Tor process ID: {}", pid);
    
    Ok(())
}
šŸ“Š

Relay Monitoring #

Monitor your Tor relay's performance and traffic statistics. Learn to query bandwidth usage, connection counts, and other operational metrics in real-time.

Relay Operators Metrics get_info
1 Traffic Statistics

Query cumulative traffic statistics from your relay. These values represent total bytes transferred since Tor started.

traffic_monitor.rs
use stem_rs::{Controller, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Query traffic statistics
    let bytes_read = controller.get_info("traffic/read").await?;
    let bytes_written = controller.get_info("traffic/written").await?;
    
    println!("Traffic Statistics:");
    println!("  Downloaded: {} bytes", bytes_read);
    println!("  Uploaded:   {} bytes", bytes_written);
    
    Ok(())
}
Output
Traffic Statistics:
  Downloaded: 33406 bytes
  Uploaded:   29649 bytes
2 Available Metrics

The get_info method provides access to numerous Tor metrics:

KeyDescriptionType
traffic/readTotal bytes downloadedInteger
traffic/writtenTotal bytes uploadedInteger
versionTor version stringString
circuit-statusActive circuit informationMulti-line
stream-statusActive stream informationMulti-line
addressBest guess at external IPIP Address
fingerprintRelay fingerprint (if relay)Hex String
3 Comprehensive Relay Monitor

A complete example that queries multiple metrics and formats them for display:

relay_dashboard.rs
use stem_rs::{Controller, Error};

fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    
    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut ctrl = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    ctrl.authenticate(None).await?;
    
    // Gather relay information
    let version = ctrl.get_version().await?;
    let pid = ctrl.get_pid().await?;
    
    let read: u64 = ctrl.get_info("traffic/read").await?.parse().unwrap_or(0);
    let written: u64 = ctrl.get_info("traffic/written").await?.parse().unwrap_or(0);
    
    println!("╔══════════════════════════════════════╗");
    println!("ā•‘       Tor Relay Dashboard            ā•‘");
    println!("╠══════════════════════════════════════╣");
    println!("ā•‘ Version:    {:<24} ā•‘", version);
    println!("ā•‘ Process ID: {:<24} ā•‘", pid);
    println!("╠══════════════════════════════════════╣");
    println!("ā•‘ Downloaded: {:<24} ā•‘", format_bytes(read));
    println!("ā•‘ Uploaded:   {:<24} ā•‘", format_bytes(written));
    println!("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
    
    Ok(())
}
šŸŒ

Geographic Exit Selection #

Configure Tor to route traffic through specific countries. Useful for accessing geo-restricted content, testing localization, or bypassing regional censorship.

Exit Nodes Country Codes SOCKS Proxy
1 Configuring Exit Nodes

Use the ExitNodes configuration option to specify which countries Tor should exit through. Country codes follow the ISO 3166-1 alpha-2 standard enclosed in braces.

exit_selection.rs
use stem_rs::{Controller, Error, Signal};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Configure exit through Germany
    controller.set_conf("ExitNodes", "{de}").await?;
    
    // Request new circuits with the updated exit policy
    controller.signal(Signal::Newnym).await?;
    
    println!("Exit nodes configured for Germany");
    println!("New circuits will exit through DE relays");
    
    // To reset to default behavior:
    // controller.reset_conf("ExitNodes").await?;
    
    Ok(())
}
āš ļø
Privacy Consideration

Restricting exit nodes reduces anonymity by limiting the pool of available relays. Use this feature only when geographic location is essential for your use case.

2 Common Country Codes
CodeCountryCodeCountry
{us}United States{de}Germany
{gb}United Kingdom{fr}France
{nl}Netherlands{ch}Switzerland
{se}Sweden{jp}Japan
3 Making HTTP Requests Through Tor

Combine stem-rs with an HTTP client that supports SOCKS5 proxies to make requests through Tor:

Cargo.toml (additional dependencies)
[dependencies]
reqwest = { version = "0.11", features = ["socks"] }
tor_http_client.rs
use stem_rs::{Controller, Error, Signal};
use std::time::Duration;

const SOCKS_PORT: u16 = 9050;

async fn query_through_tor(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let proxy = reqwest::Proxy::all(
        format!("socks5h://127.0.0.1:{}", SOCKS_PORT)
    )?;
    
    let client = reqwest::Client::builder()
        .proxy(proxy)
        .timeout(Duration::from_secs(30))
        .build()?;
    
    let response = client.get(url).send().await?.text().await?;
    Ok(response)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Configure exit through Germany
    controller.set_conf("ExitNodes", "{de}").await?;
    controller.signal(Signal::Newnym).await?;
    
    // Wait for new circuits to be established
    tokio::time::sleep(Duration::from_secs(5)).await;
    
    // Verify exit IP location
    let response = query_through_tor(
        "https://check.torproject.org/api/ip"
    ).await?;
    
    println!("Exit IP information: {}", response);
    
    Ok(())
}
4 Multi-Country Rotation

Iterate through multiple countries for testing or data collection:

country_rotation.rs
use stem_rs::{Controller, Error, Signal};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    let countries = [
        ("{us}", "United States"),
        ("{de}", "Germany"),
        ("{jp}", "Japan"),
        ("{nl}", "Netherlands"),
    ];
    
    for (code, name) in countries {
        println!("\n--- Switching to {} ({}) ---", name, code);
        
        controller.set_conf("ExitNodes", code).await?;
        controller.signal(Signal::Newnym).await?;
        
        // Wait for circuit establishment
        tokio::time::sleep(Duration::from_secs(3)).await;
        
        // Perform your operations here...
        println!("Ready to route through {}", name);
    }
    
    // Reset to default exit selection
    controller.reset_conf("ExitNodes").await?;
    println!("\nReset to default exit node selection");
    
    Ok(())
}
5 Manual Circuit Management

For advanced use cases, you can inspect and manage circuits directly:

circuit_management.rs
use stem_rs::{Controller, Error, CircStatus};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // List current circuits
    let circuits = controller.get_circuits().await?;
    
    println!("Active Circuits:");
    for circuit in &circuits {
        if circuit.status == CircStatus::Built {
            println!("  Circuit {} ({} hops):", circuit.id, circuit.path.len());
            for relay in &circuit.path {
                println!("    → {} ({:?})", relay.fingerprint, relay.nickname);
            }
        }
    }
    
    // Create a new circuit (Tor selects the path)
    let circuit_id = controller.new_circuit(None).await?;
    println!("\nCreated new circuit: {}", circuit_id);
    
    // Wait for circuit to be built
    tokio::time::sleep(Duration::from_secs(5)).await;
    
    // Close the circuit when done
    controller.close_circuit(&circuit_id).await?;
    println!("Circuit {} closed", circuit_id);
    
    Ok(())
}
šŸ“”

Asynchronous Event Handling #

Subscribe to real-time events from Tor including bandwidth updates, circuit changes, stream activity, and log messages. Build responsive monitoring applications.

Events Async Monitoring
1 Available Event Types

Tor emits various events that you can subscribe to. Each event type provides different information about Tor's operation:

EventDescriptionFrequency
BwBandwidth usage (bytes read/written)Every second
CircCircuit status changesOn change
StreamStream status changesOn change
NoticeNotice-level log messagesOn occurrence
WarnWarning-level log messagesOn occurrence
ErrError-level log messagesOn occurrence
2 Real-Time Bandwidth Monitor

Monitor Tor's bandwidth usage in real-time. Bandwidth events are emitted every second with the bytes transferred:

bandwidth_monitor.rs
use stem_rs::{Controller, Error, EventType};
use stem_rs::events::ParsedEvent;

fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    
    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Subscribe to bandwidth events
    controller.set_events(&[EventType::Bw]).await?;
    
    println!("Monitoring bandwidth (Ctrl+C to stop)...\n");
    
    let mut total_read = 0u64;
    let mut total_written = 0u64;
    
    loop {
        match controller.recv_event().await? {
            ParsedEvent::Bandwidth(bw) => {
                total_read += bw.read;
                total_written += bw.written;
                
                println!(
                    "↓ {:>8} B/s  ↑ {:>8} B/s  │  Total: ↓ {} ↑ {}",
                    bw.read,
                    bw.written,
                    format_bytes(total_read),
                    format_bytes(total_written)
                );
            }
            _ => {}
        }
    }
}
Output
↓     1024 B/s  ↑      512 B/s  │  Total: ↓ 1.00 KB ↑ 512 B
↓     2048 B/s  ↑     1024 B/s  │  Total: ↓ 3.00 KB ↑ 1.50 KB
↓      512 B/s  ↑      256 B/s  │  Total: ↓ 3.50 KB ↑ 1.75 KB
3 Circuit Event Monitoring

Track circuit lifecycle events including creation, extension, and closure:

circuit_monitor.rs
use stem_rs::{Controller, Error, EventType, CircStatus};
use stem_rs::events::ParsedEvent;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Subscribe to circuit events
    controller.set_events(&[EventType::Circ]).await?;
    
    println!("Monitoring circuit events...\n");
    
    loop {
        match controller.recv_event().await? {
            ParsedEvent::Circuit(circ) => {
                let status_icon = match circ.status {
                    CircStatus::Launched => "šŸš€ LAUNCHED",
                    CircStatus::Built => "āœ… BUILT",
                    CircStatus::Extended => "šŸ“” EXTENDED",
                    CircStatus::Failed => "āŒ FAILED",
                    CircStatus::Closed => "šŸ”’ CLOSED",
                    _ => "ā“ UNKNOWN",
                };
                
                println!(
                    "{} Circuit {} ({} hops)",
                    status_icon,
                    circ.id,
                    circ.path.len()
                );
                
                if !circ.path.is_empty() {
                    for (fp, nick) in &circ.path {
                        println!("   → {} ({:?})", &fp[..8], nick);
                    }
                }
            }
            _ => {}
        }
    }
}
4 Multi-Event Handler with Channels

For production applications, use async channels to handle events without blocking other operations:

multi_event_handler.rs
use stem_rs::{Controller, Error, EventType};
use stem_rs::events::ParsedEvent;
use tokio::sync::mpsc;
use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    let controller = Arc::new(Mutex::new(controller));
    
    // Authenticate and subscribe to events
    {
        let mut ctrl = controller.lock().await;
        ctrl.authenticate(None).await?;
        ctrl.set_events(&[
            EventType::Bw,
            EventType::Circ,
            EventType::Notice,
        ]).await?;
    }
    
    // Create channel for event processing
    let (tx, mut rx) = mpsc::channel::<ParsedEvent>(100);
    
    // Spawn event receiver task
    let ctrl_clone = controller.clone();
    tokio::spawn(async move {
        loop {
            let event = {
                let mut ctrl = ctrl_clone.lock().await;
                ctrl.recv_event().await
            };
            
            match event {
                Ok(e) => {
                    if tx.send(e).await.is_err() {
                        break;
                    }
                }
                Err(_) => break,
            }
        }
    });
    
    // Process events in main task
    while let Some(event) = rx.recv().await {
        match event {
            ParsedEvent::Bandwidth(bw) => {
                if bw.read > 0 || bw.written > 0 {
                    println!("[BW] ↓{} ↑{}", bw.read, bw.written);
                }
            }
            ParsedEvent::Circuit(circ) => {
                println!("[CIRC] {} {:?}", circ.id, circ.status);
            }
            ParsedEvent::Log(log) => {
                println!("[{}] {}", log.runlevel, log.message);
            }
            _ => {}
        }
    }
    
    Ok(())
}
šŸ§…

Onion Services (Hidden Services) #

Create and manage Tor hidden services programmatically. Host services accessible only through the Tor network without exposing your server's IP address.

Onion Services ED25519-V3 Ephemeral
1 Ephemeral Hidden Service

Ephemeral hidden services exist only in memory and are the recommended approach for programmatic service creation. They don't persist to disk and are automatically cleaned up.

šŸ“
V3 Onion Services

Always use ED25519-V3 for new services. V2 onion services (RSA-based) are deprecated and no longer supported by the Tor network.

ephemeral_service.rs
use stem_rs::{Controller, Error};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    println!("Creating ephemeral hidden service...");
    
    // Create a v3 hidden service
    // Maps port 80 on the .onion address to localhost:8080
    let response = controller.create_ephemeral_hidden_service(
        &[(80, "127.0.0.1:8080")],  // Port mappings
        "NEW",                        // Generate new key
        "ED25519-V3",                 // Key type (v3 onion)
        &[],                          // No special flags
    ).await?;
    
    println!("\nāœ… Hidden service created!");
    println!("   Address: {}.onion", response.service_id);
    println!("   Key type: {:?}", response.private_key_type);
    
    // The private key can be saved to recreate the same address later
    if let Some(ref key) = response.private_key {
        println!("   Private key: {}...", &key[..50.min(key.len())]);
    }
    
    println!("\nService running. Press Ctrl+C to stop.");
    
    // Keep the service running
    tokio::time::sleep(Duration::from_secs(60)).await;
    
    // Clean up
    controller.remove_ephemeral_hidden_service(&response.service_id).await?;
    println!("\nHidden service removed.");
    
    Ok(())
}
2 Persistent Key Storage

To maintain the same .onion address across restarts, save and restore the private key:

persistent_service.rs
use stem_rs::{Controller, Error};
use std::path::Path;
use tokio::fs;

const KEY_FILE: &str = "hidden_service.key";

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    let key_path = Path::new(KEY_FILE);
    
    let response = if key_path.exists() {
        // Load existing key to restore the same .onion address
        let key_data = fs::read_to_string(key_path).await
            .map_err(|e| Error::Protocol(format!("Failed to read key: {}", e)))?;
        
        let (key_type, key_content) = key_data.split_once(':')
            .ok_or_else(|| Error::Protocol("Invalid key format".into()))?;
        
        println!("Restoring hidden service from saved key...");
        
        controller.create_ephemeral_hidden_service(
            &[(80, "127.0.0.1:8080")],
            key_type,
            key_content,
            &[],
        ).await?
    } else {
        // Create new service and save the key
        println!("Creating new hidden service...");
        
        let response = controller.create_ephemeral_hidden_service(
            &[(80, "127.0.0.1:8080")],
            "NEW",
            "ED25519-V3",
            &[],
        ).await?;
        
        // Save the key for future use
        if let (Some(key_type), Some(key)) = 
            (&response.private_key_type, &response.private_key) {
            let key_data = format!("{}:{}", key_type, key);
            fs::write(key_path, key_data).await
                .map_err(|e| Error::Protocol(format!("Failed to save key: {}", e)))?;
            println!("Key saved to {}", KEY_FILE);
        }
        
        response
    };
    
    println!("\nšŸ§… Hidden service: {}.onion", response.service_id);
    println!("   Forwarding port 80 → 127.0.0.1:8080");
    
    // Wait for shutdown signal
    println!("\nPress Ctrl+C to stop.");
    tokio::signal::ctrl_c().await.ok();
    
    controller.remove_ephemeral_hidden_service(&response.service_id).await?;
    
    Ok(())
}
3 Multi-Port Hidden Service

A single hidden service can expose multiple ports, each mapping to different local services:

multi_port_service.rs
use stem_rs::{Controller, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Create service with multiple port mappings
    let response = controller.create_ephemeral_hidden_service(
        &[
            (80, "127.0.0.1:8080"),    // HTTP
            (443, "127.0.0.1:8443"),  // HTTPS
            (22, "127.0.0.1:2222"),   // SSH
        ],
        "NEW",
        "ED25519-V3",
        &["Detach"],  // Keep running if controller disconnects
    ).await?;
    
    println!("Multi-port hidden service created:");
    println!("  Address: {}.onion", response.service_id);
    println!("  Port 80  → localhost:8080 (HTTP)");
    println!("  Port 443 → localhost:8443 (HTTPS)");
    println!("  Port 22  → localhost:2222 (SSH)");
    
    Ok(())
}
4 Complete Web Server with Hidden Service

A production-ready example combining an HTTP server with a hidden service:

Cargo.toml (additional dependencies)
[dependencies]
axum = "0.7"
tower = "0.4"
onion_web_server.rs
use stem_rs::{Controller, Error};
use axum::{routing::get, Router};
use std::net::SocketAddr;

async fn index() -> &'static str {
    "Welcome to the hidden service! šŸ§…"
}

async fn health() -> &'static str {
    "OK"
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build the web application
    let app = Router::new()
        .route("/", get(index))
        .route("/health", get(health));
    
    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    
    // Start the web server
    let server = tokio::spawn(async move {
        let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
        axum::serve(listener, app).await.unwrap();
    });
    
    // Create the hidden service
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    let response = controller.create_ephemeral_hidden_service(
        &[(80, "127.0.0.1:8080")],
        "NEW",
        "ED25519-V3",
        &[],
    ).await?;
    
    println!("šŸ§… Service available at: http://{}.onion", response.service_id);
    println!("   Local access: http://127.0.0.1:8080");
    
    // Wait for shutdown signal
    tokio::signal::ctrl_c().await?;
    
    // Cleanup
    controller.remove_ephemeral_hidden_service(&response.service_id).await?;
    server.abort();
    
    println!("\nService stopped.");
    Ok(())
}
šŸ“„

Network Descriptors #

Parse and analyze Tor network descriptors. Access relay metadata, consensus documents, bandwidth statistics, and build network analysis tools.

Consensus Server Descriptors Microdescriptors
1 Descriptor Types

The Tor network publishes several types of descriptors containing relay information:

šŸ“‹
Server Descriptor
Full relay metadata including keys, policies, and bandwidth
šŸ“
Microdescriptor
Compact client-side info for circuit building
šŸ“Š
Consensus
Network status document listing all relays
šŸ“ˆ
Extra-Info
Bandwidth statistics and additional metrics
šŸ§…
Hidden Service
Onion service descriptors (v3)
2 Downloading the Network Consensus

The consensus is the authoritative document listing all relays in the network:

consensus_analysis.rs
use stem_rs::descriptor::{download_consensus, NetworkStatusDocument};
use stem_rs::{Error, Flag};

#[tokio::main]
async fn main() -> Result<(), Error> {
    println!("Downloading consensus from directory authorities...");
    
    let consensus = download_consensus(None).await?;
    
    println!("\nšŸ“‹ Consensus Information:");
    println!("   Valid after: {}", consensus.valid_after);
    println!("   Valid until: {}", consensus.valid_until);
    println!("   Total relays: {}", consensus.routers.len());
    
    // Count relays by flag
    let mut guards = 0;
    let mut exits = 0;
    let mut fast = 0;
    let mut stable = 0;
    
    for router in &consensus.routers {
        if router.flags.contains(&Flag::Guard) { guards += 1; }
        if router.flags.contains(&Flag::Exit) { exits += 1; }
        if router.flags.contains(&Flag::Fast) { fast += 1; }
        if router.flags.contains(&Flag::Stable) { stable += 1; }
    }
    
    println!("\nšŸ“Š Relay Statistics:");
    println!("   Guards: {}", guards);
    println!("   Exits:  {}", exits);
    println!("   Fast:   {}", fast);
    println!("   Stable: {}", stable);
    
    Ok(())
}
Output
šŸ“‹ Consensus Information:
   Valid after: 2024-01-15 12:00:00
   Valid until: 2024-01-15 15:00:00
   Total relays: 6847

šŸ“Š Relay Statistics:
   Guards: 2341
   Exits:  1523
   Fast:   5892
   Stable: 5234
3 Finding High-Bandwidth Exit Relays

Analyze server descriptors to find the highest-bandwidth exit relays:

top_exits.rs
use stem_rs::descriptor::{download_server_descriptors, ServerDescriptor, Descriptor};
use stem_rs::Error;
use std::collections::BinaryHeap;

fn format_bandwidth(bytes: u64) -> String {
    const MB: u64 = 1024 * 1024;
    const GB: u64 = MB * 1024;
    
    if bytes >= GB {
        format!("{:.2} GB/s", bytes as f64 / GB as f64)
    } else {
        format!("{:.2} MB/s", bytes as f64 / MB as f64)
    }
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    println!("Downloading server descriptors...");
    
    let descriptors = download_server_descriptors(None).await?;
    
    // Collect exit relays with bandwidth
    let mut exits: Vec<(&str, &str, u64)> = Vec::new();
    
    for desc in &descriptors {
        if desc.exit_policy.is_exiting_allowed() {
            // Advertised bandwidth is min of avg, burst, and observed
            let advertised = desc.bandwidth_avg
                .min(desc.bandwidth_burst)
                .min(desc.bandwidth_observed);
            
            exits.push((&desc.nickname, &desc.fingerprint, advertised));
        }
    }
    
    // Sort by bandwidth (descending)
    exits.sort_by(|a, b| b.2.cmp(&a.2));
    
    println!("\nšŸ† Top 10 Exit Relays by Bandwidth:\n");
    
    for (i, (nickname, fingerprint, bandwidth)) in exits.iter().take(10).enumerate() {
        println!(
            "{:>2}. {} ({}) - {}",
            i + 1,
            nickname,
            &fingerprint[..8],
            format_bandwidth(*bandwidth)
        );
    }
    
    Ok(())
}
4 Parsing Local Descriptor Files

Parse descriptors from Tor's cached files or your own archives:

parse_local.rs
use stem_rs::descriptor::{parse_file, ServerDescriptor, Descriptor};
use stem_rs::descriptor::{DigestHash, DigestEncoding};
use stem_rs::Error;
use std::fs;

fn main() -> Result<(), Error> {
    // Read from Tor's cached descriptors
    let content = fs::read("/var/lib/tor/cached-descriptors")
        .map_err(|e| Error::Protocol(format!("Failed to read file: {}", e)))?;
    
    let descriptor: ServerDescriptor = parse_file(&content)?;
    
    println!("šŸ“„ Server Descriptor:");
    println!("   Nickname: {}", descriptor.nickname);
    println!("   Address: {}:{}", descriptor.address, descriptor.or_port);
    println!("   Fingerprint: {}", descriptor.fingerprint);
    println!("   Platform: {:?}", descriptor.platform);
    println!("   Published: {}", descriptor.published);
    
    // Compute descriptor digest
    let digest = descriptor.digest(DigestHash::Sha1, DigestEncoding::Hex)?;
    println!("   Digest (SHA1): {}", digest);
    
    // Check exit policy
    println!("\n🚪 Exit Policy:");
    println!("   Allows exiting: {}", descriptor.exit_policy.is_exiting_allowed());
    println!("   Summary: {}", descriptor.exit_policy.summary());
    
    Ok(())
}
šŸ”§

Utility Functions #

Helper functions for working with Tor data including exit policy evaluation, version comparison, validation utilities, and digest computation.

Exit Policy Version Validation
1 Exit Policy Evaluation

Parse and evaluate exit policies to determine what traffic a relay allows:

exit_policy.rs
use stem_rs::exit_policy::{ExitPolicy, ExitPolicyRule, MicroExitPolicy};
use std::net::IpAddr;

fn main() -> Result<(), stem_rs::Error> {
    // Parse a full exit policy
    let policy = ExitPolicy::parse(
        "accept *:80, accept *:443, accept *:8080-8090, reject *:*"
    )?;
    
    let addr: IpAddr = "93.184.216.34".parse().unwrap();
    
    println!("Exit Policy Evaluation:");
    println!("  Port 80 (HTTP):   {}", 
        if policy.can_exit_to(addr, 80) { "āœ… Allowed" } else { "āŒ Blocked" });
    println!("  Port 443 (HTTPS): {}", 
        if policy.can_exit_to(addr, 443) { "āœ… Allowed" } else { "āŒ Blocked" });
    println!("  Port 22 (SSH):    {}", 
        if policy.can_exit_to(addr, 22) { "āœ… Allowed" } else { "āŒ Blocked" });
    println!("  Port 8085:        {}", 
        if policy.can_exit_to(addr, 8085) { "āœ… Allowed" } else { "āŒ Blocked" });
    
    println!("\nPolicy summary: {}", policy.summary());
    println!("Allows any exit: {}", policy.is_exiting_allowed());
    
    // Parse individual rules
    let rule = ExitPolicyRule::parse("reject 10.0.0.0/8:*")?;
    println!("\nRule rejects private IPs: {}", !rule.is_accept);
    
    // Microdescriptor policies (port-only)
    let micro = MicroExitPolicy::parse("accept 80,443,8080-8090")?;
    println!("Micro policy allows port 80: {}", micro.can_exit_to(80));
    
    Ok(())
}
Output
Exit Policy Evaluation:
  Port 80 (HTTP):   āœ… Allowed
  Port 443 (HTTPS): āœ… Allowed
  Port 22 (SSH):    āŒ Blocked
  Port 8085:        āœ… Allowed

Policy summary: accept 80,443,8080-8090
Allows any exit: true
2 Version Parsing and Comparison

Parse Tor version strings and compare them for feature detection:

version_check.rs
use stem_rs::Version;

fn main() -> Result<(), stem_rs::Error> {
    let v1 = Version::parse("0.4.7.10")?;
    let v2 = Version::parse("0.4.8.0")?;
    let v3 = Version::parse("0.4.7.10-dev")?;
    
    println!("Version comparisons:");
    println!("  {} < {}: {}", v1, v2, v1 < v2);
    println!("  {} == {}: {}", v1, v3, v1 == v3);
    println!("  {} >= 0.4.0.0: {}", v1, v1 >= Version::parse("0.4.0.0")?);
    
    // Check feature availability
    let min_v3_onion = Version::parse("0.3.2.1")?;
    if v1 >= min_v3_onion {
        println!("\nāœ… Tor {} supports v3 onion services", v1);
    }
    
    Ok(())
}
3 Validation Utilities

Validate Tor-specific identifiers like fingerprints and nicknames:

validation.rs
use stem_rs::util::{is_valid_fingerprint, is_valid_nickname};

fn main() {
    // Fingerprint validation (40 hex characters)
    let valid_fp = "9695DFC35FFEB861329B9F1AB04C46397020CE31";
    let invalid_fp = "not-a-fingerprint";
    
    println!("Fingerprint validation:");
    println!("  '{}': {}", valid_fp, 
        if is_valid_fingerprint(valid_fp) { "āœ… Valid" } else { "āŒ Invalid" });
    println!("  '{}': {}", invalid_fp, 
        if is_valid_fingerprint(invalid_fp) { "āœ… Valid" } else { "āŒ Invalid" });
    
    // Nickname validation (1-19 alphanumeric characters)
    let valid_nick = "MyRelay123";
    let invalid_nick = "way-too-long-nickname-for-tor";
    
    println!("\nNickname validation:");
    println!("  '{}': {}", valid_nick, 
        if is_valid_nickname(valid_nick) { "āœ… Valid" } else { "āŒ Invalid" });
    println!("  '{}': {}", invalid_nick, 
        if is_valid_nickname(invalid_nick) { "āœ… Valid" } else { "āŒ Invalid" });
}
4 Digest Computation

Compute cryptographic digests used by Tor for descriptor identification:

digests.rs
use stem_rs::descriptor::{compute_digest, DigestHash, DigestEncoding};

fn main() {
    let content = b"Example descriptor content";
    
    // SHA-1 digest (used by legacy descriptors)
    let sha1_hex = compute_digest(content, DigestHash::Sha1, DigestEncoding::Hex);
    let sha1_b64 = compute_digest(content, DigestHash::Sha1, DigestEncoding::Base64);
    
    println!("SHA-1 digests:");
    println!("  Hex:    {}", sha1_hex);
    println!("  Base64: {}", sha1_b64);
    
    // SHA-256 digest (used by modern descriptors)
    let sha256_hex = compute_digest(content, DigestHash::Sha256, DigestEncoding::Hex);
    
    println!("\nSHA-256 digest:");
    println!("  Hex: {}", sha256_hex);
}
5 Configuration Management

Read and modify Tor's runtime configuration:

config_management.rs
use stem_rs::{Controller, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // Read current configuration
    let socks_ports = controller.get_conf("SocksPort").await?;
    println!("Current SOCKS ports: {:?}", socks_ports);
    
    // Modify configuration (temporary, not saved to torrc)
    controller.set_conf("MaxCircuitDirtiness", "600").await?;
    println!("Set MaxCircuitDirtiness to 600 seconds");
    
    // Reset to default value
    controller.reset_conf("MaxCircuitDirtiness").await?;
    println!("Reset MaxCircuitDirtiness to default");
    
    // Load configuration from text
    controller.load_conf("
        # Temporary configuration
        MaxCircuitDirtiness 300
    ").await?;
    
    Ok(())
}
6 Stream and Circuit Inspection

List and inspect active streams and circuits:

inspect_connections.rs
use stem_rs::{Controller, Error, StreamStatus, CircStatus};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut controller = Controller::from_port(
        "127.0.0.1:9051".parse()?
    ).await?;
    controller.authenticate(None).await?;
    
    // List all streams
    println!("šŸ“” Active Streams:");
    let streams = controller.get_streams().await?;
    for stream in &streams {
        let status = match stream.status {
            StreamStatus::New => "šŸ†•",
            StreamStatus::Succeeded => "āœ…",
            StreamStatus::Failed => "āŒ",
            StreamStatus::Closed => "šŸ”’",
            _ => "ā“",
        };
        println!(
            "  {} Stream {} → {}:{} (circuit: {:?})",
            status, stream.id, stream.target_host, stream.target_port, stream.circuit_id
        );
    }
    
    // List all circuits
    println!("\nšŸ”„ Active Circuits:");
    let circuits = controller.get_circuits().await?;
    for circuit in &circuits {
        let status = match circuit.status {
            CircStatus::Built => "āœ…",
            CircStatus::Extended => "šŸ“”",
            CircStatus::Failed => "āŒ",
            _ => "ā“",
        };
        println!(
            "  {} Circuit {} ({} hops)",
            status, circuit.id, circuit.path.len()
        );
    }
    
    Ok(())
}