@celilo/e2e 0.9.2 → 0.10.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/config/cpanel-host/cpanel-host-ca.crt +20 -0
- package/config/cpanel-host/docroot/.htaccess +14 -0
- package/config/cpanel-host/docroot/cms.html +4 -0
- package/config/cpanel-host/site-tls.crt +20 -0
- package/config/cpanel-host/site-tls.key +28 -0
- package/config/cpanel-host/site.conf +27 -0
- package/config/dns/com.zone +7 -0
- package/config/dns/knot-namecheap.conf +6 -0
- package/config/dns/tangohost.com.zone +14 -0
- package/config/proxy/squid.conf +7 -2
- package/config/resolver/unbound.conf +39 -3
- package/config/routing/management-routes.sh +9 -4
- package/config/routing/resolver-internal-routes.sh +10 -0
- package/docker/Dockerfile.cpanel-host-sim +70 -0
- package/docker/Dockerfile.firewall +12 -0
- package/docker/Dockerfile.management +6 -0
- package/docker/Dockerfile.minio +1 -1
- package/docker/Dockerfile.observer +11 -0
- package/docker/Dockerfile.signal-cli +60 -0
- package/docker/Dockerfile.signal-release +36 -0
- package/docker/Dockerfile.signal-sim +17 -0
- package/docker/Dockerfile.target-machine +10 -1
- package/docker/Dockerfile.target-machine-docker +1 -1
- package/docker/Dockerfile.target-technitium +1 -1
- package/package.json +1 -1
- package/registry-server/src/auth.ts +2 -2
- package/registry-server/src/introspection.test.ts +247 -0
- package/registry-server/src/introspection.ts +204 -0
- package/registry-server/src/module-owner-store.test.ts +85 -0
- package/registry-server/src/module-owner-store.ts +108 -0
- package/registry-server/src/server.test.ts +262 -0
- package/registry-server/src/server.ts +139 -10
- package/scripts/pack-celilo-packages.ts +79 -51
- package/scripts/stage-libsignal.ts +116 -0
- package/simulators/signal-cli/server.ts +272 -0
- package/src/cli/build.ts +35 -9
- package/src/cli/command-registry.ts +34 -6
- package/src/cli/index.ts +59 -21
- package/src/container-manager.ts +6 -2
- package/src/docker-compose-generator.ts +108 -4
- package/src/network-builder.ts +52 -0
- package/src/runner.ts +128 -57
- package/src/simulator-ips.ts +9 -0
- package/src/types.ts +34 -0
- package/config/dns/example.net.zone +0 -16
- package/config/dns/iamtheinternet.org.zone +0 -16
|
@@ -25,16 +25,31 @@ export const COMMANDS: CommandDef[] = [
|
|
|
25
25
|
name: 'run',
|
|
26
26
|
description: 'Run e2e tests — pass a module path or pattern, or --all / --all-modules',
|
|
27
27
|
flags: [
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
{
|
|
29
|
+
name: '--all',
|
|
30
|
+
description:
|
|
31
|
+
'Full regression: every module suite + top-level e2e/tests/ (quarantined cele2e-ci-unsafe tests skipped)',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: '--complete',
|
|
35
|
+
description:
|
|
36
|
+
'Run EVERYTHING including quarantined (cele2e-ci-unsafe) tests — the only mode that opts into them',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: '--ci-safe',
|
|
40
|
+
description:
|
|
41
|
+
'Alias for the default full regression (quarantined tests skipped); kept for the nightly',
|
|
42
|
+
},
|
|
31
43
|
{ name: '--all-modules', description: 'Run e2e tests for all modules with e2e/ directories' },
|
|
32
44
|
{ name: '--keep', description: 'Keep network running after tests' },
|
|
33
45
|
{ name: '--reuse', description: 'Reuse existing network if running' },
|
|
34
46
|
{ name: '--live', description: 'Use live (non-simulated) internet' },
|
|
35
47
|
{ name: '--published', description: 'Use published .netapp packages' },
|
|
36
48
|
{ name: '--notify', description: 'Desktop notification when the run finishes' },
|
|
37
|
-
{
|
|
49
|
+
{
|
|
50
|
+
name: '--shuffle',
|
|
51
|
+
description: 'Randomize test order to surface order-dependent bugs (seed is logged)',
|
|
52
|
+
},
|
|
38
53
|
{ name: '--seed=', description: 'Replay a specific --shuffle order (e.g. --seed=12345)' },
|
|
39
54
|
],
|
|
40
55
|
},
|
|
@@ -65,7 +80,10 @@ export const COMMANDS: CommandDef[] = [
|
|
|
65
80
|
description: 'Rebuild Docker images and package all modules as .netapps',
|
|
66
81
|
flags: [
|
|
67
82
|
{ name: '--save', description: 'Save Docker images to tarball after build' },
|
|
68
|
-
{
|
|
83
|
+
{
|
|
84
|
+
name: '--skip-modules',
|
|
85
|
+
description: 'Skip .netapp packaging (only rebuild Docker images)',
|
|
86
|
+
},
|
|
69
87
|
],
|
|
70
88
|
},
|
|
71
89
|
{
|
|
@@ -109,7 +127,17 @@ export const COMMANDS: CommandDef[] = [
|
|
|
109
127
|
|
|
110
128
|
export const UP_PRESETS = ['--caddy', '--full-stack', '--infrastructure'];
|
|
111
129
|
|
|
112
|
-
export const RUN_FLAGS = [
|
|
130
|
+
export const RUN_FLAGS = [
|
|
131
|
+
'--all',
|
|
132
|
+
'--complete',
|
|
133
|
+
'--ci-safe',
|
|
134
|
+
'--all-modules',
|
|
135
|
+
'--keep',
|
|
136
|
+
'--reuse',
|
|
137
|
+
'--live',
|
|
138
|
+
'--published',
|
|
139
|
+
'--notify',
|
|
140
|
+
];
|
|
113
141
|
|
|
114
142
|
export const DOWN_FLAGS = ['--keep', '--all'];
|
|
115
143
|
|
package/src/cli/index.ts
CHANGED
|
@@ -19,19 +19,27 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
19
|
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
20
20
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { parse as parseYaml } from 'yaml';
|
|
22
|
+
import {
|
|
23
|
+
E2eBusyError,
|
|
24
|
+
acquireRunLock,
|
|
25
|
+
clearLock,
|
|
26
|
+
formatBusy,
|
|
27
|
+
lockStatus,
|
|
28
|
+
markKept,
|
|
29
|
+
} from '../run-lock';
|
|
22
30
|
import { runBuild } from './build';
|
|
23
|
-
import { runScaffold } from './scaffold';
|
|
24
31
|
import { generateBashCompletion, generateZshCompletion, getCompletions } from './completion';
|
|
25
32
|
import { findAllModules, findModulesWithMatchingTest } from './module-discovery';
|
|
26
|
-
import {
|
|
33
|
+
import { runScaffold } from './scaffold';
|
|
27
34
|
|
|
28
35
|
const PKG_DIR = resolve(import.meta.dir, '../..');
|
|
29
36
|
const BIN_DIR = join(PKG_DIR, 'bin');
|
|
30
37
|
|
|
31
|
-
|
|
32
38
|
function readManifestTestsDir(manifestPath: string): string | undefined {
|
|
33
39
|
try {
|
|
34
|
-
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
|
|
40
|
+
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
|
|
41
|
+
e2e?: { tests_dir?: string };
|
|
42
|
+
};
|
|
35
43
|
return parsed?.e2e?.tests_dir;
|
|
36
44
|
} catch {
|
|
37
45
|
return undefined;
|
|
@@ -53,6 +61,12 @@ function detectTestDir(): { stateDir: string; testsPath: string } {
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
if (existsSync(join(cwd, 'tests'))) return { stateDir: cwd, testsPath: join(cwd, 'tests') };
|
|
64
|
+
// The repo's own top-level suite lives at e2e/tests/, which is what `list`
|
|
65
|
+
// enumerates. Without this, `cele2e list` shows a test that `cele2e run
|
|
66
|
+
// <name>` then cannot find — the run/list disagreement in #412.
|
|
67
|
+
if (existsSync(join(cwd, 'e2e', 'tests'))) {
|
|
68
|
+
return { stateDir: join(cwd, 'e2e'), testsPath: join(cwd, 'e2e', 'tests') };
|
|
69
|
+
}
|
|
56
70
|
return { stateDir: cwd, testsPath: join(cwd, 'tests') };
|
|
57
71
|
}
|
|
58
72
|
|
|
@@ -90,14 +104,26 @@ function discoverSuiteTests(suiteRoot: string): { name: string; file: string; gr
|
|
|
90
104
|
? resolve(moduleDir, readManifestTestsDir(join(moduleDir, 'manifest.yml')) ?? 'e2e')
|
|
91
105
|
: tp;
|
|
92
106
|
if (!existsSync(testsDir)) continue;
|
|
93
|
-
for (const f of readdirSync(testsDir)
|
|
94
|
-
|
|
107
|
+
for (const f of readdirSync(testsDir)
|
|
108
|
+
.filter((f) => f.endsWith('.test.ts'))
|
|
109
|
+
.sort()) {
|
|
110
|
+
out.push({
|
|
111
|
+
name: f.replace(/\.test\.ts$/, ''),
|
|
112
|
+
file: join(testsDir, f),
|
|
113
|
+
group: basename(moduleDir),
|
|
114
|
+
});
|
|
95
115
|
}
|
|
96
116
|
}
|
|
97
117
|
const topLevel = join(suiteRoot, 'e2e', 'tests');
|
|
98
118
|
if (existsSync(topLevel)) {
|
|
99
|
-
for (const f of readdirSync(topLevel)
|
|
100
|
-
|
|
119
|
+
for (const f of readdirSync(topLevel)
|
|
120
|
+
.filter((f) => f.endsWith('.test.ts'))
|
|
121
|
+
.sort()) {
|
|
122
|
+
out.push({
|
|
123
|
+
name: f.replace(/\.test\.ts$/, ''),
|
|
124
|
+
file: join(topLevel, f),
|
|
125
|
+
group: '(top-level)',
|
|
126
|
+
});
|
|
101
127
|
}
|
|
102
128
|
}
|
|
103
129
|
return out;
|
|
@@ -200,9 +226,10 @@ switch (command) {
|
|
|
200
226
|
args.includes('--complete') ||
|
|
201
227
|
args.includes('--ci-safe')
|
|
202
228
|
) {
|
|
203
|
-
// --complete
|
|
204
|
-
// the repo's top-level e2e/tests/).
|
|
205
|
-
//
|
|
229
|
+
// --all / --complete / --ci-safe all run the full discovery (every module +
|
|
230
|
+
// the repo's top-level e2e/tests/). Quarantined (`cele2e-ci-unsafe`) tests
|
|
231
|
+
// are skipped by default; only --complete opts back into them (the runner
|
|
232
|
+
// sees the flag — it's kept below). --all-modules stays modules-only.
|
|
206
233
|
const wantTopLevel =
|
|
207
234
|
args.includes('--all') || args.includes('--complete') || args.includes('--ci-safe');
|
|
208
235
|
const suiteRoot = resolveSuiteRoot();
|
|
@@ -222,9 +249,9 @@ switch (command) {
|
|
|
222
249
|
}
|
|
223
250
|
runScript(
|
|
224
251
|
'e2e-run',
|
|
225
|
-
// Strip the discovery aliases; KEEP --
|
|
226
|
-
//
|
|
227
|
-
args.filter((a) => a !== '--all' && a !== '--all-modules'
|
|
252
|
+
// Strip the discovery-only aliases; KEEP --complete (the runner uses it to
|
|
253
|
+
// opt back into quarantined tests) and --ci-safe (harmless/no-op now).
|
|
254
|
+
args.filter((a) => a !== '--all' && a !== '--all-modules'),
|
|
228
255
|
env,
|
|
229
256
|
);
|
|
230
257
|
}
|
|
@@ -236,10 +263,14 @@ switch (command) {
|
|
|
236
263
|
|
|
237
264
|
if (modulePath) {
|
|
238
265
|
const absModulePath = resolve(modulePath);
|
|
239
|
-
runScript(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
266
|
+
runScript(
|
|
267
|
+
'e2e-run',
|
|
268
|
+
args.filter((a) => a !== modulePath),
|
|
269
|
+
{
|
|
270
|
+
E2E_TEST_DIR: stateDir,
|
|
271
|
+
E2E_MODULE_DIR: absModulePath,
|
|
272
|
+
},
|
|
273
|
+
);
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
// If cwd is not a module, walk sibling modules so the user can invoke
|
|
@@ -261,7 +292,9 @@ switch (command) {
|
|
|
261
292
|
for (const m of matches) {
|
|
262
293
|
console.error(` ${basename(m.moduleDir)}: ${m.matchedTests.join(', ')}`);
|
|
263
294
|
}
|
|
264
|
-
console.error(
|
|
295
|
+
console.error(
|
|
296
|
+
'\nDisambiguate by running from inside the module directory or passing its path.',
|
|
297
|
+
);
|
|
265
298
|
process.exit(1);
|
|
266
299
|
}
|
|
267
300
|
} else if (positionals.length === 0) {
|
|
@@ -364,7 +397,9 @@ switch (command) {
|
|
|
364
397
|
console.log(` Reuse: cele2e run --reuse`);
|
|
365
398
|
console.log(` Tear down: cele2e down`);
|
|
366
399
|
console.log('');
|
|
367
|
-
} catch {
|
|
400
|
+
} catch {
|
|
401
|
+
/* ignore parse errors */
|
|
402
|
+
}
|
|
368
403
|
}
|
|
369
404
|
runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
|
|
370
405
|
}
|
|
@@ -380,7 +415,10 @@ switch (command) {
|
|
|
380
415
|
// build-infra mutates the shared Docker images — serialize against runs.
|
|
381
416
|
// Released via the lock's exit handler (runBuild may process.exit on failure).
|
|
382
417
|
try {
|
|
383
|
-
acquireRunLock({
|
|
418
|
+
acquireRunLock({
|
|
419
|
+
test: 'build-infra',
|
|
420
|
+
runId: process.env.CELE2E_RUN_ID ?? crypto.randomUUID(),
|
|
421
|
+
});
|
|
384
422
|
} catch (err) {
|
|
385
423
|
if (err instanceof E2eBusyError) {
|
|
386
424
|
console.error(err.message);
|
package/src/container-manager.ts
CHANGED
|
@@ -922,7 +922,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
922
922
|
if (config.managementVariant !== 'vanilla') {
|
|
923
923
|
assertCliVersion(projectName, composeDir);
|
|
924
924
|
console.log('[progress:start] initializing celilo | celilo initialized');
|
|
925
|
-
// Honest harness (
|
|
925
|
+
// Honest harness (openspec/specs/progressive-zone-disclosure/spec.md): seed ONLY the
|
|
926
926
|
// `internal` zone — the network the management box is genuinely on —
|
|
927
927
|
// plus DNS. dmz/app/secure are NOT pre-seeded; they come into being
|
|
928
928
|
// when a test deploys the firewall (net.deployFirewall()), exactly as
|
|
@@ -935,7 +935,11 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
935
935
|
`celilo system init --accept-defaults \
|
|
936
936
|
network.internal.subnet=${ZONE_SUBNETS.internal} \
|
|
937
937
|
network.internal.gateway=${ZONE_GATEWAYS.internal} \
|
|
938
|
-
|
|
938
|
+
${
|
|
939
|
+
config.managementZone === 'secure-mgmt'
|
|
940
|
+
? 'network.secure-mgmt.subnet=10.0.120.0/24 network.secure-mgmt.gateway=10.0.120.1 '
|
|
941
|
+
: ''
|
|
942
|
+
}dns.primary=100.100.0.1 \
|
|
939
943
|
dns.fallback=1.0.0.1,8.8.8.8`,
|
|
940
944
|
);
|
|
941
945
|
if (initResult.exitCode !== 0) {
|
|
@@ -79,6 +79,8 @@ function baseService(opts: {
|
|
|
79
79
|
tmpfs?: string[];
|
|
80
80
|
privileged?: boolean;
|
|
81
81
|
dns?: string[];
|
|
82
|
+
/** Host devices to expose, e.g. `/dev/net/tun` for a userspace WireGuard tunnel. */
|
|
83
|
+
devices?: string[];
|
|
82
84
|
}) {
|
|
83
85
|
// Auto-assign an image tag from the Dockerfile name so Docker can
|
|
84
86
|
// skip builds when pre-built images exist.
|
|
@@ -109,6 +111,16 @@ export const SHARED_NETWORKS = {
|
|
|
109
111
|
* Default topology: fw-isp (greenwave) + fw-main (iptables)
|
|
110
112
|
* Two-layer NAT: internet → fw-isp → fw-main → target
|
|
111
113
|
*/
|
|
114
|
+
/**
|
|
115
|
+
* celilo's own control-plane network, used only when `managementZone` is
|
|
116
|
+
* 'secure-mgmt'. Deliberately NOT one of the segmented data-plane tiers: it is
|
|
117
|
+
* where celilo-mgr itself lives, mirroring the production topology the suite
|
|
118
|
+
* previously could not express.
|
|
119
|
+
*/
|
|
120
|
+
const SECURE_MGMT_SUBNET = '10.0.120.0/24';
|
|
121
|
+
const SECURE_MGMT_GATEWAY = '10.0.120.1';
|
|
122
|
+
const SECURE_MGMT_MANAGEMENT_IP = '10.0.120.100';
|
|
123
|
+
|
|
112
124
|
function defaultTopologyServices(): Record<string, unknown> {
|
|
113
125
|
return {
|
|
114
126
|
'fw-main': baseService({
|
|
@@ -120,6 +132,10 @@ function defaultTopologyServices(): Record<string, unknown> {
|
|
|
120
132
|
secure: { ipv4_address: '10.0.30.1' },
|
|
121
133
|
},
|
|
122
134
|
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
135
|
+
// A firewall may also terminate a VPN (the admin tunnel the `wireguard`
|
|
136
|
+
// module owns). wireguard-go needs the tun device; the kernel module is
|
|
137
|
+
// deliberately not relied on — see Dockerfile.firewall.
|
|
138
|
+
devices: ['/dev/net/tun'],
|
|
123
139
|
sysctls: ROUTER_SYSCTLS,
|
|
124
140
|
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
125
141
|
}),
|
|
@@ -130,6 +146,10 @@ function defaultTopologyServices(): Record<string, unknown> {
|
|
|
130
146
|
'isp-external': { ipv4_address: '100.100.0.100' },
|
|
131
147
|
},
|
|
132
148
|
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
149
|
+
// A firewall may also terminate a VPN (the admin tunnel the `wireguard`
|
|
150
|
+
// module owns). wireguard-go needs the tun device; the kernel module is
|
|
151
|
+
// deliberately not relied on — see Dockerfile.firewall.
|
|
152
|
+
devices: ['/dev/net/tun'],
|
|
133
153
|
sysctls: ROUTER_SYSCTLS,
|
|
134
154
|
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
135
155
|
}),
|
|
@@ -153,6 +173,10 @@ function directInternetTopologyServices(): Record<string, unknown> {
|
|
|
153
173
|
'isp-external': { ipv4_address: '100.100.0.100' },
|
|
154
174
|
},
|
|
155
175
|
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
176
|
+
// A firewall may also terminate a VPN (the admin tunnel the `wireguard`
|
|
177
|
+
// module owns). wireguard-go needs the tun device; the kernel module is
|
|
178
|
+
// deliberately not relied on — see Dockerfile.firewall.
|
|
179
|
+
devices: ['/dev/net/tun'],
|
|
156
180
|
sysctls: ROUTER_SYSCTLS,
|
|
157
181
|
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
158
182
|
}),
|
|
@@ -232,6 +256,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
232
256
|
'./config/dns/celilo.computer.zone:/seed/celilo.computer.zone:ro',
|
|
233
257
|
'./config/dns/example.net.zone:/seed/example.net.zone:ro',
|
|
234
258
|
'./config/dns/park-your-domain.com.zone:/seed/park-your-domain.com.zone:ro',
|
|
259
|
+
'./config/dns/tangohost.com.zone:/seed/tangohost.com.zone:ro',
|
|
235
260
|
],
|
|
236
261
|
});
|
|
237
262
|
|
|
@@ -292,6 +317,17 @@ export function generateSharedInfraYaml(): string {
|
|
|
292
317
|
// packages/e2e/scripts/pack-celilo-packages.ts (cli, capabilities,
|
|
293
318
|
// cli-display, e2e, event-bus). Reachable at http://npm-registry.lab from
|
|
294
319
|
// any container that uses the internal resolver.
|
|
320
|
+
// OFF-FLEET web host (external_web). A PUBLIC PEER on internet-external, not
|
|
321
|
+
// a fleet member and not behind the customer firewall: celilo reaches it by
|
|
322
|
+
// SSH egress the way it would reach any host on the real internet, and public
|
|
323
|
+
// DNS resolves tangohost.com straight to this address. It runs its own Apache
|
|
324
|
+
// and its own pre-installed TLS — celilo governs none of it.
|
|
325
|
+
services['cpanel-host'] = baseService({
|
|
326
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.cpanel-host-sim' },
|
|
327
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.CPANEL_HOST } },
|
|
328
|
+
cap_add: ['NET_ADMIN'],
|
|
329
|
+
});
|
|
330
|
+
|
|
295
331
|
services['npm-registry'] = baseService({
|
|
296
332
|
build: { context: '.', dockerfile: 'docker/Dockerfile.npm-registry-sim' },
|
|
297
333
|
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.NPM_REGISTRY } },
|
|
@@ -337,11 +373,17 @@ export function generateSharedInfraYaml(): string {
|
|
|
337
373
|
* Creates its own internal, dmz, app, secure networks.
|
|
338
374
|
*/
|
|
339
375
|
export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: string): string {
|
|
376
|
+
const mgmtOnOwnNetwork = config.managementZone === 'secure-mgmt';
|
|
340
377
|
const networks: Record<string, unknown> = {
|
|
341
378
|
internal: networkDef('192.168.0.0/24', '192.168.0.250'),
|
|
342
379
|
dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
|
|
343
380
|
app: networkDef('10.0.20.0/24', '10.0.20.250'),
|
|
344
381
|
secure: networkDef('10.0.30.0/24', '10.0.30.250'),
|
|
382
|
+
// Only when celilo-mgr lives off the internal LAN — keeps the default
|
|
383
|
+
// topology's generated compose byte-identical for every existing test.
|
|
384
|
+
...(mgmtOnOwnNetwork
|
|
385
|
+
? { 'secure-mgmt': networkDef(SECURE_MGMT_SUBNET, '10.0.120.250') }
|
|
386
|
+
: {}),
|
|
345
387
|
'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
|
|
346
388
|
// internet-external is owned by shared infra; real-internet is per-test
|
|
347
389
|
// (it just bridges fw-ext ↔ comcast-resolver, no shared service uses it).
|
|
@@ -392,11 +434,15 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
392
434
|
// first. container-manager's celilo-init + routing-verify target the
|
|
393
435
|
// `management` container by name, so `celilo-mgr-2` comes up fresh and
|
|
394
436
|
// un-init'd — the empty target restore needs.
|
|
395
|
-
const emitManagement = (
|
|
437
|
+
const emitManagement = (
|
|
438
|
+
variant: 'default' | 'vanilla' | undefined,
|
|
439
|
+
ip: string,
|
|
440
|
+
zone: 'internal' | 'secure-mgmt' = 'internal',
|
|
441
|
+
) =>
|
|
396
442
|
baseService({
|
|
397
443
|
image:
|
|
398
444
|
variant === 'vanilla' ? 'celilo-e2e/management:vanilla' : 'celilo-e2e/management:latest',
|
|
399
|
-
networks: {
|
|
445
|
+
networks: { [zone]: { ipv4_address: ip } },
|
|
400
446
|
cap_add: ['NET_ADMIN'],
|
|
401
447
|
// ISS-0157: `module import`'s `bun install` hard-links packages from the
|
|
402
448
|
// bun cache into node_modules, which on overlayfs forces a copy-up + fsync
|
|
@@ -416,12 +462,17 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
416
462
|
],
|
|
417
463
|
environment: {
|
|
418
464
|
DDNS_ENDPOINT: `http://${SIMULATOR_IPS.NAMECHEAP_DNS}:8080/update`,
|
|
419
|
-
|
|
465
|
+
// On its own control-plane network the only router in reach is fw-main's
|
|
466
|
+
// leg there — both for the default route and for the segmented zones.
|
|
467
|
+
DEFAULT_GATEWAY: zone === 'secure-mgmt' ? SECURE_MGMT_GATEWAY : managementDefaultGw,
|
|
468
|
+
...(zone === 'secure-mgmt' ? { FW_MAIN_HOP: SECURE_MGMT_GATEWAY } : {}),
|
|
420
469
|
CELILO_REGISTRY_URL: 'http://e2e-registry.lab',
|
|
421
470
|
},
|
|
422
471
|
});
|
|
423
472
|
|
|
424
|
-
services.management =
|
|
473
|
+
services.management = mgmtOnOwnNetwork
|
|
474
|
+
? emitManagement(config.managementVariant, SECURE_MGMT_MANAGEMENT_IP, 'secure-mgmt')
|
|
475
|
+
: emitManagement(config.managementVariant, '192.168.0.100');
|
|
425
476
|
|
|
426
477
|
if (config.secondaryManagementVariant) {
|
|
427
478
|
services['celilo-mgr-2'] = emitManagement(config.secondaryManagementVariant, '192.168.0.101');
|
|
@@ -451,6 +502,17 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
451
502
|
const topologyFn = TOPOLOGY_SERVICES[config.topology] || TOPOLOGY_SERVICES.default;
|
|
452
503
|
Object.assign(services, topologyFn());
|
|
453
504
|
|
|
505
|
+
// Give fw-main a leg on the control-plane network when celilo-mgr lives there,
|
|
506
|
+
// so the management box is routed like any other segmented network rather than
|
|
507
|
+
// being stranded. Applied as a post-step so the default topology's generated
|
|
508
|
+
// compose is untouched.
|
|
509
|
+
if (mgmtOnOwnNetwork) {
|
|
510
|
+
const fwMain = services['fw-main'] as { networks?: Record<string, unknown> } | undefined;
|
|
511
|
+
if (fwMain?.networks) {
|
|
512
|
+
fwMain.networks['secure-mgmt'] = { ipv4_address: SECURE_MGMT_GATEWAY };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
454
516
|
// --- Routing infrastructure (bridges per-test to shared infra) ---
|
|
455
517
|
services['fw-ext'] = baseService({
|
|
456
518
|
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
@@ -518,6 +580,44 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
518
580
|
});
|
|
519
581
|
}
|
|
520
582
|
|
|
583
|
+
// --- Optional real signal-cli daemon ---
|
|
584
|
+
// On `internal`, matching the signal module's declared zone — it is a local
|
|
585
|
+
// service celilo talks to, not an internet-facing one, so it does not belong
|
|
586
|
+
// on internet-external. Unlinked: the JSON-RPC surface needs no account.
|
|
587
|
+
if (config.signalCli) {
|
|
588
|
+
services['signal-cli'] = baseService({
|
|
589
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.signal-cli' },
|
|
590
|
+
networks: { internal: { ipv4_address: '192.168.0.90' } },
|
|
591
|
+
cap_add: ['NET_ADMIN'],
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// --- Optional signal-cli release host ---
|
|
596
|
+
// On internet-external because that is what it models: an artifact the
|
|
597
|
+
// module downloads FROM the internet at deploy time.
|
|
598
|
+
if (config.signalRelease) {
|
|
599
|
+
services['signal-release'] = baseService({
|
|
600
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.signal-release' },
|
|
601
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.SIGNAL_RELEASE } },
|
|
602
|
+
cap_add: ['NET_ADMIN'],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// --- Optional signal-cli simulator ---
|
|
607
|
+
// Same network placement as the real daemon so a test can swap one for the
|
|
608
|
+
// other without touching the module's configured endpoint.
|
|
609
|
+
if (config.signalSim) {
|
|
610
|
+
services['signal-sim'] = baseService({
|
|
611
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.signal-sim' },
|
|
612
|
+
networks: { internal: { ipv4_address: '192.168.0.91' } },
|
|
613
|
+
cap_add: ['NET_ADMIN'],
|
|
614
|
+
environment: {
|
|
615
|
+
SIGNAL_ACCOUNT: '+15551234567',
|
|
616
|
+
SIGNAL_KNOWN_RECIPIENTS: '+15550001,+15550002',
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
521
621
|
// --- Observer vantages (passive spies; ISS-0117) ---
|
|
522
622
|
// Each is the SAME image at a different network location with a routing profile that
|
|
523
623
|
// mirrors a real device there — that routing profile is what makes it a faithful seat
|
|
@@ -528,6 +628,9 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
528
628
|
build: { context: '.', dockerfile: 'docker/Dockerfile.observer' },
|
|
529
629
|
networks: { [p.network]: { ipv4_address: p.ip } },
|
|
530
630
|
cap_add: ['NET_ADMIN'],
|
|
631
|
+
// A vantage may dial a VPN and probe from INSIDE the tunnel — the only way
|
|
632
|
+
// to assert VPN reach by real signal instead of by reading a rule string.
|
|
633
|
+
devices: ['/dev/net/tun'],
|
|
531
634
|
environment: observerEnv(p),
|
|
532
635
|
});
|
|
533
636
|
}
|
|
@@ -660,6 +763,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
660
763
|
'./config/dns/celilo.computer.zone:/seed/celilo.computer.zone:ro',
|
|
661
764
|
'./config/dns/example.net.zone:/seed/example.net.zone:ro',
|
|
662
765
|
'./config/dns/park-your-domain.com.zone:/seed/park-your-domain.com.zone:ro',
|
|
766
|
+
'./config/dns/tangohost.com.zone:/seed/tangohost.com.zone:ro',
|
|
663
767
|
],
|
|
664
768
|
});
|
|
665
769
|
|
package/src/network-builder.ts
CHANGED
|
@@ -39,6 +39,9 @@ export class NetworkBuilder {
|
|
|
39
39
|
secureMachines: [],
|
|
40
40
|
internalMachines: [],
|
|
41
41
|
dhcpClient: false,
|
|
42
|
+
signalCli: false,
|
|
43
|
+
signalSim: false,
|
|
44
|
+
signalRelease: false,
|
|
42
45
|
observers: [],
|
|
43
46
|
domain: 'iamtheinternet.org',
|
|
44
47
|
ddnsPassword: 'test123',
|
|
@@ -51,6 +54,17 @@ export class NetworkBuilder {
|
|
|
51
54
|
return this;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Place the celilo management container in a given zone. Defaults to
|
|
59
|
+
* `internal`; `secure-mgmt` gives celilo-mgr its own control-plane network
|
|
60
|
+
* (the production topology), so a test can exercise celilo reaching and
|
|
61
|
+
* resolving for a fleet from OFF the internal LAN.
|
|
62
|
+
*/
|
|
63
|
+
managementZone(zone: 'internal' | 'secure-mgmt'): this {
|
|
64
|
+
this.config.managementZone = zone;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
/** Append `{name, ip, zone, ...extra}` for each entry to a zone's machine list. */
|
|
55
69
|
private addMachines(
|
|
56
70
|
target: MachineSpec[],
|
|
@@ -97,6 +111,44 @@ export class NetworkBuilder {
|
|
|
97
111
|
return this;
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Add a real, UNLINKED signal-cli daemon on the internal network.
|
|
116
|
+
*
|
|
117
|
+
* For verifying the JSON-RPC contract against the actual binary. It cannot
|
|
118
|
+
* send to a phone (no account, and CDSI/SVR2 attestation makes a local
|
|
119
|
+
* Signal substitute impossible), but every endpoint, envelope and error
|
|
120
|
+
* shape celilo's client relies on is checkable without one.
|
|
121
|
+
*/
|
|
122
|
+
withSignalCli(): this {
|
|
123
|
+
this.config.signalCli = true;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Add the signal-cli SIMULATOR on the internal network.
|
|
129
|
+
*
|
|
130
|
+
* Use this for anything that drives the delivery or ack loop: unlike the
|
|
131
|
+
* real daemon it exposes a control surface (`/_control/inbound`,
|
|
132
|
+
* `/_control/sent`, `/_control/unlink`) so a test can make a human "reply"
|
|
133
|
+
* and then assert on what celilo did about it.
|
|
134
|
+
*/
|
|
135
|
+
withSignalSim(): this {
|
|
136
|
+
this.config.signalSim = true;
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Serve the signal-cli release tarball from the simulated internet.
|
|
142
|
+
*
|
|
143
|
+
* Needed to DEPLOY the signal module: its ansible role downloads signal-cli
|
|
144
|
+
* at deploy time, and the e2e network is sealed. Point the module at it with
|
|
145
|
+
* `module config set signal release_base_url http://signal-release.lab`.
|
|
146
|
+
*/
|
|
147
|
+
withSignalRelease(): this {
|
|
148
|
+
this.config.signalRelease = true;
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
100
152
|
/**
|
|
101
153
|
* Inject one or more observer vantages (ISS-0117) — passive spies carrying the probe
|
|
102
154
|
* toolbox, each placed at its vantage's network location with a routing profile that
|