/** * Recurrence gate: every simulator on `internet-external` must route the * customer's public prefix via the ISP edge. * * `internet-external` models the public internet. A host there replies to a * customer's public address through its ISP — modelled by defaulting via * fw-ext (100.64.0.1). Docker's bridge gateway has no path across networks, so * a simulator without that route silently blackholes every reply. * * This was invisible for as long as fw-ext MASQUERADEd the customer to its own * on-link 100.64.0.1: nothing needed routing, so five simulators shipped * without it. Removing that second NAT (an ISP routes a subscriber's public * prefix, it does not NAT it) turned the omission into a hang — first in * `bun add -g` against the sim npm registry, then in the signal module's * release download. Both cost a full build-and-run cycle to find. * * A static check because the honest runtime one is impossible here: fw-ext is * per-test, so at shared-infra readiness the route's next hop does not exist * yet. Reading the compose the generator actually emits (rather than a * hand-kept list) means a NEW simulator is covered the day it is added. */ import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { parse } from 'yaml'; import { generateSharedInfraYaml, generateTestComposeYaml } from './docker-compose-generator'; import type { NetworkConfig } from './types'; /** fw-ext's address on internet-external — the ISP edge every public host exits by. */ const ISP_EDGE = '100.64.0.1'; /** The one container that IS the edge, so it cannot route through itself. */ const EDGE_SERVICE = 'fw-ext'; interface ComposeService { image?: string; networks?: Record; } /** Every optional simulator switched on, so none escapes the sweep. */ const EVERYTHING: NetworkConfig = { topology: 'default', dmzMachines: [], appMachines: [], secureMachines: [], internalMachines: [], secureMgmtMachines: [], domain: 'iamtheinternet.org', ddnsPassword: 'test123', verifyRouting: false, managementVolumes: [], dhcpClient: true, signalCli: true, signalSim: true, signalRelease: true, proxmoxSim: true, }; function publicSimulators(yaml: string): Array<{ name: string; dockerfile: string }> { const compose = parse(yaml) as { services?: Record }; return Object.entries(compose.services ?? {}) .filter(([name, svc]) => name !== EDGE_SERVICE && svc.networks?.['internet-external']) .map(([name, svc]) => ({ name, // The compose carries the tag, not the Dockerfile: the bake convention // is tag `celilo-e2e/` <-> `docker/Dockerfile.` (build-infra // tags every Dockerfile exactly that way). A drift in the convention // makes the reads below fail loudly, which is what we want. dockerfile: `docker/Dockerfile.${(svc.image ?? '').replace('celilo-e2e/', '').replace(/:.*/, '')}`, })); } /** * True if the image establishes the route — either through the shared * entrypoint or by naming the ISP edge itself (a few sims predate the shared * script and do it inline in their own entrypoint). */ function establishesIspRoute(dockerfile: string): boolean { const e2eDir = join(import.meta.dir, '..'); const text = readFileSync(join(e2eDir, dockerfile), 'utf-8'); if (text.includes('public-sim-entrypoint.sh') || text.includes(ISP_EDGE)) return true; // Otherwise the route may live in a script the image copies in. Follow every // COPY source that looks like a script and check those too. for (const match of text.matchAll(/^COPY\s+(?:--from=\S+\s+)?(\S+\.sh)\s/gm)) { try { if (readFileSync(join(e2eDir, match[1]), 'utf-8').includes(ISP_EDGE)) return true; } catch { // A COPY source outside the build context is not a route carrier. } } // Some sims ship their entrypoint in a simulators// directory copied // wholesale; check the obvious sibling. for (const match of text.matchAll(/^COPY\s+(?:--from=\S+\s+)?(simulators\/\S+)\s/gm)) { try { if (readFileSync(join(e2eDir, match[1], 'entrypoint.sh'), 'utf-8').includes(ISP_EDGE)) { return true; } } catch { // Not an entrypoint-bearing directory. } } return false; } describe('public simulators route back through the ISP edge', () => { const simulators = [ ...publicSimulators(generateSharedInfraYaml()), ...publicSimulators(generateTestComposeYaml(EVERYTHING)), ]; test('the sweep actually found simulators', () => { // Guard against the check silently passing because the filter broke. expect(simulators.length).toBeGreaterThan(5); }); for (const { name, dockerfile } of simulators) { test(`${name} routes the customer prefix via ${ISP_EDGE}`, () => { expect(dockerfile).not.toBe(''); expect(establishesIspRoute(dockerfile)).toBe(true); }); } });