Configuration Options for a Tailcat Server: Complete Guide

A Tailcat server can be configured either programmatically by constructing a tailcat.Server struct or via command‑line flags, with both approaches exposing identical knobs for keys, DERP regions, access control, and traffic handling.

The tailscale/tailcat repository provides a lightweight, embedded relay server that exposes its configuration through the Server struct defined in tailcat.go. Whether you are integrating Tailcat as a library into a Go application or deploying it as a standalone binary, understanding these configuration options controls identity persistence, bootstrap connectivity, and security policy.

Core Configuration Fields

The Server struct in tailcat.go (lines 277–352) defines every tunable parameter. These fields accept values directly when instantiating the server programmatically.

Identity and Keys

The Key field (Server.Key at line 277) holds the node private key used for WireGuard identity. If left zero, the server generates an ephemeral key on each start, resulting in a new address token every time. Providing a persistent key enables stable addressing across restarts.

srv := &tailcat.Server{
    Key: privKey, // key.NodePrivate value
}

Logging

Server.Logf (line 282) accepts a logger function matching func(format string, args ...interface{}). When nil, it defaults to log.Printf. This controls verbosity for DERP region selection and connection diagnostics.

DERP Region Selection

Three fields govern how the server discovers its bootstrap relay:

  • Server.Region (line 286): Accepts a *tailcfg.DERPRegion struct directly, bypassing map lookups entirely. Use this when operating a private DERP server.
  • Server.RegionID (line 290): An integer ID referencing the default DERP map. When zero, the serverauto-selects the nearest region via latency probe.
  • Server.DERPMapURL (line 294): Overrides the default https://tailcat.dev/derpmap.json endpoint for fetching the DERP map.
  • Server.DERPMapCache (line 298): Implements caching for fetched maps. If nil, an in-process memory cache is used.

Access Control

Security boundaries are enforced through two fields:

  • Server.AllowedClients (line 304): A slice of []key.NodePublic. Only these clients may connect; an empty slice permits all connections.
  • Server.AllowProxy (line 312): A callback func(netip.AddrPort) bool that determines whether a specific TCP/UDP proxy destination is permitted when operating in SOCKS5 proxy mode.

Traffic Handling

Inbound connection behavior is defined by three fields:

  • Server.OnTCP (line 324): A handler factory func(port uint16) func(net.Conn) for connections directed to the server’s own address. Return nil to send RST.
  • Server.OnTCPForward (line 334): A handler factory for connections relayed through the server in exit-node mode.
  • Server.ServedTCPPorts (line 338): A slice of []filter.PortRange restricting which ports the server admits. When nil, all ports are allowed.

Command-Line Interface Options

The cmd/tailcat/tailcat.go file (lines 48–58) maps CLI flags directly to the Server struct fields inside the server() function (lines 74–88).

Key Management

The --key flag accepts a path to a *.private.json file or the literal string new to force ephemeral key generation. This directly sets Server.Key.


# Use a persistent key file

tailcat --key=myserver.private.json

# Force ephemeral (default if file missing)

tailcat --key=new

Service Declaration

The --serve flag (parsed in parsePortSet at line 660) accepts a comma-separated list of ports or service names (all, exit-node, no-auth-ssh). This populates ServedTCPPorts, OnTCP, and OnTCPForward automatically.


# Serve ports 8080 and 22, plus built-in SSH

tailcat --serve=8080,22,no-auth-ssh

Client Restrictions

The --allow flag accepts a comma-separated list of client public keys (prefixed with nodekey:) or none to deny all. The CLI populates Server.AllowedClients via Server.AddAllowedClient.


# Restrict to specific clients

tailcat --allow=nodekey:abcdef123456...,nodekey:fedcba654321...

DERP Customization

  • --derpmap-url: Overrides Server.DERPMapURL to point at a custom DERP map endpoint.
  • --full-address: Embeds the complete DERP node list into the printed ConnBlob rather than just a region ID, producing a self-contained but longer address token.

Output Formatting

The --json flag emits the server address as JSON ({ "listenAddr": "<token>" }) on stdout for programmatic parsing.

Environment Variables

