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:

  1. Argument parsing — parseForwardSpec processes each port mapping (e.g., 8080, 18080:8080, 13306:192.168.1.10:3306)

  2. Listener creation — net.Listen binds to the local address

  3. Goroutine spawning — forwardListener runs indefinitely for each mapping

  4. Connection handling — On each accept, the listener dials the remote side via tailcat.Client.DialTCP or DialTCPPort, then hands both connections to tailcat.ProxyConns for bidirectional data copying

  5. 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): Use DialTCPPort → connects to 127.0.0.1:<port> on the server

  • Full address targets (13306:192.168.1.10:3306): Use DialTCP → connects to the specified netip.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 forward creates 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →