@celilo/e2e 0.10.4 → 0.11.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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The recurrence gate for #539.
3
+ *
4
+ * The sim used to number its zones exactly like a real celilo fleet, so a
5
+ * leaked stack on celilo's own forgejo-builder (which lives at 10.0.10.12 in
6
+ * the REAL dmz) put a second interface on the builder's own /24 and blackholed
7
+ * every containerized CI job's route to the forge. These tests make the two
8
+ * properties that prevent it non-negotiable:
9
+ *
10
+ * 1. every simulated private zone lives inside SIM_PRIVATE_SUPERNET, and
11
+ * 2. the retired fleet-colliding prefixes appear NOWHERE in the suite.
12
+ *
13
+ * (2) is the one that matters in practice: the plan itself is one table, but
14
+ * shell scripts, Knot zones and dnsmasq configs can't import TypeScript and so
15
+ * hard-code the values. This is what catches "I changed it in one place".
16
+ */
17
+
18
+ import { describe, expect, test } from 'bun:test';
19
+ import { execSync } from 'node:child_process';
20
+ import { SIM_PRIVATE_SUPERNET, ZONE_GATEWAYS, ZONE_SUBNETS, type Zone } from './types';
21
+ import { inCidr } from './zone-classifier';
22
+
23
+ const REPO_ROOT = execSync('git rev-parse --show-toplevel', {
24
+ cwd: import.meta.dir,
25
+ encoding: 'utf-8',
26
+ }).trim();
27
+
28
+ /**
29
+ * Prefixes the sim must never use again: the fleet's zone /24s and the
30
+ * operator's LAN. Prefix match — `10.0.10.` catches an address, a CIDR and a
31
+ * prose mention alike, which is what we want.
32
+ */
33
+ const RETIRED_PREFIXES = ['10.0.10.', '10.0.20.', '10.0.30.', '10.0.120.', '192.168.'];
34
+
35
+ /**
36
+ * Match the plain form AND the regex-escaped form (`192\.168\.0\.10`). Two
37
+ * escaped literals inside `toThrow(/…/)` survived the #539 sweep precisely
38
+ * because a naive search for the plain text does not see them.
39
+ */
40
+ function toPattern(prefix: string): string {
41
+ return prefix.replace(/\./g, '\\\\?\\.');
42
+ }
43
+
44
+ /** Everything that can put an address on a simulated network or assert one. */
45
+ const SCANNED_PATHS = ['packages/e2e', 'e2e/tests', 'modules/*/e2e'];
46
+
47
+ /**
48
+ * CHANGELOG is history — it describes what the addresses WERE. This test file
49
+ * necessarily names the retired prefixes to ban them.
50
+ */
51
+ const EXCLUDED = [/^packages\/e2e\/CHANGELOG\.md$/, /address-plan\.test\.ts$/];
52
+
53
+ function grepRetired(): string[] {
54
+ const pattern = RETIRED_PREFIXES.map(toPattern).join('|');
55
+ let out = '';
56
+ try {
57
+ out = execSync(`git grep -nE '${pattern}' -- ${SCANNED_PATHS.join(' ')}`, {
58
+ cwd: REPO_ROOT,
59
+ encoding: 'utf-8',
60
+ maxBuffer: 8 * 1024 * 1024,
61
+ });
62
+ } catch {
63
+ // git grep exits 1 when there are no matches — that is the passing case.
64
+ return [];
65
+ }
66
+ return out
67
+ .split('\n')
68
+ .filter(Boolean)
69
+ .filter((line) => !EXCLUDED.some((re) => re.test(line.split(':')[0])));
70
+ }
71
+
72
+ describe('simulated address plan (#539)', () => {
73
+ test('every zone lives inside the sim supernet', () => {
74
+ for (const zone of Object.keys(ZONE_SUBNETS) as Zone[]) {
75
+ const base = ZONE_SUBNETS[zone].split('/')[0];
76
+ expect(inCidr(base, SIM_PRIVATE_SUPERNET)).toBe(true);
77
+ expect(inCidr(ZONE_GATEWAYS[zone], SIM_PRIVATE_SUPERNET)).toBe(true);
78
+ expect(inCidr(ZONE_GATEWAYS[zone], ZONE_SUBNETS[zone])).toBe(true);
79
+ }
80
+ });
81
+
82
+ test('zones do not overlap each other', () => {
83
+ const subnets = Object.values(ZONE_SUBNETS);
84
+ expect(new Set(subnets).size).toBe(subnets.length);
85
+ });
86
+
87
+ test('the fleet-colliding prefixes appear nowhere in the suite', () => {
88
+ const hits = grepRetired();
89
+ expect(
90
+ hits,
91
+ `The e2e suite must not use a real fleet's addresses (#539). Offending lines:\n${hits.join('\n')}\n\n` +
92
+ `Use zoneIp()/ZONE_SUBNETS from packages/e2e/src/types.ts, or an address inside ${SIM_PRIVATE_SUPERNET}.`,
93
+ ).toEqual([]);
94
+ });
95
+ });
@@ -60,8 +60,8 @@ describe('${name}', () => {
60
60
  }
61
61
 
62
62
  net = await network({ projectName: PROJECT_NAME })
63
- .dmz({ 'machine-1': '10.0.10.10' })
64
- // .app({ 'app-1': '10.0.20.10' })
63
+ .dmz({ 'machine-1': '10.226.10.10' })
64
+ // .app({ 'app-1': '10.226.20.10' })
65
65
  .start();
66
66
  }, 120_000);
67
67
 
@@ -186,18 +186,20 @@ process.on('SIGINT', () => {
186
186
  // FAILURE path, where the test never reaches `handle.stop()`, teardown was
187
187
  // never running and the per-test networks leaked.
188
188
  //
189
- // That leak is not confined to the run that caused it. The sim's networks use
190
- // the same subnets as a real celilo fleet (dmz 10.0.10.0/24, app 10.0.20.0/24,
191
- // …), so on a host that lives in those subnets — celilo's own forgejo-builder
192
- // does — a leaked bridge leaves a DUPLICATE route for a production prefix.
193
- // Traffic from any later container to the real host at that address is routed
189
+ // That leak used not to be confined to the run that caused it. The sim's
190
+ // networks were numbered exactly like a real celilo fleet's zones, so on a host
191
+ // that lives in those subnets — celilo's own forgejo-builder does — a leaked
192
+ // bridge left a DUPLICATE route for a production prefix, and
193
+ // traffic from any later container to the real host at that address was routed
194
194
  // into the dead simulated network and blackholed. That took out three
195
195
  // consecutive release runs, each dying in `git fetch` after a ~132s connect
196
196
  // timeout, long after the e2e run that stranded the network had ended.
197
197
  //
198
- // The start-of-run prefix sweep stays as the crash-recovery backstop (it is
199
- // the only thing that can clean up after SIGKILL); this makes the common case
200
- // clean up after itself instead of poisoning the next job. See #508.
198
+ // The sim now lives in SIM_PRIVATE_SUPERNET (#539), so a leak can no longer
199
+ // collide with a fleet address — but a leak is still a leak. The start-of-run
200
+ // prefix sweep stays as the crash-recovery backstop (it is the only thing that
201
+ // can clean up after SIGKILL); this makes the common case clean up after itself
202
+ // instead of leaving containers and volumes behind. See #508.
201
203
  process.on('exit', () => {
202
204
  cleanupOnExit();
203
205
  });
@@ -961,6 +963,21 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
961
963
  // in ~2-5s).
962
964
  60_000,
963
965
  `${machine.name} target-setup`,
966
+ // Self-diagnosing via waitFor's onTimeout (e2e-confidence #255). This
967
+ // timeout has twice been read as a simulator or deployment bug when the
968
+ // real cause was inside the unit — target-setup fetching from a slow
969
+ // third party (#560). The unit's own journal says which line it is on.
970
+ () => {
971
+ const dump = (label: string, cmd: string): string => {
972
+ const r = dockerExec(projectName, composeDir, machine.name, cmd);
973
+ return `--- ${label} ---\n${(r.stdout || r.stderr || '(no output)').trim()}`;
974
+ };
975
+ return [
976
+ `=== target-setup diagnostics (${machine.name}) ===`,
977
+ dump('systemctl status', 'systemctl status target-setup --no-pager 2>&1 | head -20'),
978
+ dump('journal', 'journalctl -u target-setup --no-pager 2>&1 | tail -30'),
979
+ ].join('\n');
980
+ },
964
981
  );
965
982
  }
966
983
 
@@ -3,8 +3,14 @@ import { join } from 'node:path';
3
3
  import { stringify } from 'yaml';
4
4
  import { normalizeObservers, observerEnv, observerPlacement } from './observer';
5
5
  import { SIMULATOR_IPS } from './simulator-ips';
6
- import type { MachineSpec, NetworkConfig, TopologyPreset } from './types';
7
- import { ZONE_GATEWAYS } from './types';
6
+ import type { MachineSpec, NetworkConfig, TopologyPreset, Zone } from './types';
7
+ import {
8
+ ZONE_GATEWAYS,
9
+ ZONE_SUBNETS,
10
+ greenwaveRouterIp,
11
+ internalResolverIp,
12
+ zoneIp,
13
+ } from './types';
8
14
 
9
15
  /**
10
16
  * Volumes for the registry container. Picks between source-bootstrap and
@@ -52,6 +58,15 @@ function networkDef(subnet: string, gateway: string) {
52
58
  };
53
59
  }
54
60
 
61
+ /**
62
+ * A zone's bridge, derived entirely from the address plan in `types.ts`. The
63
+ * bridge itself takes `.250` (Docker's own gateway address on the network — NOT
64
+ * the routing gateway, which is the firewall at `ZONE_GATEWAYS[zone]`).
65
+ */
66
+ function zoneNetworkDef(zone: Zone) {
67
+ return networkDef(ZONE_SUBNETS[zone], zoneIp(zone, 250));
68
+ }
69
+
55
70
  /**
56
71
  * Image tag prefix for pre-built e2e images. When images with these
57
72
  * tags exist locally, `docker compose build` skips rebuilding them.
@@ -117,19 +132,29 @@ export const SHARED_NETWORKS = {
117
132
  * where celilo-mgr itself lives, mirroring the production topology the suite
118
133
  * previously could not express.
119
134
  */
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';
135
+ const SECURE_MGMT_GATEWAY = ZONE_GATEWAYS['secure-mgmt'];
136
+ const SECURE_MGMT_MANAGEMENT_IP = zoneIp('secure-mgmt', 100);
137
+
138
+ /**
139
+ * The zones fw-main routes between WITHOUT NAT, so the dmz-resident resolver
140
+ * sees each client's real zone (source-based split-horizon, ISS-0156).
141
+ * `internal` is deliberately absent: a LAN device's query must arrive SNAT'd to
142
+ * a zone-gateway address so it matches the passthrough view and gets the natIp
143
+ * answer, not the protected one.
144
+ */
145
+ const PROTECTED_SUBNETS = (['dmz', 'app', 'secure', 'secure-mgmt'] as const)
146
+ .map((zone) => ZONE_SUBNETS[zone])
147
+ .join(' ');
123
148
 
