Drop-in client + zero-config server for Tor hidden services
Project description
🧅 tornion
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.
✨ Features
- 🔌 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]
📦 Installation
# Client only
pip install tornion
# With server features
pip install tornion[server]
🚀 Quick start
Client — call a .onion API
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")
Server — publish your app on a .onion
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.
Hybrid — server that also makes outbound calls
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)
🛠️ CLI
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 →
🔍 How does it actually work?
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.
📚 Documentation
| 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 |
🧪 Try it
git clone https://github.com/LouisCourrian/tornion
cd tornion
pip install -e ".[dev]"
pytest
python examples/server_fastapi.py
✅ Status
tornion 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.
🗺️ Roadmap
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.
📄 License
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tornion-1.2.0.tar.gz.
File metadata
- Download URL: tornion-1.2.0.tar.gz
- Upload date:
- Size: 59.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
687671a50a045c80e64ba213167faee1abad97422908b4ba5d7c1cd2746b0f32
|
|
| MD5 |
b4dc51208349048067b7cf00158d7d76
|
|
| BLAKE2b-256 |
fbb67aacc4003dfd4ac2ac03ac67b6fd928ec96d5202eaf706ba89a161a4f2ef
|
Provenance
The following attestation bundles were made for tornion-1.2.0.tar.gz:
Publisher:
release.yml on LouisCourrian/tornion
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tornion-1.2.0.tar.gz -
Subject digest:
687671a50a045c80e64ba213167faee1abad97422908b4ba5d7c1cd2746b0f32 - Sigstore transparency entry: 1511554105
- Sigstore integration time:
-
Permalink:
LouisCourrian/tornion@577632dcf9e9adfc7403dcd4b85627c0ebadf8ef -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/LouisCourrian
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@577632dcf9e9adfc7403dcd4b85627c0ebadf8ef -
Trigger Event:
push
-
Statement type:
File details
Details for the file tornion-1.2.0-py3-none-any.whl.
File metadata
- Download URL: tornion-1.2.0-py3-none-any.whl
- Upload date:
- Size: 38.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66ec96fcd23993f682f32eb754a44f3091b171d502980a96d2b280319c438d57
|
|
| MD5 |
f8b666043308453df6940ea9c3185fc7
|
|
| BLAKE2b-256 |
f53474afd823244d3afc61dbf9b7c2a8cba0843e6f42c364089eb84a1098342e
|
Provenance
The following attestation bundles were made for tornion-1.2.0-py3-none-any.whl:
Publisher:
release.yml on LouisCourrian/tornion
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tornion-1.2.0-py3-none-any.whl -
Subject digest:
66ec96fcd23993f682f32eb754a44f3091b171d502980a96d2b280319c438d57 - Sigstore transparency entry: 1511554318
- Sigstore integration time:
-
Permalink:
LouisCourrian/tornion@577632dcf9e9adfc7403dcd4b85627c0ebadf8ef -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/LouisCourrian
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@577632dcf9e9adfc7403dcd4b85627c0ebadf8ef -
Trigger Event:
push
-
Statement type: