@celilo/e2e 0.11.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/e2e-status +1 -1
- package/bin/e2e-up +2 -2
- package/config/dns/tangohost.com.zone +1 -1
- package/config/dns/templates/example.net.zone +3 -3
- package/config/dns/templates/iamtheinternet.org.zone +2 -2
- package/config/resolver/unbound.conf +4 -0
- package/config/routing/fw-ext-routes.sh +8 -8
- package/config/routing/fw-isp-routes.sh +2 -2
- package/config/routing/fw-main-routes.sh +5 -5
- package/config/routing/management-routes.sh +1 -1
- package/config/routing/observer-setup.sh +1 -1
- package/config/routing/public-resolver-routes.sh +33 -0
- package/config/routing/public-sim-entrypoint.sh +1 -1
- package/config/routing/resolver-internal-routes.sh +7 -2
- package/config/routing/resolver-routes.sh +1 -1
- package/config/routing/target-routes.sh +1 -1
- package/config/routing/target-setup.sh +1 -1
- package/config/socks/startup.sh +2 -2
- package/docker/Dockerfile.ip-echo +20 -0
- package/docker/Dockerfile.resolver +5 -1
- package/package.json +5 -4
- package/simulators/greenwave/server.ts +35 -0
- package/simulators/greenwave/state.ts +79 -7
- package/simulators/ip-echo/server.ts +76 -0
- package/src/address-plan.test.ts +4 -4
- package/src/cli/build.ts +30 -0
- package/src/cli/command-registry.ts +19 -0
- package/src/cli/completion.ts +16 -1
- package/src/cli/index.ts +85 -4
- package/src/container-manager.ts +75 -12
- package/src/docker-compose-generator.ts +97 -29
- package/src/doctor.test.ts +279 -0
- package/src/doctor.ts +421 -0
- package/src/extract-failure.ts +41 -0
- package/src/index.ts +12 -0
- package/src/last-run.test.ts +62 -0
- package/src/last-run.ts +54 -0
- package/src/network-builder.ts +11 -0
- package/src/observer.test.ts +2 -2
- package/src/observer.ts +1 -1
- package/src/router-swap.ts +121 -0
- package/src/run-lock.test.ts +73 -2
- package/src/run-lock.ts +110 -14
- package/src/runner.ts +99 -5
- package/src/simulator-ips.ts +21 -0
- package/src/socks-proxy.ts +2 -2
- package/src/types.ts +34 -4
- package/src/vantage.test.ts +1 -1
- package/src/zone-classifier.test.ts +11 -16
- package/src/zone-classifier.ts +16 -31
package/src/types.ts
CHANGED
|
@@ -55,6 +55,15 @@ export interface NetworkConfig {
|
|
|
55
55
|
domain: string;
|
|
56
56
|
ddnsPassword: string;
|
|
57
57
|
verifyRouting: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Which real ISP router the `fw-isp` simulator stands in for, expressed as
|
|
60
|
+
* its TR-181 vendor extension prefix. `X_GWS_` (the default) is the GreenWave
|
|
61
|
+
* C4000XG that `modules/greenwave` drives; `X_AXON_` is the Axon Networks
|
|
62
|
+
* Q1000K that `modules/axon` drives. The CGI protocol is identical across the
|
|
63
|
+
* two — this prefix IS the entire difference, which is why one simulator
|
|
64
|
+
* serves both rather than a near-duplicate second one.
|
|
65
|
+
*/
|
|
66
|
+
routerVendorPrefix?: string;
|
|
58
67
|
/** Extra volume mounts for the management container (host:container format) */
|
|
59
68
|
managementVolumes: string[];
|
|
60
69
|
/** Include a DHCP client container on the internal network */
|
|
@@ -354,7 +363,12 @@ export interface NetworkHandle {
|
|
|
354
363
|
* Targets fw-main (the customer firewall) using the standard topology
|
|
355
364
|
* subnets from ZONE_SUBNETS / ZONE_GATEWAYS.
|
|
356
365
|
*
|
|
357
|
-
*
|
|
366
|
+
* `opts.zones` says which zones get `provided_networks` written to system
|
|
367
|
+
* config. It does NOT declare which NICs the firewall has — the topology
|
|
368
|
+
* decides that, and `deployFirewall` derives the declared list from it, so a
|
|
369
|
+
* test cannot under-declare the box's hardware by omission.
|
|
370
|
+
*
|
|
371
|
+
* @param opts.zones - zones to PROVIDE (default: dmz, app, secure)
|
|
358
372
|
* @param opts.natIp - NAT IP for port-forwarded traffic (default 10.226.1.253)
|
|
359
373
|
*/
|
|
360
374
|
deployFirewall(opts?: {
|
|
@@ -362,6 +376,22 @@ export interface NetworkHandle {
|
|
|
362
376
|
natIp?: string;
|
|
363
377
|
}): Promise<void>;
|
|
364
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Deploy the `greenwave` module against the ISP router (`fw-isp`) — the box
|
|
381
|
+
* that actually owns the WAN in the `default` topology.
|
|
382
|
+
*
|
|
383
|
+
* This is what makes the production shape work end to end. celilo's own
|
|
384
|
+
* firewall is DOWNSTREAM: it holds only RFC1918 legs, reports no external
|
|
385
|
+
* edge, and `exposeService` therefore has to delegate up the chain to a
|
|
386
|
+
* provider whose firewall capability declares `has_external: true`. greenwave
|
|
387
|
+
* is that provider. Without it a `default`-topology test that exposes
|
|
388
|
+
* anything publicly fails at the delegation step, which is correct — there is
|
|
389
|
+
* genuinely nothing that knows the fleet's public address.
|
|
390
|
+
*
|
|
391
|
+
* Not needed under `direct-internet`, where fw-main owns the edge itself.
|
|
392
|
+
*/
|
|
393
|
+
deployGreenwave(): Promise<void>;
|
|
394
|
+
|
|
365
395
|
/** Tear down the entire network */
|
|
366
396
|
stop(): Promise<void>;
|
|
367
397
|
|
|
@@ -502,7 +532,7 @@ export function greenwaveRouterIp(): string {
|
|
|
502
532
|
* by construction. Reference this instead of hardcoding it.
|
|
503
533
|
*/
|
|
504
534
|
export function externalWanIp(): string {
|
|
505
|
-
return '
|
|
535
|
+
return '203.0.113.100';
|
|
506
536
|
}
|
|
507
537
|
|
|
508
538
|
/**
|
|
@@ -511,7 +541,7 @@ export function externalWanIp(): string {
|
|
|
511
541
|
* WAN address when it reaches a public service.
|
|
512
542
|
*/
|
|
513
543
|
export function externalWanSubnet(): string {
|
|
514
|
-
return '
|
|
544
|
+
return '203.0.113.0/24';
|
|
515
545
|
}
|
|
516
546
|
|
|
517
547
|
/** The internal split-horizon DNS resolver (dns-int) in the e2e topology. */
|
|
@@ -521,5 +551,5 @@ export function internalResolverIp(): string {
|
|
|
521
551
|
|
|
522
552
|
/** The public upstream recursive resolver (the comcast-resolver sim). */
|
|
523
553
|
export function publicResolverIp(): string {
|
|
524
|
-
return '
|
|
554
|
+
return '203.0.113.1';
|
|
525
555
|
}
|
package/src/vantage.test.ts
CHANGED
|
@@ -86,7 +86,7 @@ describe('assertResolvesInZone — the killer verb', () => {
|
|
|
86
86
|
|
|
87
87
|
test('public vantage expecting a public answer passes; a leaked zone IP fails', async () => {
|
|
88
88
|
const ok = new VantageProbe(
|
|
89
|
-
fakeTransport({ resolve: async () => ({ ips: ['
|
|
89
|
+
fakeTransport({ resolve: async () => ({ ips: ['203.0.113.100'] }) }),
|
|
90
90
|
);
|
|
91
91
|
await ok.assertResolvesInZone({
|
|
92
92
|
from: 'publicInternet',
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
inCidr,
|
|
5
|
-
isPublicLeakSafe,
|
|
6
|
-
isSegmentedZoneIp,
|
|
7
|
-
parseIpv4,
|
|
8
|
-
} from './zone-classifier';
|
|
2
|
+
import { parseIpv4, subnetContains } from '@celilo/capabilities';
|
|
3
|
+
import { classifyIp, isPublicLeakSafe, isSegmentedZoneIp } from './zone-classifier';
|
|
9
4
|
|
|
10
5
|
describe('parseIpv4', () => {
|
|
11
6
|
test('parses dotted-quad to unsigned 32-bit', () => {
|
|
@@ -25,17 +20,17 @@ describe('parseIpv4', () => {
|
|
|
25
20
|
});
|
|
26
21
|
});
|
|
27
22
|
|
|
28
|
-
describe('
|
|
23
|
+
describe('subnetContains (shared, @celilo/capabilities)', () => {
|
|
29
24
|
test('membership within /24', () => {
|
|
30
|
-
expect(
|
|
31
|
-
expect(
|
|
32
|
-
expect(
|
|
25
|
+
expect(subnetContains('10.226.10.0/24', '10.226.10.5')).toBe(true);
|
|
26
|
+
expect(subnetContains('10.226.10.0/24', '10.226.10.255')).toBe(true);
|
|
27
|
+
expect(subnetContains('10.226.10.0/24', '10.226.11.1')).toBe(false);
|
|
33
28
|
});
|
|
34
29
|
test('high-octet subnet — no sign-bit bug', () => {
|
|
35
30
|
// Arithmetic fixture, not a sim address: a first octet >= 128 is what trips
|
|
36
31
|
// the sign bit, and no address in the sim plan has one any more.
|
|
37
|
-
expect(
|
|
38
|
-
expect(
|
|
32
|
+
expect(subnetContains('200.100.50.0/24', '200.100.50.253')).toBe(true);
|
|
33
|
+
expect(subnetContains('200.100.50.0/24', '200.100.51.1')).toBe(false);
|
|
39
34
|
});
|
|
40
35
|
});
|
|
41
36
|
|
|
@@ -49,7 +44,7 @@ describe('classifyIp', () => {
|
|
|
49
44
|
expect(classifyIp('10.226.1.253')).toEqual({ kind: 'zone', zone: 'internal' });
|
|
50
45
|
});
|
|
51
46
|
test('public: the CGNAT stand-in and real public space', () => {
|
|
52
|
-
expect(classifyIp('
|
|
47
|
+
expect(classifyIp('203.0.113.100')).toEqual({ kind: 'public' }); // sim firewall external
|
|
53
48
|
expect(classifyIp('8.8.8.8')).toEqual({ kind: 'public' });
|
|
54
49
|
expect(classifyIp('71.36.115.155')).toEqual({ kind: 'public' }); // the prod DDNS target
|
|
55
50
|
});
|
|
@@ -70,13 +65,13 @@ describe('isSegmentedZoneIp', () => {
|
|
|
70
65
|
expect(isSegmentedZoneIp('10.226.10.10')).toBe(true); // caddy DMZ — the ISS-0101/0111 bug value
|
|
71
66
|
expect(isSegmentedZoneIp('10.226.20.30')).toBe(true);
|
|
72
67
|
expect(isSegmentedZoneIp('10.226.1.253')).toBe(false); // internal IS the LAN
|
|
73
|
-
expect(isSegmentedZoneIp('
|
|
68
|
+
expect(isSegmentedZoneIp('203.0.113.100')).toBe(false); // public
|
|
74
69
|
});
|
|
75
70
|
});
|
|
76
71
|
|
|
77
72
|
describe('isPublicLeakSafe (CLAUDE.md inviolable rule #1)', () => {
|
|
78
73
|
test('only public addresses are leak-safe', () => {
|
|
79
|
-
expect(isPublicLeakSafe('
|
|
74
|
+
expect(isPublicLeakSafe('203.0.113.100')).toBe(true);
|
|
80
75
|
expect(isPublicLeakSafe('8.8.8.8')).toBe(true);
|
|
81
76
|
expect(isPublicLeakSafe('10.226.10.10')).toBe(false); // managed segmented zone
|
|
82
77
|
expect(isPublicLeakSafe('10.226.1.253')).toBe(false); // internal/RFC-1918 must not leak
|
package/src/zone-classifier.ts
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* e.g. "www.<domain> resolved from internalDevice must NOT be a segmented-zone
|
|
9
9
|
* container IP" — the ISS-0101 / ISS-0111 bug class.
|
|
10
10
|
*
|
|
11
|
-
* Classification uses `ZONE_SUBNETS` (exact) + a reserved-range check
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Classification uses `ZONE_SUBNETS` (exact) + a reserved-range check. RFC-1918 is
|
|
12
|
+
* deliberately absent from the reserved list: in this topology those ranges are the
|
|
13
|
+
* managed zones. RFC-6598 (CGNAT) used to be absent too, because the sim's "public"
|
|
14
|
+
* edge was a CGNAT stand-in — that exemption is gone with the renumber to TEST-NET-3
|
|
15
|
+
* (RFC 5737), and a carrier address is now correctly non-public here as it is in
|
|
16
|
+
* `@celilo/capabilities`.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { parseIpv4, subnetContains } from '@celilo/capabilities';
|
|
19
20
|
import { ZONE_SUBNETS, type Zone } from './types';
|
|
20
21
|
|
|
21
22
|
export type ZoneClass =
|
|
@@ -33,46 +34,30 @@ export const SEGMENTED_ZONES: readonly Zone[] = ['dmz', 'app', 'secure'];
|
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Special-use IPv4 ranges that must never be a real service address (nor appear in
|
|
36
|
-
* any DNS answer). RFC-1918
|
|
37
|
-
*
|
|
37
|
+
* any DNS answer). RFC-1918 is intentionally absent — in this topology those ranges
|
|
38
|
+
* are the managed zones.
|
|
39
|
+
*
|
|
40
|
+
* The `internet-external` network still numbers its public simulators out of
|
|
41
|
+
* `100.64.0.0/24`; nothing classifies those addresses, and renumbering that network
|
|
42
|
+
* is not part of this change.
|
|
38
43
|
*/
|
|
39
44
|
const RESERVED: ReadonlyArray<{ cidr: string; label: string }> = [
|
|
40
45
|
{ cidr: '0.0.0.0/8', label: 'unspecified/this-host (incl. the 0.0.0.0 sentinel)' },
|
|
46
|
+
{ cidr: '100.64.0.0/10', label: 'carrier-grade NAT (RFC 6598)' },
|
|
41
47
|
{ cidr: '127.0.0.0/8', label: 'loopback' },
|
|
42
48
|
{ cidr: '169.254.0.0/16', label: 'link-local (RFC 3927)' },
|
|
43
49
|
{ cidr: '224.0.0.0/4', label: 'multicast' },
|
|
44
50
|
{ cidr: '240.0.0.0/4', label: 'reserved (RFC 1112)' },
|
|
45
51
|
];
|
|
46
52
|
|
|
47
|
-
/** Parse a dotted-quad IPv4 to an unsigned 32-bit int, or null if malformed. */
|
|
48
|
-
export function parseIpv4(ip: string): number | null {
|
|
49
|
-
const m = ip.trim().match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
50
|
-
if (!m) return null;
|
|
51
|
-
const octets = m.slice(1, 5).map(Number);
|
|
52
|
-
if (octets.some((o) => o > 255)) return null;
|
|
53
|
-
return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** True if `ip` falls within `cidr` (e.g. "10.226.10.0/24"). */
|
|
57
|
-
export function inCidr(ip: string, cidr: string): boolean {
|
|
58
|
-
const [base, bitsStr] = cidr.split('/');
|
|
59
|
-
const bits = Number(bitsStr);
|
|
60
|
-
const ipN = parseIpv4(ip);
|
|
61
|
-
const baseN = parseIpv4(base);
|
|
62
|
-
if (ipN === null || baseN === null || Number.isNaN(bits) || bits < 0 || bits > 32) return false;
|
|
63
|
-
if (bits === 0) return true;
|
|
64
|
-
const mask = (0xffffffff << (32 - bits)) >>> 0;
|
|
65
|
-
return (ipN & mask) >>> 0 === (baseN & mask) >>> 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
53
|
/** Classify a resolved IPv4 against the e2e topology. */
|
|
69
54
|
export function classifyIp(ip: string): ZoneClass {
|
|
70
55
|
if (parseIpv4(ip) === null) return { kind: 'invalid' };
|
|
71
56
|
for (const r of RESERVED) {
|
|
72
|
-
if (
|
|
57
|
+
if (subnetContains(r.cidr, ip)) return { kind: 'reserved', label: r.label };
|
|
73
58
|
}
|
|
74
59
|
for (const zone of Object.keys(ZONE_SUBNETS) as Zone[]) {
|
|
75
|
-
if (
|
|
60
|
+
if (subnetContains(ZONE_SUBNETS[zone], ip)) return { kind: 'zone', zone };
|
|
76
61
|
}
|
|
77
62
|
return { kind: 'public' };
|
|
78
63
|
}
|