# How webtor-rs Implements Stream Isolation: Architecture and Code Examples

> Explore how webtor-rs implements stream isolation by grouping HTTP requests into separate Tor circuits. Learn about its architecture and view code examples for key derivation and circuit binding.

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

---

**webtor-rs implements stream isolation by grouping HTTP requests into separate Tor circuits based on a configurable `StreamIsolationPolicy`, using unique isolation keys derived from URLs to bind specific requests to dedicated circuits.**

The webtor-rs library provides a Rust-based interface to the Tor network with built-in support for **stream isolation**—a privacy feature that prevents different websites from sharing the same circuit. This implementation, located primarily in [`webtor/src/isolation.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/isolation.rs) and [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs), allows developers to enforce circuit separation based on domains, subdomains, or full origins, mirroring the first-party isolation model used by the Tor Browser.

## Stream Isolation Policies

At the core of the implementation is the `StreamIsolationPolicy` enum, which defines four distinct strategies for grouping requests. The policy is stored in `TorClientOptions` and defaults to `PerDomain`, ensuring that requests to different registrable domains use separate circuits.

### The StreamIsolationPolicy Enum

The policy is defined in [`webtor/src/isolation.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/isolation.rs) as a serializable, copyable enum:

```rust
/// Stream isolation policy determining how requests are grouped into circuits
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StreamIsolationPolicy {
    /// eTLD+1 (first-party domain) – all sub-domains share a circuit
    PerDomain,
    /// Full hostname – different sub-domains get different circuits
    PerSubdomain,
    /// Full origin (scheme + host + port) – different ports or http/https are separate
    PerOrigin,
    /// No isolation – legacy behaviour, all requests share circuits
    None,
}

```

* **PerDomain**: Treats all subdomains of the same registrable domain (eTLD+1) as a single isolation group. This matches Tor Browser's default behavior where `foo.example.com` and `bar.example.com` share a circuit.
* **PerSubdomain**: Uses the full hostname, isolating `foo.example.com` from `bar.example.com`.
* **PerOrigin**: Considers the scheme, host, and port together, meaning `https://example.com:443` and `http://example.com:8080` receive separate circuits.
* **None**: Disables isolation entirely, falling back to legacy circuit pooling.

The default value is `PerDomain`, configured via `TorClientOptions::stream_isolation` in [`webtor/src/config.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/config.rs).

## Isolation Key Generation

An **IsolationKey** uniquely identifies the group a request belongs to. It is constructed from a URL according to the selected policy using the `IsolationKey::from_url` method in [`webtor/src/isolation.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/isolation.rs).

### Generating Isolation Keys from URLs

The key generation logic extracts the relevant components based on the active policy:

```rust
pub fn from_url(url: &Url, policy: StreamIsolationPolicy) -> Option<Self> {
    match policy {
        StreamIsolationPolicy::None => None,
        _ => {
            let host = url.host_str().unwrap_or("");
            let port = url.port_or_known_default().unwrap_or(0);
            let key = match policy {
                StreamIsolationPolicy::PerOrigin => format!("{}://{}:{}", url.scheme(), host, port),
                StreamIsolationPolicy::PerSubdomain => host.to_string(),
                StreamIsolationPolicy::PerDomain => extract_domain(host),
                StreamIsolationPolicy::None => unreachable!(),
            };
            Some(IsolationKey(key))
        }
    }
}

```

For `PerDomain` isolation, the implementation uses the **psl** crate to extract the registrable domain (`example.com` from `foo.bar.example.co.uk`). The `PerSubdomain` variant uses the raw hostname, while `PerOrigin` concatenates the scheme, host, and port into a single identifier.

## Circuit Management and Binding

Each `Circuit` optionally stores an `Option<IsolationKey>`. When a circuit is created with a specific key, it is bound **before** being added to the global circuit list to prevent race conditions where two different keys could claim the same unassigned circuit.

### Binding Isolation Keys to Circuits

The `create_circuit_with_isolation` method in [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs) handles the atomic binding:

```rust
pub async fn create_circuit_with_isolation(
    &self,
    isolation_key: Option<IsolationKey>,
) -> Result<Arc<RwLock<Circuit>>> {
    // ... circuit creation logic ...
    let mut circuit = Circuit::new(circuit_id.clone(), Some(Arc::new(tunnel)));
    
    // Bind isolation key BEFORE adding to list to prevent races
    if let Some(key) = isolation_key {
        circuit.set_isolation_key(key);
    }
    
    let circuit_arc = Arc::new(RwLock::new(circuit));
    circuits.push(circuit_arc.clone());
    Ok(circuit_arc)
}

```

This ensures that once a circuit appears in the shared pool, it is already associated with its isolation group.

### Circuit Selection Logic

The `CircuitManager::get_circuit_for_isolation_key` method implements the four-tier selection strategy found in [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs):

1. **Reuse**: Find an existing ready circuit already bound to the requested key.
2. **Bind**: Claim an unassigned ready circuit and bind it to the key (first-come-first-serve).
3. **Limit**: Enforce `MAX_CIRCUITS_PER_ISOLATION_KEY` (currently set to 1). If the limit is reached, reuse an existing circuit for that key.
4. **Create**: Spawn a new circuit already bound to the isolation key.

If the isolation policy is `None`, the method returns `None` and the HTTP client falls back to general circuit pooling.

