Documentation
¶
Overview ¶
Package daemonapi is the dependency-free contract layer between the daemon engine and its plugins.
The cycle problem: handshake / runtime / libpilot are daemon plugins that historically imported web4/pkg/daemon for the concrete Daemon struct, Config, Connection, Listener, and assorted services. The daemon binary in turn imported the plugins to compose itself. This is a real cycle (web4 ↔ plugins) that survives only because of `replace` directives during local dev.
daemonapi breaks the cycle by hosting:
Pure-interface contracts (Daemon, Connection, Listener, TrustChecker, HandshakeService, PolicyManager, PolicyRunner, WebhookManager, ...). The concrete *daemon.Daemon and its members satisfy these via Go's structural typing — the daemon engine never imports daemonapi to register, and plugins never import the daemon engine.
A runtime plugin lifecycle (Plugin interface) and a process- global registry (RegisterPlugin, LoadAll). Plugins register themselves from an init() block in their own package; the daemon engine, with no compile-time knowledge of which plugins exist, iterates whatever the registry contains.
"Not static" wiring means:
The daemon engine has zero hardcoded list of plugins. Adding or removing a plugin from a binary is a single blank-import line in cmd/daemon/main.go; the daemon, plugin, and other plugin packages all stay unchanged.
The Plugin contract is interface-based, so a plugin's source code is interchangeable: two `handshake` implementations satisfy the same Plugin interface, the daemon doesn't know the difference.
The registration mechanism is identical for in-process plugins (Go packages linked into the binary) and for true runtime plugins (Go plugin.Open of .so files). The .so's init() block calls RegisterPlugin the same way; the daemon engine then iterates the registry.
What this package does NOT do:
It does not own daemon implementation code. Concrete types stay in web4/pkg/daemon (and plugins' own implementations stay in their own repos). Interfaces only.
It does not specify how the daemon engine starts or shuts down. That's the daemon's job. The Plugin lifecycle methods (Init, Shutdown) tell plugins when the daemon is ready and when it's stopping; the daemon decides the broader sequencing.
Index ¶
- Constants
- func RegisterPlugin(name string, f Factory)
- func Registered() []string
- func ShutdownAll(ctx context.Context, plugins []Plugin) error
- type ConnReadWriter
- type Connection
- type ConnectionInfo
- type Daemon
- type Event
- type EventBus
- type Factory
- type HandshakePendingRecord
- type HandshakeService
- type HandshakeTrustRecord
- type Listener
- type Plugin
- type PolicyEventType
- type PolicyManager
- type PolicyRunner
- type PortAllocator
- type TrustChecker
- type TunnelRegistry
- type VersionInfo
- type WebhookManager
- type WebhookStats
Constants ¶
const ( PolicyEventConnect = "connect" PolicyEventDial = "dial" PolicyEventDatagram = "datagram" PolicyEventJoin = "join" PolicyEventLeave = "leave" PolicyEventCycle = "cycle" )
PolicyEvent* are the event-type constants the daemon engine passes into PolicyManager / PolicyRunner. Match coreapi.PolicyEvent* values; both are aliases of string.
Variables ¶
This section is empty.
Functions ¶
func RegisterPlugin ¶
RegisterPlugin records a factory under name. Typical use:
func init() {
daemonapi.RegisterPlugin("handshake", func() daemonapi.Plugin {
return &handshakePlugin{}
})
}
Registering twice with the same name is a programming error and panics — two factories under one name would race in LoadAll. The panic surfaces during package init, not at runtime, which makes the conflict obvious in test output and at first daemon launch.
func Registered ¶
func Registered() []string
Registered returns the names of every registered plugin, sorted. Useful for status output and for tests that want to verify a blank-import set wired the expected plugins in.
func ShutdownAll ¶
ShutdownAll calls Shutdown on every plugin in REVERSE-sorted order (the inverse of LoadAll's startup order). Each Shutdown gets the same context — typically a deadline-bound context from the daemon's shutdown timeout. Errors are collected and returned as a wrapped multi-error so the daemon still attempts to shut down every plugin even if an earlier one returned an error.
Types ¶
type ConnReadWriter ¶
type ConnReadWriter interface {
Read(p []byte) (int, error)
Write(p []byte) (int, error)
Close() error
}
ConnReadWriter is the read/write adapter plugins use when they need net.Conn-style I/O on a Connection. Construct one via Daemon.NewConnReadWriter(conn).
type Connection ¶
type Connection interface {
// Info returns an endpoint snapshot. The returned struct is a
// value copy, so plugins may hold it across goroutines.
Info() ConnectionInfo
}
Connection is the daemon-facing handle to a stream connection. Plugins receive Connection values from DialConnection / Accept and pass them back to SendData / CloseConnection / NewConnReadWriter.
The Info accessor returns a struct snapshot of the four endpoint quantities a plugin commonly needs (local/remote address + port). This avoids exposing every field of the concrete *daemon.Connection through the interface, and avoids name collisions between Go's exported struct fields and same-named methods on the interface.
type ConnectionInfo ¶ added in v0.4.0
type ConnectionInfo struct {
LocalAddr protocol.Addr
LocalPort uint16
RemoteAddr protocol.Addr
RemotePort uint16
}
ConnectionInfo is the endpoint snapshot returned by Connection.Info.
type Daemon ¶
type Daemon interface {
// Start brings the daemon online. Returns when the listeners are
// bound and the daemon is ready to accept traffic, or with an
// error if any step of bootstrap fails.
Start() error
// Stop drains in-flight work and tears down the daemon. Idempotent;
// safe to call from a signal handler.
Stop() error
// NodeID returns this daemon's stable 32-bit node ID. 0 when the
// identity has not been loaded yet.
NodeID() uint32
// Identity returns the daemon's Ed25519 keypair holder. Returns
// nil when the daemon was started without an identity file
// (in-memory tests).
Identity() *crypto.Identity
// IdentityPath returns the on-disk path to the identity file.
// Empty when running in-memory.
IdentityPath() string
// Sign signs the message with the local Ed25519 private key.
// Returns nil when no identity is loaded.
Sign(msg []byte) []byte
// AdminToken returns the local admin token used to authenticate
// privileged registry RPCs. Empty when not configured.
AdminToken() string
// TrustAutoApprove reports whether the daemon was started with
// the auto-approve flag set. Plugins gating user-visible decisions
// on this flag (handshake auto-accept) read it once at Init.
TrustAutoApprove() bool
// Addr returns the daemon's pilot-network address. Stable for
// the life of the daemon process.
Addr() protocol.Addr
// DialConnection opens an outbound stream to (dstAddr, dstPort)
// and returns an opaque Connection handle. The handle is passed
// back to SendData, CloseConnection, NewConnReadWriter, etc.
DialConnection(dstAddr protocol.Addr, dstPort uint16) (Connection, error)
// DialConnectionContext is DialConnection with a deadline. The
// context's Done channel cancels the dial.
DialConnectionContext(ctx context.Context, dstAddr protocol.Addr, dstPort uint16) (Connection, error)
// SendData writes the byte slice to the stream connection.
// Blocks until the data is queued for transmission.
SendData(conn Connection, data []byte) error
// SendDatagram sends an unconnected (UDP-shaped) payload to
// (dstAddr, dstPort). Best-effort, no retransmission.
SendDatagram(dstAddr protocol.Addr, dstPort uint16, data []byte) error
// CloseConnection tears down the connection. Idempotent.
CloseConnection(conn Connection)
// NewConnReadWriter wraps a stream Connection as a net.Conn-style
// adapter. Plugins that need read/write semantics use this; the
// daemon retains ownership of the underlying connection state.
NewConnReadWriter(conn Connection) ConnReadWriter
// Ports returns the daemon's port allocator. Opaque to most
// plugins; usually handed off to other plugins (e.g. handshake)
// that bind well-known ports through it.
Ports() PortAllocator
// Tunnels returns the daemon's tunnel registry. Opaque to most
// plugins.
Tunnels() TunnelRegistry
// RegistryClient returns the L8 registry-side-channel client.
// nil when the daemon is running without a registry connection.
RegistryClient() *registry.Client
// RegConnListNodes is the privileged list_nodes RPC, used by the
// policy plugin to enumerate per-network members.
RegConnListNodes(netID uint16, token string) (map[string]any, error)
// SetMemberTags updates the local node's per-network tag list
// via the registry.
SetMemberTags(netID uint16, tags []string)
// PublishEvent is the bus.Publish wrapper, exposed at top level
// because plugins commonly publish without holding a Bus reference.
PublishEvent(topic string, payload map[string]any)
// Bus returns the in-process event bus for plugins that subscribe.
Bus() EventBus
// GetTrustChecker returns the currently-registered trust checker
// (typically the trustedagents plugin). Returns nil when no
// checker is wired.
GetTrustChecker() TrustChecker
// RegisterTrustChecker installs the given checker as the daemon's
// trust authority. Called once at startup by the trustedagents
// plugin via the runtime adapter.
RegisterTrustChecker(tc TrustChecker)
// HandshakeService returns the currently-registered handshake
// service. Returns nil when no handshake plugin is wired (tests
// that bypass plugins).
HandshakeService() HandshakeService
// RegisterHandshakeService installs the handshake plugin's
// service. Called once at startup via the runtime adapter.
RegisterHandshakeService(svc HandshakeService)
// TrustedPeers proxies through to HandshakeService().TrustedPeers().
// Returns nil when no handshake plugin is wired.
TrustedPeers() []HandshakeTrustRecord
// HandshakeRevokeTrust proxies through to HandshakeService().RevokeTrust.
HandshakeRevokeTrust(nodeID uint32) error
// HandshakeSendRequest proxies through to HandshakeService().SendRequest.
HandshakeSendRequest(nodeID uint32, reason string) error
// RegisterPolicyManager installs the policy plugin's manager.
RegisterPolicyManager(pm PolicyManager)
// SetWebhookURL hot-swaps the active webhook URL on the registered
// webhook plugin. No-op when no plugin is registered.
SetWebhookURL(url string)
// RegisterWebhookManager installs the webhook plugin's manager.
RegisterWebhookManager(wm WebhookManager)
}
Daemon is the dependency-free contract a daemon engine exposes to plugins. The concrete *daemon.Daemon in web4/pkg/daemon satisfies this interface via Go's structural typing — neither side needs to import the other.
Every method here exists because at least one plugin (handshake, runtime, libpilot) calls it. Adding a method later is a backwards- compatible change as long as concrete daemon implementations grow the corresponding method first.
Return types deliberately use:
Common-package concrete types (*crypto.Identity, *registry.Client, protocol.Addr) — those types already live in the dependency-free common module, so referencing them here doesn't create cycles.
daemonapi-local interfaces (Connection, PortAllocator, EventBus, etc.) — where the concrete return type lives inside the daemon engine. The opaque-marker interfaces keep plugins from poking at engine internals while still letting the daemon hand the value back to plugins as a typed token.
type Event ¶
Event is the payload structure carried over the daemon event bus. Mirrors the layout of the concrete event type in web4/pkg/daemon so subscriber channels receive identical-shaped values across the interface boundary.
type EventBus ¶
type EventBus interface {
// Publish emits the topic + payload to every subscriber whose
// pattern matches the topic. Non-blocking; events may be dropped
// on per-subscriber backpressure.
Publish(topic string, payload map[string]any)
// Subscribe registers a pattern-matching subscriber and returns
// the receive channel plus a cancellation closure. Calling the
// closure stops delivery and drains the buffer.
Subscribe(pattern string) (<-chan Event, func())
}
EventBus is the in-process pub/sub surface plugins consume. The concrete *inProcessBus in web4/pkg/daemon satisfies this; plugins retrieve it via Daemon.Bus() and Subscribe / Publish without knowing the underlying implementation.
type Factory ¶
type Factory func() Plugin
Factory builds a new instance of a plugin. Registered factories run when the daemon calls LoadAll; each call produces a fresh Plugin value, so plugins do not share state across daemon restarts.
type HandshakePendingRecord ¶
type HandshakePendingRecord struct {
NodeID uint32
PublicKey string
Justification string
ReceivedAt time.Time
}
HandshakePendingRecord mirrors the handshake plugin's PendingHandshake for the same reason as HandshakeTrustRecord.
type HandshakeService ¶
type HandshakeService interface {
IsTrusted(nodeID uint32) bool
TrustedPeers() []HandshakeTrustRecord
PendingRequests() []HandshakePendingRecord
PendingCount() int
SendRequest(peerNodeID uint32, justification string) error
ApproveHandshake(peerNodeID uint32) error
RejectHandshake(peerNodeID uint32, reason string) error
RevokeTrust(peerNodeID uint32) error
// WaitForTrust blocks until the peer transitions to trusted, or
// the timeout elapses. Returns true if trust was granted in
// time. Wired through the daemon so callers (typically pilotctl
// before a first send to a trusted-list peer) can block
// bidirectional operations on trust establishment instead of
// racing the data send against the handshake reply.
WaitForTrust(peerNodeID uint32, timeout time.Duration) bool
// ProcessRelayedRequest / ProcessRelayedApproval /
// ProcessRelayedRejection are invoked from the daemon's relay
// poller after parsing the registry-inbox payload.
ProcessRelayedRequest(fromNodeID uint32, justification string)
ProcessRelayedApproval(fromNodeID uint32)
ProcessRelayedRejection(fromNodeID uint32)
// Stop drains background RPCs and stops the replay reaper.
Stop()
}
HandshakeService is the daemon-facing surface of the manual trust-handshake plugin (port 444). The plugin's *Manager satisfies this via Go's structural typing — the daemon engine never imports the handshake package.
All trust-handshake operations route through this interface: IPC command dispatch, trust-gate checks on inbound SYN / datagrams, registry-relay polling, and trust-pair re-sync after reconnect.
type HandshakeTrustRecord ¶
type HandshakeTrustRecord struct {
NodeID uint32
PublicKey string
ApprovedAt time.Time
Mutual bool
Network uint16
}
HandshakeTrustRecord mirrors the handshake plugin's TrustRecord so the daemon-facing HandshakeService interface stays primitive- only (no upward import). Field set is identical to the plugin's TrustRecord — the plugin's adapter returns a converted []HandshakeTrustRecord built from its own TrustRecord values.
type Listener ¶ added in v0.4.0
type Listener interface {
// Accept blocks until an inbound Connection lands on the
// listener or the listener is closed. ok=false signals close
// (no further Connections will arrive).
Accept() (conn Connection, ok bool)
// Port returns the bound port. Stable for the listener's lifetime.
Port() uint16
// Close releases the port and stops accepting new Connections.
Close() error
}
Listener is a bound port that yields inbound Connection values via Accept. Plugins (notably handshake on PortHandshake) hold a Listener for the lifetime of their server loop.
type Plugin ¶
type Plugin interface {
// Name returns the registration key of this plugin. Must be
// stable across releases of the plugin; the daemon engine may
// use it for keyed lookups, persistence, and operator-facing
// status output.
Name() string
// Init wires the plugin to a running daemon engine. The plugin
// retains the Daemon for the rest of its lifetime. Init returns
// an error if the plugin cannot bootstrap; the daemon aborts
// startup on any plugin Init failure.
Init(d Daemon) error
// Shutdown stops the plugin's background work and drains any
// in-flight requests. The Daemon passed to Init is still valid
// during Shutdown — plugins may use it for last-mile work — but
// the daemon engine will not accept new traffic. Shutdown should
// honor the context's deadline; the daemon will not wait
// indefinitely on a stuck plugin.
Shutdown(ctx context.Context) error
}
Plugin is the lifecycle contract every daemon plugin implements. The daemon calls Init once after the engine is running and Shutdown once when the engine is stopping. Name returns a stable identifier used for registration, log lines, and metrics labels.
A plugin holds the Daemon it was initialized with for the rest of its lifetime; the daemon engine will not be replaced under it. If the daemon shuts down, plugins receive Shutdown before the engine finishes its own teardown — they are guaranteed to be able to use the Daemon during Shutdown for any final cleanup (closing pending streams, dispatching final webhook deliveries, etc.).
func LoadAll ¶
LoadAll instantiates every registered plugin against d and calls Init in sorted-by-name order. Returns the slice of loaded plugins so the caller can drive Shutdown later, plus the first error encountered (subsequent plugins are not started after a failure).
Plugins are returned in registration-sorted order so Shutdown can run them in reverse and respect inter-plugin ordering by giving later-named plugins priority during teardown. This matches the common pattern where alphabetic name choice doubles as a startup- order hint (a-something starts before z-something).
type PolicyEventType ¶
type PolicyEventType = string
PolicyEventType is the kind of protocol event a policy is evaluated against. Type alias to string so plugin signatures stay primitive end-to-end.
type PolicyManager ¶
type PolicyManager interface {
Start(netID uint16, policyJSON []byte) (PolicyRunner, error)
Stop(netID uint16)
Get(netID uint16) PolicyRunner
All() []PolicyRunner
StopAll()
LoadPersisted() error
}
PolicyManager owns the per-network registry of policy runners.
type PolicyRunner ¶
type PolicyRunner interface {
NetworkID() uint16
HasMember(peerNodeID uint32) bool
// EvaluatePortGate takes a string event-type ("connect", "dial",
// "datagram", ...). The plugin's EventType is a type alias to
// coreapi.PolicyEventType which is itself a type alias to string,
// so plugin signatures match this exactly.
EvaluatePortGate(
eventType string,
port uint16,
peerNodeID uint32,
payloadSize int,
direction string,
localTags, nodeInfoTags []string,
) bool
EvaluateActions(eventType string, ctx map[string]any)
Status() map[string]any
PeerList() []map[string]interface{}
ForceCycle() map[string]any
ReconcileNow()
PolicyJSON() ([]byte, error)
Stop()
}
PolicyRunner is the daemon-facing surface of a single network's running policy. The plugin's concrete *PolicyRunner satisfies this via structural typing.
type PortAllocator ¶
type PortAllocator interface {
// Bind takes ownership of the given port and returns a Listener
// that will receive Connections targeting it. Returns an error
// if the port is already bound.
Bind(port uint16) (Listener, error)
// Unbind releases the port. The Listener returned by Bind also
// stops accepting (its Accept loop returns ok=false). Idempotent;
// unbinding an unbound port is a no-op.
Unbind(port uint16)
}
PortAllocator is the daemon's port table. Plugins receive it via Daemon.Ports() and bind / unbind well-known ports through it. The concrete *daemon.PortManager satisfies this via structural typing.
type TrustChecker ¶
TrustChecker is the daemon-facing surface of the trustedagents plugin. The handshake handler consults this for auto-accept.
type TunnelRegistry ¶
type TunnelRegistry interface {
// RemovePeer tears down the encrypted tunnel for the given peer.
// All per-peer state (session keys, retransmit queues, routing
// entries) is dropped. Safe to call when no tunnel exists.
RemovePeer(nodeID uint32)
}
TunnelRegistry is the daemon's tunnel table. Plugins use RemovePeer to tear down a peer's tunnel state when revoking trust or closing a connection. The concrete *daemon.TunnelManager satisfies this via structural typing.
type VersionInfo ¶ added in v0.4.3
type VersionInfo struct {
// Version is the semver of the running daemon, e.g. "v1.10.5".
Version string
// Commit is the git SHA the daemon was built from. Empty in dev builds.
Commit string
// BuildTime is the UTC timestamp the daemon binary was assembled.
BuildTime string
}
VersionInfo describes a daemon's currently-running version and build metadata. Plugins call Daemon.VersionInfo() to discover what they're hosted by, useful for compatibility checks before issuing version- sensitive RPCs.
type WebhookManager ¶
type WebhookManager interface {
// SetURL hot-swaps the active webhook URL. Empty URL disables
// delivery (no-op until set again).
SetURL(url string)
// Stats returns dispatcher counters for daemon Info. All-zero
// when no client is configured (nil-safe at the plugin level).
Stats() WebhookStats
}
WebhookManager is the daemon-facing surface of the webhook plugin. The plugin owns the HTTP client; the daemon only needs to (a) hot-swap the URL when IPC's set-webhook fires and (b) read counters for the daemon info health snapshot.
type WebhookStats ¶
WebhookStats is the daemon-facing mirror of the webhook plugin's Stats. Same shape, different package — the daemon engine can hold the value type without importing the plugin.