@celilo/e2e 0.11.0 → 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.
- package/npm-registry-server/src/packument.test.ts +2 -5
- package/npm-registry-server/src/packument.ts +3 -3
- package/npm-registry-server/src/server.ts +1 -1
- package/package.json +3 -3
- package/registry-server/src/bootstrap.ts +1 -1
- package/registry-server/src/introspection.test.ts +8 -12
- package/registry-server/src/server.test.ts +2 -2
- package/registry-server/src/server.ts +1 -1
- package/registry-server/src/storage.test.ts +8 -4
- package/registry-server/src/storage.ts +1 -1
- package/registry-server/src/validation.ts +2 -4
- package/scripts/pack-celilo-packages.ts +1 -4
- package/scripts/stage-apt-repo.ts +1 -1
- package/scripts/stage-libsignal.ts +1 -3
- package/simulators/greenwave/server.ts +3 -1
- package/simulators/greenwave/state.ts +17 -10
- package/simulators/isitup/server.ts +1 -5
- package/simulators/namecheap-ddns/server.ts +2 -4
- package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
- package/simulators/namecheap-ddns/zone-updater.ts +6 -10
- package/simulators/signal-cli/server.ts +3 -1
- package/src/browser.ts +3 -3
- package/src/bus-events.ts +2 -2
- package/src/cli/command-tree-parser.ts +8 -2
- package/src/cli/completion.ts +5 -8
- package/src/cli/index.ts +9 -2
- package/src/cli/module-discovery.ts +3 -1
- package/src/cli-version-contract.ts +5 -6
- package/src/extract-failure.ts +92 -0
- package/src/fixtures.ts +4 -11
- package/src/network-builder.ts +12 -7
- package/src/observer.test.ts +42 -11
- package/src/observer.ts +12 -5
- package/src/registry-bundle.ts +1 -3
- package/src/run-lock.test.ts +2 -2
- package/src/run-lock.ts +19 -5
- package/src/runner.ts +9 -67
- package/src/shared-infra.ts +50 -22
- package/src/socks-proxy.ts +14 -17
- package/src/vantage-helpers.ts +5 -1
- package/src/vantage.test.ts +78 -24
- package/src/vantage.ts +24 -17
- package/src/wait-for-run.ts +1 -1
- package/src/zone-classifier.test.ts +7 -1
- package/src/zone-classifier.ts +2 -2
package/src/shared-infra.ts
CHANGED
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { execSync } from 'node:child_process';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
14
|
import { copyFileSync, existsSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
|
-
import {
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import {
|
|
17
|
+
SHARED_NETWORKS,
|
|
18
|
+
SHARED_PROJECT_NAME,
|
|
19
|
+
generateSharedInfraYaml,
|
|
20
|
+
} from './docker-compose-generator';
|
|
17
21
|
import { ensureRegistryServerBundle } from './registry-bundle';
|
|
18
22
|
|
|
19
23
|
const SHARED_COMPOSE_FILE = 'docker-compose.shared.yml';
|
|
@@ -37,8 +41,10 @@ function run(cmd: string, opts?: { cwd?: string; timeout?: number }): string {
|
|
|
37
41
|
*/
|
|
38
42
|
function nukeE2eResources(e2eDir: string): void {
|
|
39
43
|
try {
|
|
40
|
-
run(
|
|
41
|
-
{
|
|
44
|
+
run(
|
|
45
|
+
`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
|
|
46
|
+
{ cwd: e2eDir, timeout: 30_000 },
|
|
47
|
+
);
|
|
42
48
|
} catch {}
|
|
43
49
|
try {
|
|
44
50
|
const ids = run('docker ps -aq --filter name=celilo-e2e', { timeout: 10_000 });
|
|
@@ -49,11 +55,17 @@ function nukeE2eResources(e2eDir: string): void {
|
|
|
49
55
|
.split('\n')
|
|
50
56
|
.filter((n) => n.startsWith('celilo-e2e'));
|
|
51
57
|
for (const net of nets) {
|
|
52
|
-
try {
|
|
58
|
+
try {
|
|
59
|
+
run(`docker network rm ${net}`, { timeout: 5_000 });
|
|
60
|
+
} catch {}
|
|
53
61
|
}
|
|
54
62
|
} catch {}
|
|
55
|
-
try {
|
|
56
|
-
|
|
63
|
+
try {
|
|
64
|
+
run('docker network prune -f', { timeout: 10_000 });
|
|
65
|
+
} catch {}
|
|
66
|
+
try {
|
|
67
|
+
run('docker volume prune -f', { timeout: 10_000 });
|
|
68
|
+
} catch {}
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/**
|
|
@@ -117,7 +129,9 @@ export async function ensureSharedInfra(): Promise<void> {
|
|
|
117
129
|
}
|
|
118
130
|
} catch {
|
|
119
131
|
// DNS check failed — restart shared infra
|
|
120
|
-
console.log(
|
|
132
|
+
console.log(
|
|
133
|
+
'[progress:start] shared infra DNS check failed, restarting | shared infra restarted',
|
|
134
|
+
);
|
|
121
135
|
await stopSharedInfra();
|
|
122
136
|
}
|
|
123
137
|
}
|
|
@@ -130,7 +144,9 @@ export async function ensureSharedInfra(): Promise<void> {
|
|
|
130
144
|
// This self-heals orphans left by a crashed / cross-path / cross-checkout
|
|
131
145
|
// prior run (e.g. a botched-mount container that wedges the next `up`),
|
|
132
146
|
// which the old compose-project-scoped `down` couldn't see (#212).
|
|
133
|
-
console.log(
|
|
147
|
+
console.log(
|
|
148
|
+
'[progress:start] cleaning up stale shared infrastructure | shared infra cleanup complete',
|
|
149
|
+
);
|
|
134
150
|
nukeE2eResources(e2eDir);
|
|
135
151
|
|
|
136
152
|
// Refresh the bundled registry-server source so Dockerfile.registry's
|
|
@@ -161,12 +177,16 @@ export async function ensureSharedInfra(): Promise<void> {
|
|
|
161
177
|
|
|
162
178
|
// Build and start
|
|
163
179
|
console.log('[progress:start] building shared infrastructure images | shared images built');
|
|
164
|
-
run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} build`,
|
|
165
|
-
|
|
180
|
+
run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} build`, {
|
|
181
|
+
cwd: e2eDir,
|
|
182
|
+
timeout: 300_000,
|
|
183
|
+
});
|
|
166
184
|
|
|
167
185
|
console.log('[progress:start] starting shared infrastructure | shared infra started');
|
|
168
|
-
run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} up -d`,
|
|
169
|
-
|
|
186
|
+
run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} up -d`, {
|
|
187
|
+
cwd: e2eDir,
|
|
188
|
+
timeout: 120_000,
|
|
189
|
+
});
|
|
170
190
|
|
|
171
191
|
// Wait for DNS convergence — robustly. The old single 60s window with no
|
|
172
192
|
// recovery was an intermittent build-infra failure on the builder (#319:
|
|
@@ -196,23 +216,29 @@ export async function ensureSharedInfra(): Promise<void> {
|
|
|
196
216
|
if (Date.now() - lastRestart > 45_000) {
|
|
197
217
|
console.log('[progress:sub] DNS not converged yet — restarting namecheap-dns');
|
|
198
218
|
try {
|
|
199
|
-
run(
|
|
200
|
-
|
|
219
|
+
run(
|
|
220
|
+
`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} restart namecheap-dns`,
|
|
221
|
+
{ cwd: e2eDir, timeout: 30_000 },
|
|
222
|
+
);
|
|
201
223
|
} catch {}
|
|
202
224
|
lastRestart = Date.now();
|
|
203
225
|
}
|
|
204
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
226
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
205
227
|
}
|
|
206
228
|
// Diagnostics before failing, so the log shows WHY it didn't converge.
|
|
207
229
|
let psOut = '';
|
|
208
230
|
try {
|
|
209
|
-
psOut = run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} ps`,
|
|
210
|
-
|
|
231
|
+
psOut = run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} ps`, {
|
|
232
|
+
cwd: e2eDir,
|
|
233
|
+
timeout: 10_000,
|
|
234
|
+
});
|
|
211
235
|
} catch {}
|
|
212
236
|
let dnsLog = '';
|
|
213
237
|
try {
|
|
214
|
-
dnsLog = run(
|
|
215
|
-
{
|
|
238
|
+
dnsLog = run(
|
|
239
|
+
`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} logs --tail 40 namecheap-dns`,
|
|
240
|
+
{ cwd: e2eDir, timeout: 10_000 },
|
|
241
|
+
);
|
|
216
242
|
} catch {}
|
|
217
243
|
throw new Error(
|
|
218
244
|
`Timeout waiting for shared infrastructure DNS convergence (${dnsTimeout / 1000}s)\n` +
|
|
@@ -227,8 +253,10 @@ export async function stopSharedInfra(): Promise<void> {
|
|
|
227
253
|
console.log('[progress:start] stopping shared infrastructure | shared infra stopped');
|
|
228
254
|
const e2eDir = getE2eDir();
|
|
229
255
|
try {
|
|
230
|
-
run(
|
|
231
|
-
{
|
|
256
|
+
run(
|
|
257
|
+
`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
|
|
258
|
+
{ cwd: e2eDir, timeout: 60_000 },
|
|
259
|
+
);
|
|
232
260
|
} catch {
|
|
233
261
|
// Best effort
|
|
234
262
|
}
|
package/src/socks-proxy.ts
CHANGED
|
@@ -165,8 +165,7 @@ async function waitForTcpAccept(port: number, timeoutMs = 10_000): Promise<void>
|
|
|
165
165
|
await new Promise((r) => setTimeout(r, 100));
|
|
166
166
|
}
|
|
167
167
|
throw new Error(
|
|
168
|
-
`SOCKS proxy on 127.0.0.1:${port} never accepted connections within ${timeoutMs}ms`
|
|
169
|
-
(lastErr ? ` (last error: ${lastErr})` : ''),
|
|
168
|
+
`SOCKS proxy on 127.0.0.1:${port} never accepted connections within ${timeoutMs}ms${lastErr ? ` (last error: ${lastErr})` : ''}`,
|
|
170
169
|
);
|
|
171
170
|
}
|
|
172
171
|
|
|
@@ -233,8 +232,7 @@ function ensureProxyImage(): void {
|
|
|
233
232
|
const dockerfilePath = join(PACKAGE_ROOT, PROXY_DOCKERFILE);
|
|
234
233
|
if (!existsSync(dockerfilePath)) {
|
|
235
234
|
throw new Error(
|
|
236
|
-
`SOCKS proxy Dockerfile not found at ${dockerfilePath}.
|
|
237
|
-
`Expected it to ship with the @celilo/e2e package.`,
|
|
235
|
+
`SOCKS proxy Dockerfile not found at ${dockerfilePath}. Expected it to ship with the @celilo/e2e package.`,
|
|
238
236
|
);
|
|
239
237
|
}
|
|
240
238
|
|
|
@@ -247,18 +245,17 @@ function ensureProxyImage(): void {
|
|
|
247
245
|
});
|
|
248
246
|
} catch (err) {
|
|
249
247
|
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Failed to build ${PROXY_IMAGE} from ${PROXY_DOCKERFILE}: ${msg}`,
|
|
252
|
-
);
|
|
248
|
+
throw new Error(`Failed to build ${PROXY_IMAGE} from ${PROXY_DOCKERFILE}: ${msg}`);
|
|
253
249
|
}
|
|
254
250
|
}
|
|
255
251
|
|
|
256
252
|
function inspectContainer(name: string): boolean {
|
|
257
253
|
try {
|
|
258
|
-
const result = execSync(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
254
|
+
const result = execSync(`docker inspect -f '{{.State.Running}}' ${name} 2>/dev/null`, {
|
|
255
|
+
timeout: 5_000,
|
|
256
|
+
encoding: 'utf-8',
|
|
257
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
258
|
+
});
|
|
262
259
|
return result.trim() === 'true';
|
|
263
260
|
} catch {
|
|
264
261
|
return false;
|
|
@@ -276,15 +273,15 @@ async function waitForContainerListening(name: string): Promise<void> {
|
|
|
276
273
|
let lastInspect = '';
|
|
277
274
|
while (Date.now() < deadline) {
|
|
278
275
|
try {
|
|
279
|
-
lastInspect = execSync(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
276
|
+
lastInspect = execSync(`docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' ${name}`, {
|
|
277
|
+
timeout: 3_000,
|
|
278
|
+
encoding: 'utf-8',
|
|
279
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
280
|
+
}).trim();
|
|
283
281
|
} catch {
|
|
284
282
|
// Container may have already self-removed (--rm) after exiting
|
|
285
283
|
throw new Error(
|
|
286
|
-
`SOCKS proxy container '${name}' disappeared during startup.
|
|
287
|
-
`It likely exited immediately. Check the image's required env vars.`,
|
|
284
|
+
`SOCKS proxy container '${name}' disappeared during startup. It likely exited immediately. Check the image's required env vars.`,
|
|
288
285
|
);
|
|
289
286
|
}
|
|
290
287
|
const [status] = lastInspect.split(/\s+/);
|
package/src/vantage-helpers.ts
CHANGED
|
@@ -52,7 +52,11 @@ export async function assertFrontedServicePositives(
|
|
|
52
52
|
const url = `https://${expectation.fqdn}${expectation.path ?? ''}`;
|
|
53
53
|
|
|
54
54
|
// The internet sees the service at a public IP and a valid HTTPS endpoint.
|
|
55
|
-
await probe.assertResolvesInZone({
|
|
55
|
+
await probe.assertResolvesInZone({
|
|
56
|
+
from: 'publicInternet',
|
|
57
|
+
name: expectation.fqdn,
|
|
58
|
+
zone: 'public',
|
|
59
|
+
});
|
|
56
60
|
await probe.assertHttps({
|
|
57
61
|
from: 'publicInternet',
|
|
58
62
|
url,
|
package/src/vantage.test.ts
CHANGED
|
@@ -36,7 +36,9 @@ 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(
|
|
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
44
|
expect(r.ips).toEqual(['10.226.1.253']);
|
|
@@ -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(
|
|
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,25 +64,45 @@ 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(
|
|
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(
|
|
71
|
-
|
|
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
|
+
});
|
|
72
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(
|
|
77
|
-
|
|
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(
|
|
97
|
+
const leaked = new VantageProbe(
|
|
98
|
+
fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
|
|
99
|
+
);
|
|
80
100
|
await expect(
|
|
81
|
-
leaked.assertResolvesInZone({
|
|
101
|
+
leaked.assertResolvesInZone({
|
|
102
|
+
from: 'publicInternet',
|
|
103
|
+
name: 'www.example.com',
|
|
104
|
+
zone: 'public',
|
|
105
|
+
}),
|
|
82
106
|
).rejects.toThrow(VantageAssertionError);
|
|
83
107
|
});
|
|
84
108
|
|
|
@@ -94,7 +118,9 @@ describe('assertResolvesInZone — the killer verb', () => {
|
|
|
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(
|
|
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',
|
|
@@ -106,7 +132,9 @@ describe('assertResolvesTo — exact-IP DNS correctness', () => {
|
|
|
106
132
|
});
|
|
107
133
|
|
|
108
134
|
test('fails (naming the server) when the expected IP is absent', async () => {
|
|
109
|
-
const probe = new VantageProbe(
|
|
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',
|
|
@@ -119,31 +147,49 @@ describe('assertResolvesTo — exact-IP DNS correctness', () => {
|
|
|
119
147
|
});
|
|
120
148
|
|
|
121
149
|
test('accepts an array of expected IPs (all must be present)', async () => {
|
|
122
|
-
const probe = new VantageProbe(
|
|
123
|
-
|
|
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({
|
|
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(
|
|
133
|
-
|
|
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(
|
|
139
|
-
|
|
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(
|
|
186
|
+
const reachable = new VantageProbe(
|
|
187
|
+
fakeTransport({ tcpConnect: async () => ({ connected: true }) }),
|
|
188
|
+
);
|
|
145
189
|
await reachable.assertReachable({ from: LAN, host: '10.226.1.253', port: 443 });
|
|
146
|
-
const blocked = new VantageProbe(
|
|
190
|
+
const blocked = new VantageProbe(
|
|
191
|
+
fakeTransport({ tcpConnect: async () => ({ connected: false }) }),
|
|
192
|
+
);
|
|
147
193
|
await blocked.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 });
|
|
148
194
|
});
|
|
149
195
|
});
|
|
@@ -151,9 +197,17 @@ describe('reachability + isolation (§D6)', () => {
|
|
|
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({
|
|
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());
|
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 {
|
|
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<{
|
|
68
|
-
|
|
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<{
|
|
71
|
-
|
|
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(
|
|
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) {
|
package/src/wait-for-run.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* decides whether to spawn cele2e themselves or just observe.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { type Bus, type BusEvent, defineEvents, openBus } from '@celilo/event-bus';
|
|
14
14
|
|
|
15
15
|
export type WaitResult =
|
|
16
16
|
| {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
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', () => {
|
package/src/zone-classifier.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* caller's policy, layered on top of this primitive.)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { type Zone
|
|
19
|
+
import { ZONE_SUBNETS, type Zone } from './types';
|
|
20
20
|
|
|
21
21
|
export type ZoneClass =
|
|
22
22
|
| { kind: 'zone'; zone: Zone }
|
|
@@ -62,7 +62,7 @@ export function inCidr(ip: string, cidr: string): boolean {
|
|
|
62
62
|
if (ipN === null || baseN === null || Number.isNaN(bits) || bits < 0 || bits > 32) return false;
|
|
63
63
|
if (bits === 0) return true;
|
|
64
64
|
const mask = (0xffffffff << (32 - bits)) >>> 0;
|
|
65
|
-
return (ipN & mask) >>> 0 === (
|
|
65
|
+
return (ipN & mask) >>> 0 === (baseN & mask) >>> 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Classify a resolved IPv4 against the e2e topology. */
|