How to Use `tailcat --serve` for Port Forwarding and Tunnel Services
tailcat --serve=<ports> activates server mode and restricts incoming TCP connections to specific port numbers, ranges, or special services like SSH and exit-node forwarding.
The tailcat command-line tool in the tailscale/tailcat repository creates encrypted tunnels between machines. When you invoke the --serve flag, the utility transitions from a simple data copier into a full TCP server that listens on designated ports and handles incoming connections according to your specifications.
What tailcat --serve Does
When you append --serve to your tailcat invocation, the binary enters server mode and begins listening for incoming TCP connections over the Tailscale-style encrypted tunnel. According to the source code in cmd/tailcat/tailcat.go (lines 49-52), the flag accepts a comma-separated string that defines which local ports the server should accept.
If you provide an empty value or omit the flag entirely, tailcat listens on a single random port (port 0) and streams all received data directly to STDOUT. Once the client reads the EOF, the process terminates. However, when you supply explicit port values, the server constructs a packet filter that only allows traffic on those specific ports, rejecting all other connection attempts with an RST (connection reset).
Parsing Port Specifications with parsePortSet
The internal parsePortSet function (defined at lines 998-1024 in cmd/tailcat/tailcat.go) processes your --serve argument and splits it into two distinct sets:
- A set of port numbers (
ports set.Set[uint16]) - A set of service names (
services set.Set[string])
Invalid tokens trigger an immediate error that aborts execution (lines 1025-1030).
Supported Token Types
The parser recognizes several distinct token formats:
all— Serves every TCP port from 1 to 65535.exit-node— Enables exit node mode, allowing the client to request any destination IP:port and proxying traffic to that endpoint.no-auth-ssh— Activates the built-in auth-free SSH server on port 22 (requires the SSH binary to be compiled).<num>— A single port number, such as8080.<low>-<high>— An inclusive range of ports, such as8000-8099.
Configuring the Server Instance
After parsing completes, the main routine instantiates a tailcat.Server object. If your specification includes a non-empty port list and you did not request the exit-node service, the server tightens its packet filter to allow only those specific ports (lines 661-671).
The implementation stores these ports in the ServedTCPPorts field, coalescing adjacent numbers into ranges using the portRanges helper function. This optimization reduces the complexity of the firewall rules while maintaining the same access restrictions.
Connection Handling Logic
The server’s OnTCP callback (beginning at line 704 in cmd/tailcat/tailcat.go) evaluates every incoming connection against your --serve criteria:
- Port 22 with
no-auth-ssh— Routes the connection to the built-in Tailscale SSH handler. exit-nodeservice — Accepts any port and proxies the connection to the destination supplied by the client.- Empty port set — Default mode streams data to STDOUT and exits after EOF.
- Port not in explicit set — Returns an RST packet, immediately terminating the unauthorized connection.
Special Services (SSH, Exit Node)
When you include no-auth-ssh in your port specification, tailcat intercepts traffic on TCP port 22 and hands it to an internal SSH handler. This allows passwordless SSH access through the tunnel without requiring an external SSH daemon.
The exit-node service transforms your server into a network gateway. Clients can request any arbitrary IP:port combination, and the server proxies the traffic accordingly. This is useful for reaching internal network resources that aren't directly exposed to the client.
Port Filtering Behavior
Unless running in exit-node or all mode, tailcat maintains a strict default-deny posture. The packet filter explicitly blocks any port not listed in your ServedTCPPorts set, ensuring that only your specified services are accessible through the tunnel.
Practical Examples
Serve a single port and proxy connections to localhost:
tailcat --serve=8080
# Output: # 🐈 Server listening with new address: <token>
Serve a range plus the auth-free SSH server:
tailcat --serve=8000-8099,no-auth-ssh
Enable universal port forwarding (all ports):
tailcat --serve=all
Run as an exit node for arbitrary destination forwarding:
tailcat --serve=exit-node
Connect from the client side to a specific served port:
tailcat <token> 8080
Summary
tailcat --serveactivates TCP server mode with granular port control.- The
parsePortSetfunction incmd/tailcat/tailcat.gohandles comma-separated tokens including single ports, ranges (8000-8099), and special keywords (all,exit-node,no-auth-ssh). - The server constructs a restrictive packet filter that defaults to denying unauthorized ports with an RST response.
- Special services like exit-node mode bypass port restrictions to enable full network proxying, while no-auth-ssh enables built-in SSH on port 22.
Frequently Asked Questions
What happens if I don't specify --serve?
Without the --serve flag, tailcat listens on a random ephemeral port (port 0) and copies all received data to STDOUT. The process terminates automatically after the client closes the connection and EOF is reached.
Can I combine multiple port ranges and services?
Yes. The parsePortSet function accepts comma-separated values, allowing combinations like --serve=8000-8099,22,no-auth-ssh or --serve=8080,exit-node. Each token is validated individually, and invalid entries cause immediate program termination.
How does tailcat handle unauthorized port attempts?
When a client attempts to connect to a port not included in your ServedTCPPorts set (and you're not running exit-node or all mode), the server sends an RST (connection reset) packet. This immediately terminates the connection attempt at the network layer.
What is the difference between --serve=all and --serve=exit-node?
--serve=all allows incoming connections on every TCP port (1-65535) but proxies them to the corresponding localhost port on the server (e.g., connecting to port 8080 reaches localhost:8080). --serve=exit-node allows the client to specify any arbitrary destination IP:port, effectively using the server as a network gateway to reach external or internal addresses beyond the server itself.
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 →