tailcat

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: BSD-3-Clause Imports: 78 Imported by: 0

Image README

Tailcat

"Tailscale without Tailscale, by Tailscale"

Tailcat

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 tailcat address. The other side passes that tailcat address 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 (see below), 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 (#4).

Install

Prebuilt binaries are on the Releases page: static Linux binaries (tar.gz) plus Debian (.deb) and RPM (.rpm) packages for amd64, arm64, and armv7, and Windows binaries (zip) for amd64 and arm64.

There's also a container image:

$ docker pull ghcr.io/tailscale/tailcat:v0.1.0  # or :latest
$ docker run --rm -it ghcr.io/tailscale/tailcat:latest

For macOS, install with Homebrew:

$ brew install tailcat

Or build from source with a Go toolchain:

$ go install github.com/tailscale/tailcat/cmd/tailcat@latest

Or with Nix, from nixpkgs:

$ nix profile install nixpkgs#tailcat
$ nix-env -iA nixpkgs.tailcat  # or with classic nix-env

Or with Nix flakes from this repo, run it directly or install it:

$ nix run github:tailscale/tailcat
$ nix profile install github:tailscale/tailcat

Or from archlinux AUR:

tailcat on AUR tailcat-bin on AUR

# Build release package from source
yay -S tailcat

# OR install the binary release
yay -S tailcat-bin

Or from conda-forge:

tailcat on conda-forge tailcat on conda-forge

pixi global install tailcat
# run without installation
pixi exec tailcat
Packaging from source

The official binaries are built with a list of build tags that omits unused Tailscale features, making them about 16% smaller. The recommended tag list is checked in as build-tags.txt (and kept accurate by a CI test), so packagers (Homebrew, AUR, NixOS, etc.) can build the same way:

$ go build -tags "$(cat build-tags.txt)" -ldflags "-s -w" ./cmd/tailcat

See build-tags.md for the details.

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: tailcat 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
....
Forward local ports to a tailcat server

To make ports served by a tailcat server available as ordinary local TCP ports (for browsers, database clients, or other tools that do not support SOCKS or stdio), run forward with the server's tailcat address:

$ tailcat serve 8080,3306
# 🐈 Server listening with new address: tcXXXXXXXXX

$ tailcat forward tcXXXXXXXXX 18080:8080 3306

A local port of 0 asks the operating system for a free port; each listener prints its address once it's listening.

To forward local ports to assets on the network reachable by an exit-node server, run the server in exit-node mode and specify each remote IP address and port in the mapping:

$ tailcat serve exit-node
# 🐈 Server listening with new address: tcXXXXXXXXX

$ tailcat forward tcXXXXXXXXX \
    3001:172.23.52.30:3001 \
    17170:172.23.52.31:17170

This forwards 127.0.0.1:3001 to 172.23.52.30:3001 and 127.0.0.1:17170 to 172.23.52.31:17170 through the exit-node server.

By default, listeners bind to 127.0.0.1 and diagnostic logs are suppressed. Pass --verbose before the subcommand to enable verbose networking logs. Use --bind=0.0.0.0 only when clients on other machines should be able to connect:

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

Press Ctrl-C to stop forwarding.

Public-key-authenticated SSH server

Run an SSH server that accepts keys from local authorized_keys files, literal OpenSSH public key lines, or GitHub accounts:

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

Multiple sources can be comma-separated. A user@github source fetches https://github.com/user.keys once, before the server starts:

$ tailcat serve --ssh-authorized-keys=bradfitz@github,./contractor.pub ssh

Every source must exist, fetch successfully, and contain valid public key lines or startup fails. Authorized-key options such as command= and from= are rejected because the built-in server does not implement them. Running tailcat serve ssh without --ssh-authorized-keys also fails; use the explicit no-auth-ssh service when the tunnel identity alone is sufficient.

Auth-free SSH server

On Linux, macOS, and Windows, you can also explicitly run the SSH server with no client authentication. The encrypted tunnel provides the client identity.

$ tailcat serve no-auth-ssh
# 🐈 Server listening with new address: tcXXXXXXXXX

And on the client side:

$ tailcat ssh tcXXXXXXXXX
$ tailcat ssh tcXXXXXXXXX ls -la
Send and receive files

To receive files, run a drop box and share the printed tailcat address:

$ tailcat recv ~/inbox
# 🐈 Server listening with new address: tcXXXXXXXXX

The sender then runs:

$ tailcat cp report.pdf tcXXXXXXXXX:

tailcat cp runs the system scp with the connection routed through tailcat, so you get its usual progress display, and -r for directory trees. The drop box is write-only: senders can't list the directory, read anything back, or touch existing files.

To offer files instead, serve a directory read-only (the default) or read-write:

$ tailcat serve files                  # current directory, read-only
$ tailcat serve --files=/pub:rw files  # a given directory, read-write
$ tailcat ls -l tcXXXXXXXXX
$ tailcat cp tcXXXXXXXXX:report.pdf .

tailcat ls speaks SFTP natively, so it works even without OpenSSH installed.

The server confines all paths to the served directory (via Go's os.Root), so neither .. nor symlinks escape it. The file service speaks SFTP, so the stock sftp and scp clients also work against it, given a ProxyCommand that pipes through tailcat (the same trick tailcat cp and tailcat ssh use). Both ssh and no-auth-ssh servers serve SFTP too, with the same access as the shell.

Transfers are not compressed: the SFTP protocol has no compression of its own, and the SSH transport here doesn't either (Go's SSH stack omits it; transport compression has a history of security problems, and TLS dropped it too). Compress files before sending if it matters.

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 <tc-addr>
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 <tc-addr> curl http://server.tailcat:8081/

Tailcat addresses also work directly as URL hostnames: the SOCKS proxy recognizes and dials them, so the tailcat address argument is optional. (Tailcat addresses are case-sensitive; this works with curl and most CLI tools, but not with browsers, which lowercase hostnames.)

$ tailcat socks curl http://<tc-addr>:8081/

Act as an exit node so the client can reach the server's network:

$ tailcat serve exit-node

Parse a tailcat address 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 tailcat address (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 tailcat address 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 tailcat serve --full-address flag.

Key Management

A server's tailcat address contains its WireGuard public key and an independent WireGuard pre-shared key, so the saved key material 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 genkey generates 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 tailcat serve --allow (see tailcat 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.

WireGuard pre-shared keys are enabled by default and strongly recommended. For compatibility with tailcat clients v0.5.0 and earlier, --psk=false on serve or genkey produces shorter addresses, but removes post-quantum protection and protection from public DERP operators that observe the peers' public keys.

$ tailcat genkey --key=default --region=nyc
# prints the tailcat address; 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 --key=new 8080
# 🐈 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.

Tailcat addresses can also be published as DNS TXT records and looked up by name; a DNS name works anywhere the CLI takes a tailcat address:

# 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 --key=client-default
# 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 --key=default --fixed-region
# wrote file to ~/.config/tailcat/keys/default.private.json
tcXXXXXXXXX

server$ tailcat serve --allow=nodekey:cfb6bf...ddfd16 22
# 🐈 Server listening with saved key "default": tcXXXXXXXXX

Publish the tailcat address 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.

Why --fixed-region: it discovers the nearest DERP region once, at genkey time, and bakes its ID into both the printed tailcat address and the saved key file, so server restarts bind to the same region (keeping the published tailcat address valid) without re-probing. Otherwise genkey defaults to --region=auto, which instead bakes in "pick at startup": fine for one-off use, but a tailcat address published in DNS should name a fixed region so clients and future server restarts all rendezvous in the same place. (--region=<name> pins an explicit one instead; --region=list shows the choices.)

TODO: make the client more robust here if the DERP map changes over time: https://github.com/tailscale/tailcat/issues/7

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 --key=default --region=derp.example.com
tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ

server$ tailcat serve 22

The tailcat address embeds your relay's hostname:

$ tailcat parse tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
{
    "ServerPublic": "nodekey:8022c28ea8f52ec7a0a51b644ce00fef3aae150731a01c61a3abd3ac26e14a49",
    "Region": [
        {
            "Nodes": [
                {
                    "HostName": "derp.example.com"
                }
            ]
        }
    ]
}

so clients need no extra flags and never contact Tailscale's DERP map server or relays, and the only rate limits are yours. Alternatively, if you run a whole fleet of relays, serve your own DERP map JSON and point both sides at it with --derpmap-url.

Go library

A minimal server that answers any TCP port through the tunnel and prints its tailcat address. The zero value Server picks defaults for anything unset: a fresh ephemeral key, the nearest region of the default DERP map, and log.Printf logging (set Logf to logger.Discard for quiet):

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.TailcatAddr())
	select {}
}

