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.0.1.tar.gz (44.3 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.0.1-py3-none-any.whl (28.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tornion-1.0.1.tar.gz
  • Upload date:
  • Size: 44.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tornion-1.0.1.tar.gz
Algorithm Hash digest
SHA256 5c505bb8cc59334b634214a74c6043873564ec2f0c8b7e6cadf4b3e9862fbae4
MD5 3c62ce92bbc7df98dcbd0f3110e1370a
BLAKE2b-256 42b066a807a0f321f0dac1c306f3093c606e993cfa0fc573e842ff0a7de7ca64

See more details on using hashes here.

Provenance

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

Publisher: publish.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.0.1-py3-none-any.whl.

File metadata

  • Download URL: tornion-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 28.0 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.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1bf963fee104cde8b432603f9caed72e1c0cac6d011f27fd655ae08c9fd7cc2e
MD5 00b9c4c0214df19c36d1573691bcac4a
BLAKE2b-256 9710224a1a770bac8bc77d3c41603ead1a0ceafe5d02c12d19475fe83a57787c

See more details on using hashes here.

Provenance

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

Publisher: publish.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