@celilo/e2e 0.10.4 → 0.11.1

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.
Files changed (68) hide show
  1. package/README.md +37 -28
  2. package/bin/e2e-status +1 -1
  3. package/bin/e2e-up +14 -14
  4. package/config/dhcp/dnsmasq.conf +3 -3
  5. package/config/resolver/unbound.conf +2 -2
  6. package/config/routing/fw-ext-routes.sh +4 -4
  7. package/config/routing/fw-isp-routes.sh +3 -3
  8. package/config/routing/fw-main-routes.sh +15 -4
  9. package/config/routing/management-routes.sh +11 -11
  10. package/config/routing/observer-setup.sh +3 -3
  11. package/config/routing/resolver-internal-routes.sh +5 -5
  12. package/config/routing/target-routes.sh +1 -1
  13. package/config/routing/target-setup.sh +18 -16
  14. package/config/socks/startup.sh +3 -3
  15. package/docker/Dockerfile.target-machine +15 -1
  16. package/docker/Dockerfile.target-machine-docker +6 -0
  17. package/docker/Dockerfile.target-technitium +6 -0
  18. package/npm-registry-server/src/packument.test.ts +2 -5
  19. package/npm-registry-server/src/packument.ts +3 -3
  20. package/npm-registry-server/src/server.ts +1 -1
  21. package/package.json +3 -3
  22. package/registry-server/src/bootstrap.ts +1 -1
  23. package/registry-server/src/introspection.test.ts +8 -12
  24. package/registry-server/src/server.test.ts +2 -2
  25. package/registry-server/src/server.ts +1 -1
  26. package/registry-server/src/storage.test.ts +8 -4
  27. package/registry-server/src/storage.ts +1 -1
  28. package/registry-server/src/validation.ts +2 -4
  29. package/scripts/pack-celilo-packages.ts +1 -4
  30. package/scripts/stage-apt-repo.ts +1 -1
  31. package/scripts/stage-libsignal.ts +1 -3
  32. package/simulators/greenwave/server.ts +4 -2
  33. package/simulators/greenwave/state.ts +24 -17
  34. package/simulators/isitup/server.ts +1 -5
  35. package/simulators/namecheap-ddns/server.ts +2 -4
  36. package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
  37. package/simulators/namecheap-ddns/zone-updater.ts +6 -10
  38. package/simulators/signal-cli/server.ts +3 -1
  39. package/src/address-plan.test.ts +95 -0
  40. package/src/browser.ts +3 -3
  41. package/src/bus-events.ts +2 -2
  42. package/src/cli/command-tree-parser.ts +8 -2
  43. package/src/cli/completion.ts +5 -8
  44. package/src/cli/index.ts +9 -2
  45. package/src/cli/module-discovery.ts +3 -1
  46. package/src/cli/scaffold.ts +2 -2
  47. package/src/cli-version-contract.ts +5 -6
  48. package/src/container-manager.ts +25 -8
  49. package/src/docker-compose-generator.ts +67 -39
  50. package/src/extract-failure.ts +92 -0
  51. package/src/fixtures.ts +10 -17
  52. package/src/index.ts +2 -2
  53. package/src/network-builder.ts +14 -9
  54. package/src/observer.test.ts +50 -19
  55. package/src/observer.ts +15 -8
  56. package/src/registry-bundle.ts +1 -3
  57. package/src/run-lock.test.ts +2 -2
  58. package/src/run-lock.ts +19 -5
  59. package/src/runner.ts +9 -67
  60. package/src/shared-infra.ts +50 -22
  61. package/src/socks-proxy.ts +16 -19
  62. package/src/types.ts +67 -17
  63. package/src/vantage-helpers.ts +6 -2
  64. package/src/vantage.test.ts +94 -40
  65. package/src/vantage.ts +24 -17
  66. package/src/wait-for-run.ts +1 -1
  67. package/src/zone-classifier.test.ts +32 -21
  68. package/src/zone-classifier.ts +7 -6
@@ -36,10 +36,12 @@ 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(
40
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
41
+ );
40
42
  const r = await probe.resolveFrom({ from: LAN, name: 'www.example.com' });
41
43
  expect(r.method).toBe('system');
42
- expect(r.ips).toEqual(['192.168.0.253']);
44
+ expect(r.ips).toEqual(['10.226.1.253']);
43
45
  });
44
46
  });
45
47
 
