Blog / 2025-07-04
Building Rabbit: A Self-Hosted TCP Tunnel with Persistent Ports
Harsh Vardhan Goswami
On this page
The problem is reachability, not a trust shortcut
We built Rabbit while working on a data platform that needed to reach services running on laptops, private networks, and restricted environments. The practical problem was not how to make a local port public as quickly as possible. It was how to create a controlled TCP path between a platform and a resource that could not accept inbound connections from that platform.
The common pattern is an outbound connection from the private side to a server that both sides can reach. That can be useful for a database, staging service, or a review environment. It also creates a new access path. Self-hosting the server changes who operates the infrastructure; it does not make the path private by definition.
Rabbit is a Go-based TCP tunneling system with a server, a client, token-backed tunnel registrations, and PostgreSQL-backed state. The design is intended to give a token a stable port assignment and to restore that assignment after server restarts. Those are design goals, not a substitute for network policy, TLS, authentication, monitoring, or incident response.
The parts and their responsibilities
The system has three connection roles. Keeping them separate makes failures easier to reason about.
| Part | Responsibility | Operator concern |
|---|---|---|
| Rabbit server | Accepts client control and data connections, listens on assigned tunnel ports, and bridges streams | Firewall rules, TLS termination or transport encryption, port range, capacity |
| Rabbit client | Connects outward from the private network and reaches the configured local service | Local bind address, credentials, reconnect policy, host permissions |
| PostgreSQL | Stores tunnel registrations and port assignments | Backups, access control, migrations, restore testing |
| Control connection | Carries registration and requests for a data connection | Client authentication, liveness, replay and timeout handling |
| Data connection | Carries proxied TCP bytes for one external connection | Encryption, connection limits, accounting, idle timeouts |
A token maps to a tunnel and a port. The server uses that mapping to decide which client registration is eligible to serve a connection arriving on the assigned port. The mapping must be unique and durable. A port should not silently change hands because a process reconnects or a database row is recreated.
The token is therefore a credential, not a label. Treat it as secret material: issue it through an authenticated control plane, avoid putting it in logs and shell history, scope who can create or revoke it, and rotate it when exposure is suspected.
Control flow and data flow
The control channel is long-lived. A client establishes it with the Rabbit server, presents its tunnel identity or token, and registers the local target it will serve. The server records the active client connection against the persisted tunnel assignment. This channel lets the server request capacity without accepting an unsolicited inbound connection into the private network.
When another system connects to the tunnel's server-side port, Rabbit does not send that application's bytes down the control channel. Instead, the server notifies the client that a new stream is needed. The client opens a corresponding data connection back to the server. Once the server associates the pending external connection with that data connection, it copies bytes in both directions until either side closes.
external caller
| TCP to assigned server port
v
Rabbit server ---- control request ----> Rabbit client
^ |
|<---- matching data connection -------|
|
+---- bidirectional byte stream ---- local service
This separation prevents an application payload from competing with registration messages, but it introduces coordination work. The server needs timeouts for an external connection that never receives a matching data connection. The client needs limits for concurrent requests. Both sides need cleanup for a control connection that disappears while data streams are active.
Capacity is therefore a queueing decision. Set a maximum number of pending and active streams per tunnel, then decide whether excess callers are rejected, held briefly, or routed through a separate pool. The policy should be observable rather than inferred from an eventual timeout.
Persistent ports and reconnect behavior
Rabbit persists tunnel state in PostgreSQL. The stored record includes the durable identity and port assignment needed to restore a tunnel configuration after the server restarts. Persisting a row does not preserve an in-memory TCP session: a restart still interrupts active traffic, and clients must reconnect before the server can bridge new connections.
On reconnect, the client should present the same authorized tunnel identity and the server should reattach it only to the matching persisted port. A reconnect must not create a second active owner for the same tunnel. Decide explicitly how the server resolves duplicate clients: reject the new client, replace the old client after a liveness check, or require an administrative action. The correct choice depends on the failure detector and the risk of routing traffic to the wrong machine.
The practical expectation is bounded disruption, not uninterrupted sessions. Existing database or application clients may need their own retry logic when Rabbit, the local service, or the network reconnects. Document those layers separately so an incident does not become a search for a nonexistent guarantee.
Authentication and threat boundaries
Rabbit's token-to-port model controls which registered client can claim a tunnel. It does not, by itself, authenticate every caller that can reach the server-side port. If that port is reachable from a broad network, any host on that network may be able to attempt a connection to the forwarded service.
The exposure is determined by deployment:
| Deployment choice | What it changes | What it does not guarantee |
|---|---|---|
| Server on a public address | Makes the listener routable if firewall rules allow it | That callers are trusted or traffic is encrypted |
| Server in a private network | Limits reachability to connected networks and routing rules | That every connected network principal is authorized |
| Network allowlist or security group | Restricts which source networks can connect | Application-level identity or payload confidentiality |
| TLS or mTLS | Protects transport confidentiality and can authenticate peers | Authorization to the downstream local service |
| Token revocation | Prevents future client registration for that tunnel | Termination of every already-established downstream session unless implemented and verified |
Operators need to choose the network boundary deliberately. Bind management endpoints separately from data ports. Restrict the tunnel port range with security groups or firewall rules. Encrypt control and data connections where untrusted networks are involved. Require authenticated access to any API that issues, lists, or revokes tokens. Apply authorization at the local service as well, because a tunnel is transport plumbing rather than an application authorization layer.
The original implementation planned mTLS, rate limiting, and advanced analytics as future work. Until a feature is implemented and validated in a deployed version, it should not be assumed to exist.
Deployment is an operating model
Rabbit can run in a container or directly on a host, but a deployment still needs a concrete operating plan. Start by reserving a server port range and ensuring it does not overlap with operating-system ephemeral ports or other listeners. Place PostgreSQL on a network path accessible to the server but not exposed to tunnel clients. Supply database and token-management credentials through the platform's secret mechanism rather than image layers or committed environment files.
Before admitting users, test the actual path from the caller network to the assigned port, from the Rabbit client to the server, and from the client to the intended local bind address. A client configured for 127.0.0.1 reaches a service on its own host; a client configured for 0.0.0.0 may expose a different service than intended. Treat that target configuration as security-relevant input.
Health checks should distinguish process health from tunnel usefulness. A server can be alive while PostgreSQL is unavailable, a client can be connected while its local service is down, and a listening port can be reachable while no client is available to serve it. Monitor each condition separately.
Plan upgrades as connection events. Drain or reject new streams before replacing a server when the service requires it, communicate the expected reconnect window to client owners, and test the procedure with a noncritical tunnel before using it during maintenance.
Observability and failure handling
Connection logs are useful only if they are structured enough to answer an incident question. Record a tunnel identifier, assigned port, client connection lifecycle, external connection lifecycle, byte counts if collected, termination reason, and timestamps. Do not log tokens or raw application payloads. Define retention for logs because connection metadata can itself be sensitive.
Useful operational signals include active tunnel count, connected clients, pending stream requests, data connections, reconnect attempts, failed authentication, port allocation failures, PostgreSQL errors, and byte or connection rates. Alerting needs thresholds and owners; a metrics endpoint alone does not establish a response process.
Backups are equally specific. Back up PostgreSQL on a schedule that matches the recovery point objective, protect the backup credentials and storage, and rehearse a restore into an isolated environment. Verify that a restore retains the intended tunnel-to-port assignments and does not accidentally reactivate credentials in a test environment.
Revocation and lifecycle management
Revoking a token should remove its ability to establish future client control connections and should release or quarantine its port according to a documented policy. Existing connections require a separate decision: allow them to drain, close them immediately, or close them after a timeout. The implementation and runbook must agree, especially for an incident involving suspected token exposure.
Delete and reuse are different operations. If a freed port is reassigned quickly, stale callers may reach a different service. A quarantine period, explicit reuse process, and connection audit trail reduce that risk. In multi-team deployments, namespace or policy controls should make ownership discoverable before an operator revokes a tunnel that another team depends on.
Limitations to state plainly
Rabbit is TCP transport. It does not inspect whether the downstream protocol is safe, validate application queries, or replace database authentication. It cannot keep a local service available when the client machine sleeps, loses its network route, or stops the service. It also cannot make an external caller's retry behavior correct after an interrupted stream.
The token model gives a durable port association, but it is not a complete identity system for callers. Self-hosting supplies operational control, while also assigning the operator responsibility for patching hosts, protecting secrets, configuring TLS, restricting networks, maintaining PostgreSQL, and responding to abuse.
Readiness checklist
Use this checklist before relying on a Rabbit tunnel for an internal workflow:
- The server's control, data, and management ports have documented bind addresses and firewall rules.
- TLS and authentication responsibilities are assigned for both client-to-server and caller-to-server paths.
- Tokens are issued, stored, rotated, and revoked through an authenticated process; tokens never appear in logs.
- PostgreSQL backups and restores have been tested, including port-assignment recovery.
- Reconnect behavior, duplicate-client handling, timeouts, and active-stream interruption are documented.
- Health checks and alerts cover the server, database, client control connection, local target, and pending data connections.
- Port reuse and incident revocation have named operators and a written procedure.
- The downstream service still enforces its own authentication and authorization.
The practical takeaway is modest: Rabbit can provide a controlled TCP bridge when an outbound client connection is the right network shape. Whether that bridge is safe for a particular service depends on the deployment boundary and the operating controls around it.
