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
- 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 ParseAddrRaw(addr Addr) (any, error)
- func ParseConnBlobRaw(addr ConnBlob) (any, error)deprecated
- func ParseMeowPing(pkt []byte) (nodeKey key.NodePublic, discoKey key.DiscoPublic, ok bool)
- func PickBestRegion(ctx context.Context, dm *tailcfg.DERPMap) (regionID tailcfg.DERPRegionID, err error)
- func ProxyConns(a, b net.Conn)
- func ProxyPacketConns(a, b ConnPacketConn)
- func SupportsSSHServer() bool
- func ValidateSSHAuthorizedKeys(texts []string) error
- type Addr
- 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) DialUDP(ctx context.Context, ap netip.AddrPort) (ConnPacketConn, error)
- func (c *Client) DialUDPPort(ctx context.Context, port uint16) (ConnPacketConn, error)
- func (c *Client) DiscoPing(ctx context.Context) (*ipnstate.PingResult, error)
- func (c *Client) DrainTCP(ctx context.Context) error
- func (c *Client) Ping(ctx context.Context) (PingResult, error)
- func (c *Client) PublicKey() key.NodePublic
- type ConnBlobdeprecated
- type ConnInfo
- type ConnPacketConn
- type DERPMapCache
- type DERPMapURL
- type DiscoPublic
- type FileServeMode
- type FileService
- type NodePublic
- type PingResult
- type PresharedKey
- type PrivateKey
- type SSHOptions
- type Server
- func (s *Server) AddAllowedClient(k key.NodePublic)
- func (s *Server) Addr() netip.Addr
- func (s *Server) Close() error
- func (s *Server) ConnBlob() ConnBlobdeprecated
- func (s *Server) DrainTCP(ctx context.Context) error
- func (s *Server) HandleTailscaleSSHConn(c net.Conn)
- func (s *Server) SSHConnHandler(opts SSHOptions) func(net.Conn)
- func (s *Server) Start() error
- func (s *Server) Status() *ipnstate.Status
- func (s *Server) TailcatAddr() Addr
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.
const DefaultUDPIdleTimeout = 2 * time.Minute
DefaultUDPIdleTimeout is the amount of inactivity after which an incoming UDP flow is closed.
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 ¶
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 subcommand. 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 ParseAddrRaw ¶ added in v0.5.0
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
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 ¶
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
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
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 ¶
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 ¶
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 Addr 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) DialUDP ¶ added in v0.6.0
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
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 ¶
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
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 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
// 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
ParseAddr decodes an Addr back into a ConnInfo, restoring fields that were stripped during encoding (RegionID, RegionCode, node names).
func (*ConnInfo) Addr ¶ added in v0.5.0
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
ConnBlob serializes ci into a tailcat address.
Deprecated: use ConnInfo.Addr instead.
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 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
// 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
// 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 ¶
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
deprecated
ConnBlob returns the tailcat address that clients use to connect to s.
Deprecated: use Server.TailcatAddr instead.
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 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 ¶
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) TailcatAddr ¶ added in v0.5.0
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.
Source Files
¶
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. |