diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index c698795..94aa253 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -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.` 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{} diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index 8246eb5..8e93364 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -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() diff --git a/go/delivery/noderecord.go b/go/delivery/noderecord.go index b159d42..3b662b0 100644 --- a/go/delivery/noderecord.go +++ b/go/delivery/noderecord.go @@ -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 { diff --git a/go/delivery/noderecord_test.go b/go/delivery/noderecord_test.go index c816e71..e69e24f 100644 --- a/go/delivery/noderecord_test.go +++ b/go/delivery/noderecord_test.go @@ -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"}, } { diff --git a/go/delivery/placement.go b/go/delivery/placement.go index 4f48a1b..293fb0e 100644 --- a/go/delivery/placement.go +++ b/go/delivery/placement.go @@ -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 {