Tailscale — Interfaces#

Interface catalog#

wgengine.Engine#

  • Package: tailscale.com/wgengine
  • File: wgengine/engine.go (not present in this checkout; interface reconstructed from mock in ipn/ipnlocal/state_test.go:1894)
  • Methods:
    • Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config) error
    • Config() *wgcfg.Config
    • RouterConfig() *router.Config
    • DNSConfig() *dns.Config
    • PeerForIP(netip.Addr) (PeerForIP, bool)
    • GetFilter() *filter.Filter
    • SetFilter(f *filter.Filter)
    • GetJailedFilter() *filter.Filter
    • SetJailedFilter(f *filter.Filter)
    • SetStatusCallback(cb StatusCallback)
    • RequestStatus()
    • ResetAndStop() (*Status, error)
    • PeerByKey(key.NodePublic) (wgint.Peer, bool)
    • SetNetworkMap(*netmap.NetworkMap)
    • UpdateStatus(*ipnstate.StatusBuilder)
    • Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult))
    • InstallCaptureHook(packet.CaptureCallback)
    • Close()
    • Done() <-chan struct{}
  • Purpose: The central abstraction for the WireGuard data plane. Encapsulates all interaction with the wireguard-go device: peer configuration, packet filter management, path probing callbacks, and liveness monitoring.
  • Implementations: UserspaceEngine (primary, wraps wireguard-go), Watchdog (decorator that adds liveness monitoring), mockEngine (test double in ipnlocal/state_test.go).
  • Design quality: Broad (19 methods), but necessarily so given the data plane’s many responsibilities. The filter methods come in SetFilter/SetJailedFilter pairs, hinting at the sandboxed-subnet-router model. The Done() channel follows the standard Go shutdown idiom. The callback-based Ping signals an asynchronous design appropriate for network operations.

controlclient.Client#

  • Package: tailscale.com/control/controlclient
  • File: control/controlclient/client.go:40
  • Methods:
    • Shutdown()
    • Login(LoginFlags)
    • Logout(context.Context) error
    • SetPaused(bool)
    • AuthCantContinue() bool
    • SetHostinfo(*tailcfg.Hostinfo)
    • SetNetInfo(*tailcfg.NetInfo)
    • SetTKAHead(headHash string)
    • UpdateEndpoints(endpoints []tailcfg.Endpoint)
    • SetDiscoPublicKey(key.DiscoPublic)
    • SetIPForwardingBroken(bool)
    • ClientID() int64
  • Purpose: Abstracts the authenticated HTTPS long-poll connection to Tailscale’s coordination server. Allows LocalBackend to drive authentication flows, push updated host and network information to control, and receive the resulting NetworkMap via the companion Observer callback interface.
  • Implementations: Auto (production long-poll client), Direct (single-shot request client for testing/registration).
  • Design quality: Well-focused on the control plane lifecycle. The SetPaused method is a pragmatic addition to minimize unnecessary network activity when the machine is idle. The many Set* methods (Hostinfo, NetInfo, TKAHead, DiscoPublicKey) reflect the reality that the control protocol is state-accumulating — callers push incremental updates rather than re-sending complete state.

controlclient.Observer#

  • Package: tailscale.com/control/controlclient
  • File: control/controlclient/direct.go:122
  • Methods:
    • SetControlClientStatus(Client, Status)
  • Purpose: Callback interface through which the control client reports network map changes, authentication state changes, and other status events back to LocalBackend. Receives a Client reference to allow stale-client detection (if the client delivering the status is no longer the current one, LocalBackend ignores the update).
  • Implementations: LocalBackend (primary consumer).
  • Design quality: Single-method — excellent ISP adherence. The inclusion of the Client parameter in the callback is an elegant stale-client detection mechanism without shared mutable state.

controlclient.NetmapUpdater / NetmapDeltaUpdater#

  • Package: tailscale.com/control/controlclient
  • File: control/controlclient/direct.go:210,221
  • Methods:
    • NetmapUpdater: UpdateFullNetmap(*netmap.NetworkMap)
    • NetmapDeltaUpdater (optional extension): UpdateNetmapDelta([]netmap.NodeMutation) (ok bool)
  • Purpose: Two-level interface for receiving network map updates. NetmapUpdater is the required baseline (full replacements); NetmapDeltaUpdater is an optional extension that implementations may satisfy to receive incremental peer mutations instead of full replacements, reducing CPU and allocation cost during large netmap convergence events.
  • Implementations: LocalBackend implements both; the control client checks at runtime via type assertion whether the NetmapDeltaUpdater path is available.
  • Design quality: Excellent example of the optional-interface pattern — the core contract is minimal (NetmapUpdater) and the performance extension is strictly opt-in via NetmapDeltaUpdater. The ok bool return from UpdateNetmapDelta lets the implementation signal “I couldn’t apply this delta; please send a full map instead,” gracefully falling back without panic.

