Tor hidden service toolkit for Python — drop-in client + zero-config server.
Consume any .onion API, or publish your own — in 3 lines of Python.
from tornion import client, server
# Consume a .onion API
r = client.get("http://xxxxxxxxxxxxxx.onion/ping")
# Publish your own
from fastapi import FastAPI
app = FastAPI()
server.serve(app)That's it. No Docker, no torrc, no manual tor install — tornion handles
everything: discovers or downloads the tor binary, spawns it, manages the
SOCKS proxy or the hidden service, and tears it down on exit.
- 🔌 Drop-in
requests—client.Sessionis a realrequests.Sessionsubclass - 🚀 Zero-config server —
server.serve(app)takes any ASGI or WSGI app - 🧠 Smart tor reuse — auto-detects an already-running tor on
:9050/:9150 - 📦 Auto-install tor — downloads the official Tor Expert Bundle on first use
- 🎯 Framework-agnostic — FastAPI, Flask, Starlette, Django, Quart, Litestar…
- 🔑 Persistent
.onion— the address stays stable across restarts - 🪶 Lightweight client — server features are opt-in via
pip install tornion[server]
# Client only
pip install tornion
# With server features
pip install tornion[server]from tornion import client
r = client.get("http://xxx.onion/ping")
print(r.json())
# Reusable session for multiple calls
with client.Session() as s:
s.post("http://xxx.onion/items", json={"name": "foo"})
s.get("http://xxx.onion/items/1")from fastapi import FastAPI
from tornion import server
app = FastAPI()
@app.get("/")
def root():
return {"hello": "from .onion"}
server.serve(app)When you run this, tornion prints the .onion URL and blocks until Ctrl+C.
Same code works with Flask, Starlette, Django, etc. — auto-detected.
from fastapi import FastAPI
from tornion import client, server
app = FastAPI()
@app.get("/relay")
def relay(target: str):
r = client.get(target, timeout=30)
return {"upstream": r.status_code, "body": r.json()}
server.serve(app)tornion install-tor # pre-download the tor binary
tornion serve myapp:app # uvicorn-style: run an app on a .onion
tornion get http://xxx.onion/ # one-shot HTTP request
tornion info # diagnostic: where's tor, what's installed📖 Configuration & CLI reference →
tornion orchestrates a real tor binary as a subprocess. Python doesn't
implement Tor — it uses the C reference implementation (tor) under the
hood, talks to it via SOCKS5 (client side) or the hidden-service API
(server side), and shuts it down on exit.
| Topic | Doc |
|---|---|
Calling .onion APIs from Python |
docs/client.md |
| Publishing your app on a hidden service | docs/server.md |
| Env vars, CLI, paths, diagnostic | docs/configuration.md |
| Architecture, design choices, pitfalls | docs/internals.md |
| Examples (client / server / hybrid) | examples/ |
| Release history & versioning policy | CHANGELOG.md |
git clone https://github.com/LouisCourrian/tornion
cd tornion
pip install -e ".[dev]"
pytest
python examples/server_fastapi.pytornion is stable as of 1.0.0. The public API of tornion,
tornion.client, and tornion.server follows
Semantic Versioning; see
CHANGELOG.md for the full versioning policy and release
history. Pin to a major version (tornion>=1.0,<2.0) and you're good.
Required for 1.0.0 (✅ all shipped):
- Stable
.onionby default.Stop derivingapp_namefromapp.title(fragile).app_namenow defaults to the entry-script basename (python myserver.py→myserver);serve()prints the resolvedkey_dirand a fresh-vs-existing identity status before tor bootstrap so the first run is never silent. - Verify Tor Expert Bundle downloads. SHA-256 pinning. Hashes
live in
KNOWN_TOR_HASHES, populated from the Tor Project's signedsha256sums-signed-build.txt. Unknown versions are refused unless the caller passes an explicitsha256=.... Mismatched archives are deleted before extraction. -
CHANGELOG.md+ written SemVer policy. See CHANGELOG.md — Keep-a-Changelog format, with an explicit policy at the top stating what counts as MAJOR / MINOR / PATCH and the deprecation window. - Publish to PyPI. Auto-release workflow at
.github/workflows/release.yml:
on
v*.*.*tag push, extracts the matching CHANGELOG section, creates the GitHub Release with it as the body, then publishes to PyPI via OIDC trusted publisher.
Quick wins (shipped in 1.1.0):
-
tornion keygen [--out DIR]— generates a freshhs_ed25519_secret_keywithout spinning up tor. -
tornion onion <key_dir>— prints the.onionaddress from an existing key dir, fully offline (readshostnameor derives fromhs_ed25519_public_keyvia SHA3-256 + base32). -
TORNION_KEY_DIRenv var, symmetric toTORNION_TOR_PATH. Resolution order in_resolve_key_dir:explicit arg > $TORNION_KEY_DIR > <data>/hs/<app_name>/.
Shipped in 1.2.0:
- Tor v3 client authorization (restrict who can reach your HS).
Pure-Python x25519 (RFC 7748, vectors checked), server-side
authorizeCLI, client-sideclient-authCLI, full e2e integration test. See the Client authorization section above.
Deferred to 1.x:
- Async client (
httpx.AsyncClient-style) alongside the sync one.
MIT — see LICENSE.
tornion is an independent project, not affiliated with the Tor Project.
The bundled tor binary comes from torproject.org
under 3-clause BSD.