Tailcat (GitHub Repo)
Tailscale open-sourced Tailcat, a tool that uses their WireGuard-based data plane for peer-to-peer tunnels without requiring a control plane or user accounts.
Deep dive
- Utilizes Tailscale's WireGuard implementation for encryption and 'Magicsock' for NAT traversal.
- Does not require a Tailscale account, kernel TUN/TAP device, or administrative privileges.
- Supports ephemeral connections by default with an option for saved keys for stable addresses.
- Leverages DERP (Designated Encrypted Relay for Packets) for NAT hole-punching and fallback relaying.
- Provides a native Go library for embedding tunneling capabilities directly into applications.
- Can be used to expose local TCP ports or run auth-free SSH sessions over an encrypted tunnel.
Decoder
- Magicsock: Tailscale's networking layer that multiplexes traffic across direct UDP connections and DERP relays.
- DERP: Tailscale's relay protocol used for establishing initial connections and fallback relaying when NAT traversal is blocked.
- NAT Traversal: The process of establishing direct peer-to-peer network connections between devices behind NATs or firewalls.
Original article
Tailcat
"Tailscale without Tailscale, by Tailscale"
Tailcat is a remix of Tailscale open source pieces to act like netcat, but over Tailscale's data plane, without Tailscale's control plane. Tailscale's data plane (magicsock, internally) gives you point-to-point WireGuard®-encrypted tunnels between two machines with DERP as the NAT-hole-punching communication side channel and the ultimate relay-of-last-resort if NAT traversal fails. Instead of using the Tailscale control plane, all tailcat connection metadata is exchanged out of band, however you want.
The tailcat CLI (in cmd/tailcat) is built on the tailcat Go library (importable as github.com/tailscale/tailcat).
Whether you use tailcat as a CLI tool or library, one side runs a tailcat server (listener) and gets back a short connection token. The other side passes that token to tailcat's client side to connect. All traffic between the two is encrypted end-to-end with WireGuard. The initial connection bootstraps through a DERP server, and then magicsock performs NAT traversal to upgrade to a direct peer-to-peer UDP connection when possible (usually!).
You don't need a Tailscale account, root/admin access on the machine (it doesn't alter your machine's routing tables, DNS, etc.). It's just a userspace library and CLI tool.
And it's all open source.
You can use our free rate-limited DERP relays (the default DERP map is https://tailcat.dev/derpmap.json) or you can run your own.
There's also an experimental in-browser web demo (tailcat compiled to WebAssembly) at https://tailscale.github.io/tailcat/ that can send and receive files or text, interoperating with the CLI. Browser traffic is relayed over DERP only, with no direct connections until WebRTC support.
Install
$ go install github.com/tailscale/tailcat/cmd/tailcat@latest
Or with Nix flakes, run it directly or install it:
$ nix run github:tailscale/tailcat
$ nix profile install github:tailscale/tailcat
Usage
Pipe stdin/stdout between two machines
Server starts, printing out its ephemeral address:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
(hangs, waiting...)
And then the client can:
$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
$
Then the server unblocks:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
hello
$
Expose local ports through the tunnel
Or you can serve a local TCP port, forwarded to localhost:
$ tailcat --serve=8080,8443 # or --serve=all
# 🐈 Server listening with new address: tcXXXXXXXXX
And then the client:
$ tailcat tcXXXXXXXXX 8080
GET / HTTP/1.1
Host: foo
HTTP/1.1 200 OK
....
Auth-free SSH server
On Linux and macOS, you can run an SSH server too with no auth. (If you want auth, you can just tailcat --serve=22 and proxy to your system SSH server)
$ tailcat --serve=no-auth-ssh
# 🐈 Server listening with new address: tcXXXXXXXXX
And on the client side:
$ tailcat ssh tcXXXXXXXXX
$ tailcat ssh tcXXXXXXXXX ls -la
Misc commands
Ping to test connectivity; each pong reports whether it arrived via a DERP relay or a direct path. --until-direct keeps pinging (up to --timeout, default 10s) until a direct path works, exiting non-zero if one doesn't:
$ tailcat ping --until-direct <token>
pong in 42.1ms via DERP(sfo)
pong in 1.2ms via 203.0.113.7:41641
Run a command through a SOCKS5 proxy routed over the tunnel:
$ tailcat socks <token> curl http://server.tailcat:8081/
Tokens also work directly as URL hostnames: the SOCKS proxy recognizes and dials them, so the token argument is optional. (Tokens are case-sensitive; this works with curl and most CLI tools, but not with browsers, which lowercase hostnames.)
$ tailcat socks curl http://<token>:8081/
Act as an exit node so the client can reach the server's network:
$ tailcat --serve=exit-node
Parse a connection token and print its contents (the server's WireGuard public key and DERP info) as JSON, without connecting to anything:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"RegionID": 302
}
Resolve a short token (which references a DERP region by ID, requiring clients to fetch the DERP map) into a longer self-contained one with the DERP server info embedded, letting clients connect more quickly:
$ tailcat resolve tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA
Parsing that resolved token shows the embedded DERP info:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"Region": [
{
"Nodes": [
{
"HostName": "tc302a.ipn.dev",
"IPv4": "208.111.39.38",
"IPv6": "2607:f740:0:3f::720"
}
]
}
]
}
A server can print the long self-contained form directly with the --full-address flag.
Key Management
A server's address (connection token) is derived from its WireGuard key, so the key you use determines who can reach you:
- Ephemeral keys (the default): each server run generates a fresh key in memory and prints an address nobody has ever seen. When the process exits, the key is discarded and the address is dead forever. This is the safe default: sharing that address only ever refers to that one run.
- Saved keys:
tailcat genkeygenerates a key saved to disk so the address stays stable across restarts. The flip side: anyone you've ever shared that address with can connect to any future server using that key, unless you restrict clients with--allow(seetailcat genkey --client).
The CLI says at startup which kind it's using, so you know whether you're starting a fresh single-use server or re-listening on an address you may have shared in the past.
$ tailcat genkey --region=nyc
# prints the token; key saved to ~/.config/tailcat/keys/default.private.json
# later; the key named "default" is used automatically once it exists:
$ tailcat --serve=8080
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
# ... unless you force a one-off ephemeral key:
$ tailcat --serve=8080 --key=new
# 🐈 Server listening with new address: tcXXXXXXXXX
That is, default is a magic key name: once it exists, plain tailcat silently uses it instead of generating an ephemeral key, and the startup line above is what tells you which happened. Use --key=new to get an ephemeral key anyway, --key=<name> to use a different saved key, or tailcat genkey --delete --key=default to remove the saved default key. tailcat genkey --list lists your saved keys.
Tokens can also be published as DNS TXT records and looked up by name; a DNS name works anywhere the CLI takes a token:
# If example.com has a TXT record "tailcat=tc..."
$ tailcat example.com 8080
$ tailcat ssh example.com
$ tailcat ping example.com
Examples
Protected SSH server over DNS
Who needs port forwarding or port knocking? This runs an SSH server reachable from anywhere by name, with no open inbound ports on the server, where WireGuard authenticates the client before the SSH server ever sees a packet.
On the client machine, generate a client identity keypair. It prints the public key, which is all the server needs to know:
client$ tailcat genkey --client
# wrote file to ~/.config/tailcat/keys/client-default.private.json
nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16
On the server, generate a server keypair pinned to its nearest DERP region (see why below), then serve SSH to only that client:
server$ tailcat genkey --fixed-region
# wrote file to ~/.config/tailcat/keys/default.private.json
tcXXXXXXXXX
server$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
Publish the token in DNS as a TXT record:
my-server.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX"
And then the client side is just:
client$ tailcat ssh my-server.example.com
Client modes automatically use the saved client-default key when it exists, so no extra flags are needed to present the allowed identity. Anyone else's handshake is silently ignored: they can't reach the SSH server, or even learn that one is running.
Bring your own DERP relay
Nothing requires Tailscale's relays: run your own DERP server (it needs a hostname with a TLS certificate, which derper can get itself via Let's Encrypt), then generate a server key that uses it by passing its hostname (or several, comma-separated) as the region:
server$ tailcat genkey --region=derp.example.com
tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
server$ tailcat --serve=22
Go library
A minimal server that answers any TCP port through the tunnel and prints its token.
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)
}
fmt.Println(s.ConnBlob())
select {}
}
How it works
Connection tokens
A Tailcat server is identified by a connection token (called a ConnBlob internally). It looks like tcXYZ... and is a "tc" prefix followed by base64-encoded CBOR containing the server's WireGuard public key and DERP info.
Network stack
- WireGuard -- a userspace WireGuard implementation for encrypting all tunnel traffic.
- magicsock -- Tailscale's transport layer that multiplexes traffic over direct UDP and DERP relays.
- Netstack (gVisor) -- a userspace TCP/IP stack that terminates TCP connections inside the process.
- DERP relay -- Tailscale's encrypted relay protocol, used as a rendezvous channel and as a fallback data path when direct connectivity isn't possible.
Connection flow
- Server starts. It generates (or loads) a WireGuard keypair, connects to a DERP relay, and prints its connection token to stderr.
- Client parses the token to learn the server's public key and DERP region.
- Discovery handshake. The client sends a "Meow" ping message to the server through the DERP relay.
- WireGuard tunnel. With both sides configured as WireGuard peers, the standard WireGuard handshake proceeds.
- NAT traversal. In parallel, each side advertises its UDP endpoints to the other in disco messages over DERP, attempting UDP hole-punching.
- Data transfer. The client dials a TCP port on the server through the tunnel.
Stability
Tailcat is free to use, but it comes with no API or CLI stability promises: the Go API, the CLI flags and output, and the wire format may all change. The public rate-limited Tailcat DERP relays have no uptime SLAs or throughput targets.
History
Tailcat began life in September 2023. It was open sourced August 2026 at the TailscaleUp conference.
Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi