How Is the Tailcat Address Encoded? Inside the tailscale/tailcat Source Code
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 and 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 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—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, 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. 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)
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)
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 |
Defines wireConnInfo, wireRegion, and wireNode structs with compact CBOR field tags to minimize encoded size. |
tailcat.go |
Implements Addr() for encoding and ParseAddr()/parseWire() for decoding; contains the tc prefix logic and CBOR unmarshalling. |
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
tcis prepended to create a recognizable, self-describing identifier. - Decoding is handled by
parseWire(Base64/CBOR reversal) andParseAddr(struct hydration) intailcat.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, 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. 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.
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 →