ipn.StateStore#

  • Package: tailscale.com/ipn
  • File: ipn/store.go:91
  • Methods:
    • ReadState(id StateKey) ([]byte, error)
    • WriteState(id StateKey, bs []byte) error
  • Purpose: Abstracts durable key-value storage for the daemon’s persistent state: machine key, user profiles, current profile, server mode start key, and feature-specific state (e.g., Taildrop received marker). Implementations must be safe for concurrent use.
  • Implementations: File store (default on Linux/macOS), Kubernetes Secret store (ipn/store/kubestore), AWS SSM Parameter Store (ipn/store/awsstore), memory store (tests, tsnet).
  • Design quality: Deliberately minimal — just two methods over opaque byte blobs keyed by StateKey (a typed string). No transactions, no batching. The companion WriteState helper function adds write-if-changed semantics at the call site. Two optional extension interfaces add capabilities without modifying the base: StateStoreDialerSetter (inject a custom dialer for network-backed stores) and EncryptedStateStore (marker interface for at-rest encryption, checked before allowing plaintext export).

ipnext.Extension#

  • Package: tailscale.com/ipn/ipnext
  • File: ipn/ipnext/ipnext.go:38
  • Methods:
    • Name() string
    • Init(Host) error
    • Shutdown() error
  • Purpose: Lifecycle interface for optional subsystems that augment LocalBackend. Extensions register via RegisterExtension (called from init() functions), are instantiated when LocalBackend starts, and are torn down in reverse order on shutdown. An extension that returns SkipExtension from its factory is silently omitted rather than causing a fatal error, allowing platform-conditional features.
  • Implementations: SSH server extension, audit logging extension, app connector extension, desktop session manager. Registered via blank imports in build-specific cmd/tailscaled files.
  • Design quality: Minimal lifecycle interface (3 methods) that avoids polluting LocalBackend with optional feature logic. The SkipExtension sentinel error is an elegant convention for “not supported on this platform” — avoids boolean flags and allows the factory function to inspect any condition it needs. Paired with ipnext.Host (the rich API the extension receives), this forms a clean plugin boundary.

ipnext.Host#

  • Package: tailscale.com/ipn/ipnext
  • File: ipn/ipnext/ipnext.go:181
  • Methods:
    • Extensions() ExtensionServices
    • Profiles() ProfileServices
    • AuditLogger() ipnauth.AuditLogFunc
    • Hooks() *Hooks
    • SendNotifyAsync(ipn.Notify)
    • NodeBackend() NodeBackend
    • AuthReconfigAsync()
  • Purpose: The controlled API surface that LocalBackend exposes to Extension instances. Extensions interact with the backend exclusively through Host (read state, register callbacks, trigger async actions). This prevents extensions from calling arbitrary LocalBackend methods while holding locks, avoiding deadlocks.
  • Implementations: LocalBackend’s internal extensionHost type (unexported).
  • Design quality: Thoughtfully designed to enforce safe concurrency semantics. Actions initiated by extensions are explicitly async (Async suffix), while callbacks provided by the host are synchronous. The separation of ExtensionServices, ProfileServices, and NodeBackend into distinct sub-interfaces (returned by Extensions(), Profiles(), NodeBackend()) demonstrates Interface Segregation Principle applied within a richer interface.

