Tailscale — Architecture#
Architectural style#
Layered daemon with service-container dependency injection and a feature-hook extension model.
Tailscale is not a microservices architecture nor a classical monolith. It is a single long-running daemon (tailscaled) composed of distinct, independently testable subsystems wired together by an explicit dependency container (tsd.System). The daemon is augmented by a large family of satellite binaries (CLI, DERP relay, Kubernetes operator, embedded library, etc.) that share the same module and many of the same packages.
The architecture is best described as layered with a central orchestrator:
- Bottom layer — networking primitives (
net/*,disco/,derp/) - Connectivity layer — WireGuard engine + MagicSock (
wgengine/,wgengine/magicsock/) - Control plane client layer — coordination server protocol (
control/controlclient/) - Orchestration layer — the central state machine (
ipn/ipnlocal/) - IPC layer — socket server + HTTP API (
ipn/ipnserver/,ipn/localapi/) - Entry layer —
cmd/tailscaled(binary bootstrap and DI wiring)
A notable cross-cutting mechanism is the feature.Hook pattern: optional subsystems (SSH server, netstack, Taildrop, web client, etc.) register themselves at init() time via typed function hooks rather than direct imports, enabling dead-code elimination for lean builds without littering the code with #ifdef-equivalents.
Component diagram (textual)#
┌─────────────────────────────────────────────────────────────────────┐
│ cmd/tailscaled (binary bootstrap) │
│ - stdlib flag parsing │
│ - feature.Hook registration via blank imports │
│ - creates tsd.System, wires all subsystems │
└──────────────────────────┬──────────────────────────────────────────┘
│ owns *tsd.System
▼
┌─────────────────────────────────────────────────────────────────────┐
│ tsd.System (dependency injection container) │
│ Bus · NetMon · Engine · MagicSock · Tun · Router · DNSManager │
│ Dialer · StateStore · Netstack · HealthTracker · PolicyClient │
└────────┬──────────────────────────────────────────────────────────┬─┘
│ owns │
▼ ▼
┌─────────────────────────────┐ ┌────────────────────────────┐
│ ipn/ipnlocal.LocalBackend │◄─────────│ ipn/ipnserver.Server │
│ (central state machine) │ sets lb │ (Unix socket listener) │
│ - manages profiles/prefs │ │ - per-connection HTTP mux │
│ - drives control client │ │ - auth/actor model │
│ - applies network maps │ └───────────┬────────────────┘
│ - configures wgengine │ │ HTTP
│ - manages DNS, routes │ ▼
│ - extension host (ipnext) │ ┌────────────────────────────┐
└────────┬────────────────────┘ │ ipn/localapi.Handler │
│ │ (REST endpoint dispatch) │
├──► control/controlclient └────────────────────────────┘
│ (long-poll HTTPS to
│ coordination server)
│
├──► wgengine.Engine (interface)
│ └── wgengine/magicsock.Conn
│ ├── direct UDP paths
│ └── DERP relay fallback
│ └── derp/derphttp
│
└──► net/dns.Manager
net/netmon.Monitor
ipn/store (StateStore)Core components#
tsd.System#
- Package:
tailscale.com/tsd - Responsibility: Explicit dependency-injection container holding all daemon subsystems as
SubSystem[T]slots. Provides type-safeSet(v any)/Get()accessors. Created first inmain()and passed down; subsystems register themselves into it during initialization. Introduced in 2023 to unify initialization across tailscaled, Windows service, macOS GUI, tsnet, and WASM. - Key types:
System,SubSystem[T](generic slot),NetstackImpl(interface for circular-dependency avoidance) - Dependencies: Nearly all subsystem packages (by design — it is the wiring layer); should not itself be imported by low-level packages.
ipn/ipnlocal.LocalBackend#
- Package:
tailscale.com/ipn/ipnlocal - Responsibility: The heart of the daemon. Implements the overall state machine for the Tailscale node. Bridges the cloud control plane (via
controlclient), the network data plane (viawgengine), and user-facing frontends (via the IPC server). Manages user profiles, preferences, authentication state, network map application, DNS configuration, packet-filter updates, SSH server lifecycle, extension host, and more. - Key types:
LocalBackend(central struct, ~300 fields),nodeBackend(per-profile inner state),SSHServer(interface for conditionally-linked SSH),ExtensionHost(bridge toipnext.Extensionplugins) - Dependencies:
tsd.System,wgengine.Engine,control/controlclient.Client,ipn.StateStore,net/dns,tailcfg, virtually all domain packages.
control/controlclient#
- Package:
tailscale.com/control/controlclient - Responsibility: Manages the authenticated HTTPS long-poll connection to Tailscale’s coordination server. Handles node registration, authentication flows, network map streaming, and TKA (Tailscale Key Authority) head updates. Delivers
NetworkMapupdates toLocalBackendvia callback. - Key types:
Client(interface withLogin,Logout,SetPaused,SetHostinfo,UpdateEndpoints),Auto(primary implementation),Direct(single-request client) - Dependencies:
control/controlbase(Noise session layer),control/controlhttp,tailcfg,tka
wgengine#
- Package:
tailscale.com/wgengine - Responsibility: Defines the
Engineinterface that abstracts the WireGuard data plane. The primary implementation (UserspaceEngine) wraps wireguard-go and manages TUN device configuration, packet filtering, and peer routing. AWatchdogwrapper adds liveness monitoring. - Key types:
Engine(interface:Reconfig,SetFilter,PeerForIP,SetStatusCallback,Done),UserspaceEngine,Watchdog,Config,BIRDClient - Dependencies:
wgengine/magicsock,wgengine/router,wgengine/filter,net/tstun,wireguard-go
wgengine/magicsock#
- Package:
tailscale.com/wgengine/magicsock - Responsibility: The most critical and complex package. Implements WireGuard’s UDP transport with multi-path capabilities: it maintains multiple endpoint candidates per peer (direct UDP via STUN-discovered addresses, relayed via DERP), performs continuous path probing, and selects the best available path. Implements the Disco protocol for peer-to-peer handshake and path establishment. Falls back seamlessly to DERP when direct paths are blocked by NAT.
- Key types:
Conn(main struct), endpoint management, DERP client management - Dependencies:
derp/derphttp,disco,net/netcheck,net/portmapper,tailcfg,types/key
derp#
- Package:
tailscale.com/derp - Responsibility: Implements Tailscale’s in-house Designated Encrypted Relay for Packets (DERP) protocol. DERP is a low-latency, end-to-end-encrypted relay used when direct WireGuard paths are not possible. The
derp/derphttpsub-package provides the HTTP(S)-based client used by MagicSock;derp/derpserveris the full relay server. - Key types:
Client,Server,ReceivedMessage - Dependencies:
types/key,net/netmon,tailcfg(for DERPMap)
ipn/ipnserver + ipn/localapi#
- Package:
tailscale.com/ipn/ipnserver,tailscale.com/ipn/localapi - Responsibility:
ipnserver.Serverlistens on the tailscaled Unix socket and handles per-connection HTTP multiplexing with actor-based authentication. It holds a reference toLocalBackendand passes requests tolocalapi.Handler. Thelocalapipackage implements the/localapi/v0/REST API used by thetailscaleCLI and GUI frontends. - Key types:
ipnserver.Server,localapi.Handler,LocalAPIHandler(func type for route dispatch) - Dependencies:
ipnlocal.LocalBackend,ipnauth,safesocket
feature / feature/buildfeatures#
- Package:
tailscale.com/feature,tailscale.com/feature/buildfeatures - Responsibility: Two-level feature system.
buildfeaturesexposes boolean compile-time constants (e.g.,HasSSH,HasNetstack,HasDebug,HasTPM) generated as_enabled.go/_disabled.gopairs — exactly one file per build tag set is compiled in. Thefeaturepackage provides theHook[Func]generic type for runtime registration of optional functionality viainit()functions; code paths checkhook.GetOk()and proceed only when the optional module is linked. - Key types:
Hook[Func](typed function slot, set-once),buildfeatures.HasXxx(bool constants) - Dependencies: Minimal (only
testenv); designed to be imported by any package
Data flow#
Typical network map update (control plane → data plane)#
controlclient.Autoreceives an updatedNetworkMapfrom the coordination server via HTTPS long-poll.- Calls
LocalBackend.SetControlClientStatus()with the new map. LocalBackendacquires its mutex, updates internal state (peers, routes, DNS config, firewall rules).- Calls
wgengine.Reconfig(wgcfg, routerCfg, dnsCfg)to push updated WireGuard peer keys and routes to the engine. UserspaceEngine.Reconfig()reconfigures the underlying wireguard-go device, updates packet filter, and notifies MagicSock of new peer endpoints.MagicSockbegins probing new peer endpoints via STUN/Disco.- DNS manager (
net/dns) applies new split-DNS configuration to the OS resolver.
Typical CLI request (user → daemon → response)#
tailscale status→ connects to tailscaled Unix socket viasafesocket.- Sends HTTP
GET /localapi/v0/status. ipnserver.Serverauthenticates the caller (peer credentials on Unix socket), dispatches tolocalapi.Handler.localapi.HandlercallsLocalBackend.Status(), serializes the result as JSON.- Response flows back over the Unix socket to the CLI.
Packet path (outbound)#
- Application writes to TUN device (
tailscale0). tstun.Wrapperinspects the packet (filter, capture sink).- wireguard-go encrypts it for the destination peer.
magicsock.Connsends the encrypted UDP packet on the best available path: direct UDP (if STUN-discovered path exists) or DERP relay (if direct path is blocked).- On the remote peer, MagicSock receives the packet, wireguard-go decrypts it, and injects it into that peer’s TUN device.
Initialization / Bootstrap#
The startup sequence in cmd/tailscaled/tailscaled.go:
main(): Parsesflagarguments, handles platform-specific sub-commands (install-system-daemon,be-child), callsrun().run():- Creates
tsd.NewSystem()— allocates the DI container with a fresheventbus.Busandhealth.Tracker. - Optionally loads
conffile.Configfor declarative mode. - Creates
netmon.Monitorand registers it intosys. - Initializes
logpolicy(structured log upload to log.tailscale.io). - Calls
startIPNServer().
- Creates
startIPNServer():- Opens Unix socket via
safesocket.Listen(). - Creates
ipnserver.New()(the socket server, no backend yet). - Spawns a goroutine that calls
getLocalBackend()to build the full backend asynchronously; signals the server when ready viasrv.SetLocalBackend(lb). - Calls
srv.Run(ctx, ln)which blocks serving connections.
- Opens Unix socket via
getLocalBackend()(runs in goroutine):- Creates
tsdial.Dialer, registers intosys. - Calls
createEngine()→tryEngine():- Allocates
tstun.Wrapper(TUN device), registers intosys. - Creates
router.Router(OS kernel route manager), registers intosys. - Creates
dns.OSConfigurator, registers intosys. - Calls
wgengine.NewUserspaceEngine(), wraps withwgengine.NewWatchdog(), registers intosys.
- Allocates
- Optionally creates netstack via
hookNewNetstackfeature hook. - Creates
store.New()(state store: file, kube, AWS SSM, mem), registers intosys. - Calls
ipnlocal.NewLocalBackend(logf, logID, sys, loginFlags)— theLocalBackendpulls all its dependencies fromsys. - Calls
lb.Start()to begin the state machine.
- Creates
Dependency injection approach: Manual wiring via the tsd.System container. No codegen DI framework (no Wire, Dig, or Fx). The sys.Set(v) / sys.SomeField.Get() pattern is the convention. LocalBackend receives the whole *tsd.System and reaches into it for its dependencies.
Configuration#
- CLI flags: stdlib
flagpackage. Flags:--tun,--port,--state,--statedir,--socket,--config,--verbose,--no-logs-no-support, and feature-gated flags (--debug,--bird-socket,--encrypt-state,--hardware-attestation). - Environment variables:
envknobpackage wrapsos.Getenvwith lazy evaluation and enforces that env checks don’t happen ininit(). Key knobs:TS_LOG_VERBOSITY,PORT,TS_DEBUG_*,TS_BE_CLI. - Disk config file:
conffilepackage supports a declarative JSON config file (path via--configorvm:user-datafor EC2). Puts the node in “managed” mode where policy overrides interactive prefs. - Runtime policy:
util/syspolicy/policyclientreads system policy (Group Policy on Windows, MDM profiles on macOS, environment on Linux) for enterprise management. - Feature flags (compile-time):
feature/buildfeaturesboolean constants (HasSSH,HasNetstack,HasDebug,HasTPM,HasTaildrop, etc.) — one_enabled.go/_disabled.gofile pair per feature, selected by Go build tags. This is how the same codebase produces both a lean container image and a full desktop client. - Feature hooks (link-time): Optional packages register themselves via
feature.Hook.Set()in theirinit()functions (e.g.,ssh/tailsshregistersnewSSHServer; netstack registershookNewNetstack). The core code checkshook.GetOk()and branches accordingly.
Key design decisions#
tsd.Systemas explicit DI container. Rather than passing individual subsystems as constructor arguments (which would mean dozens of parameters) or using a global registry,tsd.Systemis a typed, set-once struct that acts as a “service locator” for daemon-wide subsystems. It’s simpler than a DI framework, avoids reflection at runtime, and makes the dependency graph explicit and auditable. The 2023 redesign documented in the package comment was specifically motivated by the complexity of wiring the same subsystems for five different host environments (Linux daemon, Windows service, macOS GUI, tsnet embed, WASM).feature.Hookfor optional module registration. Instead of interface-based plugins or build-tag//go:buildguards on call sites, Tailscale uses a genericHook[Func]type that is set at most once (panics on double-set). Optional features (SSH server, netstack, web client, outbound proxy) register themselves ininit()via blank imports, and core code gates onhook.GetOk(). This is more type-safe thaninterface{}hooks and more explicit than build-tag littering.MagicSock: multi-path connectivity with seamless DERP fallback. The architectural bet is that WireGuard’s encryption should be decoupled from its transport. WireGuard only sees a single UDP socket per peer from wireguard-go’s perspective, but MagicSock internally manages multiple UDP endpoints per peer and switches between direct paths and DERP relay without any WireGuard reconfiguration. This allows Tailscale to work everywhere (even through symmetric NAT and corporate firewalls) with minimal latency impact when direct paths are available.
tailcfgas the shared language. Thetailcfgpackage defines the wire format types used between the coordination server and daemon (NetworkMap,Node,DERPMap,Hostinfo,Endpoint). By keeping these types in a stable, minimal package with few dependencies, bothcontrolclient(which produces them) andLocalBackend/wgengine(which consume them) can importtailcfgwithout circular dependencies.Flat public namespace as a library commitment. The use of
tailscale.comas the module path with almost everything exported (minimalinternal/) is an architectural statement: Tailscale is a library as much as an application. Thetsnetpackage (embedded Tailscale for user applications) andclient/tailscale(Go API client for the coordination API) are first-class library APIs maintained with stable import paths. This design decision constrains refactoring but dramatically increases the ecosystem value.