How to Use the Tailcat Go Library as a Server: Complete Implementation Guide

The Tailcat Go library provides a zero-value functional server type that creates a WireGuard-encrypted tunnel over Tailscale's DERP relay network without requiring root privileges or host network configuration.

The tailscale/tailcat repository offers a lightweight, userspace networking solution for Go applications. When you use the Tailcat Go library as a server, you embed a complete TCP/IP stack that routes encrypted traffic through Tailscale's infrastructure. This approach eliminates the need for kernel modules or elevated permissions while providing secure connectivity between distributed components.

Understanding the Server Architecture

The tailcat.Server Type

At the core of the implementation is the tailcat.Server struct defined in tailcat.go (lines 315-388). This type encapsulates the entire server state, including WireGuard key management, DERP region selection, and TCP connection handlers. The design philosophy emphasizes simplicity: the zero value of tailcat.Server is immediately functional, requiring no mandatory configuration fields to begin operation.

Zero-Value Ready Design

Unlike traditional network servers that require extensive setup, you can instantiate and start a Tailcat server with minimal boilerplate. Optional fields such as Logf for logging, Region for specific DERP regions, or RegionID for geographic selection allow customization, but only the OnTCP callback is typically necessary for handling inbound connections.

Server Initialization and Startup Process

Key Generation and DERP Selection

When you invoke the Start() method (implemented in tailcat.go lines 90-140), the server executes a precise initialization sequence. First, it checks the Server.Key field; if zero, it generates an ephemeral WireGuard key using key.NewNode. Next, it determines the optimal DERP region by evaluating the Region, RegionID, or automatically selecting the nearest region based on latency through ConnInfo.Expand.

Transport Stack Initialization

The startup process initializes three critical components in tailcat.go (lines 90-140):

  • magicsock: Handles UDP and DERP relay transport
  • netstack: Provides the userspace TCP/IP implementation using gVisor (located in internal/netstack/netstack.go)
  • locoBackend: Orchestrates the coordination between WireGuard and the transport layer

After initialization, the server registers DERP handshake callbacks to respond to client probes.

Handling TCP Connections

Once running, the server exposes TCP listeners and dispatches connections based on their origin. The logic in tailcat.go (lines 84-98) differentiates between direct connections and forwarded traffic.

OnTCP vs OnTCPForward

Configure the OnTCP field to handle connections destined for the server's own address. Alternatively, use OnTCPForward for exit-node mode where the server forwards traffic to other destinations. Both handlers receive a port number and return a function that accepts a net.Conn, allowing you to implement protocol-specific logic for each port.

Complete Server Implementation Example

The following example demonstrates a minimal server implementation that listens on all TCP ports:

// server.go
package main

import (
	"fmt"
	"log"
	"net"

	"github.com/tailscale/tailcat"
)

func main() {
	// Create a Server; the zero value is ready to use.
	s := &tailcat.Server{
		// Handle any TCP port the server receives.
		OnTCP: func(port uint16) func(net.Conn) {
			return func(c net.Conn) {
				fmt.Fprintf(c, "hello from port %v\n", port)
				c.Close()
			}
		},
	}

	// Start the server – this blocks until the DERP connection is ready.
	if err := s.Start(); err != nil {
		log.Fatalf("tailcat server failed: %v", err)
	}

	// Print the connection token that clients will use.
	fmt.Println("Server token:", s.ConnBlob())

	// Keep the process alive (the server runs in background goroutines).
	select {}
}

After starting, the server prints a connection token generated by the ConnBlob() method (logic defined in wire.go). This token encodes the server's WireGuard public key and DERP region information.

Connecting a Client

Clients use this token to establish connections without needing the server's IP address:

// client.go
package main

import (
	"context"
	"io"
	"log"
	"os"

	"github.com/tailscale/tailcat"
)

func main() {
	// The token printed by the server is passed as the first argument.
	client := tailcat.NewClient(tailcat.ConnBlob(os.Args[1]))
	defer client.Close()

	// Dial TCP port 80 on the server.
	c, err := client.DialTCPPort(context.Background(), 80)
	if err != nil {
		log.Fatalf("dial error: %v", err)
	}
	io.Copy(os.Stdout, c) // prints "hello from port 80"
}

Summary

  • The tailcat.Server type in tailcat.go (lines 315-388) provides a zero-value functional server requiring no root privileges
  • The Start() method handles WireGuard key generation, DERP region selection, and initialization of the gVisor-based userspace TCP/IP stack
  • Configure OnTCP callbacks in tailcat.go (lines 84-98) to handle inbound TCP connections by port number
  • The server outputs a connection token via ConnBlob() (implemented in wire.go) that clients use to establish secure tunnels
  • All networking occurs through Tailscale's DERP relay infrastructure without modifying host network configuration

Frequently Asked Questions

Does the Tailcat server require root privileges or kernel modules?

No. The Tailcat Go library operates entirely in userspace using the gVisor netstack implementation (internal/netstack/netstack.go) and Tailscale's magicsock for transport. According to the tailscale/tailcat source code, the server creates a WireGuard tunnel over the existing network interface without modifying routing tables or requiring CAP_NET_ADMIN capabilities.

How does the server handle WireGuard key management?

The Start() method in tailcat.go (lines 90-140) automatically handles key generation. If the Server.Key field is zero upon startup, the library invokes key.NewNode() to create an ephemeral WireGuard key pair. This ephemeral key is then encoded into the connection token returned by ConnBlob(), allowing clients to derive the server's public key for tunnel establishment.

What is the difference between OnTCP and OnTCPForward callbacks?

The OnTCP callback handles connections destined for the server itself, as implemented in the TCP dispatch logic of tailcat.go (lines 84-98). Conversely, OnTCPForward manages exit-node scenarios where the server acts as a forwarder to other destinations. Both callbacks follow the signature func(port uint16) func(net.Conn), enabling per-port handler registration.

How does the DERP handshake establish connectivity?

During startup, the server registers callbacks to handle the Tailcat protocol handshake. When a client sends a "Meow" ping probe through the DERP relay, the server responds with a "Meowed" acknowledgment, completing the WireGuard tunnel establishment. This handshake mechanism, referenced in the Start() method implementation, allows NAT traversal without public IP addresses or port forwarding.

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 →