How to Use tailcat for TCP Port Forwarding: A Complete Guide with Examples
Use the tailcat forward sub-command to create local TCP listeners that proxy connections through an encrypted WireGuard tunnel to a remote tailcat server without requiring a central control plane.
tailcat implements a lightweight, control-plane-free network pipe built on top of Tailscale's WireGuard data-plane. The forward sub-command enables TCP port forwarding by spawning local listeners and routing every incoming connection through a tailcat server over an encrypted tunnel. This guide walks through the architecture, mapping syntax, and practical examples based on the tailscale/tailcat source code.
Architecture of tailcat TCP Port Forwarding
Understanding how tailcat port forwarding works helps diagnose issues and choose the right mapping strategy.
Core Components
The implementation spans several key functions in cmd/tailcat/forward.go:
| Component | Responsibility | Source Location |
|---|---|---|
forwardCommand |
Registers the CLI sub-command and parses flags | forward.go:L24-L31 |
runForward |
Validates arguments, creates client, spawns listeners | forward.go:L50-L82 |
parseForwardSpec |
Parses mapping strings into structured specs | forward.go:L63-L88 |
forwardListener |
Accepts local connections and proxies to remote | forward.go:L15-L47 |
Connection Flow
When you run tailcat forward, here's what happens:
-
Argument parsing —
parseForwardSpecprocesses each port mapping (e.g.,8080,18080:8080,13306:192.168.1.10:3306) -
Listener creation —
net.Listenbinds to the local address -
Goroutine spawning —
forwardListenerruns indefinitely for each mapping -
Connection handling — On each accept, the listener dials the remote side via
tailcat.Client.DialTCPorDialTCPPort, then hands both connections totailcat.ProxyConnsfor bidirectional data copying -
Encryption — All traffic travels through the same WireGuard-protected tunnel used by standard tailcat connections
This design provides NAT traversal, end-to-end encryption, and zero dependency on a central control plane.
Port Mapping Syntax for tailcat Forward
The parseForwardSpec function in cmd/tailcat/forward.go supports flexible mapping specifications:
| Syntax | Behavior |
|---|---|
8080 |
Listen on 127.0.0.1:8080, forward to remote port 8080 |
18080:8080 |
Listen locally on 18080, forward to remote port 8080 |
0:8080 |
Auto-assign local port, print chosen address on startup |
13306:192.168.1.10:3306 |
Listen on 13306, forward to specific IP:port (requires exit node) |
--bind=0.0.0.0 |
Override bind address for all listeners |
Remote Target Resolution
-
Port-only targets (
8080,18080:8080): UseDialTCPPort→ connects to127.0.0.1:<port>on the server -
Full address targets (
13306:192.168.1.10:3306): UseDialTCP→ connects to the specifiednetip.AddrPort
The server must run as an exit node to reach arbitrary remote IPs.
Practical tailcat TCP Port Forwarding Examples
Basic SSH Forwarding
Expose a remote server's SSH port locally:
tailcat forward tc1abc… 22
This listens on 127.0.0.1:22 and forwards to port 22 on the tailcat server.
Listen on All Interfaces
Access forwarded ports from other machines on your network:
tailcat forward --bind=0.0.0.0 tc1abc… 2222:22
Now ssh -p 2222 user@<your-local-ip> works from any reachable host.
Dynamic Port Assignment for Scripts
Let the OS pick an available port—useful when hardcoding isn't possible:
tailcat forward tc1abc… 0:80
# Output: forwarding [::]:xxxxx -> remote 127.0.0.1:80
Parse the output to discover the assigned port programmatically.
Exit Node Forwarding to Internal Services
Reach a database behind the tailcat server:
tailcat forward tc1abc… 13306:192.168.1.10:3306
Connect with: mysql -h 127.0.0.1 -P 13306 -u dbuser -p
Background Operation
Run persistently on POSIX systems:
nohup tailcat forward --bind=0.0.0.0 tc1abc… 8080 > /var/log/tailcat-forward.log 2>&1 &
Verification
Test a forward mapping local 8080 → remote 80:
curl http://localhost:8080/
Key Implementation Files
| File | Purpose | Link |
|---|---|---|
cmd/tailcat/forward.go |
CLI implementation, parsing, listener management, connection proxying | Source |
cmd/tailcat/forward_test.go |
Unit tests for mapping parsing and lifecycle handling | Source |
tailcat.go |
Core library: Client, Server, address handling, WireGuard tunnel |
Source |
The tailcat.Client type provides DialTCP and DialTCPPort methods that establish TCP streams inside the encrypted tunnel. These are called by forwardListener in cmd/tailcat/forward.go for each proxied connection.
Summary
-
tailcat forwardcreates local TCP listeners that tunnel through WireGuard to a remote server -
Mapping syntax supports simple ports (
8080), local:remote pairs (18080:8080), auto-assignment (0:8080), and full address targets (13306:192.168.1.10:3306) -
No control plane required — direct peer-to-peer encryption via Tailscale's data plane
-
Exit node mode enables forwarding to arbitrary IPs reachable by the server
-
Background-friendly with standard shell tools like
nohup
Frequently Asked Questions
Does tailcat forward require a Tailscale control plane?
No. Unlike standard Tailscale, tailcat operates control-plane-free. The WireGuard tunnel is established directly between peers using the tailcat address (tc1abc…). The tailcat.Client in tailcat.go manages the encrypted connection without coordinating with Tailscale's coordination server.
Can I forward multiple ports in one command?
Yes. Pass multiple mapping arguments: tailcat forward tc1abc… 8080 8443 3000. Each spawns an independent forwardListener goroutine. You can mix syntax styles: 8080 18080:8080 0:3000.
What's the difference between DialTCP and DialTCPPort?
DialTCPPort accepts a port number and connects to 127.0.0.1:<port> on the remote server—used for simple port-only mappings. DialTCP accepts a full netip.AddrPort—used when you specify a complete destination address like 192.168.1.10:3306. Both return a net.Conn over the encrypted tunnel.
How do I troubleshoot a failing forward?
Check: (1) The tailcat server is running and reachable—test with tailcat dial tc1abc…; (2) For exit-node forwarding, verify the server runs with exit node privileges; (3) Local bind permissions—ports <1024 need root; (4) Firewall rules on the --bind address. Enable verbose logging if available in your tailcat build.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →