Transit, host firewalls, and the whole topology raising
The full topology now raises: four machines, three routers, a transit router, six segments, in 35 seconds. Everything the declaration model can express except `place`, which is refused because the node host it would place does not exist yet. Transit was a real gap, not a bug. The design says public networks are unrelated and routed to each other, never bridged — and I built the segments and never built the thing that routes between them, so three public networks were islands and nothing crossed. A transit router now holds an interface on every public segment, forwarding and no translation: the closest thing the lab has to the internet, deliberately dumb. Proven rather than asserted, by ping TTL across the raised topology: within one segment ttl=64 no hops across two unrelated public networks ttl=62 gateway + transit multicast between public networks 0 replies A flat internet would have shown ttl=64 and answered multicast — which would let a node discover a peer it could never reach in production, and report success. That is the fault the as-is layer records the mesh already hitting with multicast name resolution. inbound: deny is implemented as a host firewall on the machine, read back after applying. A declared refusal that silently did not load leaves the machine wide open, which looks exactly like a machine that is working. Established and related traffic is accepted, so a defended machine can still dial out rather than being a disconnected one. Verified by running, all of it: home -> devices (policy allow) reachable devices -> home (policy deny) blocked behind unforwardable NAT -> out reachable in -> behind unforwardable NAT unreachable inbound: deny, dialling out reachable reaching a machine that denies inbound refused The two routers differ exactly as declared: the forwardable one carries the policy rule and no inbound drop, the unforwardable one carries `ct state new drop` and no DNAT.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* `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 } 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`,
|
||||
);
|
||||
}
|
||||
log(` ${machine} refuses unsolicited inbound`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user