And a minimal client that dials it, given that tailcat address as its argument. Like Server, the Client zero value works with just its Server field set to a tailcat address (tailcat.NewClient is shorthand for exactly that), and the tunnel is established lazily by the first dial:

package main

import (
	"context"
	"io"
	"log"
	"os"

	"github.com/tailscale/tailcat"
)

func main() {
	cl := tailcat.NewClient(tailcat.Addr(os.Args[1]))
	defer cl.Close()
	c, err := cl.DialTCPPort(context.Background(), 80)
	if err != nil {
		log.Fatal(err)
	}
	io.Copy(os.Stdout, c)
}
$ ./client tcomFwWCAWf933BLELdzd3RkHiOufJ...
hello from port 80

UDP uses a connected packet connection for each client flow, preserving datagram boundaries and both endpoint addresses:

s.OnUDP = func(port uint16) func(tailcat.ConnPacketConn) {
	if port != 53 {
		return nil
	}
	return func(c tailcat.ConnPacketConn) {
		defer c.Close()
		buf := make([]byte, tailcat.MaxUDPPayload)
		for {
			n, err := c.Read(buf)
			if err != nil {
				return
			}
			c.Write(buf[:n])
		}
	}
}

pc, err := cl.DialUDPPort(context.Background(), 53)

ConnPacketConn implements both net.Conn and net.PacketConn. Keep payloads at or below tailcat.MaxUDPPayload (1232 bytes) to fit the IPv6 tunnel MTU without fragmentation. Use OnUDPForward and DialUDP for exit-node traffic; ProxyPacketConns provides datagram-safe bidirectional forwarding. Inactive server-side UDP flows close after tailcat.DefaultUDPIdleTimeout (two minutes); set Server.UDPIdleTimeout to change the timeout.

How it works

Tailcat addresses

A Tailcat server is identified by a tailcat address, represented by the Go type tailcat.Addr. It looks like tcXYZ... and is a "tc" prefix followed by base64-encoded CBOR containing:

  • The server's WireGuard public key (Curve25519, 32 bytes)
  • A separate path-discovery public key (Curve25519, 32 bytes)
  • By default, an independent WireGuard pre-shared key (256 random bits), which prevents a DERP operator that observes the peers' public keys from joining the tunnel and provides post-quantum protection against recorded traffic
  • DERP info. Either:
    1. a small integer referencing one of the default Tailscale-run tailcat servers, or
    2. full DERP server metadata, to either use a custom DERP server, or to avoid the client needing a potential round-trip to fetch the latest DERP map (the tailcat serve --full-address flag and the tailcat resolve subcommand produce this form)

A typical tailcat address with just an integer region ID is around 140 bytes. With embedded DERP node details it's longer but self-contained.

The default address is a secret bearer capability because it contains the pre-shared key. Share it only with clients that should be able to connect.

Network stack

