@celilo/e2e 0.20.5 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/e2e-bake-management +362 -10
- package/config/routing/management-routes.sh +11 -0
- package/config/routing/minio-startup.sh +27 -32
- package/config/routing/resolver-internal-routes.sh +14 -6
- package/config/socks/startup.sh +46 -0
- package/config/ssh/sshd-celilo-api.conf +6 -0
- package/docker/Dockerfile.management +47 -0
- package/docker/Dockerfile.minio +34 -16
- package/docker/Dockerfile.socks-proxy +14 -0
- package/package.json +2 -2
- package/registry-server/src/bootstrap.ts +43 -5
- package/registry-server/src/index.ts +1 -0
- package/registry-server/src/server.ts +18 -1
- package/simulators/greenwave/state.ts +85 -4
- package/src/bake-probe-explanation.test.ts +43 -0
- package/src/bake-probe-fails-closed.test.ts +35 -0
- package/src/bake-probe-no-shell-expansion.test.ts +40 -0
- package/src/cli/build.test.ts +50 -0
- package/src/cli/build.ts +36 -1
- package/src/cli/host.ts +71 -2
- package/src/docker-compose-generator.ts +7 -3
- package/src/doctor.test.ts +87 -6
- package/src/doctor.ts +112 -23
- package/src/greenwave-external-interface.test.ts +63 -0
- package/src/index.ts +1 -0
- package/src/installed-cli-digest.test.ts +113 -0
- package/src/installed-cli-digest.ts +102 -0
- package/src/parse-line.ts +8 -2
- package/src/resolver-internal-config.test.ts +65 -0
- package/src/shared-infra-pebble-root.test.ts +26 -0
- package/src/shared-infra-self-contained.test.ts +53 -0
- package/src/shared-infra.ts +61 -0
- package/src/socks-proxy.ts +45 -11
- package/src/stale-acme-root.ts +66 -0
- package/src/types.ts +8 -0
package/bin/e2e-bake-management
CHANGED
|
@@ -24,14 +24,22 @@
|
|
|
24
24
|
|
|
25
25
|
import { execSync } from 'node:child_process';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
|
-
import { ensureSharedInfra } from '../src/shared-infra';
|
|
27
|
+
import { ensureSharedInfra, stopSharedInfra } from '../src/shared-infra';
|
|
28
28
|
import { network } from '../src/network-builder';
|
|
29
29
|
import { imageTag } from '../src/docker-compose-generator';
|
|
30
30
|
import { findMonorepoRoot } from '../src/repo-root';
|
|
31
|
+
import { detectStaleAcmeRoot, explainStaleRoot } from '../src/stale-acme-root';
|
|
31
32
|
import {
|
|
32
33
|
packNpmRegistryTarballs,
|
|
33
34
|
stageWebsiteDist,
|
|
34
35
|
} from '../src/stage-simulator-inputs';
|
|
36
|
+
import {
|
|
37
|
+
INSTALLED_CLI_DIR,
|
|
38
|
+
STAGED_TARBALL,
|
|
39
|
+
digestScript,
|
|
40
|
+
mismatchMessage,
|
|
41
|
+
parseDigests,
|
|
42
|
+
} from '../src/installed-cli-digest';
|
|
35
43
|
import {
|
|
36
44
|
CONSUMER_FINGERPRINT_PREFIX,
|
|
37
45
|
PUBLISHED_FINGERPRINT_PREFIX,
|
|
@@ -221,6 +229,43 @@ function labelSafeVersion(version: string): string {
|
|
|
221
229
|
return version.trim().split(/\s+/).pop() || 'unknown';
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The staged tarball for a package, as pack-celilo-packages recorded it.
|
|
234
|
+
*/
|
|
235
|
+
function stagedTarballPath(packageName: string): string {
|
|
236
|
+
const manifestPath = join(PACKAGE_ROOT, '.npm-registry-cache', 'pack-manifest.json');
|
|
237
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
|
|
238
|
+
packages?: Array<{ name: string; tarball: string }>;
|
|
239
|
+
};
|
|
240
|
+
const entry = manifest.packages?.find((p) => p.name === packageName);
|
|
241
|
+
if (!entry) {
|
|
242
|
+
throw new Error(`pack-manifest.json lists no ${packageName} tarball to verify the install against.`);
|
|
243
|
+
}
|
|
244
|
+
// pack() records the basename; the tarballs sit beside the manifest.
|
|
245
|
+
return join(PACKAGE_ROOT, '.npm-registry-cache', entry.tarball);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Refuse to stamp a freshness label over a CLI that is not the one we packed.
|
|
250
|
+
* See src/installed-cli-digest.ts for why the label alone cannot say this.
|
|
251
|
+
*/
|
|
252
|
+
async function verifyInstalledCliIsStaged(exec: Exec, containerId: string): Promise<void> {
|
|
253
|
+
process.stdout.write(' installed CLI = staged ');
|
|
254
|
+
const t = Date.now();
|
|
255
|
+
run(`docker cp ${JSON.stringify(stagedTarballPath('@celilo/cli'))} ${containerId}:${STAGED_TARBALL}`);
|
|
256
|
+
const b64 = Buffer.from(digestScript(INSTALLED_CLI_DIR)).toString('base64');
|
|
257
|
+
const probe = await exec(
|
|
258
|
+
`echo ${b64} | base64 -d > /tmp/verify-cli.sh && bash /tmp/verify-cli.sh 2>&1`,
|
|
259
|
+
120_000,
|
|
260
|
+
);
|
|
261
|
+
const digests = parseDigests(`${probe.stdout}${probe.stderr}`);
|
|
262
|
+
if (digests.staged !== digests.installed) {
|
|
263
|
+
console.log('\u2717');
|
|
264
|
+
throw new Error(mismatchMessage(digests));
|
|
265
|
+
}
|
|
266
|
+
console.log(`\u2714 ${Math.round((Date.now() - t) / 1000)}s (${digests.staged.slice(0, 16)})`);
|
|
267
|
+
}
|
|
268
|
+
|
|
224
269
|
function sourceStamp(published: boolean, version: string): string {
|
|
225
270
|
if (published) return `${PUBLISHED_FINGERPRINT_PREFIX}${labelSafeVersion(version)}`;
|
|
226
271
|
// Consumer mode: restageSimulatorInputs is the only writer of
|
|
@@ -280,6 +325,148 @@ function restageSimulatorInputs(): void {
|
|
|
280
325
|
* Default bake: install.sh against the simulated celilo.computer / npm
|
|
281
326
|
* registry, inside the standard topology.
|
|
282
327
|
*/
|
|
328
|
+
/**
|
|
329
|
+
* The probe waits for HTTPS to VERIFY, so a trust failure and an unfinished
|
|
330
|
+
* issuance both spend the whole budget and both used to print "timed out".
|
|
331
|
+
* They need opposite responses, so branch the headline on curl's own exit code
|
|
332
|
+
* rather than making the reader infer it from the layers (crew-builder,
|
|
333
|
+
* celilo#1427).
|
|
334
|
+
*/
|
|
335
|
+
/**
|
|
336
|
+
* The TLS/DNS layers, read from inside the management container.
|
|
337
|
+
*
|
|
338
|
+
* Hoisted out of the probe's failure path because the probe is not the only
|
|
339
|
+
* step that can fail on TLS: install.sh's own curl-pipe-bash talks to the same
|
|
340
|
+
* host a moment later, and when THAT failed the bake printed the curl error
|
|
341
|
+
* alone with no layers under it (measured, run 7255: `curl: (35) TLS connect
|
|
342
|
+
* error ... tlsv1 alert internal error`, one second after the probe had
|
|
343
|
+
* verified the same URL successfully). A failure with no layers is the exact
|
|
344
|
+
* shape celilo#1365 spent two weeks in.
|
|
345
|
+
*/
|
|
346
|
+
async function tlsDiagnosticLayers(exec: Exec, projectName?: string): Promise<string> {
|
|
347
|
+
const diag = async (label: string, cmd: string): Promise<string> => {
|
|
348
|
+
const out = await exec(cmd, 10_000);
|
|
349
|
+
return `--- ${label} ---\n${out.stdout.trim() || '(no output)'}`;
|
|
350
|
+
};
|
|
351
|
+
return [
|
|
352
|
+
await diag('resolv.conf', 'cat /etc/resolv.conf'),
|
|
353
|
+
await diag('dig @10.226.1.10 (internal resolver)', 'dig +time=2 +tries=1 @10.226.1.10 celilo.computer'),
|
|
354
|
+
await diag('dig @203.0.113.1 (public fallback)', 'dig +time=2 +tries=1 @203.0.113.1 celilo.computer'),
|
|
355
|
+
await diag('trusted CA files', 'ls -la /usr/local/share/ca-certificates/'),
|
|
356
|
+
await diag('Pebble runtime root reachable from management', 'curl -sk --max-time 5 https://100.64.0.100:15000/roots/0 | head -c 300; echo'),
|
|
357
|
+
await diag('fetched runtime root (if any)', 'openssl x509 -in /usr/local/share/ca-certificates/pebble-acme-root.crt -noout -subject -enddate 2>/dev/null'),
|
|
358
|
+
await diag('cert chain served by website-sim', 'timeout 8 openssl s_client -connect celilo.computer:443 -servername celilo.computer -showcerts </dev/null 2>/dev/null | grep -E "^ [0-9] s:|^ i:"'),
|
|
359
|
+
await diag('curl -v (DNS/connect/TLS/HTTP stages)', 'curl -v --max-time 5 https://celilo.computer/install.sh -o /dev/null 2>&1 | tail -20'),
|
|
360
|
+
// The probe's EXACT command, re-run here. Runs 7255-7260 all showed the
|
|
361
|
+
// probe returning 0 and install.sh's curl failing 130ms later on the same
|
|
362
|
+
// host, which is either two different answers from one server or a probe
|
|
363
|
+
// reading something other than what it thinks. Printing both side by side
|
|
364
|
+
// decides it, instead of reasoning about which flag matters.
|
|
365
|
+
await diag(
|
|
366
|
+
're-run of the probe command',
|
|
367
|
+
"curl -sS --max-time 3 -o /dev/null -w 'curl-exit=%{exitcode}\n' https://celilo.computer/install.sh 2>&1",
|
|
368
|
+
),
|
|
369
|
+
await diag(
|
|
370
|
+
'the same URL with -f, as install.sh fetches it',
|
|
371
|
+
"curl -fsS --max-time 5 -o /dev/null -w 'curl-exit=%{exitcode}\n' https://celilo.computer/install.sh 2>&1",
|
|
372
|
+
),
|
|
373
|
+
// Did management's own startup script reach its Pebble-root fetch at all?
|
|
374
|
+
// management-routes.sh runs under `set -e` with six `ip route add` calls
|
|
375
|
+
// AHEAD of that fetch (line 61), and a route add exiting 2 has killed this
|
|
376
|
+
// script before — celilo#1351, recorded in docker-compose-generator.ts. If
|
|
377
|
+
// the trust store has no runtime root, this log says whether the fetch
|
|
378
|
+
// failed, is still looping, or was never reached.
|
|
379
|
+
await diag('is /startup.sh still running?', 'ps -ef | grep -c "[s]tartup.sh"'),
|
|
380
|
+
managementStartupLog(projectName),
|
|
381
|
+
// Every layer above is read from INSIDE management, so all of them describe
|
|
382
|
+
// the client. When the server aborts the handshake before sending a
|
|
383
|
+
// certificate (run 7258), the client has nothing left to report and the
|
|
384
|
+
// reason is only in Caddy's own log — whether it got an ACME cert at all.
|
|
385
|
+
websiteSimLog(),
|
|
386
|
+
].join('\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** The per-test management container's own startup output, read from the host. */
|
|
390
|
+
function managementStartupLog(projectName?: string): string {
|
|
391
|
+
if (!projectName) return '--- management startup log ---\n(project name unavailable)';
|
|
392
|
+
try {
|
|
393
|
+
const id = execSync(`docker ps -aq --filter name=${projectName} --filter name=management`, {
|
|
394
|
+
encoding: 'utf-8',
|
|
395
|
+
timeout: 15_000,
|
|
396
|
+
})
|
|
397
|
+
.trim()
|
|
398
|
+
.split('\n')[0];
|
|
399
|
+
if (!id) return '--- management startup log ---\n(container not found)';
|
|
400
|
+
const out = execSync(`docker logs --tail 40 ${id} 2>&1`, {
|
|
401
|
+
encoding: 'utf-8',
|
|
402
|
+
timeout: 20_000,
|
|
403
|
+
});
|
|
404
|
+
return `--- management startup log (server side) ---\n${out.trim() || '(no output)'}`;
|
|
405
|
+
} catch (err) {
|
|
406
|
+
return `--- management startup log ---\n(unreadable: ${(err as Error).message})`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** celilo-website-sim's own Caddy log, read from the host. */
|
|
411
|
+
function websiteSimLog(): string {
|
|
412
|
+
try {
|
|
413
|
+
const out = execSync(
|
|
414
|
+
'docker compose -f docker-compose.shared.yml -p celilo-e2e-shared logs --tail 40 celilo-website 2>&1',
|
|
415
|
+
// The package's OWN root, not the monorepo's: npm-consumer-smoke runs this
|
|
416
|
+
// from node_modules outside any checkout, where findMonorepoRoot() is
|
|
417
|
+
// undefined and join() throws (measured, run 7260). Same resolution
|
|
418
|
+
// shared-infra.ts:getE2eDir uses, for the same reason.
|
|
419
|
+
{ cwd: join(import.meta.dir, '..'), encoding: 'utf-8', timeout: 20_000 },
|
|
420
|
+
);
|
|
421
|
+
return `--- celilo-website-sim log (server side) ---\n${out.trim() || '(no output)'}`;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return `--- celilo-website-sim log (server side) ---\n(unreadable: ${(err as Error).message})`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Sentinel: the probe ran but its `curl-exit=` marker was not in the output. */
|
|
428
|
+
const CA_WAIT_SECONDS = 240;
|
|
429
|
+
const PROBE_UNREADABLE = -2;
|
|
430
|
+
|
|
431
|
+
function explainProbeFailure(curlExit: number): string {
|
|
432
|
+
// 60 unknown issuer, 35 TLS connect, 51 peer cert not ok.
|
|
433
|
+
if ([60, 35, 51].includes(curlExit)) {
|
|
434
|
+
return (
|
|
435
|
+
`celilo.computer served a certificate this container cannot verify (curl ${curlExit}). ` +
|
|
436
|
+
"The step above already waited for management's runtime root, so this is the STALE-ROOT " +
|
|
437
|
+
'case, not the not-yet-fetched one — those need opposite responses and exit 60 alone ' +
|
|
438
|
+
'cannot tell them apart (celilo#1365 spent runs on the wrong one). ' +
|
|
439
|
+
'This is NOT a timeout and waiting longer cannot fix it. Pebble mints a random root CA ' +
|
|
440
|
+
'at every startup and Caddy caches its cert until expiry, so a Pebble that restarted ' +
|
|
441
|
+
'without the website sim restarting is serving a cert from a root that no longer exists ' +
|
|
442
|
+
'(celilo#1427). Check the chain above against the live root. Remedy: `cele2e down` then ' +
|
|
443
|
+
'`cele2e build-infra`, so Pebble and Caddy start together.'
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
// 6 could not resolve, 7 could not connect, 28 operation timeout.
|
|
447
|
+
if ([6, 7, 28].includes(curlExit)) {
|
|
448
|
+
return (
|
|
449
|
+
`celilo.computer could not be reached at all (curl ${curlExit}) within 30s — a resolution ` +
|
|
450
|
+
'or connection failure, not a trust one. Either the website sim is down or its ACME ' +
|
|
451
|
+
'issuance genuinely has not finished. Read the DNS layers above.'
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (curlExit === PROBE_UNREADABLE) {
|
|
455
|
+
return (
|
|
456
|
+
'The HTTPS probe never returned a readable curl exit code in 30s — its ' +
|
|
457
|
+
'`curl-exit=` marker was missing from both streams. Treated as NOT ready: ' +
|
|
458
|
+
'the probe cannot prove the site is serving, and a probe that cannot fail ' +
|
|
459
|
+
'is how celilo#1365 stayed invisible. See the layers above.'
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return (
|
|
463
|
+
`Timed out (${Math.round(CA_WAIT_SECONDS)}s of wall clock) waiting for celilo.computer to serve install.sh over HTTPS (curl ${curlExit}). ` +
|
|
464
|
+
'See the DNS / curl -v layers above for which one failed. A cold shared-infra ' +
|
|
465
|
+
'caddy needs ~90s to obtain its FIRST certificate, so this is a real failure ' +
|
|
466
|
+
'rather than an impatient probe.'
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
283
470
|
async function bakeViaSim(): Promise<void> {
|
|
284
471
|
console.log('▸ Baking celilo into management:latest via install.sh');
|
|
285
472
|
console.log('');
|
|
@@ -302,30 +489,146 @@ async function bakeViaSim(): Promise<void> {
|
|
|
302
489
|
const exec: Exec = (cmd, timeoutMs) => net.exec('management', cmd, timeoutMs);
|
|
303
490
|
|
|
304
491
|
let containerId = '';
|
|
492
|
+
let bakeFailed = false;
|
|
305
493
|
try {
|
|
306
494
|
// Wait for the management container's Pebble CA fetch to complete so
|
|
307
495
|
// the curl-pipe-bash below can verify TLS to celilo.computer. Without
|
|
308
496
|
// pipefail an early curl failure returns 0 silently and install.sh
|
|
309
497
|
// "runs" in 0s installing nothing.
|
|
498
|
+
// WAIT FOR THE DEPENDENCY THIS STEP IS NAMED AFTER, not for a symptom of it.
|
|
499
|
+
// management-routes.sh fetches Pebble's runtime root in a FOREGROUND retry
|
|
500
|
+
// loop of 60 tries x 2s (config/routing/management-routes.sh:61-71) — worst
|
|
501
|
+
// case 120s, and it only starts once the per-test routing is up. The HTTPS
|
|
502
|
+
// probe below budgeted 30s, so it gave up while that fetch was still
|
|
503
|
+
// running and blamed the certificate. Measured on run 7266: /startup.sh
|
|
504
|
+
// still running, its log stopped at "Waiting for Pebble ACME root CA..."
|
|
505
|
+
// with no "installed" line, and the trust store held only build-time certs
|
|
506
|
+
// — while the website sim had already obtained a valid cert. The two waits
|
|
507
|
+
// were simply sized wrong relative to each other.
|
|
508
|
+
process.stdout.write(' pebble root installed .... ');
|
|
509
|
+
const tRoot = Date.now();
|
|
510
|
+
let rootReady = false;
|
|
511
|
+
// 240s, not the 150 this was first written with. MEASURED on run 7268:
|
|
512
|
+
// the fetch took 136s — past the loop's nominal 120s ceiling, because each
|
|
513
|
+
// failing curl spends its own connect timeout before the 2s sleep. 150
|
|
514
|
+
// left 10% headroom on an idle builder, which is how a green gate starts
|
|
515
|
+
// flaking under load. The budget costs nothing when the root arrives fast:
|
|
516
|
+
// the wait ends the moment the file appears.
|
|
517
|
+
for (let i = 0; i < 240; i++) {
|
|
518
|
+
const probe = await exec(
|
|
519
|
+
'test -s /usr/local/share/ca-certificates/pebble-acme-root.crt && echo root-present || echo root-absent',
|
|
520
|
+
5_000,
|
|
521
|
+
);
|
|
522
|
+
if (`${probe.stdout}${probe.stderr}`.includes('root-present')) {
|
|
523
|
+
rootReady = true;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
527
|
+
}
|
|
528
|
+
if (!rootReady) {
|
|
529
|
+
console.log('✗');
|
|
530
|
+
console.error(await tlsDiagnosticLayers(exec, net.projectName));
|
|
531
|
+
throw new Error(
|
|
532
|
+
"management never installed Pebble's runtime ACME root in 240s. Its startup script " +
|
|
533
|
+
'fetches it in a foreground loop before celilo.computer can be verified at all; the ' +
|
|
534
|
+
'layers above say whether that loop is still running, died early, or is failing its ' +
|
|
535
|
+
'fetch (config/routing/management-routes.sh:61).',
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
console.log(`✔ ${Math.round((Date.now() - tRoot) / 1000)}s`);
|
|
539
|
+
|
|
310
540
|
process.stdout.write(' pebble CA ready .......... ');
|
|
311
541
|
const tCa = Date.now();
|
|
312
542
|
let caReady = false;
|
|
313
|
-
|
|
543
|
+
let lastProbeExit = -1;
|
|
544
|
+
let lastProbeOutput = '';
|
|
545
|
+
// Re-checked while INCONCLUSIVE, at most every 30s. A single check at t=0
|
|
546
|
+
// sits at the flakiest moment in the run (shared infra has just come up), and
|
|
547
|
+
// an exec that answers nothing there scores `unknown` — so a check that fired
|
|
548
|
+
// only once would silently fall back to burning the whole budget, which is the
|
|
549
|
+
// exact defect this is here to remove.
|
|
550
|
+
let staleCheckAt = 0;
|
|
551
|
+
// WALL CLOCK, not an iteration count. Each pass costs up to 3s of curl plus a
|
|
552
|
+
// 1s sleep, so `i < 30` was never 30 seconds — and the error text said "30s"
|
|
553
|
+
// regardless, which is how this budget got read as generous when it was not.
|
|
554
|
+
// Measured 2026-09-26 on a torn-down host: shared-infra caddy needs ~90s to
|
|
555
|
+
// obtain its FIRST certificate (caddy's own log: "certificate obtained
|
|
556
|
+
// successfully" at 90.9s elapsed, after one tls-alpn-01 retry). A 30-iteration
|
|
557
|
+
// cap could not reach that, so a cold rig failed the bake three runs running
|
|
558
|
+
// while the website was merely still issuing. 240s covers the measurement with
|
|
559
|
+
// margin and still fails in well under the suite's own budget.
|
|
560
|
+
const CA_WAIT_MS = 240_000;
|
|
561
|
+
const caDeadline = Date.now() + CA_WAIT_MS;
|
|
562
|
+
while (Date.now() < caDeadline) {
|
|
563
|
+
// NO `$?` HERE, EVER. dockerExec wraps the command as
|
|
564
|
+
// `bash -c ${JSON.stringify(cmd)}` (container-manager.ts:636) and hands
|
|
565
|
+
// that to a HOST shell, which expands `$?` itself — to its own last
|
|
566
|
+
// status, which is 0. So `echo "curl-exit=$?"` reported curl-exit=0 on
|
|
567
|
+
// every run this bead has ever had, including the ones where curl printed
|
|
568
|
+
// `(60) SSL certificate problem: unable to get local issuer certificate`
|
|
569
|
+
// two lines above it (run 7262 prints exactly that pair). The probe was
|
|
570
|
+
// structurally incapable of failing, which is why celilo#1365 read as a
|
|
571
|
+
// 30s timeout and then as a green step: the step never had an opinion.
|
|
572
|
+
// curl's own %{exitcode} needs no shell variable and cannot be expanded
|
|
573
|
+
// away.
|
|
314
574
|
const probe = await exec(
|
|
315
|
-
|
|
575
|
+
"curl -sS --max-time 3 -o /dev/null -w 'curl-exit=%{exitcode}\n' https://celilo.computer/install.sh 2>&1",
|
|
316
576
|
5_000,
|
|
317
577
|
);
|
|
318
|
-
|
|
578
|
+
const text = `${probe.stdout}\n${probe.stderr}`.trim();
|
|
579
|
+
const code = /curl-exit=(\d+)/.exec(text);
|
|
580
|
+
// FAIL CLOSED. The probe command ends in `echo`, so the shell's exit is 0
|
|
581
|
+
// whatever curl did — falling back to probe.exitCode scored "I could not
|
|
582
|
+
// read the answer" as "ready". Measured on run 7258: the probe reported
|
|
583
|
+
// `✔ 0s` while celilo.computer was serving NO certificate at all
|
|
584
|
+
// (openssl s_client returned an empty chain and the server aborted the
|
|
585
|
+
// handshake with `internal error (592)`), and the real failure surfaced
|
|
586
|
+
// one step later as install.sh's curl (35). A probe that cannot fail is
|
|
587
|
+
// worse than no probe: it converted celilo#1365's defect into a green
|
|
588
|
+
// step plus a mystery. Unreadable now counts as not-ready, the same bias
|
|
589
|
+
// live-stack.ts already applies to its own evidence test.
|
|
590
|
+
lastProbeExit = code ? Number(code[1]) : PROBE_UNREADABLE;
|
|
591
|
+
lastProbeOutput = text;
|
|
592
|
+
if (lastProbeExit === 0) {
|
|
319
593
|
caReady = true;
|
|
320
594
|
break;
|
|
321
595
|
}
|
|
596
|
+
// REFUSE INSTANTLY on a dead root rather than spending the 240s budget
|
|
597
|
+
// and then reporting a timeout (celilo#1427). The chain either names
|
|
598
|
+
// Pebble's live root or it does not, and that answer does not change by
|
|
599
|
+
// waiting. A cold caddy serving NOTHING yet scores `unknown` and keeps
|
|
600
|
+
// the wait, which is the case the budget exists for.
|
|
601
|
+
if (Date.now() >= staleCheckAt) {
|
|
602
|
+
staleCheckAt = Date.now() + 30_000;
|
|
603
|
+
const [live, served] = await Promise.all([
|
|
604
|
+
exec('curl -sk --max-time 5 https://100.64.0.100:15000/roots/0 | openssl x509 -noout -subject 2>/dev/null', 10_000),
|
|
605
|
+
// Deliberately a LOOSE grep: openssl's indentation for the issuer line
|
|
606
|
+
// differs across versions, and detectStaleAcmeRoot does the real
|
|
607
|
+
// parsing anyway. An anchored pattern that misses scores `unknown`,
|
|
608
|
+
// which reads exactly like a cold caddy and disables the check.
|
|
609
|
+
exec('timeout 8 openssl s_client -connect celilo.computer:443 -servername celilo.computer -showcerts </dev/null 2>/dev/null | grep i:', 12_000),
|
|
610
|
+
]);
|
|
611
|
+
const verdict = detectStaleAcmeRoot(live.stdout, served.stdout);
|
|
612
|
+
if (verdict.kind === 'stale') {
|
|
613
|
+
console.log('✗');
|
|
614
|
+
console.error(await tlsDiagnosticLayers(exec, net.projectName));
|
|
615
|
+
throw new Error(explainStaleRoot(verdict));
|
|
616
|
+
}
|
|
617
|
+
// Settled: the served chain names the live root, so it cannot go stale
|
|
618
|
+
// by waiting. Stop paying ~20s of exec per cycle for a known answer.
|
|
619
|
+
if (verdict.kind === 'current') staleCheckAt = Number.POSITIVE_INFINITY;
|
|
620
|
+
}
|
|
322
621
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
323
622
|
}
|
|
324
623
|
if (!caReady) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
624
|
+
// Self-diagnosing (e2e-confidence #255, mirroring install-sh.test.ts):
|
|
625
|
+
// a bake that fails here used to name two GUESSES and discard the probe
|
|
626
|
+
// output, so celilo#1365 produced two CI failures with no cause visible.
|
|
627
|
+
// Dump the DNS / reachability / TLS layers from inside the container.
|
|
628
|
+
const layers = await tlsDiagnosticLayers(exec, net.projectName);
|
|
629
|
+
console.error(layers);
|
|
630
|
+
console.error(`--- last probe (curl exit ${lastProbeExit}) ---\n${lastProbeOutput || '(no output)'}`);
|
|
631
|
+
throw new Error(explainProbeFailure(lastProbeExit));
|
|
329
632
|
}
|
|
330
633
|
console.log(`✔ ${Math.round((Date.now() - tCa) / 1000)}s`);
|
|
331
634
|
|
|
@@ -340,6 +643,10 @@ async function bakeViaSim(): Promise<void> {
|
|
|
340
643
|
console.error('--- install.sh output (tail) ---');
|
|
341
644
|
console.error(result.stdout.slice(-3000));
|
|
342
645
|
console.error(result.stderr.slice(-2000));
|
|
646
|
+
// The probe verified this very URL seconds ago, so a TLS failure HERE is
|
|
647
|
+
// a change between the two calls, not a static misconfiguration — and it
|
|
648
|
+
// is unreadable without the layers (run 7255).
|
|
649
|
+
console.error(await tlsDiagnosticLayers(exec, net.projectName));
|
|
343
650
|
throw new Error('install.sh failed inside the bake container');
|
|
344
651
|
}
|
|
345
652
|
console.log(`✔ ${Math.round((Date.now() - t2) / 1000)}s`);
|
|
@@ -348,14 +655,40 @@ async function bakeViaSim(): Promise<void> {
|
|
|
348
655
|
// CLI, genuinely against the installed binary rather than the mount.
|
|
349
656
|
const version = await installShimAndVerify(exec, 'sim');
|
|
350
657
|
|
|
351
|
-
process.stdout.write(' docker commit .......... ');
|
|
352
|
-
const t5 = Date.now();
|
|
353
658
|
containerId = run(
|
|
354
659
|
`docker compose -f ${COMPOSE_FILE} -p ${net.projectName} ps -q management`,
|
|
355
660
|
).trim();
|
|
356
661
|
if (!containerId) {
|
|
357
662
|
throw new Error('Could not resolve management container id');
|
|
358
663
|
}
|
|
664
|
+
// CONSUMER MODE HAS NOTHING TO VERIFY AGAINST, and the check reads a file
|
|
665
|
+
// that is deliberately not shipped. verifyInstalledCliIsStaged compares the
|
|
666
|
+
// installed CLI against the tarball restageSimulatorInputs packed, recorded
|
|
667
|
+
// in PACKAGE_ROOT/.npm-registry-cache/pack-manifest.json. Without a checkout
|
|
668
|
+
// that restage is skipped (see restageSimulatorInputs), nothing writes the
|
|
669
|
+
// manifest, and `.npm-registry-cache` is excluded from the e2e tarball on
|
|
670
|
+
// purpose (celilo#116) — so PACKAGE_ROOT resolves into node_modules and the
|
|
671
|
+
// read is a guaranteed ENOENT. sourceStamp already reasons exactly this way
|
|
672
|
+
// a few lines up.
|
|
673
|
+
//
|
|
674
|
+
// It is not merely impossible here, it is meaningless: in consumer mode the
|
|
675
|
+
// CLI came from the registry this run, so there is no staged copy for it to
|
|
676
|
+
// have drifted from. The freshness label records CONSUMER_FINGERPRINT for
|
|
677
|
+
// the same reason.
|
|
678
|
+
//
|
|
679
|
+
// Measured 2026-09-26: npm-consumer-smoke run 7335 died at 174s with
|
|
680
|
+
// `ENOENT ... node_modules/@celilo/e2e/.npm-registry-cache/pack-manifest.json`,
|
|
681
|
+
// which failed PR #1437 and blocked the release the fleet was waiting on.
|
|
682
|
+
// The check landed in 00e2367d for celilo#1423 (image-freshness certifying a
|
|
683
|
+
// stale CLI) — a real fix whose guard was written for monorepo mode only.
|
|
684
|
+
if (findMonorepoRoot(PACKAGE_ROOT)) {
|
|
685
|
+
await verifyInstalledCliIsStaged(exec, containerId);
|
|
686
|
+
} else {
|
|
687
|
+
console.log(' installed CLI = staged skipped (consumer mode: nothing was staged to compare)');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
process.stdout.write(' docker commit .......... ');
|
|
691
|
+
const t5 = Date.now();
|
|
359
692
|
const superseded = imageIdOnTag('celilo-e2e/management:latest');
|
|
360
693
|
// The committed container runs under the sim topology's compose env, and
|
|
361
694
|
// commit captures its Env. Since the default managementZone became
|
|
@@ -377,11 +710,30 @@ async function bakeViaSim(): Promise<void> {
|
|
|
377
710
|
);
|
|
378
711
|
console.log(`✔ ${Math.round((Date.now() - t5) / 1000)}s`);
|
|
379
712
|
removeSupersededImage(superseded, 'celilo-e2e/management:latest');
|
|
713
|
+
} catch (err) {
|
|
714
|
+
bakeFailed = true;
|
|
715
|
+
throw err;
|
|
380
716
|
} finally {
|
|
381
717
|
process.stdout.write(' tearing down ........... ');
|
|
382
718
|
const tDown = Date.now();
|
|
383
719
|
await net.stop();
|
|
384
720
|
console.log(`✔ ${Math.round((Date.now() - tDown) / 1000)}s`);
|
|
721
|
+
if (bakeFailed) {
|
|
722
|
+
// A failed bake must not leave the shared stack live. On the builder the
|
|
723
|
+
// job dies at this step and the workflow's `if: always()` teardown steps
|
|
724
|
+
// never run (observed on runs 6645/6682/6693/6698: the log ends at
|
|
725
|
+
// "Cleaning up container for job"), so the 13-container shared stack
|
|
726
|
+
// then refuses every full run for the next 60 minutes under the
|
|
727
|
+
// orphan-age gate in live-stack.ts. Success keeps the stack: the Mac dev
|
|
728
|
+
// flow reuses it and the workflow's teardown step owns the builder's
|
|
729
|
+
// success path. (celilo#1365, celilo#1320)
|
|
730
|
+
try {
|
|
731
|
+
await stopSharedInfra();
|
|
732
|
+
} catch {
|
|
733
|
+
// best-effort: the workflow teardown or the next run's sweep is the
|
|
734
|
+
// backstop, and hiding the reason here beats failing twice.
|
|
735
|
+
}
|
|
736
|
+
}
|
|
385
737
|
}
|
|
386
738
|
|
|
387
739
|
console.log('');
|
|
@@ -6,6 +6,17 @@ set -e
|
|
|
6
6
|
# Generate SSH keys if needed
|
|
7
7
|
/config/ssh/generate-keys.sh /ssh-keys
|
|
8
8
|
|
|
9
|
+
# Serve the remote API. `celilo --remote` and celilo-web-console both arrive
|
|
10
|
+
# as an ssh forced command; without sshd the console is granted in the
|
|
11
|
+
# database and cannot open a connection (celilo#1442). Host keys are generated
|
|
12
|
+
# per container -- clients in this rig do not pin them.
|
|
13
|
+
ssh-keygen -A >/dev/null 2>&1 || true
|
|
14
|
+
mkdir -p /run/sshd
|
|
15
|
+
# Not fatal: this script runs under `set -e`, and letting a failed sshd abort
|
|
16
|
+
# it would take the whole management box down -- turning one suite's remote-API
|
|
17
|
+
# problem into every suite failing with no container.
|
|
18
|
+
/usr/sbin/sshd || echo "WARNING: sshd did not start; the remote API is unreachable" >&2
|
|
19
|
+
|
|
9
20
|
# Set up SSH
|
|
10
21
|
mkdir -p /root/.ssh
|
|
11
22
|
cp /ssh-keys/id_ed25519 /root/.ssh/id_ed25519
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Start
|
|
2
|
+
# Start the S3 simulator and pre-create the backup bucket.
|
|
3
|
+
#
|
|
4
|
+
# ⚠️ The server is s3proxy, not MinIO — see Dockerfile.minio's header for why
|
|
5
|
+
# (MinIO's images stopped being anonymously pullable from both Docker Hub and
|
|
6
|
+
# quay.io; celilo#1389). The hostname `minio.lab` and this file's name are
|
|
7
|
+
# deliberately unchanged.
|
|
3
8
|
#
|
|
4
9
|
# celilo's S3 provider never creates buckets (its initialize() only probes via
|
|
5
10
|
# ListObjectsV2), so the bucket must exist before `storage add s3` verifies or
|
|
6
|
-
# `backup --storage` uploads.
|
|
7
|
-
# `mc mb --ignore-existing`.
|
|
11
|
+
# `backup --storage` uploads.
|
|
8
12
|
set -e
|
|
9
13
|
|
|
10
14
|
# Route the customer's public prefix via the ISP edge (fw-ext), like every
|
|
@@ -12,38 +16,29 @@ set -e
|
|
|
12
16
|
# across networks, so without this a reply to the customer's WAN address is
|
|
13
17
|
# dropped and `backup --storage` hangs. fw-ext is per-test, so this is
|
|
14
18
|
# expected to fail while shared infra runs alone — hence the tolerance.
|
|
15
|
-
# (`busybox ip` because the
|
|
19
|
+
# (`busybox ip` because the s3proxy image ships no iproute2.)
|
|
16
20
|
busybox ip route del default 2>/dev/null || true
|
|
17
21
|
busybox ip route add default via 100.64.0.1 2>/dev/null || true
|
|
18
22
|
|
|
23
|
+
# Pre-create the bucket. Under the jclouds filesystem backend a bucket IS a
|
|
24
|
+
# directory under JCLOUDS_FILESYSTEM_BASEDIR, so this needs no running server,
|
|
25
|
+
# no credentials and no client binary — which is the entire reason the
|
|
26
|
+
# `minio/mc` image is gone. Measured 2026-09-24: with only this mkdir,
|
|
27
|
+
# ListObjectsV2 on the bucket answers 200 and an object PUT/GET round-trips.
|
|
28
|
+
#
|
|
29
|
+
# It also means the bucket is ready BEFORE the server accepts a connection,
|
|
30
|
+
# so the old "wait up to 30s for the admin alias, then mc mb" loop is gone.
|
|
31
|
+
# There is no readiness race left to lose.
|
|
32
|
+
mkdir -p "${JCLOUDS_FILESYSTEM_BASEDIR}/${MINIO_BUCKET}"
|
|
33
|
+
echo "minio-startup: bucket '${MINIO_BUCKET}' ready at ${JCLOUDS_FILESYSTEM_BASEDIR}/${MINIO_BUCKET}"
|
|
34
|
+
|
|
19
35
|
# Serve the S3 API on :80 so the management box reaches it over the SAME
|
|
20
36
|
# proven HTTP egress path as the other public sims (apt-repo, npm-registry):
|
|
21
|
-
# fw-ext's Squid-bypass exception is scoped to dport 80/443
|
|
37
|
+
# fw-ext's Squid-bypass exception is scoped to dport 80/443 -> 100.64.0.0/24, so
|
|
22
38
|
# plain HTTP to minio.lab:80 forwards straight through (no TLS bump, no cert,
|
|
23
|
-
# no nonstandard-port filtering).
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
i=0
|
|
30
|
-
while [ "$i" -lt 30 ]; do
|
|
31
|
-
if mc alias set local "http://127.0.0.1:80" "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null 2>&1; then
|
|
32
|
-
ready=1
|
|
33
|
-
break
|
|
34
|
-
fi
|
|
35
|
-
i=$((i + 1))
|
|
36
|
-
sleep 1
|
|
37
|
-
done
|
|
38
|
-
|
|
39
|
-
if [ "$ready" -ne 1 ]; then
|
|
40
|
-
echo "minio-startup: MinIO did not become ready in 30s" >&2
|
|
41
|
-
exit 1
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
mc mb --ignore-existing "local/${MINIO_BUCKET}"
|
|
45
|
-
echo "minio-startup: bucket '${MINIO_BUCKET}' ready; MinIO serving on :80"
|
|
46
|
-
|
|
47
|
-
# Hand the foreground back to the server so the container stays up and signals
|
|
48
|
-
# propagate to it.
|
|
49
|
-
wait "$MINIO_PID"
|
|
39
|
+
# no nonstandard-port filtering). S3PROXY_ENDPOINT pins the listen address.
|
|
40
|
+
#
|
|
41
|
+
# exec so the server becomes PID 1's child directly and signals propagate; the
|
|
42
|
+
# old script backgrounded the server and `wait`ed on it, which was only needed
|
|
43
|
+
# because the bucket step ran after startup.
|
|
44
|
+
exec /opt/s3proxy/run-docker-container.sh
|
|
@@ -26,12 +26,19 @@ ip route add default via "$GATEWAY"
|
|
|
26
26
|
SPLIT_HORIZON_BLOCK=""
|
|
27
27
|
IFS=',' read -ra DOMAIN_LIST <<< "${DOMAINS}"
|
|
28
28
|
for d in "${DOMAIN_LIST[@]}"; do
|
|
29
|
-
SPLIT_HORIZON_BLOCK="${SPLIT_HORIZON_BLOCK} local-zone: \"${d}.\" redirect
|
|
29
|
+
SPLIT_HORIZON_BLOCK="${SPLIT_HORIZON_BLOCK} local-zone: \"${d}.\" redirect
|
|
30
|
+
local-data: \"${d}. IN A ${CADDY_IP}\"
|
|
31
|
+
"
|
|
30
32
|
done
|
|
31
33
|
|
|
32
34
|
# Generate unbound config with split-horizon for each test domain plus
|
|
33
35
|
# the standard simulator overrides (Pebble, registry).
|
|
34
|
-
|
|
36
|
+
# Quoted heredocs: nothing inside is expanded, so a backtick or a $ in a
|
|
37
|
+
# comment is text, not code. The split-horizon block is printed BETWEEN them
|
|
38
|
+
# as data. (A `internal` in a comment used to run as a command on every boot —
|
|
39
|
+
# celilo#1430.)
|
|
40
|
+
{
|
|
41
|
+
cat << 'EOF'
|
|
35
42
|
server:
|
|
36
43
|
interface: 0.0.0.0
|
|
37
44
|
access-control: 0.0.0.0/0 allow
|
|
@@ -41,7 +48,9 @@ server:
|
|
|
41
48
|
do-daemonize: no
|
|
42
49
|
do-not-query-localhost: no
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
EOF
|
|
52
|
+
printf '%s' "${SPLIT_HORIZON_BLOCK}"
|
|
53
|
+
cat << 'EOF'
|
|
45
54
|
# Mirror the public resolver's simulated endpoint overrides so that
|
|
46
55
|
# management uses Pebble for ACME and the e2e registry for modules.
|
|
47
56
|
local-data: "acme-v02.api.letsencrypt.org. IN A 100.64.0.100"
|
|
@@ -52,7 +61,6 @@ $(printf "${SPLIT_HORIZON_BLOCK}")
|
|
|
52
61
|
local-data: "npm-registry.lab. IN A 100.64.0.59"
|
|
53
62
|
# apt-repo simulator — serves the celilo + celilo-bootstrap .debs to the
|
|
54
63
|
# apt install step in the bootstrap-apt test. See SIMULATOR_IPS.APT_REPO.
|
|
55
|
-
# (No backticks in this heredoc — it is unquoted, so backticks execute.)
|
|
56
64
|
local-data: "apt.celilo.lab. IN A 100.64.0.60"
|
|
57
65
|
# MinIO S3 simulator — backup/restore target for the migration e2e.
|
|
58
66
|
# See SIMULATOR_IPS.MINIO.
|
|
@@ -60,10 +68,9 @@ $(printf "${SPLIT_HORIZON_BLOCK}")
|
|
|
60
68
|
# IP echo simulator — the rig's api.ipify.org. celilo's public_dns check
|
|
61
69
|
# asks it the one thing the fleet cannot ask itself: what address it is
|
|
62
70
|
# currently reachable at. See SIMULATOR_IPS.IP_ECHO.
|
|
63
|
-
# (Backtick-free on purpose — see the note above; this heredoc is unquoted.)
|
|
64
71
|
local-data: "ip-echo.lab. IN A 100.64.0.65"
|
|
65
72
|
# signal-cli daemon (real, unlinked) and its simulator counterpart. Both
|
|
66
|
-
# sit on the
|
|
73
|
+
# sit on the internal network — celilo-mgr reaches a notification
|
|
67
74
|
# transport locally; a transport needing public ingress could not tell
|
|
68
75
|
# you the ingress was broken.
|
|
69
76
|
local-data: "signal-cli.lab. IN A 10.226.1.90"
|
|
@@ -77,6 +84,7 @@ forward-zone:
|
|
|
77
84
|
name: "."
|
|
78
85
|
forward-addr: 203.0.113.1
|
|
79
86
|
EOF
|
|
87
|
+
} > /etc/unbound/unbound.conf
|
|
80
88
|
|
|
81
89
|
/usr/sbin/unbound -d &
|
|
82
90
|
|
package/config/socks/startup.sh
CHANGED
|
@@ -47,4 +47,50 @@ echo "nameserver $NAMESERVER" > /etc/resolv.conf
|
|
|
47
47
|
|
|
48
48
|
echo "socks-proxy ready: vantage=$VANTAGE gateway=$GATEWAY dns=$NAMESERVER" >&2
|
|
49
49
|
|
|
50
|
+
# TEACH DANTE ABOUT THE TUNNEL ONCE IT EXISTS.
|
|
51
|
+
#
|
|
52
|
+
# danted.conf names `external: eth0`, the interface dante binds OUTBOUND
|
|
53
|
+
# connections to. That is correct at startup and wrong the moment a test brings
|
|
54
|
+
# up a WireGuard tunnel: a destination routed over wg0 is then dialled from
|
|
55
|
+
# eth0's address, and the far side drops it. The symptom is that a client INSIDE
|
|
56
|
+
# this container reaches the target fine (ordinary routing picks wg0) while the
|
|
57
|
+
# same target through the SOCKS port times out — so the proxy looks up, the
|
|
58
|
+
# tunnel looks up, and only the relay fails.
|
|
59
|
+
#
|
|
60
|
+
# Measured 2026-09-26 (celilo#1438): celilo-web-console-oidc stage 6 passed
|
|
61
|
+
# (its curl runs inside the container) and stage 7 failed with
|
|
62
|
+
# `net::ERR_TIMED_OUT` on the identical URL through the proxy. From the host:
|
|
63
|
+
# inside=302, via-socks=000.
|
|
64
|
+
#
|
|
65
|
+
# wg0 does not exist when danted starts, so it cannot simply be listed. This
|
|
66
|
+
# watches for it, adds it, and reloads — and VERIFIES the config first, because
|
|
67
|
+
# a danted that dies on a bad reload takes the proxy out for every suite.
|
|
68
|
+
(
|
|
69
|
+
while :; do
|
|
70
|
+
if ip link show wg0 >/dev/null 2>&1; then
|
|
71
|
+
if ! grep -q '^external: wg0' /etc/danted.conf; then
|
|
72
|
+
cp /etc/danted.conf /tmp/danted.conf.pre-wg0
|
|
73
|
+
sed -i 's|^external: eth0$|external: eth0\nexternal: wg0\nexternal.rotation: route|' /etc/danted.conf
|
|
74
|
+
# READ THE OUTPUT, NOT THE EXIT CODE. `danted -V` exits 0 even on a
|
|
75
|
+
# config it refuses outright — measured 2026-09-26: a config naming a
|
|
76
|
+
# nonexistent interface logged `error: ... could not resolve hostname`
|
|
77
|
+
# and `alert: mother[1/1]: shutting down`, and still returned 0. A guard
|
|
78
|
+
# keyed on the status would have HUPped a config danted cannot load and
|
|
79
|
+
# killed the proxy for every suite, which is the thing it exists to stop.
|
|
80
|
+
/usr/sbin/danted -V -f /etc/danted.conf >/tmp/danted-verify.log 2>&1 || true
|
|
81
|
+
if ! grep -qiE '(^|[^a-z])(error|alert):' /tmp/danted-verify.log; then
|
|
82
|
+
kill -HUP 1 2>/dev/null || true
|
|
83
|
+
echo "socks-proxy: wg0 up, danted now routes outbound by route" >&2
|
|
84
|
+
else
|
|
85
|
+
cp /tmp/danted.conf.pre-wg0 /etc/danted.conf
|
|
86
|
+
echo "socks-proxy: REFUSED to reload danted, config invalid — proxy left on eth0 only:" >&2
|
|
87
|
+
tail -3 /tmp/danted-verify.log >&2
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
break
|
|
91
|
+
fi
|
|
92
|
+
sleep 1
|
|
93
|
+
done
|
|
94
|
+
) &
|
|
95
|
+
|
|
50
96
|
exec /usr/sbin/danted -f /etc/danted.conf
|