OnionApp
Host anonymous services on the Tor network. As simple as Flask or FastAPI, but accessible via .onion addresses.
Overview
OnionApp lets you create HTTP servers accessible only through the Tor network. When you start the app, it:
- Generates or loads cryptographic keys
- Registers with the Tor network
- Serves requests at a
.onionaddress
Basic Usage
Rust
use hypertor::{OnionApp, Request, Response, Result};
#[tokio::main]
async fn main() -> Result<()> {
// Create app
let app = OnionApp::new();
// Define routes
app.get("/", |_req| async {
Response::text("Hello from the dark web!")
});
app.get("/api/data", |_req| async {
Response::json(&serde_json::json!({
"message": "Secret data",
"timestamp": chrono::Utc::now()
}))
});
// Start server (prints .onion address)
let addr = app.run().await?;
println!("Service running at: {}", addr);
// Keep running
tokio::signal::ctrl_c().await?;
Ok(())
}
Python
import asyncio
from hypertor import OnionApp
app = OnionApp()
@app.get("/")
async def index(request):
return {"message": "Hello from the dark web!"}
@app.post("/api/echo")
async def echo(request):
data = await request.json()
return {"received": data}
async def main():
async with app.run() as addr:
print(f"Service running at: {addr}")
# Keep running until interrupted
await asyncio.Event().wait()
asyncio.run(main())
Configuration
use hypertor::{OnionApp, SecurityLevel};
use std::time::Duration;
let app = OnionApp::builder()
// Security
.pow_enabled(true)
.pow_target_bits(16)
.vanguards_enabled(true)
// Performance
.connection_limit(100)
.request_timeout(Duration::from_secs(30))
// Identity
.key_path("/path/to/keys") // Persist .onion address
// Build
.build();
Configuration Options
| Option | Default | Description |
|---|---|---|
pow_enabled |
false | Require proof-of-work from clients |
pow_target_bits |
20 | PoW difficulty (higher = harder) |
vanguards_enabled |
true | Use Vanguards-lite for guard protection |
connection_limit |
50 | Max concurrent connections |
request_timeout |
60s | Timeout per request |
key_path |
None | Path to persist keypair |
Routing
// Basic routes
app.get("/", handler);
app.post("/api/create", handler);
app.put("/api/update", handler);
app.delete("/api/delete", handler);
app.patch("/api/patch", handler);
// Path parameters
app.get("/users/:id", |req| async move {
let id = req.param("id")?;
Response::json(&get_user(id).await)
});
// Multiple parameters
app.get("/posts/:post_id/comments/:comment_id", |req| async move {
let post_id = req.param("post_id")?;
let comment_id = req.param("comment_id")?;
// ...
});
Working with Requests
use hypertor::{Request, Response};
app.post("/api/data", |req: Request| async move {
// Headers
let content_type = req.header("content-type");
let auth = req.header("authorization");
// Query parameters (?key=value)
let page = req.query("page").unwrap_or("1");
let limit = req.query("limit").unwrap_or("10");
// JSON body
let data: MyStruct = req.json()?;
// Form data
let form = req.form()?;
let name = form.get("name");
// Raw body
let bytes = req.body();
Response::ok()
});
Response Building
use hypertor::{Response, StatusCode};
// Text
Response::text("Hello, World!")
// JSON
Response::json(&data)
// With status code
Response::with_status(StatusCode::CREATED)
.json(&created_resource)
// With headers
Response::json(&data)
.header("X-Custom", "value")
.header("Cache-Control", "no-store")
// Redirect
Response::redirect("/new-location")
// Errors
Response::not_found()
Response::bad_request("Invalid input")
Response::internal_error("Something went wrong")
Middleware
use hypertor::{OnionApp, Request, Response, Middleware};
// Custom middleware
struct LoggingMiddleware;
impl Middleware for LoggingMiddleware {
async fn call(&self, req: Request, next: Next) -> Response {
let start = std::time::Instant::now();
let path = req.path().to_string();
let resp = next.call(req).await;
println!("{} {} - {:?}",
resp.status(), path, start.elapsed());
resp
}
}
let app = OnionApp::new();
app.middleware(LoggingMiddleware);
Built-in Middleware
use hypertor::middleware::*;
// CORS
app.middleware(Cors::permissive());
// Rate limiting
app.middleware(RateLimit::new(100, Duration::from_secs(60)));
// Compression
app.middleware(Compression::default());
// Timeout
app.middleware(Timeout::new(Duration::from_secs(30)));
Persistent Identity
By default, each restart generates a new .onion address. To keep the same address:
// Save/load keys to persist identity
let app = OnionApp::builder()
.key_path("/var/lib/myapp/tor_keys")
.build();
The key directory will contain:
hs_ed25519_secret_key— Private key (KEEP SECRET!)hs_ed25519_public_key— Public keyhostname— Your .onion address
Client Authorization
Restrict access to specific clients:
let app = OnionApp::builder()
// Enable client auth
.client_auth_enabled(true)
// Add authorized clients
.authorized_client("descriptor:x25519:CLIENT_KEY_HERE")
.build();
Only clients with the corresponding private key can connect.
Managing Authorized Clients
use hypertor::{OnionApp, ClientAuth};
// Generate a new client key pair
let (client_public, client_private) = ClientAuth::generate_keypair()?;
println!("Give to client: {}", client_private.to_string());
// Add to server
let app = OnionApp::builder()
.client_auth_enabled(true)
.authorized_client(&client_public)
.build();
// Dynamic client management
let app = OnionApp::new();
app.add_authorized_client(&client_public).await?;
app.remove_authorized_client(&client_public).await?;
app.list_authorized_clients().await?;
Proof of Work (Anti-DDoS)
Require clients to solve a computational puzzle before connecting:
let app = OnionApp::builder()
.pow_enabled(true)
.pow_target_bits(20) // Difficulty level
.build();
This makes DDoS attacks expensive while legitimate clients only pay a small one-time cost.
Dynamic PoW Adjustment
use hypertor::{OnionApp, PowConfig};
let app = OnionApp::builder()
.pow(PowConfig {
enabled: true,
base_difficulty: 16,
max_difficulty: 24,
// Auto-adjust based on load
auto_adjust: true,
target_latency: Duration::from_millis(500),
})
.build();
Traffic Padding
Add dummy traffic to prevent traffic analysis:
use hypertor::{OnionApp, PaddingConfig};
let app = OnionApp::builder()
.traffic_padding(PaddingConfig {
enabled: true,
// Pad responses to multiples of this size
block_size: 512,
// Add random delay to responses
timing_jitter: Duration::from_millis(50),
// Send periodic dummy packets
dummy_traffic: true,
dummy_interval: Duration::from_secs(30),
})
.build();
WebSocket Support
use hypertor::{OnionApp, WebSocket, Message};
app.websocket("/ws", |ws: WebSocket| async move {
while let Some(msg) = ws.recv().await? {
match msg {
Message::Text(text) => {
ws.send(Message::Text(format!("Echo: {}", text))).await?;
}
Message::Binary(data) => {
ws.send(Message::Binary(data)).await?;
}
Message::Ping(data) => {
ws.send(Message::Pong(data)).await?;
}
Message::Close(_) => break,
_ => {}
}
}
Ok(())
});
Static File Serving
use hypertor::{OnionApp, StaticFiles};
// Serve directory
app.static_files("/assets", StaticFiles::new("/path/to/assets")
.index_file("index.html")
.cache_control("max-age=3600")
);
// Single file
app.get("/favicon.ico", StaticFiles::file("/path/to/favicon.ico"));
Request Validation
use hypertor::{OnionApp, Request, Response};
use validator::Validate;
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3, max = 32))]
username: String,
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
app.post("/users", |req: Request| async move {
let user: CreateUser = req.json()?;
if let Err(errors) = user.validate() {
return Response::bad_request(errors.to_string());
}
// Proceed with validated data
Response::json(&create_user(user).await?)
});
Error Handling
use hypertor::{OnionApp, Error};
// Global error handler
app.on_error(|err: Error| async move {
eprintln!("Error: {}", err);
Response::internal_error("Something went wrong")
});
// Per-route error handling
app.get("/risky", |req| async move {
match do_risky_thing().await {
Ok(data) => Response::json(&data),
Err(e) => Response::bad_request(e.to_string()),
}
});
// Custom error types
#[derive(Debug)]
enum ApiError {
NotFound(String),
Validation(String),
Internal(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::NotFound(msg) => Response::not_found().json(&json!({"error": msg})),
ApiError::Validation(msg) => Response::bad_request().json(&json!({"error": msg})),
ApiError::Internal(msg) => Response::internal_error().json(&json!({"error": msg})),
}
}
}
State Management
use std::sync::Arc;
use tokio::sync::RwLock;
struct AppState {
counter: RwLock<u64>,
db: Database,
}
let state = Arc::new(AppState {
counter: RwLock::new(0),
db: Database::connect().await?,
});
let app = OnionApp::with_state(state.clone());
app.get("/count", move |req| {
let state = state.clone();
async move {
let mut counter = state.counter.write().await;
*counter += 1;
Response::json(&serde_json::json!({
"count": *counter
}))
}
});
Graceful Shutdown
use hypertor::OnionApp;
use tokio::signal;
let app = OnionApp::new();
// ... define routes ...
let server = app.run().await?;
println!("Service running at: {}", server.onion_address());
// Wait for shutdown signal
signal::ctrl_c().await?;
// Graceful shutdown with timeout
server.shutdown(Duration::from_secs(30)).await?;
println!("Server shut down gracefully");
Health Checks
use hypertor::{OnionApp, HealthCheck};
let app = OnionApp::builder()
.health_check(HealthCheck {
enabled: true,
path: "/health",
include_details: false, // Don't leak internal info
})
.build();
// Custom health check
app.get("/health", |_| async {
let db_ok = check_database().await.is_ok();
let cache_ok = check_cache().await.is_ok();
if db_ok && cache_ok {
Response::ok().json(&json!({
"status": "healthy",
"checks": {
"database": "ok",
"cache": "ok"
}
}))
} else {
Response::with_status(503).json(&json!({
"status": "unhealthy"
}))
}
});
Observability
Prometheus Metrics
use hypertor::{OnionApp, MetricsConfig};
let app = OnionApp::builder()
.metrics(MetricsConfig {
enabled: true,
path: "/metrics",
prefix: "myservice",
})
.build();
// Metrics exposed:
// myservice_requests_total{method="GET", path="/api", status="200"}
// myservice_request_duration_seconds{quantile="0.99"}
// myservice_active_connections
// myservice_pow_attempts_total
Structured Logging
use hypertor::{OnionApp, LogConfig};
let app = OnionApp::builder()
.logging(LogConfig {
format: LogFormat::Json,
level: "info",
// Redact sensitive data
redact_headers: vec!["authorization", "cookie"],
redact_body_fields: vec!["password", "token"],
})
.build();
Multi-Service Architecture
Run multiple .onion services from one application:
use hypertor::{OnionApp, ServiceConfig};
// Public API service
let public_app = OnionApp::builder()
.key_path("/keys/public")
.build();
public_app.get("/api", public_handler);
// Admin service (separate .onion address)
let admin_app = OnionApp::builder()
.key_path("/keys/admin")
.client_auth_enabled(true)
.build();
admin_app.get("/admin", admin_handler);
// Run both
tokio::try_join!(
public_app.run(),
admin_app.run(),
)?;
Next Steps
- TorClient Documentation — Make requests to .onion services
- Security Features — PoW, Vanguards, Leak Detection
- Python Bindings — Full Python API reference