`mesh-lab diagram` renders a scenario as draw.io, from either source, through one layout — so a difference between what was asked for and what exists is a difference you can see. The shape says what a resource is and is fixed per kind. The badges say what is true about that particular one and come entirely from metadata: translation, forwardability, mapping expiry, refuses-inbound, container-or-VM, running. The interesting properties of a network are exactly the ones with no visual consequence — a translated address looks identical to an untranslated one. For the live picture to be a record rather than a restatement, raise now writes down what it applied: a segment's kind, ranges and MTU on the link; a gateway's translation, forwardability and expiry on the gateway; inbound: deny on the machine. Every behavioural tag is written AFTER the thing works, never at creation — a failed raise leaves wreckage standing on purpose, and a picture of that wreckage must not badge translation the router never got. The pairing earned itself immediately: drawn side by side, every virtual machine held no addresses. A container's interface carries the device's name and a VM names its own, so joining them by name silently dropped one whole class of machine. Fixed by joining on MAC. Also brings tests under the typecheck gate, which caught integration timeouts being passed as a 4th argument and therefore ignored entirely.
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
/**
|
|
* `inbound: deny` — a host firewall on the machine itself.
|
|
*
|
|
* Distinct from NAT and behaving differently: a machine can be perfectly routable and
|
|
* still refuse everything unsolicited, which is the normal state of a v6-addressed
|
|
* machine. Without this, v6 addressing would silently imply reachability, and a scenario
|
|
* that said a machine refuses traffic would produce one that accepts it.
|
|
*
|
|
* Established and related traffic is accepted, so the machine can still dial out. That is
|
|
* what a host firewall does; a machine that could not reach anything would be reproducing
|
|
* a disconnected machine rather than a defended one.
|
|
*/
|
|
|
|
import type { Scenario } from "../declaration/types.ts";
|
|
import { incus, succeeds } from "../incus/client.ts";
|
|
|
|
const RULESET = `flush ruleset
|
|
table inet mlab {
|
|
chain input {
|
|
type filter hook input priority filter; policy drop;
|
|
ct state established,related accept
|
|
iif lo accept
|
|
ct state invalid drop
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function applyHostFirewalls(
|
|
scenario: Scenario,
|
|
machineNames: Map<string, string>,
|
|
log: (message: string) => void = () => {},
|
|
): Promise<void> {
|
|
for (const [machine, spec] of Object.entries(scenario.machines)) {
|
|
if (spec.inbound !== "deny") continue;
|
|
const name = machineNames.get(machine);
|
|
if (!name) continue;
|
|
|
|
await incus(
|
|
["exec", name, "--", "sh", "-c",
|
|
`cat > /tmp/mlab-host.nft <<'MLABNFT'\n${RULESET}MLABNFT\nnft -f /tmp/mlab-host.nft`],
|
|
60_000,
|
|
);
|
|
|
|
// Read back. A declared refusal that silently did not apply is the fault this lab
|
|
// exists to catch, and a ruleset that failed to load leaves the machine wide open —
|
|
// which looks exactly like a machine that is working.
|
|
const check = await incus(
|
|
["exec", name, "--", "sh", "-c", "nft list table inet mlab >/dev/null 2>&1 && echo present || echo absent"],
|
|
20_000,
|
|
);
|
|
if (check.stdout.trim() !== "present") {
|
|
throw new Error(
|
|
`${machine}: inbound: deny was declared but the ruleset is not loaded — the machine ` +
|
|
`would accept traffic the scenario says it refuses`,
|
|
);
|
|
}
|
|
// Recorded only after the read-back proved it loaded. A tag written before the check
|
|
// would be a claim rather than a record, and anything reading the instance back would
|
|
// report a defended machine that is in fact wide open.
|
|
await succeeds(["config", "set", name, "user.mesh-lab.inbound", "deny"], 20_000);
|
|
log(` ${machine} refuses unsolicited inbound`);
|
|
}
|
|
}
|