Tailcat reuses Tailscale's client networking components but without the control plane.

  • WireGuard -- a userspace WireGuard implementation for encrypting all tunnel traffic. It doesn't use a kernel TUN/TAP device (nor does it configure any networking routes or DNS settings), so root isn't required.
  • magicsock -- Tailscale's transport layer that multiplexes traffic over direct UDP and DERP relays. It handles STUN-based endpoint discovery and UDP hole-punching for NAT traversal.
  • Netstack (gVisor) -- a userspace TCP/IP stack that terminates TCP connections inside the process. This is what lets Tailcat accept inbound connections and dial outbound ones without any OS network configuration.
  • 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
  1. Server starts. It generates (or loads) a WireGuard keypair and, by default, a pre-shared key, connects to a DERP relay, and prints its tailcat address to stderr. It then waits for clients.

  2. Client parses the tailcat address to learn the server's public key, path-discovery key, optional pre-shared key, and DERP region. It generates its own ephemeral keypair and connects to the same DERP relay. The separate path-discovery key can appear in cleartext direct-path disco frames without revealing the WireGuard public key. The pre-shared key remains the secret connection capability even when a relay operator observes both peers' public keys.

  3. Discovery handshake. The client sends a "Meow" ping message to the server through the DERP relay. This message carries the client's node public key. The server receives it, adds the client to its WireGuard peer list and network map, reconfigures the WireGuard engine, and replies with a "Meowed" acknowledgment.

  4. WireGuard tunnel. With both sides configured as WireGuard peers using the address's pre-shared key when present, the WireGuard handshake proceeds (routed through DERP initially). Once complete, the tunnel is up and encrypted traffic can flow.

  5. NAT traversal. In parallel, each side advertises its UDP endpoints (public IP:port learned via STUN, plus local interface addresses) to the other in disco call-me-maybe messages over DERP, re-advertising whenever they change. Both sides then run Tailscale's disco protocol and attempt UDP hole-punching. If successful, traffic upgrades from the DERP relay to a direct peer-to-peer path. If hole-punching fails, DERP continues as a fallback and the connection still works, just with rate-limited throughput if you're using our public hosted DERP relays.

  6. Data transfer. The client dials a TCP port on the server through the tunnel. gVisor's TCP/IP stack on both sides handles connection setup. On the server, the incoming connection is dispatched to a handler based on the port: forwarding to localhost, piping to stdout, running an SSH session, etc.

Addressing

Each peer currently derives a deterministic IPv6 address from its WireGuard public key, but that's an implementation detail not exposed to end users and might change. (e.g. we might remove those bytes from the IP headers entirely and recover that redundant MTU)

Security

See SECURITY.md for how to report security issues, and for notes on tailcat's current threat model.

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, and we may revoke access to them at any time, for any reason. Everything is provided best effort, without a contractual relationship (e.g. dedicated DERP relays and/or support) saying otherwise.

Contact Sales?

If you don't want to run and support things on your own, or want any help, contact sales and we can exchange money for goods and services.

History

Tailcat began life in September 2023 as "derpcat", written on a long flight while catching up on bad movies: the first sketch was commit 9e4d925cc ("cmd/dc: start of derpcat tool"), and it first worked in commit 911915fbb ("derpcat: it's alive!", whose commit message notes "UA 605 PDX-ORD en route to Ireland. yay not buying the wifi."). Back then it lived inside a fork of the tailscale.com repo and it bitrot several times as the Tailscale internals moved on without it. We've since brought it back to life and refactored it to be a regular Go module client of the tailscale.com repo instead of a fork of it.

It was open sourced August 2026 at the TailscaleUp conference.

Image Documentation

Overview

Package tailcat implements a control-plane-free network pipe built on Tailscale's data plane which provides encryption (WireGuard) and NAT traversal. This is the library behind the "tailcat" CLI command (cmd/tailcat).

A Server listens for incoming clients via a DERP relay. Clients discover the server through a compact Addr (a tailcat address) that encodes the server's WireGuard and path-discovery public keys, optional WireGuard pre-shared key, and DERP region. DERP is used only for the initial bootstrap; once both sides learn each other's endpoints, Tailscale's magicsock layer upgrades to a direct peer-to-peer UDP path whenever possible, just like the normal Tailscale data plane. DERP remains available as a fallback relay if a direct path cannot be established.

Once connected, the two sides exchange arbitrary TCP streams and UDP datagrams over the WireGuard tunnel with no Tailscale account or coordination server required. Optionally, the server can run an SSH server on port 22, either requiring authorized public keys or relying on the tunnel for client identity.

The name "tailcat" is a nod to the classic "netcat" tool, but with Tailscale's WireGuard encryption + NAT traversal.

Using Tailscale's DERP servers is not required; you can run your own DERP server and provide its region information in the tailcat address.

