Documentation
¶
Overview ¶
Package cellagent is the logic of headmesh-cellagent, the process that runs beside each Headscale.
The agent applies desired state from the Headmesh server to its local Headscale, but it never trusts that state blindly. Every change passes local guardrails that protect a production tailnet even from a buggy or compromised control server:
- operations must carry a valid Ed25519 signature from a pinned server key, must not be expired and are executed at most once;
- observe mode and an operator "freeze" refuse every change;
- protected nodes can never be deleted, retagged, renamed, expired or given routes;
- deletes and retags are rate limited; exit-node routes are never approved;
- a policy is applied only if it parses, keeps every local reachability invariant, and passes Headscale's own CheckPolicy (which also runs the policy's isolation tests);
- the last policy that applied cleanly is kept on disk for one-command rollback.
Future policy revisions (scheduled grant expiries) are applied on time by Tick even while the server is unreachable.
Index ¶
- Variables
- func SignOp(key ed25519.PrivateKey, op *cellv1.Op) error
- func ToNode(n *headscalev1.Node) *cellv1.Node
- type Agent
- func (a *Agent) AcceptServerKey(key []byte) error
- func (a *Agent) Freeze() error
- func (a *Agent) Frozen() bool
- func (a *Agent) HandleOp(ctx context.Context, op *cellv1.Op) *cellv1.OpResult
- func (a *Agent) Observe(ctx context.Context) *cellv1.Observed
- func (a *Agent) Rollback(ctx context.Context) error
- func (a *Agent) Run(ctx context.Context, client cellv1connect.CellServiceClient, hello HelloInfo, ...) error
- func (a *Agent) SetTimeline(tl []*cellv1.PolicyRevision) error
- func (a *Agent) Status() Status
- func (a *Agent) Tick(ctx context.Context) *cellv1.PolicyApplied
- func (a *Agent) Unfreeze() error
- type Config
- type HelloInfo
- type Invariant
- type RunOptions
- type Status
Constants ¶
This section is empty.
Variables ¶
var ErrRefused = errors.New("refused by cell guardrail")
ErrRefused marks a change a guardrail rejected. Results carry refused_by_guardrail so the server can alert.
Functions ¶
Types ¶
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent applies desired state to one Headscale under local guardrails. It is safe for concurrent use.
func New ¶
New returns an Agent. It restores the record of executed operations from StateDir so a restart cannot replay an operation.
func (*Agent) AcceptServerKey ¶
AcceptServerKey records the op-signing key the server announced, refusing keys that are not pinned.
func (*Agent) Freeze ¶
Freeze stops all changes until Unfreeze. It is a file in StateDir so it survives restarts and can be set by an operator with `headmesh-cellagent freeze` even while the agent runs.
func (*Agent) HandleOp ¶
HandleOp verifies and executes one operation and returns its result. It never panics on bad input.
func (*Agent) Observe ¶
Observe returns a full snapshot of Headscale's nodes and the active policy digest.
func (*Agent) Rollback ¶
Rollback re-applies the last policy that applied cleanly. It is the operator's undo button and works without the server.
func (*Agent) Run ¶
func (a *Agent) Run(ctx context.Context, client cellv1connect.CellServiceClient, hello HelloInfo, opt RunOptions) error
Run keeps a Sync stream to the server open, reconnecting with jittered exponential backoff, and applies the policy timeline on every tick whether or not the server is reachable. It returns when ctx is cancelled.
func (*Agent) SetTimeline ¶
func (a *Agent) SetTimeline(tl []*cellv1.PolicyRevision) error
SetTimeline replaces the desired policy timeline. Revisions must be strictly increasing in both revision number and effective time.
type Config ¶
type Config struct {
CellName string
Mode cellv1.Mode
// StateDir holds last-known-good policy, the freeze flag and the record of executed operations.
StateDir string
// ServerKeys are the Ed25519 public keys allowed to sign operations. The key announced in Welcome must be
// one of them, so a compromised transport cannot substitute its own.
ServerKeys []ed25519.PublicKey
Invariants []Invariant
ProtectedNodes []uint64
// MaxDeletesPerMinute and MaxRetagsPerHour bound blast radius. Zero means the defaults (5 and 50).
MaxDeletesPerMinute int
// MaxPreAuthKeyTTL bounds pre-auth key lifetimes. Zero means 24h.
MaxPreAuthKeyTTL time.Duration
Now func() time.Time
}
Config controls an Agent. Cell-local safety settings live here, on the cell host, not on the server.
type Invariant ¶
type Invariant struct {
Name string `json:"name"`
Src []string `json:"src"`
Dst []string `json:"dst"`
Proto string `json:"proto"`
Port int `json:"port"`
}
Invariant is reachability that must survive every policy change on this cell, for example "control nodes reach GPU hosts on tcp:22". Principals use Headscale policy syntax (tags or user@).
type RunOptions ¶
type RunOptions struct {
TickInterval time.Duration // how often scheduled revisions are checked (default 1s)
ObserveInterval time.Duration // how often a full node snapshot is sent (default 30s)
MinBackoff time.Duration // reconnect backoff floor (default 1s)
MaxBackoff time.Duration // reconnect backoff ceiling (default 30s)
Logger *slog.Logger
}
RunOptions tune the run loop. Zero values use production defaults.
type Status ¶
type Status struct {
Cell string `json:"cell"`
Mode string `json:"mode"`
Frozen bool `json:"frozen"`
ActiveRevision uint64 `json:"active_revision"`
ActiveSHA256 string `json:"active_sha256"`
PendingCount int `json:"pending_revisions"`
NextChangeAt time.Time `json:"next_change_at,omitzero"`
}
Status is the local, operator-facing summary printed by `headmesh-cellagent status`.