124
149
  function defaultTopologyServices(): Record<string, unknown> {
125
150
  return {
126
151
  'fw-main': baseService({
127
152
  build: { context: '.', dockerfile: 'docker/Dockerfile.firewall' },
128
153
  networks: {
129
- internal: { ipv4_address: '192.168.0.254' },
130
- dmz: { ipv4_address: '10.0.10.1' },
131
- app: { ipv4_address: '10.0.20.1' },
132
- secure: { ipv4_address: '10.0.30.1' },
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 },
133
158
  },
134
159
  cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
135
160
  // A firewall may also terminate a VPN (the admin tunnel the `wireguard`
@@ -138,11 +163,12 @@ function defaultTopologyServices(): Record<string, unknown> {
138
163
  devices: ['/dev/net/tun'],
139
164
  sysctls: ROUTER_SYSCTLS,
140
165
  volumes: ['ssh-keys:/ssh-keys:ro'],
166
+ environment: { PROTECTED_SUBNETS },
141
167
  }),
142
168
  'fw-isp': baseService({
143
169
  build: { context: '.', dockerfile: 'docker/Dockerfile.greenwave-sim' },
144
170
  networks: {
145
- internal: { ipv4_address: '192.168.0.1' },
171
+ internal: { ipv4_address: greenwaveRouterIp() },
146
172
  'isp-external': { ipv4_address: '100.100.0.100' },
147
173
  },
148
174
  cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
@@ -166,10 +192,10 @@ function directInternetTopologyServices(): Record<string, unknown> {
166
192
  'fw-main': baseService({
167
193
  build: { context: '.', dockerfile: 'docker/Dockerfile.firewall' },
168
194
  networks: {
169
- internal: { ipv4_address: '192.168.0.254' },
170
- dmz: { ipv4_address: '10.0.10.1' },
171
- app: { ipv4_address: '10.0.20.1' },
172
- secure: { ipv4_address: '10.0.30.1' },
195
+ internal: { ipv4_address: ZONE_GATEWAYS.internal },
196
+ dmz: { ipv4_address: ZONE_GATEWAYS.dmz },
197
+ app: { ipv4_address: ZONE_GATEWAYS.app },
198
+ secure: { ipv4_address: ZONE_GATEWAYS.secure },
173
199
  'isp-external': { ipv4_address: '100.100.0.100' },
174
200
  },
175
201
  cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
@@ -179,6 +205,7 @@ function directInternetTopologyServices(): Record<string, unknown> {
179
205
  devices: ['/dev/net/tun'],
180
206
  sysctls: ROUTER_SYSCTLS,
181
207
  volumes: ['ssh-keys:/ssh-keys:ro'],
208
+ environment: { PROTECTED_SUBNETS },
182
209
  }),
183
210
  // No fw-isp — fw-main handles external routing directly
184
211
  };
@@ -379,15 +406,13 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
379
406
  // land whether or not the management box shares the network (#436).
380
407
  const needsSecureMgmt = mgmtOnOwnNetwork || (config.secureMgmtMachines ?? []).length > 0;
381
408
  const networks: Record<string, unknown> = {
382
- internal: networkDef('192.168.0.0/24', '192.168.0.250'),
383
- dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
384
- app: networkDef('10.0.20.0/24', '10.0.20.250'),
385
- secure: networkDef('10.0.30.0/24', '10.0.30.250'),
409
+ internal: zoneNetworkDef('internal'),
410
+ dmz: zoneNetworkDef('dmz'),
411
+ app: zoneNetworkDef('app'),
412
+ secure: zoneNetworkDef('secure'),
386
413
  // Only when celilo-mgr lives off the internal LAN — keeps the default
387
414
  // topology's generated compose byte-identical for every existing test.
388
- ...(needsSecureMgmt
389
- ? { 'secure-mgmt': networkDef(SECURE_MGMT_SUBNET, '10.0.120.250') }
390
- : {}),
415
+ ...(needsSecureMgmt ? { 'secure-mgmt': zoneNetworkDef('secure-mgmt') } : {}),
391
416
  'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
392
417
  // internet-external is owned by shared infra; real-internet is per-test
393
418
  // (it just bridges fw-ext ↔ comcast-resolver, no shared service uses it).
@@ -411,7 +436,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
411
436
 
412
437
  // --- Management (always present) ---
413
438
  const managementDefaultGw =
414
- config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
439
+ config.topology === 'direct-internet' ? ZONE_GATEWAYS.internal : greenwaveRouterIp();
415
440
 
416
441
  // Pick the management image based on variant. Both branches use
417
442
  // `image:` (not `build:`) so docker compose uses the existing tag —
@@ -476,24 +501,27 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
476
501
 
477
502
  services.management = mgmtOnOwnNetwork
478
503
  ? emitManagement(config.managementVariant, SECURE_MGMT_MANAGEMENT_IP, 'secure-mgmt')
479
- : emitManagement(config.managementVariant, '192.168.0.100');
504
+ : emitManagement(config.managementVariant, zoneIp('internal', 100));
480
505
 
481
506
  if (config.secondaryManagementVariant) {
482
- services['celilo-mgr-2'] = emitManagement(config.secondaryManagementVariant, '192.168.0.101');
507
+ services['celilo-mgr-2'] = emitManagement(
508
+ config.secondaryManagementVariant,
509
+ zoneIp('internal', 101),
510
+ );
483
511
  }
484
512
 
485
513
  // --- Internal split-horizon DNS resolver ---
486
514
  // Resolves the test domain to caddy's internal DMZ IP so management can
487
515
  // reach it via a direct route rather than through the external interface
488
516
  // (hairpin NAT would be needed otherwise). Management already has
489
- // nameserver 192.168.0.10 as its primary resolver in /etc/resolv.conf.
517
+ // `internalResolverIp()` as its primary resolver in /etc/resolv.conf.
490
518
  const caddyIp =
491
519
  config.dmzMachines.find((m) => m.name === 'caddy')?.ip ??
492
520
  config.dmzMachines[0]?.ip ??
493
- '10.0.10.10';
521
+ zoneIp('dmz', 10);
494
522
  services['dns-int'] = baseService({
495
523
  build: { context: '.', dockerfile: 'docker/Dockerfile.internal-resolver' },
496
- networks: { internal: { ipv4_address: '192.168.0.10' } },
524
+ networks: { internal: { ipv4_address: internalResolverIp() } },
497
525
  cap_add: ['NET_ADMIN'],
498
526
  environment: {
499
527
  CADDY_IP: caddyIp,
@@ -574,7 +602,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
574
602
  if (config.dhcpClient) {
575
603
  services['dhcp-client'] = baseService({
576
604
  build: { context: '.', dockerfile: 'docker/Dockerfile.dhcp-client' },
577
- networks: { internal: { ipv4_address: '192.168.0.210' } },
605
+ networks: { internal: { ipv4_address: zoneIp('internal', 210) } },
578
606
  cap_add: ['NET_ADMIN'],
579
607
  });
580
608
  }
@@ -586,7 +614,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
586
614
  if (config.signalCli) {
587
615
  services['signal-cli'] = baseService({
588
616
  build: { context: '.', dockerfile: 'docker/Dockerfile.signal-cli' },
589
- networks: { internal: { ipv4_address: '192.168.0.90' } },
617
+ networks: { internal: { ipv4_address: zoneIp('internal', 90) } },
590
618
  cap_add: ['NET_ADMIN'],
591
619
  });
592
620
  }
@@ -608,7 +636,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
608
636
  if (config.signalSim) {
609
637
  services['signal-sim'] = baseService({
610
638
  build: { context: '.', dockerfile: 'docker/Dockerfile.signal-sim' },
611
- networks: { internal: { ipv4_address: '192.168.0.91' } },
639
+ networks: { internal: { ipv4_address: zoneIp('internal', 91) } },
612
640
  cap_add: ['NET_ADMIN'],
613
641
  environment: {
614
642
  SIGNAL_ACCOUNT: '+15551234567',
@@ -649,10 +677,10 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
649
677
 
650
678
  export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): string {
651
679
  const networks: Record<string, unknown> = {
652
- internal: networkDef('192.168.0.0/24', '192.168.0.250'),
653
- dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
654
- app: networkDef('10.0.20.0/24', '10.0.20.250'),
655
- secure: networkDef('10.0.30.0/24', '10.0.30.250'),
680
+ internal: zoneNetworkDef('internal'),
681
+ dmz: zoneNetworkDef('dmz'),
682
+ app: zoneNetworkDef('app'),
683
+ secure: zoneNetworkDef('secure'),
656
684
  'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
657
685
  'internet-external': networkDef('100.64.0.0/24', '100.64.0.250'),
658
686
  'real-internet': {
@@ -668,11 +696,11 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
668
696
  const services: Record<string, unknown> = {};
669
697
 
670
698
  const managementDefaultGw =
671
- config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
699
+ config.topology === 'direct-internet' ? ZONE_GATEWAYS.internal : greenwaveRouterIp();
672
700
 
673
701
  services.management = baseService({
674
702
  build: { context: '.', dockerfile: 'docker/Dockerfile.management' },
675
- networks: { internal: { ipv4_address: '192.168.0.100' } },
703
+ networks: { internal: { ipv4_address: zoneIp('internal', 100) } },
676
704
  cap_add: ['NET_ADMIN'],
677
705
  volumes: [
678
706
  ...(celiloRoot ? [`${celiloRoot}:/celilo`] : []),
@@ -692,10 +720,10 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
692
720
  const legacyCaddyIp =
693
721
  config.dmzMachines.find((m) => m.name === 'caddy')?.ip ??
694
722
  config.dmzMachines[0]?.ip ??
695
- '10.0.10.10';
723
+ zoneIp('dmz', 10);
696
724
  services['dns-int'] = baseService({
697
725
  build: { context: '.', dockerfile: 'docker/Dockerfile.internal-resolver' },
698
- networks: { internal: { ipv4_address: '192.168.0.10' } },
726
+ networks: { internal: { ipv4_address: internalResolverIp() } },
699
727
  cap_add: ['NET_ADMIN'],
700
728
  environment: {
701
729
  CADDY_IP: legacyCaddyIp,
@@ -831,7 +859,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
831
859
  if (config.dhcpClient) {
832
860
  services['dhcp-client'] = baseService({
833
861
  build: { context: '.', dockerfile: 'docker/Dockerfile.dhcp-client' },
834
- networks: { internal: { ipv4_address: '192.168.0.210' } },
862
+ networks: { internal: { ipv4_address: zoneIp('internal', 210) } },
835
863
  cap_add: ['NET_ADMIN'],
836
864
  });
837
865
  }
package/src/fixtures.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { NetworkBuilder, network } from './network-builder';
2
2
 
3
3
  /** Single caddy machine in the DMZ — basic web server deployment */
4
- export const CADDY_DEPLOYMENT = (): NetworkBuilder => network().dmz({ caddy: '10.0.10.10' });
4
+ export const CADDY_DEPLOYMENT = (): NetworkBuilder => network().dmz({ caddy: '10.226.10.10' });
5
5
 
6
6
  /** Full stack: web server, identity provider, database */
7
7
  export const FULL_STACK = (): NetworkBuilder =>
8
8
  network()
9
- .dmz({ caddy: '10.0.10.10' })
10
- .app({ idp: '10.0.20.100' })
11
- .secure({ db: '10.0.30.50' });
9
+ .dmz({ caddy: '10.226.10.10' })
10
+ .app({ idp: '10.226.20.100' })
11
+ .secure({ db: '10.226.30.50' });
12
12
 
13
13
  /** Minimal: no dynamic machines, just fixed infrastructure */
14
14
  export const INFRASTRUCTURE_ONLY = (): NetworkBuilder => network();
@@ -16,25 +16,25 @@ export const INFRASTRUCTURE_ONLY = (): NetworkBuilder => network();
16
16
  /** Caddy + Authentik: web server in DMZ, identity provider in app zone */
17
17
  export const AUTHENTIK_DEPLOYMENT = (): NetworkBuilder =>
18
18
  network()
19
- .dmz({ caddy: '10.0.10.10' })
20
- .app({ authentik: '10.0.20.30' });
19
+ .dmz({ caddy: '10.226.10.10' })
20
+ .app({ authentik: '10.226.20.30' });
21
21
 
22
22
  /** Multiple DMZ machines for testing multi-service deployments */
23
23
  export const MULTI_DMZ = (): NetworkBuilder =>
24
24
  network().dmz({
25
- caddy: '10.0.10.10',
26
- 'web-app': '10.0.10.20',
25
+ caddy: '10.226.10.10',
26
+ 'web-app': '10.226.10.20',
27
27
  });
28
28
 
29
29
  /** Technitium DNS + Caddy: internal DNS resolver + full web stack */
30
30
  export const TECHNITIUM_STACK = (): NetworkBuilder =>
31
31
  network()
32
- .internal({ 'dns-int': '192.168.0.53' })
33
- .dmz({ caddy: '10.0.10.10' });
32
+ .internal({ 'dns-int': '10.226.1.53' })
33
+ .dmz({ caddy: '10.226.10.10' });
34
34
 
35
35
  /** Technitium DNS with DHCP client: tests DHCP DNS propagation */
36
36
  export const TECHNITIUM_DHCP_STACK = (): NetworkBuilder =>
37
37
  network()
38
- .internal({ 'dns-int': '192.168.0.53' })
39
- .dmz({ caddy: '10.0.10.10' })
38
+ .internal({ 'dns-int': '10.226.1.53' })
39
+ .dmz({ caddy: '10.226.10.10' })
40
40
  .withDhcpClient();
package/src/index.ts CHANGED
@@ -12,8 +12,8 @@
12
12
  * import { network } from '@celilo/e2e';
13
13
  *
14
14
  * const net = await network()
15
- * .dmz({ caddy: '10.0.10.10' })
16
- * .app({ myapp: '10.0.20.10' })
15
+ * .dmz({ caddy: '10.226.10.10' })
16
+ * .app({ myapp: '10.226.20.10' })
17
17
  * .start();
18
18
  *
19
19
  * await net.celilo('module deploy myapp');
@@ -170,7 +170,7 @@ export class NetworkBuilder {
170
170
  * `VantageProbe`. `management` is not injectable (that vantage reuses the real
171
171
  * management container).
172
172
  *
173
- * network().dmz({ caddy: '10.0.10.10' }).observe('internalDevice', 'publicInternet')
173
+ * network().dmz({ caddy: '10.226.10.10' }).observe('internalDevice', 'publicInternet')
174
174
  */
175
175
  observe(...vantages: Vantage[]): this {
176
176
  for (const vantage of vantages) {
@@ -219,7 +219,7 @@ export class NetworkBuilder {
219
219
  }
220
220
 
221
221
  /**
222
- * Bring up a SECOND management-class box named `celilo-mgr-2` (192.168.0.101)
222
+ * Bring up a SECOND management-class box named `celilo-mgr-2` (10.226.1.101)
223
223
  * using the given image variant. The migration restore e2e uses this to get a
224
224
  * fresh target box alongside the primary backup source. The second box is not
225
225
  * auto-init'd (container-manager keys off the `management` name), so it stays
@@ -67,36 +67,36 @@ describe('normalizeObservers + vantageContainer', () => {
67
67
  describe('observer transport — parsing + container targeting', () => {
68
68
  test('resolve(system) parses getent first column and targets the right observer', async () => {
69
69
  const { handle, calls } = fakeHandle((_c, cmd) =>
70
- cmd.startsWith('getent') ? { stdout: '192.168.0.253 www.iamtheinternet.org' } : {},
70
+ cmd.startsWith('getent') ? { stdout: '10.226.1.253 www.iamtheinternet.org' } : {},
71
71
  );
72
72
  const t = createObserverTransport(handle);
73
73
  const r = await t.resolve({ from: 'internalDevice', name: 'www.iamtheinternet.org', method: 'system' });
74
- expect(r.ips).toEqual(['192.168.0.253']);
74
+ expect(r.ips).toEqual(['10.226.1.253']);
75
75
  expect(calls[0].container).toBe('observer-internal');
76
76
  });
77
77
 
78
78
  test('resolve(dns) with explicit server', async () => {
79
- const { handle, calls } = fakeHandle((_c, cmd) => (cmd.includes('dig') ? { stdout: '10.0.10.10\n' } : {}));
79
+ const { handle, calls } = fakeHandle((_c, cmd) => (cmd.includes('dig') ? { stdout: '10.226.10.10\n' } : {}));
80
80
  const t = createObserverTransport(handle);
81
- const r = await t.resolve({ from: 'dmzSystem', name: 'auth.x', method: 'dns', server: '192.168.0.10' });
82
- expect(r.ips).toEqual(['10.0.10.10']);
83
- expect(calls[0].cmd).toContain('@192.168.0.10');
81
+ const r = await t.resolve({ from: 'dmzSystem', name: 'auth.x', method: 'dns', server: '10.226.1.10' });
82
+ expect(r.ips).toEqual(['10.226.10.10']);
83
+ expect(calls[0].cmd).toContain('@10.226.1.10');
84
84
  });
85
85
 
86
86
  test('resolve(hostfiles) parses /etc/hosts first column', async () => {
87
87
  const { handle } = fakeHandle((_c, cmd) =>
88
- cmd.startsWith('grep -F') ? { stdout: '10.0.10.10 auth.iamtheinternet.org' } : {},
88
+ cmd.startsWith('grep -F') ? { stdout: '10.226.10.10 auth.iamtheinternet.org' } : {},
89
89
  );
90
90
  const t = createObserverTransport(handle);
91
91
  const r = await t.resolve({ from: 'dmzSystem', name: 'auth.iamtheinternet.org', method: 'hostfiles' });
92
- expect(r.ips).toEqual(['10.0.10.10']);
92
+ expect(r.ips).toEqual(['10.226.10.10']);
93
93
  });
94
94
 
95
95
  test('tcpConnect maps the container exit code', async () => {
96
96
  const { handle } = fakeHandle((_c, cmd) => ({ exitCode: cmd.includes('443') ? 0 : 1 }));
97
97
  const t = createObserverTransport(handle);
98
- expect((await t.tcpConnect({ from: 'internalDevice', host: '192.168.0.253', port: 443 })).connected).toBe(true);
99
- expect((await t.tcpConnect({ from: 'internalDevice', host: '10.0.10.10', port: 22 })).connected).toBe(false);
98
+ expect((await t.tcpConnect({ from: 'internalDevice', host: '10.226.1.253', port: 443 })).connected).toBe(true);
99
+ expect((await t.tcpConnect({ from: 'internalDevice', host: '10.226.10.10', port: 22 })).connected).toBe(false);
100
100
  });
101
101
 
102
102
  test('httpsRequest parses cert (CN/issuer/SAN/selfSigned) + status', async () => {
@@ -140,10 +140,10 @@ describe('host-shell footgun guard', () => {
140
140
  const { handle, calls } = fakeHandle(() => ({ stdout: '200', exitCode: 0 }));
141
141
  const t = createObserverTransport(handle);
142
142
  await t.resolve({ from: 'internalDevice', name: 'www.x', method: 'system' });
143
- await t.resolve({ from: 'internalDevice', name: 'www.x', method: 'dns', server: '192.168.0.10' });
143
+ await t.resolve({ from: 'internalDevice', name: 'www.x', method: 'dns', server: '10.226.1.10' });
144
144
  await t.resolve({ from: 'internalDevice', name: 'www.x', method: 'hostfiles' });
145
- await t.tcpConnect({ from: 'internalDevice', host: '10.0.10.10', port: 443 });
146
- await t.ping({ from: 'internalDevice', target: '192.168.0.253' });
145
+ await t.tcpConnect({ from: 'internalDevice', host: '10.226.10.10', port: 443 });
146
+ await t.ping({ from: 'internalDevice', target: '10.226.1.253' });
147
147
  await t.httpsRequest({ from: 'internalDevice', url: 'https://www.x' });
148
148
  await t.inspectResolution({ from: 'internalDevice', name: 'www.x' });
149
149
  expect(calls.length).toBeGreaterThan(0);
package/src/observer.ts CHANGED
@@ -45,8 +45,8 @@ export interface ObserverPlacement {
45
45
  // — the firewall to the segmented zones — NOT the LAN's default route, which is the home
46
46
  // router below. A faithful internalDevice routes its default via the home router and has
47
47
  // NO route to the segmented zones, so a dmz/app/secure container IP is unreachable.
48
- const HOME_ROUTER = '192.168.0.1';
49
- const INTERNAL_RESOLVER = '192.168.0.10';
48
+ const HOME_ROUTER = '10.226.1.1';
49
+ const INTERNAL_RESOLVER = '10.226.1.10';
50
50
  const PUBLIC_RESOLVER = '100.100.0.1';
51
51
  const INTERNET_GATEWAY = '100.64.0.1'; // fw-ext, on internet-external
52
52
 
@@ -64,7 +64,7 @@ export const OBSERVER_PLACEMENTS: Record<Exclude<Vantage, 'management'>, Observe
64
64
  internalDevice: {
65
65
  service: 'observer-internal',
66
66
  network: 'internal',
67
- ip: '192.168.0.211', // a LAN host (dhcp range is .200-.220; dhcp-client is .210)
67
+ ip: '10.226.1.211', // a LAN host (dhcp range is .200-.220; dhcp-client is .210)
68
68
  gateway: HOME_ROUTER,
69
69
  interZoneRoutes: false,
70
70
  // Identical DNS to the management box on purpose: the only difference vs management
@@ -17,7 +17,7 @@ export interface SocksProxyOptions {
17
17
  * ACME, Caddy host routing). Recommended for tests that need to
18
18
  * simulate what a real user sees.
19
19
  * - 'internal': operator-on-LAN view, attaches to the internal zone,
20
- * uses split-horizon DNS (192.168.0.10) so iamtheinternet.org resolves
20
+ * uses split-horizon DNS (10.226.1.10) so iamtheinternet.org resolves
21
21
  * to caddy's internal IP directly. Useful for debugging — reaches all
22
22
  * zones via the firewall.
23
23
  */
@@ -65,7 +65,7 @@ const VANTAGE_CONFIGS: Record<SocksProxyVantage, VantageConfig> = {
65
65
  },
66
66
  internal: {
67
67
  network: 'internal',
68
- ip: '192.168.0.150',
68
+ ip: '10.226.1.150',
69
69
  },
70
70
  };
71
71