# How to Use `tailcat --serve` for Port Forwarding and Tunnel Services

> Learn how tailcat --serve restricts TCP connections to specified ports or services like SSH and exit-node forwarding. Master port forwarding with this powerful tool.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: how-to-guide
- Published: 2026-08-30

---

**`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](https://github.com/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`](https://github.com/tailscale/tailcat/blob/main/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`](https://github.com/tailscale/tailcat/blob/main/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 as `8080`.
- **`<low>-<high>`** — An inclusive range of ports, such as `8000-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`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go)) evaluates every incoming connection against your `--serve` criteria:

1. **Port 22 with `no-auth-ssh`** — Routes the connection to the built-in Tailscale SSH handler.
2. **`exit-node` service** — Accepts any port and proxies the connection to the destination supplied by the client.
3. **Empty port set** — Default mode streams data to STDOUT and exits after EOF.
4. **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:

```bash
tailcat --serve=8080

# Output: # 🐈 Server listening with new address: <token>

```

Serve a range plus the auth-free SSH server:

```bash
tailcat --serve=8000-8099,no-auth-ssh

```

Enable universal port forwarding (all ports):

```bash
tailcat --serve=all

```

Run as an exit node for arbitrary destination forwarding:

```bash
tailcat --serve=exit-node

```

Connect from the client side to a specific served port:

```bash
tailcat <token> 8080

```

## Summary

- **`tailcat --serve`** activates TCP server mode with granular port control.
- The **`parsePortSet`** function in [`cmd/tailcat/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go) handles 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.