Tailscale — Interfaces#
Interface catalog#
wgengine.Engine#
- Package:
tailscale.com/wgengine - File:
wgengine/engine.go(not present in this checkout; interface reconstructed from mock inipn/ipnlocal/state_test.go:1894) - Methods:
Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config) errorConfig() *wgcfg.ConfigRouterConfig() *router.ConfigDNSConfig() *dns.ConfigPeerForIP(netip.Addr) (PeerForIP, bool)GetFilter() *filter.FilterSetFilter(f *filter.Filter)GetJailedFilter() *filter.FilterSetJailedFilter(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 inipnlocal/state_test.go). - Design quality: Broad (19 methods), but necessarily so given the data plane’s many responsibilities. The filter methods come in
SetFilter/SetJailedFilterpairs, hinting at the sandboxed-subnet-router model. TheDone()channel follows the standard Go shutdown idiom. The callback-basedPingsignals 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) errorSetPaused(bool)AuthCantContinue() boolSetHostinfo(*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
LocalBackendto drive authentication flows, push updated host and network information to control, and receive the resultingNetworkMapvia the companionObservercallback 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
SetPausedmethod is a pragmatic addition to minimize unnecessary network activity when the machine is idle. The manySet*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 aClientreference to allow stale-client detection (if the client delivering the status is no longer the current one,LocalBackendignores the update). - Implementations:
LocalBackend(primary consumer). - Design quality: Single-method — excellent ISP adherence. The inclusion of the
Clientparameter 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.
NetmapUpdateris the required baseline (full replacements);NetmapDeltaUpdateris 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:
LocalBackendimplements both; the control client checks at runtime via type assertion whether theNetmapDeltaUpdaterpath 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 viaNetmapDeltaUpdater. Theok boolreturn fromUpdateNetmapDeltalets 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 companionWriteStatehelper 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) andEncryptedStateStore(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() stringInit(Host) errorShutdown() error
- Purpose: Lifecycle interface for optional subsystems that augment
LocalBackend. Extensions register viaRegisterExtension(called frominit()functions), are instantiated whenLocalBackendstarts, and are torn down in reverse order on shutdown. An extension that returnsSkipExtensionfrom 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/tailscaledfiles. - Design quality: Minimal lifecycle interface (3 methods) that avoids polluting
LocalBackendwith optional feature logic. TheSkipExtensionsentinel 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 withipnext.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() ExtensionServicesProfiles() ProfileServicesAuditLogger() ipnauth.AuditLogFuncHooks() *HooksSendNotifyAsync(ipn.Notify)NodeBackend() NodeBackendAuthReconfigAsync()
- Purpose: The controlled API surface that
LocalBackendexposes toExtensioninstances. Extensions interact with the backend exclusively throughHost(read state, register callbacks, trigger async actions). This prevents extensions from calling arbitraryLocalBackendmethods while holding locks, avoiding deadlocks. - Implementations:
LocalBackend’s internalextensionHosttype (unexported). - Design quality: Thoughtfully designed to enforce safe concurrency semantics. Actions initiated by extensions are explicitly async (
Asyncsuffix), while callbacks provided by the host are synchronous. The separation ofExtensionServices,ProfileServices, andNodeBackendinto distinct sub-interfaces (returned byExtensions(),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.WindowsUserIDUsername() (string, error)ClientID() (ClientID, ok bool)Context() context.ContextCheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogFn AuditLogFunc) errorIsLocalSystem() bool(deprecated)IsLocalAdmin(operatorUID string) bool(deprecated)
- Purpose: Represents the security principal performing a
LocalBackendoperation — 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 includedContext()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
IsLocalSystemandIsLocalAdminhonestly document a permissions model in transition (corp#18342). TheCheckProfileAccessmethod centralizes access control rather than scatteringif user.IsAdmin()checks. The optionalActorCloserinterface (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) errorSupportsSplitDNS() boolGetBaseConfig() (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.confdirectly),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. TheGetBaseConfig()contract (must return the tailscale-free base config even afterSetDNShas 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) errorNumActiveConns() intOnPolicyChange()Shutdown()
- Purpose: Decoupling interface that allows
LocalBackendto interact with the optional SSH server (ssh/tailssh) without a direct import. The SSH server registers itself at init time viaRegisterNewSSHServer(anewSSHServerFunchook).LocalBackenduses 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 —
LocalBackendnever importsssh/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 generalipnext.Extensionsystem 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.Conninterface embedsio.WriteCloser.ActorCloseris an optional extension ofActor(not via embedding, but via type assertion).NetmapDeltaUpdaterextendsNetmapUpdaterthrough a separate optional interface rather than embedding.AlgorithmSignerin the SSH tempfork embedsSigner. - Implicit satisfaction: Consumer-defined interfaces are the dominant pattern. Interfaces like
controlclient.Observer,ipnext.Extension,ipn.StateStore, andwgengine.Engineare 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 inderp.Conn),net.Connsemantics mirrored inderp.Conn(SetDeadline family),context.Contextpervasively carried throughActor.Context().
Key abstractions#
wgengine.Engine— The most architecturally load-bearing interface. It is the seam betweenLocalBackend(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.controlclient.Client+controlclient.Observer— Together these two interfaces define the full control plane protocol boundary.Clientis the command surface (push state to control, drive auth);Observeris the event surface (receive netmap updates from control). Their separation keepsLocalBackendfrom needing to understand HTTP/Noise internals.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.ipnext.Extension+ipnext.Host— The newest and most architecturally significant pair. These represent Tailscale’s pivot away from ad-hoc feature hooks (likeSSHServer/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.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.