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"` // 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"` }