The tunnel found at adoption and the names of the hub's peers are kept in the placement; whether the hub took it over and who enrolled under a peer's key are read from placement and records, as the controller reads them (issue 1126, ADR 0555)
This commit is contained in:
@@ -11,8 +11,9 @@
|
||||
// indexer's state `placement`, one key per node (ADR 0549 §2);
|
||||
// - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and
|
||||
// accounts): the same placement key, Placement.Decided;
|
||||
// - what each node observes of itself (its tunnel key, capabilities, keys, networks, the tunnel it found, and on the
|
||||
// hub the adopted tunnel and its carried peers): the node-engine's state `records`;
|
||||
// - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the
|
||||
// node-engine's state `records`; the tunnel found at a node's adoption and the names of the hub's peers are decided,
|
||||
// in its placement, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both;
|
||||
// - every module's manifest: the indexer's `modules.<module>` and its manifests by digest, or the digest the
|
||||
// composing node's want names where it runs another build (Scope.Kept);
|
||||
// - each module's config values on a node: mesh-config's resolved state, the composing node's slice alone unless the
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -168,22 +170,24 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
|
||||
}
|
||||
in.Nodes = append(in.Nodes, n)
|
||||
in.Holdings = append(in.Holdings, p.Held()...)
|
||||
if rec.Tunnel != nil {
|
||||
if in.Tunnels == nil {
|
||||
in.Tunnels = map[string]compose.FoundTunnel{}
|
||||
// A tunnel found at adoption is the node's private network's when the node's tunnel key is the tunnel's (the
|
||||
// controller's inv.Tunnels); the hub's found tunnel is the mesh's adopted one whether or not the hub took it over
|
||||
// (inv.AdoptedTunnel), and its peers are carried once it did (inv.CarriedPeers).
|
||||
if t := p.Decided.Tunnel; t != nil {
|
||||
if rec.TunnelKey == t.PublicKey {
|
||||
if in.Tunnels == nil {
|
||||
in.Tunnels = map[string]compose.FoundTunnel{}
|
||||
}
|
||||
in.Tunnels[node] = compose.FoundTunnel{Tunnel: tunnelOf(*t), NodeAdopted: p.Decided.Adopted}
|
||||
}
|
||||
in.Tunnels[node] = compose.FoundTunnel{Tunnel: tunnelOf(*rec.Tunnel), NodeAdopted: p.Decided.Adopted}
|
||||
if p.Decided.Hub {
|
||||
in.Adopted = &compose.AdoptedTunnel{Tunnel: tunnelOf(*rec.Tunnel), Hub: node, Adopted: rec.HubAdopted}
|
||||
}
|
||||
}
|
||||
if p.Decided.Hub {
|
||||
for _, c := range rec.Carried {
|
||||
in.CarriedPeers = append(in.CarriedPeers, compose.CarriedPeer{PublicKey: c.PublicKey, Address: c.Address,
|
||||
EnrolledAs: c.EnrolledAs, Named: c.Named})
|
||||
in.Adopted = &compose.AdoptedTunnel{Tunnel: tunnelOf(*t), Hub: node, Adopted: rec.TunnelKey == t.PublicKey}
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Adopted != nil && in.Adopted.Adopted {
|
||||
in.CarriedPeers = carriedPeers(in.Adopted.Tunnel, placements[in.Adopted.Hub].Decided.PeerNames, records)
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return compose.Input{}, fmt.Errorf("frombus: modules are placed on %s, whose agent has written no record yet",
|
||||
strings.Join(missing, ", "))
|
||||
@@ -237,6 +241,31 @@ func tunnelOf(t delivery.RecordTunnel) compose.Tunnel {
|
||||
return out
|
||||
}
|
||||
|
||||
// carriedPeers is every peer of the hub's adopted tunnel, by address, each with the node that enrolled under its key
|
||||
// where one has (a node whose tunnel key is the peer's) and the name a person gave it (inv.CarriedPeers).
|
||||
func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]delivery.NodeRecord) []compose.CarriedPeer {
|
||||
byKey := map[string]string{}
|
||||
for node, rec := range records {
|
||||
if rec.TunnelKey != "" {
|
||||
byKey[rec.TunnelKey] = node
|
||||
}
|
||||
}
|
||||
var out []compose.CarriedPeer
|
||||
for _, p := range t.Peers {
|
||||
address, _, _ := strings.Cut(p.Address, "/")
|
||||
out = append(out, compose.CarriedPeer{PublicKey: p.PublicKey, Address: address, EnrolledAs: byKey[p.PublicKey], Named: names[p.PublicKey]})
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
a, errA := netip.ParseAddr(out[i].Address)
|
||||
b, errB := netip.ParseAddr(out[j].Address)
|
||||
if errA != nil || errB != nil {
|
||||
return out[i].Address < out[j].Address
|
||||
}
|
||||
return a.Less(b)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// nodesOf is every node named by a placement or a record, by name: the order the controller reads them in.
|
||||
func nodesOf(placements map[string]delivery.Placement, records map[string]delivery.NodeRecord) []string {
|
||||
seen := map[string]bool{}
|
||||
|
||||
@@ -67,11 +67,12 @@ func TestInput(t *testing.T) {
|
||||
keys := map[string]string{"anchor": publicKey(t), "laptop": publicKey(t)}
|
||||
caps := []string{"container-runtime", "systemd", "wireguard"}
|
||||
tunnel := &delivery.RecordTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
|
||||
Peers: []delivery.RecordTunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}}}
|
||||
Peers: []delivery.RecordTunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
|
||||
mk := func() *bus {
|
||||
return &bus{
|
||||
placements: map[string]delivery.Placement{
|
||||
"anchor": {Node: "anchor", Decided: delivery.NodeDecided{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1"},
|
||||
"anchor": {Node: "anchor", Decided: delivery.NodeDecided{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1",
|
||||
Tunnel: tunnel, PeerNames: map[string]string{"peer": "phone"}},
|
||||
Modules: []delivery.PlacedModule{{Module: overlay.Name}},
|
||||
Holds: []delivery.Holding{{Claim: "mesh-terminal", Scope: "mesh", Module: "xterm"}}},
|
||||
"laptop": {Node: "laptop", Decided: delivery.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"},
|
||||
@@ -79,8 +80,7 @@ func TestInput(t *testing.T) {
|
||||
{Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}}}},
|
||||
},
|
||||
records: map[string]delivery.NodeRecord{
|
||||
"anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Tunnel: tunnel, Contract: 2,
|
||||
Carried: []delivery.RecordCarriedPeer{{PublicKey: "peer", Address: "10.77.0.9", EnrolledAs: "phone"}}},
|
||||
"anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2},
|
||||
"laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps},
|
||||
},
|
||||
modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "unread": {}},
|
||||
@@ -101,7 +101,7 @@ func TestInput(t *testing.T) {
|
||||
}
|
||||
capsSet := map[string]bool{"container-runtime": true, "systemd": true, "wireguard": true}
|
||||
tun := compose.Tunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
|
||||
Peers: []compose.TunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}}}
|
||||
Peers: []compose.TunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
|
||||
want := compose.Input{
|
||||
Catalogue: map[string]catalogue.Manifest{overlay.Name: private, "xterm": parsed},
|
||||
Nodes: []compose.Node{
|
||||
@@ -112,10 +112,12 @@ func TestInput(t *testing.T) {
|
||||
Ports: map[string]map[int]int{"xterm": {80: 8080}},
|
||||
Settings: map[string][]catalogue.Layer{"xterm": {{From: "laptop", Values: map[string]any{"font": "mono"}}}}},
|
||||
},
|
||||
Holdings: []catalogue.Held{{Claim: "mesh-terminal", Scope: "mesh", Node: "anchor", Module: "xterm"}},
|
||||
Tunnels: map[string]compose.FoundTunnel{"anchor": {Tunnel: tun}},
|
||||
Adopted: &compose.AdoptedTunnel{Tunnel: tun, Hub: "anchor"},
|
||||
CarriedPeers: []compose.CarriedPeer{{PublicKey: "peer", Address: "10.77.0.9", EnrolledAs: "phone"}},
|
||||
Holdings: []catalogue.Held{{Claim: "mesh-terminal", Scope: "mesh", Node: "anchor", Module: "xterm"}},
|
||||
Tunnels: map[string]compose.FoundTunnel{"anchor": {Tunnel: tun}},
|
||||
Adopted: &compose.AdoptedTunnel{Tunnel: tun, Hub: "anchor", Adopted: true},
|
||||
// The controller's order is by address; laptop enrolled under its peer's key, and a person named the other.
|
||||
CarriedPeers: []compose.CarriedPeer{{PublicKey: keys["laptop"], Address: "10.77.0.2", EnrolledAs: "laptop"},
|
||||
{PublicKey: "peer", Address: "10.77.0.9", Named: "phone"}},
|
||||
}
|
||||
|
||||
b := mk()
|
||||
|
||||
+10
-24
@@ -45,7 +45,9 @@ func ParseNodeRecordKey(key string) (node string, err error) {
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// RecordTunnel is a tunnel found on the node, as compose.Tunnel holds it: public keys and addresses only.
|
||||
// RecordTunnel is a tunnel found on a node when it was adopted, as compose.Tunnel holds it: public keys and addresses
|
||||
// only. After the node took it over the tunnel is no longer there to observe, so it is kept as what was decided at
|
||||
// adoption, in the node's placement (NodeDecided.Tunnel).
|
||||
type RecordTunnel struct {
|
||||
Interface string `json:"interface"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
@@ -64,20 +66,11 @@ type RecordTunnelPeer struct {
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
// RecordCarriedPeer is a peer of the tunnel the hub adopted, which the hub carries (compose.CarriedPeer).
|
||||
type RecordCarriedPeer struct {
|
||||
PublicKey string `json:"public-key"`
|
||||
Address string `json:"address,omitempty"`
|
||||
EnrolledAs string `json:"enrolled-as,omitempty"`
|
||||
Named string `json:"named,omitempty"`
|
||||
}
|
||||
|
||||
// NodeRecord is one node's record: what the machine observes of itself (issue 1126's K2: what a person decides lives
|
||||
// in the catalogue's placement file, Placement.Decided; what the machine observes lives here). TunnelKey is the public
|
||||
// key of its private-network tunnel; Tunnel the tunnel found on it whose key that is; on the hub alone, HubAdopted says
|
||||
// whether the hub took its found tunnel over and Carried is every peer of it the hub carries, so the tunnels and the
|
||||
// carried peers are each one node's own and need no other home. Certificate is the one the mesh issued for its
|
||||
// internal name, public, as the node holds it.
|
||||
// NodeRecord is one node's record: what the machine observes of itself (issue 1126, ADR 0555: what a person decides
|
||||
// lives in the catalogue's placement file, Placement.Decided; what the machine observes lives here). TunnelKey is the
|
||||
// public key of its private-network tunnel; Certificate the one the mesh issued for its internal name, public, as the
|
||||
// node holds it. The tunnel found at adoption and the names given to its peers are decisions kept in the placement;
|
||||
// whether the hub took its found tunnel over, and which node enrolled under a carried peer's key, are read from both.
|
||||
type NodeRecord struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
@@ -90,18 +83,12 @@ type NodeRecord struct {
|
||||
Networks []catalogue.AttachedNetwork `json:"networks,omitempty"`
|
||||
NetworksSaid bool `json:"networks-said,omitempty"`
|
||||
Contract int `json:"contract,omitempty"`
|
||||
Tunnel *RecordTunnel `json:"tunnel,omitempty"`
|
||||
HubAdopted bool `json:"hub-adopted,omitempty"`
|
||||
Carried []RecordCarriedPeer `json:"carried,omitempty"`
|
||||
}
|
||||
|
||||
func (r NodeRecord) check() error {
|
||||
if err := checkName("node", r.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.HubAdopted && r.Tunnel == nil {
|
||||
return fmt.Errorf("delivery: the record of %s says the hub adopted its tunnel, and names none", r.Node)
|
||||
}
|
||||
for _, c := range r.Capabilities {
|
||||
if c == "" {
|
||||
return fmt.Errorf("delivery: the record of %s names an empty capability", r.Node)
|
||||
@@ -119,9 +106,8 @@ func EncodeNodeRecord(r NodeRecord) ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// DecodeNodeRecord reads a node record; it refuses an unknown version (ErrUnknownVersion), a node that is not a name, an
|
||||
// adopted tunnel with no tunnel, and an empty capability. Whether the node is the hub is the catalogue's (Placement.Decided):
|
||||
// a reader takes carried peers and an adopted tunnel only from the hub's record.
|
||||
// DecodeNodeRecord reads a node record; it refuses an unknown version (ErrUnknownVersion), a node that is not a name, and
|
||||
// an empty capability.
|
||||
func DecodeNodeRecord(data []byte) (NodeRecord, error) {
|
||||
var r NodeRecord
|
||||
if err := decode(data, &r); err != nil {
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
// check (ADR 0201 §13) refuses such a field in a state every node reads.
|
||||
var secretNamed = regexp.MustCompile(`(?i)(secret|passw|token|api[-_]?key|private[-_]?key|credential|bearer|cookie|salt|signing)`)
|
||||
|
||||
// Issue 1126 (K2): a node's record keys, reads back, refuses an adopted tunnel it does not name, and neither it nor the
|
||||
// placement file names a field like a secret.
|
||||
// Issue 1126 (ADR 0555): a node's record keys, reads back, refuses a node that is not a name and an empty capability, and
|
||||
// neither it nor the placement file names a field like a secret.
|
||||
func TestNodeRecord(t *testing.T) {
|
||||
key, err := NodeRecordKey("ace")
|
||||
if err != nil || key != "record.ace" || NodeRecordsBucket != "node-engine_records" {
|
||||
@@ -31,9 +31,7 @@ func TestNodeRecord(t *testing.T) {
|
||||
|
||||
hub := NodeRecord{Node: "novox", TunnelKey: "k", Capabilities: []string{"docker"}, SealingKey: "age1x", ServingKey: "pub",
|
||||
Certificate: "cert", OutwardLinks: []string{"eth0"}, Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}},
|
||||
NetworksSaid: true, Contract: 3,
|
||||
Tunnel: &RecordTunnel{Interface: "wg0", PublicKey: "k", Peers: []RecordTunnelPeer{{PublicKey: "p"}}},
|
||||
HubAdopted: true, Carried: []RecordCarriedPeer{{PublicKey: "p", EnrolledAs: "phone"}}}
|
||||
NetworksSaid: true, Contract: 3}
|
||||
raw, err := EncodeNodeRecord(hub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -44,7 +42,6 @@ func TestNodeRecord(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, r := range map[string]NodeRecord{
|
||||
"adopted with no tunnel": {Node: "novox", HubAdopted: true},
|
||||
"empty capability": {Node: "ace", Capabilities: []string{""}},
|
||||
"node that is not a name": {Node: "a.b"},
|
||||
} {
|
||||
|
||||
@@ -119,7 +119,8 @@ type Placement struct {
|
||||
// lives in the catalogue, what the machine observes lives in its record, NodeRecord): where it is dialled (endpoint) and
|
||||
// its site, whether it is the private network's hub, its address on the private network, the domain its routed names
|
||||
// are composed under, its operator and agent logins by name and their homes, whether it is adopted rather than
|
||||
// converged, and the modules taken on an adopted node. The address is a decision written once, at enrolment, by a
|
||||
// converged, the modules taken on an adopted node, the tunnel found on it at adoption and, on the hub, the names given
|
||||
// to that tunnel's peers. The address is a decision written once, at enrolment, by a
|
||||
// catalogue pull request: nothing allocates it at run time, and CheckPlacements refuses two nodes with one address.
|
||||
// No secret and no key material: accounts are names, never credentials.
|
||||
type NodeDecided struct {
|
||||
@@ -134,6 +135,10 @@ type NodeDecided struct {
|
||||
AgentHome string `json:"agent-account-home,omitempty"`
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
Taken []string `json:"taken,omitempty"`
|
||||
// Tunnel is the tunnel found on the node when it was adopted, kept as decided then: once the node takes it over it is
|
||||
// no longer there to observe. PeerNames is, on the hub, the name a person gave a peer of that tunnel, by its public key.
|
||||
Tunnel *RecordTunnel `json:"tunnel,omitempty"`
|
||||
PeerNames map[string]string `json:"peer-names,omitempty"`
|
||||
}
|
||||
|
||||
func (p Placement) check() error {
|
||||
|
||||
Reference in New Issue
Block a user