This package has no API stability promises: types, functions, and the wire format may all change. See the Stability section of the README (https://github.com/tailscale/tailcat/#readme) for details, including the terms of Tailscale's public DERP relays.

Index

Constants

View Source
const DefaultDERPMapURL = "https://tailcat.dev/derpmap.json"

DefaultDERPMapURL is the URL of the JSON-encoded tailcfg.DERPMap that ConnInfo.Expand fetches when no alternate DERP map source is specified via options.

View Source
const DefaultUDPIdleTimeout = 2 * time.Minute

DefaultUDPIdleTimeout is the amount of inactivity after which an incoming UDP flow is closed.

View Source
const MaxUDPPayload = 1232

MaxUDPPayload is the largest UDP payload that fits the tunnel's 1280-byte IPv6 MTU without IP fragmentation (1280 minus 40 bytes of IPv6 header and 8 bytes of UDP header). Applications should keep datagrams at or below this size; larger writes are not guaranteed to reach the peer.

Variables

View Source
var ExpandForServer expandForServer

ExpandForServer is an option for ConnInfo.Expand that marks the DERP map fetch as being on behalf of a tailcat server (which will listen on the chosen region) rather than a client. It is sent as a hint header to the DERP map server.

View Source
var README string

README is the tailcat README.md, embedded so the CLI can print it with its readme subcommand. That lets people (and AI agents) with only the binary learn how to use it without web access.

View Source
var Verbose = false

Verbose controls whether extra diagnostic logging is emitted during DERP region auto-detection (netcheck).

Functions

func EncodeMeowPing

func EncodeMeowPing(nodeKey key.NodePublic, discoKey key.DiscoPublic) []byte

EncodeMeowPing encodes a meow ping packet containing the sender's node public key and disco public key.

func EncodeMeowed

func EncodeMeowed() []byte

EncodeMeowed encodes a meowed (acknowledgment) packet.

func FetchDERPMap

func FetchDERPMap(ctx context.Context, opts ...any) (*tailcfg.DERPMap, error)

FetchDERPMap fetches and decodes the JSON DERP map. The opts may contain any of the following types:

func IsMeowPacket

func IsMeowPacket(pkt []byte) bool

IsMeowPacket reports whether pkt starts with the meow magic prefix.

func IsMeowedPacket

func IsMeowedPacket(pkt []byte) bool

IsMeowedPacket reports whether pkt is a meowed (acknowledgment) packet.

func ParseAddrRaw added in v0.5.0

func ParseAddrRaw(addr Addr) (any, error)

ParseAddrRaw decodes an address into its wire form, without restoring the implicit fields that ParseAddr synthesizes (region and node IDs, region codes, node names). The returned value is only meant for JSON display, as by the CLI's "parse" subcommand: its JSON form shows just the fields the encoded address actually carries.

func ParseConnBlobRaw deprecated

func ParseConnBlobRaw(addr ConnBlob) (any, error)

ParseConnBlobRaw decodes an address into its wire form.

Deprecated: use ParseAddrRaw instead.

func ParseMeowPing

func ParseMeowPing(pkt []byte) (nodeKey key.NodePublic, discoKey key.DiscoPublic, ok bool)

ParseMeowPing parses a meow ping packet, returning the sender's node public key and disco public key. The pkt must have already been verified with IsMeowPacket.

func PickBestRegion

func PickBestRegion(ctx context.Context, dm *tailcfg.DERPMap) (regionID tailcfg.DERPRegionID, err error)

PickBestRegion runs a netcheck over the DERP regions in dm and returns the region ID with the lowest latency. It returns 0 (and a nil error) if the netcheck report contained no usable region latencies.

func ProxyConns

func ProxyConns(a, b net.Conn)

ProxyConns copies data between a and b in both directions until both sides have finished, then closes both connections.

When one direction's copy finishes (its source reached EOF), the destination gets a write shutdown via CloseWrite if supported, propagating the TCP half-close instead of tearing down the whole connection. This lets protocols where one side signals end-of-request with a FIN and then reads the response (netcat style) work through the proxy.

func ProxyPacketConns added in v0.6.0

func ProxyPacketConns(a, b ConnPacketConn)

ProxyPacketConns copies whole datagrams between a and b until either socket fails or is closed, then closes both sockets. The maximum-size UDP buffer avoids turning a large datagram into multiple writes or truncating it.

func SupportsSSHServer

func SupportsSSHServer() bool

SupportsSSHServer reports whether the platform supports running the built-in SSH server.

func ValidateSSHAuthorizedKeys added in v0.6.0

func ValidateSSHAuthorizedKeys(texts []string) error

ValidateSSHAuthorizedKeys reports whether texts contains at least one valid SSH public key and consists only of blank lines, comments, and public key lines in OpenSSH authorized_keys format. Authorized-key options are rejected because the built-in server does not implement their restrictions.

Types

type Addr added in v0.5.0

type Addr string

Addr is a compact, URL-safe tailcat address that a server gives to clients so they can connect. It is the "tc"-prefixed base64url encoding of a CBOR-encoded ConnInfo. A typical Addr looks like "tcomFwWC…".

func (Addr) Resolve added in v0.5.0

func (a Addr) Resolve(ctx context.Context, opts ...any) (Addr, error)

Resolve returns a self-contained equivalent of a with the DERP relay's details embedded, so that later use of the address requires no network access to fetch the DERP map. It is to an Addr roughly what a DNS lookup is to a hostname: the resolved form is longer, works offline, and pins the relay details as they were at resolution time. If a already embeds its relay details, it is returned unchanged. The opts are as documented on ConnInfo.Expand.

type Client

type Client struct {
	// Server is the tailcat address identifying the server to connect to.
	// It is required and must be set before the client's first use.
	Server Addr

	// Key is the client's node identity, which servers can allowlist.
	// If zero, a new ephemeral key is generated at first use.
	// If set, it must be set before the client's first use.
	Key key.NodePrivate

	// Logf is the logger used for debug messages. If nil, log.Printf
	// is used. If set, it must be set before the client's first use.
	Logf logger.Logf

	// DERPMapURL, if non-empty, is an alternate URL to fetch the DERP
	// map from when the address doesn't embed the relay details.
	// If empty, [DefaultDERPMapURL] is used. If set, it must be set
	// before the client's first use.
	DERPMapURL string

	// DERPMapCache, if non-nil, caches fetched DERP maps. If nil, a
	// process-wide in-memory cache is used. If set, it must be set
	// before the client's first use.
	DERPMapCache DERPMapCache
	// contains filtered or unexported fields
}

Client connects to a Server over a WireGuard tunnel relayed through DERP. Populate Server (the only required field, or use the NewClient shorthand), then just dial: Client.Dial, the DialTCP methods, and the DialUDP methods lazily establish the tunnel on first use, picking defaults for any unset fields. Client.Ping does the same and is useful to test connectivity first or to measure the relay round-trip time.

func NewClient

func NewClient(server Addr) *Client

NewClient returns a client that will connect to the server identified by the given tailcat address. It is shorthand for &Client{Server: server}; see Client for the optional fields that may also be set before the client's first use.

func (*Client) Close

func (c *Client) Close() error

Close shuts down the client, closing the WireGuard engine and DERP connections.

func (*Client) Dial

func (c *Client) Dial(ctx context.Context, network, addr string) (net.Conn, error)

Dial opens a connection to the given network/address through the server's WireGuard tunnel. The address is resolved relative to the server.

On a Client's first use (any Dial method or Client.Ping), the client lazily brings up its network stack, resolving the server's DERP region over the network if the Addr didn't embed it, and registers itself with the server.

func (*Client) DialTCP

func (c *Client) DialTCP(ctx context.Context, ap netip.AddrPort) (net.Conn, error)

DialTCP opens a TCP connection to an arbitrary IP:port through the server, which must be configured as an exit node (see Server.OnTCPForward). IPv4 addresses are mapped into the NAT64 prefix (64:ff9b::/96) for transport over the IPv6-only WireGuard tunnel. See Client.Dial for the lazy startup behavior.

func (*Client) DialTCPPort

func (c *Client) DialTCPPort(ctx context.Context, port uint16) (net.Conn, error)

DialTCPPort opens a TCP connection to the given port on the server. See Client.Dial for the lazy startup behavior.

func (*Client) DialUDP added in v0.6.0

func (c *Client) DialUDP(ctx context.Context, ap netip.AddrPort) (ConnPacketConn, error)

DialUDP opens a connected UDP packet connection to an arbitrary IP:port through the server, which must be configured to forward UDP (see Server.OnUDPForward). IPv4 addresses are mapped into the NAT64 prefix for transport over the IPv6-only WireGuard tunnel. See Client.Dial for the lazy startup behavior.

func (*Client) DialUDPPort added in v0.6.0

func (c *Client) DialUDPPort(ctx context.Context, port uint16) (ConnPacketConn, error)

DialUDPPort opens a connected UDP packet connection to the given port on the server. Each Write sends one datagram and each Read receives one datagram. See Client.Dial for the lazy startup behavior.

func (*Client) DiscoPing

func (c *Client) DiscoPing(ctx context.Context) (*ipnstate.PingResult, error)

DiscoPing sends a disco ping to the server and reports how the pong came back: the result's Endpoint field is set if it arrived over a direct path, else DERPRegionID (and DERPRegionCode) say which relay carried it. Unlike Client.Ping, which always measures the DERP path, a disco ping also actively triggers direct path discovery, so pinging repeatedly upgrades the connection when NAT traversal is possible. It starts the client and registers with the server first if needed.

func (*Client) DrainTCP added in v0.3.0

func (c *Client) DrainTCP(ctx context.Context) error

DrainTCP waits until none of the client's TCP connections have segments left to send or retransmit. It returns nil once drained, or ctx's error.

It is meant to be called after the last connection has been closed or has reached EOF. Like Server.DrainTCP, it exists because the whole TCP stack runs inside this process: a client that reads the server's EOF and exits immediately can lose the final ACK of the server's FIN before it is ever transmitted, leaving the server retransmitting its FIN to a dead peer until it gives up.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) (PingResult, error)

