CLI tool
tailscale/tailcat avatar
tailscale/tailcat

tailcat: netcat over WireGuard, and you are the control plane

like netcat, but over Tailscale's data plane, without Tailscale's control plane

9,788 stars452 forksGoBSD-3-Clause

At a glance

What is it?
tailcat borrows Tailscale's data plane, magicsock and WireGuard, and throws away the part that needs an account: the control server. Two machines run a listener and a client, one short address beginning with tc is copied between them by hand, and the traffic is encrypted end to end. It is a userspace tool, so nothing on either machine's routing or DNS changes.
Who is it for?
tailcat suits someone who wants to move a shell, a port or a file between two machines over an encrypted tunnel without provisioning accounts, installing a VPN client, or touching the host's routing table, and who can pass one short address to the other side by whatever channel they already trust.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • tailscale/tailcat9,788 ★ · +2,140
Star history
Repository2026-09-222026-11-14
tailscale/tailcat7.6k9.8k

2,140 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The data plane is reused, the control plane is replaced by a string you copy

The project's tagline is "Tailscale without Tailscale, by Tailscale", and the mechanism is a split. 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 as the relay of last resort if NAT traversal fails.

What tailcat removes is the control plane. Normally a coordination server tells your node who its peers are and what their keys are. Here, all connection metadata is exchanged out of band, however you want: over chat, over a ticket, shouted down a corridor.

The shape of a session is two commands. One side runs a tailcat server, a listener, and gets back a short tailcat address. The other side passes that address to the client. Between them, traffic 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, which the README annotates with "usually!".

There is no Tailscale account involved and no root or admin access, because it does not alter your machine's routing tables or DNS. It is a userspace library and CLI tool.

The whole handshake is one printed address

The first example in the README is a pipe between two machines, and it is worth reading as the minimum viable use.

The server prints an ephemeral address and then waits:

sh
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
(hangs, waiting...)

The client takes that address and pipes something in:

sh
$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu

and the server unblocks and prints what it received.

Two details are structural rather than cosmetic. Addresses begin with `tc`, and the first output line names the bootstrap relay region that was selected, which tells you where the connection starts before any hole punching happens.

Because all connection metadata is exchanged out of band, the channel you use to pass the address matters.

Behind the CLI there is a library, importable as `github.com/tailscale/tailcat`, and the CLI itself lives in `cmd/tailcat`.

serve publishes, forward consumes, and a local port of 0 means any

Ports are handled by two subcommands with opposite directions.

`serve` runs on the machine that has the service. You can list ports or ask for everything:

sh
$ tailcat serve 8080,8443 # or: tailcat serve all

`forward` runs on the machine that wants to reach them, and it takes mappings rather than bare ports, which is what lets a local port differ from the remote one:

sh
$ tailcat serve 8080,3306
$ tailcat forward tcXXXXXXXXX 18080:8080 3306

In that example the server exposes 8080 and 3306, and the client maps its own 18080 to the server's 8080 and passes 3306 through unchanged. A local port of 0 asks the operating system for a free port, and each listener prints its own address once it is listening rather than assuming the mapping succeeded.

A port on the server can also point somewhere else entirely. `tailcat serve 5555:10.2.200.213:5555` proxies to an Android device's adb port on the LAN, which the README notes exposes one port rather than the whole network the way an exit node would. IPv6 targets go in brackets, as in `5555:[fd7a::1]:5555`.

Server ports can be reached through the rest of the server's network with `tailcat serve exit-node` plus a mapping that names each remote address.

Listeners bind to 127.0.0.1 and logs are off unless you ask

Two defaults are stated, and both are the kind that decide whether a tool is safe to leave running.

By default, listeners bind to `127.0.0.1`, and diagnostic logs are suppressed. The escape hatches are `--verbose` before the subcommand to get verbose networking logs, and `--bind=0.0.0.0` for when clients on other machines should be able to connect:

sh
$ tailcat forward --bind=0.0.0.0 tcXXXXXXXXX 18080:8080

The wording around that flag is the guidance: use it only when other machines need to connect. A forwarded port that binds to all interfaces turns a loopback tunnel into a listening service on the network, which is a different risk than the one you started with.

Stopping is Ctrl-C, which matters because these commands block. `tailcat` with no subcommand waits for input, `forward` keeps forwarding connections until interrupted, and `browse` sits there with a browser open.

If you are scripting this, the per-listener address output is the thing to capture, since the OS chose it.

browse is forward with --open-browser, and the 0:80 alias is why it works

The `browse` subcommand exists for the case where the thing behind the tunnel is a web server and you do not want to find a port to paste into a browser.

The server exposes port 80:

sh
$ tailcat serve 80
# 🐈 Server listening with new address: tcXXXXXXXXX

and the client runs:

sh
$ tailcat browse tcXXXXXXXXX