@@ -51,7 +53,9 @@ describe('assertResolves / assertNxdomain', () => {
51
53
  );
52
54
  });
53
55
  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'] }) }));
56
+ const probe = new VantageProbe(
57
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.20.5'] }) }),
58
+ );
55
59
  await expect(
56
60
  probe.assertNxdomain({ from: 'publicInternet', name: 'authentik.internal' }),
57
61
  ).rejects.toThrow(/expected NXDOMAIN/);
@@ -60,100 +64,150 @@ describe('assertResolves / assertNxdomain', () => {
60
64
 
61
65
  describe('assertResolvesInZone — the killer verb', () => {
62
66
  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'] }) }));
67
+ const probe = new VantageProbe(
68
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.10.10'] }) }),
69
+ );
64
70
  await expect(
65
71
  probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' }),
66
72
  ).rejects.toThrow(/ISS-0101/);
67
73
  });
68
74
 
69
75
  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'] }) }));
71
- const r = await probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' });
72
- expect(r.ips).toEqual(['192.168.0.253']);
76
+ const probe = new VantageProbe(
77
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
78
+ );
79
+ const r = await probe.assertResolvesInZone({
80
+ from: LAN,
81
+ name: 'www.example.com',
82
+ zone: 'internal',
83
+ });
84
+ expect(r.ips).toEqual(['10.226.1.253']);
73
85
  });
74
86
 
75
87
  test('public vantage expecting a public answer passes; a leaked zone IP fails', async () => {
76
- const ok = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['100.100.0.100'] }) }));
77
- await ok.assertResolvesInZone({ from: 'publicInternet', name: 'www.example.com', zone: 'public' });
88
+ const ok = new VantageProbe(
89
+ fakeTransport({ resolve: async () => ({ ips: ['100.100.0.100'] }) }),
90
+ );
91
+ await ok.assertResolvesInZone({
92
+ from: 'publicInternet',
93
+ name: 'www.example.com',
94
+ zone: 'public',
95
+ });
78
96
 
79
- const leaked = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['192.168.0.253'] }) }));
97
+ const leaked = new VantageProbe(
98
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
99
+ );
80
100
  await expect(
81
- leaked.assertResolvesInZone({ from: 'publicInternet', name: 'www.example.com', zone: 'public' }),
101
+ leaked.assertResolvesInZone({
102
+ from: 'publicInternet',
103
+ name: 'www.example.com',
104
+ zone: 'public',
105
+ }),
82
106
  ).rejects.toThrow(VantageAssertionError);
83
107
  });
84
108
 
85
109
  test('any one leaked IP among several fails the whole answer', async () => {
86
110
  const probe = new VantageProbe(
87
- fakeTransport({ resolve: async () => ({ ips: ['192.168.0.253', '10.0.30.9'] }) }),
111
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253', '10.226.30.9'] }) }),
88
112
  );
89
113
  await expect(
90
114
  probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' }),
91
- ).rejects.toThrow(/10\.0\.30\.9/);
115
+ ).rejects.toThrow(/10\.226\.30\.9/);
92
116
  });
93
117
  });
94
118
 
95
119
  describe('assertResolvesTo — exact-IP DNS correctness', () => {
96
120
  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'] }) }));
121
+ const probe = new VantageProbe(
122
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.53'] }) }),
123
+ );
98
124
  const r = await probe.assertResolvesTo({
99
125
  from: 'internalDevice',
100
126
  name: 'host.infra.example.com',
101
127
  method: 'dns',
102
- server: '192.168.0.10',
103
- expected: '192.168.0.53',
128
+ server: '10.226.1.10',
129
+ expected: '10.226.1.53',
104
130
  });
105
- expect(r.ips).toEqual(['192.168.0.53']);
131
+ expect(r.ips).toEqual(['10.226.1.53']);
106
132
  });
107
133
 
108
134
  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'] }) }));
135
+ const probe = new VantageProbe(
136
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.10.99'] }) }),
137
+ );
110
138
  await expect(
111
139
  probe.assertResolvesTo({
112
140
  from: 'internalDevice',
113
141
  name: 'host.infra.example.com',
114
142
  method: 'dns',
115
- server: '192.168.0.10',
116
- expected: '192.168.0.53',
143
+ server: '10.226.1.10',
144
+ expected: '10.226.1.53',
117
145
  }),
118
- ).rejects.toThrow(/via 192\.168\.0\.10.*expected to include \[192\.168\.0\.53\]/);
146
+ ).rejects.toThrow(/via 10\.226\.1\.10.*expected to include \[10\.226\.1\.53\]/);
119
147
  });
