How to Run a Minimal Tailcat Client in Go: Complete Setup Guide
You can run a minimal Tailcat client by passing a connection token to tailcat.NewClient, dialing a TCP port with DialTCPPort, and using the returned net.Conn for encrypted communication.
Tailcat is an open-source networking library from the tailscale/tailcat repository that enables secure TCP connections over WireGuard without manual key management. According to the source code, the library handles NAT traversal, DERP relay selection, and encryption behind a minimal Go API. This guide demonstrates how to build a production-ready client in fewer than 20 lines of code.
How the Tailcat Client Works Internally
Token Parsing and Server Discovery
The client receives a connection token—a CBOR-encoded base64 string containing the server’s WireGuard public key and DERP relay coordinates. The Server type definition at line 277 of tailcat.go handles token generation, while the client decodes this blob via tailcat.ConnBlob to auto-configure its network stack.
Ephemeral WireGuard Tunnels
Upon initialization, the client generates an ephemeral WireGuard keypair and connects through the same DERP relay as the server using magicsock. The peers exchange keys, complete a WireGuard handshake, and establish a userspace encrypted tunnel. This low-level plumbing is implemented in wire.go and disco.go, which manage UDP hole-punching and automatic NAT traversal.
Lazy Dialing Architecture
Tailcat conserves resources through lazy dialing. The tunnel remains dormant and consumes zero bandwidth until you explicitly call DialTCPPort, at which point the client negotiates the WireGuard session and establishes the TCP stream.
Building a Minimal Tailcat Client in Go
Import the library and instantiate the client with tailcat.NewClient, passing the server token as a ConnBlob:
package main
import (
"context"
"io"
"log"
"os"
"github.com/tailscale/tailcat"
)
func main() {
// The server token is passed as the first CLI argument.
// Example token: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
cl := tailcat.NewClient(tailcat.ConnBlob(os.Args[1]))
defer cl.Close()
// Connect to TCP port 80 on the server (you can change the port).
c, err := cl.DialTCPPort(context.Background(), 80)
if err != nil {
log.Fatal(err)
}
// Copy whatever the server sends to stdout.
io.Copy(os.Stdout, c)
}
Build and execute the client with a valid server token:
go build -o client .
./client tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
The DialTCPPort method returns a standard net.Conn, allowing you to use familiar Go networking patterns while Tailcat transparently handles encryption, routing, and NAT traversal.
Creating a Minimal Server for Testing
To generate a connection token for your client, run a minimal server using the tailcat.Server type. Define an OnTCP handler that responds to incoming connections:
package main
import (
"fmt"
"log"
"net"
"github.com/tailscale/tailcat"
)
func main() {
s := &tailcat.Server{
OnTCP: func(port uint16) func(net.Conn) {
return func(c net.Conn) {
fmt.Fprintf(c, "hello from port %v\n", port)
c.Close()
}
},
}
if err := s.Start(); err != nil {
log.Fatal(err)
}
// Print the token that the client must use.
fmt.Println(s.ConnBlob())
select {} // keep the server running
}
Execute go run server.go to output a fresh token. Copy the printed blob and provide it as the command-line argument to your minimal client.
Key Source Files and Architecture
Understanding the repository structure helps when debugging or extending functionality:
tailcat.go– Core API containing theServerandClienttypes, plus theNewClientconstructor and token handling logic at line 277.wire.go– Low-level magicsock and WireGuard integration for managing encrypted tunnels.disco.go– Discovery protocol implementation for UDP hole-punching and DERP fallback.cmd/tailcat/tailcat.go– Reference CLI implementation demonstrating production-grade flag parsing and library integration.
Summary
- Obtain a connection token from a running
tailcat.ServerusingConnBlob(). - Initialize the client with
tailcat.NewClient(), passing the token wrapped in atailcat.ConnBlob. - Establish connections lazily via
DialTCPPort()to create the WireGuard tunnel only when needed. - Use standard
net.Conninterfaces for I/O operations; Tailcat handles encryption and routing transparently. - Leverage automatic NAT traversal through the magicsock implementation in
wire.goanddisco.go.
Frequently Asked Questions
What data format does the Tailcat connection token use?
The token is a base64-encoded CBOR blob containing the server’s WireGuard public key and DERP relay coordinates. You pass this string directly to tailcat.NewClient() as a ConnBlob; the library handles decoding and network configuration automatically without manual key distribution.
Do I need to configure WireGuard keys manually?
No. The client generates ephemeral WireGuard keypairs automatically during initialization. The server’s public key embedded in the connection token enables the handshake, eliminating the need for static configuration files or manual key exchange as implemented in tailcat.go.
How does Tailcat handle firewall or NAT blocking?
If direct UDP connectivity fails, the client automatically falls back to DERP (Designated Encrypted Relay for Packets) relays. This failover logic in disco.go operates transparently, ensuring connectivity even through strict NATs or firewalls without changing the application-level DialTCPPort API.
Can I use Tailcat for UDP traffic?
While the underlying magicsock transport in wire.go supports UDP, the high-level minimal client API currently exposes TCP streaming through DialTCPPort, which returns a net.Conn suitable for standard TCP read/write operations.
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 →