The README describes it as an alias for `tailcat forward --open-browser <tc-addr> 0:80`. That single line explains the behaviour: the local port is 0, so the OS picks a free one, and `--open-browser` opens `http://127.0.0.1:<port>/` once the listener is ready rather than guessing a port. It then blocks, forwarding connections, until you interrupt it.

The flag is not special to `browse`: `--open-browser` works with any single `forward` port mapping.

The dependency list backs this up, with `chromedp` for driving a browser and `toqueteos/webbrowser` for opening one on the machine running the client.

An SSH server that accepts keys from authorized_keys or a GitHub account

tailcat can serve SSH instead of TCP ports, and the interesting part is where the keys come from.

sh
$ tailcat serve --ssh-authorized-keys=~/.ssh/authorized_keys ssh
# 🐈 Server listening with new address: tcXXXXXXXXX

The accepted sources are the local `authorized_keys` files, literal OpenSSH public key lines, or GitHub accounts. Multiple sources can be comma-separated.

A `user@github` source fetches `https://github.com/user.keys` once, before the server starts. That detail matters for how you think about rotation: the key is read at startup, not on every connection, so revoking a key on GitHub does not affect a tunnel that is already running.

The SSH server implementation is not new code. The dependency list includes `tailscale/gliderssh`, and the source tree has `tailcat_ssh.go` with platform splits for Unix and Windows plus a stub, and a separate `tailcat_ssh_keys.go` for the key sources.

There are also SFTP and file paths in the tree, `tailcat_sftp.go` and `tailcat_files.go`, with `pkg/sftp` as the dependency.

The dependency on tailscale.com is itself a pre-release

Look at the module requirements and the build is explained.

The module is `github.com/tailscale/tailcat`, the Go version is 1.27.1, and among the direct requirements are `tailscale.com v1.103.0-pre.0...`, a pre-release build of Tailscale itself, alongside `tailscale/wireguard-go` and `tailscale/gliderssh`. So the data plane is vendored by version rather than reimplemented, and the version chosen is a preview.

Other requirements explain the features: `chromedp/cdproto` for the browser demo and `browse`, `creack/pty` and `u-root/u-root`, `peterbourgon/ff/v4` for the CLI flag parsing, `fxamacker/cbor` and `klauspost/compress`, `go4.org/netipx` for address handling, `gvisor.dev/gvisor` for the userspace network stack, and `golang.org/x/crypto`.

Two source files are worth knowing about. `connblob_deprecated.go` says in its name that an older connection mechanism is deprecated. `pickregion.go` has a `pickregion_js.go` sibling, which is how the WebAssembly build selects a bootstrap region differently.

Releases are frequent and pre-1.0: v0.7.0 on 2026-09-20, v0.6.0 on 2026-09-04, v0.5.0 on 2026-09-02, with the last push to main on 2026-10-07. The licence is BSD-3-Clause, and the experimental browser build at tailscale.github.io/tailcat is DERP-only until WebRTC support lands.

Editorial conclusion

tailcat suits someone who wants to move a shell, a port or a file between two machines over an encrypted tunnel without provisioning accounts, installing a VPN client, or touching the host's routing table, and who can pass one short address to the other side by whatever channel they already trust. It does not suit unattended connections, since the address is exchanged by hand, and it does not suit you if you need a direct path, because the README is explicit that NAT traversal usually but not always succeeds and the browser build has no direct connections at all. Verify three things before you rely on it: that the DERP relay you use is one you accept, whether the free rate-limited default or your own, that your client can reach the bootstrap region it selects, and which direction you want the ports to run in, since `serve` and `forward` are not interchangeable.

Frequently asked questions

what is tailcat

A CLI tool and Go library that behaves like netcat over Tailscale's data plane without Tailscale's control plane. It uses magicsock and WireGuard for point-to-point encrypted tunnels with DERP as the relay, one side runs a listener and prints a short address beginning with tc, and the other side passes that address in. The module is importable as github.com/tailscale/tailcat.

how to use tailcat

Run `tailcat` on one machine and it prints an ephemeral address, then pass that address to the other machine, which can pipe data to it as in `echo hello | tailcat <address>`. For ports, `tailcat serve 8080,8443` on the server and `tailcat forward <address> 18080:8080` on the client. `tailcat browse <address>` opens a web server behind the tunnel in a browser, and `tailcat serve --ssh-authorized-keys=~/.ssh/authorized_keys ssh` serves SSH instead of ports.

Does tailcat need a Tailscale account or root access?

Neither. The README states you do not need a Tailscale account or root and admin access on the machine, because it does not alter your machine's routing tables or DNS, and describes it as a userspace library and CLI tool. For relays you can use the free rate-limited DERP relays, with the default map at tailcat.dev/derpmap.json, or run your own derper.

Official sources

  1. License: BSD-3-Clause
  2. Project website
  3. README
  4. Releases
  5. tailscale/tailcat on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tailscale-tailcat.svg)](https://hysenlabs.com/projects/tailscale-tailcat)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/tailscale-tailcat.svg)](https://hysenlabs.com/projects/tailscale-tailcat)

Was this useful?