onionlink
onionlink is a small Rust Tor v3 onion-service client with Python bindings. It talks directly to Tor relays, builds the minimum circuits needed for v3 onion-service access, and can exchange raw bytes or a simple HTTP request with the service.
Security and anonymity are explicit non-goals. This is a protocol experiment and interoperability tool, not a replacement for Tor Browser, Arti, or the Tor daemon.
What It Implements
- Downloads and parses the live microdescriptor consensus.
- Hydrates relay microdescriptors to obtain Ed25519 identities and ntor keys.
- Derives the v3 onion-service blinded key and subcredential.
- Selects HSDirs and fetches the v3 descriptor over a guarded
EXTEND2circuit. - Decrypts unprotected v3 onion-service descriptors.
- Parses introduction points, including link specifiers, intro ntor keys, auth keys, and service encryption keys.
- Establishes a rendezvous point over a guarded
EXTEND2circuit. - Sends
INTRODUCE1over a guarded intro-point circuit. - Completes hs-ntor from
RENDEZVOUS2. - Opens a stream to
:<port>and sends/receives relay data.
Dependencies
- Rust 1.83 or newer
- Python 3.10 or newer for the Python package
maturinfor Python wheel builds
On Arch Linux:
On Debian/Ubuntu-style systems:
|
Build
Build Linux Python wheels with Docker:
This writes Python 3.10+ manylinux wheels into dist/, including cp313t
and cp314t free-threaded wheels. The wheel build is Linux-only and uses
maturin to build the PyO3 extension.
Build a wheel directly on a Linux host with the native dependencies installed:
Run deterministic Rust parity tests:
Run Python API tests after installing the test extra:
PYTHONPATH=src
Python Client
The Python package exposes an OOP session API. A Session downloads the
microdescriptor consensus and hydrates relay microdescriptors once, then reuses
that directory state for multiple onion-service requests. Request methods release
the Python GIL while doing network work, so one initialized session can be used
from asyncio.to_thread, a ThreadPoolExecutor, or regular worker threads.
=
return .
=
For native asyncio call sites, use AsyncSession. It keeps the synchronous
API available and uses the native PyO3/Tokio awaitables when the compiled
extension is available:
= await
Cancelling an awaited async request stops waiting for its result, but the
underlying native blocking task can continue until the request finishes or the
configured timeout_ms is reached.
The PyO3 extension declares gil_used = false, and native bootstrap/request
work detaches from the Python runtime while running. Free-threaded wheels are
therefore built for py313t and py314t without re-enabling the GIL on import.
Raw request bytes are also supported:
=
=
Use request() for full HTTP control:
=
=
Session constructor arguments:
bootstrap: HTTP directory cache ashost:port.consensus_file: optional localconsensus-microdescfile.timeout_ms: TCP/TLS read timeout.verbose: print native bootstrap and rendezvous progress to stderr.
Request methods:
request(method, onion, *, port=80, path="/", params=None, headers=None, body=None, data=None, json=None, form=None, host=None, http_version="HTTP/1.0", response_limit=4194304) -> Responseget/head/post/put/patch/delete/options(onion, **request_options) -> Responseraw_request(onion, port, payload=b"", response_limit=4194304) -> bytes
AsyncSession exposes the same request methods as awaitables, plus
await AsyncSession.create(...) for eager initialization and
async with AsyncSession(...) for context-manager style initialization.
Response exposes status_code, reason, headers, body, raw,
http_version, ok, text, encoding, header(name), and
raise_for_status().
Usage
Fetch / over HTTP from the container:
Send raw text:
Forward standard input:
|
Options
--bootstrap host:portselects the HTTP directory cache used for bootstrap. The default is128.31.0.39:9131.--consensus-file pathuses a local microdescriptor consensus instead of downloading one.--timeout-ms nsets TCP/TLS read timeouts. The default is30000.--http-get [path]sends a simple HTTP/1.0 GET after connecting. Ifpathis omitted,/is used.--send textsends raw text after the stream opens.--stdinforwards standard input after the stream opens.--verboseenables progress logging for bootstrap, descriptor, intro, rendezvous, and stream activity.
Logging uses env_logger. --verbose defaults the CLI log level to info; set
RUST_LOG for more control, for example:
RUST_LOG=onionlink_core=debug,onionlink=info
If no send mode is provided, --http-get / is used by default.
Limitations
The implementation intentionally omits substantial parts of a real Tor client:
- no consensus, directory, relay, descriptor, or certificate signature validation;
- no relay-family, guard, path-bias, or anonymity-aware path selection;
- no bridges, pluggable transports, proxies, IPv6 dialing, or DNS helpers;
- no onion-service client authorization;
- no proof-of-work support for protected services;
- no authenticated SENDMEs or modern congestion-control behavior;
- no stream isolation, SOCKS server, circuit pooling, or persistent state;
- no traffic shaping or padding.
The client uses direct TLS connections to selected relays and short guarded circuits only where current relays require them, such as HSDir descriptor fetches, rendezvous establishment, and intro-point delivery.
Notes
The default bootstrap source is a public Tor directory authority/cache endpoint. Live behavior depends on relay reachability, descriptor availability, and the onion service accepting a connection at the requested port.