@celilo/e2e 0.10.3 → 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.
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
 
package/src/types.ts CHANGED
@@ -123,7 +123,7 @@ export interface NetworkConfig {
123
123
 
124
124
  /**
125
125
  * When set, brings up a SECOND management-class box named `celilo-mgr-2`
126
- * (at 192.168.0.101 on `internal`) using the given image variant. Used by
126
+ * (at 10.226.1.101 on `internal`) using the given image variant. Used by
127
127
  * the migration restore e2e: the primary box is the backup source, the
128
128
  * second box is a fresh target that `apt install celilo` + `celilo restore`s
129
129
  * into. The second box is NOT auto-init'd by container-manager (which keys
@@ -355,7 +355,7 @@ export interface NetworkHandle {
355
355
  * subnets from ZONE_SUBNETS / ZONE_GATEWAYS.
356
356
  *
357
357
  * @param opts.zones - zones to provide (default: dmz, app, secure)
358
- * @param opts.natIp - NAT IP for port-forwarded traffic (default 192.168.0.253)
358
+ * @param opts.natIp - NAT IP for port-forwarded traffic (default 10.226.1.253)
359
359
  */
360
360
  deployFirewall(opts?: {
361
361
  zones?: Array<'dmz' | 'app' | 'secure'>;
@@ -407,22 +407,72 @@ export interface ResponderValues {
407
407
  aspects?: Record<string, boolean>;
408
408
  }
409
409
 
410
- export const ZONE_GATEWAYS: Record<Zone, string> = {
411
- dmz: '10.0.10.1',
412
- app: '10.0.20.1',
413
- secure: '10.0.30.1',
414
- internal: '192.168.0.254',
415
- 'secure-mgmt': '10.0.120.1',
410
+ /**
411
+ * The parent block every simulated PRIVATE zone lives inside (#539).
412
+ *
413
+ * The sim used to number its zones exactly like a real celilo fleet: its dmz,
414
+ * app and secure /24s WERE the fleet's, and its internal LAN was the operator's.
415
+ * That is not a cosmetic overlap — celilo's own forgejo-builder lives in the
416
+ * REAL dmz, so a leaked stack put a second interface on the builder's own /24
417
+ * and blackholed every containerized CI job's route to the forge for ~135s.
418
+ * Twice in one afternoon. Teardown can't be made reliable enough to fix that (a
419
+ * SIGKILL runs no handler), so the fix is that a leak stops MATTERING.
420
+ *
421
+ * `10.226.0.0/16` — mnemonic `0xE2` = "e2", the e2e supernet. It must be
422
+ * RFC-1918 (the private zones model private networks — CLAUDE.md inviolable
423
+ * rules #1/#3), and this block collides with nothing plausible:
424
+ * - the fleet numbers its zones out of the bottom of 10/8 → far away
425
+ * - the operator's VPN sits at the very top of 10/8 → far away
426
+ * - the operator's LAN is in 192.168/16 → different block
427
+ * - Docker's default address pools are 172.16/12 and
428
+ * 192.168/16 → different block
429
+ *
430
+ * The second reason matters more than the collision: there must be nothing
431
+ * magic about the octets production happens to use. A suite that only passes on
432
+ * those exact numbers is asserting one site's address plan rather than celilo's
433
+ * behaviour. Renumbering proves address-independence.
434
+ *
435
+ * Do not move a zone out of this block. `address-plan.test.ts` fails if you do,
436
+ * and fails if the retired fleet-colliding prefixes reappear anywhere in the
437
+ * suite.
438
+ */
439
+ export const SIM_PRIVATE_SUPERNET = '10.226.0.0/16';
440
+
441
+ /**
442
+ * Third-octet plan inside {@link SIM_PRIVATE_SUPERNET}. Host octets are
443
+ * unchanged from the pre-#539 numbering, so `caddy` is still `…10.10` and
444
+ * fw-main is still `…1.254` — only the network part moved.
445
+ */
446
+ const ZONE_PREFIXES: Record<Zone, string> = {
447
+ internal: '10.226.1',
448
+ dmz: '10.226.10',
449
+ app: '10.226.20',
450
+ secure: '10.226.30',
451
+ 'secure-mgmt': '10.226.120',
416
452
  };
417
453
 
418
- export const ZONE_SUBNETS: Record<Zone, string> = {
419
- dmz: '10.0.10.0/24',
420
- app: '10.0.20.0/24',
421
- secure: '10.0.30.0/24',
422
- internal: '192.168.0.0/24',
423
- 'secure-mgmt': '10.0.120.0/24',
454
+ /**
455
+ * An address in a zone, by host octet. Reference this instead of writing an
456
+ * octet: it is what makes the whole plan a one-line change, and what makes a
457
+ * stray hardcoded address stand out as the anomaly it is.
458
+ */
459
+ export function zoneIp(zone: Zone, host: number): string {
460
+ return `${ZONE_PREFIXES[zone]}.${host}`;
461
+ }
462
+
463
+ /** The router/firewall leg in each zone. `internal`'s is fw-main's LAN side. */
464
+ export const ZONE_GATEWAYS: Record<Zone, string> = {
465
+ dmz: zoneIp('dmz', 1),
466
+ app: zoneIp('app', 1),
467
+ secure: zoneIp('secure', 1),
468
+ internal: zoneIp('internal', 254),
469
+ 'secure-mgmt': zoneIp('secure-mgmt', 1),
424
470
  };
425
471
 
472
+ export const ZONE_SUBNETS: Record<Zone, string> = Object.fromEntries(
473
+ (Object.keys(ZONE_PREFIXES) as Zone[]).map((zone) => [zone, `${ZONE_PREFIXES[zone]}.0/24`]),
474
+ ) as Record<Zone, string>;
475
+
426
476
  /**
427
477
  * The firewall's internal-side DNAT ingress (the natIp) in the e2e topology — the role by
428
478
  * which an internal device reaches a service that lives in a segmented zone. Single source
@@ -431,7 +481,7 @@ export const ZONE_SUBNETS: Record<Zone, string> = {
431
481
  * hardcoding the address in a test.
432
482
  */
433
483
  export function internalNatIp(): string {
434
- return '192.168.0.253';
484
+ return zoneIp('internal', 253);
435
485
  }
436
486
 
437
487
  /**
@@ -441,7 +491,7 @@ export function internalNatIp(): string {
441
491
  * this instead of hardcoding the address in a test.
442
492
  */
443
493
  export function greenwaveRouterIp(): string {
444
- return '192.168.0.1';
494
+ return zoneIp('internal', 1);
445
495
  }
446
496
 
447
497
  /**
@@ -466,7 +516,7 @@ export function externalWanSubnet(): string {
466
516
 
467
517
  /** The internal split-horizon DNS resolver (dns-int) in the e2e topology. */
468
518
  export function internalResolverIp(): string {
469
- return '192.168.0.10';
519
+ return zoneIp('internal', 10);
470
520
  }
471
521
 
472
522
  /** The public upstream recursive resolver (the comcast-resolver sim). */
@@ -8,7 +8,7 @@
8
8
  * here — they wait on real firewall isolation (ISS-0118).
9
9
  *
10
10
  * Topology prerequisite: inject the observers the assertion needs, e.g.
11
- * network().dmz({ caddy: '10.0.10.10' }).observe('internalDevice', 'publicInternet')
11
+ * network().dmz({ caddy: '10.226.10.10' }).observe('internalDevice', 'publicInternet')
12
12
  */
13
13
 
14
14
  import { createObserverTransport } from './observer';
@@ -36,10 +36,10 @@ const LAN: Vantage = 'internalDevice';
36
36
 
37
37
  describe('resolveFrom', () => {
38
38
  test('defaults method to system, passes through ips', async () => {
39
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['192.168.0.253'] }) }));
39
+ const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }));
40
40
  const r = await probe.resolveFrom({ from: LAN, name: 'www.example.com' });
41
41
  expect(r.method).toBe('system');
42
- expect(r.ips).toEqual(['192.168.0.253']);
42
+ expect(r.ips).toEqual(['10.226.1.253']);
43
43
  });