Ping starts the client if needed (see Client.Dial for the lazy startup behavior), sends a meow ping to the server via DERP (resending periodically in case of packet loss), and waits for the meowed acknowledgment, which also tells the server to add us as a WireGuard peer. Calling it is optional (Dial does it implicitly) but useful to test connectivity or measure the relay round-trip time. The internal timeout is 10 seconds regardless of ctx.

func (*Client) PublicKey

func (c *Client) PublicKey() key.NodePublic

PublicKey returns the client's node public key, generating the key first if the Key field is zero and the client hasn't yet been used.

type ConnBlob deprecated

type ConnBlob = Addr

ConnBlob is an alias for Addr.

Deprecated: use Addr instead.

type ConnInfo

type ConnInfo struct {
	ServerPublic NodePublic // a key.NodePublic
	// ServerDiscoPublic is the server's public key for path discovery.
	// It is deliberately independent from ServerPublic: disco packets carry
	// this key in cleartext on direct UDP paths, while ServerPublic is the
	// unguessable part of the server's tailcat address.
	ServerDiscoPublic DiscoPublic // a key.DiscoPublic

	// PresharedKey is mixed into the WireGuard handshake. It is an independent
	// random secret, providing post-quantum confidentiality and preventing a
	// DERP operator that observes the peers' node public keys from joining the
	// tunnel. When non-zero, treat the entire tailcat address as a secret
	// because it contains this key. The zero value disables the pre-shared-key
	// layer for compatibility with old clients.
	PresharedKey PresharedKey

	// Region, if non-empty, lists the regions of a DERPMap.
	// Either Region or RegionID must be set. If Region is set
	// the client can avoid doing a lookup to discover the DERP map
	// but the tailcat address is longer.
	//
	// As of 2023-09-22, a maximum of 1 region may be provided.
	// In the future, a server might advertise its presence in
	// multiple DERP regions and clients could try them all.
	Region []*tailcfg.DERPRegion `json:",omitempty"`

	// RegionID lists the number of one of Tailscale's provided
	// DERP servers. If set, Region may be omitted and the tailcat address
	// is shorter, at the cost of the client needing to fetch
	// the derpmap from tailscale.com once at startup.
	// If -1 (for use when saving a keypair to disk for reuse later), a region
	// is selected automatically at startup based on latency.
	RegionID tailcfg.DERPRegionID `json:",omitempty"`
}

ConnInfo describes how to reach a server: its WireGuard and path-discovery public keys, WireGuard pre-shared key, and which DERP relay region to use. It is serialized into an Addr for exchange, via the wire types in wire.go.

func ParseAddr added in v0.5.0

func ParseAddr(addr Addr) (ConnInfo, error)

ParseAddr decodes an Addr back into a ConnInfo, restoring fields that were stripped during encoding (RegionID, RegionCode, node names).

func ParseConnBlob deprecated

func ParseConnBlob(addr ConnBlob) (ConnInfo, error)

ParseConnBlob decodes an address into a ConnInfo.

Deprecated: use ParseAddr instead.

func (*ConnInfo) Addr added in v0.5.0

func (ci *ConnInfo) Addr() Addr

Addr serializes the ConnInfo into a compact Addr string. It is encoded via the wire types (see wire.go), which drop the DERP region fields tailcat doesn't use. Some other fields (RegionID, RegionCode, RegionName, node names that are redundant next to an explicit HostName) are zeroed before encoding to reduce size; ParseAddr restores them.

func (*ConnInfo) ConnBlob deprecated

func (ci *ConnInfo) ConnBlob() ConnBlob

ConnBlob serializes ci into a tailcat address.

Deprecated: use ConnInfo.Addr instead.

