Twenty-eight integration tests each carried their own copy of the same two helpers, which pointed a manifest and the substrate bundle at whatever the lab's registry had assigned. They now share two in the harness, and the difference is the point: ours is rewritten to the ID the machine holds it under, and everything else is left exactly as written so the machine pulls it. **The substrate bundle is where the fiction was most load-bearing.** mesh-host's `examples/substrate-first-node.lock` pins all three of its images at `192.0.2.250:5000/…`, which is the address the lab's registry served from — it was written for a target, and the target was the lab. Two of those are ordinary third-party images and become the digests mesh-catalog's own postgres and lavinmq modules pin, so the substrate's store and broker are literally the images the mesh runs. mesh-control exists in no registry at all and becomes the ID the machine was handed. **The bundle itself should be fixed in mesh-host and this substitution deleted with it.** Beds that wrote a manifest by hand named an image by repository and let the rewrite supply a digest. There is nothing to supply one now, so `onTheMachine` refuses an unpinned reference and hands back the digest the catalogue pins — a bed runs the image the mesh ships, and a bed that drifts from the catalogue is testing a different postgres. Three beds took a third-party image out of the raised list, which no longer contains one: certificates (pebble), objectstore (minio and its client) and provisioner (postgres) now name theirs and pull it. builds and mesh publish into the MESH's own artifact store — the `registry` module's image, on the node, on 5000 — rather than into scenery the lab raised. That is a different claim, and only one of them exists in production. New unit tests cover what a full raise would otherwise be the only way to check: the routes an egress machine gets (that its gateway is still the path to the rest of the scenario, that a range with no path is unreachable rather than leaked to the uplink, that each family gets its own next hop), which machine is handed which of our images, and the `images:` rule that refuses a third-party entry. The "shipped scenarios are valid" test now loads every scenario rather than two of them. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
129 lines
4.9 KiB
TypeScript
129 lines
4.9 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: [192.168.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: [192.168.1.10] }
|
|
egress: true
|
|
sealed:
|
|
at: { segment: home, address: [192.168.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: "192.168.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 === "192.168.1.0/24"), JSON.stringify(routes));
|
|
});
|
|
|
|
/**
|
|
* **The dangerous one.** `home` is 192.168.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: "192.168.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" },
|
|
]);
|
|
});
|