What Are the Components of a Tailcat Address? ServerPublic, ServerDiscoPublic, and PresharedKey Explained

A tailcat address is a compact, URL-safe string that encodes a server's WireGuard public key (NodeKey), a discovery public key (DiscoKey), an optional pre-shared key (PSK), and DERP routing metadata to enable secure tunnel establishment without DNS exposure.

A tailcat address is the primary connection credential generated by the Tailcat server when it starts. According to the tailscale/tailcat source code, this identifier encapsulates everything a client needs to authenticate the server and route packets through the correct DERP relay. Understanding the individual components—ServerPublic (NodeKey), ServerDiscoPublic (DiscoKey), and PresharedKey (PSK)—is essential for both operators deploying servers and developers integrating the protocol.

Core Cryptographic Components

ServerPublic (NodeKey)

The ServerPublic component, referred to as NodeKey in the source code, is the server's WireGuard node-public key. This is a 32-byte Curve25519 public key that serves as the primary authentication material for the tunnel.

When a client connects, it uses this key to derive the shared secret that encrypts the data channel. In tailcat.go, the type Addr struct defines this field (lines 146–166), and the key is populated via the tcAddrForKey function when the server generates its identity.

ServerDiscoPublic (DiscoKey)

The ServerDiscoPublic (or DiscoKey) is a separate Curve25519 public key used exclusively for the discovery protocol. This key is exchanged out-of-band via the address string itself, allowing the client to reach the server's DERP relay without exposing the sensitive node key in DNS lookups.

According to the comments in Addr.MarshalBinary (lines 174–182), this separation allows the server to prove ownership of the address without leaking the NodeKey itself, providing an additional layer of security during the initial handshake phase.

PresharedKey (PSK)

The PresharedKey (PSK) is an optional 32-byte random value included to harden the tunnel against key-compromise attacks. When enabled via the --psk flag (which defaults to true), the PSK ensures that even if the node key were leaked, an attacker would still need this additional secret to derive the correct shared secret.

The source code conditionally appends this value in Addr.MarshalBinary (lines 188–196) only when a.PSK != nil. The PSK is deliberately omitted when targeting compatibility with very old clients (≤ v0.5.0).

Routing and Metadata Fields

DERP Region and Embedded Maps

Beyond cryptographic keys, the tailcat address contains routing information for the DERP (Designated Encrypted Relay for Packets) network. This is either a numeric region ID (e.g., 1 for "us-west") or a full DERP map listing relay nodes.

When using the --full-address or --embed-derp-map flags, the server bakes this data directly into the address, removing the need for a separate map fetch and speeding up connection setup. The parsing logic resides in parseTailcatAddr around lines 1030–1080, which handles region validation and emits "invalid tailcat address" errors when parsing fails.

Version Prefix and Compatibility

Every tailcat address begins with the literal prefix tc, followed by a version byte (currently 0). This versioning scheme, checked early in ParseTailcatAddr (lines 1030–1035), guarantees forward compatibility by allowing older clients to reject newer address formats they cannot parse.

Parsing and Decoding Examples

You can inspect the components of any tailcat address using the CLI or programmatically via the Go API.


# Generate a new server key with full address embedding and PSK enabled

$ tailcat genkey --full-address --psk

# Output: tc-1B2C3D4E5F6G7H8I9J0K...

# Decode the address to inspect its components

$ tailcat decode tc-1B2C3D4E5F6G7H8I9J0K...
{
  "nodeKey": "e6c8e3a6...",
  "discoKey": "c7d2f1...",
  "psk": "7f4b...",
  "regionID": 2,
  "derpMap": null
}

To parse addresses programmatically:

import "github.com/tailscale/tailcat"

addrStr := "tc-ABCD..." // Address obtained from server output
addr, err := tailcat.ParseTailcatAddr(addrStr)
if err != nil {
    log.Fatalf("invalid tailcat address: %v", err)
}

fmt.Printf("Server node key: %x\n", addr.NodeKey)
fmt.Printf("Disco key: %x\n", addr.DiscoKey)
if addr.PSK != nil {
    fmt.Printf("PSK present (hex): %x\n", addr.PSK)
}

Summary

  • A tailcat address encodes the server's NodeKey (WireGuard public key), DiscoKey (discovery public key), and optional PSK into a single URL-safe string.
  • The NodeKey (defined in tailcat.go lines 146–166) provides primary tunnel authentication, while the DiscoKey (serialized in MarshalBinary lines 174–182) enables secure DERP routing without DNS exposure.
  • The PresharedKey (conditionally appended in lines 188–196) adds a layer of post-quantum resistance and is controlled via the --psk CLI flag.
  • DERP region IDs or embedded maps (parsed in lines 1030–1080) tell clients which relay to contact for initial connection.
  • The version prefix (tc + version byte) ensures backward compatibility as specified in ParseTailcatAddr.

Frequently Asked Questions

What is the difference between NodeKey and DiscoKey in a tailcat address?

The NodeKey is the server's Curve25519 WireGuard public key used for encrypting the actual data tunnel, while the DiscoKey is a separate Curve25519 key used only for the discovery protocol to locate the server via DERP relays. This separation prevents exposure of the encryption key during the routing discovery phase.

Is the PresharedKey (PSK) mandatory for tailcat connections?

No, the PSK is optional but enabled by default. When the --psk flag is set (the default behavior), a 32-byte random key is appended to the address. You can disable it with --psk=false for compatibility with legacy clients (version 0.5.0 and earlier), though this removes the additional security layer against key-compromise attacks.

How does the DERP region information help establish a connection?

The DERP region ID or embedded map tells the client which encrypted relay server to contact initially when a direct peer-to-peer connection is not yet established. By embedding this data directly in the tailcat address (via --embed-derp-map), clients can connect immediately without fetching a separate configuration file from a coordination server.

Can I decode a tailcat address without the tailcat CLI?

Yes, you can use the Go API by importing github.com/tailscale/tailcat and calling ParseTailcatAddr(addrStr). This function, located in tailcat.go around line 1030, returns a struct containing the NodeKey, DiscoKey, PSK, and RegionID fields as byte slices and integers.

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 →