120
148
 
121
149
  test('accepts an array of expected IPs (all must be present)', async () => {
122
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['1.2.3.4', '5.6.7.8'] }) }));
123
- await probe.assertResolvesTo({ from: 'publicInternet', name: 'x', expected: ['1.2.3.4', '5.6.7.8'] });
150
+ const probe = new VantageProbe(
151
+ fakeTransport({ resolve: async () => ({ ips: ['1.2.3.4', '5.6.7.8'] }) }),
152
+ );
153
+ await probe.assertResolvesTo({
154
+ from: 'publicInternet',
155
+ name: 'x',
156
+ expected: ['1.2.3.4', '5.6.7.8'],
157
+ });
124
158
  await expect(
125
- probe.assertResolvesTo({ from: 'publicInternet', name: 'x', expected: ['1.2.3.4', '9.9.9.9'] }),
159
+ probe.assertResolvesTo({
160
+ from: 'publicInternet',
161
+ name: 'x',
162
+ expected: ['1.2.3.4', '9.9.9.9'],
163
+ }),
126
164
  ).rejects.toThrow(/9\.9\.9\.9/);
127
165
  });
128
166
  });
129
167
 
130
168
  describe('reachability + isolation (§D6)', () => {
131
169
  test('assertReachable throws when the connection fails (ISS-0111 git-ssh:2222)', async () => {
132
- const probe = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: false, detail: 'timeout' }) }));
133
- await expect(probe.assertReachable({ from: LAN, host: 'git-ssh.example.com', port: 2222 })).rejects.toThrow(
134
- /timeout/,
170
+ const probe = new VantageProbe(
171
+ fakeTransport({ tcpConnect: async () => ({ connected: false, detail: 'timeout' }) }),
135
172
  );
173
+ await expect(
174
+ probe.assertReachable({ from: LAN, host: 'git-ssh.example.com', port: 2222 }),
175
+ ).rejects.toThrow(/timeout/);
136
176
  });
137
177
  test('assertUnreachable throws when isolation is violated', async () => {
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(
140
- /zone isolation/,
178
+ const probe = new VantageProbe(
179
+ fakeTransport({ tcpConnect: async () => ({ connected: true }) }),
141
180
  );
181
+ await expect(
182
+ probe.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 }),
183
+ ).rejects.toThrow(/zone isolation/);
142
184
  });
143
185
  test('the reachable/unreachable happy paths do not throw', async () => {
144
- const reachable = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: true }) }));
145
- await reachable.assertReachable({ from: LAN, host: '192.168.0.253', port: 443 });
146
- const blocked = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: false }) }));
147
- await blocked.assertUnreachable({ from: 'appSystem', host: '10.0.30.5', port: 443 });
186
+ const reachable = new VantageProbe(
187
+ fakeTransport({ tcpConnect: async () => ({ connected: true }) }),
188
+ );
189
+ await reachable.assertReachable({ from: LAN, host: '10.226.1.253', port: 443 });
190
+ const blocked = new VantageProbe(
191
+ fakeTransport({ tcpConnect: async () => ({ connected: false }) }),
192
+ );
193
+ await blocked.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 });
148
194
  });
149
195
  });
150
196
 
