@celilo/e2e 0.10.0 → 0.10.2
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/config/routing/fw-ext-routes.sh +16 -1
- package/config/routing/management-routes.sh +5 -0
- package/config/routing/minio-startup.sh +9 -0
- package/config/routing/public-sim-entrypoint.sh +24 -0
- package/docker/Dockerfile.apt-repo-sim +8 -0
- package/docker/Dockerfile.cpanel-host-sim +5 -0
- package/docker/Dockerfile.minio +7 -0
- package/docker/Dockerfile.npm-registry-sim +8 -0
- package/docker/Dockerfile.registry +8 -0
- package/docker/Dockerfile.signal-release +8 -0
- package/package.json +2 -2
- package/registry-server/src/bootstrap.ts +7 -3
- package/simulators/signal-cli/server.ts +65 -9
- package/src/container-manager.ts +44 -11
- package/src/docker-compose-generator.ts +14 -14
- package/src/exit-cleanup.test.ts +56 -0
- package/src/index.ts +7 -0
- package/src/network-builder.ts +14 -0
- package/src/public-sim-routes.test.ts +117 -0
- package/src/types.ts +37 -1
|
@@ -13,7 +13,22 @@ ip route add 10.0.30.0/24 via 100.100.0.100 # secure via fw-isp
|
|
|
13
13
|
|
|
14
14
|
# IP forwarding is set via sysctls in docker-compose
|
|
15
15
|
|
|
16
|
-
# NAT/forward between isp-external and internet-external
|
|
16
|
+
# NAT/forward between isp-external and internet-external.
|
|
17
|
+
#
|
|
18
|
+
# The customer's public /24 is ROUTED, never re-NAT'd. An ISP does not NAT a
|
|
19
|
+
# subscriber that already holds a public address — the whole point of
|
|
20
|
+
# 100.100.0.100 is that it IS the customer's address on the internet. fw-ext
|
|
21
|
+
# used to MASQUERADE it anyway, so every packet leaving the fleet reached the
|
|
22
|
+
# public simulators bearing fw-ext's own 100.64.0.1.
|
|
23
|
+
#
|
|
24
|
+
# That is not cosmetic. Namecheap-style DDNS registers the SOURCE address of
|
|
25
|
+
# the update when the caller omits `ip=` (which is now how celilo registers
|
|
26
|
+
# public names — #464/#466), so the double NAT published 100.64.0.1 for every
|
|
27
|
+
# public hostname: an address that DNATs to nothing, breaking ACME validation
|
|
28
|
+
# and any inbound reach. It also silently clobbered seeded apex records.
|
|
29
|
+
# Anything that reasons about "the address the internet sees us as" was
|
|
30
|
+
# measuring the simulator's own artefact.
|
|
31
|
+
iptables -t nat -A POSTROUTING -s 100.100.0.0/24 -j RETURN
|
|
17
32
|
iptables -t nat -A POSTROUTING -j MASQUERADE
|
|
18
33
|
iptables -P FORWARD ACCEPT
|
|
19
34
|
|
|
@@ -32,6 +32,11 @@ ip route add default via "$GATEWAY"
|
|
|
32
32
|
ip route add 10.0.10.0/24 via "$FW_MAIN" # dmz via fw-main
|
|
33
33
|
ip route add 10.0.20.0/24 via "$FW_MAIN" # app via fw-main
|
|
34
34
|
ip route add 10.0.30.0/24 via "$FW_MAIN" # secure via fw-main
|
|
35
|
+
# Control plane. Only reachable when a secure-mgmt network exists — that is,
|
|
36
|
+
# when celilo-mgr lives there itself, or when a module declaring
|
|
37
|
+
# `zone: secure-mgmt` needs a system to land on (#436). Tolerated silently
|
|
38
|
+
# otherwise so the default topology is unchanged.
|
|
39
|
+
ip route add 10.0.120.0/24 via "$FW_MAIN" 2>/dev/null || true
|
|
35
40
|
|
|
36
41
|
# DNS — Technitium internal resolver (split-horizon) with public fallback.
|
|
37
42
|
# Technitium at 192.168.0.10 is authoritative for iamtheinternet.org internally,
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
# `mc mb --ignore-existing`.
|
|
8
8
|
set -e
|
|
9
9
|
|
|
10
|
+
# Route the customer's public prefix via the ISP edge (fw-ext), like every
|
|
11
|
+
# other host on internet-external. Docker's default bridge gateway has no path
|
|
12
|
+
# across networks, so without this a reply to the customer's WAN address is
|
|
13
|
+
# dropped and `backup --storage` hangs. fw-ext is per-test, so this is
|
|
14
|
+
# expected to fail while shared infra runs alone — hence the tolerance.
|
|
15
|
+
# (`busybox ip` because the MinIO image ships no iproute2.)
|
|
16
|
+
busybox ip route del default 2>/dev/null || true
|
|
17
|
+
busybox ip route add default via 100.64.0.1 2>/dev/null || true
|
|
18
|
+
|
|
10
19
|
# Serve the S3 API on :80 so the management box reaches it over the SAME
|
|
11
20
|
# proven HTTP egress path as the other public sims (apt-repo, npm-registry):
|
|
12
21
|
# fw-ext's Squid-bypass exception is scoped to dport 80/443 → 100.64.0.0/24, so
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Entrypoint wrapper for a simulator on `internet-external` — a host out on
|
|
3
|
+
# the simulated public internet.
|
|
4
|
+
#
|
|
5
|
+
# Such a host must reach the customer's public prefix (100.100.0.0/24) through
|
|
6
|
+
# the ISP edge, fw-ext. Docker's default bridge gateway has no path across
|
|
7
|
+
# networks, so without this route a reply to the customer's WAN address is
|
|
8
|
+
# simply dropped and the connection hangs.
|
|
9
|
+
#
|
|
10
|
+
# This used to be invisible: fw-ext MASQUERADEd the customer to its own
|
|
11
|
+
# 100.64.0.1, which is on-link here, so replies never needed routing at all.
|
|
12
|
+
# Once fw-ext stopped re-NATing a subscriber that already holds a public
|
|
13
|
+
# address (the ISP routes it; it does not NAT it), the omission surfaced —
|
|
14
|
+
# every sim that had this route already kept working, and the four that did
|
|
15
|
+
# not started timing out. celilo-website-sim, isitup, pebble, namecheap-dns
|
|
16
|
+
# and the DNS hierarchy have carried it all along; this is the same two lines.
|
|
17
|
+
#
|
|
18
|
+
# fw-ext is per-test, so the route is unreachable while shared infra runs
|
|
19
|
+
# alone. That is harmless — nothing dials the customer before a test brings
|
|
20
|
+
# fw-ext up — hence the tolerant `|| true`.
|
|
21
|
+
ip route del default 2>/dev/null || true
|
|
22
|
+
ip route add default via 100.64.0.1 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
exec "$@"
|
|
@@ -49,8 +49,16 @@ RUN set -eux; \
|
|
|
49
49
|
-o APT::FTPArchive::Release::Architectures="amd64 arm64" \
|
|
50
50
|
release dists/stable > dists/stable/Release
|
|
51
51
|
|
|
52
|
+
# Route the customer's public prefix via the ISP edge, like every other host
|
|
53
|
+
# on internet-external. See config/routing/public-sim-entrypoint.sh.
|
|
54
|
+
RUN apt-get update \
|
|
55
|
+
&& apt-get install -y --no-install-recommends iproute2 \
|
|
56
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
57
|
+
COPY config/routing/public-sim-entrypoint.sh /usr/local/bin/public-sim-entrypoint.sh
|
|
58
|
+
|
|
52
59
|
EXPOSE 80
|
|
53
60
|
|
|
54
61
|
# python3's http.server is enough for apt (GET + range requests over static
|
|
55
62
|
# files). No special MIME/headers needed.
|
|
63
|
+
ENTRYPOINT ["/bin/sh", "/usr/local/bin/public-sim-entrypoint.sh"]
|
|
56
64
|
CMD ["python3", "-m", "http.server", "80", "--directory", "/repo"]
|
|
@@ -67,4 +67,9 @@ EXPOSE 7822 80 443
|
|
|
67
67
|
# which is what we want from a fixture host. The runtime/lock dirs live under
|
|
68
68
|
# /var/run, which the container wipes at start, so recreate them before apache
|
|
69
69
|
# reads DefaultRuntimeDir.
|
|
70
|
+
# Route the customer's public prefix via the ISP edge, like every other host
|
|
71
|
+
# on internet-external. See config/routing/public-sim-entrypoint.sh.
|
|
72
|
+
COPY config/routing/public-sim-entrypoint.sh /usr/local/bin/public-sim-entrypoint.sh
|
|
73
|
+
|
|
74
|
+
ENTRYPOINT ["/bin/sh", "/usr/local/bin/public-sim-entrypoint.sh"]
|
|
70
75
|
CMD ["/bin/sh", "-c", "mkdir -p /var/run/apache2 /var/lock/apache2 /run/sshd && /usr/sbin/sshd && . /etc/apache2/envvars && exec apache2 -D FOREGROUND"]
|
package/docker/Dockerfile.minio
CHANGED
|
@@ -19,8 +19,15 @@
|
|
|
19
19
|
|
|
20
20
|
FROM minio/mc:latest AS mc
|
|
21
21
|
|
|
22
|
+
# Static busybox purely for its `ip` applet: the MinIO image has no iproute2
|
|
23
|
+
# and no package manager worth invoking, but it still needs the ISP route
|
|
24
|
+
# every host on internet-external carries (see minio-startup.sh).
|
|
25
|
+
FROM busybox:stable-musl AS busybox
|
|
26
|
+
|
|
22
27
|
FROM minio/minio:latest
|
|
23
28
|
|
|
29
|
+
COPY --from=busybox /bin/busybox /usr/local/bin/busybox
|
|
30
|
+
|
|
24
31
|
# Bring the MinIO client in from the mc image so the entrypoint can create the
|
|
25
32
|
# bucket once the server is up.
|
|
26
33
|
COPY --from=mc /usr/bin/mc /usr/bin/mc
|
|
@@ -22,5 +22,13 @@ ENV PACKUMENT_DIR=/var/lib/npm \
|
|
|
22
22
|
PORT=80 \
|
|
23
23
|
REGISTRY_URL=http://npm-registry.lab
|
|
24
24
|
|
|
25
|
+
# Route the customer's public prefix via the ISP edge, like every other host
|
|
26
|
+
# on internet-external. See config/routing/public-sim-entrypoint.sh.
|
|
27
|
+
RUN apt-get update \
|
|
28
|
+
&& apt-get install -y --no-install-recommends iproute2 \
|
|
29
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
30
|
+
COPY config/routing/public-sim-entrypoint.sh /usr/local/bin/public-sim-entrypoint.sh
|
|
31
|
+
|
|
25
32
|
EXPOSE 80
|
|
33
|
+
ENTRYPOINT ["/bin/sh", "/usr/local/bin/public-sim-entrypoint.sh"]
|
|
26
34
|
CMD ["bun", "run", "src/server.ts"]
|
|
@@ -30,5 +30,13 @@ ENV DATA_DIR=/var/lib/celilo-registry \
|
|
|
30
30
|
# somewhere to put files before the server runs.
|
|
31
31
|
RUN mkdir -p /uploads
|
|
32
32
|
|
|
33
|
+
# Route the customer's public prefix via the ISP edge, like every other host
|
|
34
|
+
# on internet-external. See config/routing/public-sim-entrypoint.sh.
|
|
35
|
+
RUN apt-get update \
|
|
36
|
+
&& apt-get install -y --no-install-recommends iproute2 \
|
|
37
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
38
|
+
COPY config/routing/public-sim-entrypoint.sh /usr/local/bin/public-sim-entrypoint.sh
|
|
39
|
+
|
|
33
40
|
EXPOSE 80
|
|
41
|
+
ENTRYPOINT ["/bin/sh", "/usr/local/bin/public-sim-entrypoint.sh"]
|
|
34
42
|
CMD ["bun", "run", "src/index.ts"]
|
|
@@ -33,4 +33,12 @@ RUN mkdir -p "/out/v${SIGNAL_CLI_VERSION}" \
|
|
|
33
33
|
|
|
34
34
|
FROM nginx:alpine
|
|
35
35
|
COPY --from=fetch /out /usr/share/nginx/html
|
|
36
|
+
|
|
37
|
+
# Route the customer's public prefix via the ISP edge, like every other host on
|
|
38
|
+
# internet-external. See config/routing/public-sim-entrypoint.sh. (Alpine's
|
|
39
|
+
# busybox already provides `ip`, so nothing to install.)
|
|
40
|
+
COPY config/routing/public-sim-entrypoint.sh /usr/local/bin/public-sim-entrypoint.sh
|
|
41
|
+
|
|
36
42
|
EXPOSE 80
|
|
43
|
+
ENTRYPOINT ["/bin/sh", "/usr/local/bin/public-sim-entrypoint.sh"]
|
|
44
|
+
CMD ["nginx", "-g", "daemon off;"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@celilo/cli-display": "^0.1.9",
|
|
41
|
-
"@celilo/event-bus": "^0.1.
|
|
41
|
+
"@celilo/event-bus": "^0.1.9",
|
|
42
42
|
"yaml": "^2.8.0",
|
|
43
43
|
"zod": "^3.24.1"
|
|
44
44
|
},
|
|
@@ -129,11 +129,15 @@ export function bootstrapIndexEntry(entry: BootstrapEntry): IndexEntry {
|
|
|
129
129
|
* `walkDir`'s output conforms to the canonical rule. `walkDir` is exported for
|
|
130
130
|
* that test.
|
|
131
131
|
*/
|
|
132
|
-
export function walkDir(dir: string, base = dir): string[] {
|
|
132
|
+
export function walkDir(dir: string, base = dir, inNodeModules = false): string[] {
|
|
133
133
|
const results: string[] = [];
|
|
134
134
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
135
135
|
if (entry.name.startsWith('.')) continue;
|
|
136
136
|
if (entry.name.endsWith('.test.ts')) continue;
|
|
137
|
+
// The module's own tsconfig.json is dev-only (CI typechecks hooks with it);
|
|
138
|
+
// build.ts drops it too. A dependency's tsconfig.json inside node_modules is
|
|
139
|
+
// that package's published content and stays — build.ts keeps those as well.
|
|
140
|
+
if (entry.name === 'tsconfig.json' && !inNodeModules) continue;
|
|
137
141
|
|
|
138
142
|
const fullPath = join(dir, entry.name);
|
|
139
143
|
|
|
@@ -152,14 +156,14 @@ export function walkDir(dir: string, base = dir): string[] {
|
|
|
152
156
|
// Other node_modules: collect only @celilo/capabilities.
|
|
153
157
|
const capDir = join(fullPath, '@celilo', 'capabilities');
|
|
154
158
|
if (existsSync(capDir)) {
|
|
155
|
-
results.push(...walkDir(capDir, base));
|
|
159
|
+
results.push(...walkDir(capDir, base, true));
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
continue;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
if (entry.isDirectory()) {
|
|
162
|
-
results.push(...walkDir(fullPath, base));
|
|
166
|
+
results.push(...walkDir(fullPath, base, inNodeModules));
|
|
163
167
|
} else {
|
|
164
168
|
results.push(relative(base, fullPath));
|
|
165
169
|
}
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* that checks `response.ok` believes a rejected send succeeded;
|
|
10
10
|
* - an unlinked/revoked account answers HTTP perfectly and can send nothing;
|
|
11
11
|
* - `receive` returns receipts and typing indicators alongside real
|
|
12
|
-
* messages, and those are not replies
|
|
12
|
+
* messages, and those are not replies;
|
|
13
|
+
* - `receive` is REFUSED unless the daemon was started with
|
|
14
|
+
* `--receive-mode=manual`, which is the real default and the reason the
|
|
15
|
+
* ack path shipped broken.
|
|
13
16
|
*
|
|
14
17
|
* It pushes back rather than saying yes. Sending to a number that has not been
|
|
15
18
|
* registered with the simulator fails the way Signal fails.
|
|
@@ -36,6 +39,23 @@
|
|
|
36
39
|
const ACCOUNT = process.env.SIGNAL_ACCOUNT ?? '+15551234567';
|
|
37
40
|
const PORT = Number(process.env.SIGNAL_RPC_PORT ?? 8080);
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Mirrors `signal-cli daemon --receive-mode=<mode>`, and DEFAULTS TO THE REAL
|
|
44
|
+
* DEFAULT (`on-start`) rather than to the convenient one.
|
|
45
|
+
*
|
|
46
|
+
* In on-start the daemon registers a strong receive handler, drains every
|
|
47
|
+
* inbound message into its own SSE stream, and REFUSES the `receive` JSON-RPC
|
|
48
|
+
* call outright. A simulator that answers `receive` regardless is a placebo
|
|
49
|
+
* for the whole ack path: that is precisely how the deployed module shipped
|
|
50
|
+
* with an unreadable transport while this suite stayed green. Only `manual`
|
|
51
|
+
* lets `receive` work — so a caller has to be started the way it must be
|
|
52
|
+
* deployed.
|
|
53
|
+
*
|
|
54
|
+
* Verified in signal-cli 0.14.6: DaemonCommand.java:78, :114;
|
|
55
|
+
* ManagerImpl.java:1494-1501, :1605; ReceiveCommand.java:125.
|
|
56
|
+
*/
|
|
57
|
+
const RECEIVE_MODE = process.env.SIGNAL_RECEIVE_MODE ?? 'on-start';
|
|
58
|
+
|
|
39
59
|
/** Numbers the simulator will accept as recipients. Anything else is rejected. */
|
|
40
60
|
const KNOWN_RECIPIENTS = new Set(
|
|
41
61
|
(process.env.SIGNAL_KNOWN_RECIPIENTS ?? '')
|
|
@@ -126,6 +146,18 @@ function handleSend(id: unknown, params: Record<string, unknown>): Response {
|
|
|
126
146
|
* will misbehave on them.
|
|
127
147
|
*/
|
|
128
148
|
function handleReceive(id: unknown): Response {
|
|
149
|
+
// The refusal comes BEFORE the link check, as it does in the real daemon:
|
|
150
|
+
// the receive thread is started per account at daemon start, so an
|
|
151
|
+
// already-receiving manager rejects the call without ever looking at what
|
|
152
|
+
// is queued. Verbatim message from ReceiveCommand.java:125.
|
|
153
|
+
if (RECEIVE_MODE !== 'manual') {
|
|
154
|
+
return rpcError(
|
|
155
|
+
id,
|
|
156
|
+
-32001,
|
|
157
|
+
'Receive command cannot be used if messages are already being received.',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
if (!linked) {
|
|
130
162
|
return rpcError(id, -32000, 'Account is not registered or the device link was removed');
|
|
131
163
|
}
|
|
@@ -142,13 +174,35 @@ function handleReceive(id: unknown): Response {
|
|
|
142
174
|
|
|
143
175
|
while (inboundQueue.length > 0) {
|
|
144
176
|
const item = inboundQueue.shift() as QueuedInbound;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
// A reply from the linked account itself — the default single-operator
|
|
178
|
+
// setup, where the route points at the number the transport is a
|
|
179
|
+
// secondary device of — is a NOTE-TO-SELF, and the real daemon delivers
|
|
180
|
+
// it as a sync transcript, never as a dataMessage. Emitting it as a
|
|
181
|
+
// dataMessage would let a client that only reads dataMessage look
|
|
182
|
+
// correct here while dropping every reply in production (#460).
|
|
183
|
+
envelopes.push(
|
|
184
|
+
item.from === ACCOUNT
|
|
185
|
+
? {
|
|
186
|
+
envelope: {
|
|
187
|
+
sourceNumber: ACCOUNT,
|
|
188
|
+
timestamp: nextTimestamp(),
|
|
189
|
+
syncMessage: {
|
|
190
|
+
sentMessage: {
|
|
191
|
+
destinationNumber: ACCOUNT,
|
|
192
|
+
timestamp: item.timestamp,
|
|
193
|
+
message: item.body,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {
|
|
199
|
+
envelope: {
|
|
200
|
+
sourceNumber: item.from,
|
|
201
|
+
timestamp: item.timestamp,
|
|
202
|
+
dataMessage: { message: item.body },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
);
|
|
152
206
|
console.log(`[signal-sim] <- ${item.from}: ${item.body}`);
|
|
153
207
|
}
|
|
154
208
|
|
|
@@ -261,7 +315,9 @@ Bun.serve({
|
|
|
261
315
|
},
|
|
262
316
|
});
|
|
263
317
|
|
|
264
|
-
console.log(
|
|
318
|
+
console.log(
|
|
319
|
+
`[signal-sim] signal-cli JSON-RPC simulator on :${PORT} as ${ACCOUNT} (receive-mode=${RECEIVE_MODE})`,
|
|
320
|
+
);
|
|
265
321
|
if (KNOWN_RECIPIENTS.size > 0) {
|
|
266
322
|
console.log(`[signal-sim] known recipients: ${[...KNOWN_RECIPIENTS].join(', ')}`);
|
|
267
323
|
}
|
package/src/container-manager.ts
CHANGED
|
@@ -4,7 +4,11 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { basename, join, resolve } from 'node:path';
|
|
5
5
|
import { startBrowser } from './browser';
|
|
6
6
|
import { MIN_CLI_VERSION, checkCliVersion } from './cli-version-contract';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
SHARED_PROJECT_NAME,
|
|
9
|
+
generateTestComposeYaml,
|
|
10
|
+
getAllMachines,
|
|
11
|
+
} from './docker-compose-generator';
|
|
8
12
|
import { ensureSharedInfra } from './shared-infra';
|
|
9
13
|
import { SIMULATOR_IPS } from './simulator-ips';
|
|
10
14
|
import { startSocksProxy } from './socks-proxy';
|
|
@@ -67,8 +71,16 @@ const activeProjects: Set<string> = new Set();
|
|
|
67
71
|
*
|
|
68
72
|
* NOTE: only tears down per-test projects. Shared infra is managed
|
|
69
73
|
* separately (by the test runner or explicit stopSharedInfra call).
|
|
74
|
+
*
|
|
75
|
+
* MUST stay fully synchronous: it runs from an `exit` handler, where the
|
|
76
|
+
* event loop is already closed and any async work is silently dropped.
|
|
70
77
|
*/
|
|
71
78
|
function cleanupOnExit() {
|
|
79
|
+
// `--keep` / `--reuse` mean the operator wants the stack to survive for
|
|
80
|
+
// debugging — and they want it MOST after a failure, which is exactly the
|
|
81
|
+
// path this handler covers. Tearing down here would delete the evidence.
|
|
82
|
+
if (process.env.CELILO_E2E_KEEP === '1' || process.env.CELILO_E2E_REUSE === '1') return;
|
|
83
|
+
|
|
72
84
|
for (const project of activeProjects) {
|
|
73
85
|
try {
|
|
74
86
|
execSync(`docker compose -p ${project} down --volumes --remove-orphans`, {
|
|
@@ -77,6 +89,7 @@ function cleanupOnExit() {
|
|
|
77
89
|
});
|
|
78
90
|
} catch {}
|
|
79
91
|
}
|
|
92
|
+
activeProjects.clear();
|
|
80
93
|
try {
|
|
81
94
|
execSync('docker network prune -f', { timeout: 10_000, stdio: 'pipe' });
|
|
82
95
|
} catch {}
|
|
@@ -90,7 +103,25 @@ process.on('SIGINT', () => {
|
|
|
90
103
|
cleanupOnExit();
|
|
91
104
|
process.exit(1);
|
|
92
105
|
});
|
|
93
|
-
process.
|
|
106
|
+
// `exit`, NOT `beforeExit`: the runner always terminates via `process.exit()`
|
|
107
|
+
// (runner.ts ends with `process.exit(failed > 0 ? 1 : 0)`), and `beforeExit`
|
|
108
|
+
// does not fire for an explicit exit — nor after an uncaught throw. So on the
|
|
109
|
+
// FAILURE path, where the test never reaches `handle.stop()`, teardown was
|
|
110
|
+
// never running and the per-test networks leaked.
|
|
111
|
+
//
|
|
112
|
+
// That leak is not confined to the run that caused it. The sim's networks use
|
|
113
|
+
// the same subnets as a real celilo fleet (dmz 10.0.10.0/24, app 10.0.20.0/24,
|
|
114
|
+
// …), so on a host that lives in those subnets — celilo's own forgejo-builder
|
|
115
|
+
// does — a leaked bridge leaves a DUPLICATE route for a production prefix.
|
|
116
|
+
// Traffic from any later container to the real host at that address is routed
|
|
117
|
+
// into the dead simulated network and blackholed. That took out three
|
|
118
|
+
// consecutive release runs, each dying in `git fetch` after a ~132s connect
|
|
119
|
+
// timeout, long after the e2e run that stranded the network had ended.
|
|
120
|
+
//
|
|
121
|
+
// The start-of-run prefix sweep stays as the crash-recovery backstop (it is
|
|
122
|
+
// the only thing that can clean up after SIGKILL); this makes the common case
|
|
123
|
+
// clean up after itself instead of poisoning the next job. See #508.
|
|
124
|
+
process.on('exit', () => {
|
|
94
125
|
cleanupOnExit();
|
|
95
126
|
});
|
|
96
127
|
|
|
@@ -829,13 +860,11 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
829
860
|
);
|
|
830
861
|
}
|
|
831
862
|
|
|
832
|
-
// Wait for all target machines to complete setup (systemd boot + routing)
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
...config.internalMachines,
|
|
838
|
-
];
|
|
863
|
+
// Wait for all target machines to complete setup (systemd boot + routing).
|
|
864
|
+
// Via getAllMachines so a newly-supported zone cannot be silently skipped —
|
|
865
|
+
// this list was hand-maintained and omitted `secure-mgmt`, so the first
|
|
866
|
+
// machine placed there raced `machine add` and failed on SSH intermittently.
|
|
867
|
+
const allMachines = getAllMachines(config);
|
|
839
868
|
if (allMachines.length > 0) {
|
|
840
869
|
console.log('[progress:start] waiting for target machines | target machines ready');
|
|
841
870
|
}
|
|
@@ -936,8 +965,12 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
936
965
|
network.internal.subnet=${ZONE_SUBNETS.internal} \
|
|
937
966
|
network.internal.gateway=${ZONE_GATEWAYS.internal} \
|
|
938
967
|
${
|
|
939
|
-
|
|
940
|
-
|
|
968
|
+
// The control plane needs its subnet declared if celilo-mgr lives
|
|
969
|
+
// there OR if a machine does — infrastructure selection for a module
|
|
970
|
+
// declaring `zone: secure-mgmt` reads it either way (#436).
|
|
971
|
+
config.managementZone === 'secure-mgmt' ||
|
|
972
|
+
(config.secureMgmtMachines ?? []).length > 0
|
|
973
|
+
? `network.secure-mgmt.subnet=${ZONE_SUBNETS['secure-mgmt']} network.secure-mgmt.gateway=${ZONE_GATEWAYS['secure-mgmt']} `
|
|
941
974
|
: ''
|
|
942
975
|
}dns.primary=100.100.0.1 \
|
|
943
976
|
dns.fallback=1.0.0.1,8.8.8.8`,
|
|
@@ -374,6 +374,10 @@ export function generateSharedInfraYaml(): string {
|
|
|
374
374
|
*/
|
|
375
375
|
export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: string): string {
|
|
376
376
|
const mgmtOnOwnNetwork = config.managementZone === 'secure-mgmt';
|
|
377
|
+
// The control-plane network exists if celilo-mgr lives there OR if any
|
|
378
|
+
// machine does — a module declaring `zone: secure-mgmt` needs somewhere to
|
|
379
|
+
// land whether or not the management box shares the network (#436).
|
|
380
|
+
const needsSecureMgmt = mgmtOnOwnNetwork || (config.secureMgmtMachines ?? []).length > 0;
|
|
377
381
|
const networks: Record<string, unknown> = {
|
|
378
382
|
internal: networkDef('192.168.0.0/24', '192.168.0.250'),
|
|
379
383
|
dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
|
|
@@ -381,7 +385,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
381
385
|
secure: networkDef('10.0.30.0/24', '10.0.30.250'),
|
|
382
386
|
// Only when celilo-mgr lives off the internal LAN — keeps the default
|
|
383
387
|
// topology's generated compose byte-identical for every existing test.
|
|
384
|
-
...(
|
|
388
|
+
...(needsSecureMgmt
|
|
385
389
|
? { 'secure-mgmt': networkDef(SECURE_MGMT_SUBNET, '10.0.120.250') }
|
|
386
390
|
: {}),
|
|
387
391
|
'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
|
|
@@ -506,7 +510,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
506
510
|
// so the management box is routed like any other segmented network rather than
|
|
507
511
|
// being stranded. Applied as a post-step so the default topology's generated
|
|
508
512
|
// compose is untouched.
|
|
509
|
-
if (
|
|
513
|
+
if (needsSecureMgmt) {
|
|
510
514
|
const fwMain = services['fw-main'] as { networks?: Record<string, unknown> } | undefined;
|
|
511
515
|
if (fwMain?.networks) {
|
|
512
516
|
fwMain.networks['secure-mgmt'] = { ipv4_address: SECURE_MGMT_GATEWAY };
|
|
@@ -535,12 +539,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
535
539
|
});
|
|
536
540
|
|
|
537
541
|
// --- Dynamic test machines ---
|
|
538
|
-
const allMachines =
|
|
539
|
-
...config.dmzMachines,
|
|
540
|
-
...config.appMachines,
|
|
541
|
-
...config.secureMachines,
|
|
542
|
-
...config.internalMachines,
|
|
543
|
-
];
|
|
542
|
+
const allMachines = getAllMachines(config);
|
|
544
543
|
|
|
545
544
|
for (const machine of allMachines) {
|
|
546
545
|
const zoneNetwork = machine.zone;
|
|
@@ -614,6 +613,11 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
614
613
|
environment: {
|
|
615
614
|
SIGNAL_ACCOUNT: '+15551234567',
|
|
616
615
|
SIGNAL_KNOWN_RECIPIENTS: '+15550001,+15550002',
|
|
616
|
+
// Mirrors the flag the module's systemd unit must pass. Without it the
|
|
617
|
+
// simulator refuses `receive`, exactly as the real daemon does — which
|
|
618
|
+
// is the point: a test may only read replies from a daemon started the
|
|
619
|
+
// way celilo has to deploy it.
|
|
620
|
+
SIGNAL_RECEIVE_MODE: 'manual',
|
|
617
621
|
},
|
|
618
622
|
});
|
|
619
623
|
}
|
|
@@ -795,12 +799,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
795
799
|
volumes: getRegistryVolumes(),
|
|
796
800
|
});
|
|
797
801
|
|
|
798
|
-
const allMachines =
|
|
799
|
-
...config.dmzMachines,
|
|
800
|
-
...config.appMachines,
|
|
801
|
-
...config.secureMachines,
|
|
802
|
-
...config.internalMachines,
|
|
803
|
-
];
|
|
802
|
+
const allMachines = getAllMachines(config);
|
|
804
803
|
|
|
805
804
|
for (const machine of allMachines) {
|
|
806
805
|
const zoneNetwork = machine.zone;
|
|
@@ -847,5 +846,6 @@ export function getAllMachines(config: NetworkConfig): MachineSpec[] {
|
|
|
847
846
|
...config.appMachines,
|
|
848
847
|
...config.secureMachines,
|
|
849
848
|
...config.internalMachines,
|
|
849
|
+
...(config.secureMgmtMachines ?? []),
|
|
850
850
|
];
|
|
851
851
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recurrence gate for #508: a failed e2e run must not leak its Docker networks.
|
|
3
|
+
*
|
|
4
|
+
* The sim's networks use the same subnets as a real celilo fleet, so on a host
|
|
5
|
+
* that lives in those subnets (celilo's forgejo-builder does) a stranded bridge
|
|
6
|
+
* leaves a duplicate route for a production prefix and blackholes later
|
|
7
|
+
* containers' traffic to the real address. Three consecutive release runs died
|
|
8
|
+
* that way — in `git fetch`, ~132s connect timeout — well after the e2e run
|
|
9
|
+
* that stranded the network had finished.
|
|
10
|
+
*
|
|
11
|
+
* The leak's mechanism was a wiring bug, so the gate is on the wiring.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { expect, test } from 'bun:test';
|
|
18
|
+
|
|
19
|
+
const SRC = readFileSync(
|
|
20
|
+
join(dirname(fileURLToPath(import.meta.url)), 'container-manager.ts'),
|
|
21
|
+
'utf-8',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
test("teardown is wired to 'exit', which is the event an explicit process.exit() fires", () => {
|
|
25
|
+
// `beforeExit` alone was the bug: the runner ends with
|
|
26
|
+
// `process.exit(failed > 0 ? 1 : 0)`, so on the failure path — where the test
|
|
27
|
+
// never reaches handle.stop() — nothing ever tore the network down.
|
|
28
|
+
expect(SRC).toContain("process.on('exit'");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('cleanup honors --keep / --reuse so a failed run stays debuggable', () => {
|
|
32
|
+
// Without this guard, adding the `exit` hook would destroy exactly the stack
|
|
33
|
+
// an operator asked to keep, since --keep matters most after a failure.
|
|
34
|
+
const body = SRC.slice(
|
|
35
|
+
SRC.indexOf('function cleanupOnExit'),
|
|
36
|
+
SRC.indexOf('process.on(\'SIGTERM\''),
|
|
37
|
+
);
|
|
38
|
+
expect(body).toContain('CELILO_E2E_KEEP');
|
|
39
|
+
expect(body).toContain('CELILO_E2E_REUSE');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the premise holds: process.exit() fires 'exit' but NOT 'beforeExit'", async () => {
|
|
43
|
+
// The whole fix rests on this runtime behavior. If it ever changes, the
|
|
44
|
+
// reasoning above is void and this should fail loudly rather than silently
|
|
45
|
+
// leaving the leak unfixed.
|
|
46
|
+
const proc = Bun.spawn([
|
|
47
|
+
'bun',
|
|
48
|
+
'-e',
|
|
49
|
+
"process.on('beforeExit', () => console.log('BEFORE_EXIT')); process.on('exit', () => console.log('EXIT')); process.exit(0);",
|
|
50
|
+
]);
|
|
51
|
+
const out = await new Response(proc.stdout).text();
|
|
52
|
+
await proc.exited;
|
|
53
|
+
|
|
54
|
+
expect(out).toContain('EXIT');
|
|
55
|
+
expect(out).not.toContain('BEFORE_EXIT');
|
|
56
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,8 @@ export type {
|
|
|
48
48
|
} from './types';
|
|
49
49
|
export {
|
|
50
50
|
CeliloCommandError,
|
|
51
|
+
externalWanIp,
|
|
52
|
+
externalWanSubnet,
|
|
51
53
|
greenwaveRouterIp,
|
|
52
54
|
internalNatIp,
|
|
53
55
|
internalResolverIp,
|
|
@@ -84,6 +86,11 @@ export {
|
|
|
84
86
|
TECHNITIUM_DHCP_STACK,
|
|
85
87
|
} from './fixtures';
|
|
86
88
|
|
|
89
|
+
// Simulator addresses on `internet-external` — canonical for code, so a test
|
|
90
|
+
// asserting against the authoritative DNS server never hardcodes its IP.
|
|
91
|
+
export { SIMULATOR_IPS, SIMULATOR_IP_ENTRIES } from './simulator-ips';
|
|
92
|
+
export type { SimulatorIpName } from './simulator-ips';
|
|
93
|
+
|
|
87
94
|
// Shared infrastructure management (for test runners)
|
|
88
95
|
export { ensureSharedInfra, stopSharedInfra, isSharedInfraRunning } from './shared-infra';
|
|
89
96
|
|
package/src/network-builder.ts
CHANGED
|
@@ -38,6 +38,7 @@ export class NetworkBuilder {
|
|
|
38
38
|
appMachines: [],
|
|
39
39
|
secureMachines: [],
|
|
40
40
|
internalMachines: [],
|
|
41
|
+
secureMgmtMachines: [],
|
|
41
42
|
dhcpClient: false,
|
|
42
43
|
signalCli: false,
|
|
43
44
|
signalSim: false,
|
|
@@ -94,6 +95,19 @@ export class NetworkBuilder {
|
|
|
94
95
|
return this.addMachines(this.config.internalMachines, 'internal', machines);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Add machines on celilo's control plane (`secure-mgmt`).
|
|
100
|
+
*
|
|
101
|
+
* For a module whose manifest declares `zone: secure-mgmt` — the signal
|
|
102
|
+
* transport does, because it holds the operator's Signal identity and must
|
|
103
|
+
* depend on nothing it reports about. Without this the module deploys onto
|
|
104
|
+
* whatever zone a test happened to add a machine in, which is the placement
|
|
105
|
+
* defect #435 is about, or fails selection outright.
|
|
106
|
+
*/
|
|
107
|
+
secureMgmt(machines: Record<string, string>): this {
|
|
108
|
+
return this.addMachines(this.config.secureMgmtMachines, 'secure-mgmt', machines);
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
/** Add internal-zone machines that need the Docker-capable image (e.g., Technitium) */
|
|
98
112
|
internalDocker(machines: Record<string, string>): this {
|
|
99
113
|
return this.addMachines(this.config.internalMachines, 'internal', machines, { docker: true });
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recurrence gate: every simulator on `internet-external` must route the
|
|
3
|
+
* customer's public prefix via the ISP edge.
|
|
4
|
+
*
|
|
5
|
+
* `internet-external` models the public internet. A host there replies to a
|
|
6
|
+
* customer's public address through its ISP — modelled by defaulting via
|
|
7
|
+
* fw-ext (100.64.0.1). Docker's bridge gateway has no path across networks, so
|
|
8
|
+
* a simulator without that route silently blackholes every reply.
|
|
9
|
+
*
|
|
10
|
+
* This was invisible for as long as fw-ext MASQUERADEd the customer to its own
|
|
11
|
+
* on-link 100.64.0.1: nothing needed routing, so five simulators shipped
|
|
12
|
+
* without it. Removing that second NAT (an ISP routes a subscriber's public
|
|
13
|
+
* prefix, it does not NAT it) turned the omission into a hang — first in
|
|
14
|
+
* `bun add -g` against the sim npm registry, then in the signal module's
|
|
15
|
+
* release download. Both cost a full build-and-run cycle to find.
|
|
16
|
+
*
|
|
17
|
+
* A static check because the honest runtime one is impossible here: fw-ext is
|
|
18
|
+
* per-test, so at shared-infra readiness the route's next hop does not exist
|
|
19
|
+
* yet. Reading the compose the generator actually emits (rather than a
|
|
20
|
+
* hand-kept list) means a NEW simulator is covered the day it is added.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, expect, test } from 'bun:test';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { parse } from 'yaml';
|
|
27
|
+
import { generateSharedInfraYaml, generateTestComposeYaml } from './docker-compose-generator';
|
|
28
|
+
import type { NetworkConfig } from './types';
|
|
29
|
+
|
|
30
|
+
/** fw-ext's address on internet-external — the ISP edge every public host exits by. */
|
|
31
|
+
const ISP_EDGE = '100.64.0.1';
|
|
32
|
+
|
|
33
|
+
/** The one container that IS the edge, so it cannot route through itself. */
|
|
34
|
+
const EDGE_SERVICE = 'fw-ext';
|
|
35
|
+
|
|
36
|
+
interface ComposeService {
|
|
37
|
+
build?: { dockerfile?: string };
|
|
38
|
+
networks?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Every optional simulator switched on, so none escapes the sweep. */
|
|
42
|
+
const EVERYTHING: NetworkConfig = {
|
|
43
|
+
topology: 'default',
|
|
44
|
+
dmzMachines: [],
|
|
45
|
+
appMachines: [],
|
|
46
|
+
secureMachines: [],
|
|
47
|
+
internalMachines: [],
|
|
48
|
+
secureMgmtMachines: [],
|
|
49
|
+
domain: 'iamtheinternet.org',
|
|
50
|
+
ddnsPassword: 'test123',
|
|
51
|
+
verifyRouting: false,
|
|
52
|
+
managementVolumes: [],
|
|
53
|
+
dhcpClient: true,
|
|
54
|
+
signalCli: true,
|
|
55
|
+
signalSim: true,
|
|
56
|
+
signalRelease: true,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function publicSimulators(yaml: string): Array<{ name: string; dockerfile: string }> {
|
|
60
|
+
const compose = parse(yaml) as { services?: Record<string, ComposeService> };
|
|
61
|
+
return Object.entries(compose.services ?? {})
|
|
62
|
+
.filter(([name, svc]) => name !== EDGE_SERVICE && svc.networks?.['internet-external'])
|
|
63
|
+
.map(([name, svc]) => ({ name, dockerfile: svc.build?.dockerfile ?? '' }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True if the image establishes the route — either through the shared
|
|
68
|
+
* entrypoint or by naming the ISP edge itself (a few sims predate the shared
|
|
69
|
+
* script and do it inline in their own entrypoint).
|
|
70
|
+
*/
|
|
71
|
+
function establishesIspRoute(dockerfile: string): boolean {
|
|
72
|
+
const e2eDir = join(import.meta.dir, '..');
|
|
73
|
+
const text = readFileSync(join(e2eDir, dockerfile), 'utf-8');
|
|
74
|
+
if (text.includes('public-sim-entrypoint.sh') || text.includes(ISP_EDGE)) return true;
|
|
75
|
+
|
|
76
|
+
// Otherwise the route may live in a script the image copies in. Follow every
|
|
77
|
+
// COPY source that looks like a script and check those too.
|
|
78
|
+
for (const match of text.matchAll(/^COPY\s+(?:--from=\S+\s+)?(\S+\.sh)\s/gm)) {
|
|
79
|
+
try {
|
|
80
|
+
if (readFileSync(join(e2eDir, match[1]), 'utf-8').includes(ISP_EDGE)) return true;
|
|
81
|
+
} catch {
|
|
82
|
+
// A COPY source outside the build context is not a route carrier.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Some sims ship their entrypoint in a simulators/<name>/ directory copied
|
|
87
|
+
// wholesale; check the obvious sibling.
|
|
88
|
+
for (const match of text.matchAll(/^COPY\s+(?:--from=\S+\s+)?(simulators\/\S+)\s/gm)) {
|
|
89
|
+
try {
|
|
90
|
+
if (readFileSync(join(e2eDir, match[1], 'entrypoint.sh'), 'utf-8').includes(ISP_EDGE)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Not an entrypoint-bearing directory.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe('public simulators route back through the ISP edge', () => {
|
|
101
|
+
const simulators = [
|
|
102
|
+
...publicSimulators(generateSharedInfraYaml()),
|
|
103
|
+
...publicSimulators(generateTestComposeYaml(EVERYTHING)),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
test('the sweep actually found simulators', () => {
|
|
107
|
+
// Guard against the check silently passing because the filter broke.
|
|
108
|
+
expect(simulators.length).toBeGreaterThan(5);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const { name, dockerfile } of simulators) {
|
|
112
|
+
test(`${name} routes the customer prefix via ${ISP_EDGE}`, () => {
|
|
113
|
+
expect(dockerfile).not.toBe('');
|
|
114
|
+
expect(establishesIspRoute(dockerfile)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A placement zone a machine can be brought up in.
|
|
3
|
+
*
|
|
4
|
+
* `secure-mgmt` is celilo's own control plane, not a data-plane tier. It was
|
|
5
|
+
* expressible for the management box alone until a module declared
|
|
6
|
+
* `zone: secure-mgmt` (#436, the signal transport) and infrastructure
|
|
7
|
+
* selection had nowhere to put it — so a module that named the zone correctly
|
|
8
|
+
* could not be deployed by any test.
|
|
9
|
+
*/
|
|
10
|
+
export type Zone = 'dmz' | 'app' | 'secure' | 'internal' | 'secure-mgmt';
|
|
2
11
|
|
|
3
12
|
/**
|
|
4
13
|
* Where a network assertion is made FROM — named by the box's network location
|
|
@@ -38,6 +47,11 @@ export interface NetworkConfig {
|
|
|
38
47
|
appMachines: MachineSpec[];
|
|
39
48
|
secureMachines: MachineSpec[];
|
|
40
49
|
internalMachines: MachineSpec[];
|
|
50
|
+
/**
|
|
51
|
+
* Machines on celilo's control-plane network. Bringing one up implies the
|
|
52
|
+
* `secure-mgmt` network exists, whether or not celilo-mgr itself sits there.
|
|
53
|
+
*/
|
|
54
|
+
secureMgmtMachines: MachineSpec[];
|
|
41
55
|
domain: string;
|
|
42
56
|
ddnsPassword: string;
|
|
43
57
|
verifyRouting: boolean;
|
|
@@ -398,6 +412,7 @@ export const ZONE_GATEWAYS: Record<Zone, string> = {
|
|
|
398
412
|
app: '10.0.20.1',
|
|
399
413
|
secure: '10.0.30.1',
|
|
400
414
|
internal: '192.168.0.254',
|
|
415
|
+
'secure-mgmt': '10.0.120.1',
|
|
401
416
|
};
|
|
402
417
|
|
|
403
418
|
export const ZONE_SUBNETS: Record<Zone, string> = {
|
|
@@ -405,6 +420,7 @@ export const ZONE_SUBNETS: Record<Zone, string> = {
|
|
|
405
420
|
app: '10.0.20.0/24',
|
|
406
421
|
secure: '10.0.30.0/24',
|
|
407
422
|
internal: '192.168.0.0/24',
|
|
423
|
+
'secure-mgmt': '10.0.120.0/24',
|
|
408
424
|
};
|
|
409
425
|
|
|
410
426
|
/**
|
|
@@ -428,6 +444,26 @@ export function greenwaveRouterIp(): string {
|
|
|
428
444
|
return '192.168.0.1';
|
|
429
445
|
}
|
|
430
446
|
|
|
447
|
+
/**
|
|
448
|
+
* The customer firewall's EXTERNAL (WAN) address — what the public internet
|
|
449
|
+
* sees, and the only address a public A record for a fronted service may hold
|
|
450
|
+
* (CLAUDE.md inviolable rule #1). Also the source address a DDNS update
|
|
451
|
+
* acquires on its way out, which is what makes source-IP registration correct
|
|
452
|
+
* by construction. Reference this instead of hardcoding it.
|
|
453
|
+
*/
|
|
454
|
+
export function externalWanIp(): string {
|
|
455
|
+
return '100.100.0.100';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The customer's public /24 on `isp-external`. The ISP ROUTES this prefix; it
|
|
460
|
+
* never re-NATs it, so a packet leaving the fleet still carries the firewall's
|
|
461
|
+
* WAN address when it reaches a public service.
|
|
462
|
+
*/
|
|
463
|
+
export function externalWanSubnet(): string {
|
|
464
|
+
return '100.100.0.0/24';
|
|
465
|
+
}
|
|
466
|
+
|
|
431
467
|
/** The internal split-horizon DNS resolver (dns-int) in the e2e topology. */
|
|
432
468
|
export function internalResolverIp(): string {
|
|
433
469
|
return '192.168.0.10';
|