It is the commonest home LAN range there is, so on an ordinary workstation the lab's private segment and the machine's own network are the same addresses. The scenario routes an egress machine explicitly and marks the rest unreachable, so nothing leaked — but that guard was carrying the whole weight of a collision nobody chose, and a guard is a bad place for that. 10.99.1.0/24 is still RFC 1918, so the bed still models a home LAN behind an access point. It is simply far from what this kind of machine already has: 192.168.1 is the LAN, 172.16-31 and 192.168.16-95 are container bridges, and 10.10/10.42/10.208 are a tunnel, the mesh overlay and the virtualisation daemon. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { parseScenario } from "../src/declaration/parse.ts";
|
|
import { scenarioRoutesFor } from "../src/lifecycle/address.ts";
|
|
|
|
/**
|
|
* The uplink and the declared gateway must not fight.
|
|
*
|
|
* **This is the one decision the registry's removal turned on, and it is invisible in a raise.**
|
|
* Every machine that needs an image now has an `egress` uplink, and the uplink's DHCP offers a
|
|
* default route. So did the scenario: a machine behind a household gateway defaulted through it, a
|
|
* machine on a public segment defaulted through transit. Both of those are containers that reach
|
|
* the scenario and nothing else — no route to the real internet, by design, because they exist to
|
|
* reproduce a household router rather than to be one.
|
|
*
|
|
* A default route through either is therefore a black hole for anything outside, and it beats the
|
|
* uplink's route on metric. The machine would sit failing every pull with a routing table that
|
|
* looks perfectly reasonable.
|
|
*
|
|
* The answer is that an egress machine states the scenario's ranges explicitly and lets the uplink
|
|
* be the default. These tests are how that is checked without spending an hour raising four nodes.
|
|
*/
|
|
|
|
const HOUSEHOLD = `
|
|
scenario: household
|
|
segments:
|
|
hosting:
|
|
kind: public
|
|
cidr: [192.0.2.0/24]
|
|
home:
|
|
kind: private
|
|
cidr: [10.99.1.0/24]
|
|
gateway:
|
|
to: hosting
|
|
address: [192.0.2.50]
|
|
nat: [v4]
|
|
forwardable: true
|
|
machines:
|
|
novox:
|
|
at: { segment: hosting, address: [192.0.2.20] }
|
|
egress: true
|
|
ace:
|
|
at: { segment: home, address: [10.99.1.10] }
|
|
egress: true
|
|
sealed:
|
|
at: { segment: home, address: [10.99.1.99] }
|
|
`;
|
|
|
|
test("a machine behind a gateway still reaches the scenario through that gateway", () => {
|
|
// The whole point of the topology: home→public is a masqueraded outbound path, and the overlay
|
|
// handshake has to survive it. An egress machine that stopped using its gateway would be
|
|
// testing a flat network with extra steps.
|
|
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace");
|
|
assert.deepEqual(routes, [{ cidr: "192.0.2.0/24", via: "10.99.1.1" }]);
|
|
});
|
|
|
|
test("a machine's own segment gets no route — it is already on-link", () => {
|
|
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace");
|
|
assert.ok(!routes.some((r) => r.cidr === "10.99.1.0/24"), JSON.stringify(routes));
|
|
});
|
|
|
|
/**
|
|
* **The dangerous one.** `home` is 10.99.1.0/24 — a documentation range in spirit, an ordinary
|
|
* private one in fact, and very possibly the network the workstation itself is on.
|
|
*
|
|
* With one public segment there is no transit router, so novox has no path to `home` at all. Left
|
|
* to fall through, that traffic would leave by the uplink and land on whatever the workstation can
|
|
* reach. Unreachable is both the faithful reproduction of what it had before — a default route into
|
|
* scenery that dropped it — and the only safe answer.
|
|
*/
|
|
test("a range with no path inside the scenario is unreachable, not leaked to the uplink", () => {
|
|
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "novox");
|
|
assert.deepEqual(routes, [{ cidr: "10.99.1.0/24", via: null }]);
|
|
});
|
|
|
|
test("a machine without egress is left to its default route, and states nothing", () => {
|
|
// Not because it needs no routes — it has one, a default through its gateway, applied the old
|
|
// way. This function is only asked about machines whose default belongs to the uplink.
|
|
const scenario = parseScenario(HOUSEHOLD);
|
|
assert.equal(scenario.machines["sealed"]?.egress, undefined);
|
|
});
|
|
|
|
const TWO_PUBLIC = `
|
|
scenario: two-public
|
|
segments:
|
|
hosting:
|
|
kind: public
|
|
cidr: [192.0.2.0/24]
|
|
elsewhere:
|
|
kind: public
|
|
cidr: [198.51.100.0/24]
|
|
machines:
|
|
anchor:
|
|
at: { segment: hosting, address: [192.0.2.10] }
|
|
egress: true
|
|
`;
|
|
|
|
test("with a second public segment the transit router is the way across, as it always was", () => {
|
|
// Transit is raised only when there is more than one public segment, so this is exactly the
|
|
// case where pointing at it means something.
|
|
const routes = scenarioRoutesFor(parseScenario(TWO_PUBLIC), "anchor");
|
|
assert.deepEqual(routes, [{ cidr: "198.51.100.0/24", via: "192.0.2.254" }]);
|
|
});
|
|
|
|
const V6 = `
|
|
scenario: both-families
|
|
segments:
|
|
hosting:
|
|
kind: public
|
|
cidr: [192.0.2.0/24, "2001:db8:a::/48"]
|
|
elsewhere:
|
|
kind: public
|
|
cidr: [198.51.100.0/24, "2001:db8:b::/48"]
|
|
machines:
|
|
anchor:
|
|
at: { segment: hosting, address: [192.0.2.10, "2001:db8:a::10"] }
|
|
egress: true
|
|
`;
|
|
|
|
test("each family is routed through its own next hop", () => {
|
|
// A v6 range routed via a v4 next hop is not a route, and the reverse is not either.
|
|
const routes = scenarioRoutesFor(parseScenario(V6), "anchor");
|
|
assert.deepEqual(routes, [
|
|
{ cidr: "198.51.100.0/24", via: "192.0.2.254" },
|
|
{ cidr: "2001:db8:b::/48", via: "2001:db8:a::fffe" },
|
|
]);
|
|
});
|