TAILCAT_ADDR_FILE (handled at lines 34–47 in cmd/tailcat/tailcat.go) specifies a file path or TCP address where the server writes its connection token immediately after startup.

Configuration Precedence and Interaction

The server() function initializes the Server struct following a strict precedence:

  1. Key selection: Loads from --key path or generates ephemeral.
  2. DERP resolution: Checks Server.Region → Server.RegionID → auto-detect ( probes for lowest latency).
  3. Access control: Parses --allow list; none creates an empty allow-list blocking all clients.
  4. Port handling: Expands --serve into ServedTCPPorts, OnTCP, and OnTCPForward handlers.
  5. Address formatting: --full-address determines whether ConnBlob contains full DERP node details or a compact region ID.

Practical Implementation Examples

Programmatic Server Setup

This example configures a server with a fixed DERP region, specific port restrictions, and a custom TCP handler:

package main

import (
	"log"
	"net"

	"github.com/tailscale/tailcat"
	"tailscale.com/types/key"
	"tailscale.com/types/logger"
	"tailscale.com/wgengine/filter"
)

func main() {
	priv := key.NewNode() // Or load from disk
	
	srv := &tailcat.Server{
		Key:            priv,
		Logf:           logger.StdLogger(log.Printf),
		RegionID:       302, // San Francisco region
		AllowedClients: []key.NodePublic{}, // Allow all
		ServedTCPPorts: []filter.PortRange{{First: 8080, Last: 8080}},
		OnTCP: func(port uint16) func(net.Conn) {
			return func(c net.Conn) {
				c.Write([]byte("HTTP/1.1 200 OK\r\n\r\nHello from Tailcat!\n"))
				c.Close()
			}
		},
	}

	if err := srv.Start(); err != nil {
		log.Fatal(err)
	}
	log.Printf("Token: %s", srv.ConnBlob())
	select {} // Block forever
}

Basic CLI Usage

Run an ephemeral server with auto-selected DERP:

tailcat

# Selected bootstrap relay region 302, San Francisco

# 🐈 Server listening with new address: tcomFwWCCcjS...

Advanced CLI with Persistent Keys

Generate a persistent key, then run with full address embedding:


# Generate key

tailcat genkey --key=myserver

# Run with restrictions

tailcat --key=myserver \
        --serve=22,no-auth-ssh \
        --allow=nodekey:abc123... \
        --full-address \
        --json

Summary

  • Two configuration methods: Direct struct manipulation in Go or CLI flags that map 1:1 to fields.
  • Identity control: Use Server.Key or --key for persistent addressing; omit for ephemeral.
  • Bootstrap flexibility: Hard-code a Region, specify a RegionID, or auto-detect via latency probe.
  • Security layers: AllowedClients provides allow-list authentication; ServedTCPPorts restricts the packet filter.
  • Traffic routing: OnTCP handles direct connections; OnTCPForward enables exit-node relaying.

Frequently Asked Questions

What is the difference between Server.Region and Server.RegionID?

Server.Region accepts a complete *tailcfg.DERPRegion struct, allowing you to hard-code a private DERP server and bypass map lookups entirely. Server.RegionID is an integer referencing a region in the fetched DERP map (default or custom via --derpmap-url). When both are zero, the server performs automatic latency-based selection.

How do I generate a persistent key for my Tailcat server?

Use the built-in key generation command: tailcat genkey --key=filename. This writes a filename.private.json file. Reference it in subsequent runs with --key=filename. Programmatically, save the key.NodePrivate bytes to disk and load them into Server.Key.

Can I restrict which clients connect to my Tailcat server?

Yes. Populate Server.AllowedClients with a slice of key.NodePublic values, or use the --allow CLI flag with comma-separated node keys. An empty slice (CLI default) permits all clients. Specifying --allow=none creates an empty allow-list that rejects every connection attempt.

What DERP region does Tailcat use by default?

If neither Server.Region nor Server.RegionID is set (and no --derpmap-url override exists), the server fetches the default map from https://tailcat.dev/derpmap.json and selects the nearest region via latency probe. This auto-detection runs during Server.Start() and can be observed via the configured Logf logger.

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 →