Files
mesh-lab/test/supported.test.ts
T
jschoubben 5d01006eab 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.
2026-08-24 01:49:30 +02:00

79 lines
3.2 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { parseScenario } from "../src/declaration/parse.ts";
import { assertSupported, UnsupportedError } from "../src/lifecycle/supported.ts";
/**
* A declaration the runtime silently ignores is the fault this lab exists to catch —
* novox/hq 04-ISSUES/003, where a firewall key is declared in five manifests and read by no
* code. These tests exist so the lab never commits it, and they move as the runtime catches
* up with the model.
*/
const withGateway = `scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`;
test("a plain scenario is raisable", () => {
const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }`);
assert.doesNotThrow(() => assertSupported(scenario));
});
test("gateways are implemented — a router is materialised for them", () => {
assert.doesNotThrow(() => assertSupported(parseScenario(withGateway)));
});
test("published ports and policy are implemented", () => {
const scenario = parseScenario(`scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
iot: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
policy: [{ from: iot, to: home, allow: false }]
machines:
a:
at: { segment: home, address: [192.168.1.9] }
published: [{ port: 443, on: home }]`);
assert.doesNotThrow(() => assertSupported(scenario));
});
test("inbound: deny is implemented — a host firewall is applied and read back", () => {
const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, inbound: deny } }`);
assert.doesNotThrow(() => assertSupported(scenario));
});
test("place is still refused — there is nothing to place yet", () => {
const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
place: { all: [host] }`);
assert.throws(() => assertSupported(scenario), UnsupportedError);
});
test("the refusal explains what would silently be missing", () => {
const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
place: { all: [host] }`);
try {
assertSupported(scenario);
assert.fail("should have refused");
} catch (err) {
assert.equal((err as UnsupportedError).missing.length, 1);
assert.match(err instanceof Error ? err.message : "", /silently lacks them/);
}
});
test("inbound: allow is not a gap — only deny needs enforcing", () => {
const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, inbound: allow } }`);
assert.doesNotThrow(() => assertSupported(scenario));
});