Build privacy-focused applications with type-safe, async-first APIs
🌐 Website • 📚 Documentation • 📖 Tutorials • 🚀 Quick Start • 💡 Examples
stem-rs is a Rust implementation of Stem, the Python library for interacting with Tor's control protocol. It provides idiomatic, type-safe Rust APIs while maintaining complete functional parity with Python Stem.
Whether you're building privacy tools, monitoring Tor relays, managing circuits, or creating onion services — stem-rs gives you the building blocks you need with the safety guarantees Rust provides.
use stem_rs::{Controller, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
// Connect to Tor's control port
let mut ctrl = Controller::from_port("127.0.0.1:9051".parse()?).await?;
// Authenticate (auto-detects method)
ctrl.authenticate(None).await?;
// Query Tor version
let version = ctrl.get_version().await?;
println!("Connected to Tor {}", version);
Ok(())
}|
Connect to Tor via TCP or Unix domain sockets with full async I/O powered by Tokio.
|
All authentication methods with automatic detection and secure credential handling.
|
|
High-level interface for complete Tor interaction.
|
Complete parsing for all Tor descriptor types.
|
|
Subscribe to real-time Tor events with strongly-typed event structs.
|
Parse and evaluate relay exit policies.
|
Add stem-rs to your Cargo.toml:
[dependencies]
stem-rs = "1.2"
tokio = { version = "1", features = ["full"] }Or install via cargo:
cargo add stem-rs tokio --features tokio/fullAdd to your torrc:
ControlPort 9051
CookieAuthentication 1
Or for password authentication:
ControlPort 9051
HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
Generate a hashed password:
tor --hash-password "your-password"stem-rs uses feature flags to allow you to compile only what you need, reducing compile time and binary size.
By default, all features are enabled:
[dependencies]
stem-rs = "1.2" # Includes all featuresFor a minimal build with just the core functionality:
[dependencies]
stem-rs = { version = "1.2", default-features = false }This includes: socket communication, authentication, protocol parsing, utilities, and version handling.
| Feature | Description | Dependencies |
|---|---|---|
full |
All features (default) | All features below |
controller |
High-level Controller API | events |
descriptors |
Tor descriptor parsing | client, exit-policy |
events |
Event subscription and handling | None |
exit-policy |
Exit policy parsing and evaluation | None |
client |
ORPort relay communication | None |
interpreter |
Interactive Tor control interpreter | controller, events |
compression |
Gzip decompression for descriptors | None |
Controller only (no descriptor parsing):
[dependencies]
stem-rs = { version = "1.2", default-features = false, features = ["controller"] }Descriptors only (offline analysis):
[dependencies]
stem-rs = { version = "1.2", default-features = false, features = ["descriptors"] }Controller + Descriptors (most common):
[dependencies]
stem-rs = { version = "1.2", default-features = false, features = ["controller", "descriptors"] }Approximate compile time reductions with feature flags:
- Minimal build: ~40% faster (excludes descriptors, controller, events)
- Controller-only: ~30% faster (excludes descriptor parsing)
- Descriptors-only: ~20% faster (excludes controller, events)
Binary size reductions follow similar patterns.
use stem_rs::{Controller, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
// Connect via TCP
let mut ctrl = Controller::from_port("127.0.0.1:9051".parse()?).await?;
// Or via Unix socket
// let mut ctrl = Controller::from_socket_file(Path::new("/var/run/tor/control")).await?;
// Auto-detect authentication method
ctrl.authenticate(None).await?;
// Or use password
// ctrl.authenticate(Some("my_password")).await?;
println!("Connected!");
Ok(())
}// Get Tor version
let version = ctrl.get_version().await?;
println!("Tor {}", version);
// Get process ID
let pid = ctrl.get_pid().await?;
println!("PID: {}", pid);
// Query arbitrary info
let traffic_read = ctrl.get_info("traffic/read").await?;
let traffic_written = ctrl.get_info("traffic/written").await?;
println!("Traffic: {} read, {} written", traffic_read, traffic_written);
// Get configuration
let socks_ports = ctrl.get_conf("SocksPort").await?;
for port in socks_ports {
println!("SOCKS port: {}", port);
}use stem_rs::CircStatus;
// List all circuits
let circuits = ctrl.get_circuits().await?;
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
let circuit_id = ctrl.new_circuit(None).await?;
println!("Created circuit: {}", circuit_id);
// Close a circuit
ctrl.close_circuit(circuit_id).await?;use stem_rs::StreamStatus;
// List all streams
let streams = ctrl.get_streams().await?;
for stream in streams {
println!("Stream {} → {}:{} ({:?})",
stream.id,
stream.target_host,
stream.target_port,
stream.status
);
}use stem_rs::EventType;
// Subscribe to events
ctrl.set_events(&[
EventType::Bw, // Bandwidth
EventType::Circ, // Circuits
EventType::Stream, // Streams
EventType::Notice, // Log messages
]).await?;
// Process events
loop {
let event = ctrl.recv_event().await?;
match event {
ParsedEvent::Bandwidth(bw) => {
println!("BW: {} read, {} written", bw.read, bw.written);
}
ParsedEvent::Circuit(circ) => {
println!("Circuit {}: {:?}", circ.id, circ.status);
}
ParsedEvent::Log(log) => {
println!("[{}] {}", log.runlevel, log.message);
}
_ => {}
}
}use stem_rs::Signal;
// Request new identity (new circuits)
ctrl.signal(Signal::Newnym).await?;
// Clear DNS cache
ctrl.signal(Signal::ClearDnsCache).await?;
// Reload configuration
ctrl.signal(Signal::Reload).await?;
// Graceful shutdown
ctrl.signal(Signal::Shutdown).await?;Hidden Services
// Create ephemeral hidden service (v3 onion)
let response = ctrl.create_ephemeral_hidden_service(
&[(80, "127.0.0.1:8080")], // Map port 80 to local 8080
"NEW", // Generate new key
"ED25519-V3", // Use v3 onion (recommended)
&[], // No special flags
).await?;
println!("Hidden service: {}.onion", response.service_id);
println!("Private key: {:?}", response.private_key);
// Remove hidden service
ctrl.remove_ephemeral_hidden_service(&response.service_id).await?;use stem_rs::descriptor::{
ServerDescriptor, Microdescriptor, NetworkStatusDocument,
Descriptor, DigestHash, DigestEncoding,
download_consensus, download_server_descriptors,
};
// Download and parse consensus
let consensus = download_consensus(None).await?;
println!("Valid until: {}", consensus.valid_until);
println!("Relays: {}", consensus.routers.len());
// Parse server descriptor
let content = std::fs::read_to_string("cached-descriptors")?;
let descriptor = ServerDescriptor::parse(&content)?;
println!("Relay: {} ({})", descriptor.nickname, descriptor.fingerprint);
println!("Bandwidth: {} avg, {} burst",
descriptor.bandwidth_avg, descriptor.bandwidth_burst);
// Compute digest
let digest = descriptor.digest(DigestHash::Sha1, DigestEncoding::Hex)?;
println!("Digest: {}", digest);use stem_rs::exit_policy::ExitPolicy;
use std::net::IpAddr;
let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*")?;
// Check if traffic is allowed
let addr: IpAddr = "93.184.216.34".parse()?;
if policy.can_exit_to(addr, 443) {
println!("HTTPS traffic allowed");
}
// Get policy summary
println!("Policy: {}", policy.summary());use stem_rs::Version;
let version = ctrl.get_version().await?;
// Compare versions
let min_version = Version::parse("0.4.0.0")?;
if version >= min_version {
println!("Tor {} supports required features", version);
}| Module | Description |
|---|---|
controller |
High-level Tor control interface |
socket |
Low-level control socket communication |
auth |
Authentication methods and protocol info |
descriptor |
Tor descriptor parsing (server, micro, consensus, hidden service) |
events |
Event types and real-time handling |
exit_policy |
Exit policy parsing and evaluation |
version |
Version parsing and comparison |
client |
Direct ORPort relay communication |
interpreter |
Interactive Tor control interpreter |
util |
Validation utilities (fingerprints, nicknames, etc.) |
stem-rs is designed with security as a priority:
- 100% Safe Rust — No
unsafecode - Constant-time comparison — For authentication tokens and cookies
- Memory clearing — Sensitive data cleared after use
- Input validation — Prevents protocol injection attacks
- Signature verification — Optional cryptographic validation for descriptors
- Async-first — Built on Tokio for high-performance async I/O
- Zero-copy parsing — Efficient descriptor parsing where possible
- Event streaming — Non-blocking real-time event handling
- Connection pooling — Efficient socket management
- Rust 1.70+
- Tokio runtime
- Tor instance with control port enabled
# Run unit tests
cargo test
# Run with integration tests (requires running Tor)
cargo test --features integration
# Run extensive tests
cargo test --features extensivestem-rs maintains functional parity with Python Stem while providing Rust's safety guarantees:
| Feature | Python Stem | stem-rs |
|---|---|---|
| Control Protocol | ✅ | ✅ |
| All Auth Methods | ✅ | ✅ |
| Descriptor Parsing | ✅ | ✅ |
| Event Handling | ✅ | ✅ |
| Exit Policy | ✅ | ✅ |
| Hidden Services | ✅ | ✅ |
| Type Safety | ❌ | ✅ |
| Memory Safety | ❌ | ✅ |
| Async/Await | ❌ | ✅ |
| Zero-cost Abstractions | ❌ | ✅ |
Copyright 2026 stem-rs contributors
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Contributions are welcome! Please feel free to submit issues and pull requests.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Website • Documentation • Tutorials • crates.io • GitHub • Python Stem
Built with 🦀 by the stem-rs contributors