Introduction
Have you ever needed to integrate Tor into your Go application for privacy-focused features or security research? I recently built Tornago, a lightweight Go wrapper for the Tor command-line tool, and I'd like to share the story behind it and how it works.
This project came from a real-world need: monitoring dark web marketplaces for stolen credit card data as part of fraud prevention work. While Python's torproject/stem is commonly used for Tor integration, I wanted a Go alternative that would provide the production stability and simplicity I was looking for.
Why I Built Tornago
I work in fraud prevention, and there are times when accessing the dark web becomes necessary for legitimate security work. This isn't science fiction—it's a daily reality in cybersecurity.
When exploring tools for this purpose, the natural choice was Python's torproject/stem, the official Tor controller library. It's mature, well-documented, and widely used. However, I encountered a few challenges:
- Maintenance concerns: The official documentation states "Stem is mostly unmaintained"
- Language preference: I prefer Go's stability and compatibility guarantees for production services
- Ecosystem diversity: Having multiple language options strengthens the overall ecosystem
So I built Tornago—not to replace stem, but to offer a Go alternative for those who share similar needs.
Design Philosophy
Tornago follows three core principles:
1. Simple Wrapper, Not a Reimplementation
Tornago doesn't reimplement the Tor protocol. Instead, it's a thin wrapper around the existing tor command-line tool. This approach:
- Leverages the security-audited Tor implementation
- Reduces the attack surface
- Makes maintenance simpler
- Follows the same philosophy as Python's stem
2. Minimal Interface
The library provides exactly three core functionalities:
- Tor Client: Route HTTP/TCP traffic through Tor's SOCKS5 proxy
- Tor Daemon Management: Launch and manage Tor processes programmatically
- Tor Server: Create and manage Hidden Services (.onion sites) via ControlPort
Nothing more, nothing less. This minimalism serves two purposes:
- Easier to understand and use
- Discourages potential misuse by not providing unnecessary convenience features
3. Production-Ready
- Zero external dependencies: Standard library only
- Cross-platform tested: CI runs on Linux, macOS, Windows, FreeBSD, OpenBSD, NetBSD, DragonFly BSD
-
Robust error handling: Structured errors with
errors.Is/errors.Assupport - Automatic retries: Built-in exponential backoff for transient failures
How Tor Works: A Quick Primer
Before diving into Tornago's implementation, let's understand how Tor actually works. This knowledge is crucial for using the library effectively.
Onion Routing: Multi-Layer Encryption
Tor (The Onion Router) provides anonymity by routing traffic through multiple encrypted layers. Think of it like sending a letter inside multiple envelopes, where each postal worker can only see the next destination, not the final recipient. (Of course, this analogy isn't perfect—unlike envelopes that can be torn open, Tor uses cryptographic encryption that can't be easily broken.)
Key Security Properties
Layered Encryption (Onion Layers):
- Each relay only knows its immediate predecessor and successor
- Entry node (Guard) knows your IP but not your destination
- Exit node knows your destination but not your IP
- Middle node knows neither your IP nor destination
Privacy Guarantees:
- Your ISP sees: You connect to a Tor entry node (but not what you're accessing)
- Entry node sees: Your IP address (but not your destination)
- Middle node sees: Only relay traffic (no source or destination)
- Exit node sees: Your destination (but not your real IP)
- Target server sees: Exit node's IP (not your real IP)
Important Limitations:
- Exit nodes can see unencrypted traffic (always use HTTPS!)
- Exit node operators could monitor traffic (but can't trace back to you)
- Timing analysis might correlate traffic patterns
- Slower than direct connections due to 3-hop routing
What Tornago Does
Tornago simplifies Tor integration by handling the complex parts for you:
1. SOCKS5 Proxy Communication
Automatically routes your HTTP/TCP traffic through Tor's SOCKS5 proxy with:
- Automatic connection retries with exponential backoff
- Configurable timeouts
- Support for both HTTP and raw TCP connections
2. Tor Daemon Lifecycle Management
Handles the complex bootstrapping process:
- Launches tor process with proper configuration
- Waits for SOCKS5 and ControlPort to become available (polls every 200ms)
- Manages graceful shutdown and cleanup
- Handles temporary or persistent data directories
3. Hidden Service Creation
Simplifies .onion site hosting via ControlPort:
- Creates ED25519-V3 onion addresses
- Manages port mappings (e.g., onion port 80 → local port 8080)
- Supports persistent private keys
- Handles client authorization
Getting Started
Prerequisites
Install Tor:
# Ubuntu/Debian
sudo apt update && sudo apt install tor
# Fedora/RHEL
sudo dnf install tor
# Arch Linux
sudo pacman -S tor
# macOS (Homebrew)
brew install tor
Verify installation:
tor --version
Install Tornago:
go get github.com/nao1215/tornago
Real-World Examples
Example 1: Accessing a Website Through Tor
This is the simplest use case—routing HTTP requests through Tor for privacy:
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/nao1215/tornago"
)
func main() {
// Step 1: Launch Tor daemon
fmt.Println("Starting Tor daemon...")
launchCfg, err := tornago.NewTorLaunchConfig(
tornago.WithTorSocksAddr(":0"), // Random available port
tornago.WithTorControlAddr(":0"), // Random available port
tornago.WithTorStartupTimeout(60*time.Second),
)
if err != nil {
log.Fatalf("Failed to create launch config: %v", err)
}
torProcess, err := tornago.StartTorDaemon(launchCfg)
if err != nil {
log.Fatalf("Failed to start Tor daemon: %v", err)
}
defer torProcess.Stop()
fmt.Printf("Tor daemon started successfully!\n")
fmt.Printf(" SOCKS address: %s\n", torProcess.SocksAddr())
fmt.Printf(" Control address: %s\n", torProcess.ControlAddr())
// Step 2: Create Tor client
clientCfg, err := tornago.NewClientConfig(
tornago.WithClientSocksAddr(torProcess.SocksAddr()),
tornago.WithClientRequestTimeout(60*time.Second),
)
if err != nil {
log.Fatalf("Failed to create client config: %v", err)
}
client, err := tornago.NewClient(clientCfg)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
// Step 3: Make HTTP request through Tor
fmt.Println("\nFetching https://example.com through Tor...")
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
"https://example.com",
http.NoBody,
)
if err != nil {
log.Fatalf("Failed to create request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
fmt.Printf("Status: %s\n", resp.Status)
body, err := io.ReadAll(io.LimitReader(resp.Body, 500))
if err != nil {
log.Fatalf("Failed to read response: %v", err)
}
fmt.Printf("\nResponse preview (first 500 bytes):\n%s\n", string(body))
}
Output:
Starting Tor daemon...
Tor daemon started successfully!
SOCKS address: 127.0.0.1:42715
Control address: 127.0.0.1:35199
Fetching https://example.com through Tor...
Status: 200 OK
Response preview (first 500 bytes):
<!doctype html><html lang="en"><head><title>Example Domain</title>...
Example 2: Creating a Hidden Service (.onion site)
Here's how to host your own .onion site:
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/nao1215/tornago"
)
func main() {
// Step 1: Launch Tor daemon
launchCfg, _ := tornago.NewTorLaunchConfig(
tornago.WithTorSocksAddr(":0"),
tornago.WithTorControlAddr(":0"),
tornago.WithTorStartupTimeout(60*time.Second),
)
torProcess, err := tornago.StartTorDaemon(launchCfg)
if err != nil {
log.Fatalf("Failed to start Tor daemon: %v", err)
}
defer torProcess.Stop()
fmt.Printf("Tor daemon started!\n")
fmt.Printf(" SOCKS address: %s\n", torProcess.SocksAddr())
fmt.Printf(" Control address: %s\n", torProcess.ControlAddr())
// Step 2: Start local HTTP server
localAddr := "127.0.0.1:8080"
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
html := `<!DOCTYPE html>
<html>
<head>
<title>Tornago Hidden Service</title>
</head>
<body>
<h1>🧅 Welcome to Tornago Hidden Service!</h1>
<p>This is a .onion site hosted with the tornago library.</p>
<p>Your IP: ` + r.RemoteAddr + `</p>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, html)
})
server := &http.Server{
Addr: localAddr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
lc := net.ListenConfig{}
listener, err := lc.Listen(context.Background(), "tcp", localAddr)
if err != nil {
log.Fatalf("Failed to start HTTP server: %v", err)
}
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
fmt.Printf("\nLocal HTTP server started on http://%s\n", localAddr)
// Step 3: Get control authentication
auth, _, err := tornago.ControlAuthFromTor(torProcess.ControlAddr(), 30*time.Second)
if err != nil {
log.Fatalf("Failed to get control auth: %v", err)
}
// Step 4: Create ControlClient
controlClient, err := tornago.NewControlClient(
torProcess.ControlAddr(),
auth,
30*time.Second,
)
if err != nil {
log.Fatalf("Failed to create control client: %v", err)
}
defer controlClient.Close()
if err := controlClient.Authenticate(); err != nil {
log.Fatalf("Failed to authenticate with Tor: %v", err)
}
// Step 5: Create Hidden Service
hsCfg, err := tornago.NewHiddenServiceConfig(
tornago.WithHiddenServicePort(80, 8080), // Map onion port 80 to local 8080
)
if err != nil {
log.Fatalf("Failed to create hidden service config: %v", err)
}
fmt.Println("\nCreating Hidden Service...")
hs, err := controlClient.CreateHiddenService(context.Background(), hsCfg)
if err != nil {
log.Fatalf("Failed to create hidden service: %v", err)
}
defer func() {
if err := hs.Remove(context.Background()); err != nil {
log.Printf("Failed to delete hidden service: %v", err)
}
}()
fmt.Printf("\n✅ Hidden Service created successfully!\n")
fmt.Printf(" Onion Address: http://%s\n", hs.OnionAddress())
fmt.Printf(" Local Address: http://%s\n", localAddr)
fmt.Println("\nAccess this through Tor Browser using the onion address above.")
fmt.Println("Press Ctrl+C to stop...")
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
fmt.Println("\n\nShutting down...")
}
Output:
Tor daemon started!
SOCKS address: 127.0.0.1:36065
Control address: 127.0.0.1:37285
Local HTTP server started on http://127.0.0.1:8080
Creating Hidden Service...
✅ Hidden Service created successfully!
Onion Address: http://f64ekih3d23wxhdb547wfj7nornjw5nb3ehuu4do45tw2wwmuzhad3yd.onion
Local Address: http://127.0.0.1:8080
Access this through Tor Browser using the onion address above.
Press Ctrl+C to stop...
Understanding Tor Startup Time
One thing to note: StartTorDaemon() takes a few minutes to complete. This is because Tor needs to:
- Connect to the Tor network: Download directory information (relay node lists, public keys)
- Build circuits: Select and establish encrypted connections through Entry Guard → Middle → Exit nodes
- Complete bootstrap: Wait until SOCKS5 proxy and ControlPort are ready
Tornago polls the ports every 200ms until they become reachable. On the first run, expect longer startup times since there's no cached directory information.
Here's the sequence diagram showing the initialization process:
Cross-Platform Testing
One of Tornago's strengths is comprehensive cross-platform support. Our GitHub Actions CI pipeline runs tests on:
- Linux (Ubuntu)
- macOS
- Windows
- FreeBSD
- OpenBSD
- NetBSD
- DragonFly BSD
This ensures that whether you're deploying on AWS Linux, developing on macOS, or running on a BSD-based firewall, Tornago will work consistently.
Ethical Considerations
Let me be very clear: Tor can be misused, and I'm aware of that risk.
That's why Tornago is intentionally designed as a thin wrapper with minimal convenience features. I'm not trying to make it easier to do harmful things—I'm trying to provide a legitimate tool for:
- Privacy protection: Journalists, activists, researchers who need anonymity
- Security research: Authorized penetration testing, vulnerability research
- Fraud prevention: Monitoring dark web marketplaces for stolen data (my use case)
Conclusion
While Tornago has comprehensive test coverage and CI testing across multiple platforms, it hasn't been battle-tested in production environments yet. I've written extensive tests to ensure reliability, but real-world usage always reveals edge cases that tests can't predict.
If you try Tornago in your projects, please share your experience—whether it's bugs, performance issues, or feature requests. Your feedback is invaluable for making this library production-ready.
Feel free to open an issue or start a discussion on GitHub!
Thank you for reading, and I look forward to your feedback!



Top comments (0)