|
|
|
@@ -193,6 +193,20 @@ function bundleFor(images: HeldImage[]): string {
|
|
|
|
|
return foundationBundle(bundle, images);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Put the mesh's composed user list where the anchor's bus reads it, and make it re-read.
|
|
|
|
|
*
|
|
|
|
|
* **Genesis's step, done by hand because this bed raises genesis by hand** (novox/hq 04-ISSUES/146).
|
|
|
|
|
* The bus here is the bundle's, not a module the mesh delivers, so an account the mesh composes —
|
|
|
|
|
* the enrolment when a token is issued, the node's own when it enrols — reaches it only if whoever
|
|
|
|
|
* raised it places it. Without this the first join is refused with an authorisation violation.
|
|
|
|
|
*/
|
|
|
|
|
async function composeTheBusUsers(): Promise<void> {
|
|
|
|
|
await must("anchor",
|
|
|
|
|
`docker exec mesh-controller /mesh-controller broker accounts > /var/lib/mesh-bus-conf/accounts.conf && ` +
|
|
|
|
|
`docker kill -s HUP mesh-broker >/dev/null`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Take a token out of what `token issue` printed. It is the one base64url blob on its own line. */
|
|
|
|
|
function tokenFrom(said: string): string {
|
|
|
|
|
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" "));
|
|
|
|
@@ -308,15 +322,44 @@ test("a bare machine becomes a mesh", { skip, timeout: 600_000 }, async () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("both machines join it, and the token is all they need", { skip, timeout: 900_000 }, async () => {
|
|
|
|
|
for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) {
|
|
|
|
|
await mesh(`node add ${node}`);
|
|
|
|
|
const token = tokenFrom(await mesh(`token issue --node ${node}`));
|
|
|
|
|
// **The anchor runs the bus, so it joins over its own loopback.** Every other machine joins
|
|
|
|
|
// through the tunnel (novox/hq ADR 0169): the anchor is the hub, and its tunnel comes up first.
|
|
|
|
|
await mesh(`node add anchor`);
|
|
|
|
|
const first = tokenFrom(await mesh(`token issue --node anchor`));
|
|
|
|
|
await composeTheBusUsers();
|
|
|
|
|
// No --name. The token says what the mesh calls the machine, which is the fault this walk
|
|
|
|
|
// found the first time it was run.
|
|
|
|
|
const said = await must(machine, `${HOST_PATH} enrol --token ${quote(token)}`);
|
|
|
|
|
assert.match(said, new RegExp(`enrolled as ${node}`), said);
|
|
|
|
|
assert.match(said, /sealing key/, "no sealing key was generated");
|
|
|
|
|
const anchorSaid = await must("anchor", `${HOST_PATH} enrol --token ${quote(first)}`);
|
|
|
|
|
await composeTheBusUsers();
|
|
|
|
|
assert.match(anchorSaid, /enrolled as anchor/, anchorSaid);
|
|
|
|
|
assert.match(anchorSaid, /sealing key/, "no sealing key was generated");
|
|
|
|
|
|
|
|
|
|
await mesh("overlay place anchor --hub --endpoint 192.0.2.10:51820 --site lab");
|
|
|
|
|
await mesh("assign anchor networking");
|
|
|
|
|
await must("anchor", `pgrep -x mesh-host >/dev/null || (nohup ${HOST_PATH} run > /var/log/mesh-host.log 2>&1 & sleep 3)`);
|
|
|
|
|
await mesh("push anchor");
|
|
|
|
|
// The host reports which of its links face outside with every apply, and the filter is not
|
|
|
|
|
// composed for a machine that has not — so its first apply is waited for, not slept past.
|
|
|
|
|
const appliedBy = Date.now() + 180_000;
|
|
|
|
|
while (!(await on("anchor", `grep -q 'applied [0-9]' /var/log/mesh-host.log`)).ok) {
|
|
|
|
|
if (Date.now() > appliedBy) assert.fail(`the anchor never applied what it was sent:\n${(await on("anchor", "tail -30 /var/log/mesh-host.log")).out}`);
|
|
|
|
|
await new Promise((r) => setTimeout(r, 3000));
|
|
|
|
|
}
|
|
|
|
|
await new Promise((r) => setTimeout(r, 5000));
|
|
|
|
|
await deriveTheFilterOn({ machine: "anchor", node: "anchor", hubPort: 51820, held, must, mesh, on });
|
|
|
|
|
|
|
|
|
|
// **The laptop makes its tunnel key, and the token is issued for it.** The hub is told the key
|
|
|
|
|
// before the token is shown, so the tunnel answers the first time the laptop knocks.
|
|
|
|
|
await must("laptop", `pacman -Sy --noconfirm --needed wireguard-tools >/dev/null 2>&1`);
|
|
|
|
|
const key = (await must("laptop", `${HOST_PATH} key 2>/dev/null`)).trim();
|
|
|
|
|
await mesh(`node add laptop`);
|
|
|
|
|
const second = tokenFrom(await mesh(`token issue --node laptop --overlay-key ${key}`));
|
|
|
|
|
await composeTheBusUsers();
|
|
|
|
|
const laptopSaid = await must("laptop", `${HOST_PATH} enrol --token ${quote(second)}`);
|
|
|
|
|
await composeTheBusUsers();
|
|
|
|
|
assert.match(laptopSaid, /the tunnel to the hub is up/, laptopSaid);
|
|
|
|
|
assert.match(laptopSaid, /enrolled as laptop/, laptopSaid);
|
|
|
|
|
assert.match(laptopSaid, /sealing key/, "no sealing key was generated");
|
|
|
|
|
|
|
|
|
|
const recorded = await must("anchor",
|
|
|
|
|
`docker exec mesh-store psql -U postgres -d inventory -qAt ` +
|
|
|
|
@@ -366,7 +409,7 @@ test("a credential reaches both ends and the mesh holds neither", { skip, timeou
|
|
|
|
|
// The anchor's derived filter, admitting the hub's port — what genesis does on the control-node,
|
|
|
|
|
// and what a bed raised from the bundle must do itself (ADR 0088). Until it is, the base filter
|
|
|
|
|
// keeps the hub closed and nothing on the laptop reaches anchor over the private network.
|
|
|
|
|
await deriveTheFilterOn({ machine: "anchor", node: "anchor", hubPort: 51820, must, mesh, on });
|
|
|
|
|
await deriveTheFilterOn({ machine: "anchor", node: "anchor", hubPort: 51820, held, must, mesh, on });
|
|
|
|
|
|
|
|
|
|
const onConsumer = (await must("laptop", `cat /etc/meshboard/database.password`)).trim();
|
|
|
|
|
// Named after the machine *and* the module, because a consumer is both (novox/hq
|
|
|
|
@@ -1662,3 +1705,31 @@ test("a third-party workload is adopted, with the credential it already had", {
|
|
|
|
|
// a gap that bed should close; "a consumer's cache grant means exactly its own keys" — its tenancy
|
|
|
|
|
// assertions (a write outside the consumer's keys and a FLUSHALL are refused) moved into
|
|
|
|
|
// mesh-grant-end-to-end, against the catalogue's redis.
|
|
|
|
|
|
|
|
|
|
// **A machine joins through the tunnel, and needs the bus only through it** (novox/hq ADR 0169).
|
|
|
|
|
// The bus is closed to this machine at the anchor's very first hook, before the container runtime's
|
|
|
|
|
// forwarding, so the only way its enrolment can arrive is over the tunnel the token gave it.
|
|
|
|
|
test("a machine joins through the tunnel, with the bus closed to it", { skip, timeout: 900_000 }, async () => {
|
|
|
|
|
await must("anchor", `nft add table ip lab_bus_closed && ` +
|
|
|
|
|
`nft add chain ip lab_bus_closed pre '{ type filter hook prerouting priority -300; }' && ` +
|
|
|
|
|
`nft add rule ip lab_bus_closed pre ip saddr 192.0.2.30 tcp dport 4222 drop`);
|
|
|
|
|
try {
|
|
|
|
|
await must("joiner", `pacman -Sy --noconfirm --needed wireguard-tools >/dev/null 2>&1`);
|
|
|
|
|
const key = (await must("joiner", `${HOST_PATH} key 2>/dev/null`)).trim();
|
|
|
|
|
assert.match(key, /^[A-Za-z0-9+/]{43}=$/, `not a tunnel key: ${key}`);
|
|
|
|
|
// Asked again, the same key: a token may already have been issued for it.
|
|
|
|
|
assert.equal((await must("joiner", `${HOST_PATH} key 2>/dev/null`)).trim(), key);
|
|
|
|
|
|
|
|
|
|
const token = tokenFrom(await mesh(`token issue --new joiner --overlay-key ${key}`));
|
|
|
|
|
await composeTheBusUsers();
|
|
|
|
|
const said = await must("joiner", `${HOST_PATH} enrol --token ${quote(token)}`);
|
|
|
|
|
assert.match(said, /the tunnel to the hub is up/, said);
|
|
|
|
|
assert.match(said, /enrolled as joiner/, said);
|
|
|
|
|
|
|
|
|
|
const shakes = await must("joiner", `wg show mesh0 latest-handshakes`);
|
|
|
|
|
assert.ok(/\s[1-9]\d*\s*$/m.test(shakes), `the tunnel never shook hands with the hub: ${shakes}`);
|
|
|
|
|
} finally {
|
|
|
|
|
await on("anchor", `nft delete table ip lab_bus_closed`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|