Tailcat Connection Token Format: Structure, Encoding, and Parsing
A Tailcat connection token is a URL-safe string consisting of the literal prefix tc followed by a base64-url-encoded CBOR payload containing the server's WireGuard public key and DERP relay metadata.
The Tailcat connection token (also referred to as a ConnBlob) enables clients to establish WireGuard tunnels over DERP relays without prior configuration. According to the tailscale/tailcat source code, this compact format encodes all necessary connection parameters into a single string suitable for URLs, command-line arguments, or QR codes.
Token Structure and Encoding
The tc Prefix and ConnBlob Type
Every valid token begins with the literal ASCII characters tc. This prefix identifies the string as a Tailcat connection blob before the payload is decoded. In tailcat.go (lines 36‑40), the ConnBlob type is defined as a string alias, while the underlying ConnInfo struct holds the deserialized data.
CBOR Wire Format
The payload following the tc prefix is the result of base64-url-encoding (specifically base64.RawURLEncoding without padding) a binary CBOR (Concise Binary Object Representation) blob. The CBOR schema is defined in wire.go, which declares the wireConnInfo structure using compact field keys:
p—ServerPublic(NodePublic): The server's 32-byte raw WireGuard public key (no "nodekey:" prefix).k—ServerDiscoPublic(*DiscoPublic): Optional separate discovery public key used for NAT traversal path discovery.i—RegionID(int64): Numeric DERP region ID used when the token does not embed full region details.r—Region([]*wireRegion): Full DERP region description (list of relays) embedded when the token contains complete region data, including relay hostnames and ports.
Generating a Tailcat Connection Token
Servers generate tokens by populating a ConnInfo struct and calling the ConnBlob() method. The implementation in tailcat.go (lines 44‑77) handles CBOR marshaling and base64 encoding.
// Create and start a Tailcat server.
srv := &tailcat.Server{}
if err := srv.Start(); err != nil {
log.Fatalf("Server start failed: %v", err)
}
// Obtain the self-contained connection token.
blob := srv.ConnBlob() // Returns ConnBlob type (string alias)
fmt.Println("Tailcat token:", blob)
The ConnBlob() method internally invokes lb.connBlob() to build the ConnInfo and serialize it (see tailcat.go lines 93‑99 and 174‑35). By default, this produces a minimal token containing only the RegionID (i field) rather than full relay addresses.
Parsing and Validating Tokens
Clients decode tokens using ParseConnBlob(), implemented in tailcat.go (lines 332‑370). This function validates the tc prefix, base64-url-decodes the payload, and CBOR-unmarshals it into a wireConnInfo struct before converting back to the high-level ConnInfo type.
// Assume token is received from the server.
token := tailcat.ConnBlob("tcABCdef...")
ci, err := tailcat.ParseConnBlob(token)
if err != nil {
log.Fatalf("Invalid token: %v", err)
}
fmt.Printf("Server public key: %s\n", ci.ServerPublic)
fmt.Printf("DERP region ID: %d\n", ci.RegionID)
The ParseConnBlob function delegates CBOR decoding to parseWire (see tailcat.go lines 332‑340), which restores omitted fields and handles backward compatibility for various token versions.
Full vs. Minimal Token Variants
Tailcat supports two token densities controlled by the -full-address CLI flag defined in cmd/tailcat/tailcat.go.
- Minimal tokens contain only
ServerPublic, optionalServerDiscoPublic, andRegionID. These are compact but require the client to already possess the DERP map for that region ID. - Full tokens embed the complete
Regionarray (rfield) containingwireRegionandwireNodestructures with relay hostnames and ports, resulting in a longer but self-contained token.
// Server-side: Request a full-address token.
full := true // Corresponds to -full-address flag
if full {
// ConnBlob() will populate ci.Region instead of only ci.RegionID,
// embedding the entire DERP region description.
}
fmt.Println(srv.ConnBlob())
When the full flag is set, lb.connBlob() populates ci.Region with the complete relay list (see tailcat.go lines 173‑228), producing a token that requires no external DERP configuration to connect.
Summary
- Format: Literal
tcprefix + base64-url-encoded CBOR payload. - Encoding:
base64.RawURLEncodingof CBOR-serializedwireConnInfo. - Key Fields:
p(server public key),k(disco key),i(region ID),r(full region). - Generation: Use
ConnInfo.ConnBlob()intailcat.go(lines 44‑77). - Parsing: Use
ParseConnBlob()intailcat.go(lines 332‑370). - Variants: Minimal (region ID only) vs. full (embedded relay addresses).
Frequently Asked Questions
What data is encoded inside a Tailcat connection token?
The CBOR payload encodes the server's WireGuard public key (ServerPublic), an optional discovery public key (ServerDiscoPublic), and either a DERP region ID or a full region description. This data allows the client to locate the server and establish a WireGuard tunnel over the correct relay.
How is the token encoded to ensure URL safety?
After CBOR binary serialization, the payload is encoded using Go's base64.RawURLEncoding, which produces URL-safe strings without padding characters. The resulting string is prefixed with tc, ensuring the entire token can be safely transmitted in URLs, JSON strings, or scanned from QR codes.
What is the difference between minimal and full connection tokens?
Minimal tokens contain only a RegionID integer, requiring the client to already know the DERP region's relay addresses. Full tokens embed the complete Region structure with explicit relay hostnames and ports, making them self-contained but significantly longer. The tailcat CLI -full-address flag toggles this behavior.
Where are the token format definitions located in the repository?
The high-level types (ConnBlob, ConnInfo) and serialization logic reside in tailcat.go (lines 36‑40 and parsing at lines 332‑370). The low-level CBOR wire structures (wireConnInfo, wireRegion, wireNode) and their conversion helpers are defined in wire.go.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →