From 19d2725c13dc0a2188ecad2f6639674f62d83226 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 16:52:00 +0200 Subject: [PATCH] A module can name the mesh's range: ${machine:mesh-range} (novox/hq ADR 0112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module cannot know the private network's CIDR — it is a per-mesh value chosen at genesis — but sometimes must name it: an intrusion filter that must never ban a tunnel peer. Carry the overlay range on the Rendering and offer it as the machine fact mesh-range, the same way a machine's own address is offered, so the module names it rather than hardcoding a value (data is the mesh's). Absent when the mesh has no range. Enables the fail2ban ignoreip fix. --- cmd/mesh-controller/plan.go | 12 ++++++++-- internal/catalogue/declaration.go | 8 ++++++- internal/catalogue/machine_into_files.go | 8 ++++++- internal/catalogue/machine_into_files_test.go | 23 +++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/cmd/mesh-controller/plan.go b/cmd/mesh-controller/plan.go index 6dcb96e..781f330 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -488,6 +488,13 @@ func renderingFor(ctx context.Context, open *stores, node string, if err != nil { return catalogue.Rendering{}, inventory.Node{}, err } + // The private network's range, offered to a module as ${machine:mesh-range} — a module that must + // name the whole mesh (an intrusion filter that must never ban a tunnel peer) names it here + // rather than hardcoding a value it cannot know. + meshRange, err := overlayRange(ctx, inv) + if err != nil { + return catalogue.Rendering{}, inventory.Node{}, err + } // The artifact store as this node reaches it now — the address every image and archive the // mesh built is fetched through, composed here and recorded nowhere — with what the mesh has @@ -597,8 +604,9 @@ func renderingFor(ctx context.Context, open *stores, node string, Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports, Certificate: certificate, Authority: authority, Mesh: private, Names: names, Machines: machines, - Suffix: overlay.Suffix(), Foundation: foundation, Kept: kept, Adopted: record.Adopted, - Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built, + Suffix: overlay.Suffix(), MeshRange: meshRange, Foundation: foundation, Kept: kept, + Adopted: record.Adopted, + Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built, }, record, nil } diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index ad33e90..cb8b070 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -97,6 +97,12 @@ type Rendering struct { // compose it a second time. Suffix string + // MeshRange is the private network's CIDR (the range node addresses are allocated from), for a + // module that must name the whole mesh rather than one machine — an intrusion filter that must + // never ban a tunnel peer, say. A per-mesh value the module cannot know, so it is carried here + // and offered as ${machine:mesh-range}, the same way one machine's address is. + MeshRange string + // Kept is every operator-sealed secret in the mesh, for a module that `keeps` them. Nil when // nothing on this node keeps them, or the mesh has no operator key. Kept *KeptExport @@ -545,7 +551,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri // (novox/hq ADR 0112) — resolved once per module, named by ${dir:…} from any resource. dirs := dirsFor(m, with) // And the machine underneath, which no binding of its own can tell it. - thisMachine := machineFacts(r, with.Names) + thisMachine := machineFacts(r, with.Names, with.MeshRange) // Which of this module's files carry a secret, for the rule that a container may not read // one of them as its environment without saying so (ADR 0086, issue 041). diff --git a/internal/catalogue/machine_into_files.go b/internal/catalogue/machine_into_files.go index cb642bc..d47dab6 100644 --- a/internal/catalogue/machine_into_files.go +++ b/internal/catalogue/machine_into_files.go @@ -57,7 +57,7 @@ func machineUsed(content string) []string { // the hosts file and the resolver's wildcards are written from, so a file naming the machine's // address and the file every other machine reaches it by cannot disagree. Absent, like `at`, when // the machine is off the network or the mesh has not placed it. -func machineFacts(r Resolution, names map[string]string) map[string]string { +func machineFacts(r Resolution, names map[string]string, meshRange string) map[string]string { out := map[string]string{"name": r.Node} if r.At != "" { out["at"] = r.At @@ -65,6 +65,12 @@ func machineFacts(r Resolution, names map[string]string) map[string]string { out["address"] = address } } + // The private network's whole range — a mesh-wide fact, not this machine's, but named here + // because a module cannot know it and sometimes must (an intrusion filter that must never ban a + // tunnel peer). Absent when the mesh has no range to give. + if meshRange != "" { + out["mesh-range"] = meshRange + } return out } diff --git a/internal/catalogue/machine_into_files_test.go b/internal/catalogue/machine_into_files_test.go index e59f3a2..74bc60b 100644 --- a/internal/catalogue/machine_into_files_test.go +++ b/internal/catalogue/machine_into_files_test.go @@ -103,3 +103,26 @@ func TestAModuleNamesTheAddressBehindItsMachinesName(t *testing.T) { t.Fatalf("a machine off the network was given an address, or refused for another reason: %v", err) } } + +// The mesh's private range is offered as ${machine:mesh-range}, so a module names it rather than +// hardcoding a value it cannot know (novox/hq ADR 0112) — the fail2ban ignoreip is the case. +func TestAModuleNamesTheMeshRange(t *testing.T) { + facts := machineFacts(Resolution{Node: "anchor", At: "anchor.internal"}, + map[string]string{"anchor.internal": "10.10.0.1"}, "10.10.0.0/24") + if facts["mesh-range"] != "10.10.0.0/24" { + t.Fatalf("the mesh range is not a machine fact: %v", facts) + } + res := map[string]any{"type": "file", "id": "jail", "content": "ignoreip = 127.0.0.1/8 ${machine:mesh-range}\n"} + if err := machineInto(res, facts, "fail2ban"); err != nil { + t.Fatal(err) + } + if got := res["content"].(string); !strings.Contains(got, "10.10.0.0/24") || strings.Contains(got, "${machine:") { + t.Fatalf("the mesh range was not written in: %q", got) + } + // A mesh with no range gives no such fact, and a file that names it is refused rather than + // left with a literal placeholder in it. + none := machineFacts(Resolution{Node: "anchor"}, nil, "") + if _, has := none["mesh-range"]; has { + t.Fatal("a mesh with no range still offered one") + } +} -- 2.54.0