How a Tailcat Server Manages Incoming WireGuard Clients: Deep Dive into User-Space Relay Authentication
A Tailcat server authenticates WireGuard clients by issuing CBOR-encoded connection tokens that embed the server's public key and DERP region map, then validates client public keys against a mutex-protected allowedClients map before allowing tunnel establishment.
Tailcat is a lightweight, user-space relay implementation from tailscale/tailcat that combines DERP (Designated Encrypted Relay for Packets) functionality with WireGuard networking. Unlike traditional kernel-space implementations, this architecture handles cryptographic handshakes and packet forwarding entirely within the application layer. Understanding how the server manages incoming WireGuard clients requires examining the token-based authentication flow and the in-memory authorization mechanisms defined in the source code.
Core Architecture: The locoBackend Structure
The server centers around the locoBackend struct defined in tailcat.go (lines 15-30). This structure maintains the server's cryptographic identity and access control state:
- Server private key: The WireGuard private key used for all cryptographic operations
- DERP map (
lb.dm): A mapping of region IDs to relay nodes for NAT traversal - Authorized clients map (
allowedClients): A map of client public keys to boolean values, protected bylb.mumutex
When the server starts via NewServer(), it initializes this backend with a fresh WireGuard key pair and an empty allowedClients map. All subsequent client authentication flows reference this central authority.
Token-Based Client Authentication
Generating the Connection Token
After server initialization, Server.ConnBlob constructs the authentication credential. As implemented in tailcat.go (lines 7-13), this method:
- Creates a
ConnInfostruct containing the server's public key and a copy of the DERP region map - Encodes the structure using CBOR (Concise Binary Object Representation) serialization
- Prefixes the result with
"tc"to create a compact string token
This token serves as the single source of truth for client configuration, eliminating the need for separate key distribution mechanisms.
Parsing and Validation
When a WireGuard client receives the token, ParseConnBlob (lines 44-78 in tailcat.go) performs the inverse operation:
- Decodes the CBOR payload to restore the server's public key
- Reconstructs the DERP region map for routing decisions
- Validates the token format begins with the
"tc"prefix
The extracted DERP map allows the client to route packets through the correct relay without additional network lookups.
Public Key Authorization
During the WireGuard handshake phase, the server extracts the client's public key from the cryptographic exchange. According to the source code, the server then:
- Acquires
lb.muto ensure thread-safe access - Stores the client public key in the
allowedClientsmap - Releases the mutex
Every incoming encrypted packet undergoes validation against this map. The server checks the packet's source public key against allowedClients; if the key is absent, the packet is dropped silently, preventing unknown peers from establishing tunnels.
WireGuard Data Path Processing
The server runs a user-space WireGuard engine via wireguard-go bound to a UDP socket. The packet processing flow follows this sequence:
- Ingress: Encrypted WireGuard packets arrive at the UDP socket
- Decryption: The
wireguard-goengine decrypts the packet layer - Authorization: The source public key is checked against
allowedClients(protected bylb.mu) - Forwarding: Valid packets are routed to the local Tailscale network or attached process
Because the DERP map is embedded directly in the connection token, the server routes packets to other peers without external STUN queries or DNS resolution. This design minimizes latency and eliminates external dependencies during the data path phase.
Practical Implementation Examples
Starting a Server and Generating Tokens
s, _ := tailcat.NewServer()
s.Start(ctx) // launches the DERP+WireGuard listener
blob := s.ConnBlob() // give this string to clients
fmt.Println("ConnBlob:", blob)
Client Connection Using the Token
c, _ := tailcat.NewClient(blob) // parses the blob, extracts DERP map
c.Start(ctx) // runs wireguard-go and connects to server
Internal Authorization Check
lb := s.lb // locoBackend inside the server
lb.mu.Lock()
ok := lb.allowedClients[clientPubKey]
lb.mu.Unlock()
if !ok {
log.Fatalf("unauthorised client")
}
Summary
- The
locoBackendstruct centralizes server keys, DERP routing tables, and the mutex-protectedallowedClientsauthorization map intailcat.go. Server.ConnBlobgenerates CBOR-encoded tokens prefixed with"tc"that bundle the server public key and complete DERP region information.ParseConnBlobdecodes connection tokens to establish cryptographic context without external network lookups.- Every client public key is validated against
allowedClientsbefore packets enter the WireGuard tunnel, enforcing strict peer authorization. - User-space packet forwarding leverages
wireguard-gowith DERP routing embedded directly in the initial connection token.
Frequently Asked Questions
What prevents unauthorized WireGuard clients from connecting to a Tailcat server?
The server maintains an allowedClients map that stores authorized public keys. When a client completes the WireGuard handshake, the server extracts its public key and verifies its presence in the map under protection of lb.mu. If the key is not found, the connection is rejected before any IP traffic traverses the tunnel.
How does Tailcat eliminate external lookups during peer connection?
The ConnBlob token encodes the complete DERP region map using CBOR serialization as defined in wire.go (types wireConnInfo, wireRegion, and wireNode). When the client parses this token via ParseConnBlob, it immediately possesses all routing information needed to reach DERP relays, removing dependencies on STUN queries or DNS resolution during connection establishment.
Where is the client authorization logic implemented in the codebase?
The core authorization logic resides in tailcat.go, specifically within the locoBackend struct (lines 15-30) and the handshake handling code. The allowedClients map is checked on every packet ingress to verify the source public key. The CBOR wire types used for token serialization are defined separately in wire.go.
Why does the server use a mutex-protected map for client authorization?
The lb.mu mutex protects the allowedClients map because the user-space WireGuard engine processes packets on multiple goroutines. Concurrent read and write operations on the authorization map would cause race conditions without this synchronization primitive, potentially allowing unauthorized packets to slip through during map updates.
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 →