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 ConnBlob (connection blob) that encodes the server's public 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 traffic over the WireGuard tunnel with no Tailscale account or coordination server required. Optionally, the server can run an auth-free SSH server on port 22, providing remote shell access over the tunnel.
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 ConnBlob.
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
- Variables
- func EncodeMeowPing(nodeKey key.NodePublic, discoKey key.DiscoPublic) []byte
- func EncodeMeowed() []byte
- func FetchDERPMap(ctx context.Context, opts ...any) (*tailcfg.DERPMap, error)
- func IsMeowPacket(pkt []byte) bool
- func IsMeowedPacket(pkt []byte) bool
- func ParseConnBlobRaw(cb ConnBlob) (any, error)
- func ParseMeowPing(pkt []byte) (nodeKey key.NodePublic, discoKey key.DiscoPublic, ok bool)
- func PickBestRegion(ctx context.Context, dm *tailcfg.DERPMap) (regionID int, err error)
- func ProxyConns(a, b net.Conn)
- func SupportsSSHServer() bool
- type Client
- func (c *Client) Close() error
- func (c *Client) Dial(ctx context.Context, network, addr string) (net.Conn, error)
- func (c *Client) DialTCP(ctx context.Context, ap netip.AddrPort) (net.Conn, error)
- func (c *Client) DialTCPPort(ctx context.Context, port uint16) (net.Conn, error)
- func (c *Client) DiscoPing(ctx context.Context) (*ipnstate.PingResult, error)
- func (c *Client) Ping(ctx context.Context) (PingResult, error)
- func (c *Client) PublicKey() key.NodePublic
- type ConnBlob
- type ConnInfo
- type DERPMapCache
- type DERPMapURL
- type NodePublic
- type PingResult
- type PrivateKey
- type Server
- func (s *Server) AddAllowedClient(k key.NodePublic)
- func (s *Server) Addr() netip.Addr
- func (s *Server) Close() error
- func (s *Server) ConnBlob() ConnBlob
- func (s *Server) DrainTCP(ctx context.Context) error
- func (s *Server) HandleTailscaleSSHConn(c net.Conn)
- func (s *Server) Start() error
- func (s *Server) Status() *ipnstate.Status
Constants ¶
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.
Variables ¶
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.
var README string
README is the tailcat README.md, embedded so the CLI can print it with its --readme flag. That lets people (and AI agents) with only the binary learn how to use it without web access.
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 ¶
FetchDERPMap fetches and decodes the JSON DERP map. The opts may contain any of the following types:
- DERPMapURL: fetch from an alternate URL instead of DefaultDERPMapURL.
- ExpandForServer: mark the 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).
func IsMeowPacket ¶
IsMeowPacket reports whether pkt starts with the meow magic prefix.
func IsMeowedPacket ¶
IsMeowedPacket reports whether pkt is a meowed (acknowledgment) packet.
func ParseConnBlobRaw ¶
ParseConnBlobRaw decodes cb into its wire form, without restoring the implicit fields that ParseConnBlob 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 blob actually carries.
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 ¶
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 ¶
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 SupportsSSHServer ¶
func SupportsSSHServer() bool
SupportsSSHServer reports whether the platform supports running the built-in auth-free SSH server.
Types ¶
type Client ¶
type Client struct {
// Server is the token identifying the server to connect to.
// It is required and must be set before the client's first use.
Server ConnBlob
// 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 token 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, Client.DialTCPPort, and Client.DialTCP 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 ¶
NewClient returns a client that will connect to the server identified by the given token. 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 ¶
Close shuts down the client, closing the WireGuard engine and DERP connections.
func (*Client) Dial ¶
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 ConnBlob didn't embed it, and registers itself with the server.
func (*Client) DialTCP ¶
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 ¶
DialTCPPort opens a TCP connection to the given port on the server. See Client.Dial for the lazy startup behavior.
func (*Client) DiscoPing ¶
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) 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, 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 ¶
type ConnBlob string
ConnBlob is a compact, URL-safe string that a server gives to clients so they can connect. It is the "tc"-prefixed base64url encoding of CBOR-encoded ConnInfo. A typical ConnBlob looks like "tcomFwWC…".
func (ConnBlob) Resolve ¶
Resolve returns a self-contained equivalent of b with the DERP relay's details embedded, so that later use of the blob requires no network access to fetch the DERP map. It is to a ConnBlob 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 b already embeds its relay details, it is returned unchanged. The opts are as documented on ConnInfo.Expand.
type ConnInfo ¶
type ConnInfo struct {
ServerPublic NodePublic // a key.NodePublic
// 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 ConnBlob 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 ConnBlob
// 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 int `json:",omitempty"`
}
ConnInfo describes how to reach a server: its public key and which DERP relay region to use. It is serialized into a ConnBlob for exchange, via the wire types in wire.go.
func ParseConnBlob ¶
ParseConnBlob decodes a ConnBlob back into a ConnInfo, restoring fields that were stripped during encoding (RegionID, RegionCode, node names).
func (*ConnInfo) ConnBlob ¶
ConnBlob serializes the ConnInfo into a compact ConnBlob 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; ParseConnBlob restores them.
func (*ConnInfo) Expand ¶
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 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 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 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. The DERP region in Public 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 Server ¶
type Server struct {
// Key is the server's node identity.
// If zero, Start generates a new ephemeral key.
Key key.NodePrivate
// 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 int
// 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))
// 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
// contains filtered or unexported fields
}
Server listens for clients over a WireGuard tunnel relayed through DERP. Incoming TCP connections are dispatched via Server.OnTCP (for connections addressed to the server itself) and Server.OnTCPForward (for connections the server relays to other addresses, acting as an exit node).
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 ¶
Addr returns the server's IPv6 address derived from its public key. It must only be called after Server.Start.
func (*Server) Close ¶
Close shuts down the server, closing the WireGuard engine and DERP connections.
func (*Server) ConnBlob ¶
ConnBlob returns the token 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.
func (*Server) DrainTCP ¶
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 ¶
HandleTailscaleSSHConn handles an incoming TCP connection as an SSH session. Authentication is not required — the WireGuard tunnel provides identity. The connection is served using the gliderlabs/ssh library with a single ed25519 host key generated on first use in ~/.config/tailcat/ssh/.
Two modes are supported: if the SSH client sends a command, it is executed via the user's shell with "-c"; otherwise an interactive login shell is started with a PTY.
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
|
|
|
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. |
|
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. |