diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index 90205e9..6854092 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -1183,3 +1183,72 @@ test("the board names the machine that is not doing what it was told", { await mesh("unassign laptop impossible"); await mesh("push laptop"); }); + +test("the hub can be filtered without severing the mesh", { + skip, timeout: 900_000, +}, async () => { + // The machine that most needs a firewall was the one that could not have one. A hub is dialled + // by every node at other sites; a machine that is not a hub dials out and needs nothing open. + // They are the same module, so a static `listens` cannot say it — and the machine it gets wrong + // is the one facing the public internet. + // + // The failure this guards against is not subtle and is very hard to recover from: a rule set + // that closes the hub's own port takes the private network down, and the mesh's way of fixing + // anything is to send a declaration over it. + const rules = "/etc/mesh/hub-filter.nft"; + await must("anchor", `printf %s '{"module":"hubfilter","version":"1",` + + `"capabilities":["firewall"],` + + `"filtering":{"into":"${rules}"},` + + `"resources":[{"id":"nftables","type":"package","package":"nftables"},` + + `{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"},` + + `{"id":"unit","type":"file","path":"/etc/systemd/system/hub-filter.service",` + + `"mode":"0644","content":"[Unit]\\nDescription=What the mesh computed for the hub\\n` + + `[Service]\\nType=oneshot\\nRemainAfterExit=yes\\n` + + `ExecStart=/usr/bin/nft -f ${rules}\\n[Install]\\nWantedBy=multi-user.target\\n"},` + + `{"id":"filter","type":"service","unit":"hub-filter.service","state":"running",` + + `"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/hubfilter.json`); + await must("anchor", `docker cp /tmp/hubfilter.json mesh-control:/hubfilter.json`); + await mesh("module add /hubfilter.json"); + await mesh("assign anchor hubfilter"); + await mesh("push anchor"); + await new Promise((r) => setTimeout(r, 20_000)); + + // The hub's own way onto the private network is open, and derived — nothing in that manifest + // mentions a port. + const written = await must("anchor", `cat ${rules}`); + assert.match(written, /udp dport 51820 accept/, + `the hub's rule set closes the private network it is the way onto:\n${written}`); + assert.match(written, /# networking/, + `the rule does not name what caused it:\n${written}`); + + // Loaded, and the mesh still works: a declaration reaches the other machine, which it cannot if + // the overlay is severed. This is the assertion that matters — a rule file that looks right and + // a mesh that has stopped are exactly what this is guarding against. + assert.match(await must("anchor", `nft list table inet mesh`), /dport 51820/); + + await must("laptop", `rm -f /etc/mesh-still-works`); + await must("anchor", `printf %s '{"module":"stillworks","version":"1",` + + `"resources":[{"id":"marker","type":"file","path":"/etc/mesh-still-works",` + + `"content":"yes","mode":"0644"}]}' > /tmp/stillworks.json`); + await must("anchor", `docker cp /tmp/stillworks.json mesh-control:/stillworks.json`); + await mesh("module add /stillworks.json"); + await mesh("assign laptop stillworks"); + await mesh("push laptop"); + + let arrived = false; + for (let i = 0; i < 20 && !arrived; i++) { + arrived = (await on("laptop", `test -f /etc/mesh-still-works`)).ok; + if (!arrived) await new Promise((r) => setTimeout(r, 3000)); + } + assert.ok(arrived, + "the hub applied its own rule set and the mesh stopped reaching the other machine"); + + // And the other machine still reaches the hub over the private network, which is what the + // opened port is for. + assert.ok((await on("laptop", `ping -c 1 -W 5 anchor.internal`)).ok, + "the private network is down after the hub filtered itself"); + + await mesh("unassign anchor hubfilter"); + await mesh("unassign laptop stillworks"); + await mesh("push"); +});