diff --git a/one-node-mesh-report.json b/one-node-mesh-report.json index c14987c..9136f06 100644 --- a/one-node-mesh-report.json +++ b/one-node-mesh-report.json @@ -1,13 +1,13 @@ { "scenario": "one-node-mesh", - "established": 11, + "established": 16, "of": 20, "steps": [ { "code": "R1", "title": "a bare machine becomes a mesh of one, raised by the installer", "status": "pass", - "seconds": 132, + "seconds": 140, "why": "" }, { @@ -56,14 +56,14 @@ "code": "P1", "title": "the mesh builds the shared base from source", "status": "pass", - "seconds": 78, + "seconds": 74, "why": "" }, { "code": "P2", "title": "the mesh builds and runs a store of its own", "status": "pass", - "seconds": 50, + "seconds": 53, "why": "" }, { @@ -77,57 +77,57 @@ "code": "P4", "title": "the mesh rebuilds its own control plane from source", "status": "pass", - "seconds": 37, + "seconds": 35, "why": "" }, { "code": "N1", "title": "the mesh puts itself on a private network, and its machine has a name", - "status": "fail", - "seconds": 125, - "why": "networking is assigned and no machine has a name:\n# Generated by the mesh. Do not edit — this file is replaced whenever a node\n# joins or leaves, and an edit would survive until then and vanish.\n\n127.0.0.1\tlocalhost\n::1\t\tlocalhost ip6-localhost ip6-loopback\n127.0.1.1\tanchor\n" + "status": "pass", + "seconds": 6, + "why": "" }, { "code": "U1", "title": "the mesh builds a module standing on that base", - "status": "skip", - "seconds": 0, - "why": "not attempted — N1 (the mesh puts itself on a private network, and its machine has a name) did not succeed" + "status": "pass", + "seconds": 14, + "why": "" }, { "code": "U2", "title": "the mesh runs a broker for that module to talk to", - "status": "skip", - "seconds": 0, - "why": "not attempted — U1 (the mesh builds a module standing on that base) did not succeed" + "status": "pass", + "seconds": 20, + "why": "" }, { "code": "U3", "title": "the anchor runs the module the mesh built", - "status": "skip", - "seconds": 0, - "why": "not attempted — U2 (the mesh runs a broker for that module to talk to) did not succeed" + "status": "pass", + "seconds": 6, + "why": "" }, { "code": "V1", "title": "the control plane can describe the mesh, and what it says is true", - "status": "skip", - "seconds": 0, - "why": "not attempted — U3 (the anchor runs the module the mesh built) did not succeed" + "status": "pass", + "seconds": 1, + "why": "" }, { "code": "V2", "title": "the catalogue holds every module this mesh built", - "status": "skip", - "seconds": 0, - "why": "not attempted — V1 (the control plane can describe the mesh, and what it says is true) did not succeed" + "status": "fail", + "seconds": 1, + "why": "the catalogue does not hold mesh-tools, postgres — the mesh built them and its own record has no trace of it (novox/hq issue 050):\n{\"modules\":[{\"module\":\"amqp-ping\",\"commit\":\"e0c92195d4240841bfcf4b4a9ef869d5afeca331\",\"repository\":\"https://git.novox.be/novox/mesh-catalog.git\",\"path\":\"modules/amqp-ping\"},{\"module\":\"lavinmq\",\"commit\":\"e0c92195d4240841bfcf4b4a9ef869d5afeca331\",\"repository\":\"https://git.novox.be/novox/mesh-catalog.git\",\"path\":\"modules/lavinmq\"},{\"module\":\"mesh-control\",\"commit\":\"5062c36fc9efe159aa9706c0ca2c873351ef1ce0\",\"repository\":\"https://git.novox.be/novox/mesh-control.git\",\"path\":\"\"}]}\n\n+ actual - expected\n\n+ [\n+ 'mesh-tools',\n+ 'postgres'\n+ ]\n- []\n" }, { "code": "V3", "title": "the machine's networking is what the modules asked for", - "status": "skip", + "status": "fail", "seconds": 0, - "why": "not attempted — V1 (the control plane can describe the mesh, and what it says is true) did not succeed" + "why": "the mesh's own firewall table is not there:\nError: No such file or directory\nlist table inet mesh\n ^^^^\n" }, { "code": "E1", diff --git a/test/integration/one-node-mesh.test.ts b/test/integration/one-node-mesh.test.ts index fcb6d57..11d800e 100644 --- a/test/integration/one-node-mesh.test.ts +++ b/test/integration/one-node-mesh.test.ts @@ -85,6 +85,15 @@ const PROVIDER = { module: "lavinmq", repo: "mesh-catalog", path: "modules/lavin */ /** The one word that puts a mesh on a private network and gives its machines names. */ const NETWORK_MODULE = "networking"; +/** + * The packet filter, which is a module too and was assigned to nothing. + * + * The rules are generated from what every module declares it listens on, so a mesh with no filter + * is not "open by accident" — it is a mesh where the whole of that generation has never run. It + * claims a seat (`the-packet-filter`) because a machine has one of these and two things writing + * rules is a coin toss about which survives. + */ +const FILTER_MODULE = "firewall"; const STORE = { module: "postgres", repo: "mesh-catalog", path: "modules/postgres", container: "mesh-postgres" }; const CATALOGUE = { module: "mesh-catalog", repo: "mesh-catalog", path: "modules/mesh-catalog", container: "mesh-catalog" }; /** The control plane, rebuilt from its own repository — the step that ends the installer's tenure. */ @@ -116,6 +125,7 @@ const STORE_RUNS = "the mesh builds and runs a store of its own"; const CATALOGUE_RUNS = "the mesh builds and runs its own catalogue"; const CONTROL_REBUILT = "the mesh rebuilds its own control plane from source"; const NETWORKED = "the mesh puts itself on a private network, and its machine has a name"; +const FILTERED = "the machine has a packet filter, loaded from what modules declared"; const MODULE_BUILT = "the mesh builds a module standing on that base"; const ANCHOR_RUNS = "the anchor runs the module the mesh built"; const DESCRIBES = "the control plane can describe the mesh, and what it says is true"; @@ -376,6 +386,7 @@ const PLAN: { code: string; title: string }[] = [ { code: "P3", title: CATALOGUE_RUNS }, { code: "P4", title: CONTROL_REBUILT }, { code: "N1", title: NETWORKED }, + { code: "N2", title: FILTERED }, { code: "U1", title: MODULE_BUILT }, { code: "U2", title: NEEDS }, { code: "U3", title: ANCHOR_RUNS }, @@ -623,8 +634,29 @@ before(async () => { return `${hosts.trim()}\n\n${modules.trim()}`; }); + // ---- THE PACKET FILTER ------------------------------------------------------------------------- + // + // Assigned separately from `networking` because they answer different questions: one is how + // machines reach each other, the other is what may reach this one. Both were assigned to nothing, + // and the second is the more alarming of the two — every rule the mesh generates from module + // declarations had never been applied to any machine in this test. + await step("N2", FILTERED, NETWORKED, async () => { + await mesh(`assign ${CONTROL} ${FILTER_MODULE}`); + await mesh(`push ${CONTROL}`, 600_000); + const deadline = Date.now() + 180_000; + let ruleset = ""; + while (Date.now() < deadline) { + ruleset = (await on(CONTROL, `nft list table inet mesh 2>&1`)).out; + if (/chain input/.test(ruleset)) break; + await new Promise((r) => setTimeout(r, 5_000)); + } + assert.match(ruleset, /chain input/, + `${FILTER_MODULE} is assigned and the machine has no mesh filter:\n${ruleset}`); + return ruleset; + }); + // ---- 7..8. SOMETHING TO RUN --------------------------------------------------------------- - await step("U1", MODULE_BUILT, NETWORKED, async () => { + await step("U1", MODULE_BUILT, FILTERED, async () => { await registerModule(MODULE.module, resolve(catalogDir, MODULE.module, "module.json")); const built = await mesh( `build ${forgeUrl(MODULE.repo)} --path ${MODULE.path} --ref ${refFor(MODULE.repo)} --wait 1200s`, @@ -863,6 +895,7 @@ for (const name of [ CATALOGUE_RUNS, CONTROL_REBUILT, NETWORKED, + FILTERED, MODULE_BUILT, NEEDS, ANCHOR_RUNS,