How to Use the Tailcat Go Library as a Client
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, 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 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 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.
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:
// 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— Core library containingConnBlobdefinitions,ClientandServerstructs, and the networking logic includingNewClient,ensureStarted, andDial.wire.go— CBOR wire format implementation for encoding/decodingConnInfoandConnBlobtokens.pickregion.go— DERP region auto-selection logic used whenRegionIDis-1.cmd/tailcat/tailcat.go— CLI implementation demonstrating real-world usage of theClienttype.
Summary
- Import
github.com/tailscale/tailcatand initialize a client usingNewClientwith a server-generatedConnBlobtoken. - The client defers all network operations until first use via the lazy
ensureStartedmechanism (line 89). - WireGuard peer registration occurs through the "meow" handshake protocol implemented in the
upmethod (line 26). - Use
DialTCPPort(line 48) for server-local services orDialTCPfor 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, 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 or specify a concrete RegionID in the ConnBlob to bypass auto-selection.
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 →