`declare` sends a node a signed declaration; `serve` now also consumes reports. Signed over the exact bytes published, which is what the node verifies. Anything re-encoding in between would sign one thing and check another, and a difference in key order alone would have a node refuse a declaration that was genuinely the mesh's. Sent to the node's queue directly rather than through the exchange: a declaration is for one node, and routing by name through a shared exchange means a binding per node that nothing removes when a node is retired. Enrolment now issues the node its own broker password, replacing the token's secret, and tells it the broker address, the fingerprint and the signing key -- so a node can reconnect after a restart without a person and a new token, which is what makes disconnection ordinary rather than a crisis. A report is a statement, not a write. What a node says it applied is its own account of its own machine, kept as a copy for recovery rather than as a source.
94 lines
4.1 KiB
Go
94 lines
4.1 KiB
Go
// Package link is the control plane's side of the connection nodes hold open.
|
|
//
|
|
// novox/hq ADR 0002: nodes communicate over a message broker, not over HTTP. One exchange, and
|
|
// the control plane is the single consumer behind it — ADR 0006 makes that a property worth
|
|
// having rather than an accident, because two consumers sharing a queue silently split the
|
|
// traffic between them, each receiving half of what it expects. That has happened here before.
|
|
package link
|
|
|
|
// Exchange is where nodes publish everything they have to say.
|
|
const Exchange = "mesh"
|
|
|
|
// ControlQueue is what the control plane consumes. One queue, one consumer.
|
|
const ControlQueue = "control"
|
|
|
|
// Routing keys. A node may publish these; it may not publish anything else, because its broker
|
|
// account is scoped to this exchange and its own queue.
|
|
const (
|
|
KeyEnrol = "enrol"
|
|
KeyReport = "report"
|
|
)
|
|
|
|
// QueueFor is the queue a node consumes from — the only one it may read.
|
|
func QueueFor(node string) string { return "node." + node }
|
|
|
|
// EnrolRequest is what a joining node says.
|
|
//
|
|
// It arrives on a connection the broker has already authenticated, because the account was
|
|
// created when the token was issued and the token's secret is its password. So this message is
|
|
// not how a node gets in — it is what it says once it is in.
|
|
type EnrolRequest struct {
|
|
// Node is what this machine believes it is called. Checked against the token, never trusted.
|
|
Node string `json:"node"`
|
|
|
|
// Secret is the one-time right to join. The account password and this are the same string,
|
|
// which is deliberate: the broker proves somebody holds the token, and this proves the same
|
|
// thing to the control plane without the control plane having to ask the broker who connected.
|
|
Secret string `json:"secret"`
|
|
|
|
// PublicKey is what the mesh will believe from now on. The node generated it; the private
|
|
// half has never left that machine (novox/hq ADR 0004).
|
|
PublicKey []byte `json:"public_key"`
|
|
|
|
// Profile is what this machine can be asked to do. The control plane cannot decide what a
|
|
// node should run without it, so it arrives with enrolment rather than being asked for after.
|
|
Profile map[string]any `json:"profile,omitempty"`
|
|
}
|
|
|
|
// Signed is a declaration and the signature over it.
|
|
//
|
|
// The signature is over Declaration exactly as it will arrive, bytes unchanged — a node verifies
|
|
// what it received rather than what it re-encoded, because any difference in key order or spacing
|
|
// would break a signature over the same meaning.
|
|
type Signed struct {
|
|
Declaration []byte `json:"declaration"`
|
|
Signature []byte `json:"signature"`
|
|
}
|
|
|
|
// Report is what a node states after applying. It states; the owning context writes.
|
|
type Report struct {
|
|
Node string `json:"node"`
|
|
Applied []string `json:"applied,omitempty"`
|
|
Failed map[string]string `json:"failed,omitempty"`
|
|
Refused string `json:"refused,omitempty"`
|
|
}
|
|
|
|
// EnrolReply is what the mesh says back.
|
|
type EnrolReply struct {
|
|
// Accepted says whether the node is now known.
|
|
Accepted bool `json:"accepted"`
|
|
|
|
// Node is the name the mesh has for this machine, which settles any disagreement: the token
|
|
// was issued for a node record, and that record's name wins over what the machine called
|
|
// itself.
|
|
Node string `json:"node,omitempty"`
|
|
|
|
// Queue is where this node listens from now on.
|
|
Queue string `json:"queue,omitempty"`
|
|
|
|
// Password is this node's own broker account from now on, replacing the token's secret.
|
|
// A credential that lives for as long as the node should not be the same string as one that
|
|
// was meant to be used once.
|
|
Password string `json:"password,omitempty"`
|
|
|
|
// Fingerprint and Signer are what the node keeps so it can reconnect and keep verifying
|
|
// without a person and a new token.
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
Signer []byte `json:"signer,omitempty"`
|
|
Broker string `json:"broker,omitempty"`
|
|
|
|
// Refusal says why not, in words for a person. Deliberately the same for every reason a
|
|
// token can fail — unknown, spent, expired — so that guessing learns nothing.
|
|
Refusal string `json:"refusal,omitempty"`
|
|
}
|