Tutorials & Examples
Learn to build Tor-integrated applications with stem-rs through practical, production-ready examples. From basic connections to advanced hidden service deployment.
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)
Add the following lines to your torrc file to enable the control port:
# Enable the control port for local connections
ControlPort 9051
# Cookie authentication (recommended for local use)
CookieAuthentication 1
# Alternative: Password authentication
# HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4CCookie authentication is preferred for local connections as it doesn't require storing passwords. The cookie file is automatically created with restricted permissions.
[dependencies]
stem-rs = "1"
tokio = { version = "1", features = ["full"] }Establish a connection to Tor and authenticate. The controller automatically detects the best authentication method available.
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(())
}Connected to Tor 0.4.8.10
stem-rs supports all standard Tor authentication methods:
| Method | Description | Configuration |
|---|---|---|
SAFECOOKIE | Challenge-response authentication (most secure) | CookieAuthentication 1 |
COOKIE | File-based authentication token | CookieAuthentication 1 |
HASHEDPASSWORD | Password-based authentication | HashedControlPassword ... |
NULL | No authentication (not recommended) | Default when no auth configured |
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(())
}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(())
}Query cumulative traffic statistics from your relay. These values represent total bytes transferred since Tor started.
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(())
}Traffic Statistics: Downloaded: 33406 bytes Uploaded: 29649 bytes
The get_info method provides access to numerous Tor metrics:
| Key | Description | Type |
|---|---|---|
traffic/read | Total bytes downloaded | Integer |
traffic/written | Total bytes uploaded | Integer |
version | Tor version string | String |
circuit-status | Active circuit information | Multi-line |
stream-status | Active stream information | Multi-line |
address | Best guess at external IP | IP Address |
fingerprint | Relay fingerprint (if relay) | Hex String |
A complete example that queries multiple metrics and formats them for display:
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(())
}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.
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(())
}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.
| Code | Country | Code | Country |
|---|---|---|---|
{us} | United States | {de} | Germany |
{gb} | United Kingdom | {fr} | France |
{nl} | Netherlands | {ch} | Switzerland |
{se} | Sweden | {jp} | Japan |
Combine stem-rs with an HTTP client that supports SOCKS5 proxies to make requests through Tor:
[dependencies]
reqwest = { version = "0.11", features = ["socks"] }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(())
}Iterate through multiple countries for testing or data collection:
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(())
}For advanced use cases, you can inspect and manage circuits directly:
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(())
}Tor emits various events that you can subscribe to. Each event type provides different information about Tor's operation:
| Event | Description | Frequency |
|---|---|---|
Bw | Bandwidth usage (bytes read/written) | Every second |
Circ | Circuit status changes | On change |
Stream | Stream status changes | On change |
Notice | Notice-level log messages | On occurrence |
Warn | Warning-level log messages | On occurrence |
Err | Error-level log messages | On occurrence |
Monitor Tor's bandwidth usage in real-time. Bandwidth events are emitted every second with the bytes transferred:
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)
);
}
_ => {}
}
}
}ā 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
Track circuit lifecycle events including creation, extension, and closure:
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);
}
}
}
_ => {}
}
}
}For production applications, use async channels to handle events without blocking other operations:
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(())
}The Tor network publishes several types of descriptors containing relay information:
The consensus is the authoritative document listing all relays in the network:
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(())
}š 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
Analyze server descriptors to find the highest-bandwidth exit relays:
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(())
}Parse descriptors from Tor's cached files or your own archives:
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(())
}Parse and evaluate exit policies to determine what traffic a relay allows:
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(())
}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
Parse Tor version strings and compare them for feature detection:
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(())
}Validate Tor-specific identifiers like fingerprints and nicknames:
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" });
}Compute cryptographic digests used by Tor for descriptor identification:
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);
}Read and modify Tor's runtime configuration:
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(())
}List and inspect active streams and circuits:
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(())
}