What Is a Tailcat Address? Structure, Contents, and Parsing Guide

A tailcat address is a compact, URL-safe string starting with "tc" that contains a base64-url-encoded, CBOR-serialized ConnInfo structure encoding the server's WireGuard public key, optional discovery key, pre-shared key, and DERP relay region, enabling clients to establish connections without external configuration files.

A tailcat address functions as the sole credential required for clients to connect to a Tailcat server in the tailscale/tailcat repository. Unlike traditional VPN systems requiring separate configuration distribution, these self-contained addresses encapsulate all necessary cryptographic and routing information in a single shareable string.

Structure and Format of a Tailcat Address

Every tailcat address follows a strict binary-to-text encoding scheme designed for portability and compactness.

The "tc" Prefix and Encoding Scheme

A valid tailcat address always begins with the literal prefix tc followed by a base64-url-encoded representation (URL-safe base64 without padding) of a CBOR-encoded ConnInfo structure. This design ensures the address remains compact while being safely embeddable in URLs and QR codes.

In tailcat.go lines 46-49, the Addr type is defined as a string wrapper that guarantees this format. The encoding logic resides in the ConnInfo.Addr() method (lines 146-155), while parsing is handled by ParseAddr and ParseAddrRaw (lines 52-55).

CBOR Wire Format

The underlying binary structure uses Concise Binary Object Representation (CBOR) rather than JSON for space efficiency. The wireConnInfo struct defined in wire.go specifies the exact field layout transmitted over the wire. The parseWire function in tailcat.go (lines 26-40) handles CBOR unmarshaling using cbor.Unmarshal to convert the wire format back into the in-memory ConnInfo structure.

What Information a Tailcat Address Contains

A tailcat address encapsulates exactly the fields defined in the ConnInfo structure. When you decode an address, you extract the following cryptographic and routing data:

ServerPublic (WireGuard Node Key) The 32-byte WireGuard public key of the server serves as the unguessable identifier. According to the source code, this is stored as a raw key without the "nodekey:" prefix. This field is always present and forms the cryptographic identity of the server.

ServerDiscoPublic (Discovery Key) An optional 32-byte public key used for path-discovery packets (DERP hole-punching). This is separate from the WireGuard key and may be omitted for compatibility with older clients.

PresharedKey (WireGuard PSK) An optional 256-bit pre-shared key that hardens the WireGuard handshake against quantum attacks. When this field contains non-zero data, the entire tailcat address must be treated as a secret, as possession of the address grants connection capability.

Region Routing Information The address must contain either RegionID (a short numeric identifier like 1 for "us-east") or a full Region structure containing the complete DERP map including node lists. Using RegionID produces shorter addresses, while including the full Region structure creates a self-contained address requiring no external DERP lookup.

Synthetic Fields When parsed via ParseAddr, missing fields such as RegionCode and node names are synthesized for convenience, though these do not exist in the raw encoded address.

Encoding and Decoding Implementation

The tailscale/tailcat source code implements address generation and parsing through specific methods in tailcat.go.

Generating Addresses

Servers create addresses through the ConnInfo.Addr() method (lines 146-155), which:

  1. Serializes the ConnInfo to the wireConnInfo format
  2. CBOR-encodes the structure
  3. Applies base64-url encoding without padding
  4. Prepends the tc prefix

Parsing Addresses

Clients use ParseAddr (lines 52-55) to decode addresses. This function:

  1. Strips the tc prefix
  2. Base64-url decodes the payload
  3. CBOR-deserializes into wireConnInfo via parseWire (lines 26-40)
  4. Converts to the high-level ConnInfo type

For advanced use cases requiring raw byte access, ParseAddrRaw provides lower-level parsing capabilities.

Address Resolution

When an address contains only a RegionID rather than full region details, the Resolve method (lines 1000-1023) expands the address to include complete DERP map information, either from cache or by fetching region definitions.

Practical Code Examples

Creating a Server Address

Generate a new server key pair and produce its tailcat address:

// Create server keys and configure DERP region
priv := tailcat.NewPrivateKey()          // tailcat.go lines 16-27
priv.Public.RegionID = 1                 // DERP region 1 = "us-east"
addr := priv.Public.Addr()               // Returns "tc..." string
fmt.Println("Tailcat address:", addr)

Parsing a Client Address

Extract connection details from a received address:

// Parse the tailcat address string
addr := tailcat.Addr("tcZJ...")
ci, err := tailcat.ParseAddr(addr)       // tailcat.go lines 52-55
if err != nil {
    log.Fatalf("invalid address: %v", err)
}
fmt.Printf("Server public key: %s\n", ci.ServerPublic)
fmt.Printf("DERP region ID: %d\n", ci.RegionID)

Resolving Region Information

Expand a minimal address to include full DERP details:

// Resolve region ID to full region data
resolved, err := addr.Resolve(context.Background())
if err != nil {
    log.Fatalf("resolve failed: %v", err)
}
fmt.Println("Resolved address:", resolved) // Now contains full DERP map

Summary

  • A tailcat address is a tc-prefixed string encoding server connection parameters as base64-url CBOR data
  • The address contains the server's WireGuard public key, optional discovery key, optional pre-shared key, and DERP region information
  • Addresses are generated via ConnInfo.Addr() in tailcat.go and parsed via ParseAddr or ParseAddrRaw
  • The wire format uses CBOR compression rather than JSON for space efficiency
  • When a preshared key is present, the entire address must be treated as sensitive credentials
  • Minimal addresses using RegionID can be expanded to full addresses using the Resolve method

Frequently Asked Questions

What does a tailcat address look like?

A tailcat address appears as a short string beginning with "tc" followed by URL-safe base64 characters. For example: tcZJ4OMLGW5dU8Bz8J1fT2K3mN9pQr5sTu7vWx9YzAbCdEfGhIjKlMnOpQrStUvWxYz. The exact length varies based on whether the address contains a numeric RegionID or a full DERP map.

Is a tailcat address sensitive information?

It depends on the contents. Addresses containing a non-zero PresharedKey field must be treated as secrets, as they grant connection access to the server. Even without a pre-shared key, the address contains the server's public keys, so sharing it allows anyone to attempt connections to your server, though they cannot decrypt traffic without the corresponding private keys.

How does the encoding differ from standard base64?

Tailcat addresses use base64-url encoding (RFC 4648 §5), which replaces the standard + and / characters with - and _ respectively, and omits padding characters (=). This ensures the address remains valid in URL paths and JSON strings without additional escaping.

Can I parse a tailcat address without network access?

Yes, basic parsing via ParseAddr requires no network connectivity because the address is self-contained. However, if the address only contains a RegionID rather than full DERP details, calling Resolve will require network access to fetch the complete region definitions unless they exist in the local cache.

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 →