How to Use webtor-rs in a Web Browser: A Complete WASM Guide

webtor-rs compiles to WebAssembly and runs a full Tor client in any modern browser, exposing a JavaScript API for anonymous HTTP requests through Snowflake bridges without requiring browser plugins.

webtor-rs is a pure-Rust Tor client maintained by igor53627 that targets WebAssembly (WASM) to bring censorship-resistant browsing directly to web pages. According to the source code in webtor-wasm/src/lib.rs, the library uses wasm-bindgen to expose Rust structs like TorClient and TorClientOptions to JavaScript, while the underlying implementation in webtor/src/client.rs handles circuit creation using the official Arti tor-proto stack.

Building and Loading the WASM Module

The workflow begins with wasm-pack, which generates a JavaScript glue file (webtor_wasm.js) and the binary (webtor_wasm_bg.wasm). The module exports an initialization function that verifies secure cryptographically-secure pseudorandom number generator (CSPRNG) availability and installs a tracing layer that forwards Rust logs to the browser console.

In your HTML or JavaScript module, load the package dynamically and bootstrap the runtime:

import * as wasm from './pkg/webtor_wasm.js';

// Fetch and instantiate the .wasm binary
await wasm.default();

// Verify CSPRNG and set up logging
await wasm.init();

The init() function in webtor-wasm/src/lib.rs performs a mandatory security check that aborts early in insecure environments, ensuring the Tor client has access to SubtleCrypto for key generation.

Configuring TorClientOptions for Browser Use

The bindings expose a JavaScript class that mirrors the Rust TorClientOptions builder pattern defined in webtor/src/config.rs. In browser environments, only the Snowflake transport is supported, so you must provide a Snowflake relay URL during construction.

const options = new wasm.TorClientOptions('wss://snowflake.torproject.net/')
    .withCreateCircuitEarly(true)      // Start handshake immediately
    .withConnectionTimeout(60_000)     // 60 seconds in milliseconds
    .withCircuitTimeout(180_000);      // 3 minutes

The BridgeType enum in webtor/src/config.rs determines transport selection; WASM builds restrict this to Snowflake (WebSocket) and SnowflakeWebRtc. All builder methods map 1-to-1 to the underlying Rust implementation, allowing fine-tuning of timeouts and early circuit creation.

Instantiating the Tor Client

Create the client by passing the options to the TorClient constructor, which returns a JavaScript Promise that resolves to a wrapper holding an Arc pointer to the native webtor::TorClient:

const client = await new wasm.TorClient(options);
console.log('Tor client ready');

The constructor logic in webtor-wasm/src/lib.rs (lines 24-44) handles the conversion between JavaScript objects and Rust structures, managing the reference-counted pointer to ensure safe memory access across the WASM boundary.

Establishing Tor Circuits

Before making requests, you must wait for the client to bootstrap and build a 3-hop circuit. The waitForCircuit() method initiates a connection through the Snowflake WebSocket, then layers Turbo framing, KCP reliability, and SMUX multiplexing before establishing the TLS channel:

await client.waitForCircuit();
console.log('Circuit established');

The implementation in webtor/src/client.rs uses the wait_for_circuit method to create a tor_proto::Channel and manage circuit state through the CircuitManager. This process includes loading an embedded compressed consensus snapshot from webtor/src/cached/consensus.txt.br to avoid CORS issues during directory fetches.

Making Anonymous HTTP Requests

Once the circuit is active, the wrapper provides JavaScript-friendly methods for HTTP traffic. All requests route through the established Tor circuit and return a JsHttpResponse object containing status, headers, body (as Uint8Array), and url.

Simple GET request:

const resp = await client.fetch('https://api64.ipify.org?format=json');
const body = await resp.text();
console.log('Exit IP:', JSON.parse(body).ip);

POST with JSON:

const resp = await client.postJson('https://httpbin.org/post', { foo: 'bar' });
const json = await resp.json();

Generic request with custom headers and timeout:

const resp = await client.request(
    'POST',
    'https://httpbin.org/post',
    { 'Content-Type': 'application/json', 'X-Custom-Header': 'value' },
    new TextEncoder().encode(JSON.stringify({ data: 'payload' })),
    30_000  // Timeout in milliseconds
);

The request methods in webtor-wasm/src/lib.rs handle conversion between JavaScript strings/Uint8Arrays and Rust byte vectors, while the underlying client in webtor/src/client.rs manages stream multiplexing across the Tor circuit.

Error Handling and Lifecycle Management

All Rust TorError types convert to structured JsTorError objects containing:

  • code: Stable identifier (e.g., "CIRCUIT_CREATION")
  • kind: Category ("network", "timeout", "configuration")
  • message: Human-readable description
  • retryable: Boolean indicating if retry might succeed
try {
    const resp = await client.fetch('https://example.invalid');
} catch (err) {
    console.error('Tor error:', err.code, err.message);
    if (err.retryable) {
        // Implement retry logic
    }
}

When finished, explicitly close the client to shut down channels and clean up resources:

await client.close();

You can also abort pending operations immediately using client.abort().

React Integration Example

For React applications, dynamically import the WASM package in a component effect as demonstrated in example/src/App.jsx:

const loadWasm = async () => {
    const wasm = await import('../pkg/webtor_wasm.js');
    await wasm.default();
    wasm.init();
    return wasm;
};

// Inside component
const client = await new wasm.TorClient(options);
await client.waitForCircuit();
const response = await client.fetch('https://check.torproject.org');
// ...
await client.close();

Summary

  • Load the module using dynamic import() and call wasm.default() followed by wasm.init() to initialize the CSPRNG and logging.
  • Configure options with TorClientOptions, specifying a Snowflake bridge URL as the transport (the only supported method in browsers).
  • Instantiate the client with new wasm.TorClient(options) and await waitForCircuit() to complete the 3-hop Tor handshake.
  • Perform requests using fetch(), post(), postJson(), or the generic request() method for full control over HTTP semantics.
  • Handle errors by catching JsTorError objects that expose structured metadata for programmatic retry decisions.
  • Clean up by calling client.close() to release the Arc-held native client and terminate WebSocket connections.

Frequently Asked Questions

What browsers support webtor-rs?

Any browser supporting WebAssembly and the WebCrypto API (specifically SubtleCrypto) can run webtor-rs, including Chrome, Firefox, Safari, and Edge. The initialization function in webtor-wasm/src/lib.rs explicitly checks for secure randomness availability and aborts if the environment lacks required cryptographic primitives.

Which transport protocols work in the browser?

Only Snowflake bridges function in WASM builds. The BridgeType enum in webtor/src/config.rs restricts browser clients to Snowflake (WebSocket-based) or SnowflakeWebRtc transports, routing through Turbo framing, KCP reliability, and SMUX multiplexing before reaching the Tor network. Traditional TCP or obfs4 bridges are unavailable due to browser sandboxing constraints.

How does webtor-rs handle the Tor consensus without CORS issues?

The client embeds a compressed consensus snapshot directly in the WASM binary at webtor/src/cached/consensus.txt.br. During bootstrap, the Directory implementation loads this embedded data before attempting network fetches, eliminating CORS complications since the initial relay discovery requires no external HTTP requests.

Can I use webtor-rs with frontend frameworks like React or Vue?

Yes. The repository includes a complete React example in example/src/App.jsx showing dynamic module loading, state management for the Tor client, and proper cleanup in component unmount effects. The WASM package exports standard ES modules compatible with webpack, Vite, and other modern bundlers.

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 →