@celilo/e2e 0.7.14 → 0.7.16

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,92 @@
1
+ /**
2
+ * Zone classification for e2e network assertions (ISS-0117, §D5).
3
+ *
4
+ * Pure functions: given a resolved IPv4, say which celilo zone it belongs to (by
5
+ * matching the topology's `ZONE_SUBNETS`), or whether it is a reserved/special-use
6
+ * range, or a globally-routable "public" address. The vantage-assertion verbs
7
+ * (`resolveFrom` / `assertResolvesInZone`, ISS-0117 §D3) build on this to enforce
8
+ * e.g. "www.<domain> resolved from internalDevice must NOT be a segmented-zone
9
+ * container IP" — the ISS-0101 / ISS-0111 bug class.
10
+ *
11
+ * Classification uses `ZONE_SUBNETS` (exact) + a reserved-range check — deliberately
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.)
16
+ */
17
+
18
+ import { type Zone, ZONE_SUBNETS } from './types';
19
+
20
+ export type ZoneClass =
21
+ | { kind: 'zone'; zone: Zone }
22
+ | { kind: 'reserved'; label: string }
23
+ | { kind: 'public' }
24
+ | { kind: 'invalid' };
25
+
26
+ /**
27
+ * Segmented zones a device on the operator's LAN (`internal`) cannot route into —
28
+ * traffic to them must go via the firewall natIp. `internal` IS the LAN, so it is
29
+ * NOT segmented (a record at an internal-zone IP is reachable).
30
+ */
31
+ export const SEGMENTED_ZONES: readonly Zone[] = ['dmz', 'app', 'secure'];
32
+
33
+ /**
34
+ * Special-use IPv4 ranges that must never be a real service address (nor appear in
35
+ * any DNS answer). RFC-1918 and RFC-6598 (CGNAT) are intentionally absent — in this
36
+ * topology the former are the managed zones and the latter is the public stand-in.
37
+ */
38
+ const RESERVED: ReadonlyArray<{ cidr: string; label: string }> = [
39
+ { cidr: '0.0.0.0/8', label: 'unspecified/this-host (incl. the 0.0.0.0 sentinel)' },
40
+ { cidr: '127.0.0.0/8', label: 'loopback' },
41
+ { cidr: '169.254.0.0/16', label: 'link-local (RFC 3927)' },
42
+ { cidr: '224.0.0.0/4', label: 'multicast' },
43
+ { cidr: '240.0.0.0/4', label: 'reserved (RFC 1112)' },
44
+ ];
45
+
46
+ /** Parse a dotted-quad IPv4 to an unsigned 32-bit int, or null if malformed. */
47
+ export function parseIpv4(ip: string): number | null {
48
+ const m = ip.trim().match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
49
+ if (!m) return null;
50
+ const octets = m.slice(1, 5).map(Number);
51
+ if (octets.some((o) => o > 255)) return null;
52
+ return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
53
+ }
54
+
55
+ /** True if `ip` falls within `cidr` (e.g. "10.0.10.0/24"). */
56
+ export function inCidr(ip: string, cidr: string): boolean {
57
+ const [base, bitsStr] = cidr.split('/');
58
+ const bits = Number(bitsStr);
59
+ const ipN = parseIpv4(ip);
60
+ const baseN = parseIpv4(base);
61
+ if (ipN === null || baseN === null || Number.isNaN(bits) || bits < 0 || bits > 32) return false;
62
+ if (bits === 0) return true;
63
+ const mask = (0xffffffff << (32 - bits)) >>> 0;
64
+ return (ipN & mask) >>> 0 === ((baseN & mask) >>> 0);
65
+ }
66
+
67
+ /** Classify a resolved IPv4 against the e2e topology. */
68
+ export function classifyIp(ip: string): ZoneClass {
69
+ if (parseIpv4(ip) === null) return { kind: 'invalid' };
70
+ for (const r of RESERVED) {
71
+ if (inCidr(ip, r.cidr)) return { kind: 'reserved', label: r.label };
72
+ }
73
+ for (const zone of Object.keys(ZONE_SUBNETS) as Zone[]) {
74
+ if (inCidr(ip, ZONE_SUBNETS[zone])) return { kind: 'zone', zone };
75
+ }
76
+ return { kind: 'public' };
77
+ }
78
+
79
+ /** True if `ip` is inside a segmented zone (dmz/app/secure) — unroutable from the LAN. */
80
+ export function isSegmentedZoneIp(ip: string): boolean {
81
+ const c = classifyIp(ip);
82
+ return c.kind === 'zone' && SEGMENTED_ZONES.includes(c.zone);
83
+ }
84
+
85
+ /**
86
+ * The "no leak past the public boundary" rule (ISS-0117 §D5; CLAUDE.md inviolable
87
+ * rule #1): a public DNS answer must be a routable public address — never a managed
88
+ * zone (incl. `internal`/RFC-1918) nor a reserved range.
89
+ */
90
+ export function isPublicLeakSafe(ip: string): boolean {
91
+ return classifyIp(ip).kind === 'public';
92
+ }
@@ -1,16 +0,0 @@
1
- $ORIGIN iamtheinternet.org.
2
- $TTL 300
3
-
4
- @ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
5
- 2024020247 300 60 604800 300
6
- )
7
-
8
- ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
- ; the e2e simulation is to model real internet connectivity, where
10
- ; private IPs are unreachable from outside. All public-facing names
11
- ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
- ; inbound to the right internal host.
13
- @ IN NS ns1.iamtheinternet.org.
14
- ns1 IN A 100.64.0.55
15
- @ IN A 0.0.0.0
16
- www IN A 100.100.0.100
@@ -1,16 +0,0 @@
1
- $ORIGIN lunacycle.net.
2
- $TTL 300
3
-
4
- @ IN SOA ns1.lunacycle.net. admin.lunacycle.net. (
5
- 2024020108 300 60 604800 300
6
- )
7
-
8
- ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
- ; the e2e simulation is to model real internet connectivity, where
10
- ; private IPs are unreachable from outside. All public-facing names
11
- ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
- ; inbound to the right internal host.
13
- @ IN NS ns1.lunacycle.net.
14
- ns1 IN A 100.64.0.55
15
- @ IN A 100.100.0.100
16
- www IN A 100.100.0.100