The supervision was already right: a clean exit means the host stood aside, and the launcher's next turn runs what is on disk. Two things made it dead code — nothing told the running host a successor was waiting, and the rollback resolved its known-good version through pacman, which no machine here uses and which two of three operating systems do not have. Keeping a version rather than a path was the clue. Versions now live in directories named for them: - the launcher picks the newest delivered one every time round the loop, or the one a rollback pinned, or the host placed by hand when nothing is delivered; - the running host stands aside between reconciles, never inside one, by exiting cleanly — and returns nil so the launcher does not count it as a crash; - a completed reconcile retires what is older than the predecessor, keeping the predecessor because that is what a rollback starts, and never the running one; - rollback pins the predecessor instead of reinstalling a package: no package manager, no cache anyone may clean, same script on every operating system; - the report says which host version produced it, so 'behind' is answerable. Newest is when it arrived, never how the name sorts: '1.10' orders before '1.9', and ordering by name would start an older host and call it an upgrade. novox/hq ADR 0141. The delivery half — a module carrying the next host — follows; until then nothing delivers a version and every machine takes the fallback, which is what it does today.
217 lines
10 KiB
Go
217 lines
10 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"`
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|