44
44
  });
45
45
 
@@ -51,7 +51,7 @@ describe('assertResolves / assertNxdomain', () => {
51
51
  );
52
52
  });
53
53
  test('assertNxdomain throws when a public vantage CAN see an internal-only name', async () => {
54
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.0.20.5'] }) }));
54
+ const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.20.5'] }) }));
55
55
  await expect(
56
56
  probe.assertNxdomain({ from: 'publicInternet', name: 'authentik.internal' }),
57
57
  ).rejects.toThrow(/expected NXDOMAIN/);
@@ -60,23 +60,23 @@ describe('assertResolves / assertNxdomain', () => {
60
60
 
61
61
  describe('assertResolvesInZone — the killer verb', () => {
62
62
  test('ISS-0101: www at caddy DMZ container IP FAILS from internalDevice', async () => {
63
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.0.10.10'] }) }));
63
+ const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.10.10'] }) }));
64
64
  await expect(
65
65
  probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' }),
66
66
  ).rejects.toThrow(/ISS-0101/);
67
67
  });
68
68
 
69
69
  test('www at the firewall natIp PASSES from internalDevice (the right answer)', async () => {
70
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['192.168.0.253'] }) }));
70
+ const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }));
71
71
  const r = await probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' });
72
- expect(r.ips).toEqual(['192.168.0.253']);
72
+ expect(r.ips).toEqual(['10.226.1.253']);
73
73
  });
74
74
 
75
75
  test('public vantage expecting a public answer passes; a leaked zone IP fails', async () => {
76
76
  const ok = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['100.100.0.100'] }) }));
77
77
  await ok.assertResolvesInZone({ from: 'publicInternet', name: 'www.example.com', zone: 'public' });
78
78
 
79
- const leaked = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['192.168.0.253'] }) }));
79
+ const leaked = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }));
80
80
  await expect(
81
81
  leaked.assertResolvesInZone({ from: 'publicInternet', name: 'www.example.com', zone: 'public' }),
82
82
  ).rejects.toThrow(VantageAssertionError);
@@ -84,38 +84,38 @@ describe('assertResolvesInZone — the killer verb', () => {
84
84
 
85
85
  test('any one leaked IP among several fails the whole answer', async () => {
86
86
  const probe = new VantageProbe(
87
- fakeTransport({ resolve: async () => ({ ips: ['192.168.0.253', '10.0.30.9'] }) }),
87
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253', '10.226.30.9'] }) }),
88
88
  );
89
89
  await expect(
90
90
  probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' }),
91
- ).rejects.toThrow(/10\.0\.30\.9/);
91
+ ).rejects.toThrow(/10\.226\.30\.9/);
92
92
  });
93
93
  });
94
94
 
95
95
  describe('assertResolvesTo — exact-IP DNS correctness', () => {
96
96
  test('passes when the resolver returns the expected IP (via a specific server)', async () => {
97
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['192.168.0.53'] }) }));
97
+ const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.53'] }) }));
98
98
  const r = await probe.assertResolvesTo({
99
99
  from: 'internalDevice',
100
100
  name: 'host.infra.example.com',
101
101
  method: 'dns',
102
- server: '192.168.0.10',
103
- expected: '192.168.0.53',
102
+ server: '10.226.1.10',
103
+ expected: '10.226.1.53',
104
104
  });
105
- expect(r.ips).toEqual(['192.168.0.53']);
105
+ expect(r.ips).toEqual(['10.226.1.53']);
106
106
  });
107
107
 
108
108
  test('fails (naming the server) when the expected IP is absent', async () => {
109
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.0.10.99'] }) }));
109
+ const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.10.99'] }) }));
110
110
  await expect(
111
111
  probe.assertResolvesTo({
112
112
  from: 'internalDevice',
113
113
  name: 'host.infra.example.com',
114
114
  method: 'dns',
115
- server: '192.168.0.10',
116
- expected: '192.168.0.53',
115
+ server: '10.226.1.10',
116
+ expected: '10.226.1.53',
117
117
  }),
118
- ).rejects.toThrow(/via 192\.168\.0\.10.*expected to include \[192\.168\.0\.53\]/);
118
+ ).rejects.toThrow(/via 10\.226\.1\.10.*expected to include \[10\.226\.1\.53\]/);
119
119
  });
120
120
 
121
121
  test('accepts an array of expected IPs (all must be present)', async () => {
@@ -136,15 +136,15 @@ describe('reachability + isolation (§D6)', () => {
136
136
  });
137
137
  test('assertUnreachable throws when isolation is violated', async () => {
138
138
  const probe = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: true }) }));
139
- await expect(probe.assertUnreachable({ from: 'appSystem', host: '10.0.30.5', port: 443 })).rejects.toThrow(
139
+ await expect(probe.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 })).rejects.toThrow(
140
140
  /zone isolation/,
141
141
  );
142
142
  });
143
143
  test('the reachable/unreachable happy paths do not throw', async () => {
144
144
  const reachable = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: true }) }));
145
- await reachable.assertReachable({ from: LAN, host: '192.168.0.253', port: 443 });
145
+ await reachable.assertReachable({ from: LAN, host: '10.226.1.253', port: 443 });
146
146
  const blocked = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: false }) }));
147
- await blocked.assertUnreachable({ from: 'appSystem', host: '10.0.30.5', port: 443 });
147
+ await blocked.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 });
148
148
  });
149
149
  });
150
150
 
