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

> Learn about tailcat addresses, a URL-safe string encoding WireGuard keys and DERP regions for effortless client connections. Understand its structure and contents.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: api-reference
- Published: 2026-09-08

---

**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`](https://github.com/tailscale/tailcat/blob/main/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`](https://github.com/tailscale/tailcat/blob/main/wire.go) specifies the exact field layout transmitted over the wire. The `parseWire` function in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/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`](https://github.com/tailscale/tailcat/blob/main/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:

```go
// 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:

```go
// 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:

```go
// 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`](https://github.com/tailscale/tailcat/blob/main/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.