Every held thing carries what a take compares: for a found container its image and the image's date, the networks it is on and the other containers on each, its mounts and published ports, beside the declared image (and its date once pulled), ports and volumes, with the downgrade decided when both dates are known; for a found file whether the declared content differs and how, as lines lost and lines new. A resource whose target moved keeps the former target on record as an orphan, so the next apply removes the container or file the host wrote under the old name (issue 097). Every apply reports the strays: containers the mesh neither wrote nor holds.
236 lines
11 KiB
Go
236 lines
11 KiB
Go
package link
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The wire formats shared with the control plane, which defines them separately because this
|
|
// binary requires nothing present and does not import it. A test on each side asserts the field
|
|
// names, so a rename breaks both at once rather than on a real machine months later.
|
|
|
|
// Routing keys a node may publish. Its broker account is scoped to this exchange and its own
|
|
// queue, so it can say these things and nothing else.
|
|
const (
|
|
KeyReport = "report"
|
|
KeyAlive = "alive"
|
|
)
|
|
|
|
// Alive is a node saying nothing except that it is there.
|
|
//
|
|
// novox/hq 09-the-node-lifecycle: *how long it has been disconnected is a fact the mesh must
|
|
// hold, and nothing holds it today. Without it, a node running last month's assignments looks
|
|
// exactly like one that is current.*
|
|
//
|
|
// Separate from a report because the two happen at completely different rates: a node is alive
|
|
// constantly and applies something rarely, and reading one as the other would make a quiet node
|
|
// look like a stale one.
|
|
type Alive struct {
|
|
Node string `json:"node"`
|
|
}
|
|
|
|
// Signed is a declaration and the signature over it.
|
|
//
|
|
// novox/hq ADR 0004: the transport is verified once at connect, and **each declaration is
|
|
// verified by its signature, every time**. The two are different questions — a node connects to
|
|
// the broker and takes instruction from the control plane behind it, and pinning only the first
|
|
// would make the second transitive.
|
|
//
|
|
// The signature is over Declaration exactly as it arrived, bytes unchanged. Re-encoding before
|
|
// verifying would mean checking a signature over something other than what was sent, and any
|
|
// difference in key order or spacing would break it — so the raw message is what is signed and
|
|
// what is checked.
|
|
type Signed struct {
|
|
Declaration []byte `json:"declaration"`
|
|
Signature []byte `json:"signature"`
|
|
}
|
|
|
|
// Report is what a node says after applying, and it is a statement rather than a write.
|
|
//
|
|
// A node states; the context that owns the data writes (novox/hq ADR 0006). The difference is the
|
|
// security boundary: something that can write cannot be prevented from writing anything, and
|
|
// something that can only state has its blast radius bounded by what this struct can say.
|
|
type Report struct {
|
|
Node string `json:"node"`
|
|
|
|
// Applied is what this machine now owns, by resource id.
|
|
Applied []string `json:"applied,omitempty"`
|
|
|
|
// Failed says what could not be applied, and why, in words for a person.
|
|
Failed map[string]string `json:"failed,omitempty"`
|
|
|
|
// Refused is set when the declaration was rejected whole rather than applied in part.
|
|
Refused string `json:"refused,omitempty"`
|
|
|
|
// Superseded names the newer declaration this one was set aside for, unapplied.
|
|
//
|
|
// A machine asked to be five successive things becomes the last one (novox/hq issue 031):
|
|
// when several declarations are waiting, the host applies the newest and acknowledges the
|
|
// rest without applying them. Each of those is still reported, because silence reads as a
|
|
// machine that ignored an instruction and "applied" would be a lie — this is the third word.
|
|
Superseded string `json:"superseded,omitempty"`
|
|
|
|
// Carried are the machine's ports held by what this host raised from its own bundle.
|
|
//
|
|
// **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node
|
|
// raises its foundation before any mesh exists, so the control plane has never heard of the
|
|
// store or the broker — and would hand a module a port one of them holds, discovering it only
|
|
// when a container runtime refused to start.
|
|
//
|
|
// A node *states* and the mesh writes, which is the whole shape of this message: this is the
|
|
// machine saying what is true of it, not asking for anything.
|
|
Carried []int `json:"carried,omitempty"`
|
|
|
|
// Declared is the digest of the declaration this report is about — sha256 of the exact bytes
|
|
// the mesh sent, which the mesh recorded when it sent them.
|
|
//
|
|
// **Which declaration, not when.** The mesh compared its send time to this report's arrival
|
|
// to decide whether a machine had caught up, and lost the race it invited: an apply started
|
|
// under the previous declaration finishes after the next one is sent, its report lands newer
|
|
// than the send, and the machine reads as caught up with words it has not read yet. Clocks
|
|
// cannot answer "which"; the digest is the answer itself.
|
|
Declared string `json:"declared,omitempty"`
|
|
|
|
// Held is what this adopted node found and is keeping as it was until its module is taken
|
|
// (novox/hq ADR 0100). Without it an adopted node reads as converged.
|
|
Held []Held `json:"held,omitempty"`
|
|
|
|
// Firewall is the firewall found on this machine — "ufw" or "none" — and empty on a node that
|
|
// was never asked, which is every converged one.
|
|
Firewall string `json:"firewall,omitempty"`
|
|
|
|
// Reachable is what can be reached on this machine now: every listening socket and every
|
|
// published container port. Only an adopted node reports it; it is what converging the node
|
|
// previews, so nothing closes without being named first.
|
|
Reachable []Reach `json:"reachable,omitempty"`
|
|
|
|
// Tunnel is what this adopted node says about the tunnel it found and carried (novox/hq ADR
|
|
// 0105): which interface, its port, range and peer count, whether the found interface is down
|
|
// and the mesh's up in its place, and where the found configuration's original was kept.
|
|
Tunnel *CarriedTunnel `json:"tunnel,omitempty"`
|
|
|
|
// Strays is what runs on the machine that the mesh neither wrote nor holds (novox/hq ADR
|
|
// 0163): containers nobody declared and nobody holds, the ones a cutover leaves behind.
|
|
Strays []Stray `json:"strays,omitempty"`
|
|
|
|
// Profile is what this machine can do, detected again by the apply that reports (novox/hq
|
|
// ADR 0161) — the same shape enrolment sends — so a capability gained or lost since enrolment,
|
|
// a network manager switched, reaches the mesh at the next push rather than never.
|
|
Profile map[string]any `json:"profile,omitempty"`
|
|
|
|
// Host is the version of the host that produced this report (novox/hq ADR 0141).
|
|
//
|
|
// Without it nothing can say a machine is behind, so "every machine current with its source"
|
|
// could not include the host — the one component the mesh did not deliver. It is a fact the
|
|
// machine states about itself, like the firewall it found and the links that face outside.
|
|
Host string `json:"host,omitempty"`
|
|
|
|
// Outward is the links on this machine that face outside it — the ones carrying a default
|
|
// route (novox/hq ADR 0140). Every node reports it, adopted or converged, because a converged
|
|
// node's filter is written around it.
|
|
//
|
|
// **It replaces a list of addresses.** The filter used to block everything passing through the
|
|
// machine and then allow the machine's own containers back by naming the ranges they sit on.
|
|
// A range describes one machine and goes stale in silence; the link carrying the default route
|
|
// is read afresh on every report and does not change when a module is added or removed.
|
|
//
|
|
// Empty means this machine has no route off itself. The mesh then composes no filter for it and
|
|
// leaves the one it has, rather than writing a rule around a link with no name — a rule set
|
|
// that does not load is a machine filtering nothing while its unit reports success.
|
|
Outward []string `json:"outward,omitempty"`
|
|
|
|
// Rekey is this node taking a found tunnel's key as its overlay key after enrolment (novox/hq
|
|
// ADR 0105). Not an account of the machine: a report carrying one says nothing else.
|
|
Rekey *Rekey `json:"rekey,omitempty"`
|
|
}
|
|
|
|
// CarriedTunnel is this node's account of the tunnel it took over. State is one of the Carried
|
|
// states below; Note is what the host did about it, when it did something.
|
|
type CarriedTunnel struct {
|
|
Interface string `json:"interface"`
|
|
Port int `json:"port"`
|
|
Range string `json:"range"`
|
|
Peers int `json:"peers"`
|
|
State string `json:"state"`
|
|
Note string `json:"note,omitempty"`
|
|
Kept string `json:"kept,omitempty"`
|
|
}
|
|
|
|
// The states a carried tunnel can be in: the found interface still up and the mesh's not; the
|
|
// found one down and the mesh's up with its key; or the found one down and the mesh's not up — the
|
|
// one state where the peers reach nothing, said as its own word so nothing reads it as either of
|
|
// the others.
|
|
const (
|
|
CarriedNotTaken = "not-taken"
|
|
CarriedTaken = "taken"
|
|
CarriedDown = "down"
|
|
)
|
|
|
|
// Rekey is this node saying it took a found tunnel's key as its overlay key after enrolling
|
|
// (novox/hq ADR 0105): the path for a node that enrolled before the mesh knew to take a tunnel
|
|
// over, since re-enrolling would rotate every key it holds. Signed with the identity key over
|
|
// RekeyProof, so a report forged on a stolen broker account cannot move this node's overlay key.
|
|
type Rekey struct {
|
|
// Previous is the overlay key this node held until now, as the mesh records it; the mesh
|
|
// refuses a rekey naming another, which is how a replayed one is refused.
|
|
Previous string `json:"previous"`
|
|
OverlayKey string `json:"overlay_key"`
|
|
Tunnel *Tunnel `json:"tunnel"`
|
|
Proof []byte `json:"proof"`
|
|
}
|
|
|
|
// RekeyProof is what a node signs when it rekeys — the node, the key it leaves, the key it takes
|
|
// and the tunnel it took it from — so a proof cannot be moved to another node or another tunnel.
|
|
// Byte for byte the mesh's own (mesh-controller internal/link RekeyProof).
|
|
func RekeyProof(node, previous, key string, tunnel *Tunnel) []byte {
|
|
var t Tunnel
|
|
if tunnel != nil {
|
|
t = *tunnel
|
|
}
|
|
peers := make([]string, 0, len(t.Peers))
|
|
for _, p := range t.Peers {
|
|
peers = append(peers, p.PublicKey+"@"+p.Address)
|
|
}
|
|
return []byte("novox-mesh-rekey\x00" + node + "\x00" + previous + "\x00" + key + "\x00" +
|
|
t.Interface + "\x00" + t.Unit + "\x00" + t.Config + "\x00" + strconv.Itoa(t.Port) + "\x00" +
|
|
t.Address + "\x00" + t.Range + "\x00" + t.PublicKey + "\x00" + strings.Join(peers, ","))
|
|
}
|
|
|
|
// Held is one file or container found on an adopted node and kept as it was.
|
|
type Held struct {
|
|
ID string `json:"id"`
|
|
Module string `json:"module"`
|
|
Kind string `json:"kind"`
|
|
Target string `json:"target"`
|
|
Since time.Time `json:"since"`
|
|
// Changed is what something other than the mesh did to it since — rewritten, stopped,
|
|
// replaced or gone — and empty while it is as found.
|
|
Changed string `json:"changed,omitempty"`
|
|
// Kept is where a file's original was kept.
|
|
Kept string `json:"kept,omitempty"`
|
|
// Facts is the found thing beside what the module declares — what a take compares (novox/hq
|
|
// ADR 0163). The same shape the host keeps; the controller reads it as data.
|
|
Facts map[string]any `json:"facts,omitempty"`
|
|
}
|
|
|
|
// A Stray is a container the mesh neither wrote nor holds (ADR 0163).
|
|
type Stray struct {
|
|
Kind string `json:"kind"`
|
|
Name string `json:"name"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// Reach is one thing reachable on the machine: a listening socket, or a published container port.
|
|
type Reach struct {
|
|
Protocol string `json:"protocol"`
|
|
Address string `json:"address"`
|
|
Port int `json:"port"`
|
|
// By is what holds it — a process, or a container's name.
|
|
By string `json:"by,omitempty"`
|
|
// Published is a container port the runtime publishes, reached on the forwarded path; its
|
|
// container's own port is ContainerPort.
|
|
Published bool `json:"published,omitempty"`
|
|
ContainerPort int `json:"container-port,omitempty"`
|
|
}
|