# How to Use the Tailcat Go Library as a Client

> Learn how to use the Tailcat Go library as a client to connect to a Tailcat server over a WireGuard tunnel. Discover its lightweight, control-plane-free design.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: how-to-guide
- Published: 2026-08-30

---

**The Tailcat Go library provides a lightweight, control-plane-free client that connects to a Tailcat server over a WireGuard tunnel bootstrapped through a DERP relay using a compact `ConnBlob` token.**

The `tailscale/tailcat` repository enables direct, programmatic connections to Tailcat servers without requiring the Tailscale control plane. When you use the Tailcat Go library as a client, you leverage a lazy-loading architecture that initializes networking infrastructure only upon first use, enabling efficient TCP tunneling through encrypted DERP relays.

## Core Architecture Concepts

Tailcat’s client implementation centers on three foundational concepts that eliminate traditional control plane dependencies.

### ConnBlob Token Parsing

The **ConnBlob** is a compact, URL-safe token encoding the server’s public WireGuard key, disco key, and DERP relay information. According to [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), the client parses this token using `ParseConnBlob` to discover the server’s address and preferred DERP region without requiring DNS or centralized coordination.

### Client Struct Configuration

The `Client` struct (defined in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) lines 90-108) encapsulates connection parameters including the server token, optional persistent node key (`Key`), custom DERP map URL overrides (`DERPMapURL`), and logging callbacks (`Logf`). The primary constructor, `NewClient` (lines 46-48), initializes this struct but deliberately avoids network operations.

### Lazy Startup Mechanism

The client employs **deferred initialization**—it does not allocate networking resources until you invoke methods like `Dial`, `DialTCPPort`, or `Ping`. When triggered, the private `ensureStarted` method (line 89) performs three critical operations: it expands the `ConnBlob` (fetching the DERP map if necessary), creates a userspace WireGuard engine, and initializes the netstack. Subsequently, the `up` method (line 26) executes a "meow" handshake to register the client as a WireGuard peer.

## Connection Workflow

Understanding the initialization sequence helps debug connectivity issues and optimize startup performance.

### Triggering Network Initialization

Any network operation triggers the lazy startup sequence. When you call `Ping` (lines 55-78), `Dial` (line 40), or `DialTCPPort` (line 48), the client invokes `up` → `ensureStarted`, expanding the compact token into a full network configuration.

### The Meow Handshake Protocol

After `ensureStarted` establishes the WireGuard engine, the client sends a *meow* ping via `EncodeMeowPing` to the server. The server responds with *meowed* (handled in server-side `onMeow` at lines 47-63), completing peer registration. Once this handshake succeeds, `c.lb.sys.Dialer.Get().UserDial` routes all outbound traffic through the encrypted tunnel.

### DERP Region Selection

If the `ConnBlob` specifies `RegionID: -1`, the client uses the auto-selection logic implemented in [`pickregion.go`](https://github.com/tailscale/tailcat/blob/main/pickregion.go) to identify the lowest-latency DERP relay. You can override this behavior by providing a custom `DERPMapURL` in the `Client` struct configuration.

## Implementation Examples

These self-contained snippets demonstrate how to use the Tailcat Go library as a client. Replace `"tc..."` with the actual `ConnBlob` obtained from your Tailcat server.

```go
package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"github.com/tailscale/tailcat"
)

func main() {
	// 1. Create the client from a server token.
	//   Replace the placeholder with the real ConnBlob string.
	const token tailcat.ConnBlob = "tc..." // ← server‑generated token
	c := tailcat.NewClient(token)

	// Optional: provide a persistent key so the server can allowlist the client.
	// c.Key = myNodePrivateKey // (type key.NodePrivate)

	// Optional: set a logger if you want debug output.
	c.Logf = log.Printf

	// 2. Verify connectivity (optional but useful for diagnostics).
	ping, err := c.Ping(context.Background())
	if err != nil {
		log.Fatalf("ping failed: %v", err)
	}
	fmt.Printf("ping latency: %v\n", ping.Latency)

	// 3. Open a TCP connection to a service running on the server.
	//    Here we connect to port 22 (SSH) as an example.
	conn, err := c.DialTCPPort(context.Background(), 22)
	if err != nil {
		log.Fatalf("dial failed: %v", err)
	}
	defer conn.Close()

	// 4. Use the connection like any net.Conn.
	fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
	buf := make([]byte, 1024)
	n, _ := conn.Read(buf)
	fmt.Printf("server response (%d bytes): %s\n", n, buf[:n])
}

```

For connections to arbitrary addresses through the server (exit node behavior), use `DialTCP` instead of `DialTCPPort`:

```go
// Connect to example.com:80 via the Tailcat tunnel.
addr := netip.AddrPortFrom(netip.MustParseAddr("93.184.216.34"), 80)
conn, err := c.DialTCP(context.Background(), addr)

```

## Key Source Files

These files contain the implementation details referenced above:

- **[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)** — Core library containing `ConnBlob` definitions, `Client` and `Server` structs, and the networking logic including `NewClient`, `ensureStarted`, and `Dial`.
- **[`wire.go`](https://github.com/tailscale/tailcat/blob/main/wire.go)** — CBOR wire format implementation for encoding/decoding `ConnInfo` and `ConnBlob` tokens.
- **[`pickregion.go`](https://github.com/tailscale/tailcat/blob/main/pickregion.go)** — DERP region auto-selection logic used when `RegionID` is `-1`.
- **[`cmd/tailcat/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go)** — CLI implementation demonstrating real-world usage of the `Client` type.

## Summary

- Import `github.com/tailscale/tailcat` and initialize a client using `NewClient` with a server-generated `ConnBlob` token.
- The client defers all network operations until first use via the lazy `ensureStarted` mechanism (line 89).
- WireGuard peer registration occurs through the "meow" handshake protocol implemented in the `up` method (line 26).
- Use `DialTCPPort` (line 48) for server-local services or `DialTCP` for exit-node routing through the established WireGuard tunnel.

## Frequently Asked Questions

### What is a ConnBlob and how do I obtain one?

A **ConnBlob** is a URL-safe string encoding the server’s WireGuard public key, disco key, and DERP region metadata. You obtain this token from a running Tailcat server instance; the client then uses `ParseConnBlob` to extract connection parameters without requiring DNS lookups or control plane queries.

### When does the Tailcat client actually initiate network connections?

According to [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), the client performs **zero network operations** during initialization. The `ensureStarted` method (line 89) executes only when you call `Dial`, `DialTCPPort`, or `Ping`, creating the userspace WireGuard engine and netstack on demand.

### How can I configure persistent client identity?

Set the `Key` field on the `Client` struct (lines 90-108) to a `key.NodePrivate` value before initiating connections. This allows the server to implement allowlist-based authentication, recognizing your client across restarts via its stable public key fingerprint.

### Can I customize DERP relay selection?

Yes. Provide a custom `DERPMapURL` in the `Client` struct to override the default relay map fetched during `ensureStarted`. You can also implement custom selection logic in [`pickregion.go`](https://github.com/tailscale/tailcat/blob/main/pickregion.go) or specify a concrete `RegionID` in the `ConnBlob` to bypass auto-selection.