From 6f038810fd11e01b6d7857b85ab9af431281a1cd Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 11:45:17 +0100 Subject: [PATCH] Move route-proxy out of the controller's repository, which is archived with the controller route-proxy provides `route` to five modules and lived in novox/mesh-controller, archived once the controller is removed (issue 1126). Its history comes along (git filter-repo of modules/route-proxy and its earlier home examples/route-proxy). Its contract's Go module path names this repository, and route-proxy builds the contract from its own tree through a replace, so no tag is needed before the first build. The two manifest tests that held its public ACME account and mounted program lived in the controller's internal/ownmanifest; they move beside the module. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .gitignore | 4 + README.md | 11 ++ modules/route-proxy/README.md | 11 +- .../route-proxy/cmd/route-provider/main.go | 2 +- .../cmd/route-provider/manifest_test.go | 161 ++++++++++++++++++ .../cmd/route-provider/provider.go | 2 +- .../route-proxy/cmd/route-provider/tools.go | 2 +- .../cmd/route-provider/tools_test.go | 2 +- modules/route-proxy/cmd/route-proxy/bus.go | 2 +- modules/route-proxy/cmd/route-proxy/ledger.go | 2 +- .../cmd/route-proxy/ledger_test.go | 2 +- modules/route-proxy/cmd/route-proxy/main.go | 2 +- modules/route-proxy/cmd/route-proxy/plans.go | 2 +- .../route-proxy/cmd/route-proxy/reach_test.go | 2 +- .../cmd/route-proxy/routes_test.go | 2 +- modules/route-proxy/contract/go.mod | 2 +- .../route-proxy/contract/route.schema.json | 2 +- modules/route-proxy/go.mod | 5 +- modules/route-proxy/go.sum | 2 - 19 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 modules/route-proxy/cmd/route-provider/manifest_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a9bfb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor/ +# A compiled program never enters a commit (issue 1577): `go build .` in a command directory leaves one there. +modules/*/cmd/*/route-* +!modules/*/cmd/*/*.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..417332e --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# mesh-networking + +Novox Mesh — networking: the modules that carry traffic into and across the mesh, each under `modules/` with its +own manifest (`module.json`) and Go module. + +- **route-proxy** provides `route`: the public HTTP and HTTPS entry, serving each name a consumer asks for from the + seat mesh-provisioner's plans (its README says how). It moved here from the controller's repository, history kept, + when the controller was removed (issue 1126). + +Made by the mesh from the gitea module's config value `repositories` (novox/hq ADR 0428). Main takes no push: a change +reaches it by a pull request whose `mesh/check` passed and the operator approved. diff --git a/modules/route-proxy/README.md b/modules/route-proxy/README.md index f5ddb81..e45def7 100644 --- a/modules/route-proxy/README.md +++ b/modules/route-proxy/README.md @@ -18,9 +18,10 @@ consumer on its own machine. ## Its contract: a package of its own **route-proxy defines its own contract, and other modules use it** (the operator, 2026-10-31). It is the Go module -`git.novox.be/novox/mesh-controller/modules/route-proxy/contract`, in `contract/` here with its own `go.mod`, standard -library only, released by tags `modules/route-proxy/contract/vX.Y.Z`, the way mesh-sdk is released (`go/vX.Y.Z`); this -module depends on it by its version like any other. It holds: +`git.novox.be/novox/mesh-networking/modules/route-proxy/contract`, in `contract/` here with its own `go.mod`, standard +library only, released by tags `modules/route-proxy/contract/vX.Y.Z` of this repository, the way mesh-sdk is released +(`go/vX.Y.Z`); another module depends on it by its version like any other, and this module builds it from its own +tree (a `replace` of `./contract` in `go.mod`), so the two never drift. It holds: - **the route a consumer asks** (`Request`, read strictly by `DecodeRequest`: no field it does not name, a name of either kind, a port where the rule forwards, http or https, a positive body limit, a secret's file never a @@ -64,8 +65,8 @@ directory URL — `https://acme-v02.api.letsencrypt.org:443/directory`, port inc spelled it — and of the root bundle the `trust` container copies in. Any change to either is a new authority to the proxy: a new account, and every routed name ordered again against Let's Encrypt's rate limits. So the URL keeps the `:443`, `ACME_ROOTS_PATH` stays empty, and the `trust` artifact's -pinned image is not moved without a decision; mesh-controller's -`TestRouteProxyKeepsItsPublicAccountDirectory` holds all three. +pinned image is not moved without a decision; `TestRouteProxyKeepsItsPublicAccountDirectory` +(cmd/route-provider) holds all three. ## How it ships the Go proxy diff --git a/modules/route-proxy/cmd/route-provider/main.go b/modules/route-proxy/cmd/route-provider/main.go index b1eecda..613405e 100644 --- a/modules/route-proxy/cmd/route-provider/main.go +++ b/modules/route-proxy/cmd/route-provider/main.go @@ -14,7 +14,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/provider" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) func say(format string, args ...any) { diff --git a/modules/route-proxy/cmd/route-provider/manifest_test.go b/modules/route-proxy/cmd/route-provider/manifest_test.go new file mode 100644 index 0000000..40fdef3 --- /dev/null +++ b/modules/route-proxy/cmd/route-provider/manifest_test.go @@ -0,0 +1,161 @@ +package main + +import ( + "path/filepath" + "slices" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Moved from mesh-controller's internal/ownmanifest (issue 1126), which is archived with the controller: the two tests +// that held route-proxy's manifest to what keeps its public account and runs its program, now beside the module. + +// The public issuer is the proxy's own fact (novox/hq ADR 0226), and the folding of it must not move the proxy's account. +// +// route-proxy keeps each ACME authority's account and certificates in a directory named after a digest of the directory +// URL and of the root bundle its `trust` container copies in (cmd/route-proxy, forThisAuthority). Until ADR 0226 the URL +// was rendered from a binding to `public-acme`'s `acme-ca`, spelled with the port. A URL spelled any other way (even the +// same authority without `:443`) or a root bundle from another image is a new authority to the proxy: a new account, and +// every routed name ordered again, on two machines at once, against Let's Encrypt's rate limits. So what the module now +// states is held to exactly what the binding rendered. + +// renderedBeforeTheFold is route-proxy's acme.env as the binding to public-acme rendered it on every machine running the +// proxy, read from the controller's plan on 2026-10-06. +const renderedBeforeTheFold = "ACME_DIRECTORY=https://acme-v02.api.letsencrypt.org:443/directory\n" + + "ACME_ROOTS=https://acme-v02.api.letsencrypt.org:443\n" + + "ACME_ROOTS_PATH=\n" + +// trustImage is the image whose system bundle becomes the public root the account directory is named after. Moving it +// renames that directory. +const trustImage = "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" + +// ownManifest is route-proxy's manifest as its tree holds it: this repository's root and the module's directory. +func ownManifest(t *testing.T) catalogue.Manifest { + t.Helper() + m, err := catalogue.ReadTreeManifest(filepath.Join("..", "..", "..", ".."), filepath.Join("..", "..")) + if err != nil { + t.Fatalf("route-proxy's manifest does not parse:\n%v", err) + } + return m +} + +func resourceNamed(out []map[string]any, id string) map[string]any { + for _, r := range out { + if r["id"] == id { + return r + } + } + return nil +} + +func TestRouteProxyKeepsItsPublicAccountDirectory(t *testing.T) { + m := ownManifest(t) + if slices.Contains(m.Requires, "acme-ca") { + t.Fatal("route-proxy still asks for acme-ca; the public issuer is its own fact (ADR 0226)") + } + // As a build would leave it: each artifact a container names resolved to an image. Which image does not matter to + // the file checked here. An archive resource carries no image: it unpacks a bundle. + for _, res := range m.Resources { + if res["type"] == "archive" { + continue + } + if artifact, ok := res["artifact"].(string); ok { + delete(res, "artifact") + res["image"] = "registry.invalid/route-proxy/" + artifact + + "@sha256:1111111111111111111111111111111111111111111111111111111111111111" + } + } + r := catalogue.Resolution{ + Node: "anchor", + Modules: []catalogue.Manifest{m}, + Needs: []catalogue.Needed{{ + Name: "internal-acme-ca", From: "home", At: "home.internal", For: "route-proxy", + Serves: map[string]any{"port": float64(9000), "path": "/acme/acme/directory", "roots": "/roots.pem"}, + }}, + } + // Its own broker credential, sealed as the mesh would; what it is does not matter here. + out, err := r.Declaration(catalogue.Rendering{Needed: map[string]map[string]string{"route-proxy": {"broker": "sealed"}}}) + if err != nil { + t.Fatalf("route-proxy could not be composed for a machine: %v", err) + } + env := resourceNamed(out, "route-proxy.acme-env") + if env == nil { + t.Fatalf("nothing writes the public issuer's environment: %v", out) + } + if got, _ := env["content"].(string); got != renderedBeforeTheFold { + t.Fatalf("acme.env changed, which moves the proxy's account and reorders every certificate:\ngot %q\nwant %q", + got, renderedBeforeTheFold) + } + if resourceNamed(out, "route-proxy.bound-acme-ca") != nil { + t.Error("a binding to acme-ca is still written") + } + var trust string + if m.Build != nil { + for _, a := range m.Build.Artifacts { + if a.Name == "trust" { + trust = a.From + } + } + } + if trust != trustImage { + t.Errorf("the trust image is %q, not %q: its system bundle is the public root the account directory is named "+ + "after, so moving it reorders every certificate. Move it only with a plan for the account (ADR 0226)", + trust, trustImage) + } +} + +// The proxy's program is mounted into the pinned trust image, not baked into an image (novox/hq ADR 0480 clauses 1, 2 +// and 4): a Go bundle of the module's own Go module stamped for alpine, unpacked by an archive resource, mounted +// read-only at /opt/mesh, called through args, and named under restart-on so a new program never leaves the old one +// running. The container's name, network and logging are what the jail and the routes rely on. And its routes are the +// seat mesh-provisioner's plans alone (issue 774, research 072 row 34): it reads them, and receives no file. +func TestRouteProxyRunsItsMountedProgramOnTheTrustImage(t *testing.T) { + m := ownManifest(t) + if !slices.Contains(m.Reads, "mesh-provisioner.plans") { + t.Errorf("route-proxy does not read mesh-provisioner.plans, where its routes are: %v", m.Reads) + } + if len(m.Receives) > 0 { + t.Errorf("route-proxy receives %v: a route is served from the seat's plans, never a received file", m.Receives) + } + var bundle *catalogue.Artifact + for i, a := range m.Build.Artifacts { + if a.Name == "server" { + bundle = &m.Build.Artifacts[i] + } + if a.Kind == "image" { + t.Errorf("artifact %s is an image built from a Dockerfile (ADR 0452 §1)", a.Name) + } + } + if bundle == nil || bundle.Kind != "bundle" || bundle.Language != "go" || bundle.System != "alpine" || + bundle.From != "cmd/route-proxy" || bundle.Binary != "route-proxy" { + t.Fatalf("the server artifact is not a Go bundle of cmd/route-proxy stamped for alpine: %+v", bundle) + } + var archive, server map[string]any + for _, r := range m.Resources { + switch { + case r["type"] == "archive" && r["artifact"] == "server": + archive = r + case r["id"] == "server": + server = r + } + } + if archive == nil || server == nil { + t.Fatalf("archive %v or server container %v is missing", archive, server) + } + if server["artifact"] != "trust" || server["name"] != "route-proxy" || server["network"] != "host" || server["logging"] != "journald" { + t.Errorf("the server container moved: %v", server) + } + mount := archive["path"].(string) + ":/opt/mesh:ro" + volumes, _ := server["volumes"].([]any) + if !slices.Contains(volumes, any(mount)) { + t.Errorf("the archive is not mounted read-only at /opt/mesh (%s): %v", mount, server["volumes"]) + } + if args, _ := server["args"].([]any); len(args) != 1 || args[0] != "/opt/mesh/route-proxy" { + t.Errorf("args are %v, want the mounted program", server["args"]) + } + restartOn, _ := server["restart-on"].([]any) + if !slices.Contains(restartOn, archive["id"]) { + t.Errorf("restart-on %v does not name the archive %v", server["restart-on"], archive["id"]) + } +} diff --git a/modules/route-proxy/cmd/route-provider/provider.go b/modules/route-proxy/cmd/route-provider/provider.go index 39973d4..46e089d 100644 --- a/modules/route-proxy/cmd/route-provider/provider.go +++ b/modules/route-proxy/cmd/route-provider/provider.go @@ -13,7 +13,7 @@ package main import ( "git.novox.be/novox/mesh-sdk/go/provider" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // ProvisionName is what route-proxy provides, as its manifest says, and ModuleName this module. diff --git a/modules/route-proxy/cmd/route-provider/tools.go b/modules/route-proxy/cmd/route-provider/tools.go index 6129a4b..1b9109f 100644 --- a/modules/route-proxy/cmd/route-provider/tools.go +++ b/modules/route-proxy/cmd/route-provider/tools.go @@ -14,7 +14,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // The tools' names, as the manifest declares them. diff --git a/modules/route-proxy/cmd/route-provider/tools_test.go b/modules/route-proxy/cmd/route-provider/tools_test.go index da91dfe..3d4c7be 100644 --- a/modules/route-proxy/cmd/route-provider/tools_test.go +++ b/modules/route-proxy/cmd/route-provider/tools_test.go @@ -9,7 +9,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // held is a state as the bus holds it, read-only. diff --git a/modules/route-proxy/cmd/route-proxy/bus.go b/modules/route-proxy/cmd/route-proxy/bus.go index ff34c07..911bfc7 100644 --- a/modules/route-proxy/cmd/route-proxy/bus.go +++ b/modules/route-proxy/cmd/route-proxy/bus.go @@ -17,7 +17,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/provisions" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // keptStates is every state route-proxy keeps, as both its halves write them: the routes and the refusals this diff --git a/modules/route-proxy/cmd/route-proxy/ledger.go b/modules/route-proxy/cmd/route-proxy/ledger.go index 30aecb1..e6d4d70 100644 --- a/modules/route-proxy/cmd/route-proxy/ledger.go +++ b/modules/route-proxy/cmd/route-proxy/ledger.go @@ -17,7 +17,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // What this proxy says on the bus about the names it serves and the names it refuses (issue 774). diff --git a/modules/route-proxy/cmd/route-proxy/ledger_test.go b/modules/route-proxy/cmd/route-proxy/ledger_test.go index 91c0111..f025e00 100644 --- a/modules/route-proxy/cmd/route-proxy/ledger_test.go +++ b/modules/route-proxy/cmd/route-proxy/ledger_test.go @@ -25,7 +25,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // fakeState is a state on the bus, in memory, counting what was written to it. diff --git a/modules/route-proxy/cmd/route-proxy/main.go b/modules/route-proxy/cmd/route-proxy/main.go index 9203ba4..52c94c8 100644 --- a/modules/route-proxy/cmd/route-proxy/main.go +++ b/modules/route-proxy/cmd/route-proxy/main.go @@ -65,7 +65,7 @@ import ( "golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/bcrypt" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // Where public certificates come from when nothing says otherwise. diff --git a/modules/route-proxy/cmd/route-proxy/plans.go b/modules/route-proxy/cmd/route-proxy/plans.go index e10c627..06bd22b 100644 --- a/modules/route-proxy/cmd/route-proxy/plans.go +++ b/modules/route-proxy/cmd/route-proxy/plans.go @@ -20,7 +20,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/provisions" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // planned is the routes the seat's plans give this proxy on its node, by plan key, kept so a plan that cannot be read diff --git a/modules/route-proxy/cmd/route-proxy/reach_test.go b/modules/route-proxy/cmd/route-proxy/reach_test.go index 25372c1..74200c7 100644 --- a/modules/route-proxy/cmd/route-proxy/reach_test.go +++ b/modules/route-proxy/cmd/route-proxy/reach_test.go @@ -18,7 +18,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // behind is a workload the proxy can send to, and a table routing one public name and one diff --git a/modules/route-proxy/cmd/route-proxy/routes_test.go b/modules/route-proxy/cmd/route-proxy/routes_test.go index edf01d0..76ede98 100644 --- a/modules/route-proxy/cmd/route-proxy/routes_test.go +++ b/modules/route-proxy/cmd/route-proxy/routes_test.go @@ -13,7 +13,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/provisions" - "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" + "git.novox.be/novox/mesh-networking/modules/route-proxy/contract" ) // routesFrom is the table a test's routes make, written as `{"given":[{"from","node","at","values"}…]}`: the diff --git a/modules/route-proxy/contract/go.mod b/modules/route-proxy/contract/go.mod index 9111045..51aac58 100644 --- a/modules/route-proxy/contract/go.mod +++ b/modules/route-proxy/contract/go.mod @@ -1,3 +1,3 @@ -module git.novox.be/novox/mesh-controller/modules/route-proxy/contract +module git.novox.be/novox/mesh-networking/modules/route-proxy/contract go 1.26.0 diff --git a/modules/route-proxy/contract/route.schema.json b/modules/route-proxy/contract/route.schema.json index af55a78..b59d0fb 100644 --- a/modules/route-proxy/contract/route.schema.json +++ b/modules/route-proxy/contract/route.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.novox.be/novox/mesh-controller/modules/route-proxy/contract/route.schema.json", + "$id": "https://git.novox.be/novox/mesh-networking/modules/route-proxy/contract/route.schema.json", "title": "route", "description": "One route a consumer asks route-proxy for, as a manifest's contributes.route says it (one, or several by local name) and the controller composes it. Version 1 of route-proxy's contract.", "type": "object", diff --git a/modules/route-proxy/go.mod b/modules/route-proxy/go.mod index 70768d6..a1c2699 100644 --- a/modules/route-proxy/go.mod +++ b/modules/route-proxy/go.mod @@ -9,7 +9,7 @@ require ( ) require ( - git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0 + git.novox.be/novox/mesh-networking/modules/route-proxy/contract v0.1.0 github.com/klauspost/compress v1.20.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect @@ -17,3 +17,6 @@ require ( golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.42.0 // indirect ) + +// The contract is this repository's own Go module, beside this one (contract/): built from the tree, not fetched. +replace git.novox.be/novox/mesh-networking/modules/route-proxy/contract => ./contract diff --git a/modules/route-proxy/go.sum b/modules/route-proxy/go.sum index dc764c9..37a799f 100644 --- a/modules/route-proxy/go.sum +++ b/modules/route-proxy/go.sum @@ -1,5 +1,3 @@ -git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0 h1:89jE1oBK+l7gFJjz6wx8mUD3KxrqSDa7pUTlQkzC1do= -git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0/go.mod h1:Yr+Xp89kpMiKH6kA06EU+d9I2CyzrK9yMX7J3pSv0hE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=