# How Tailcat Uses Netstack (gVisor) to Enable Inbound Connections

> Discover how Tailcat uses Netstack (gVisor) to enable inbound connections by embedding a full TCP/IP stack that routes encrypted packets to user callbacks.

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

---

**Tailcat embeds a full gVisor TCP/IP stack inside the process, wiring it to a WireGuard engine that routes encrypted DERP packets to user-defined callbacks when inbound TCP connections arrive on the server's virtual IPv6 address.**

The `tailscale/tailcat` repository implements a lightweight networking layer that enables inbound connections without external control planes by running a complete TCP/IP stack in userspace. By leveraging Tailscale's gVisor-based `netstack`, Tailcat processes inbound TCP SYN packets directly inside the process, decrypting WireGuard traffic and dispatching connections to registered handlers. This architecture allows servers to accept connections over encrypted tunnels while maintaining full control over the networking stack.

## What Is the Tailcat Netstack?

Tailcat's netstack is an instance of the gVisor TCP/IP implementation maintained by Tailscale, instantiated when `newNetstack` is called during server initialization. Unlike traditional socket-based networking, this approach creates a virtual network interface entirely within the process memory, allowing the application to intercept and handle packets before they reach the host operating system.

The netstack is attached to a WireGuard engine that receives encrypted UDP packets from DERP relays. When the backend starts via `locoBackend.Start`, the stack begins processing IP packets and matching them against configured handlers.

## How Inbound Connections Work in Tailcat

Inbound connections follow a deterministic path from DERP relay to user callback, managed by three core mechanisms.

### The TCP Handler Selector

The netstack uses `GetTCPHandlerForFlow` to determine how to handle each inbound TCP connection. In [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) (lines 49-66), the server configures this selector to distinguish between direct connections and forwarded traffic:

```go
ns.GetTCPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
    if dst.Addr() == lb.addr {
        // Direct connections to the server's own address
        if s.OnTCP == nil {
            return nil, true // send RST
        }
        return s.OnTCP(dst.Port()), true
    }
    // Relayed "exit-node" connections
    if s.OnTCPForward == nil {
        return nil, true // send RST
    }
    return s.OnTCPForward(dst), true
}

```

When the destination address matches the server's virtual IPv6 address—derived from the node key via `tcAddrForKey`—the selector invokes `Server.OnTCP`. If the destination differs and `Server.OnTCPForward` is configured, the connection enters exit-node mode and forwards to the target address. Returning `nil` with `intercept=true` causes the stack to send a TCP RST, rejecting the connection cleanly.

### Packet Filtering and Admission Control

Before packets reach the TCP stack, Tailcat builds a packet filter in `Server.buildFilter` (lines 95-102) to admit only expected traffic:

```go
matches := []filter.Match{{
    IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP}),
    Srcs:    []netip.Prefix{allIPv6},
    Dsts:    selfDsts,
}}

```

This filter permits TCP traffic from any IPv6 source to the server's own address when `ServedTCPPorts` are specified. When `OnTCPForward` is enabled, the filter expands to allow all TCP traffic, supporting exit-node functionality. By filtering at the network layer, Tailcat prevents unwanted packets from consuming resources in the TCP stack.

### The Connection Lifecycle

A complete inbound connection follows this sequence:

1. **DERP Handshake**: The client sends a *Meow* packet; the server responds with *Meowed* via `locoBackend.onMeow`
2. **WireGuard Peer Creation**: The server adds the client's node key as a peer with allowed IPs through `peerAllowedIPs`
3. **Packet Delivery**: Encrypted UDP packets arrive at the WireGuard engine, decrypt, and pass to the netstack
4. **TCP Processing**: The netstack matches the SYN to the server's IPv6 address and invokes the `OnTCP` handler with an established `net.Conn`

The stack maintains connection state until `Server.DrainTCP` signals graceful shutdown, waiting for all TCP endpoints to close before process termination.

## Implementing Inbound Connection Handlers

Applications register callbacks to handle inbound traffic. The following example implements an SSH-style server accepting connections on port 22:

```go
import (
    "log"
    "net"

    "tailscale.com/tailcat"
)

func main() {
    srv := &tailcat.Server{
        OnTCP: func(port uint16) func(net.Conn) {
            if port != 22 {
                return nil // reject other ports
            }
            return func(c net.Conn) {
                defer c.Close()
                log.Printf("incoming SSH connection from %s", c.RemoteAddr())
                // …handle SSH session…
            }
        },
    }
    if err := srv.Start(); err != nil {
        log.Fatalf("server start: %v", err)
    }
    log.Printf("server address: %s", srv.Addr())
    select {} // keep running
}

```

The corresponding client dials the server using the connection blob:

```go
import (
    "context"
    "log"
    "net"

    "tailscale.com/tailcat"
)

func main() {
    // Assume `blob` is the ConnBlob printed by the server
    client := tailcat.NewClient(blob)
    
    conn, err := client.DialTCPPort(context.Background(), 22)
    if err != nil {
        log.Fatalf("dial error: %v", err)
    }
    defer conn.Close()
    log.Printf("connected to %s", conn.RemoteAddr())
}

```

These handlers demonstrate how Tailcat abstracts the complexity of DERP relays and WireGuard encryption, presenting a standard `net.Conn` interface to application code.

## Graceful Shutdown with DrainTCP

Proper resource management requires draining active connections before shutdown. The `Server.DrainTCP` method iterates through all TCP endpoints in the netstack, ensuring each connection closes completely before the process exits. This prevents data loss on inflight connections and allows clean peer removal from the WireGuard engine.

## Summary

- **Tailcat embeds gVisor netstack** to process TCP/IP packets entirely in userspace, avoiding host network configuration
- **Inbound routing** relies on `GetTCPHandlerForFlow` in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) to dispatch connections to `OnTCP` or `OnTCPForward` callbacks based on destination address
- **Security filtering** occurs in `buildFilter`, admitting only TCP traffic destined for the server's virtual IPv6 address or exit-node ranges
- **Connection lifecycle** spans DERP handshake, WireGuard peer setup, and netstack TCP processing, culminating in a `net.Conn` passed to user handlers
- **Graceful shutdown** via `DrainTCP` ensures all connections close properly before process termination

## Frequently Asked Questions

### What is gVisor netstack and why does Tailcat use it?

gVisor netstack is a pure-Go TCP/IP implementation originally developed by Google that runs in userspace. Tailcat uses it to avoid requiring root privileges or host network configuration while still processing raw IP packets. This allows the application to intercept inbound connections at the network layer and route them through WireGuard encryption without kernel-level networking changes.

### How does Tailcat handle forwarded connections (exit-node mode)?

When `Server.OnTCPForward` is configured, the `GetTCPHandlerForFlow` selector in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) returns the forward handler for any destination address that does not match the server's own IPv6 address. The packet filter expands to allow all TCP traffic, and the netstack forwards the connection to the target destination, enabling the server to act as an encrypted exit node for client traffic.

### What happens if OnTCP returns nil?

If `OnTCP` returns `nil` for a given port, the TCP handler selector returns `nil, true` to the netstack. The boolean `true` indicates that the stack should intercept the packet rather than pass it through, resulting in a TCP RST (reset) being sent to the client. This cleanly rejects the connection attempt without leaving the client hanging.

### How does the packet filter improve security?

The packet filter constructed in `Server.buildFilter` (lines 95-102) creates an allowlist at the IP layer before packets reach the TCP stack. By restricting inbound traffic to specific IPv6 destinations and TCP protocols, Tailcat prevents spoofed packets or unexpected protocols from consuming TCP stack resources or triggering unnecessary handler invocations.