func (*ConnInfo) Expand

func (ci *ConnInfo) Expand(ctx context.Context, opts ...any) error

Expand populates ci.Region from a DERP map if only ci.RegionID was set. If ci.Region is already populated, Expand is a no-op. When RegionID is -1, the best region is selected automatically via netcheck latency probes.

The opts may contain any of the following types:

  • DERPMapURL: fetch the DERP map from an alternate URL instead of DefaultDERPMapURL.
  • *tailcfg.DERPMap: expand from the provided DERP map instead of fetching one over the network.
  • ExpandForServer: mark the DERP map fetch as being on behalf of a tailcat server rather than a client.
  • DERPMapCache: cache fetched DERP maps (defaults to a process-wide in-memory cache).

type ConnPacketConn added in v0.6.0

type ConnPacketConn interface {
	net.Conn
	net.PacketConn
}

ConnPacketConn is a connected datagram socket. Read and Write preserve UDP datagram boundaries, while the net.PacketConn methods are available to code that prefers packet-oriented APIs. LocalAddr and RemoteAddr identify the destination and source endpoints of an incoming server flow.

type DERPMapCache

type DERPMapCache interface {
	// Get returns the previously stored DERP map response for url:
	// its raw JSON, the server's ETag (or ""), and when it was
	// stored. It returns ok == false if nothing usable is stored.
	Get(url string) (data []byte, etag string, storedAt time.Time, ok bool)

	// Put stores the DERP map response for url, replacing any prior
	// entry and marking it stored as of now. An empty etag means the
	// server sent none.
	Put(url string, data []byte, etag string) error
}

