Architecture
How TSLink works under the hood
Scenario guides: Share an app, not your whole machine.
Overview
TSLink is an app access and management layer for a PC, server or cloud host. Tailscale supplies private transport and HTTPS; TSLink manages existing apps on each host. One shared daemon reconciles a dedicated embedded node per service. It does not install apps, isolate host processes, create a VPC or aggregate hosts.
Dashed arrows show management, solid arrows access routes. Each service row belongs to this host. The private portal and requests, guest gate, scoped MCP, durations and bounded history are shipped. Multi-host inventory, admin REST/dashboard, Docker discovery, middleware, custom ACME and metrics remain planned.
Embedded tsnet Nodes
At the core of TSLink is an embedded tsnet node per service. Unlike traditional Tailscale setups that require the tailscaled daemon, tsnet runs entirely within the TSLink process:
- No system-level Tailscale required — TSLink manages its own Tailscale identity.
- One node per service (microsegmentation) — each registered service gets its own tsnet node with an independent hostname and WireGuard identity. Proxy/file HTTP services use HTTPS certificates; raw TCP does not terminate TLS in TSLink. This is network-level segmentation, not host or process isolation: TSLink does not sandbox the local process, so compromising one service's backend does not by itself stay contained -- see Standards Alignment for the full boundary.
- Independent state — each node's WireGuard keys and configuration are stored in
~/.config/tslink/nodes/<service-name>/. - Direct integration — TSLink controls each node's lifecycle, starting and stopping them as services are added or removed.
Multi-Node Server Architecture
TSLink uses a Server struct that manages N ServiceNode instances. Each ServiceNode wraps a tsnet.Server and its associated handler (proxy, file, or TCP):
┌─────────────────────────────────────────────┐
│ TSLink Server │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ ServiceNode │ │ ServiceNode │ ... │
│ │ "webapp" │ │ "api" │ │
│ │ │ │ │ │
│ │ tsnet.Server│ │ tsnet.Server│ │
│ │ WireGuard │ │ WireGuard │ │
│ │ TLS (HTTP) │ │ TLS (HTTP) │ │
│ │ Handler │ │ Handler │ │
│ │ Access logs │ │ Access logs │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Registry Watcher (fsnotify) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘Each service node operates independently with its own WireGuard identity; HTTPS certificates apply to proxy/file HTTP services. This per-service separation is a form of network segmentation: each service has its own cryptographic identity and can be individually shut down without affecting others.
WireGuard Networking
Tailscale networks are built on WireGuard, a modern VPN protocol. Traffic on the tailnet leg stays encrypted between tailnet peers even if the local network is compromised -- a key property for zero-trust architectures that assume the network is hostile. The short hop from a TSLink node to your local service is separate and may be plaintext. When TSLink starts:
- Each embedded tsnet node establishes WireGuard tunnels to other devices on your tailnet.
- Traffic between accessing devices and the TSLink node is WireGuard-encrypted; the hop from the node to your local service is as you configured it and may be plaintext.
- Tailscale handles NAT traversal; the host still needs permitted network connectivity to its coordination and peer/relay paths.
- No services are exposed to the public internet by default — the attack surface is limited to authenticated tailnet members.
NAT Traversal and DERP Relay
TSLink relies on Tailscale's NAT traversal infrastructure:
- Direct connections are established when possible using UDP hole punching.
- DERP relay fallback — when direct connections fail (e.g., behind restrictive NATs or firewalls), traffic is relayed through Tailscale's globally distributed DERP (Designated Encrypted Relay for Packets) servers. DERP relay traffic stays WireGuard-encrypted between the tailnet peers — relays cannot read it.
Service Types
Reverse Proxy
For proxy-type services (--proxy), TSLink runs an HTTP reverse proxy using Go's httputil.ReverseProxy:
Client on tailnet → WireGuard tunnel → TSLink → localhost:portThe reverse proxy:
- Terminates TLS at the TSLink node.
- Forwards requests to the specified local
host:port. - Rewrites outbound
Hostto the backend target throughSetURL. - Rebuilds
X-Forwarded-For,X-Forwarded-Host, andX-Forwarded-Protofrom the incoming request throughSetXForwarded; client-supplied forwarding headers are not preserved. The original requested host is inX-Forwarded-Host. See Go's proxy rewrite contract. - Injects Tailscale identity headers when WhoIs resolves caller identity:
| Header | Description |
|---|---|
X-Tailscale-User-Login | Authenticated user's login name |
X-Tailscale-User-Name | Authenticated user's display name |
X-Tailscale-User-Picture | URL to user's profile picture (when available) |
X-Tailscale-Node | Source node's computed name, when available |
Proxy identity headers are best-effort: TSLink strips inbound X-Tailscale-* headers and injects replacements only when WhoIs resolves a user profile. Only without an applicable people policy or --allow restriction can an unresolved caller reach the backend without identity headers. People checks and configured --allow fail closed.
Trust these headers only over a controlled TSLink-to-backend path that prevents callers from bypassing the proxy. Applications requiring identity must reject missing values and retain their required authentication and authorization. Header stripping protects the proxy path; it does not protect a directly reachable backend from forged headers.
File Server
For directory-type services (--dir), TSLink serves files using Go's built-in http.FileServer:
Client on tailnet → WireGuard tunnel → TSLink → filesystemDirectory shares are read-only over HTTPS with directory listing enabled. A regular-file share serves only the selected file and refuses siblings and listings. File services use WhoIs for configured HTTP --allow and access logging; they do not inject proxy identity headers.
TCP Forwarder
For TCP-type services (--tcp), TSLink performs raw TCP proxying using bidirectional io.Copy:
Client on tailnet → WireGuard tunnel → TSLink → localhost:port (TCP)This is useful for databases (PostgreSQL, MySQL), Redis, and any other TCP-based protocol. The connection is forwarded byte-for-byte without HTTP processing. Proper half-close semantics are implemented — when one side closes its write channel, TSLink propagates the half-close to the other side rather than tearing down the entire connection immediately.
Raw TCP uses a hostname:port endpoint. TSLink adds no HTTP identity headers, HTTP --allow filter, or application TLS termination on this path. Keep authentication and any application/backend TLS in the target service and client; WireGuard protects the separate tailnet leg.
Middleware Pipeline
The shipped HTTP path applies the built-in allowed_users ACL and access logging. Metrics instrumentation is not implemented. Configurable middleware (rate limiting, Basic Auth, IP allow lists, CORS) is roadmap/experimental. See the Experimental & Roadmap page for the full pipeline details.
Credential System
Default enrollment needs no stored credential. A human completes the needs_login / auth_url handoff and any tailnet approval; nodes are user-owned and untagged. Optional stored credentials enable a different node-authentication mode. In API-token mode, the token is not passed directly to the node: TSLink derives short-lived, single-use auth keys with the service's effective tags and its ephemeral flag. Short key lifetime does not make every node ephemeral. The API token itself is a fully permitted tailnet credential, not a scoped node key.
The optional stored mode supports two credential types:
| Credential | Prefix | Behavior |
|---|---|---|
| API access token | tskey-api-* | Verified against Tailscale API. Current most complete path for tag/device operations and auth material derivation. Expires periodically. |
| OAuth client secret | tskey-client-* | Stored directly and does not expire. Used by tsnet for direct authentication, but current Tailscale REST tag/device automation paths are narrower. Validate before unattended use. |
Credentials prefer the system keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). Plaintext 0600 file fallback is allowed only on macOS/Linux after TSLink proves an old keychain value is absent or has been deleted. An unreachable or uncertain keychain fails closed; restore access and retry. Windows does not allow credential file fallback. Headless operation alone does not guarantee fallback.
Legacy support: ~/.config/tslink/authkey is checked if no API key or OAuth secret exists (backward compatibility only).
TLS Certificates
Proxy/file HTTP services use Tailscale's automatic HTTPS certificate provisioning via the LocalAPI when tailnet HTTPS is enabled and the control server supports the required certificate flow:
- Certificates are obtained automatically from the Tailscale coordination server.
- They are valid, publicly-trusted certificates (via Let's Encrypt).
- Certificate renewal is handled transparently by the tsnet library.
- No manual certificate management is needed.
- Each proxy/file service gets its own certificate — provisioned per hostname, not shared.
Read the actual hostname from runtime output, such as <service-name>.<tailnet-name>.ts.net. Raw TCP uses the TCP listener rather than this HTTPS certificate path. A custom control URL alone does not establish certificate issuance support.
Custom domains and ACME are roadmap/experimental. There are no --domain / --acme-email flags or domain / acme_email registry fields. Legacy keys, even empty values, are rejected with unknown_config_key; remove them. Use the default tailnet hostname.
Registry and Hot Reload
The service registry (~/.config/tslink/registry.json) is the declarative configuration for TSLink. The hot-reload system works as follows:
- fsnotify watcher monitors the registry file for changes.
- A file lock (mutex) is acquired to prevent race conditions.
- On change, the new registry is loaded and diffed against the current state.
- New services are started with their own tsnet nodes and HTTP/TCP handlers.
- Removed services have their nodes shut down and handlers deregistered.
- Changed services recreate the affected runtime; enrollment-state reset depends on whether the effective authentication identity changed.
- Unchanged services continue running without interruption.
New or restarted services still need enrollment and readiness. See Hot Reload for fields and the runtime-versus-enrollment distinction.
For details on Docker integration, cluster support, admin API, and Prometheus metrics, see Experimental & Roadmap.
Structured Logging
Use tslink logs for gateway diagnostics on stderr (tslink.err.log) and daemon stdout (tslink.out.log). Access history uses a separately bounded local JSONL store for HTTP/file, TCP and guest events, combined with intent/completion and lifecycle receipts from an independent journal. WhoIs identity is attested when available; logs can have drops and crash gaps. An intent alone is not completed success.
Tailscale Funnel
Open proxy publications can optionally be exposed publicly via Tailscale Funnel using --funnel --public. This is explicit public exposure: the service is accessible from the public internet at the same https://<service-name>.<tailnet-name>.ts.net URL, does not require the accessing device to be on your tailnet, and is not covered by TSLink identity enforcement. File and TCP services do not support Funnel.
Tag System
TSLink can provision ordinary ACL tag-owner entries when an API access token with the required policy permissions is configured and --manage-acl is explicitly selected. Without that flag, credential-backed login/serve leaves ordinary remote ACL mutation unapplied and reports its plan. Credential-free serve does not ensure ordinary remote tags.
Explicit Ordinary ACL Provisioning
login --manage-acl
→ store credential and request ordinary ensure for the default tag only
→ does not collect registry tags or start service nodes
serve --manage-acl (with stored credentials and required policy permissions)
→ collect ordinary tags used by valid registered services
(including the configured default where a service uses it)
→ request ordinary tag-owner ensure, then start/reconcile service nodesEnsure is idempotent for an existing tag owner. Its outcome still depends on credential support and policy permissions; login can report degraded provisioning rather than successfully creating an owner. See ACL Mutation for the opt-in contract.
Explicitly public Funnel (--funnel --public) has a separate, default-on provisioning path for its shared tag owner and nodeAttrs grant, unless auto-provisioning is disabled. It still requires usable API access and policy permissions. Ordinary --manage-acl opt-in is a separate boundary; local cleanup does not delete the shared Funnel grant.
Hot-Reload and Tags
In credential-free mode, stored registry tags are not advertised, so editing tags retains the user-owned node's enrollment at startup and hot reload. In stored-credential mode, a change to the effective tagged identity stops the affected node, clears its local state, and restarts it with updated auth material. Remote stale-device cleanup requires exact ownership proof; without it, matching candidates are protected/skipped.
Default Tag
The default tag (tag:tsmain unless overridden via tslink tags set-default) supplies tags for stored-credential node creation when a service has no explicit --tags. Credential-free nodes remain user-owned and untagged even if their registry record contains tags; storing a tag does not create remote tag ownership.
tslink tags Commands#
The local tag registry and remote tag operations are documented in Tag Configuration. Saving local tags does not apply remote policy; remote deletion requires delete-remote --force --manage-acl and its authorization/safety checks.
Access Control
TSLink enforces access control at multiple layers, implementing defense in depth:
- Proxy/file HTTP ACL (
--allow) — restrict a service to specific Tailscale identities (users by email or tags liketag:prod). The shipped ACL check resolves the caller's identity from best-effort TailscaleWhoIs(cached ~60s by source IP) and, when--allowis configured, is fail-closed: it returns403 Forbiddenfor callers it cannot authorize. Raw TCP has no HTTP ACL. - Tailscale ACLs — tailnet access is governed by Tailscale network policy. Funnel policy authorizes a node to publish; it does not authenticate public visitors, whose authorization belongs to the backend application.
- Middleware pipeline — roadmap/experimental. Additional layers such as rate limiting, IP allowlists, and Basic Auth require runtime wiring before they are a shipped protection layer.
Process Lifecycle
TSLink uses PID-based process management:
- Foreground mode: the process runs in the current terminal session.
- Daemon mode (
--daemon): the process forks into the background, redirecting stdout/stderr to log files, and writing its PID to~/.config/tslink/tslink.pid. - Stop: Unix sends
SIGTERMand waits up to five seconds for exit; timeout returns an error without forced termination. Windows uses immediate process termination and confirms exit. See verified shutdown before maintenance. - Single instance: TSLink checks for an existing PID before starting to prevent duplicate gateways.
- Signal handling: Unix
SIGTERMandSIGINTinitiate listener/node shutdown; each HTTP server has its own drain budget, separate from the CLI process-exit wait. - Autostart:
tslink installregisters the gateway as a system login service (LaunchAgent on macOS, systemd user service on Linux, Task Scheduler with a built-in supervisor on Windows).
Public and private access
Browser guest links use explicitly public Funnel with a mandatory gate; link/PIN is forwardable and not human identity. Private connections to the same app retain independent people/allow checks. Open Funnel remains a separate publication without visitor identity. The portal stays private per host; requests need explicit requestable private HTTP/file apps, eligible members and owner approval. Scoped MCP is a management permission boundary, not an OS sandbox.