tailcat

package
v11.3.6 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT, BSD-3-Clause Imports: 50 Imported by: 0

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

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.

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 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 ParseConnBlobRaw

func ParseConnBlobRaw(cb ConnBlob) (any, error)

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

func PickBestRegion(ctx context.Context, dm *tailcfg.DERPMap) (regionID int, 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.

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

func NewClient(server ConnBlob) *Client

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

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 ConnBlob 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) 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) 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.

func (*Client) Status

func (c *Client) Status() *ipnstate.Status

Status returns the client's current WireGuard and magicsock status. It returns nil until the client has initialized its networking stack.

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

func (b ConnBlob) Resolve(ctx context.Context, opts ...any) (ConnBlob, error)

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

func ParseConnBlob(cb ConnBlob) (ConnInfo, error)

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

func (*ConnInfo) ConnBlob

func (ci *ConnInfo) ConnBlob() 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

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 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

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

func (s *Server) ConnBlob() 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

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) 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) StartContext

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

StartContext is like Start, but bounds DERP-map discovery with ctx. Once startup succeeds, the server remains active until Close is called.

func (*Server) Status

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

Status returns the current WireGuard and DERP connection status.

Jump to

Keyboard shortcuts

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