How We Built Rabbit: A Production-Ready, Private TCP Tunnel for Devs Who Need Control

How We Built Rabbit: A Production-Ready, Private TCP Tunnel for Devs Who Need Control

Harsh Vardhan Goswami

Jul 4, 2025

Product Development

Product Development

As software teams build increasingly data-driven products, the need to securely connect local databases and private services to cloud platforms has never been greater. Yet, exposing sensitive resources to the public internet or relying on third-party tunneling services introduces risk and complexity that most developers—and their security teams—would rather avoid. At SyneHQ, we faced this challenge head-on while building our own data platform, and it inspired us to create Rabbit: an open-source, production-ready TCP tunneling system that puts security, control, and reliability back in your hands.

At SyneHQ, we obsess over making data platforms seamless and secure. But as our users grew, so did a challenge: how do you connect local databases and services to a cloud platform—without opening up your firewall or trusting a third-party tunnel?

Today, we’re excited to share Rabbit: an open-source, production-grade TCP tunneling system designed for developers and teams who want secure, persistent, and fully-owned tunnels to their local resources.

In this post, we’ll walk through the problems Rabbit solves, how it works, and best practices for deploying it in real-world environments.

Why We Built Rabbit

When building Syne, we saw a pattern: developers and data teams often run databases and services on their laptops, in private VPCs, or behind strict firewalls. Exposing these to the internet—even temporarily—was a non-starter for many.

Existing tools like ngrok are great for demos, but they require you to trust a third-party, pay per tunnel, and accept session-based, public endpoints. We wanted a solution that was:

  • Persistent: Your tunnel, your port, always available.

  • Private: No public subdomains or shared infrastructure.

  • Production-ready: Survives restarts, logs everything, and scales with your team.

  • Self-hosted: You control the infrastructure, tokens, and access.

Rabbit in Action: How It Works

Rabbit is built to be developer-friendly and robust in production:

  1. You deploy Rabbit server on a VPS or cloud instance you control.

  2. Your users run Rabbit client on their local machines, connecting their local database or service.

  3. Each user gets a unique token—generated via the API—that maps to a persistent port.

  4. Your platform connects to that port to access the user’s local resource, securely tunneled through your infrastructure.

Rabbit is perfect for:

  • Data platforms (like Syne) that need secure access to user databases.

  • Dev teams sharing local services for review or testing.

  • Analytics and staging environments that need to bridge private data.

Security & Control: The Token System

Rabbit’s token model gives you fine-grained, auditable control:

  • One token = one port = one tunnel. No conflicts, no sharing.

  • Teams and environments can have their own tokens.

  • Revoke a token, free up the port. No lingering access.

  • All connections logged—know who connected, when, and how much data was transferred.

This model is inspired by best practices in access control and disaster recovery: always know your critical assets, and be able to revoke or restore them instantly

Technical Deep Dive: Under the Hood

Rabbit is written in Go, using idiomatic concurrency and a layered, modular architecture:

Core Server Structure

The Rabbit server manages tunnels, connections, and state using Go’s concurrency primitives:

type Server struct {
    config          Config
    controlListener net.Listener
    tunnels         map[string]*Tunnel
    pendingConns    map[string]chan net.Conn
    mu              sync.RWMutex
    stopChan        chan struct{}
    wg              sync.WaitGroup
    dbService       *database.Service
    apiServer       *APIServer
}

Each tunnel is tracked as a first-class object:

type Tunnel struct {
    ID           string
    Token        string
    TeamID       string
    PortAssignID string
    LocalPort    string
    RemotePort   string
    BindAddress  string
    Client       net.Conn
    Listener     net.Listener
    CreatedAt    time.Time
    stopChan     chan struct{}
    wg           sync.WaitGroup
    SessionID    string
    ConnectionLog string
}
Connection Pairing and Bridging

When a new external connection arrives, the server notifies the client via a persistent control channel. The client opens a matching data connection, and the server bridges the two using Go’s io.Copy for efficient, bidirectional streaming:

// Pair external and client connections, then bridge them
go io.Copy(externalConn, clientDataConn)
go io.Copy(clientDataConn, externalConn)
Automatic Restoration and Resilience

Rabbit’s server persists tunnel state in PostgreSQL. If the server restarts, it automatically restores all active tunnels and their port assignments:

func (s *Server) restoreActiveConnections() error {
    // Clean up stale sessions and resurrect active ones from the database
    ...
}

This ensures your tunnels survive outages and maintenance windows, minimizing downtime and manual intervention.

For a detailed flow. Checkout our Github Docs:

Real-World Reliability: Built for Production

Rabbit isn’t just for demos—it’s designed for real workloads:

  • Automatic restoration: If the server restarts, tunnels and port assignments are restored from the database.

  • Seamless reconnection: Clients can reconnect without losing their port or session.

  • Comprehensive logging: Every connection is tracked for auditing and troubleshooting.

  • Docker-ready: Deploy Rabbit anywhere, scale as needed.

  • Health checks and monitoring: API endpoints for status and metrics.

Getting Started: Deploy Your First Rabbit Tunnel

1. Set up the Server
git clone https://github.com/SyneHQ/rabbit.go
cd rabbit.go/server
go build -o rabbit.go
./rabbit.go server --bind 0.0.0.0 --port 9999 --api-port 3422
2. Connect a Client
./rabbit.go tunnel --server your-server.com:9999 --local-port 5432 --token your-token-here
3. Generate and Manage Tokens
curl -X POST http://localhost:3422/api/teams -d '{"name":"Dev Team"}'
curl -X POST http://localhost:3422/api/tokens -d '{"team":"Dev Team"}'

lightbulb_2

Pro tip

Best Practices for Running Rabbit in Production

Just like disaster recovery in Kubernetes, running a tunnel system in production means planning for the unexpected:

  • Back up your Rabbit server’s database regularly. This ensures you can restore tunnels and tokens after an outage.

  • Define your RPO/RTO: How much downtime or data loss can you tolerate? Rabbit’s persistence model helps minimize both.

  • Document your recovery process: Make sure any team member can restore tunnels or revoke tokens in an emergency.

  • Automate health checks and alerting: Use Rabbit’s APIs to monitor tunnel health and usage.

  • Use role-based access control: Limit who can create or revoke tokens, especially in multi-team environments.

Why Not ngrok or Other Tunneling Services?

ngrok / Public Tunnels

Rabbit (Self-Hosted)

Trust a third-party

You control everything

Pay per tunnel

Free, runs on your infra

Public, ephemeral endpoints

Private, persistent ports

Limited logs or monitoring

Full API, database-backed logs

Session-based, can expire

Survives restarts, always on

Looking Ahead

Rabbit is open source and ready for your feedback. We’re excited to see how the developer community uses and extends it. We’re already planning features like mTLS, rate limiting, and advanced analytics.

Ready to take control of your tunnels?
Check out the Rabbit repo and get started today.

Built with ❤️ by the SyneHQ team for developers who demand reliability, security, and control.