ipnauth.Actor#

  • Package: tailscale.com/ipn/ipnauth
  • File: ipn/ipnauth/actor.go:24
  • Methods:
    • UserID() ipn.WindowsUserID
    • Username() (string, error)
    • ClientID() (ClientID, ok bool)
    • Context() context.Context
    • CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogFn AuditLogFunc) error
    • IsLocalSystem() bool (deprecated)
    • IsLocalAdmin(operatorUID string) bool (deprecated)
  • Purpose: Represents the security principal performing a LocalBackend operation — typically the OS user on whose behalf a LocalAPI request is being executed. Enables permission checking, audit logging, and OS-level access control decisions. The included Context() provides request-scoped cancellation and metadata.
  • Implementations: Platform-specific implementations (Windows token-based, Unix peer-credential-based), test doubles.
  • Design quality: The inline deprecation comments on IsLocalSystem and IsLocalAdmin honestly document a permissions model in transition (corp#18342). The CheckProfileAccess method centralizes access control rather than scattering if user.IsAdmin() checks. The optional ActorCloser interface (checked via type assertion) allows platform-specific resource cleanup without polluting the base interface.

net/dns.OSConfigurator#

  • Package: tailscale.com/net/dns
  • File: net/dns/osconfig.go:20
  • Methods:
    • SetDNS(cfg OSConfig) error
    • SupportsSplitDNS() bool
    • GetBaseConfig() (OSConfig, error)
    • Close() error
  • Purpose: Abstracts OS-level DNS configuration across platforms (Linux resolvconf/systemd-resolved/NetworkManager, macOS, Windows, Android). Allows the DNS manager to apply split-DNS or full-resolver configuration without knowing the underlying mechanism.
  • Implementations: directManager (reads/writes /etc/resolv.conf directly), nmManager (NetworkManager DBus), resolvManager (systemd-resolved DBus), windowsManager, osxManager, and more — one per supported OS configuration backend.
  • Design quality: The SupportsSplitDNS() capability query is a pragmatic ISP compromise — callers branch on it rather than having two separate interfaces. The GetBaseConfig() contract (must return the tailscale-free base config even after SetDNS has been called) is carefully specified and important for correctness on platforms where Tailscale overlays rather than replaces DNS.

ipnlocal.SSHServer#

  • Package: tailscale.com/ipn/ipnlocal
  • File: ipn/ipnlocal/local.go:122
  • Methods:
    • HandleSSHConn(net.Conn) error
    • NumActiveConns() int
    • OnPolicyChange()
    • Shutdown()
  • Purpose: Decoupling interface that allows LocalBackend to interact with the optional SSH server (ssh/tailssh) without a direct import. The SSH server registers itself at init time via RegisterNewSSHServer (a newSSHServerFunc hook). LocalBackend uses the interface at runtime when a new SSH connection arrives or when SSH policy changes.
  • Implementations: tailssh.server (the full Tailscale SSH server), conditionally linked.
  • Design quality: Clean separation — LocalBackend never imports ssh/tailssh. The 4-method interface covers the full lifecycle: accept connections, query active connection count (for graceful drain), react to policy changes, and shut down. This predates the more general ipnext.Extension system and represents an earlier approach to optional feature integration.

Interface patterns#

  • Size distribution: Interfaces span from 1 method (controlclient.Observer) to ~19 methods (wgengine.Engine). The median is around 4-6 methods. Most non-trivial public interfaces cluster in the 3–8 method range.
  • Embedding: The derp.Conn interface embeds io.WriteCloser. ActorCloser is an optional extension of Actor (not via embedding, but via type assertion). NetmapDeltaUpdater extends NetmapUpdater through a separate optional interface rather than embedding. AlgorithmSigner in the SSH tempfork embeds Signer.
  • Implicit satisfaction: Consumer-defined interfaces are the dominant pattern. Interfaces like controlclient.Observer, ipnext.Extension, ipn.StateStore, and wgengine.Engine are defined in the package that depends on the abstraction, not in the package that implements it. This is idiomatic Go — the consumer specifies what it needs, and implementations satisfy implicitly.
  • Stdlib interfaces used: io.WriteCloser (embedded in derp.Conn), net.Conn semantics mirrored in derp.Conn (SetDeadline family), context.Context pervasively carried through Actor.Context().

Key abstractions#

  1. wgengine.Engine — The most architecturally load-bearing interface. It is the seam between LocalBackend (policy/control plane) and the actual WireGuard data plane. Everything above it can be tested without a real kernel TUN device; everything below it touches real network I/O.

  2. controlclient.Client + controlclient.Observer — Together these two interfaces define the full control plane protocol boundary. Client is the command surface (push state to control, drive auth); Observer is the event surface (receive netmap updates from control). Their separation keeps LocalBackend from needing to understand HTTP/Noise internals.

  3. ipn.StateStore — Two-method interface that enables Tailscale to run in radically different environments (container, Kubernetes, AWS, desktop). The minimal contract (opaque byte blobs by key) is what makes it possible to implement in SSM, Kubernetes secrets, or a plain file without any domain coupling.

  4. ipnext.Extension + ipnext.Host — The newest and most architecturally significant pair. These represent Tailscale’s pivot away from ad-hoc feature hooks (like SSHServer/RegisterNewSSHServer) toward a principled extension model with explicit lifecycle, safe concurrency semantics, and controlled API access. Future optional features are expected to use this system.

  5. ipnauth.Actor — The security identity interface that threads through LocalAPI request handling. By making the security principal explicit and injectable, Tailscale can apply consistent permission checking across all LocalAPI operations, support multi-user scenarios (Windows), and audit actions through a consistent path.

Interface-driven extensibility#

Tailscale uses interfaces for extensibility at three distinct levels:

1. Platform adaptation (OS backends): net/dns.OSConfigurator has ~8 implementations for different DNS configuration backends. ipn.StateStore has 4+ implementations for different storage backends. This is the classic strategy pattern — the same daemon binary adapts to radically different operating environments.

2. Optional feature integration (the hook/register pattern): The older approach, exemplified by ipnlocal.SSHServer + RegisterNewSSHServer, ties an optional feature to a typed function variable that the feature’s init() sets. LocalBackend checks whether the hook is set before calling it. This is lightweight but ad-hoc.

3. Structured extensions (ipnext.Extension): The newer approach introduced a formal extension lifecycle (Init, Shutdown) with a rich but controlled API surface (ipnext.Host). Extensions register via ipnext.RegisterExtension (called from init()) and interact with LocalBackend exclusively through Host, which enforces safe concurrency. The Hooks struct within Host uses feature.Hook[Func] typed slots for specific callback registrations (profile state change, self-node change, packet filter extension, etc.), preventing the accidental double-set footguns that plague global function variables.

The evolution from approach 2 to approach 3 is visible in the codebase: SSHServer uses the older pattern, while the audit logging and app connector features use ipnext.Extension. The architecture is mid-migration.