# How Is the Tailcat Address Encoded? Inside the tailscale/tailcat Source Code

> Discover how tailcat addresses are encoded. Learn about CBOR serialization, Base64-URL encoding, and the 'tc' prefix directly from the tailscale/tailcat source code.

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

---

**A tailcat address is encoded by first serializing the connection metadata into a compact CBOR struct with single-character field keys, then applying unpadded Base64-URL encoding, and finally prefixing the result with the literal string "tc".**

The tailscale/tailcat repository implements a secure tunneling protocol that exchanges compact, self-contained connection tokens. Understanding how the tailcat address is encoded reveals a carefully optimized serialization pipeline designed to produce short, URL-safe strings that embed cryptographic credentials without requiring external lookups.

## The Three-Step Encoding Pipeline

The encoding logic converts a `ConnInfo` struct into a printable address through a strict transformation chain defined across [`wire.go`](https://github.com/tailscale/tailcat/blob/main/wire.go) and [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go).

### Step 1: CBOR Serialization with Compact Field Keys

The process begins by mapping the full `ConnInfo` struct to a wire-format representation named `wireConnInfo`. This struct, defined in **[`wire.go`](https://github.com/tailscale/tailcat/blob/main/wire.go)** at lines 27–31, uses single-character CBOR field keys (`p`, `k`, `q`, `r`, `i`) to minimize the serialized byte length.

The `Addr()` method—implemented in **[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)**—marshals this wire struct using CBOR (Concise Binary Object Representation). This choice prioritizes compactness over human readability, ensuring the final string remains short enough to copy-paste easily.

### Step 2: Base64-URL Encoding Without Padding

After CBOR serialization, the raw bytes are encoded using Go’s `base64.RawURLEncoding`. This encoding scheme uses the URL-safe Base64 alphabet and omits padding characters, producing a compact ASCII string that can be embedded in URLs or QR codes without escaping.

### Step 3: The "tc" Prefix

The final step concatenates the literal prefix `tc` with the Base64-URL string. As noted in the source comments around lines 46–48 of [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), this prefix serves as a visual identifier, making tailcat addresses immediately recognizable and distinguishing them from other Base64-encoded blobs.

The complete pipeline follows this exact sequence:

```

ConnInfo → wireConnInfo (CBOR map) → base64.RawURLEncoding → "tc" + <encoded>

```

## Decoding a Tailcat Address

Reversing the encoding requires three precise operations implemented in **[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)**. First, `parseWire` (lines 28–40) validates that the string begins with the `tc` prefix, strips it, and decodes the remaining text using `base64.RawURLEncoding`. The resulting bytes are unmarshaled from CBOR back into a `wireConnInfo` struct.

Next, `ParseAddr` (lines 55–103) converts the wire representation into a complete `ConnInfo` instance. This function fills in default values for omitted fields—such as region IDs and node names—restoring the full connection metadata required to establish a tunnel.

Because the address embeds the server’s public key and optional pre-shared key, the decoded `ConnInfo` functions as a signed, self-contained capability that clients can use to initiate connections without additional DNS or lookup operations.

## Practical Code Examples

### Generating a Tailcat Address (Server Side)

```go
ci := tailcat.NewPrivateKey().Public // Construct ConnInfo with required fields
addr := ci.Addr()                    // Returns tailcat.Addr (e.g., "tc3K1...fR")
fmt.Println("Tailcat address:", addr)

```

The `Addr()` method internally executes the CBOR serialization, Base64-URL encoding, and prefix concatenation described above.

### Parsing a Tailcat Address (Client Side)

```go
raw := tailcat.Addr("tc3K1...fR")
info, err := tailcat.ParseAddr(raw)
if err != nil {
    log.Fatalf("invalid tailcat address: %v", err)
}
fmt.Printf("Server public key: %s\n", info.ServerPublic)
fmt.Printf("DERP region ID:   %d\n", info.RegionID)

```

Here, `ParseAddr` invokes `parseWire` to handle the Base64 decoding and CBOR unmarshaling, then hydrates the full `ConnInfo` structure.

## Key Source Files and Functions

| File | Purpose |
|------|---------|
| **[`wire.go`](https://github.com/tailscale/tailcat/blob/main/wire.go)** | Defines `wireConnInfo`, `wireRegion`, and `wireNode` structs with compact CBOR field tags to minimize encoded size. |
| **[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)** | Implements `Addr()` for encoding and `ParseAddr()`/`parseWire()` for decoding; contains the `tc` prefix logic and CBOR unmarshalling. |
| **[`connblob_deprecated.go`](https://github.com/tailscale/tailcat/blob/main/connblob_deprecated.go)** | Contains legacy wrapper functions (`ConnBlob`, `ParseConnBlob`) that forward to the current `ParseAddr` API for backward compatibility. |

## Summary

- A tailcat address encodes connection metadata using **CBOR serialization** with single-character field keys to minimize size.
- The CBOR bytes are encoded with **Base64-URL without padding** to ensure URL safety and compactness.
- The literal prefix **`tc`** is prepended to create a recognizable, self-describing identifier.
- Decoding is handled by **`parseWire`** (Base64/CBOR reversal) and **`ParseAddr`** (struct hydration) in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go).
- The resulting string acts as a **self-contained capability** containing the server’s public key and routing information.

## Frequently Asked Questions

### What serialization format does tailscale/tailcat use for addresses?

The implementation uses **CBOR** (Concise Binary Object Representation) rather than JSON or Protocol Buffers. This format was chosen specifically for its ability to produce smaller binary payloads while maintaining schema flexibility, which is critical for keeping the tailcat address short enough to be human-manageable.

### Why does every tailcat address start with "tc"?

The **"tc" prefix** serves as a protocol identifier that allows parsers to quickly validate the address type before attempting expensive decoding operations. According to the source comments in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), this prefix also provides visual confirmation that a string represents a valid tailcat address rather than a generic Base64 blob.

### Where is the Addr() method defined in the tailscale/tailcat repository?

The `Addr()` method is defined as a receiver on `ConnInfo` in **[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)**. This method orchestrates the entire encoding pipeline: it constructs a `wireConnInfo` from the receiver’s fields, marshals it to CBOR, applies Base64-URL encoding, and prepends the "tc" prefix before returning the result as a typed `Addr` string.

### How does the decoder handle missing fields in a tailcat address?

When `ParseAddr` processes an address, it first decodes the minimal `wireConnInfo` struct and then **hydrates a full `ConnInfo`** by filling in sensible defaults for omitted data such as region IDs or node names. This design allows the encoded address to remain compact while the runtime still receives a complete connection configuration object.