Onion Routing with TorJS
tor-js lets you make HTTP requests through the Tor network from JavaScript. Not through a proxy. Not through a server you control. A real Tor client — circuit construction, relay encryption, directory consensus — running entirely in your browser or Node.js process.
The core is Arti, the Tor
Project's official Rust implementation, compiled to WebAssembly. The TypeScript wrapper
provides a clean fetch()-like API on top.
Quick Start
import { TorClient } from 'tor-js';
const client = new TorClient({
gateway: 'https://tor-js-gateway.example.com',
});
const response = await client.fetch('https://check.torproject.org/api/ip');
console.log(await response.json()); // { IsTor: true, IP: "..." } Architecture
Here's how the pieces fit together when running in a browser:
client.fetch("https://example.com") 3 layers of onion encryption — each relay peels one
The Layers
1. Arti → WebAssembly
Arti is ~200k lines of Rust across dozens of crates. Compiling it to WASM required
replacing every platform assumption: std::time::Instant (no monotonic clock
in browsers), async_trait with Send bounds (WASM is single-threaded),
SQLite (no filesystem), native TLS (no OpenSSL). Two new crates handle the cross-platform
abstractions: tor-time for time and tor-async-compat for
conditional Send bounds.
2. WasmRuntime
Arti is runtime-agnostic — it works with Tokio, async-std, or anything implementing its
Runtime trait. WasmRuntime implements this trait for browsers:
setTimeout for sleep, wasm-bindgen-futures for task spawning,
and a JS callback for opening socket connections. TLS uses
rustls with rustls-rustcrypto (pure Rust, no C dependencies) —
the same TLS library, just with a WASM-compatible crypto backend.
3. ArtiSocketProvider
Arti needs TCP connections to Tor relays. Browsers can't open raw TCP sockets.
ArtiSocketProvider bridges this gap with a strategy pattern:
| Strategy | Transport | Environment |
|---|---|---|
direct | Native TCP (node:net / Deno.connect) | Node.js, Deno |
webrtc | Multiplexed data channels via gateway | Browsers with WebRTC |
websocket | One WebSocket per relay via gateway | Any browser |
In Node.js and Deno, no gateway is needed — connections go directly to Tor relays. In browsers, a gateway server proxies the bytes between WebSocket/WebRTC and the real TCP relay. The gateway is a byte pipe; all Tor protocol — encryption, circuit management, cell formatting — happens in the browser.
4. Storage
Tor clients need to persist directory state (consensus documents, microdescriptors, guard
lists) to avoid re-downloading everything on each page load. The KeyValueStore
trait provides a single interface that splits into state and directory storage internally.
In browsers it's backed by IndexedDB, in Node.js by the filesystem. A preloading cache
(CachedJsStorage) loads everything into a HashMap at startup so Arti's
synchronous storage reads don't block the main thread.
5. Fast Bootstrap
A cold Tor bootstrap normally downloads consensus + microdescriptors + authority certs
from relays through Tor circuits — slow when you don't have circuits yet. Fast bootstrap
short-circuits this: the gateway server pre-packages these documents into a single
bootstrap.zip.br file. The client fetches it over plain HTTPS (not through Tor),
seeds storage, and then Arti validates the cached data normally.
The speed comes from brotli compression, handled transparently by the browser's
Content-Encoding support. The full bundle compresses from ~40 MB down to ~3 MB,
cutting first-connect time from minutes to seconds.
Trust model: The fast bootstrap bundle is fetched from the gateway, but this doesn't require trusting the gateway. Arti verifies every document using cryptographic signatures from Tor's directory authorities. A malicious gateway can't inject fake relays or forge directory data.
What Happens When You Call fetch()
Step 1: Bootstrap
On new TorClient(), Arti downloads (or loads from cache) a network consensus —
the list of all ~10,000 Tor relays and their capabilities, signed by the directory
authorities. It selects guard relays, builds circuits, and signals readiness.
Step 2: Circuit Construction
Arti builds a 3-hop circuit: Guard → Middle → Exit. Each hop involves an ntor handshake — a one-round-trip authenticated key exchange based on Curve25519. The client combines its ephemeral key with the relay's long-term identity key to derive shared secret material, proving the relay's identity and establishing forward-secret session keys.
The result is layered encryption — the onion in "onion routing." The client encrypts data three times: first with the exit relay's key, then the middle's, then the guard's. Each relay peels off one layer and forwards to the next. No single relay ever knows both who you are and what you're accessing.
Step 3: TLS & HTTP
tor-js builds a raw HTTP request, wraps it in a TLS connection to the destination (through
the exit relay), and streams it through the circuit. The response flows back through the
same circuit, and the client returns a standard Response object.
Note on TLS: There are two TLS layers. The inner TLS protects
your HTTP request end-to-end (browser ↔ destination, using WebPKI certificates). The
outer TLS protects the Tor protocol between relays (using Tor's own certificate
scheme). Both use rustls.
The Gateway
The gateway server is built with Arti (native, not WASM) and serves two roles:
- Relay proxying: Accept a WebSocket or WebRTC connection, open a TCP connection to the target Tor relay, and pipe bytes bidirectionally. The gateway tracks the Tor consensus and only allows connections to addresses listed as Tor relays — preventing abuse as a general-purpose proxy.
- Fast bootstrap: Serve a pre-built
bootstrap.zip.brwith brotliContent-Encoding, so browsers decompress it natively.
The gateway never sees your traffic. All data is encrypted under Tor's layered encryption before it reaches the gateway. And the bootstrap data is verified by Arti using directory authority signatures.
In Node.js, no gateway is needed at all. ArtiSocketProvider uses
the direct strategy and connects to Tor relays without any intermediary.
Bundle Size
| Component | Size (gzip) | Notes |
|---|---|---|
| WASM binary | ~1.7 MB | All of Arti + rustls + crypto |
| JS wrapper | ~15 kB | TypeScript compiled to ESM |
| CDN entry point | ~15 kB | Fetches WASM on first use |
| Base64 entry point | ~2.3 MB | WASM embedded in JS (single file) |
The CDN entry point (import from 'tor-js') is recommended for production.
The WASM binary's SHA-256 hash is baked into the wrapper at build time — the CDN cannot
serve a tampered binary.
Try It
# From Node.js:
npm install tor-js
# tor-fetch.js
import { TorClient } from 'tor-js';
const client = new TorClient();
const res = await client.fetch('https://check.torproject.org/api/ip');
console.log(await res.json());
client.close(); Source: github.com/voltrevo/arti • Gateway: github.com/voltrevo/tor-fast-bootstrap