## HTTP Client Integration

The `TorHttpClient` bridges the isolation layer to actual HTTP requests. Before executing a request, it derives the isolation key from the target URL and requests a matching circuit from the manager.

In [`webtor/src/http.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/http.rs), the client performs the following:

```rust
let isolation_key = IsolationKey::from_url(&url, self.isolation_policy);
let circuit = circuit_manager
    .get_circuit_for_isolation_key(isolation_key)
    .await?;

```

If `isolation_policy` is `None`, `isolation_key` becomes `None`, triggering the legacy "any available circuit" behavior. Otherwise, the manager guarantees the returned circuit belongs exclusively to the key's isolation group.

## Configuring Stream Isolation

Users configure the isolation behavior through `TorClientOptions` during client initialization. The policy can be customized via the builder pattern:

```rust
pub struct TorClientOptions {
    // ... other fields ...
    /// Stream isolation policy for domain-based circuit separation
    #[serde(default)]
    pub stream_isolation: StreamIsolationPolicy,
}

```

The `with_stream_isolation` method allows runtime configuration, while deserialization defaults ensure backward compatibility.

## Practical Usage Examples

### Example 1: Per-Domain Isolation (Default)

Create a client that isolates circuits by registrable domain, matching Tor Browser behavior:

```rust
use webtor::client::TorClient;
use webtor::config::TorClientOptions;
use webtor::isolation::StreamIsolationPolicy;

let opts = TorClientOptions::default()
    .with_stream_isolation(StreamIsolationPolicy::PerDomain);

let client = TorClient::new(opts).await?;

// Both requests share the same circuit bound to "example.com"
let _ = client.http().get("https://example.com/").await?;
let _ = client.http().get("https://sub.example.com/").await?;

```

### Example 2: Per-Subdomain Isolation

Enforce stricter isolation where different subdomains receive separate circuits:

```rust
let opts = TorClientOptions::default()
    .with_stream_isolation(StreamIsolationPolicy::PerSubdomain);
let client = TorClient::new(opts).await?;

// Different subdomains → different circuits
let _ = client.http().get("https://foo.example.com/").await?;
let _ = client.http().get("https://bar.example.com/").await?;

```

### Example 3: Disable Isolation

Revert to legacy behavior where all requests share a global circuit pool:

```rust
let opts = TorClientOptions::default()
    .with_stream_isolation(StreamIsolationPolicy::None);
let client = TorClient::new(opts).await?;

// Circuit selection ignores domains
let _ = client.http().get("https://site-a.com/").await?;
let _ = client.http().get("https://site-b.org/").await?;

```

### Example 4: Manual Isolation Key Generation

Directly inspect how URLs map to isolation keys:

```rust
use webtor::isolation::{IsolationKey, StreamIsolationPolicy};
use url::Url;

let url = Url::parse("https://foo.bar.example.co.uk:8443/path")?;
let key = IsolationKey::from_url(&url, StreamIsolationPolicy::PerDomain);
assert_eq!(key.unwrap().to_string(), "example.co.uk");

```

## Summary

- **Stream isolation** in webtor-rs is implemented through the `StreamIsolationPolicy` enum with four variants: `PerDomain`, `PerSubdomain`, `PerOrigin`, and `None`.
- **IsolationKey** structs are generated from URLs via `IsolationKey::from_url` in [`webtor/src/isolation.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/isolation.rs), using the **psl** crate for domain extraction.
- Each `Circuit` optionally stores an isolation key set atomically during creation in [`webtor/src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/circuit.rs) to prevent race conditions.
- The `CircuitManager` selects circuits using a four-step priority system: reuse existing, bind unassigned, enforce per-key limits (`MAX_CIRCUITS_PER_ISOLATION_KEY = 1`), or create new.
- The **HTTP client** in [`webtor/src/http.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/http.rs) automatically derives isolation keys from request URLs and requests matching circuits from the manager.
- Configuration occurs through `TorClientOptions::stream_isolation` in [`webtor/src/config.rs`](https://github.com/igor53627/webtor-rs/blob/main/webtor/src/config.rs), defaulting to `PerDomain` isolation.

## Frequently Asked Questions

### What is the default stream isolation policy in webtor-rs?

The default policy is `PerDomain`, which corresponds to first-party isolation. This means all subdomains of the same registrable domain (eTLD+1) share a single Tor circuit, preventing third-party tracking while optimizing connection reuse.

### How does webtor-rs prevent race conditions when binding circuits?

When `create_circuit_with_isolation` creates a new circuit, it calls `circuit.set_isolation_key(key)` **before** wrapping the circuit in an `Arc<RwLock>` and pushing it to the global circuit list. This ensures the key is assigned atomically, preventing two different isolation groups from concurrently claiming the same unassigned circuit.

### Can I use different isolation policies for different requests in the same client?

No, the `StreamIsolationPolicy` is set once during `TorClient` initialization via `TorClientOptions`. All HTTP requests made through that client instance use the same policy. To use different policies simultaneously, you must create separate `TorClient` instances with different configurations.

### What happens when the per-key circuit limit is reached?

The implementation enforces `MAX_CIRCUITS_PER_ISOLATION_KEY` (currently hardcoded to 1). When a request requires a circuit for a key that already has an active circuit, the manager reuses the existing circuit rather than creating a new one. This prevents resource exhaustion while maintaining isolation integrity.