Files
mesh-lab/test/egress-routes.test.ts
T
jschoubben 675facdb0d The beds name images the way a machine would find them
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
2026-09-10 23:16:41 +02:00

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" },
]);
});