How webtor-rs Implements the Tor Protocol: A Deep Dive into the Pure-Rust Stack

Webtor-rs is a pure-Rust Tor client that implements the full protocol stack—from consensus retrieval and bridge negotiation to three-hop circuit construction and stream isolation—using modular, testable components that compile to both native binaries and WebAssembly.

Webtor-rs provides a complete Tor implementation in Rust, designed to operate seamlessly across desktop environments and web browsers. This article examines how the library implements the Tor protocol by dissecting its core modules, bridge handling, circuit management, and WebAssembly adaptations as found in the igor53627/webtor-rs repository.

Bootstrapping the Tor Client

The client initialization sequence begins in TorClient::new, which orchestrates the startup of the entire Tor stack. According to the source code in webtor/src/client.rs (lines 34-41), the first step calls init_wasm_modules, a no-op on native platforms that initializes the JavaScript runtime bridge when compiled to WebAssembly.

Immediately after initialization, the client loads network state. In webtor/src/directory.rs (lines 61-68), the load_cached_consensus function attempts to load a pre-fetched consensus from local storage. This is critical for WASM builds where a direct network connection is impossible without first establishing a bridge. The client then instantiates a circuit manager to track active three-hop paths and, if configured, calls create_circuit_early (lines 86-98 of client.rs) to establish the first channel before any user requests arrive.

Directory and Consensus Management

Before building circuits, the client needs to know which relays are available. The directory module (webtor/src/directory.rs) handles downloading or loading cached copies of the Tor network consensus and micro-descriptors.

For WebAssembly targets, the library cannot fetch the consensus directly over Tor. Instead, it downloads a compressed consensus file (consensus.txt.br) from a GitHub Pages URL defined as CACHED_CONSENSUS_BASE_URL, then parses it using MdConsensus::parse (lines 31-55). This bootstrap consensus enables the client to perform relay selection for the initial bridge connection. Once connected, the client can fetch a fresh consensus directly through the Tor network to update its relay list.

Bridge Negotiation and Channel Establishment

The establish_channel_impl function in webtor/src/client.rs (lines 104-170) implements the bridge protocol negotiation. This function performs several critical steps:

  • Fingerprint handling: Derives the RSA identity from the configured bridge fingerprint to verify the peer's certificate later.
  • Bridge selection: Matches the BridgeType enum to instantiate the correct transport:
    • Snowflake WebSocket: Creates a SnowflakeWsStream via snowflake_ws.rs
    • Snowflake WebRTC: Connects through SnowflakeBridge in snowflake.rs
    • WebTunnel: Establishes a stream via create_webtunnel_stream in webtunnel.rs
  • TLS handshake: Wraps the raw bridge stream in a tor_proto::channel::Channel using ChannelBuilder::launch_client. The handshake uses system_time_now (from time.rs for WASM compatibility) and verifies the bridge's certificate against the RSA identity.
  • Channel storage: Stores the resulting Arc<Channel> in self.channel to maintain the connection for subsequent circuit operations.

Circuit Construction and Management

Once a channel exists, the client constructs three-hop circuits through the CircuitManager::create_circuit_with_isolation method in webtor/src/circuit.rs (lines 120-190). The process follows the standard Tor protocol:

  1. Create a pending tunnel on the established channel using the tor-proto crate's primitives.
  2. Spawn a reactor using wasm_bindgen_futures::spawn_local for WASM or tokio::spawn for native targets.
  3. Establish the first hop (FAST): Uses make_circ_params() to build congestion-control parameters and calls create_firsthop_fast, yielding a ClientTunnel.
  4. Select middle and exit relays: Uses helpers from webtor/src/relay.rs to pick relays based on flags, explicitly ensuring the bridge is not reused as a middle node.
  5. Extend the tunnel: Calls extend to add the middle relay, then again to add the exit relay.
  6. Wrap and store: Encapsulates the tunnel in a Circuit struct, stores the relay list (bridge, middle, exit), marks the circuit as Ready, and optionally binds an isolation key.

Stream Isolation and Circuit Lifecycle

The circuit manager provides sophisticated lifecycle management. The get_circuit_for_isolation_key method ensures that requests with different origins receive separate circuits, preventing cross-site correlation attacks. The maybe_prebuild_circuit function proactively builds spare circuits when the average age exceeds a threshold, reducing latency for new requests. Finally, cleanup_circuits removes failed, outdated, or idle circuits to prevent resource leaks.

HTTP Communication Over Tor

Application-level requests flow through TorClient::fetch, which constructs an HttpRequest and forwards it to TorHttpClient::request (lines 62-68 of client.rs). The HTTP client implementation in webtor/src/http.rs performs the following:

  • Retrieves a ready circuit (or creates a new one).
  • Calls circuit.begin_stream(host, port) (lines 13-28 of circuit.rs) to open a new TCP-like stream over the circuit.
  • Wraps the stream in an HttpResponse after sending the request and reading the complete reply.

Convenience methods including get, post, and request expose the full HTTP verb set, custom headers, request bodies, and configurable timeouts.

Stateless Operation with One-Time Fetches

For short-lived operations, TorClient::fetch_one_time creates a temporary client instance, optionally enables early circuit creation, performs a single HTTP request, and immediately calls close. This pattern is ideal for "download and exit" workflows where maintaining a persistent client in memory is undesirable.

WebAssembly-Specific Adaptations

Running a Tor client in a browser requires significant platform adaptations, primarily found in webtor/src/time.rs and conditional compilation blocks throughout the codebase.

All standard library time calls are replaced with web_time::Instant to ensure compatibility with browser event loops. The system_time_now wrapper function provides a unified interface for both native and WASM targets. Additionally, the consensus fetching logic uses fetch_url via JavaScript interop rather than native TCP sockets, and the async runtime relies on wasm_bindgen_futures instead of Tokio when targeting WASM32.

Summary

  • Webtor-rs implements the complete Tor protocol in Rust, from consensus parsing to HTTP proxying.
  • Bridge support includes Snowflake (WebSocket and WebRTC) and WebTunnel, with RSA identity verification during TLS handshake.
  • Circuit management creates three-hop paths via FAST cells and selective relay extension, with automatic pre-building and cleanup.
  • Stream isolation prevents correlation by binding circuits to unique isolation keys derived from request contexts.
  • WebAssembly compatibility is achieved through web_time, cached consensus loading, and wasm_bindgen_futures for async execution.

Frequently Asked Questions

How does webtor-rs handle the initial Tor consensus download in browsers?

In WebAssembly builds, webtor-rs cannot connect to the Tor directory authorities directly. Instead, it fetches a compressed, cached consensus (consensus.txt.br) from a GitHub Pages URL via standard HTTP requests. This bootstrap consensus is parsed using MdConsensus::parse in directory.rs (lines 31-55), allowing the client to learn about available relays before establishing its first bridge connection.

What bridge protocols does webtor-rs support?

The library supports three bridge types as defined in the configuration: Snowflake WebSocket, Snowflake WebRTC, and WebTunnel. The establish_channel_impl function in client.rs (lines 104-170) selects the appropriate implementation, creates the raw stream using protocol-specific modules (snowflake_ws.rs, snowflake.rs, or webtunnel.rs), and then upgrades it to a TLS channel using the tor-proto crate's ChannelBuilder.

How does circuit isolation prevent tracking across different websites?

The CircuitManager maintains separate circuits for different isolation keys. When making a request, the client generates an IsolationKey (often derived from the target domain or a user-defined string) and calls get_circuit_for_isolation_key. This ensures that traffic to example.com travels through a different three-hop path than traffic to rust-lang.org, preventing exit nodes or malicious intermediaries from correlating user activity across different origins.

Can webtor-rs be used for one-off requests without keeping a persistent client?

Yes, the fetch_one_time static method creates a temporary TorClient, optionally establishes an early circuit, executes a single HTTP request, and then tears down all resources. This is useful for command-line tools or browser extensions that need to fetch a single resource over Tor without the memory overhead of maintaining a long-running circuit manager.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →