# How Webtor-RS Manages Tor Circuit Logic: Lifecycle, Isolation, and Thread-Safe Architecture

> Explore how Webtor-RS manages Tor circuit logic with a thread-safe CircuitManager, ensuring stream isolation and efficient circuit reuse for robust performance.

- Repository: [igor53627/webtor-rs](https://github.com/igor53627/webtor-rs)
- Tags: internals
- Published: 2026-03-04

---

**Webtor-RS implements Tor circuit logic through a centralized `CircuitManager` that creates, tracks, and reuses circuits using thread-safe `Arc<RwLock<Circuit>>` structures while enforcing stream isolation via domain-specific binding keys.**

Webtor-RS is an open-source Rust implementation of a Tor client designed for both native and WebAssembly targets. At its core, the library manages Tor circuit logic through a sophisticated `circuit` module that handles everything from relay selection to stream isolation. This article examines how the `CircuitManager` coordinates circuit lifecycle, resource limits, and concurrent access patterns according to the `igor53627/webtor-rs` source code.

## Core Architecture of Circuit Management

### The Circuit Structure

The fundamental unit is the **`Circuit`** struct defined in [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs) (Lines 35-48). Each circuit holds a unique identifier (UUID), state timestamps, selected relays (bridge, middle, and exit), and an underlying `ClientTunnel`. It also stores an optional **isolation key** that binds the circuit to a specific destination domain, preventing cross-site correlation attacks.

### Circuit Lifecycle States

Circuit state transitions are governed by the **`CircuitStatus`** enum (Lines 25-33). The status progresses through `Creating`, `Ready`, `Extending`, `Failed`, and `Closed` states. This state machine ensures that only `Ready` circuits can accept new streams, while `Creating` circuits block callers until the Fast-handshake completes.

### The CircuitManager

The **`CircuitManager`** (Lines 90-98) serves as the central authority for all circuit operations. It maintains a thread-safe list using `Arc<RwLock<Vec<Arc<RwLock<Circuit>>>>>`, a double-locking pattern that allows concurrent reads of the circuit list while permitting exclusive access to individual circuits. The manager collaborates with `RelayManager` (from [`webtor/src/relay.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/relay.rs)) for relay selection and maintains a `Channel` for the underlying Tor connection.

## Circuit Creation and Relay Selection

### Multi-Hop Relay Selection

When building a new circuit, the manager selects three relays to form the Tor path: a bridge (Snowflake), a middle relay, and an exit relay. The selection logic in [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs) (Lines 71-104 and 118-146) uses `relay::selection` helpers to avoid fingerprint collisions while optimizing for performance. This process respects the circuit parameters generated by **`make_circ_params`** (Lines 31-81), which configures congestion-control, flow-control, and round-trip estimation for each hop.

### The Creation Flow

The `create_circuit_with_isolation` method orchestrates circuit construction:

1. Generates a UUID for the `circuit_id`.
2. Retrieves a `Channel` for the underlying Tor connection.
3. Initiates a pending tunnel and executes the Fast-handshake for the bridge.
4. Extends to middle and exit relays using parameters from `make_circ_params`.
5. Stores the three relays in the `Circuit` object and transitions status to `Ready`.

## Stream Isolation and Circuit Reuse

### Isolation Key Binding

Stream isolation is enforced through **`set_isolation_key`** (Lines 78-84). When a request includes a domain-derived `IsolationKey` (implemented in [`webtor/src/isolation.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/isolation.rs)), the manager searches existing circuits for one already bound to that key. If found, the circuit is reused; if not, an unassigned circuit is bound under the same write lock to prevent race conditions. The system respects `MAX_CIRCUITS_PER_ISOLATION_KEY` (defined in [`webtor/src/config.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/config.rs)) to limit resource consumption per domain.

### Circuit Retrieval Logic

The **`get_circuit_for_isolation_key`** method implements a three-tier selection strategy:

- **First**, search for a `Ready` circuit matching the isolation key.
- **Second**, bind an unassigned circuit to the key if available.
- **Third**, create a new circuit bound to the key if limits permit.

If the per-key limit is reached, the manager reuses any non-failed circuit for that key rather than creating a new one.

## Maintenance and Resource Management

### Proactive Pre-building

To minimize latency, the manager implements **`maybe_prebuild_circuit`** (Lines 130-170). This background process creates spare circuits when the average age of existing circuits exceeds a configurable threshold (e.g., 30 seconds). A `prebuild_in_progress` atomic flag prevents concurrent pre-build operations, ensuring deterministic resource usage.

### Periodic Cleanup

The **`cleanup_circuits`** method (Lines 104-155) runs maintenance to remove unhealthy entries. It discards circuits that are `Failed`, stale (≥ 1 hour old), or idle (≥ 10 minutes without activity). The algorithm always retains at least one active circuit to prevent connection starvation, even when all circuits exceed idle timeouts. Timestamps are handled via [`webtor/src/time.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/time.rs), which provides `Instant` wrappers compatible with both native and WASM targets.

## Practical Implementation Example

Here is how to request an isolated circuit and initiate a stream:

```rust
use std::sync::Arc;
use tokio::sync::RwLock;
use webtor::circuit::{CircuitManager, IsolationKey};

// Initialize the manager (relay_manager & channel set up elsewhere)
let manager = CircuitManager::new(relay_manager.clone(), channel.clone());

// 1️⃣ Obtain a circuit bound to a specific domain (stream isolation)
let isolation = IsolationKey::from_string("example.com");
let circuit = manager
    .get_circuit_for_isolation_key(Some(isolation))
    .await
    .expect("Failed to get circuit");

// 2️⃣ Open a TCP stream through the circuit
let stream = circuit
    .read()
    .await
    .begin_stream("www.google.com", 443)
    .await
    .expect("Failed to open stream");

```

To maintain optimal performance, trigger background maintenance:

```rust
// Pre-build a spare circuit when average age exceeds 30 seconds
let max_circuits = 10;
let age_threshold = std::time::Duration::from_secs(30);
manager.maybe_prebuild_circuit(max_circuits, age_threshold).await;

// Periodic cleanup (e.g., run every minute)
manager.cleanup_circuits().await.expect("Cleanup failed");

```

## Summary

- **Webtor-RS** manages circuit logic through the `CircuitManager` in [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs), using thread-safe `Arc<RwLock<Circuit>>` structures for concurrent access.
- **Stream isolation** is enforced via `IsolationKey` binding, ensuring different domains never share circuits unless explicitly permitted by `MAX_CIRCUITS_PER_ISOLATION_KEY` limits.
- **Relay selection** uses a three-hop strategy (bridge, middle, exit) with collision avoidance, configured through `make_circ_params` for congestion and flow control.
- **Lifecycle management** includes proactive pre-building via `maybe_prebuild_circuit` and aggressive cleanup of stale/failed circuits via `cleanup_circuits`.
- **State transitions** follow the `CircuitStatus` enum (`Creating` → `Ready` → `Closed`), with only `Ready` circuits eligible for stream initiation via `begin_stream`.

## Frequently Asked Questions

### How does webtor-rs ensure stream isolation between different domains?

Webtor-RS enforces stream isolation through the `IsolationKey` mechanism. When requesting a circuit via `get_circuit_for_isolation_key`, the manager either finds an existing circuit bound to that domain's key or binds an unassigned circuit using `set_isolation_key` under a write lock. This ensures that traffic from `example.com` never traverses the same circuit as `example.org`, preventing cross-site correlation attacks unless circuit limits force reuse.

### What happens when no suitable circuit exists for a request?

When `get_circuit_for_isolation_key` finds no matching or unassigned circuit, it invokes `create_circuit_with_isolation` to build a fresh circuit. This method generates a UUID, executes the Fast-handshake with a bridge relay, extends to middle and exit nodes, and binds the resulting circuit to the caller's isolation key before marking it as `Ready` in the shared `Arc<RwLock<Vec<...>>>` structure.

### How does the CircuitManager handle concurrent access to circuit state?

The manager implements a double-locking strategy: the circuit list itself is protected by `Arc<RwLock<Vec<Arc<RwLock<Circuit>>>>>`, while individual circuits maintain their own `RwLock`. This allows concurrent reads of the circuit registry while enabling exclusive write access to specific circuits during state transitions or isolation key binding, preventing race conditions during high-concurrency scenarios.

### What maintenance operations keep the circuit pool healthy?

Two background processes maintain circuit health: `maybe_prebuild_circuit` creates spare circuits proactively when average circuit age exceeds thresholds, and `cleanup_circuits` removes entries that are `Failed`, idle for ≥ 10 minutes, or stale beyond 1 hour. The cleanup algorithm guarantees at least one active circuit remains available, preventing connection starvation while reclaiming resources from unhealthy tunnels.