@@ -188,13 +188,13 @@ describe('inspectResolution', () => {
188
188
  const probe = new VantageProbe(
189
189
  fakeTransport({
190
190
  inspectResolution: async () => ({
191
- resolvConf: 'nameserver 192.168.0.253',
192
- hostsLines: ['10.0.10.10 auth.example.com'],
191
+ resolvConf: 'nameserver 10.226.1.253',
192
+ hostsLines: ['10.226.10.10 auth.example.com'],
193
193
  }),
194
194
  }),
195
195
  );
196
196
  const r = await probe.inspectResolution({ from: 'dmzSystem', name: 'auth.example.com' });
197
- expect(r.hostsLines).toContain('10.0.10.10 auth.example.com');
198
- expect(r.resolvConf).toContain('192.168.0.253');
197
+ expect(r.hostsLines).toContain('10.226.10.10 auth.example.com');
198
+ expect(r.resolvConf).toContain('10.226.1.253');
199
199
  });
200
200
  });
@@ -5,9 +5,12 @@ describe('parseIpv4', () => {
5
5
  test('parses dotted-quad to unsigned 32-bit', () => {
6
6
  expect(parseIpv4('0.0.0.0')).toBe(0);
7
7
  expect(parseIpv4('255.255.255.255')).toBe(0xffffffff);
8
- // High first octet must not go negative (sign-bit overflow guard).
9
- expect(parseIpv4('192.168.0.253')).toBe(((192 << 24) | (168 << 16) | (0 << 8) | 253) >>> 0);
10
- expect(parseIpv4('192.168.0.253')).toBeGreaterThan(0);
8
+ // High first octet must not go negative (sign-bit overflow guard). A pure
9
+ // arithmetic fixture, deliberately NOT an address from the sim plan: it used
10
+ // to be the sim's internal natIp, so the guard would have quietly
11
+ // evaporated the moment that zone was renumbered into 10/8 (#539).
12
+ expect(parseIpv4('200.100.50.253')).toBe(((200 << 24) | (100 << 16) | (50 << 8) | 253) >>> 0);
13
+ expect(parseIpv4('200.100.50.253')).toBeGreaterThan(0);
11
14
  });
12
15
  test('rejects malformed / out-of-range', () => {
13
16
  expect(parseIpv4('not.an.ip')).toBeNull();
@@ -18,24 +21,26 @@ describe('parseIpv4', () => {
18
21
 
19
22
  describe('inCidr', () => {
20
23
  test('membership within /24', () => {
21
- expect(inCidr('10.0.10.5', '10.0.10.0/24')).toBe(true);
22
- expect(inCidr('10.0.10.255', '10.0.10.0/24')).toBe(true);
23
- expect(inCidr('10.0.11.1', '10.0.10.0/24')).toBe(false);
24
+ expect(inCidr('10.226.10.5', '10.226.10.0/24')).toBe(true);
25
+ expect(inCidr('10.226.10.255', '10.226.10.0/24')).toBe(true);
26
+ expect(inCidr('10.226.11.1', '10.226.10.0/24')).toBe(false);
24
27
  });
25
- test('high-octet subnet (192.168) — no sign-bit bug', () => {
26
- expect(inCidr('192.168.0.253', '192.168.0.0/24')).toBe(true);
27
- expect(inCidr('192.168.1.1', '192.168.0.0/24')).toBe(false);
28
+ test('high-octet subnet — no sign-bit bug', () => {
29
+ // Arithmetic fixture, not a sim address: a first octet >= 128 is what trips
30
+ // the sign bit, and no address in the sim plan has one any more.
31
+ expect(inCidr('200.100.50.253', '200.100.50.0/24')).toBe(true);
32
+ expect(inCidr('200.100.51.1', '200.100.50.0/24')).toBe(false);
28
33
  });
29
34
  });
30
35
 
31
36
  describe('classifyIp', () => {
32
37
  test('managed zones (the segmented ones)', () => {
33
- expect(classifyIp('10.0.10.10')).toEqual({ kind: 'zone', zone: 'dmz' }); // caddy
34
- expect(classifyIp('10.0.20.30')).toEqual({ kind: 'zone', zone: 'app' }); // authentik
35
- expect(classifyIp('10.0.30.5')).toEqual({ kind: 'zone', zone: 'secure' });
38
+ expect(classifyIp('10.226.10.10')).toEqual({ kind: 'zone', zone: 'dmz' }); // caddy
39
+ expect(classifyIp('10.226.20.30')).toEqual({ kind: 'zone', zone: 'app' }); // authentik
40
+ expect(classifyIp('10.226.30.5')).toEqual({ kind: 'zone', zone: 'secure' });
36
41
  });
37
42
  test('internal zone (the LAN — natIp lives here)', () => {
38
- expect(classifyIp('192.168.0.253')).toEqual({ kind: 'zone', zone: 'internal' });
43
+ expect(classifyIp('10.226.1.253')).toEqual({ kind: 'zone', zone: 'internal' });
39
44
  });
40
45
  test('public: the CGNAT stand-in and real public space', () => {
41
46
  expect(classifyIp('100.100.0.100')).toEqual({ kind: 'public' }); // sim firewall external
@@ -56,9 +61,9 @@ describe('classifyIp', () => {
56
61
 
57
62
  describe('isSegmentedZoneIp', () => {
58
63
  test('dmz/app/secure are segmented; internal and public are not', () => {
59
- expect(isSegmentedZoneIp('10.0.10.10')).toBe(true); // caddy DMZ — the ISS-0101/0111 bug value
60
- expect(isSegmentedZoneIp('10.0.20.30')).toBe(true);
61
- expect(isSegmentedZoneIp('192.168.0.253')).toBe(false); // internal IS the LAN
64
+ expect(isSegmentedZoneIp('10.226.10.10')).toBe(true); // caddy DMZ — the ISS-0101/0111 bug value
65
+ expect(isSegmentedZoneIp('10.226.20.30')).toBe(true);
66
+ expect(isSegmentedZoneIp('10.226.1.253')).toBe(false); // internal IS the LAN
62
67
  expect(isSegmentedZoneIp('100.100.0.100')).toBe(false); // public
63
68
  });
64
69
  });
@@ -67,16 +72,16 @@ describe('isPublicLeakSafe (CLAUDE.md inviolable rule #1)', () => {
67
72
  test('only public addresses are leak-safe', () => {
68
73
  expect(isPublicLeakSafe('100.100.0.100')).toBe(true);
69
74
  expect(isPublicLeakSafe('8.8.8.8')).toBe(true);
70
- expect(isPublicLeakSafe('10.0.10.10')).toBe(false); // managed segmented zone
71
- expect(isPublicLeakSafe('192.168.0.253')).toBe(false); // internal/RFC-1918 must not leak
75
+ expect(isPublicLeakSafe('10.226.10.10')).toBe(false); // managed segmented zone
76
+ expect(isPublicLeakSafe('10.226.1.253')).toBe(false); // internal/RFC-1918 must not leak
72
77
  expect(isPublicLeakSafe('0.0.0.0')).toBe(false); // reserved sentinel
73
78
  });
74
79
 
75
80
  test('the ISS-0101 scenario: a fronted name resolving to caddy DMZ IP is NOT leak-safe', () => {
76
81
  // www.<domain> from a LAN device must be the natIp (internal) or public, never
77
82
  // caddy's DMZ container IP. The doctor + e2e share this judgment.
78
- const wwwResolvedWrong = '10.0.10.10';
83
+ const wwwResolvedWrong = '10.226.10.10';
79
84
  expect(isSegmentedZoneIp(wwwResolvedWrong)).toBe(true);
80
- expect(classifyIp('192.168.0.253').kind).toBe('zone'); // the RIGHT answer (natIp, internal)
85
+ expect(classifyIp('10.226.1.253').kind).toBe('zone'); // the RIGHT answer (natIp, internal)
81
86
  });
82
87
  });
@@ -10,9 +10,10 @@
10
10
  *
11
11
  * Classification uses `ZONE_SUBNETS` (exact) + a reserved-range check — deliberately
12
12
  * NOT a blanket RFC-1918/CGNAT ban: the sim's "public" IP is a CGNAT stand-in
13
- * (100.100.0.100), so in this topology 10.x / 192.168.x are *zones* and 100.x is
14
- * *public*. (A real deployment would additionally treat RFC-1918 + RFC-6598 as
15
- * non-public; that's the caller's policy, layered on top of this primitive.)
13
+ * (100.100.0.100), so in this topology the RFC-1918 zones under
14
+ * `SIM_PRIVATE_SUPERNET` are *zones* and 100.x is *public*. (A real deployment
15
+ * would additionally treat RFC-1918 + RFC-6598 as non-public; that's the
16
+ * caller's policy, layered on top of this primitive.)
16
17
  */
17
18
 
18
19
  import { type Zone, ZONE_SUBNETS } from './types';
@@ -52,7 +53,7 @@ export function parseIpv4(ip: string): number | null {
52
53
  return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
53
54
  }
54
55
 
55
- /** True if `ip` falls within `cidr` (e.g. "10.0.10.0/24"). */
56
+ /** True if `ip` falls within `cidr` (e.g. "10.226.10.0/24"). */
56
57
  export function inCidr(ip: string, cidr: string): boolean {
57
58
  const [base, bitsStr] = cidr.split('/');
58
59
  const bits = Number(bitsStr);