Skip to main content

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.

PyPI version Python versions License: MIT

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 requestsclient.Session is a real requests.Session subclass
  • 🚀 Zero-config serverserver.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")

📖 Full client guide →

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.

📖 Full server guide →

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.

📖 Architecture & internals →

📚 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 .onion by default. Stop deriving app_name from app.title (fragile). app_name now defaults to the entry-script basename (python myserver.pymyserver); serve() prints the resolved key_dir and 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 signed sha256sums-signed-build.txt. Unknown versions are refused unless the caller passes an explicit sha256=.... 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 fresh hs_ed25519_secret_key without spinning up tor.
  • tornion onion <key_dir> — prints the .onion address from an existing key dir, fully offline (reads hostname or derives from hs_ed25519_public_key via SHA3-256 + base32).
  • TORNION_KEY_DIR env var, symmetric to TORNION_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 authorize CLI, client-side client-auth CLI, 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

tornion-1.2.0.tar.gz (59.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tornion-1.2.0-py3-none-any.whl (38.3 kB view details)

Uploaded Python 3

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

Hashes for tornion-1.2.0.tar.gz
Algorithm Hash digest
SHA256 687671a50a045c80e64ba213167faee1abad97422908b4ba5d7c1cd2746b0f32
MD5 b4dc51208349048067b7cf00158d7d76
BLAKE2b-256 fbb67aacc4003dfd4ac2ac03ac67b6fd928ec96d5202eaf706ba89a161a4f2ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for tornion-1.2.0.tar.gz:

Publisher: release.yml on LouisCourrian/tornion

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for tornion-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 66ec96fcd23993f682f32eb754a44f3091b171d502980a96d2b280319c438d57
MD5 f8b666043308453df6940ea9c3185fc7
BLAKE2b-256 f53474afd823244d3afc61dbf9b7c2a8cba0843e6f42c364089eb84a1098342e

See more details on using hashes here.

Provenance

The following attestation bundles were made for tornion-1.2.0-py3-none-any.whl:

Publisher: release.yml on LouisCourrian/tornion

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page