The Problem
Running ad‑hoc, encrypted point‑to‑point streams normally requires a VPN control plane (e.g., Tailscale) or root privileges to manipulate routing. When only a lightweight “netcat‑like” tunnel is needed, provisioning a full control plane is overkill and can be a security policy obstacle.
What This Does
tailcat re‑uses Tailscale’s magicsock data‑plane (WireGuard‑encrypted UDP + DERP fallback) without any Tailscale control service. The CLI (cmd/tailcat/tailcat.go) starts a listener that prints a short, base‑64 token. The peer supplies that token to the client side (cmd/tailcat/tailcat.go → tailcat.Dial) and the two processes exchange traffic over the magicsock tunnel.
- Library entry points –
tailcat.goimplements the coreListenerandClienttypes;wire.gocreates the underlying magicsock connection and encodes the token. - Web demo –
cmd/tailcat-web/tailcat-web.gobuilds a WASM bundle (web/main_js.go) and serves a single‑page app (web/index.html,web/app.js). The demo uses the same library, but all traffic stays on DERP because browsers cannot open UDP sockets directly yet.
The repository also ships a minimal SSH server (cmd/tailcat/ssh.go) that runs completely in userspace, and a “serve” mode that forwards arbitrary local TCP ports (--serve=8080,8443).
How It Is Wired
- CLI start –
go run cmd/tailcat/tailcat.go(or the installed binary) parses flags intailcat.go. - Listener path –
tailcat.NewListener(intailcat.go) creates awire.Connviawire.New(inwire.go).wire.Newbuilds amagicsock.Conn(imported from Tailscale) and registers the connection with the DERP map (pickregion.goselects a bootstrap region). - Token generation –
wire.Conn.Token()returns a short string that encodes the magicsock public key and DERP region; the token is printed by the CLI. - Client path – The peer calls
tailcat.Dial(token)(also intailcat.go). The token is parsed, a matchingwire.Connis created, andmagicsock.Dialcontacts the selected DERP relay. If NAT traversal succeeds, magicsock upgrades to a direct UDP path; otherwise traffic continues over DERP. - Data forwarding – Once the
wire.Connis established,tailcat.gocopies data between the connection and stdin/stdout (or a local TCP listener when--serveis used). The copy loop is a singleio.Copyper direction, so the blast radius is limited to thewire.Connimplementation. - SSH mode –
cmd/tailcat/ssh.gobuilds on the same listener/client flow but spawns an in‑process SSH server (ssh_stub.go/tailcat_ssh.go) that reads/writes from thewire.Conn. No external sshd is required. - Web demo –
cmd/tailcat-web/tailcat-web.goruns an HTTP server that serves the static files inweb/. The WASM module (web/main_js.go→ compiled bygo build -o webdemo/main.wasm) links the same library, callingtailcat.Dialfrom JavaScript viasyscall/js. All traffic goes through DERP because the browser cannot use magicsock’s UDP path.
Responsibility map
| File / Dir | Primary Role |
|---|---|
cmd/tailcat/tailcat.go | CLI flag handling, entry point for server/client |
tailcat.go | Public Listener, Dial, token encoding/decoding |
wire.go | magicsock connection wrapper, DERP bootstrap, token I/O |
pickregion.go / pickregion_js.go | Choose DERP region (CLI vs. WASM) |
cmd/tailcat/ssh.go + ssh_stub.go | In‑process SSH server implementation |
cmd/tailcat-web/*.go | Build and serve WASM demo |
web/* | Static HTML/JS for the demo |
internal/wasmbuild/wasmbuild.go | Helper for go:embed of the WASM binary |
The only hub in the call graph is wire.New, which is invoked by every entry point that needs a network tunnel. No circular dependencies are present; the library is deliberately flat to keep changes isolated.
How To Use It
# Clone the repo
git clone https://github.com/moses-y/tailcat
cd tailcat
# Build the CLI (requires Go 1.22+)
go install ./cmd/tailcat
# Simple pipe between two machines
# Machine A (server)
tailcat
# → prints token, e.g. tcomFwW...
# Machine B (client)
echo hello | tailcat tcomFwW...
# Server now prints "hello"
Port forwarding:
# Server: expose local 8080
tailcat --serve=8080
# Client: connect to remote token on port 8080
tailcat <token> 8080
Auth‑free SSH:
# Server
tailcat --serve=no-auth-ssh
# Client
tailcat ssh <token>
tailcat ssh <token> ls -la
Web demo (no build needed; hosted at https://tailscale.github.io/tailcat/):
# Run locally for development
go run ./cmd/tailcat-web
# Open http://localhost:8080 in a browser
All commands are documented verbatim in README.md; no extra environment variables are required beyond optional DERP_MAP_URL to point at a custom DERP map.
Real‑World Use
A security‑conscious ops team can replace temporary ssh -L tunnels with tailcat. A CI runner on an isolated subnet runs tailcat --serve=22 to expose its SSH daemon to a remote build orchestrator without opening firewall ports or installing a VPN client. The orchestrator connects via the token, establishing a WireGuard‑encrypted channel that falls back to DERP if direct UDP fails.
Code Health & Issues
Static inspection finds:
- Low – No license file –
LICENSEexists, but the repository is a fork; ensure the upstream license header matches the fork’s intent. - Low – Limited CI coverage – GitHub Actions run
go test ./...but do not lint or enforce code coverage thresholds. - Low – Single DERP map hard‑coded – Default DERP map lives at
https://tailcat.dev/derpmap.json; custom maps require manual flag changes, no config file is provided.
No structural red flags (unused files, circular imports, missing tests) are evident from the directory layout.
The Bottom Line
tailcat delivers a minimal, user‑space tunnel that leverages Tailscale’s proven data‑plane without any control‑plane dependencies. It is well‑structured, has a clear library/CLI split, and includes end‑to‑end tests. The main limitation is reliance on DERP for browser use and the absence of built‑in monitoring or policy enforcement, so it fits best for ad‑hoc, low‑risk data transfer or as a building block for internal tooling.