From 3f224c2876386075e1b2467fe3312adceee761fc Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 18:49:06 +0200 Subject: [PATCH] Adoption bed: an opening the operator's own rule answers is satisfied, and a failed container probe records its evidence --- test/integration/adoption.test.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test/integration/adoption.test.ts b/test/integration/adoption.test.ts index ff5dbda..ac489ee 100644 --- a/test/integration/adoption.test.ts +++ b/test/integration/adoption.test.ts @@ -606,8 +606,19 @@ before(async () => { // reaches a published port through the runtime's proxy, on the incoming path, not the forwarded. await step("B4", ["A3"], async () => { const said: string[] = []; - const fromContainer = await on(CONTROL, `docker run --rm ${ALPINE} nc -z -w 3 ${ANCHOR} ${STORE_PORT}`, 180_000); - assert.ok(fromContainer.ok, `a container on the node cannot reach the store:\n${fromContainer.out}`); + const fromContainer = await on(CONTROL, `docker run --rm ${ALPINE} nc -zv -w 3 ${ANCHOR} ${STORE_PORT} 2>&1`, 180_000); + if (!fromContainer.ok) { + // Evidence, so the cause can be read from this run rather than guessed at the next one. + const evidence = await on(CONTROL, [ + `echo '--- published'; docker ps --format '{{.Names}} {{.Ports}}' | grep -i ${STORE_PORT}`, + `echo '--- from the host'; nc -zv -w 3 ${ANCHOR} ${STORE_PORT} 2>&1`, + `echo '--- from the host network'; docker run --rm --network host ${ALPINE} nc -zv -w 3 ${ANCHOR} ${STORE_PORT} 2>&1`, + `echo '--- iptables FORWARD, DOCKER-USER, isolation'; iptables -S FORWARD; iptables -S DOCKER-USER; iptables -S | grep -i isolation`, + `echo '--- the guard'; nft list table inet mesh_guard`, + `echo '--- nat for the port'; iptables -t nat -S | grep ${STORE_PORT}`, + ].join("; "), 120_000); + assert.fail(`a container on the node cannot reach the store:\n${fromContainer.out}\n${evidence.out}`); + } said.push(` container -> ${STORE_PORT} reachable from a container on the node itself`); return said.join("\n"); }); @@ -753,9 +764,15 @@ before(async () => { assert.equal(await must(CONTROL, `cat ${SERVICE_FILE}`), catalogue, `the found file was not replaced with the module's`); assert.equal(await must(CONTROL, `cat ${kept}`), await must(CONTROL, `cat ${BEFORE}/file`), `the original was not kept`); said.push(` replaced container (spec ${label.slice(0, 12)}…) and ${SERVICE_FILE}; original still at ${kept}`); + // Either the mesh opened the port, or the predecessor's own rule already admits it — then the + // mesh adds nothing and will remove nothing (ADR 0103), and the operator's rule stays theirs. const opened = (await openings()).filter((r) => new RegExp(`opening-tcp-${SERVED}-`).test(r)); - assert.ok(opened.length > 0, `no opening for ${SERVED} once ${SERVICE} was taken:\n${(await openings()).join("\n")}`); + const operators = (await ufwAdded()).filter((r) => !marked(r) && new RegExp(`\\b${SERVED}\\b`).test(r)); + assert.ok(opened.length > 0 || operators.length > 0, + `no opening for ${SERVED} once ${SERVICE} was taken, and no rule of the operator's admits it:\n${(await ufwAdded()).join("\n")}`); + assert.ok(operators.length > 0, `the operator's own rule for ${SERVED} is gone:\n${(await ufwAdded()).join("\n")}`); said.push(...opened.map((r) => ` opened ${r}`)); + if (opened.length === 0) said.push(` opening ${SERVED} satisfied by the operator's own rule: ${operators.join(" | ")}`); const page = await until(`the taken ${SERVICE} answers over the private network`, 120, async () => { const p = await on(JOINER, `curl -s --max-time 3 http://${anchorOnMesh}:${SERVED}/`); return p.ok && p.out === catalogue ? p.out : null;