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 layercmd/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-safe Set(v any) / Get() accessors. Created first in main() 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 (via wgengine), 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 to ipnext.Extension plugins)
  • 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 NetworkMap updates to LocalBackend via callback.
  • Key types: Client (interface with Login, 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 Engine interface that abstracts the WireGuard data plane. The primary implementation (UserspaceEngine) wraps wireguard-go and manages TUN device configuration, packet filtering, and peer routing. A Watchdog wrapper 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/derphttp sub-package provides the HTTP(S)-based client used by MagicSock; derp/derpserver is 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.Server listens on the tailscaled Unix socket and handles per-connection HTTP multiplexing with actor-based authentication. It holds a reference to LocalBackend and passes requests to localapi.Handler. The localapi package implements the /localapi/v0/ REST API used by the tailscale CLI 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. buildfeatures exposes boolean compile-time constants (e.g., HasSSH, HasNetstack, HasDebug, HasTPM) generated as _enabled.go/_disabled.go pairs — exactly one file per build tag set is compiled in. The feature package provides the Hook[Func] generic type for runtime registration of optional functionality via init() functions; code paths check hook.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)#

  1. controlclient.Auto receives an updated NetworkMap from the coordination server via HTTPS long-poll.
  2. Calls LocalBackend.SetControlClientStatus() with the new map.
  3. LocalBackend acquires its mutex, updates internal state (peers, routes, DNS config, firewall rules).
  4. Calls wgengine.Reconfig(wgcfg, routerCfg, dnsCfg) to push updated WireGuard peer keys and routes to the engine.
  5. UserspaceEngine.Reconfig() reconfigures the underlying wireguard-go device, updates packet filter, and notifies MagicSock of new peer endpoints.
  6. MagicSock begins probing new peer endpoints via STUN/Disco.
  7. DNS manager (net/dns) applies new split-DNS configuration to the OS resolver.

Typical CLI request (user → daemon → response)#

  1. tailscale status → connects to tailscaled Unix socket via safesocket.
  2. Sends HTTP GET /localapi/v0/status.
  3. ipnserver.Server authenticates the caller (peer credentials on Unix socket), dispatches to localapi.Handler.
  4. localapi.Handler calls LocalBackend.Status(), serializes the result as JSON.
  5. Response flows back over the Unix socket to the CLI.

Packet path (outbound)#

  1. Application writes to TUN device (tailscale0).
  2. tstun.Wrapper inspects the packet (filter, capture sink).
  3. wireguard-go encrypts it for the destination peer.
  4. magicsock.Conn sends the encrypted UDP packet on the best available path: direct UDP (if STUN-discovered path exists) or DERP relay (if direct path is blocked).
  5. 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:

  1. main(): Parses flag arguments, handles platform-specific sub-commands (install-system-daemon, be-child), calls run().
  2. run():
    • Creates tsd.NewSystem() — allocates the DI container with a fresh eventbus.Bus and health.Tracker.
    • Optionally loads conffile.Config for declarative mode.
    • Creates netmon.Monitor and registers it into sys.
    • Initializes logpolicy (structured log upload to log.tailscale.io).
    • Calls startIPNServer().
  3. 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 via srv.SetLocalBackend(lb).
    • Calls srv.Run(ctx, ln) which blocks serving connections.
  4. getLocalBackend() (runs in goroutine):
    • Creates tsdial.Dialer, registers into sys.
    • Calls createEngine()tryEngine():
      • Allocates tstun.Wrapper (TUN device), registers into sys.
      • Creates router.Router (OS kernel route manager), registers into sys.
      • Creates dns.OSConfigurator, registers into sys.
      • Calls wgengine.NewUserspaceEngine(), wraps with wgengine.NewWatchdog(), registers into sys.
    • Optionally creates netstack via hookNewNetstack feature hook.
    • Creates store.New() (state store: file, kube, AWS SSM, mem), registers into sys.
    • Calls ipnlocal.NewLocalBackend(logf, logID, sys, loginFlags) — the LocalBackend pulls all its dependencies from sys.
    • Calls lb.Start() to begin the state machine.

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 flag package. 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: envknob package wraps os.Getenv with lazy evaluation and enforces that env checks don’t happen in init(). Key knobs: TS_LOG_VERBOSITY, PORT, TS_DEBUG_*, TS_BE_CLI.
  • Disk config file: conffile package supports a declarative JSON config file (path via --config or vm:user-data for EC2). Puts the node in “managed” mode where policy overrides interactive prefs.
  • Runtime policy: util/syspolicy/policyclient reads system policy (Group Policy on Windows, MDM profiles on macOS, environment on Linux) for enterprise management.
  • Feature flags (compile-time): feature/buildfeatures boolean constants (HasSSH, HasNetstack, HasDebug, HasTPM, HasTaildrop, etc.) — one _enabled.go/_disabled.go file 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 their init() functions (e.g., ssh/tailssh registers newSSHServer; netstack registers hookNewNetstack). The core code checks hook.GetOk() and branches accordingly.

Key design decisions#

  1. tsd.System as explicit DI container. Rather than passing individual subsystems as constructor arguments (which would mean dozens of parameters) or using a global registry, tsd.System is 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).

  2. feature.Hook for optional module registration. Instead of interface-based plugins or build-tag //go:build guards on call sites, Tailscale uses a generic Hook[Func] type that is set at most once (panics on double-set). Optional features (SSH server, netstack, web client, outbound proxy) register themselves in init() via blank imports, and core code gates on hook.GetOk(). This is more type-safe than interface{} hooks and more explicit than build-tag littering.

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

  4. tailcfg as the shared language. The tailcfg package 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, both controlclient (which produces them) and LocalBackend / wgengine (which consume them) can import tailcfg without circular dependencies.

  5. Flat public namespace as a library commitment. The use of tailscale.com as the module path with almost everything exported (minimal internal/) is an architectural statement: Tailscale is a library as much as an application. The tsnet package (embedded Tailscale for user applications) and client/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.