Agent Internals
Subsystems
These subsystems are initialized and started by plexd up:
| Subsystem | Package | Reference |
|---|---|---|
| Control Plane Client | internal/api | Control Plane Client |
| Registration | internal/registration | Registration |
| Event Verification | internal/api (Ed25519Verifier) | Event Verification |
| WireGuard | internal/wireguard | WireGuard Tunnel Management |
| NAT Traversal | internal/nat | NAT Traversal via STUN |
| Peer Exchange | internal/peerexchange | Peer Endpoint Exchange |
| Network Policy | internal/policy | Network Policy Enforcement |
| Secure Tunneling | internal/tunnel | Secure Access Tunneling |
| SSE Manager | internal/api (SSEManager) | Control Plane Client |
| Reconciler | internal/reconcile | Configuration Reconciliation |
| Heartbeat | internal/agent (HeartbeatService) | Heartbeat Service |
| Integrity | internal/integrity | Integrity Verification |
| Actions | internal/actions | Remote Actions and Hooks |
| Node API | internal/nodeapi | Local Node API |
| Metrics | internal/metrics | Metrics Collection & Reporting |
| Log Forwarding | internal/logfwd | Log Forwarding |
| Audit Forwarding | internal/auditfwd | Audit Forwarding |
| Bridge (bridge mode) | internal/bridge | Bridge Mode |
Startup Sequence (plexd up)
The runUp function in cmd/plexd/cmd/up.go performs initialization before entering steady state:
Initialization:
- Parse config — read YAML, apply CLI flag overrides, apply
PLEXD_*env overrides - Set up logger — structured
sloglogger with configured level - Create control plane client —
api.NewControlPlane()with API config and build version - Register —
registrar.Register()loads or creates node identity (fatal on failure) - Create Ed25519 verifier — decode the control plane's signing public key from identity (the key registration returned, or a newer one a state pull carried and the agent persisted);
signingKeyDispatchHandler, registered first among the reconcile dispatch handlers, installs the key every state pull carries 5a. Initialize WireGuard — measure the uplink's path MTU with the STUN servers, create WireGuard interface, configure address and MTU, bring interface up. The interface MTU is sized to the uplink unlesswireguard.mtupins it ornat.enabledis false (see MTU detection) 5b. Initialize NAT + Peer Exchange — create STUN client, NAT discoverer, and peer exchanger 5c. Initialize Network Policy — create policy engine, firewall controller, and enforcer 5d. Initialize Tunnel — load/generate host key, create JWT verifier and mesh server 5e. Initialize Bridge (bridge mode only) — create bridge manager, ACME, ingress, user access, site-to-site managers; run Setup on each - Create SSE manager —
api.NewSSEManager()with handlers for signing key rotation, WireGuard peer events, tunnel events, policy events, and bridge events (bridge mode) - Create reconciler —
reconcile.NewReconciler()with handlers for WireGuard, policy, and bridge reconciliation - Create heartbeat service —
agent.NewHeartbeatService()with subsystem status enrichment and key-rotation callback; its credential-refused callback is set in step 12, once the node API server exists - Create integrity store + verifier —
integrity.NewStore()andintegrity.NewVerifier()for hook checksums - Create action executor —
actions.NewExecutor(), register 11 built-in actions, registeractions.NewDispatcher()on the reconciler as a dispatch handler, report capabilities to control plane - Create hook watcher —
actions.NewHookWatcher()for filesystem hook scanning - Create node API server —
nodeapi.NewServer(), wire action provider, hook reloader, and reconcile handler - Create metrics collectors + manager — system collector, agent stats collector,
metrics.NewManager() - Create log sources + forwarder — platform log source (journald on Linux, the launchd log file on macOS, the Event Log on Windows), file sources from
file_patterns,logfwd.NewForwarder() - Create audit sources + forwarder — process source,
auditfwd.NewForwarder()
Goroutines (10 in node mode, 11 in bridge mode):
After initialization, goroutines are started via a sync.WaitGroup:
- SSE Manager —
sseMgr.Start()— event stream connection - Heartbeat —
heartbeat.Run()— periodic heartbeats - Reconciler —
reconciler.Run()— periodic state reconciliation - Node API —
nodeAPISrv.Start()— local endpoint (a Unix socket, or a named pipe on Windows) + optional HTTP server - Hook Watcher —
hookWatcher.Watch()— filesystem watching for hook changes - Metrics Manager —
metricsMgr.Run()— collect and report metrics - Log Forwarder —
logForwarder.Run()— collect and forward logs - Audit Forwarder —
auditForwarder.Run()— collect and forward audit events - Peer Exchange —
exchanger.Run()— STUN discovery and endpoint exchange loop - Mesh Server —
meshServer.Start()— tunnel SSH server - Bridge Relay (bridge mode only) —
bridgeMgr.StartRelay()— UDP relay listener
Shutdown Sequence
On SIGTERM or SIGINT:
- Context cancel — the signal-notify context is cancelled, which signals all goroutines to stop
sseMgr.Shutdown()— close the SSE connectionexecutor.Shutdown()— drain running actionsmeshServer.Shutdown()— stop tunnel SSH server and drain sessionsingressMgr.Teardown()— close ingress listeners (bridge mode)userAccessMgr.Teardown()— remove user access peers and interface (bridge mode)s2sMgr.Teardown()— remove site-to-site tunnels and interfaces (bridge mode)acmeMgr.Teardown()— stop ACME certificate management (bridge mode)bridgeMgr.Teardown()— remove bridge routes and forwarding (bridge mode)enforcer.Teardown()— remove firewall chains and ruleswgMgr.Teardown()— delete WireGuard interface (last, others depend on it)wg.Wait()with 30s timeout — wait for all goroutines to exit; force exit if timeout exceeded
Authentication Flow
- Bootstrap — node registers using a bootstrap token (file, env var, or metadata service)
- Node Secret Key — registration returns a
NodeSecretKeyused for all subsequent API authentication - Heartbeat Auth — each heartbeat uses the node secret key; three heartbeats answered 401 Unauthorized (
unauthorized,nsk_invalidornsk_revoked) with no accepted one between them park the agent, which then stops calling the control plane and probes the credential with the heartbeat (see Credential Park) - Signing Key — the Ed25519 verifier checks SSE envelopes and ssh session tokens against the Domain signing key. Every successful state pull installs the key its
signing_keyblock carries (verifier.Install) and persists it into the identity files, so a restart starts from it. Anullblock keeps the installed key, and a key the verifier refuses changes nothing. Asigning_key_rotatedSSE event installs a new key too (verifier.Rotate) and can keep the previous key trusted until the rotation'stransition_expires; a pulled key that differs from the current one ends that grace window, because a node trusts exactly one key of its Domain. The mesh SSH server's JWT verifier (step 5d) is separate: it keeps the key decoded at start and picks up a pulled key only after a restart
SSE Event Types
The SSE manager processes events in two tiers. Payloads of the reconcile-driving types are opaque — the reconciler's state pull is authoritative, so those events only request a reconcile. session_revoked is the exception: its payload names the session to close before the reconcile.
| Event | Tier | Handler |
|---|---|---|
node_state_updated | contract | Triggers a reconcile |
policy_updated | contract | policy.HandlePolicyUpdated — triggers a reconcile |
bridge_config_updated | contract | bridge.HandleBridgeConfigUpdated — triggers a reconcile (bridge mode) |
action_request | contract | Triggers a reconcile — the dispatch rides the executions block of the resulting pull |
session_setup | contract | Triggers a reconcile — the session rides the sessions block of the resulting pull |
session_revoked | contract | Closes the live session the payload names with the reason it carries (tunnel.Dispatcher.SessionRevokedHandler), then triggers a reconcile; the block drain stays the teardown when the event is missed |
node_secrets_updated | contract | Triggers a reconcile; the inventory rides the secrets block of the resulting pull, which feeds the node API's secret index |
peer_registered | documented-coming | Triggers a reconcile |
peer_psk_assigned | documented-coming | Triggers a reconcile |
peer_deregistered | documented-coming | Triggers a reconcile |
peer_endpoint_changed | documented-coming | Triggers a reconcile |
peer_key_rotated | documented-coming | Triggers a reconcile |
rotate_keys | documented-coming | Starts a key rotation (rotator.RotateNow) |
signing_key_rotated | documented-coming | Updates the Ed25519 verifier key (verifier.Rotate); every state pull also installs the key its signing_key block carries (verifier.Install) |
Contract types are emitted by the control plane today. Documented-coming types are named so the agent can subscribe once the platform's 14-type taxonomy starts emitting them.
See Also
- Architecture — High-level architecture diagrams, platform support
- Agent Lifecycle — Lifecycle phases, operational behavior, deregistration
- Configuration Reference — Full YAML configuration schema
- CLI Reference — Command-line interface and subcommands
- Environment Variables Reference — All
PLEXD_*overrides