How a Tailcat Client Connects to a Server: WireGuard Tunnel Establishment

A Tailcat client establishes a secure, peer-to-peer WireGuard tunnel to a server using only a compact ConnBlob token, resolving the server address, building a local networking stack, and completing a "meow" handshake over DERP relays.

The Tailcat project (tailscale/tailcat) demonstrates how to establish lightweight, secure connections using Tailscale's networking primitives. Unlike traditional VPN clients that require complex configuration files and certificate authorities, a Tailcat client connects to a server using only a compact token containing the server's public key and relay information. This article explores the three-phase connection process implemented in the source code.

Phase 1: Resolving the Server Address

The connection process begins with parsing the ConnBlob token provided by the server. In tailcat.go:71-74, the ParseConnBlob function decodes this compact representation to extract the server's public key (ServerPublic) and optional DERP region information.

ci, err := ParseConnBlob(c.Server)   // tailcat.go:71-74

If the ConnBlob does not embed a specific DERP region, the client must discover available relays. The ConnInfo.Expand method (tailcat.go:99-112) fetches the DERP map from DefaultDERPMapURL (or a user-provided URL) and selects the optimal region. This resolution step ensures the client knows which relay endpoint to use for the initial connection, even when traversing restrictive NATs or firewalls.

Phase 2: Building the Local Networking Stack

Once the server location is resolved, the client constructs a minimal, embedded version of the Tailscale networking stack. This involves three critical components configured in sequence:

The locoBackend. The newLocoBackend function (tailcat.go:75-87) creates a lightweight backend structure that holds the node's private key, derives its unique IPv6 address via tcAddrForKey, and initializes logging facilities.

The WireGuard Engine. The createEngine function (tailcat.go:57-70) instantiates a userspace WireGuard engine with a DERPAppName of "tailcat-client". This engine is specifically configured to force the disco key to match the node key, ensuring cryptographic consistency across the connection.

The Virtual Network Stack. The newNetstack function (tailcat.go:46-52) wires the WireGuard engine to a netstack.Impl, creating a virtual network interface that allows standard TCP/IP operations over the encrypted tunnel.

Together, these components create a self-contained networking environment capable of routing all traffic through the DERP relay until direct peer-to-peer paths are established.

Phase 3: The Meow Handshake

With the networking stack initialized, the client performs a custom handshake protocol to register itself with the server. This "meow" handshake operates over the DERP relay and consists of a simple request-response pattern.

The client initiates the handshake through Client.Ping (tailcat.go:388-404), which sends a MeowPing packet to the server's DERP endpoint. The client's onDERPRecv callback (tailcat.go:158-168) filters incoming traffic, specifically listening for the Meowed response packet.

On the server side, the onDERPRecv handler (tailcat.go:256-267) processes the incoming MeowPing, invokes onMeow to add the client as a WireGuard peer, and replies with the Meowed acknowledgment. This exchange simultaneously authenticates the client and establishes the peer relationship in the WireGuard configuration.

Once the handshake completes and c.upDone is marked, the engine's peerAllowedIPs and peerByIP mappings route traffic through the encrypted tunnel, enabling transparent TCP connections via Client.DialTCP or Client.Dial.

Complete Implementation Example

The following example demonstrates the entire connection flow, from token parsing to establishing a TCP tunnel:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"tailscale.com/types/key"
	"tailscale.com/tailcat"
)

func main() {
	// 1️⃣ The server gave us this token (ConnBlob).  In practice copy it from the server's output.
	const serverToken tailcat.ConnBlob = "tcomFWcYg…"

	// 2️⃣ Create a client; the token is all we need.
	c := tailcat.NewClient(serverToken)

	// 3️⃣ Optional: set a custom logger.
	c.Logf = log.Printf

	// 4️⃣ Test connectivity – this will start the stack, resolve the DERP region,
	//    perform the meow handshake, and then send a test packet.
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	if res, err := c.Ping(ctx); err != nil {
		log.Fatalf("ping failed: %v", err)
	} else {
		fmt.Printf("handshake latency: %v\n", res.Latency)
	}

	// 5️⃣ Open a TCP connection to the server (e.g. port 22 for the optional SSH server).
	conn, err := c.DialTCPPort(ctx, "127.0.0.1:22")
	if err != nil {
		log.Fatalf("dial failed: %v", err)
	}
	defer conn.Close()
	fmt.Println("TCP tunnel to server established")
}

Summary

  • A Tailcat client requires only a ConnBlob token to connect, eliminating the need for configuration files or certificate authorities.
  • The connection process involves three phases: address resolution (parsing the token and fetching DERP maps), stack construction (building the locoBackend, WireGuard engine, and netstack), and the meow handshake (exchanging MeowPing/Meowed packets over DERP).
  • The handshake simultaneously authenticates the client and registers it as a WireGuard peer, after which standard TCP/IP operations work transparently over the encrypted tunnel.
  • Key source locations include tailcat.go for the core logic and wire.go for the CBOR serialization format used in ConnBlob encoding.

Frequently Asked Questions

What is a ConnBlob in the Tailcat protocol?

A ConnBlob is a compact, CBOR-encoded token that contains the server's public key and optional DERP region information. According to the tailcat source code, this token is parsed by ParseConnBlob (tailcat.go:71-74) to extract the ConnInfo necessary to locate and authenticate the server, serving as the sole credential required for connection.

Why does Tailcat use a "meow" handshake instead of standard WireGuard key exchange?

The meow handshake provides application-layer registration over the DERP relay before WireGuard traffic begins. As implemented in tailcat.go:256-267, this allows the server to dynamically add the client as a peer (onMeow) upon receiving the MeowPing, ensuring the server knows which public keys to accept before the client attempts to send encrypted WireGuard packets.

How does Tailcat differ from the standard Tailscale client connection process?

Tailcat embeds a minimal "locoBackend" rather than using the full LocalBackend found in the main Tailscale daemon. This lightweight approach, visible in newLocoBackend (tailcat.go:75-87), creates a userspace-only implementation without requiring system TUN devices or administrative privileges, making it suitable for embedded applications or library usage.

Can a Tailcat client connect through corporate firewalls and NAT?

Yes. The client uses DERP (Designated Encrypted Relay for Packets) relays to establish the initial connection. The ConnInfo.Expand method (tailcat.go:99-112) discovers available relays, and all initial handshake traffic traverses HTTPS-like connections through these relays, allowing connectivity even when both peers are behind strict NATs or egress firewalls.

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 →