Tailscale — API Surface#
API types#
Tailscale exposes four distinct API layers:
- Local IPC API — Unix socket HTTP REST API (
/localapi/v0/) used by thetailscaleCLI and GUI frontends to talk to the runningtailscaleddaemon. - CLI — The
tailscalebinary, using theffcliframework (peterbourgon/ff). - Coordination API client — Go library (
client/tailscale) for the remote Tailscale cloud control plane REST API atapi.tailscale.com/api/v2/. - Library API (tsnet-style) — The
client/localpackage wraps the Local IPC API as a typed Go library;safewebandipn/serveexpose per-node HTTP serving capability for user applications.
There are no gRPC services and no .proto files in the repository. All IPC is plain HTTP over Unix sockets; all cloud coordination uses custom HTTPS long-polling, not gRPC.
REST/HTTP API — Local API (/localapi/v0/)#
Router#
Custom, statically registered map (var handler = map[string]LocalAPIHandler). Routes are exact-match strings keyed by the path suffix after /localapi/v0/. Some entries are prefix-match (trailing slash). All handlers are bound to (*Handler).serveXxx methods.
Route registration#
Registration happens in two layers:
- Static map in
ipn/localapi/localapi.go(compiled-in always): core routes likestatus,prefs,ping,whois,profiles/,derpmap. init()-timeRegister()calls gated onbuildfeatures.HasXxxbooleans: optional features (SSH dial, TKA, Drive, app connectors, metrics, update, etc.) only register their routes if their build feature is compiled in. This means the route table is lean in embedded or restricted builds.
Middleware / auth chain#
ipnserver.Server.serveHTTP → authentication (Unix socket peer-credentials check, ipnauth.Actor) → per-connection localapi.Handler. No classical middleware stack; authentication happens once per connection at the server level. CSRF protection is absent for the Unix socket API (physical access to the socket is the boundary); CSRF is added only for the web client (gorilla/csrf).
Authentication#
Authentication is done by inspecting Unix socket peer credentials (UID/GID) on Linux/macOS. On Windows, named pipe identity is used. Users identified as the operator user (configured via prefs.OperatorUser) or root get write access; others get read-only access. The ipnauth.Actor interface encapsulates the caller identity and drives permission checks.
Key endpoints (all under /localapi/v0/)#
| Endpoint | Methods | Purpose |
|---|---|---|
status | GET | Node status — peers, IPs, relay info, login state |
prefs | GET, PATCH | Read/update preferences (routes, exit nodes, SSH, etc.) |
check-prefs | GET | Validate preference change without applying |
profiles/ | GET, POST, DELETE | Multi-profile management (switch tailnets) |
ping | POST | Ping a peer by IP; returns latency and path info |
whois | GET | Resolve IP/port → node identity + capabilities |
derpmap | GET | Current DERP relay map |
login-interactive | POST | Initiate interactive browser-based login |
logout | POST | Log out of current profile |
start | POST | Start the daemon (resume after Stop) |
shutdown | POST | Graceful daemon shutdown |
watch-ipn-bus | GET | Long-poll SSE stream of ipn.Notify events |
dial | POST | Proxy: dial a Tailscale peer (for SSH/outbound proxy) |
cert/ | GET | TLS certificate provisioning for tailnet domains |
serve-config | GET, POST | Configure tailscale serve and Funnel |
bugreport | GET | Generate and upload a bug report |
pprof | GET | Expose Go pprof profiles |
metrics | GET | Internal Prometheus-style metrics (clientmetric) |
usermetrics | GET | User-facing metrics (OpenMetrics format) |
goroutines | GET | Dump all goroutine stacks |
logtap | GET | Tap structured log stream |
update/check | GET, POST | Check for / trigger client update |
suggest-exit-node | GET | Auto-suggest best exit node |
set-use-exit-node-enabled | POST | Enable/disable exit node |
set-dns | POST | Set DNS record (ACME challenge response) |
check-ip-forwarding | GET | Check kernel IP forwarding status |
dns-osconfig | GET | OS DNS configuration |
dns-query | POST | Issue DNS query via tailnet resolver |
id-token | GET | Retrieve a Tailscale identity token (OIDC-like) |
drive/shares | GET, POST, DELETE | Taildrive share management |
drive/fileserver-address | GET, POST | Set Taildrive file server address |
appc-route-info | GET | App connector route info |
tka/status | GET | Tailnet Key Authority status |
tka/init | POST | Initialize TKA |
tka/sign | POST | Sign a node key via TKA |
tka/disable | POST | Disable TKA |
tka/modify | POST | Modify TKA trust anchors |
tka/log | GET | TKA audit log |
tka/affected-sigs | POST | List signatures affected by disablement |
tka/generate-recovery-aum | POST | Generate recovery AUM |
tka/wrap-preauth-key | POST | Wrap a pre-auth key with TKA |
policy/ | GET | Read system policy keys |
query-feature | POST | Query feature availability from control plane |
debug | POST | Debug actions (rotate keys, reauth, etc.) |
debug-* | GET/POST | Various diagnostic endpoints |
component-debug-logging | POST | Temporarily enable verbose logging per component |
reload-config | POST | Reload declarative config file |
disconnect-control | POST | Force disconnect from coordination server |
alpha-set-device-attrs | POST | Set device attributes (alpha) |
set-push-device-token | POST | Register push notification device token |
handle-push-message | POST | Process an incoming push notification |
set-gui-visible | POST | Notify daemon that GUI is visible (macOS/Windows) |
set-udp-gro-forwarding | POST | Enable UDP GRO forwarding |
CLI#
Framework#
github.com/peterbourgon/ff/v3/ffcli — a lightweight command/flag library that wraps stdlib flag.FlagSet. Not cobra. Each command is a *ffcli.Command with a FlagSet, an Exec function, and optional subcommands.
Command structure#
Top-level commands registered in newRootCmd() (cmd/tailscale/cli/cli.go):
| Command | Purpose |
|---|---|
up | Connect to tailnet (with auth key, routes, exit node flags) |
down | Disconnect (stop advertising, keep logged in) |
set | Modify persistent preferences (operator, routes, SSH, DNS, etc.) |
login | Authenticate (browser or auth key) |
logout | Log out |
switch | Switch between multiple tailnet profiles |
configure | Platform-specific configuration (kubeconfig, JetKVM, Synology, etc.) |
netcheck | Network connectivity diagnostics (DERP latency, NAT type) |
ip | Show Tailscale IPs for this node |
dns | DNS subcommands (status, query) |
status | Show node status and peer table |
metrics | Metrics subcommands (print) |
ping | Ping a peer by hostname or IP |
nc | TCP/UDP netcat over Tailscale |
ssh | SSH to a peer via Tailscale |
funnel | Expose local servers to the public internet via Funnel |
serve | Expose local servers to the tailnet via Serve |
version | Print version info |
web | Start the web UI (legacy) |
file | Taildrop file transfer (cp, get) |
bugreport | Generate and upload bug report |
cert | Manage TLS certificates |
network-lock | Tailnet Key Authority management (init, sign, disable, etc.) |
licenses | Print open source licenses |
exit-node | Exit node management (list, suggest) |
update | Update Tailscale client |
whois | Identify a Tailscale peer by IP or address |
debug | Developer/debug commands |
drive | Taildrive file sharing |
id-token | Get an identity token |
configure-host | Host-level configuration (e.g. net for sysprefs) |
systray | Start system tray (macOS/Windows helper) |
appc-routes | App connector route inspection |
wait | Wait until daemon is ready |
syspolicy | Read system policy (enterprise) |
Flag patterns#
- Flags use stdlib
flag.FlagSetwrapped inffcli. Per-command flag sets are initialized as closures:FlagSet: func() *flag.FlagSet { ... }. - Persistent flags are defined at the root level (
--socketto override the Unix socket path). - No env-var flag binding at the CLI layer (env knobs are read directly via
envknobin the daemon; the CLI respectsTS_*vars independently). - The CLI guards against duplicate flag setting via the
onceFlagValuewrapper applied across all commands vianoDupFlagify. - Auto-completion supported via
cmd/tailscale/cli/ffcomplete— a thin completion driver over ffcli.
Cloud Coordination REST API client (client/tailscale)#
The client/tailscale package is a Go client for the Tailscale cloud control plane at https://api.tailscale.com/api/v2/. This is the admin API used by operators and CI/CD, not the daemon-to-daemon protocol (which is handled by control/controlclient using a custom Noise+HTTPS long-poll).
Note: The package itself marks itself deprecated in favor of tailscale.com/client/tailscale/v2, but remains in the repo for internal use.
Key resource endpoints:
| Resource | Operations |
|---|---|
devices | List, Get, Delete, Authorize, Tag |
keys | List, Get, Create, Delete (auth keys, API keys) |
routes | Get, Set subnet routes for a device |
dns | Get, Set nameservers, search paths, preferences |
acl | Get, Validate, Set tailnet ACL policy |
tailnet | Get tailnet-wide settings |
cert | Get TLS certificate for a device domain |
Authentication: API key or OAuth token in Authorization: Bearer header.
URL construction: BuildURL() / BuildTailnetURL() helpers — no codegen, no OpenAPI.
Library API (client/local.Client)#
The client/local package wraps all /localapi/v0/ endpoints as a typed Go library, making it the canonical way to build applications that need to inspect or control a running tailscaled. This is the primary “library API” surface.
Key method groups#
Identity & status:
WhoIs(ctx, remoteAddr)— resolve an IP:port to node identityWhoIsNodeKey(ctx, nodeKey)— same, by WireGuard public keyStatus(ctx)/StatusWithoutPeers(ctx)— full node/peer statusProfileStatus(ctx)— list login profiles
Connectivity:
DialTCP(ctx, host, port)/UserDial(ctx, network, host, port)— dial Tailscale peersPing(ctx, ip, pingType)— ICMP/TSMP/DERP latency measurement
Configuration:
GetPrefs(ctx)/EditPrefs(ctx, maskedPrefs)— read/update preferencesReloadConfig(ctx)— reload declarative config fileSwitchProfile/SwitchToEmptyProfile/DeleteProfile— multi-profile management
File transfer (Taildrop):
WaitingFiles,AwaitWaitingFiles,DeleteWaitingFile,GetWaitingFilePushFile(ctx, target, size, name, reader)
Serve / Funnel: (via EditPrefs with serve config)
- Managed through the
serve-configendpoint
DNS:
GetDNSOSConfig,QueryDNS
TLS certificates:
- Via
cert/endpoint (not directly exposed as a named method onClient)
Drive (Taildrive):
DriveSetServerAddr,DriveShareSet/Remove/Rename/List
Observability:
DaemonMetrics,UserMetrics,Goroutines,Pprof,BugReport,TailDaemonLogsWatchIPNBus(ctx, mask)— streaming SSE watcher returning*IPNBusWatcher(iterator)StreamBusEvents(ctx)— Go 1.23 iterator over raw event bus eventsEventBusGraph,EventBusQueues
Auth:
StartLoginInteractive,LogoutIDToken(ctx, aud)— OIDC-like identity token for external service auth
Update:
CheckUpdate,SetUseExitNode,SuggestExitNode
API style: Typed Go methods, context.Context as first argument, returns (T, error). JSON marshaling is handled internally. No functional options; clients configure the struct fields directly.
Backward compatibility: The client/local package is used both internally and externally. The top-level package client/tailscale bears a deprecation notice pointing to v2, but the client/local package (daemon-local API) has no such notice and is maintained as a stable surface.
Web client API (client/web)#
The embedded web UI (client/web) serves a browser-based management interface. It exposes a small REST API under /api/:
| Endpoint | Method | Purpose |
|---|---|---|
/api/data | GET | Node data (status, auth state) |
/api/exit-nodes | GET | Available exit nodes |
/api/routes | POST | Update subnet routes / exit node selection |
/api/up | POST | Initiate tailscale up (login client mode) |
/api/device-details-click | POST | Telemetry — user viewed device details |
/api/local/v0/logout | POST | Proxy logout to localapi (permission-checked) |
/api/local/v0/prefs | PATCH | Proxy prefs update to localapi (permission-checked) |
/api/local/v0/update/check | GET, POST | Proxy update check/trigger |
/api/local/v0/update/progress | POST | Proxy update progress reporting |
/api/local/v0/upload-client-metrics | POST | Proxy client metric upload |
Authentication in the web client uses a two-layer model:
- CSRF protection via
gorilla/csrf(token in cookie + header). - Per-request peer capability check:
lc.WhoIs(r.RemoteAddr)maps the requesting IP to a Tailscale node identity, thentoPeerCapabilities()extracts Tailscale Access Controls (ACL) grants (e.g.,ts.cap.webui:edit:exit-nodes,ts.cap.webui:edit:ssh) to gate write access.
The web client operates in three modes (ServerMode): loginClient, readonlyClient, managingClient — only the last allows mutations.
Plugin / Extension system#
Tailscale uses a feature hook model rather than a traditional plugin system:
feature.Hook[Func](packagetailscale.com/feature): A typed, set-once function slot. Optional modules register themselves atinit()time via blank imports:// ssh/tailssh/tailssh.go ipnlocal.RegisterNewSSHServer(func(logf, lb) (SSHServer, error) { ... })buildfeatures.HasXxx(packagetailscale.com/feature/buildfeatures): Compile-time boolean constants (HasSSH,HasNetstack,HasDNS,HasServe,HasTaildrop,HasDebug,HasTPM, etc.). Generated as_enabled.go/_disabled.gofile pairs selected by build tags. Route registration inlocalapi, feature behavior inipnlocal, and CLI command inclusion all gate on these constants.
Extension points visible in the codebase:
- SSH server:
ipnlocal.RegisterNewSSHServer→ registersssh/tailssh - Netstack (userspace networking):
hookNewNetstackfeature hook → registersnet/netstack - Web client:
hookServeWebClientfeature hook - Outbound proxy: registered via hook
- Taildrop / file sharing: registered via hook
- App connectors (
appc): gated onbuildfeatures.HasAppConnectors - Taildrive: gated on
buildfeatures.HasTaildrive - Client update: gated on
buildfeatures.HasClientUpdate
There is no runtime plugin loading (no hashicorp/go-plugin, no WASM, no shared library loading). All extensibility is link-time.
DERP server HTTP API (cmd/derper)#
The derper binary exposes:
GET /derp— WebSocket/HTTP-upgrade endpoint for the DERP relay protocol (clients connect here)GET /derp/probe— Health probe (returns 200 OK)GET /derp/latency-check— Latency check endpoint used by clients during path probingGET /bootstrap-dns— Returns the tailscale.com DNS bootstrap dataGET /— HTML status page
Notable API surface observations#
No versioning in Local API. All routes are
/localapi/v0/with no increment. Breaking changes are managed by feature flags and the requirement that CLI and daemon are always updated together (the version mismatch warning inlocal.Clienthandles the edge case).Route registration as a build-time API surface. The set of active endpoints depends on which build features are compiled in. This is unusual — the API surface is not fixed; it varies by binary variant. Lean container images have a significantly smaller route table than the full desktop client.
The
client/local.Clientis the public Go interface. Third-party applications that embed Tailscale behavior (e.g., anything usingtsnet-style integration) use this client. The typed methods are the de-facto stable API contract, even if no formal stability guarantee is documented.Dual REST API layers. The coordination cloud API (
client/tailscale,api.tailscale.com/api/v2/) and the local daemon API (client/local,/localapi/v0/) are completely separate systems targeting different actors: the former is for admin/automation; the latter is for local GUI/CLI control.WatchIPNBusas the reactive interface. For event-driven integrations (GUI frontends, the web UI, the systray), the canonical pattern is to subscribe towatch-ipn-busand react toipn.Notifyevents rather than pollingstatus. This is the closest Tailscale has to a pub/sub or event-stream API.