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 1.0.0 to PyPI. Trusted-publisher GitHub Actions workflow lives at .github/workflows/publish.yml; releases are triggered by pushing a v* tag.

Quick wins (small, high-value):

  • tornion keygen [--out DIR] — generate a fresh hs_ed25519_secret_key without spinning up tor.
  • tornion onion <key_dir> — print the .onion address derived from an existing key dir, offline.
  • TORNION_KEY_DIR env var, symmetric to TORNION_TOR_PATH.

Deferred to 1.x:

  • Tor v3 client authorization (restrict who can reach your HS).
  • 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.1.0.tar.gz (49.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.1.0-py3-none-any.whl (31.1 kB view details)

Uploaded Python 3

File details

Details for the file tornion-1.1.0.tar.gz.

File metadata

  • Download URL: tornion-1.1.0.tar.gz
  • Upload date:
  • Size: 49.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.1.0.tar.gz
Algorithm Hash digest
SHA256 ea64bb8f00255147e11f49f1cfa8a3f78baea3bc5ef255333cc944cbe97605c8
MD5 e514ec6d1b8df239061b99f893432fc2
BLAKE2b-256 225d903c72f7091d64a38a9e45abd2120ed2d21affe5ebcde4da873f8ac0523f

See more details on using hashes here.

Provenance

The following attestation bundles were made for tornion-1.1.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.1.0-py3-none-any.whl.

File metadata

  • Download URL: tornion-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.1 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 69f267d11760d787459c43ef726e039a8471a115ed5f617610afd4c9f5e43f69
MD5 4309a182131f8d9e545c16dec338c488
BLAKE2b-256 24272387a801d197f0a300c9801a32329b147ee3bafcd0d505dce0956ae865e7

See more details on using hashes here.

Provenance

The following attestation bundles were made for tornion-1.1.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