@celilo/e2e 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/e2e-status +1 -1
- package/bin/e2e-up +2 -2
- package/config/dns/tangohost.com.zone +1 -1
- package/config/dns/templates/example.net.zone +3 -3
- package/config/dns/templates/iamtheinternet.org.zone +2 -2
- package/config/routing/fw-ext-routes.sh +8 -8
- package/config/routing/fw-isp-routes.sh +2 -2
- package/config/routing/fw-main-routes.sh +5 -5
- package/config/routing/management-routes.sh +1 -1
- package/config/routing/observer-setup.sh +1 -1
- package/config/routing/public-sim-entrypoint.sh +1 -1
- package/config/routing/resolver-internal-routes.sh +2 -2
- package/config/routing/resolver-routes.sh +1 -1
- package/config/routing/target-routes.sh +1 -1
- package/config/routing/target-setup.sh +1 -1
- package/config/socks/startup.sh +2 -2
- package/package.json +4 -3
- package/simulators/greenwave/server.ts +35 -0
- package/simulators/greenwave/state.ts +64 -11
- package/simulators/ip-echo/server.ts +1 -1
- package/src/address-plan.test.ts +4 -4
- package/src/cli/build.ts +30 -0
- package/src/cli/command-registry.ts +19 -0
- package/src/cli/completion.ts +16 -1
- package/src/cli/index.ts +85 -4
- package/src/container-manager.ts +75 -12
- package/src/docker-compose-generator.ts +55 -26
- package/src/doctor.test.ts +279 -0
- package/src/doctor.ts +421 -0
- package/src/extract-failure.ts +41 -0
- package/src/index.ts +12 -0
- package/src/last-run.test.ts +62 -0
- package/src/last-run.ts +54 -0
- package/src/observer.test.ts +2 -2
- package/src/observer.ts +1 -1
- package/src/router-swap.ts +121 -0
- package/src/run-lock.test.ts +73 -2
- package/src/run-lock.ts +110 -14
- package/src/runner.ts +99 -5
- package/src/socks-proxy.ts +2 -2
- package/src/types.ts +25 -4
- package/src/vantage.test.ts +1 -1
- package/src/zone-classifier.test.ts +11 -16
- package/src/zone-classifier.ts +16 -31
package/src/cli/index.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* cele2e version
|
|
12
12
|
* cele2e shell [container]
|
|
13
13
|
* cele2e status
|
|
14
|
+
* cele2e doctor
|
|
15
|
+
* cele2e last [--json]
|
|
14
16
|
* cele2e load
|
|
15
17
|
* cele2e scaffold <test-name>
|
|
16
18
|
*/
|
|
@@ -19,11 +21,13 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
21
|
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
20
22
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
23
|
import { parse as parseYaml } from 'yaml';
|
|
24
|
+
import { diagnose, formatHolderLine, formatReport } from '../doctor';
|
|
25
|
+
import { readLastRun } from '../last-run';
|
|
22
26
|
import {
|
|
23
27
|
E2eBusyError,
|
|
28
|
+
type LockHolder,
|
|
24
29
|
acquireRunLock,
|
|
25
30
|
clearLock,
|
|
26
|
-
formatBusy,
|
|
27
31
|
lockStatus,
|
|
28
32
|
markKept,
|
|
29
33
|
} from '../run-lock';
|
|
@@ -129,6 +133,19 @@ function discoverSuiteTests(suiteRoot: string): { name: string; file: string; gr
|
|
|
129
133
|
return out;
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Say out loud when we cleared this session's own kept stack. Silence would be
|
|
138
|
+
* worse than the old refusal: the operator asked for a kept stack, and it is
|
|
139
|
+
* gone now for a reason they should be able to read.
|
|
140
|
+
*/
|
|
141
|
+
function noteAutoRelease(holder: LockHolder | null): void {
|
|
142
|
+
if (!holder) return;
|
|
143
|
+
console.log(
|
|
144
|
+
`Auto-released your own kept stack (${holder.test}, held since ${holder.startedAt}) and continuing.`,
|
|
145
|
+
);
|
|
146
|
+
console.log('Use `cele2e run --reuse` instead if you meant to run against it.');
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
function runScript(script: string, args: string[], env?: Record<string, string>): never {
|
|
133
150
|
const result = spawnSync(join(BIN_DIR, script), args, {
|
|
134
151
|
stdio: 'inherit',
|
|
@@ -155,6 +172,8 @@ Commands:
|
|
|
155
172
|
version Show cele2e version
|
|
156
173
|
shell [container] Shell into a running container (default: management)
|
|
157
174
|
status Show run-lock holder + network status (exit 3 if busy)
|
|
175
|
+
doctor Check the environment a run needs (run's implicit preflight)
|
|
176
|
+
last Show the most recent run's results dir + counts
|
|
158
177
|
release Free a run-lock left by --keep/up (does not tear down)
|
|
159
178
|
load Load cached Docker images from tarball
|
|
160
179
|
scaffold <name> Generate a new test file from template
|
|
@@ -180,6 +199,9 @@ Options for \`down\`:
|
|
|
180
199
|
--keep Stop containers but preserve volumes
|
|
181
200
|
--all Also stop competing module containers
|
|
182
201
|
|
|
202
|
+
Options for \`doctor\` / \`last\` / \`status\`:
|
|
203
|
+
--json Machine-readable output
|
|
204
|
+
|
|
183
205
|
Options for \`build-infra\`:
|
|
184
206
|
--save Save Docker images to tarball after build
|
|
185
207
|
--skip-modules Skip .netapp packaging (only rebuild Docker images)
|
|
@@ -341,7 +363,11 @@ switch (command) {
|
|
|
341
363
|
// kept state (cleared by `cele2e down`/`release`) to stop a concurrent run
|
|
342
364
|
// from wiping it.
|
|
343
365
|
try {
|
|
344
|
-
|
|
366
|
+
const outcome = acquireRunLock({
|
|
367
|
+
test: 'up',
|
|
368
|
+
runId: process.env.CELE2E_RUN_ID ?? crypto.randomUUID(),
|
|
369
|
+
});
|
|
370
|
+
noteAutoRelease(outcome.autoReleasedOwnKept);
|
|
345
371
|
markKept();
|
|
346
372
|
} catch (err) {
|
|
347
373
|
if (err instanceof E2eBusyError) {
|
|
@@ -385,7 +411,19 @@ switch (command) {
|
|
|
385
411
|
if (lock.free) {
|
|
386
412
|
console.log(' free');
|
|
387
413
|
} else if (lock.holder) {
|
|
388
|
-
|
|
414
|
+
// Heartbeat age is always printed: a wedged holder keeps a live PID, so
|
|
415
|
+
// the only thing separating "slow" from "hung for 32 minutes" is how long
|
|
416
|
+
// ago it last beat — previously visible only by reading beatAt by hand.
|
|
417
|
+
console.log(` BUSY — ${formatHolderLine(lock.holder)}`);
|
|
418
|
+
if (lock.suspect) {
|
|
419
|
+
console.log(
|
|
420
|
+
' ⚠ SUSPECT: the process is alive but has stopped beating. It is probably wedged;',
|
|
421
|
+
);
|
|
422
|
+
console.log(` inspect or kill pid ${lock.holder.pid}, then \`cele2e down\`.`);
|
|
423
|
+
}
|
|
424
|
+
if (lock.ownKept) {
|
|
425
|
+
console.log(' This is YOUR OWN kept stack — the next `cele2e run` auto-releases it.');
|
|
426
|
+
}
|
|
389
427
|
}
|
|
390
428
|
console.log('');
|
|
391
429
|
|
|
@@ -410,6 +448,48 @@ switch (command) {
|
|
|
410
448
|
break;
|
|
411
449
|
}
|
|
412
450
|
|
|
451
|
+
case 'doctor': {
|
|
452
|
+
// The single "is my environment sane?" answer. `run` calls the same
|
|
453
|
+
// diagnose() as its preflight, so what doctor reports is exactly what a run
|
|
454
|
+
// will refuse on — no second, drifting copy of the rules.
|
|
455
|
+
const report = diagnose({ pkgDir: PKG_DIR });
|
|
456
|
+
if (args.includes('--json')) {
|
|
457
|
+
console.log(JSON.stringify(report, null, 2));
|
|
458
|
+
process.exit(report.ok ? 0 : 1);
|
|
459
|
+
}
|
|
460
|
+
console.log('=== cele2e doctor ===');
|
|
461
|
+
for (const line of formatReport(report)) console.log(line);
|
|
462
|
+
console.log('');
|
|
463
|
+
console.log(
|
|
464
|
+
report.ok ? 'Environment looks runnable.' : 'Environment is NOT runnable — see fixes above.',
|
|
465
|
+
);
|
|
466
|
+
process.exit(report.ok ? 0 : 1);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
case 'last': {
|
|
471
|
+
// Which results directory was MINE? Inferring it from `ls -t results` returns
|
|
472
|
+
// the newest run, which after a refused start is somebody else's — a suite
|
|
473
|
+
// that never executed then reads as a clean pass.
|
|
474
|
+
const last = readLastRun();
|
|
475
|
+
if (args.includes('--json')) {
|
|
476
|
+
console.log(JSON.stringify(last));
|
|
477
|
+
process.exit(last ? 0 : 1);
|
|
478
|
+
}
|
|
479
|
+
if (!last) {
|
|
480
|
+
console.error('No cele2e run recorded yet.');
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
console.log(`runId: ${last.runId}`);
|
|
484
|
+
console.log(`resultsDir: ${last.resultsDir}`);
|
|
485
|
+
console.log(`status: ${last.status}`);
|
|
486
|
+
console.log(
|
|
487
|
+
`results: ${last.passed} passed, ${last.failed} failed${last.skipped > 0 ? `, ${last.skipped} skipped (blocked by an earlier stage)` : ''} of ${last.total}`,
|
|
488
|
+
);
|
|
489
|
+
process.exit(0);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
413
493
|
case 'load':
|
|
414
494
|
runScript('e2e-load', args);
|
|
415
495
|
break;
|
|
@@ -422,10 +502,11 @@ switch (command) {
|
|
|
422
502
|
// build-infra mutates the shared Docker images — serialize against runs.
|
|
423
503
|
// Released via the lock's exit handler (runBuild may process.exit on failure).
|
|
424
504
|
try {
|
|
425
|
-
acquireRunLock({
|
|
505
|
+
const outcome = acquireRunLock({
|
|
426
506
|
test: 'build-infra',
|
|
427
507
|
runId: process.env.CELE2E_RUN_ID ?? crypto.randomUUID(),
|
|
428
508
|
});
|
|
509
|
+
noteAutoRelease(outcome.autoReleasedOwnKept);
|
|
429
510
|
} catch (err) {
|
|
430
511
|
if (err instanceof E2eBusyError) {
|
|
431
512
|
console.error(err.message);
|
package/src/container-manager.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { type ExecSyncOptions, execSync, spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, join, resolve } from 'node:path';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
6
|
import { startBrowser } from './browser';
|
|
6
7
|
import { MIN_CLI_VERSION, checkCliVersion } from './cli-version-contract';
|
|
7
8
|
import {
|
|
8
9
|
SHARED_PROJECT_NAME,
|
|
10
|
+
firewallZoneLegs,
|
|
9
11
|
generateTestComposeYaml,
|
|
10
12
|
getAllMachines,
|
|
11
13
|
} from './docker-compose-generator';
|
|
14
|
+
import { explainBuildFailure } from './doctor';
|
|
12
15
|
import { ensureSharedInfra } from './shared-infra';
|
|
13
16
|
import { SIMULATOR_IPS } from './simulator-ips';
|
|
14
17
|
import { startSocksProxy } from './socks-proxy';
|
|
@@ -20,8 +23,15 @@ import type {
|
|
|
20
23
|
NetworkHandle,
|
|
21
24
|
ProxyOptions,
|
|
22
25
|
SocksProxyHandle,
|
|
26
|
+
TopologyPreset,
|
|
27
|
+
} from './types';
|
|
28
|
+
import {
|
|
29
|
+
CeliloCommandError,
|
|
30
|
+
ZONE_GATEWAYS,
|
|
31
|
+
ZONE_SUBNETS,
|
|
32
|
+
greenwaveRouterIp,
|
|
33
|
+
internalNatIp,
|
|
23
34
|
} from './types';
|
|
24
|
-
import { CeliloCommandError, ZONE_GATEWAYS, ZONE_SUBNETS, internalNatIp } from './types';
|
|
25
35
|
|
|
26
36
|
/** Package root — where docker/, config/, simulators/ live */
|
|
27
37
|
const PACKAGE_ROOT = join(__dirname, '..');
|
|
@@ -499,7 +509,11 @@ async function streamingBuild(
|
|
|
499
509
|
console.log('[progress:done] images built');
|
|
500
510
|
resolve();
|
|
501
511
|
} else {
|
|
502
|
-
reject(
|
|
512
|
+
reject(
|
|
513
|
+
new Error(
|
|
514
|
+
`docker compose build failed (exit ${code}):\n${stderr.slice(-500)}${explainBuildFailure(stderr)}`,
|
|
515
|
+
),
|
|
516
|
+
);
|
|
503
517
|
}
|
|
504
518
|
});
|
|
505
519
|
});
|
|
@@ -515,6 +529,7 @@ function buildNetworkHandle(
|
|
|
515
529
|
projectName: string,
|
|
516
530
|
composeDir: string,
|
|
517
531
|
celiloRoot: string | undefined,
|
|
532
|
+
topology: TopologyPreset,
|
|
518
533
|
): NetworkHandle {
|
|
519
534
|
// Track host-side resources spawned via the handle (SOCKS proxies,
|
|
520
535
|
// Playwright browsers). stop() tears these down before the docker
|
|
@@ -598,10 +613,21 @@ function buildNetworkHandle(
|
|
|
598
613
|
},
|
|
599
614
|
|
|
600
615
|
async deployFirewall(opts = {}): Promise<void> {
|
|
601
|
-
|
|
616
|
+
// `opts.zones` says which zones get `provided_networks` written to system
|
|
617
|
+
// config. It does NOT say which NICs the box has — the compose topology
|
|
618
|
+
// decides that, and it wires fw-main to every segmented zone regardless of
|
|
619
|
+
// what a test passed. Roughly twenty call sites passed `['dmz']` against a
|
|
620
|
+
// four-leg firewall, so every one of them under-declared the hardware.
|
|
621
|
+
//
|
|
622
|
+
// Deriving the declared list from the topology makes under-declaring
|
|
623
|
+
// structurally impossible rather than something each future test author has
|
|
624
|
+
// to remember, and removes one more hand-maintained copy of a list the
|
|
625
|
+
// compose generator already owns.
|
|
626
|
+
const providedZones = opts.zones ?? ['dmz', 'app', 'secure'];
|
|
627
|
+
const declaredZones = firewallZoneLegs(topology);
|
|
602
628
|
const firewallIp = ZONE_GATEWAYS.internal; // fw-main on internal
|
|
603
629
|
const natIp = opts.natIp ?? internalNatIp();
|
|
604
|
-
const providedNetworks =
|
|
630
|
+
const providedNetworks = providedZones.map((zone) => ({
|
|
605
631
|
zone,
|
|
606
632
|
subnet: ZONE_SUBNETS[zone],
|
|
607
633
|
gateway: ZONE_GATEWAYS[zone],
|
|
@@ -637,15 +663,23 @@ function buildNetworkHandle(
|
|
|
637
663
|
await handle.celilo('module import iptables');
|
|
638
664
|
await handle.celilo(`module config set iptables firewall_ip ${firewallIp}`);
|
|
639
665
|
await handle.celilo(`module config set iptables nat_ip ${natIp}`);
|
|
640
|
-
await handle.celilo(
|
|
641
|
-
`module config set iptables zones '${JSON.stringify(['internal', ...zones])}'`,
|
|
642
|
-
);
|
|
666
|
+
await handle.celilo(`module config set iptables zones '${JSON.stringify(declaredZones)}'`);
|
|
643
667
|
await handle.celilo(
|
|
644
668
|
`module config set iptables provided_networks '${JSON.stringify(providedNetworks)}'`,
|
|
645
669
|
);
|
|
646
670
|
await handle.celilo('module deploy iptables', 180_000);
|
|
647
671
|
},
|
|
648
672
|
|
|
673
|
+
async deployGreenwave(): Promise<void> {
|
|
674
|
+
const routerIp = greenwaveRouterIp();
|
|
675
|
+
await handle.celilo(`machine add ${routerIp} --ssh-user root --earmark greenwave`);
|
|
676
|
+
await handle.celilo('module import greenwave');
|
|
677
|
+
await handle.celilo(`module config set greenwave router_ip ${routerIp}`);
|
|
678
|
+
await handle.celilo('module secret set greenwave router_username admin');
|
|
679
|
+
await handle.celilo('module secret set greenwave router_password admin');
|
|
680
|
+
await handle.celilo('module deploy greenwave', 180_000);
|
|
681
|
+
},
|
|
682
|
+
|
|
649
683
|
async socksProxy(options: ProxyOptions = {}): Promise<SocksProxyHandle> {
|
|
650
684
|
const proxy = await startSocksProxy(projectName, options);
|
|
651
685
|
activeProxies.push(proxy);
|
|
@@ -889,7 +923,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
889
923
|
// means the test is replacing the infra resolver with a module under test
|
|
890
924
|
// (e.g., knot-unbound-internal). The bare target machine has no DNS server
|
|
891
925
|
// until its module deploys, so this wait would always time out. Management
|
|
892
|
-
// can still resolve names via the fallback `nameserver
|
|
926
|
+
// can still resolve names via the fallback `nameserver 203.0.113.1` in
|
|
893
927
|
// its resolv.conf during deploy.
|
|
894
928
|
//
|
|
895
929
|
// The resolver-under-test may sit in ANY zone, not just `internal`: ISS-0156
|
|
@@ -1063,7 +1097,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
1063
1097
|
config.managementZone === 'secure-mgmt' || (config.secureMgmtMachines ?? []).length > 0
|
|
1064
1098
|
? `network.secure-mgmt.subnet=${ZONE_SUBNETS['secure-mgmt']} network.secure-mgmt.gateway=${ZONE_GATEWAYS['secure-mgmt']} `
|
|
1065
1099
|
: ''
|
|
1066
|
-
}dns.primary=
|
|
1100
|
+
}dns.primary=203.0.113.1 \
|
|
1067
1101
|
dns.fallback=1.0.0.1,8.8.8.8`,
|
|
1068
1102
|
);
|
|
1069
1103
|
if (initResult.exitCode !== 0) {
|
|
@@ -1095,7 +1129,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
1095
1129
|
// Emit project name so the runner can persist it if --keep is set
|
|
1096
1130
|
console.log(`[e2e:project] ${projectName}`);
|
|
1097
1131
|
|
|
1098
|
-
return buildNetworkHandle(projectName, composeDir, celiloRoot);
|
|
1132
|
+
return buildNetworkHandle(projectName, composeDir, celiloRoot, config.topology);
|
|
1099
1133
|
}
|
|
1100
1134
|
|
|
1101
1135
|
/**
|
|
@@ -1106,6 +1140,26 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
1106
1140
|
*
|
|
1107
1141
|
* If the project doesn't exist, throws with an actionable error.
|
|
1108
1142
|
*/
|
|
1143
|
+
/**
|
|
1144
|
+
* Which topology a already-running stack was built with, read back from its
|
|
1145
|
+
* generated compose file. `direct-internet` is the one where fw-main holds a leg
|
|
1146
|
+
* on `isp-external`; everything else is `default`.
|
|
1147
|
+
*/
|
|
1148
|
+
function topologyFromComposeFile(composePath: string): TopologyPreset {
|
|
1149
|
+
try {
|
|
1150
|
+
const compose = parseYaml(readFileSync(composePath, 'utf-8')) as {
|
|
1151
|
+
services?: Record<string, { networks?: Record<string, unknown> }>;
|
|
1152
|
+
};
|
|
1153
|
+
return compose.services?.['fw-main']?.networks?.['isp-external']
|
|
1154
|
+
? 'direct-internet'
|
|
1155
|
+
: 'default';
|
|
1156
|
+
} catch {
|
|
1157
|
+
// An unreadable compose file means the stack was not built by this
|
|
1158
|
+
// generator; `default` is the topology the fleet runs and the safer guess.
|
|
1159
|
+
return 'default';
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1109
1163
|
export function reconnectNetwork(projectName: string): NetworkHandle {
|
|
1110
1164
|
const composeDir = PACKAGE_ROOT;
|
|
1111
1165
|
|
|
@@ -1128,5 +1182,14 @@ export function reconnectNetwork(projectName: string): NetworkHandle {
|
|
|
1128
1182
|
console.log(`[progress:done] reconnected to existing network (${projectName})`);
|
|
1129
1183
|
console.log(`[e2e:project] ${projectName}`);
|
|
1130
1184
|
|
|
1131
|
-
|
|
1185
|
+
// `--reuse` has no NetworkConfig to read, so the topology is recovered from the
|
|
1186
|
+
// compose file that built the running stack — the same artifact the generator
|
|
1187
|
+
// wrote. Guessing a default here would silently mis-declare the firewall's legs
|
|
1188
|
+
// on exactly the runs used for debugging.
|
|
1189
|
+
return buildNetworkHandle(
|
|
1190
|
+
projectName,
|
|
1191
|
+
composeDir,
|
|
1192
|
+
findCeliloRoot(),
|
|
1193
|
+
topologyFromComposeFile(join(composeDir, COMPOSE_FILE)),
|
|
1194
|
+
);
|
|
1132
1195
|
}
|
|
@@ -7,6 +7,7 @@ import type { MachineSpec, NetworkConfig, TopologyPreset, Zone } from './types';
|
|
|
7
7
|
import {
|
|
8
8
|
ZONE_GATEWAYS,
|
|
9
9
|
ZONE_SUBNETS,
|
|
10
|
+
externalWanIp,
|
|
10
11
|
greenwaveRouterIp,
|
|
11
12
|
internalResolverIp,
|
|
12
13
|
zoneIp,
|
|
@@ -146,16 +147,50 @@ const PROTECTED_SUBNETS = (['dmz', 'app', 'secure', 'secure-mgmt'] as const)
|
|
|
146
147
|
.map((zone) => ZONE_SUBNETS[zone])
|
|
147
148
|
.join(' ');
|
|
148
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The zone legs fw-main actually holds, per topology — the SINGLE source of truth
|
|
152
|
+
* for "which networks is the customer firewall wired to".
|
|
153
|
+
*
|
|
154
|
+
* This exists because `deployFirewall`'s `opts.zones` was doing two jobs at once:
|
|
155
|
+
* saying which zones get `provided_networks` written to system config, AND
|
|
156
|
+
* standing in as a claim about the box's hardware. It was only ever the first.
|
|
157
|
+
* The compose file below is what decides the second, and roughly twenty call
|
|
158
|
+
* sites passed `['dmz']` while the generator wired four legs regardless — so
|
|
159
|
+
* every one of them under-declared, silently.
|
|
160
|
+
*
|
|
161
|
+
* Both the compose services and `deployFirewall` read this, so a test cannot
|
|
162
|
+
* under-declare by omission and a new leg cannot be added in one place only.
|
|
163
|
+
* `packages/e2e/tests/firewall-zone-legs.test.ts` holds them to each other.
|
|
164
|
+
*/
|
|
165
|
+
export type FirewallLeg = Zone | 'external';
|
|
166
|
+
|
|
167
|
+
export function firewallZoneLegs(topology: TopologyPreset): FirewallLeg[] {
|
|
168
|
+
const segmented: FirewallLeg[] = ['internal', 'dmz', 'app', 'secure'];
|
|
169
|
+
return topology === 'direct-internet' ? [...segmented, 'external'] : segmented;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The compose `networks:` block for fw-main, derived from {@link firewallZoneLegs}
|
|
174
|
+
* so the wiring and the declaration cannot drift. `external` is the public leg and
|
|
175
|
+
* lives on the `isp-external` docker network rather than a zone-named one.
|
|
176
|
+
*/
|
|
177
|
+
function firewallNetworks(topology: TopologyPreset): Record<string, { ipv4_address: string }> {
|
|
178
|
+
const nets: Record<string, { ipv4_address: string }> = {};
|
|
179
|
+
for (const zone of firewallZoneLegs(topology)) {
|
|
180
|
+
if (zone === 'external') {
|
|
181
|
+
nets['isp-external'] = { ipv4_address: externalWanIp() };
|
|
182
|
+
} else {
|
|
183
|
+
nets[zone] = { ipv4_address: ZONE_GATEWAYS[zone] };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return nets;
|
|
187
|
+
}
|
|
188
|
+
|
|
149
189
|
function defaultTopologyServices(config: NetworkConfig): Record<string, unknown> {
|
|
150
190
|
return {
|
|
151
191
|
'fw-main': baseService({
|
|
152
192
|
build: { context: '.', dockerfile: 'docker/Dockerfile.firewall' },
|
|
153
|
-
networks:
|
|
154
|
-
internal: { ipv4_address: ZONE_GATEWAYS.internal },
|
|
155
|
-
dmz: { ipv4_address: ZONE_GATEWAYS.dmz },
|
|
156
|
-
app: { ipv4_address: ZONE_GATEWAYS.app },
|
|
157
|
-
secure: { ipv4_address: ZONE_GATEWAYS.secure },
|
|
158
|
-
},
|
|
193
|
+
networks: firewallNetworks('default'),
|
|
159
194
|
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
160
195
|
// A firewall may also terminate a VPN (the admin tunnel the `wireguard`
|
|
161
196
|
// module owns). wireguard-go needs the tun device; the kernel module is
|
|
@@ -169,7 +204,7 @@ function defaultTopologyServices(config: NetworkConfig): Record<string, unknown>
|
|
|
169
204
|
build: { context: '.', dockerfile: 'docker/Dockerfile.greenwave-sim' },
|
|
170
205
|
networks: {
|
|
171
206
|
internal: { ipv4_address: greenwaveRouterIp() },
|
|
172
|
-
'isp-external': { ipv4_address: '
|
|
207
|
+
'isp-external': { ipv4_address: '203.0.113.100' },
|
|
173
208
|
},
|
|
174
209
|
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
175
210
|
// A firewall may also terminate a VPN (the admin tunnel the `wireguard`
|
|
@@ -194,13 +229,7 @@ function directInternetTopologyServices(): Record<string, unknown> {
|
|
|
194
229
|
return {
|
|
195
230
|
'fw-main': baseService({
|
|
196
231
|
build: { context: '.', dockerfile: 'docker/Dockerfile.firewall' },
|
|
197
|
-
networks:
|
|
198
|
-
internal: { ipv4_address: ZONE_GATEWAYS.internal },
|
|
199
|
-
dmz: { ipv4_address: ZONE_GATEWAYS.dmz },
|
|
200
|
-
app: { ipv4_address: ZONE_GATEWAYS.app },
|
|
201
|
-
secure: { ipv4_address: ZONE_GATEWAYS.secure },
|
|
202
|
-
'isp-external': { ipv4_address: '100.100.0.100' },
|
|
203
|
-
},
|
|
232
|
+
networks: firewallNetworks('direct-internet'),
|
|
204
233
|
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
205
234
|
// A firewall may also terminate a VPN (the admin tunnel the `wireguard`
|
|
206
235
|
// module owns). wireguard-go needs the tun device; the kernel module is
|
|
@@ -302,7 +331,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
302
331
|
build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
|
|
303
332
|
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.PEBBLE } },
|
|
304
333
|
cap_add: ['NET_ADMIN'],
|
|
305
|
-
command: '-config /config/pebble-config.json -dnsserver
|
|
334
|
+
command: '-config /config/pebble-config.json -dnsserver 203.0.113.1:53',
|
|
306
335
|
volumes: [
|
|
307
336
|
'./config/pebble/pebble-config.json:/config/pebble-config.json:ro',
|
|
308
337
|
'./config/pebble/pebble-tls.crt:/config/pebble-tls.crt:ro',
|
|
@@ -325,7 +354,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
325
354
|
// celilo's `public_dns` check resolves from.
|
|
326
355
|
//
|
|
327
356
|
// A peer of comcast-resolver, not a replacement: the fleet is configured to
|
|
328
|
-
// use THAT one (`system init` writes dns.primary=
|
|
357
|
+
// use THAT one (`system init` writes dns.primary=203.0.113.1), and celilo
|
|
329
358
|
// refuses to accept a resolver it already asks as evidence about the public
|
|
330
359
|
// internet — a check that used the fleet's own resolver would pass whatever
|
|
331
360
|
// the internet sees, which is the celilo#626 blindness one layer up. So the
|
|
@@ -357,13 +386,13 @@ export function generateSharedInfraYaml(): string {
|
|
|
357
386
|
// External website prober simulator — mimics isitup.org's /api.json endpoint.
|
|
358
387
|
// Sits on internet-external so probe requests exercise the real public-facing
|
|
359
388
|
// path (target domain → DNS → ISP → firewall → Caddy). Uses comcast-resolver
|
|
360
|
-
// at
|
|
389
|
+
// at 203.0.113.1 as DNS so it can recursively resolve the simulated public
|
|
361
390
|
// zones (e.g. iamtheinternet.org).
|
|
362
391
|
services.isitup = baseService({
|
|
363
392
|
build: { context: '.', dockerfile: 'docker/Dockerfile.isitup' },
|
|
364
393
|
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.ISITUP } },
|
|
365
394
|
cap_add: ['NET_ADMIN'],
|
|
366
|
-
dns: ['
|
|
395
|
+
dns: ['203.0.113.1'],
|
|
367
396
|
});
|
|
368
397
|
|
|
369
398
|
// celilo.computer static site simulator — serves install.sh and the docs
|
|
@@ -375,7 +404,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
375
404
|
build: { context: '.', dockerfile: 'docker/Dockerfile.celilo-website-sim' },
|
|
376
405
|
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.WEBSITE } },
|
|
377
406
|
cap_add: ['NET_ADMIN'],
|
|
378
|
-
dns: ['
|
|
407
|
+
dns: ['203.0.113.1'],
|
|
379
408
|
});
|
|
380
409
|
|
|
381
410
|
// npm-compat registry simulator — serves @celilo/* tarballs to install.sh's
|
|
@@ -452,7 +481,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
452
481
|
// Only when celilo-mgr lives off the internal LAN — keeps the default
|
|
453
482
|
// topology's generated compose byte-identical for every existing test.
|
|
454
483
|
...(needsSecureMgmt ? { 'secure-mgmt': zoneNetworkDef('secure-mgmt') } : {}),
|
|
455
|
-
'isp-external': networkDef('
|
|
484
|
+
'isp-external': networkDef('203.0.113.0/24', '203.0.113.250'),
|
|
456
485
|
// internet-external is owned by shared infra; real-internet is per-test
|
|
457
486
|
// (it just bridges fw-ext ↔ comcast-resolver, no shared service uses it).
|
|
458
487
|
// real-internet uses Docker's default gateway (.1) because fw-ext's
|
|
@@ -588,7 +617,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
588
617
|
services['fw-ext'] = baseService({
|
|
589
618
|
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
590
619
|
networks: {
|
|
591
|
-
'isp-external': { ipv4_address: '
|
|
620
|
+
'isp-external': { ipv4_address: '203.0.113.101' },
|
|
592
621
|
'internet-external': { ipv4_address: '100.64.0.1' },
|
|
593
622
|
'real-internet': { ipv4_address: '172.30.0.5' },
|
|
594
623
|
},
|
|
@@ -599,7 +628,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
599
628
|
services['comcast-resolver'] = baseService({
|
|
600
629
|
build: { context: '.', dockerfile: 'docker/Dockerfile.resolver' },
|
|
601
630
|
networks: {
|
|
602
|
-
'isp-external': { ipv4_address: '
|
|
631
|
+
'isp-external': { ipv4_address: '203.0.113.1' },
|
|
603
632
|
'real-internet': { ipv4_address: '172.30.0.4' },
|
|
604
633
|
},
|
|
605
634
|
cap_add: ['NET_ADMIN'],
|
|
@@ -720,7 +749,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
720
749
|
dmz: zoneNetworkDef('dmz'),
|
|
721
750
|
app: zoneNetworkDef('app'),
|
|
722
751
|
secure: zoneNetworkDef('secure'),
|
|
723
|
-
'isp-external': networkDef('
|
|
752
|
+
'isp-external': networkDef('203.0.113.0/24', '203.0.113.250'),
|
|
724
753
|
'internet-external': networkDef('100.64.0.0/24', '100.64.0.250'),
|
|
725
754
|
'real-internet': {
|
|
726
755
|
driver: 'bridge',
|
|
@@ -774,7 +803,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
774
803
|
services['fw-ext'] = baseService({
|
|
775
804
|
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
776
805
|
networks: {
|
|
777
|
-
'isp-external': { ipv4_address: '
|
|
806
|
+
'isp-external': { ipv4_address: '203.0.113.101' },
|
|
778
807
|
'internet-external': { ipv4_address: '100.64.0.1' },
|
|
779
808
|
'real-internet': { ipv4_address: '172.30.0.5' },
|
|
780
809
|
},
|
|
@@ -785,7 +814,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
785
814
|
services['comcast-resolver'] = baseService({
|
|
786
815
|
build: { context: '.', dockerfile: 'docker/Dockerfile.resolver' },
|
|
787
816
|
networks: {
|
|
788
|
-
'isp-external': { ipv4_address: '
|
|
817
|
+
'isp-external': { ipv4_address: '203.0.113.1' },
|
|
789
818
|
'real-internet': { ipv4_address: '172.30.0.4' },
|
|
790
819
|
},
|
|
791
820
|
cap_add: ['NET_ADMIN'],
|
|
@@ -847,7 +876,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
847
876
|
build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
|
|
848
877
|
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.PEBBLE } },
|
|
849
878
|
cap_add: ['NET_ADMIN'],
|
|
850
|
-
command: '-config /config/pebble-config.json -dnsserver
|
|
879
|
+
command: '-config /config/pebble-config.json -dnsserver 203.0.113.1:53',
|
|
851
880
|
volumes: [
|
|
852
881
|
'./config/pebble/pebble-config.json:/config/pebble-config.json:ro',
|
|
853
882
|
'./config/pebble/pebble-tls.crt:/config/pebble-tls.crt:ro',
|