151
197
  describe('assertHttps — cert provenance', () => {
152
198
  test('self-signed cert is rejected (catches --insecure / skip-cert cheats)', async () => {
153
199
  const probe = new VantageProbe(
154
- fakeTransport({ httpsRequest: async () => ({ status: 200, body: 'ok', cert: realCert({ selfSigned: true }) }) }),
200
+ fakeTransport({
201
+ httpsRequest: async () => ({
202
+ status: 200,
203
+ body: 'ok',
204
+ cert: realCert({ selfSigned: true }),
205
+ }),
206
+ }),
207
+ );
208
+ await expect(probe.assertHttps({ from: LAN, url: 'https://www.example.com' })).rejects.toThrow(
209
+ /self-signed/,
155
210
  );
156
- await expect(probe.assertHttps({ from: LAN, url: 'https://www.example.com' })).rejects.toThrow(/self-signed/);
157
211
  });
158
212
  test('wrong CA issuer is rejected', async () => {
159
213
  const probe = new VantageProbe(fakeTransport());
@@ -188,13 +242,13 @@ describe('inspectResolution', () => {
188
242
  const probe = new VantageProbe(
189
243
  fakeTransport({
190
244
  inspectResolution: async () => ({
191
- resolvConf: 'nameserver 192.168.0.253',
192
- hostsLines: ['10.0.10.10 auth.example.com'],
245
+ resolvConf: 'nameserver 10.226.1.253',
246
+ hostsLines: ['10.226.10.10 auth.example.com'],
193
247
  }),
194
248
  }),
195
249
  );
196
250
  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');
251
+ expect(r.hostsLines).toContain('10.226.10.10 auth.example.com');
252
+ expect(r.resolvConf).toContain('10.226.1.253');
199
253
  });
200
254
  });
package/src/vantage.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * elsewhere, so the verbs stay pure and unit-testable without Docker.
15
15
  */
16
16
 
17
- import { type Vantage, type Zone } from './types';
17
+ import type { Vantage, Zone } from './types';
18
18
  import { classifyIp } from './zone-classifier';
19
19
 
20
20
  // `Vantage` is topology vocabulary — defined in ./types alongside Zone — and re-exported
@@ -64,11 +64,23 @@ export interface ResolutionInspection {
64
64
  * are the ONLY place raw shell / network calls live; the verbs below never touch I/O.
65
65
  */
66
66
  export interface ProbeTransport {
67
- resolve(opts: { from: Vantage; name: string; method: ResolveMethod; server?: string }): Promise<{ ips: string[] }>;
68
- tcpConnect(opts: { from: Vantage; host: string; port: number }): Promise<{ connected: boolean; detail?: string }>;
67
+ resolve(opts: { from: Vantage; name: string; method: ResolveMethod; server?: string }): Promise<{
68
+ ips: string[];
69
+ }>;
70
+ tcpConnect(opts: { from: Vantage; host: string; port: number }): Promise<{
71
+ connected: boolean;
72
+ detail?: string;
73
+ }>;
69
74
  ping(opts: { from: Vantage; target: string }): Promise<{ alive: boolean }>;
70
- httpsRequest(opts: { from: Vantage; url: string }): Promise<{ status: number; body: string; cert: CertInfo }>;
71
- inspectResolution(opts: { from: Vantage; name: string }): Promise<{ resolvConf: string; hostsLines: string[] }>;
75
+ httpsRequest(opts: { from: Vantage; url: string }): Promise<{
76
+ status: number;
77
+ body: string;
78
+ cert: CertInfo;
79
+ }>;
80
+ inspectResolution(opts: { from: Vantage; name: string }): Promise<{
81
+ resolvConf: string;
82
+ hostsLines: string[];
83
+ }>;
72
84
  }
73
85
 
74
86
  /** Thrown when a vantage assertion fails. Messages name the bug class they guard. */
@@ -163,10 +175,7 @@ export class VantageProbe {
163
175
  if (offenders.length > 0) {
164
176
  const detail = offenders.map((ip) => `${ip} (${describeZone(ip)})`).join(', ');
165
177
  throw new VantageAssertionError(
166
- `${opts.name} from ${opts.from} resolved to ${detail}, expected zone "${opts.zone}". ` +
167
- 'A managed-zone container IP is unroutable from outside that zone — this is the ' +
168
- 'e2e-green/prod-broke class (ISS-0101, ISS-0111). The right answer from a consumer ' +
169
- 'vantage is the firewall natIp (internal) or a public address.',
178
+ `${opts.name} from ${opts.from} resolved to ${detail}, expected zone "${opts.zone}". A managed-zone container IP is unroutable from outside that zone — this is the e2e-green/prod-broke class (ISS-0101, ISS-0111). The right answer from a consumer vantage is the firewall natIp (internal) or a public address.`,
170
179
  );
171
180
  }
172
181
  return result;
@@ -178,7 +187,9 @@ export class VantageProbe {
178
187
  * the internal split-horizon resolver vs the public resolver). Used by the DNS suites to
179
188
  * check a DDNS-registered IP, an nsupdate'd A record, or a split-horizon answer.
180
189
  */
181
- async assertResolvesTo(opts: ResolveOpts & { expected: string | string[] }): Promise<ResolveResult> {
190
+ async assertResolvesTo(
191
+ opts: ResolveOpts & { expected: string | string[] },
192
+ ): Promise<ResolveResult> {
182
193
  const result = await this.assertResolves(opts);
183
194
  const expected = Array.isArray(opts.expected) ? opts.expected : [opts.expected];
184
195
  const missing = expected.filter((ip) => !result.ips.includes(ip));
@@ -201,9 +212,7 @@ export class VantageProbe {
201
212
  const { connected, detail } = await this.transport.tcpConnect(opts);
202
213
  if (!connected) {
203
214
  throw new VantageAssertionError(
204
- `${opts.host}:${opts.port} was NOT reachable from ${opts.from}` +
205
- (detail ? ` (${detail})` : '') +
206
- ', expected a successful TCP connection.',
215
+ `${opts.host}:${opts.port} was NOT reachable from ${opts.from}${detail ? ` (${detail})` : ''}, expected a successful TCP connection.`,
207
216
  );
208
217
  }
209
218
  }
@@ -213,8 +222,7 @@ export class VantageProbe {
213
222
  const { connected } = await this.transport.tcpConnect(opts);
214
223
  if (connected) {
215
224
  throw new VantageAssertionError(
216
- `${opts.host}:${opts.port} WAS reachable from ${opts.from}, expected it to be blocked ` +
217
- '(zone isolation / firewall boundary violated).',
225
+ `${opts.host}:${opts.port} WAS reachable from ${opts.from}, expected it to be blocked (zone isolation / firewall boundary violated).`,
218
226
  );
219
227
  }
220
228
  }
@@ -229,8 +237,7 @@ export class VantageProbe {
229
237
 
230
238
  if (res.cert.selfSigned) {
231
239
  throw new VantageAssertionError(
232
- `${opts.url} from ${opts.from} presented a self-signed cert (issuer "${res.cert.issuer}"). ` +
233
- 'A real cert from the e2e CA (Pebble / Let’s Encrypt) is required — no --insecure.',
240
+ `${opts.url} from ${opts.from} presented a self-signed cert (issuer "${res.cert.issuer}"). A real cert from the e2e CA (Pebble / Let’s Encrypt) is required — no --insecure.`,
234
241
  );
235
242
  }
236
243
  if (opts.expectCN && res.cert.cn !== opts.expectCN) {
@@ -10,7 +10,7 @@
10
10
  * decides whether to spawn cele2e themselves or just observe.
11
11
  */
12
12
 
13
- import { defineEvents, openBus, type Bus, type BusEvent } from '@celilo/event-bus';
13
+ import { type Bus, type BusEvent, defineEvents, openBus } from '@celilo/event-bus';
14
14
 
15
15
  export type WaitResult =
16
16
  | {
@@ -1,13 +1,22 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { classifyIp, inCidr, isPublicLeakSafe, isSegmentedZoneIp, parseIpv4 } from './zone-classifier';
2
+ import {
3
+ classifyIp,
4
+ inCidr,
5
+ isPublicLeakSafe,
6
+ isSegmentedZoneIp,
7
+ parseIpv4,
8
+ } from './zone-classifier';
3
9
 
4
10
  describe('parseIpv4', () => {
5
11
  test('parses dotted-quad to unsigned 32-bit', () => {
6
12
  expect(parseIpv4('0.0.0.0')).toBe(0);
7
13
  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);
14
+ // High first octet must not go negative (sign-bit overflow guard). A pure
15
+ // arithmetic fixture, deliberately NOT an address from the sim plan: it used
16
+ // to be the sim's internal natIp, so the guard would have quietly
17
+ // evaporated the moment that zone was renumbered into 10/8 (#539).
18
+ expect(parseIpv4('200.100.50.253')).toBe(((200 << 24) | (100 << 16) | (50 << 8) | 253) >>> 0);
19
+ expect(parseIpv4('200.100.50.253')).toBeGreaterThan(0);
11
20
  });
12
21
  test('rejects malformed / out-of-range', () => {
13
22
  expect(parseIpv4('not.an.ip')).toBeNull();
@@ -18,24 +27,26 @@ describe('parseIpv4', () => {
18
27
 
19
28
  describe('inCidr', () => {
20
29
  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);
30
+ expect(inCidr('10.226.10.5', '10.226.10.0/24')).toBe(true);
31
+ expect(inCidr('10.226.10.255', '10.226.10.0/24')).toBe(true);
32
+ expect(inCidr('10.226.11.1', '10.226.10.0/24')).toBe(false);
24
33
  });
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);
34
+ test('high-octet subnet — no sign-bit bug', () => {
35
+ // Arithmetic fixture, not a sim address: a first octet >= 128 is what trips
36
+ // the sign bit, and no address in the sim plan has one any more.
37
+ expect(inCidr('200.100.50.253', '200.100.50.0/24')).toBe(true);
38
+ expect(inCidr('200.100.51.1', '200.100.50.0/24')).toBe(false);
28
39
  });
29
40
  });
30
41
 
31
42
  describe('classifyIp', () => {
32
43
  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' });
44
+ expect(classifyIp('10.226.10.10')).toEqual({ kind: 'zone', zone: 'dmz' }); // caddy
45
+ expect(classifyIp('10.226.20.30')).toEqual({ kind: 'zone', zone: 'app' }); // authentik
46
+ expect(classifyIp('10.226.30.5')).toEqual({ kind: 'zone', zone: 'secure' });
36
47
  });
37
48
  test('internal zone (the LAN — natIp lives here)', () => {
38
- expect(classifyIp('192.168.0.253')).toEqual({ kind: 'zone', zone: 'internal' });
49
+ expect(classifyIp('10.226.1.253')).toEqual({ kind: 'zone', zone: 'internal' });
39
50
  });
40
51
  test('public: the CGNAT stand-in and real public space', () => {
41
52
  expect(classifyIp('100.100.0.100')).toEqual({ kind: 'public' }); // sim firewall external
@@ -56,9 +67,9 @@ describe('classifyIp', () => {
56
67
 
57
68
  describe('isSegmentedZoneIp', () => {
58
69
  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
70
+ expect(isSegmentedZoneIp('10.226.10.10')).toBe(true); // caddy DMZ — the ISS-0101/0111 bug value
71
+ expect(isSegmentedZoneIp('10.226.20.30')).toBe(true);
72
+ expect(isSegmentedZoneIp('10.226.1.253')).toBe(false); // internal IS the LAN
62
73
  expect(isSegmentedZoneIp('100.100.0.100')).toBe(false); // public
63
74
  });
64
75
  });
@@ -67,16 +78,16 @@ describe('isPublicLeakSafe (CLAUDE.md inviolable rule #1)', () => {
67
78
  test('only public addresses are leak-safe', () => {
68
79
  expect(isPublicLeakSafe('100.100.0.100')).toBe(true);
69
80
  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
81
+ expect(isPublicLeakSafe('10.226.10.10')).toBe(false); // managed segmented zone
82
+ expect(isPublicLeakSafe('10.226.1.253')).toBe(false); // internal/RFC-1918 must not leak
72
83
  expect(isPublicLeakSafe('0.0.0.0')).toBe(false); // reserved sentinel
73
84
  });
74
85
 
75
86
  test('the ISS-0101 scenario: a fronted name resolving to caddy DMZ IP is NOT leak-safe', () => {
76
87
  // www.<domain> from a LAN device must be the natIp (internal) or public, never
77
88
  // caddy's DMZ container IP. The doctor + e2e share this judgment.
78
- const wwwResolvedWrong = '10.0.10.10';
89
+ const wwwResolvedWrong = '10.226.10.10';
79
90
  expect(isSegmentedZoneIp(wwwResolvedWrong)).toBe(true);
80
- expect(classifyIp('192.168.0.253').kind).toBe('zone'); // the RIGHT answer (natIp, internal)
91
+ expect(classifyIp('10.226.1.253').kind).toBe('zone'); // the RIGHT answer (natIp, internal)
81
92
  });
82
93
  });
@@ -10,12 +10,13 @@
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
- import { type Zone, ZONE_SUBNETS } from './types';
19
+ import { ZONE_SUBNETS, type Zone } from './types';
19
20
 
20
21
  export type ZoneClass =
21
22
  | { kind: 'zone'; zone: Zone }
@@ -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);
@@ -61,7 +62,7 @@ export function inCidr(ip: string, cidr: string): boolean {
61
62
  if (ipN === null || baseN === null || Number.isNaN(bits) || bits < 0 || bits > 32) return false;
62
63
  if (bits === 0) return true;
63
64
  const mask = (0xffffffff << (32 - bits)) >>> 0;
64
- return (ipN & mask) >>> 0 === ((baseN & mask) >>> 0);
65
+ return (ipN & mask) >>> 0 === (baseN & mask) >>> 0;
65
66
  }
66
67
 
67
68
  /** Classify a resolved IPv4 against the e2e topology. */