DEV Community

Cover image for Building Tornago: A Go Library for Tor Integration Born from Fraud Prevention Needs
nchika
nchika

Posted on

Building Tornago: A Go Library for Tor Integration Born from Fraud Prevention Needs

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:

  1. Maintenance concerns: The official documentation states "Stem is mostly unmaintained"
  2. Language preference: I prefer Go's stability and compatibility guarantees for production services
  3. 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:

  1. Easier to understand and use
  2. 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.As support
  • 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
Enter fullscreen mode Exit fullscreen mode

Verify installation:

tor --version
Enter fullscreen mode Exit fullscreen mode

Install Tornago:

go get github.com/nao1215/tornago
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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>...
Enter fullscreen mode Exit fullscreen mode

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...")
}
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

Understanding Tor Startup Time

One thing to note: StartTorDaemon() takes a few minutes to complete. This is because Tor needs to:

  1. Connect to the Tor network: Download directory information (relay node lists, public keys)
  2. Build circuits: Select and establish encrypted connections through Entry Guard → Middle → Exit nodes
  3. 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!

Links

Top comments (0)