DERPMapCache is an option for ConnInfo.Expand and FetchDERPMap that caches fetched DERP maps. Without one, a process-wide in-memory cache is used; provide an implementation (like the tailcat CLI's on-disk one) to persist across processes. Implementations just store bytes; the freshness policy lives in the fetcher: a stored map younger than an hour is used without any network traffic, an older one is revalidated with If-None-Match (the ETag is opaque to us), and a stored map of any age is used as a fallback if the fetch fails or times out.

type DERPMapURL

type DERPMapURL string

DERPMapURL is an option for ConnInfo.Expand specifying an alternate URL to fetch the DERP map from instead of DefaultDERPMapURL.

type DiscoPublic added in v0.3.0

type DiscoPublic struct {
	key.DiscoPublic
}

DiscoPublic is a wrapper around key.DiscoPublic that uses its raw 32-byte representation in tailcat addresses.

func DiscoPublicForNode added in v0.3.0

func DiscoPublicForNode(k key.NodePrivate) DiscoPublic

DiscoPublicForNode returns the path-discovery public key derived from a node private key. Code constructing a ConnInfo directly must include it as ConnInfo.ServerDiscoPublic.

func (DiscoPublic) Equal added in v0.3.0

func (a DiscoPublic) Equal(b DiscoPublic) bool

Equal reports whether a and b represent the same disco public key.

func (DiscoPublic) MarshalBinary added in v0.3.0

func (p DiscoPublic) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler.

func (*DiscoPublic) UnmarshalBinary added in v0.3.0

func (p *DiscoPublic) UnmarshalBinary(x []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler.

type FileServeMode added in v0.4.0

type FileServeMode byte

FileServeMode says what a rooted SFTP file service lets clients do.

const (
	// FileServeRO serves files read-only: clients can list, stat, and
	// download files, but not modify anything.
	FileServeRO FileServeMode = iota

	// FileServeRW serves files read-write.
	FileServeRW

	// FileServeWO serves files write-only, as a flat drop box. Each upload
	// is stored under a new, server-chosen name, so clients can't use name
	// collisions to discover existing files. Clients can't make directories,
	// list the drop box, or read anything back.
	FileServeWO

	// FileServeWOPlus serves files write-only, as a recursive drop box.
	// Clients can make and stat directories, which necessarily reveals some
	// information about existing paths. An upload keeps its requested name
	// when available and is stored under a new, server-chosen name otherwise.
	FileServeWOPlus
)

type FileService added in v0.4.0

type FileService struct {
	// Dir is the directory to serve.
	Dir string

	// Mode says what clients may do within Dir.
	Mode FileServeMode
}

FileService describes a rooted SFTP file service. All client paths are resolved inside Dir via os.Root, so neither ".." nor symlinks can escape it.

type NodePublic

type NodePublic struct {
	key.NodePublic
}

NodePublic is a wrapper around key.NodePublic just so we can have a slightly smaller CBOR representation without the "np" prefix.

func (NodePublic) Equal

func (a NodePublic) Equal(b NodePublic) bool

Equal reports whether a and b represent the same public key.

func (NodePublic) MarshalBinary

func (p NodePublic) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler for CBOR serialization, encoding the raw 32-byte key without the "nodekey:" text prefix.

func (*NodePublic) UnmarshalBinary

func (p *NodePublic) UnmarshalBinary(x []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler for CBOR deserialization.

type PingResult

type PingResult struct {
	// Latency is the round-trip time for the meow/meowed handshake
	// through the DERP relay.
	Latency time.Duration
}

PingResult is the result of a successful Client.Ping call.

type PresharedKey added in v0.6.0

type PresharedKey device.NoisePresharedKey

PresharedKey is an optional 256-bit WireGuard pre-shared key. Tailcat addresses generated by current servers always contain a non-zero key. It is a named form of device.NoisePresharedKey so it can define the CBOR and JSON encodings used by tailcat addresses and persisted server keys.

func NewPresharedKey added in v0.6.0

func NewPresharedKey() (ret PresharedKey)

NewPresharedKey returns a new cryptographically random WireGuard pre-shared key.

func (PresharedKey) Equal added in v0.6.0

func (p PresharedKey) Equal(q PresharedKey) bool

Equal reports whether p and q contain the same key.

func (PresharedKey) IsZero added in v0.6.0

func (p PresharedKey) IsZero() bool

IsZero reports whether p is the zero value.

func (PresharedKey) MarshalBinary added in v0.6.0

func (p PresharedKey) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler for CBOR serialization.

func (PresharedKey) MarshalText added in v0.6.0

func (p PresharedKey) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler for JSON serialization.

func (*PresharedKey) UnmarshalBinary added in v0.6.0

func (p *PresharedKey) UnmarshalBinary(x []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler for CBOR serialization.

func (*PresharedKey) UnmarshalText added in v0.6.0

func (p *PresharedKey) UnmarshalText(x []byte) error

UnmarshalText implements encoding.TextUnmarshaler for JSON serialization.

type PrivateKey

type PrivateKey struct {
	Private key.NodePrivate
	Public  ConnInfo
}

PrivateKey is a node identity: a private key paired with the connection info needed to reach this node. Despite its historical name, Public contains the secret WireGuard pre-shared key and must be kept private. Its DERP region must be populated by the caller before the key is usable.

func NewPrivateKey

func NewPrivateKey() *PrivateKey

NewPrivateKey returns a new PrivateKey, but without the DERP region populated. It's up to the caller to populate that.

type SSHOptions added in v0.4.0

type SSHOptions struct {
	// Shell enables shell and exec sessions.
	Shell bool

	// AuthorizedKeys contains OpenSSH authorized_keys text. Each element may
	// contain one or more public key lines. Clients authenticate with one of the
	// listed keys. Authorized-key options are not supported; rejecting them
	// avoids silently granting broader access than their author intended.
	AuthorizedKeys []string

	// Files, if non-nil, serves the SFTP subsystem rooted at
	// Files.Dir, restricted to Files.Mode. If nil and Shell is true,
	// SFTP is instead served with the same access the shell has: the
	// whole filesystem, with relative paths resolved against the
	// user's home directory.
	Files *FileService
}

SSHOptions configures the SSH server returned by Server.SSHConnHandler.

type Server

type Server struct {
	// Key is the server's node identity.
	// If zero, Start generates a new ephemeral key.
	Key key.NodePrivate

	// PresharedKey is the WireGuard pre-shared key clients must know to
	// connect. If zero, Start generates a new ephemeral key and includes it in
	// [Server.TailcatAddr]. A persistent server must restore this value along
	// with Key so its address remains usable across restarts.
	PresharedKey PresharedKey

	// DisablePresharedKey disables the pre-shared-key layer and causes Start to
	// ignore PresharedKey. This is not recommended, but produces shorter
	// addresses compatible with tailcat clients v0.5.0 and earlier.
	DisablePresharedKey bool

	// Logf is the logger used for debug messages.
	// If nil, log.Printf is used.
	Logf logger.Logf

	// Region, if non-nil, is the DERP region to use as the bootstrap
	// relay, without fetching any DERP map.
	Region *tailcfg.DERPRegion

	// RegionID, if non-zero and Region is nil, is the ID of the DERP
	// map region to use. If zero, the nearest region is picked based
	// on latency at Start.
	RegionID tailcfg.DERPRegionID

	// DERPMapURL, if non-empty, is an alternate URL to fetch the DERP
	// map from when Region is nil. If empty, [DefaultDERPMapURL] is
	// used.
	DERPMapURL string

	// DERPMapCache, if non-nil, caches fetched DERP maps. If nil, a
	// process-wide in-memory cache is used.
	DERPMapCache DERPMapCache

	// AllowedClients, if non-empty, restricts which client node keys
	// may connect; all others are silently ignored. If empty, all
	// clients are allowed. See [Server.AddAllowedClient] to add more
	// at runtime.
	AllowedClients []key.NodePublic

	// AllowProxy, if non-nil, reports whether
	// a TCP or UDP proxy is allowed for that target.
	AllowProxy func(netip.AddrPort) bool

	// OnTCP, if non-nil, specifies a func that returns a handler to handle
	// incoming connections to the provided port. If nil or if it returns nil,
	// then a RST is sent.
	//
	// This only applies to connections directly to the server node and not
	// when being a subnet router. See OnTCPForward for relayed connections.
	//
	// It must be set before calling Start.
	OnTCP func(port uint16) (handler func(net.Conn))

	// OnTCPForward, if non-nil, specifies a func that returns a handler to handle
	// incoming connections to the provided IP:port. If nil or if it returns nil,
	// then a RST is sent.
	//
	// This only applies to connections relayed through the server and not to the server
	// itself. See OnTCP for direct connections to the server.
	//
	// It must be set before calling Start. Setting it also widens the
	// packet filter installed at Start to admit traffic to any
	// destination, not just the server's own address.
	OnTCPForward func(netip.AddrPort) (handler func(net.Conn))

	// OnUDP, if non-nil, specifies a func that returns a handler for an
	// incoming UDP flow to the provided port. Each handler receives a connected
	// packet connection for one client source IP:port. Datagram boundaries are
	// preserved, and LocalAddr and RemoteAddr report the destination and source
	// of the flow. If nil or if it returns nil, the flow is dropped.
	//
	// This only applies to packets addressed directly to the server node and not
	// when being a subnet router. See OnUDPForward for relayed packets.
	//
	// It must be set before calling Start.
	OnUDP func(port uint16) (handler func(ConnPacketConn))

	// OnUDPForward is like OnUDP for UDP flows addressed through the server to
	// another IP:port. Setting it also widens the packet filter installed at
	// Start to admit UDP traffic to any destination.
	//
	// It must be set before calling Start.
	OnUDPForward func(netip.AddrPort) (handler func(ConnPacketConn))

	// ServedTCPPorts, if non-nil, restricts which TCP ports on the
	// server's own address the packet filter admits new inbound
	// connections to. If nil, connections to all ports reach OnTCP,
	// which remains the per-port gate either way. Callers that know
	// their served ports statically (like the tailcat CLI) can set
	// this for defense in depth.
	//
	// Unlike OnTCP's nil-handler response, packets dropped by the
	// filter get no RST; a client dialing a filtered port times out.
	//
	// It must be set before calling Start.
	ServedTCPPorts []filter.PortRange

	// ServedUDPPorts, if non-nil, restricts which UDP ports on the server's own
	// address the packet filter admits. If nil, packets to all ports reach
	// OnUDP, which remains the per-flow gate either way.
	//
	// It must be set before calling Start.
	ServedUDPPorts []filter.PortRange

	// UDPIdleTimeout is how long an inactive incoming UDP flow remains open.
	// A zero value uses [DefaultUDPIdleTimeout]. Successful reads and writes
	// reset the timeout. It must be set before calling Start.
	UDPIdleTimeout time.Duration
	// contains filtered or unexported fields
}

Server listens for clients over a WireGuard tunnel relayed through DERP. Incoming TCP connections and UDP flows are dispatched via the OnTCP/OnUDP callbacks (for traffic addressed to the server itself) and their Forward counterparts (for traffic the server relays to other addresses).

The zero value is a usable server: optionally populate the configuration fields, then call Server.Start, which picks defaults for anything unset.

func (*Server) AddAllowedClient

func (s *Server) AddAllowedClient(k key.NodePublic)

AddAllowedClient adds k as an allowed client.

Until a key is allowed (here or via Server.AllowedClients), all clients are allowed.

func (*Server) Addr

func (s *Server) Addr() netip.Addr

Addr returns the server's IPv6 address derived from its public key. It must only be called after Server.Start.

func (*Server) Close

func (s *Server) Close() error

Close shuts down the server, closing the WireGuard engine and DERP connections.

func (*Server) ConnBlob deprecated

func (s *Server) ConnBlob() ConnBlob

ConnBlob returns the tailcat address that clients use to connect to s.

Deprecated: use Server.TailcatAddr instead.

func (*Server) DrainTCP

func (s *Server) DrainTCP(ctx context.Context) error

DrainTCP waits until every TCP connection in the server's netstack has fully closed, meaning the peer has acknowledged all sent data and the final FIN. It returns nil once drained, or ctx's error.

The whole TCP stack runs inside this process, so exiting right after a net.Conn Close can lose the FIN before it is ever transmitted, leaving the peer waiting for an EOF that never comes. A process that closes a connection and then exits should first call DrainTCP with a timeout bounding ctx, in case the peer is gone and the FIN is never acknowledged.

It is meant for the passive closer (the side that closes second), which goes straight to CLOSED once its FIN is acked. A connection this side closed first instead parks in TIME-WAIT and would block DrainTCP until the TIME-WAIT timer fires.

func (*Server) HandleTailscaleSSHConn

func (s *Server) HandleTailscaleSSHConn(c net.Conn)

HandleTailscaleSSHConn handles an incoming TCP connection as an SSH session with a shell enabled. See Server.SSHConnHandler for the details.

func (*Server) SSHConnHandler added in v0.4.0

func (s *Server) SSHConnHandler(opts SSHOptions) func(net.Conn)

SSHConnHandler returns a handler that serves an incoming TCP connection as an SSH session with the capabilities in opts. Authentication is controlled by opts.AuthorizedKeys. Configured public keys require a client match; zero-value options rely on the WireGuard tunnel for client identity. The connection is served using the gliderlabs/ssh library with a single ed25519 host key generated on first use under tailcat/ssh in the user's config directory (os.UserConfigDir).

With opts.Shell, two session modes are supported: if the SSH client sends a command, it is run by the user's shell (PowerShell on Windows); otherwise an interactive login shell is started with a PTY. The SFTP subsystem is served per opts.Files; see SSHOptions.

func (*Server) Start

func (s *Server) Start() error

Start connects to the DERP relay and begins accepting clients, first picking defaults for any unset configuration fields: a new ephemeral key, log.Printf for logging, and the nearest region of the default DERP map.

func (*Server) Status

func (s *Server) Status() *ipnstate.Status

Status returns the current WireGuard and DERP connection status.

func (*Server) TailcatAddr added in v0.5.0

func (s *Server) TailcatAddr() Addr

TailcatAddr returns the tailcat address that clients use to connect to this server. It embeds the full DERP region, so clients don't need to fetch the DERP map from the network. It must only be called after Server.Start.

Image Directories

Path Synopsis
cmd
tailcat command
tailcat-web command
The tailcat-web command is a development server for the tailcat browser app in the web/ directory.
The tailcat-web command is a development server for the tailcat browser app in the web/ directory.
tailcat-webdist command
The tailcat-webdist command builds the distribution directory of static files needed to serve the tailcat browser app: index.html, app.js, wasm_exec.js, and the js/wasm main.wasm binary with precompressed .zst and .gz variants.
The tailcat-webdist command builds the distribution directory of static files needed to serve the tailcat browser app: index.html, app.js, wasm_exec.js, and the js/wasm main.wasm binary with precompressed .zst and .gz variants.
internal
buildtags
Package buildtags computes the go build tag lists that tailcat binaries are built with.
Package buildtags computes the go build tag lists that tailcat binaries are built with.
buildtags/printtags command
Command printtags prints the build tag list for native builds of cmd/tailcat.
Command printtags prints the build tag list for native builds of cmd/tailcat.
wasmbuild
Package wasmbuild builds the tailcat web WebAssembly binary and the distribution directory of static files that servers of the web app need.
Package wasmbuild builds the tailcat web WebAssembly binary and the distribution directory of static files that servers of the web app need.
tool
updateflakes command
updateflakes regenerates flakehashes.json, the file that records the Nix SRI hash of the Go module vendor tree for flake.nix.
updateflakes regenerates flakehashes.json, the file that records the Nix SRI hash of the Go module vendor tree for flake.nix.
The tailcat web app is the WebAssembly (js/wasm) build of tailcat for browsers.
The tailcat web app is the WebAssembly (js/wasm) build of tailcat for browsers.
Package webdemo serves the tailcat browser app (the js/wasm build of tailcat in the web/ directory) from a distribution directory of prebuilt static files, as produced by cmd/tailcat-webdist.
Package webdemo serves the tailcat browser app (the js/wasm build of tailcat in the web/ directory) from a distribution directory of prebuilt static files, as produced by cmd/tailcat-webdist.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL