@iamken/cloudtunnel 0.11.4 → 0.12.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/README.md +5 -1
- package/dist/{chunk-OT3UCBBF.js → chunk-J6FFC5ZO.js} +2 -2
- package/dist/{chunk-FILVDOPL.js → chunk-XKPTCMTJ.js} +2 -2
- package/dist/{chunk-7XACBYL7.js → chunk-ZIMTYVHN.js} +12 -4
- package/dist/chunk-ZIMTYVHN.js.map +1 -0
- package/dist/{dns-BNDEFUCM.js → dns-VUPKRH46.js} +3 -3
- package/dist/index.js +672 -199
- package/dist/index.js.map +1 -1
- package/dist/zones-CF4XFFZZ.js +11 -0
- package/package.json +1 -1
- package/dist/chunk-7XACBYL7.js.map +0 -1
- package/dist/zones-XNCXYCB4.js +0 -11
- /package/dist/{chunk-OT3UCBBF.js.map → chunk-J6FFC5ZO.js.map} +0 -0
- /package/dist/{chunk-FILVDOPL.js.map → chunk-XKPTCMTJ.js.map} +0 -0
- /package/dist/{dns-BNDEFUCM.js.map → dns-VUPKRH46.js.map} +0 -0
- /package/dist/{zones-XNCXYCB4.js.map → zones-CF4XFFZZ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
listZones,
|
|
4
4
|
resolveZone
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-J6FFC5ZO.js";
|
|
6
6
|
import {
|
|
7
7
|
createCname,
|
|
8
8
|
deleteDnsRecord,
|
|
9
9
|
findCname,
|
|
10
10
|
findDnsRecords,
|
|
11
11
|
isManagedDns
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XKPTCMTJ.js";
|
|
13
13
|
import {
|
|
14
14
|
CliError,
|
|
15
15
|
DEFAULT_API_BASE,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
binDir,
|
|
19
19
|
cfPaginate,
|
|
20
20
|
cfRequest,
|
|
21
|
+
configDir,
|
|
21
22
|
configFile,
|
|
22
23
|
confirm,
|
|
23
24
|
dim,
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
say,
|
|
43
44
|
scanCacheFile,
|
|
44
45
|
selectOne
|
|
45
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-ZIMTYVHN.js";
|
|
46
47
|
|
|
47
48
|
// src/index.ts
|
|
48
49
|
import { Command } from "commander";
|
|
@@ -50,7 +51,7 @@ import { createRequire } from "module";
|
|
|
50
51
|
import pc2 from "picocolors";
|
|
51
52
|
|
|
52
53
|
// src/config/legacy-migrate.ts
|
|
53
|
-
import { existsSync as existsSync3, readFileSync, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
54
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
54
55
|
|
|
55
56
|
// src/core/service.ts
|
|
56
57
|
import { join as join5 } from "path";
|
|
@@ -134,6 +135,14 @@ function formatTunnelSpec(s) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// src/core/service-exec.ts
|
|
138
|
+
var SERVICE_SCHEMA = 2;
|
|
139
|
+
var SCHEMA_RE = /cloudtunnel-schema:\s*(\d+)/;
|
|
140
|
+
var schemaMarker = `cloudtunnel-schema: ${SERVICE_SCHEMA}`;
|
|
141
|
+
function schemaOf(text2) {
|
|
142
|
+
const m = SCHEMA_RE.exec(text2);
|
|
143
|
+
return m ? Number(m[1]) : 1;
|
|
144
|
+
}
|
|
145
|
+
var unxml = (s) => s.replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
137
146
|
var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
|
|
138
147
|
var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
139
148
|
function buildUpArgs(p) {
|
|
@@ -192,19 +201,25 @@ __export(service_systemd_exports, {
|
|
|
192
201
|
install: () => install,
|
|
193
202
|
label: () => label,
|
|
194
203
|
legacyUnitExists: () => legacyUnitExists,
|
|
204
|
+
listInstalled: () => listInstalled,
|
|
205
|
+
parseUnit: () => parseUnit,
|
|
206
|
+
prepareChanges: () => prepareChanges,
|
|
195
207
|
removeLegacyUnit: () => removeLegacyUnit,
|
|
196
208
|
state: () => state,
|
|
197
|
-
uninstall: () => uninstall
|
|
209
|
+
uninstall: () => uninstall,
|
|
210
|
+
uninstallLabel: () => uninstallLabel
|
|
198
211
|
});
|
|
199
212
|
import { execFileSync } from "child_process";
|
|
200
|
-
import { existsSync, writeFileSync } from "fs";
|
|
213
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
201
214
|
import { tmpdir } from "os";
|
|
202
215
|
import { dirname, join as join2 } from "path";
|
|
216
|
+
var UNIT_DIR = "/etc/systemd/system";
|
|
203
217
|
var label = (fqdn) => `cloudtunnel-${serviceSlug(fqdn)}.service`;
|
|
204
|
-
var unitPath = (fqdn) =>
|
|
218
|
+
var unitPath = (fqdn) => `${UNIT_DIR}/${label(fqdn)}`;
|
|
205
219
|
function buildUnit(d) {
|
|
206
220
|
const nodeBin = dirname(d.nodePath);
|
|
207
221
|
return [
|
|
222
|
+
`# ${schemaMarker}`,
|
|
208
223
|
"[Unit]",
|
|
209
224
|
`Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
|
|
210
225
|
"After=network-online.target",
|
|
@@ -219,6 +234,8 @@ function buildUnit(d) {
|
|
|
219
234
|
`ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
|
|
220
235
|
"Restart=always",
|
|
221
236
|
"RestartSec=5",
|
|
237
|
+
"KillMode=mixed",
|
|
238
|
+
"TimeoutStopSec=15",
|
|
222
239
|
"",
|
|
223
240
|
"[Install]",
|
|
224
241
|
"WantedBy=multi-user.target",
|
|
@@ -247,20 +264,49 @@ function assertSupported() {
|
|
|
247
264
|
}
|
|
248
265
|
function install(d) {
|
|
249
266
|
assertSupported();
|
|
250
|
-
const
|
|
267
|
+
const name = label(d.fqdn);
|
|
268
|
+
const tmp = join2(tmpdir(), name);
|
|
251
269
|
writeFileSync(tmp, buildUnit(d), { mode: 420 });
|
|
252
270
|
privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
|
|
253
271
|
privileged(["systemctl", "daemon-reload"]);
|
|
254
|
-
privileged(["systemctl", "enable",
|
|
272
|
+
privileged(["systemctl", "enable", name]);
|
|
273
|
+
privileged(["systemctl", "restart", name]);
|
|
255
274
|
}
|
|
256
275
|
function uninstall(fqdn) {
|
|
276
|
+
uninstallLabel(label(fqdn));
|
|
277
|
+
}
|
|
278
|
+
function uninstallLabel(name) {
|
|
257
279
|
try {
|
|
258
|
-
privileged(["systemctl", "disable", "--now",
|
|
280
|
+
privileged(["systemctl", "disable", "--now", name]);
|
|
259
281
|
} catch {
|
|
260
282
|
}
|
|
261
|
-
privileged(["rm", "-f",
|
|
283
|
+
privileged(["rm", "-f", `${UNIT_DIR}/${name}`]);
|
|
262
284
|
privileged(["systemctl", "daemon-reload"]);
|
|
263
285
|
}
|
|
286
|
+
function prepareChanges() {
|
|
287
|
+
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
288
|
+
if (!isRoot) execFileSync("sudo", ["-v"], { stdio: "inherit" });
|
|
289
|
+
}
|
|
290
|
+
function parseUnit(name, text2, running) {
|
|
291
|
+
const exec = /^ExecStart=(.+)$/m.exec(text2)?.[1]?.trim();
|
|
292
|
+
return { label: name, command: exec ? exec.split(/\s+/) : [], schema: schemaOf(text2), running };
|
|
293
|
+
}
|
|
294
|
+
function listInstalled() {
|
|
295
|
+
let files = [];
|
|
296
|
+
try {
|
|
297
|
+
files = readdirSync(UNIT_DIR).filter((f) => /^cloudtunnel-.+\.service$/.test(f));
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
return files.map((f) => {
|
|
302
|
+
let text2 = "";
|
|
303
|
+
try {
|
|
304
|
+
text2 = readFileSync(`${UNIT_DIR}/${f}`, "utf8");
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
return parseUnit(f, text2, ["active", "activating", "reloading"].includes(query(["is-active", f])));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
264
310
|
function state(fqdn) {
|
|
265
311
|
const name = label(fqdn);
|
|
266
312
|
if (query(["is-active", name]) === "active") return "active";
|
|
@@ -289,11 +335,15 @@ __export(service_launchd_exports, {
|
|
|
289
335
|
buildPlist: () => buildPlist,
|
|
290
336
|
install: () => install2,
|
|
291
337
|
label: () => label2,
|
|
338
|
+
listInstalled: () => listInstalled2,
|
|
339
|
+
parsePlist: () => parsePlist,
|
|
340
|
+
prepareChanges: () => prepareChanges2,
|
|
292
341
|
state: () => state2,
|
|
293
|
-
uninstall: () => uninstall2
|
|
342
|
+
uninstall: () => uninstall2,
|
|
343
|
+
uninstallLabel: () => uninstallLabel2
|
|
294
344
|
});
|
|
295
345
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
296
|
-
import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
346
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
297
347
|
import { dirname as dirname2, join as join3 } from "path";
|
|
298
348
|
import os2 from "os";
|
|
299
349
|
var label2 = (fqdn) => `com.cloudtunnel.${serviceSlug(fqdn)}`;
|
|
@@ -308,6 +358,7 @@ function buildPlist(d) {
|
|
|
308
358
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
309
359
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
310
360
|
'<plist version="1.0">',
|
|
361
|
+
`<!-- ${schemaMarker} -->`,
|
|
311
362
|
"<dict>",
|
|
312
363
|
` <key>Label</key><string>${xml(label2(d.fqdn))}</string>`,
|
|
313
364
|
" <key>ProgramArguments</key>",
|
|
@@ -316,6 +367,7 @@ function buildPlist(d) {
|
|
|
316
367
|
" </array>",
|
|
317
368
|
" <key>RunAtLoad</key><true/>",
|
|
318
369
|
" <key>KeepAlive</key><true/>",
|
|
370
|
+
" <key>ThrottleInterval</key><integer>10</integer>",
|
|
319
371
|
" <key>EnvironmentVariables</key>",
|
|
320
372
|
" <dict>",
|
|
321
373
|
` <key>PATH</key><string>${xml(path)}</string>`,
|
|
@@ -336,6 +388,11 @@ function launchctl(args) {
|
|
|
336
388
|
return out ? out.toString() : "";
|
|
337
389
|
}
|
|
338
390
|
}
|
|
391
|
+
function waitUnloaded(agentLabel) {
|
|
392
|
+
for (let i = 0; i < 30 && launchctl(["list", agentLabel]).trim(); i++) {
|
|
393
|
+
execFileSync2("sleep", ["0.5"]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
339
396
|
function assertSupported2() {
|
|
340
397
|
}
|
|
341
398
|
function install2(d) {
|
|
@@ -344,13 +401,41 @@ function install2(d) {
|
|
|
344
401
|
const plist = plistPath(d.fqdn);
|
|
345
402
|
writeFileSync2(plist, buildPlist(d), { mode: 420 });
|
|
346
403
|
launchctl(["unload", "-w", plist]);
|
|
404
|
+
waitUnloaded(label2(d.fqdn));
|
|
347
405
|
execFileSync2("launchctl", ["load", "-w", plist], { stdio: "inherit" });
|
|
348
406
|
}
|
|
349
407
|
function uninstall2(fqdn) {
|
|
350
|
-
|
|
408
|
+
uninstallLabel2(label2(fqdn));
|
|
409
|
+
}
|
|
410
|
+
function uninstallLabel2(agentLabel) {
|
|
411
|
+
const plist = join3(agentsDir(), `${agentLabel}.plist`);
|
|
351
412
|
launchctl(["unload", "-w", plist]);
|
|
352
413
|
rmSync(plist, { force: true });
|
|
353
414
|
}
|
|
415
|
+
function prepareChanges2() {
|
|
416
|
+
}
|
|
417
|
+
function parsePlist(agentLabel, text2, running) {
|
|
418
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text2)?.[1] ?? "";
|
|
419
|
+
const command = [...block.matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unxml(m[1] ?? ""));
|
|
420
|
+
return { label: agentLabel, command, schema: schemaOf(text2), running };
|
|
421
|
+
}
|
|
422
|
+
function listInstalled2() {
|
|
423
|
+
let files = [];
|
|
424
|
+
try {
|
|
425
|
+
files = readdirSync2(agentsDir()).filter((f) => /^com\.cloudtunnel\..+\.plist$/.test(f));
|
|
426
|
+
} catch {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
return files.map((f) => {
|
|
430
|
+
const agentLabel = f.replace(/\.plist$/, "");
|
|
431
|
+
let text2 = "";
|
|
432
|
+
try {
|
|
433
|
+
text2 = readFileSync2(join3(agentsDir(), f), "utf8");
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
return parsePlist(agentLabel, text2, /"PID"\s*=/.test(launchctl(["list", agentLabel])));
|
|
437
|
+
});
|
|
438
|
+
}
|
|
354
439
|
function state2(fqdn) {
|
|
355
440
|
const info = launchctl(["list", label2(fqdn)]);
|
|
356
441
|
if (/"PID"\s*=/.test(info)) return "active";
|
|
@@ -364,8 +449,12 @@ __export(service_windows_exports, {
|
|
|
364
449
|
buildTaskXml: () => buildTaskXml,
|
|
365
450
|
install: () => install3,
|
|
366
451
|
label: () => label3,
|
|
452
|
+
listInstalled: () => listInstalled3,
|
|
453
|
+
parseTaskXml: () => parseTaskXml,
|
|
454
|
+
prepareChanges: () => prepareChanges3,
|
|
367
455
|
state: () => state3,
|
|
368
|
-
uninstall: () => uninstall3
|
|
456
|
+
uninstall: () => uninstall3,
|
|
457
|
+
uninstallLabel: () => uninstallLabel3
|
|
369
458
|
});
|
|
370
459
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
371
460
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -378,7 +467,7 @@ function buildTaskXml(d) {
|
|
|
378
467
|
return [
|
|
379
468
|
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
380
469
|
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
381
|
-
` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel)</Description></RegistrationInfo>`,
|
|
470
|
+
` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel) [${schemaMarker}]</Description></RegistrationInfo>`,
|
|
382
471
|
` <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml2(d.user)}</UserId></LogonTrigger></Triggers>`,
|
|
383
472
|
` <Principals><Principal id="Author"><UserId>${xml2(d.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>`,
|
|
384
473
|
" <Settings>",
|
|
@@ -389,7 +478,7 @@ function buildTaskXml(d) {
|
|
|
389
478
|
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
|
|
390
479
|
" <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>",
|
|
391
480
|
" <Enabled>true</Enabled>",
|
|
392
|
-
"
|
|
481
|
+
"</Settings>",
|
|
393
482
|
' <Actions Context="Author">',
|
|
394
483
|
` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
|
|
395
484
|
" </Actions>",
|
|
@@ -409,12 +498,32 @@ function assertSupported3() {
|
|
|
409
498
|
}
|
|
410
499
|
function install3(d) {
|
|
411
500
|
const file = join4(tmpdir2(), `${d.slug}.task.xml`);
|
|
501
|
+
const name = label3(d.fqdn);
|
|
412
502
|
writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
|
|
413
|
-
|
|
414
|
-
schtasks
|
|
503
|
+
schtasks(["/End", "/TN", name]);
|
|
504
|
+
execFileSync3("schtasks", ["/Create", "/TN", name, "/XML", file, "/F"], { stdio: "inherit" });
|
|
505
|
+
schtasks(["/Run", "/TN", name]);
|
|
415
506
|
}
|
|
416
507
|
function uninstall3(fqdn) {
|
|
417
|
-
|
|
508
|
+
uninstallLabel3(label3(fqdn));
|
|
509
|
+
}
|
|
510
|
+
function uninstallLabel3(name) {
|
|
511
|
+
schtasks(["/End", "/TN", name]);
|
|
512
|
+
schtasks(["/Delete", "/TN", name, "/F"]);
|
|
513
|
+
}
|
|
514
|
+
function prepareChanges3() {
|
|
515
|
+
}
|
|
516
|
+
function splitArgs(args) {
|
|
517
|
+
return [...args.matchAll(/"([^"]*)"|(\S+)/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
518
|
+
}
|
|
519
|
+
function parseTaskXml(name, xmlText, running) {
|
|
520
|
+
const command = unxml(/<Command>([\s\S]*?)<\/Command>/.exec(xmlText)?.[1] ?? "");
|
|
521
|
+
const args = unxml(/<Arguments>([\s\S]*?)<\/Arguments>/.exec(xmlText)?.[1] ?? "");
|
|
522
|
+
return { label: name, command: command ? [command, ...splitArgs(args)] : [], schema: schemaOf(xmlText), running };
|
|
523
|
+
}
|
|
524
|
+
function listInstalled3() {
|
|
525
|
+
const names = schtasks(["/Query", "/FO", "CSV", "/NH"]).split(/\r?\n/).map((line) => /^"([^"]+)"/.exec(line)?.[1]?.replace(/^\\/, "")).filter((n) => !!n && /^cloudtunnel\\/i.test(n));
|
|
526
|
+
return [...new Set(names)].map((name) => parseTaskXml(name, schtasks(["/Query", "/TN", name, "/XML"]), true));
|
|
418
527
|
}
|
|
419
528
|
function state3(fqdn) {
|
|
420
529
|
const out = schtasks(["/Query", "/TN", label3(fqdn), "/FO", "LIST"]);
|
|
@@ -461,6 +570,15 @@ function installServiceForSpec(params) {
|
|
|
461
570
|
function uninstallService(fqdn) {
|
|
462
571
|
pick()?.uninstall(fqdn);
|
|
463
572
|
}
|
|
573
|
+
function listInstalledServices() {
|
|
574
|
+
return pick()?.listInstalled() ?? [];
|
|
575
|
+
}
|
|
576
|
+
function uninstallServiceLabel(label4) {
|
|
577
|
+
pick()?.uninstallLabel(label4);
|
|
578
|
+
}
|
|
579
|
+
function prepareServiceChanges() {
|
|
580
|
+
pick()?.prepareChanges();
|
|
581
|
+
}
|
|
464
582
|
function serviceState(fqdn) {
|
|
465
583
|
return pick()?.state(fqdn) ?? "none";
|
|
466
584
|
}
|
|
@@ -489,7 +607,7 @@ async function migrateLegacyProfiles() {
|
|
|
489
607
|
if (!existsSync3(profilesFile) || existsSync3(skipMarker)) return;
|
|
490
608
|
let profiles;
|
|
491
609
|
try {
|
|
492
|
-
profiles = JSON.parse(
|
|
610
|
+
profiles = JSON.parse(readFileSync3(profilesFile, "utf8"));
|
|
493
611
|
} catch {
|
|
494
612
|
return;
|
|
495
613
|
}
|
|
@@ -843,7 +961,7 @@ async function ensureAuth() {
|
|
|
843
961
|
// src/connector/binary.ts
|
|
844
962
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
845
963
|
import { createHash } from "crypto";
|
|
846
|
-
import { chmodSync, existsSync as existsSync4, readFileSync as
|
|
964
|
+
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
847
965
|
import { join as join6 } from "path";
|
|
848
966
|
import { gunzipSync } from "zlib";
|
|
849
967
|
var PINNED_VERSION = "2026.7.3";
|
|
@@ -874,7 +992,7 @@ function cachedPath() {
|
|
|
874
992
|
}
|
|
875
993
|
function isMusl() {
|
|
876
994
|
try {
|
|
877
|
-
return process.platform === "linux" &&
|
|
995
|
+
return process.platform === "linux" && readFileSync4("/usr/bin/ldd", "utf8").includes("musl");
|
|
878
996
|
} catch {
|
|
879
997
|
return false;
|
|
880
998
|
}
|
|
@@ -941,17 +1059,115 @@ function extractTgz(bytes) {
|
|
|
941
1059
|
});
|
|
942
1060
|
}
|
|
943
1061
|
|
|
1062
|
+
// src/cloudflare/tunnels.ts
|
|
1063
|
+
var MANAGED_TUNNEL_PREFIX = "ct-";
|
|
1064
|
+
function isManagedTunnel(tunnel) {
|
|
1065
|
+
return tunnel.name.startsWith(MANAGED_TUNNEL_PREFIX);
|
|
1066
|
+
}
|
|
1067
|
+
async function createTunnel(cf, name) {
|
|
1068
|
+
const env = await cfRequest(cf.token, "POST", `/accounts/${cf.accountId}/cfd_tunnel`, {
|
|
1069
|
+
name,
|
|
1070
|
+
config_src: "cloudflare"
|
|
1071
|
+
});
|
|
1072
|
+
return env.result;
|
|
1073
|
+
}
|
|
1074
|
+
function listTunnels(cf) {
|
|
1075
|
+
return cfPaginate(cf.token, `/accounts/${cf.accountId}/cfd_tunnel?is_deleted=false`);
|
|
1076
|
+
}
|
|
1077
|
+
async function getTunnel(cf, id) {
|
|
1078
|
+
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
|
|
1079
|
+
}
|
|
1080
|
+
async function deleteTunnel(cf, id) {
|
|
1081
|
+
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
|
|
1082
|
+
}
|
|
1083
|
+
async function cleanupConnections(cf, id) {
|
|
1084
|
+
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
|
|
1085
|
+
}
|
|
1086
|
+
async function deleteTunnelWithConnections(cf, id) {
|
|
1087
|
+
try {
|
|
1088
|
+
await deleteTunnel(cf, id);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
if (err instanceof CliError && /active connections/i.test(err.message)) {
|
|
1091
|
+
await cleanupConnections(cf, id);
|
|
1092
|
+
await deleteTunnel(cf, id);
|
|
1093
|
+
} else {
|
|
1094
|
+
throw err;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function getTunnelToken(cf, id) {
|
|
1099
|
+
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}/token`)).result;
|
|
1100
|
+
}
|
|
1101
|
+
async function putIngress(cf, id, ingress) {
|
|
1102
|
+
await cfRequest(cf.token, "PUT", `/accounts/${cf.accountId}/cfd_tunnel/${id}/configurations`, {
|
|
1103
|
+
config: { ingress }
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
async function getConnections(cf, id) {
|
|
1107
|
+
const env = await cfRequest(
|
|
1108
|
+
cf.token,
|
|
1109
|
+
"GET",
|
|
1110
|
+
`/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
|
|
1111
|
+
);
|
|
1112
|
+
return env.result ?? [];
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/connector/health.ts
|
|
1116
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1117
|
+
async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
1118
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
1119
|
+
while (Date.now() < deadline) {
|
|
1120
|
+
if (opts.signal?.aborted) return "dead";
|
|
1121
|
+
try {
|
|
1122
|
+
const connections = await getConnections(cf, tunnelId);
|
|
1123
|
+
if (connections.length > 0) return "healthy";
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
await sleep(2e3);
|
|
1127
|
+
}
|
|
1128
|
+
return opts.signal?.aborted ? "dead" : "provisioning";
|
|
1129
|
+
}
|
|
1130
|
+
function extractMetricsPort(log) {
|
|
1131
|
+
const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
|
|
1132
|
+
if (!matches.length) return void 0;
|
|
1133
|
+
const last = matches[matches.length - 1];
|
|
1134
|
+
const port = Number(last?.[1]);
|
|
1135
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
1136
|
+
}
|
|
1137
|
+
async function checkLocalReady(port, opts = {}) {
|
|
1138
|
+
const controller = new AbortController();
|
|
1139
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
|
|
1140
|
+
try {
|
|
1141
|
+
const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
|
|
1142
|
+
if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
|
|
1143
|
+
const data = await res.json().catch(() => ({}));
|
|
1144
|
+
const readyConnections = Number(data.readyConnections) || 0;
|
|
1145
|
+
return { healthy: readyConnections > 0, readyConnections, status: res.status };
|
|
1146
|
+
} catch {
|
|
1147
|
+
return { healthy: false, readyConnections: 0 };
|
|
1148
|
+
} finally {
|
|
1149
|
+
clearTimeout(timer);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function probeEntryState(entry, gone) {
|
|
1153
|
+
if (gone || entry.state !== "running" || !entry.pid) return "down";
|
|
1154
|
+
if (!entry.metricsPort) return "up";
|
|
1155
|
+
const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
|
|
1156
|
+
return ready.healthy ? "up" : "reconnecting";
|
|
1157
|
+
}
|
|
1158
|
+
|
|
944
1159
|
// src/core/up-runner.ts
|
|
945
|
-
import { join as
|
|
946
|
-
import { readFileSync as
|
|
1160
|
+
import { join as join9 } from "path";
|
|
1161
|
+
import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
|
|
947
1162
|
import * as clack2 from "@clack/prompts";
|
|
948
1163
|
|
|
949
1164
|
// src/connector/process.ts
|
|
950
1165
|
import { execFileSync as execFileSync7, spawn as spawn2 } from "child_process";
|
|
951
|
-
import { openSync } from "fs";
|
|
1166
|
+
import { openSync, renameSync as renameSync3, statSync } from "fs";
|
|
1167
|
+
import { createServer } from "net";
|
|
952
1168
|
|
|
953
1169
|
// src/connector/registry.ts
|
|
954
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1170
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
955
1171
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
956
1172
|
import { readFile } from "fs/promises";
|
|
957
1173
|
import os3 from "os";
|
|
@@ -961,7 +1177,7 @@ function entryFqdn(e) {
|
|
|
961
1177
|
}
|
|
962
1178
|
function currentBootId() {
|
|
963
1179
|
try {
|
|
964
|
-
return
|
|
1180
|
+
return readFileSync5("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
965
1181
|
} catch {
|
|
966
1182
|
if (process.platform === "darwin") {
|
|
967
1183
|
try {
|
|
@@ -976,7 +1192,7 @@ function currentBootId() {
|
|
|
976
1192
|
}
|
|
977
1193
|
function readRegistry() {
|
|
978
1194
|
try {
|
|
979
|
-
return JSON.parse(
|
|
1195
|
+
return JSON.parse(readFileSync5(registryFile, "utf8"));
|
|
980
1196
|
} catch {
|
|
981
1197
|
return {};
|
|
982
1198
|
}
|
|
@@ -1066,14 +1282,17 @@ async function isOurConnector(entry) {
|
|
|
1066
1282
|
}
|
|
1067
1283
|
return true;
|
|
1068
1284
|
}
|
|
1285
|
+
function markConnectorRunning(fqdn, patch) {
|
|
1286
|
+
return patchEntry(fqdn, { bootId: currentBootId(), ...patch, state: "running" });
|
|
1287
|
+
}
|
|
1069
1288
|
async function reconcile() {
|
|
1070
1289
|
const entries = listEntries();
|
|
1071
1290
|
for (const entry of entries) {
|
|
1072
|
-
if (entry.state === "running" && !await isOurConnector(entry)) {
|
|
1291
|
+
if (entry.state === "running" && entry.pid && !await isOurConnector(entry)) {
|
|
1073
1292
|
const fqdn = entryFqdn(entry);
|
|
1074
1293
|
await mutateRegistry((reg) => {
|
|
1075
1294
|
const e = reg[fqdn];
|
|
1076
|
-
if (e) {
|
|
1295
|
+
if (e && e.state === "running" && e.pid === entry.pid) {
|
|
1077
1296
|
e.state = "stopped";
|
|
1078
1297
|
delete e.pid;
|
|
1079
1298
|
}
|
|
@@ -1084,11 +1303,31 @@ async function reconcile() {
|
|
|
1084
1303
|
}
|
|
1085
1304
|
|
|
1086
1305
|
// src/connector/process.ts
|
|
1087
|
-
var
|
|
1306
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1307
|
+
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
1308
|
+
function pickFreePort() {
|
|
1309
|
+
return new Promise((resolve, reject) => {
|
|
1310
|
+
const srv = createServer();
|
|
1311
|
+
srv.unref();
|
|
1312
|
+
srv.on("error", reject);
|
|
1313
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
1314
|
+
const addr = srv.address();
|
|
1315
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1316
|
+
srv.close(() => port ? resolve(port) : reject(new Error("no free port")));
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
function rotateLog(logFile) {
|
|
1321
|
+
try {
|
|
1322
|
+
if (statSync(logFile).size > MAX_LOG_BYTES) renameSync3(logFile, `${logFile}.1`);
|
|
1323
|
+
} catch {
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1088
1326
|
function startConnector(opts) {
|
|
1089
|
-
const args = ["tunnel", "--metrics",
|
|
1327
|
+
const args = ["tunnel", "--metrics", `127.0.0.1:${opts.metricsPort ?? 0}`, "run"];
|
|
1090
1328
|
if (opts.protocol) args.push("--protocol", opts.protocol);
|
|
1091
1329
|
const env = { ...process.env, TUNNEL_TOKEN: opts.token };
|
|
1330
|
+
rotateLog(opts.logFile);
|
|
1092
1331
|
const fd = openSync(opts.logFile, "a", 384);
|
|
1093
1332
|
const child = spawn2(opts.bin, args, { env, detached: opts.detach, stdio: ["ignore", fd, fd] });
|
|
1094
1333
|
if (!child.pid) throw new CliError("Failed to start the cloudflared connector.");
|
|
@@ -1116,7 +1355,7 @@ async function stopConnector(entry) {
|
|
|
1116
1355
|
} catch {
|
|
1117
1356
|
return false;
|
|
1118
1357
|
}
|
|
1119
|
-
await
|
|
1358
|
+
await sleep2(3e3);
|
|
1120
1359
|
if (await isOurConnector(entry)) {
|
|
1121
1360
|
try {
|
|
1122
1361
|
process.kill(pid, "SIGKILL");
|
|
@@ -1126,101 +1365,52 @@ async function stopConnector(entry) {
|
|
|
1126
1365
|
return true;
|
|
1127
1366
|
}
|
|
1128
1367
|
|
|
1129
|
-
// src/
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1368
|
+
// src/connector/hostname-lock.ts
|
|
1369
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1370
|
+
import { join as join7 } from "path";
|
|
1371
|
+
import lockfile2 from "proper-lockfile";
|
|
1372
|
+
var locksDir = join7(configDir, "locks");
|
|
1373
|
+
var STALE_MS = 15e3;
|
|
1374
|
+
var UPDATE_MS = 5e3;
|
|
1375
|
+
var lockPath = (fqdn) => join7(locksDir, serviceSlug(fqdn));
|
|
1376
|
+
function busyError(fqdn) {
|
|
1377
|
+
return new CliError(`${fqdn} is already being served by another cloudtunnel process.`, {
|
|
1378
|
+
hint: "a boot service may own it \u2014 see `cloudtunnel ls`; release it with `cloudtunnel delete <#>`"
|
|
1138
1379
|
});
|
|
1139
|
-
return env.result;
|
|
1140
1380
|
}
|
|
1141
|
-
function
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
async function getTunnel(cf, id) {
|
|
1145
|
-
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
|
|
1146
|
-
}
|
|
1147
|
-
async function deleteTunnel(cf, id) {
|
|
1148
|
-
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
|
|
1149
|
-
}
|
|
1150
|
-
async function cleanupConnections(cf, id) {
|
|
1151
|
-
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
|
|
1152
|
-
}
|
|
1153
|
-
async function deleteTunnelWithConnections(cf, id) {
|
|
1381
|
+
async function acquireHostnameLock(fqdn, opts = {}) {
|
|
1382
|
+
mkdirSync2(locksDir, { recursive: true, mode: 448 });
|
|
1383
|
+
const waits = Math.ceil((opts.waitMs ?? 0) / 1e3);
|
|
1154
1384
|
try {
|
|
1155
|
-
await
|
|
1385
|
+
await takeLock(fqdn, waits);
|
|
1156
1386
|
} catch (err) {
|
|
1157
|
-
if (err
|
|
1158
|
-
|
|
1159
|
-
await deleteTunnel(cf, id);
|
|
1160
|
-
} else {
|
|
1161
|
-
throw err;
|
|
1162
|
-
}
|
|
1387
|
+
if (err.code === "ELOCKED") throw busyError(fqdn);
|
|
1388
|
+
throw err;
|
|
1163
1389
|
}
|
|
1164
1390
|
}
|
|
1165
|
-
|
|
1166
|
-
return (
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
`/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
|
|
1178
|
-
);
|
|
1179
|
-
return env.result ?? [];
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// src/connector/health.ts
|
|
1183
|
-
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1184
|
-
async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
1185
|
-
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
1186
|
-
while (Date.now() < deadline) {
|
|
1187
|
-
if (opts.signal?.aborted) return "dead";
|
|
1188
|
-
try {
|
|
1189
|
-
const connections = await getConnections(cf, tunnelId);
|
|
1190
|
-
if (connections.length > 0) return "healthy";
|
|
1191
|
-
} catch {
|
|
1391
|
+
function takeLock(fqdn, waits) {
|
|
1392
|
+
return lockfile2.lock(lockPath(fqdn), {
|
|
1393
|
+
realpath: false,
|
|
1394
|
+
stale: STALE_MS,
|
|
1395
|
+
update: UPDATE_MS,
|
|
1396
|
+
retries: waits > 0 ? { retries: waits, minTimeout: 1e3, maxTimeout: 1e3 } : 0,
|
|
1397
|
+
onCompromised: () => {
|
|
1398
|
+
say.debug(`[lock] ${fqdn} lock was compromised; re-taking it`);
|
|
1399
|
+
takeLock(fqdn, 0).catch((err) => {
|
|
1400
|
+
say.warn(`Lost ${fqdn} to another cloudtunnel process (${err.message}) \u2014 exiting.`);
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
});
|
|
1192
1403
|
}
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
return opts.signal?.aborted ? "dead" : "provisioning";
|
|
1196
|
-
}
|
|
1197
|
-
function extractMetricsPort(log) {
|
|
1198
|
-
const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
|
|
1199
|
-
if (!matches.length) return void 0;
|
|
1200
|
-
const last = matches[matches.length - 1];
|
|
1201
|
-
const port = Number(last?.[1]);
|
|
1202
|
-
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
1404
|
+
});
|
|
1203
1405
|
}
|
|
1204
|
-
async function
|
|
1205
|
-
|
|
1206
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
|
|
1406
|
+
async function assertHostnameFree(fqdn) {
|
|
1407
|
+
let held = false;
|
|
1207
1408
|
try {
|
|
1208
|
-
|
|
1209
|
-
if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
|
|
1210
|
-
const data = await res.json().catch(() => ({}));
|
|
1211
|
-
const readyConnections = Number(data.readyConnections) || 0;
|
|
1212
|
-
return { healthy: readyConnections > 0, readyConnections, status: res.status };
|
|
1409
|
+
held = await lockfile2.check(lockPath(fqdn), { realpath: false, stale: STALE_MS });
|
|
1213
1410
|
} catch {
|
|
1214
|
-
|
|
1215
|
-
} finally {
|
|
1216
|
-
clearTimeout(timer);
|
|
1411
|
+
held = false;
|
|
1217
1412
|
}
|
|
1218
|
-
|
|
1219
|
-
async function probeEntryState(entry, gone) {
|
|
1220
|
-
if (gone || entry.state !== "running" || !entry.pid) return "down";
|
|
1221
|
-
if (!entry.metricsPort) return "up";
|
|
1222
|
-
const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
|
|
1223
|
-
return ready.healthy ? "up" : "reconnecting";
|
|
1413
|
+
if (held) throw busyError(fqdn);
|
|
1224
1414
|
}
|
|
1225
1415
|
|
|
1226
1416
|
// src/core/orchestrator-create.ts
|
|
@@ -1296,6 +1486,10 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1296
1486
|
const host = resolveHostSpec(opts, opts.defaultZone);
|
|
1297
1487
|
const zone = await resolveZone(cf.token, host.zone);
|
|
1298
1488
|
const existingList = await findDnsRecords(cf.token, zone.id, host.hostname);
|
|
1489
|
+
if (opts.reuse) {
|
|
1490
|
+
const reused = await tryReuseTunnel(cf, zone.id, host, opts, existingList);
|
|
1491
|
+
if (reused) return reused;
|
|
1492
|
+
}
|
|
1299
1493
|
if (existingList.length > 0) {
|
|
1300
1494
|
const isTunnelRecord = existingList.every((r) => r.content.endsWith(".cfargotunnel.com"));
|
|
1301
1495
|
if (!isTunnelRecord && !opts.force) {
|
|
@@ -1309,9 +1503,14 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1309
1503
|
throw new CliError("Cancelled.", { exitCode: 130 });
|
|
1310
1504
|
}
|
|
1311
1505
|
}
|
|
1506
|
+
const prev = getEntry(host.hostname);
|
|
1507
|
+
if (prev) await stopConnector(prev);
|
|
1312
1508
|
for (const rec of existingList) {
|
|
1313
1509
|
await releaseHostname(cf, zone.id, rec);
|
|
1314
1510
|
}
|
|
1511
|
+
} else {
|
|
1512
|
+
const prev = getEntry(host.hostname);
|
|
1513
|
+
if (prev) await stopConnector(prev);
|
|
1315
1514
|
}
|
|
1316
1515
|
await upsertEntry(host.hostname, {
|
|
1317
1516
|
subdomain: host.subdomain,
|
|
@@ -1320,7 +1519,9 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1320
1519
|
port: opts.port,
|
|
1321
1520
|
proto: opts.proto,
|
|
1322
1521
|
host: opts.host,
|
|
1323
|
-
state: "provisioning"
|
|
1522
|
+
state: "provisioning",
|
|
1523
|
+
pid: void 0,
|
|
1524
|
+
metricsPort: void 0
|
|
1324
1525
|
});
|
|
1325
1526
|
let tunnelId;
|
|
1326
1527
|
let dnsRecordId;
|
|
@@ -1342,6 +1543,25 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1342
1543
|
throw err;
|
|
1343
1544
|
}
|
|
1344
1545
|
}
|
|
1546
|
+
async function tryReuseTunnel(cf, zoneId, host, opts, existing) {
|
|
1547
|
+
const prev = getEntry(host.hostname);
|
|
1548
|
+
const tunnelId = prev?.tunnelId;
|
|
1549
|
+
const record = existing[0];
|
|
1550
|
+
if (!prev || !tunnelId || existing.length !== 1 || record?.content !== `${tunnelId}.cfargotunnel.com`) return null;
|
|
1551
|
+
let tunnel;
|
|
1552
|
+
try {
|
|
1553
|
+
tunnel = await getTunnel(cf, tunnelId);
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
if (err instanceof CliError && err.status === 404) return null;
|
|
1556
|
+
throw err;
|
|
1557
|
+
}
|
|
1558
|
+
if (!isManagedTunnel(tunnel) || tunnel.deleted_at) return null;
|
|
1559
|
+
await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
|
|
1560
|
+
const token = await getTunnelToken(cf, tunnelId);
|
|
1561
|
+
await stopConnector(prev);
|
|
1562
|
+
await recordRunning(host, zoneId, tunnelId, record.id, opts);
|
|
1563
|
+
return { host, tunnelId, token };
|
|
1564
|
+
}
|
|
1345
1565
|
async function recordRunning(host, zoneId, tunnelId, dnsRecordId, opts) {
|
|
1346
1566
|
await upsertEntry(host.hostname, {
|
|
1347
1567
|
subdomain: host.subdomain,
|
|
@@ -1388,8 +1608,105 @@ async function rollback(cf, zoneId, tunnelId, dnsRecordId, hostname) {
|
|
|
1388
1608
|
return clean;
|
|
1389
1609
|
}
|
|
1390
1610
|
|
|
1611
|
+
// src/core/service-inventory.ts
|
|
1612
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1613
|
+
|
|
1614
|
+
// src/core/transport-protocol.ts
|
|
1615
|
+
function parseTransportProtocol(value) {
|
|
1616
|
+
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
1617
|
+
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/core/service-inventory.ts
|
|
1621
|
+
function flagValue(argv, flag) {
|
|
1622
|
+
const i = argv.indexOf(flag);
|
|
1623
|
+
return i >= 0 ? argv[i + 1] : void 0;
|
|
1624
|
+
}
|
|
1625
|
+
function parseServiceArgv(argv) {
|
|
1626
|
+
const zone = flagValue(argv, "-d");
|
|
1627
|
+
if (!zone) return void 0;
|
|
1628
|
+
const proto = flagValue(argv, "--proto") === "https" ? "https" : "http";
|
|
1629
|
+
try {
|
|
1630
|
+
if (argv[0] === "relay" && argv[1]) {
|
|
1631
|
+
return { command: "relay", subdomain: argv[1], port: 0, zone, proto };
|
|
1632
|
+
}
|
|
1633
|
+
if (argv[0] === "up" && argv[1]) {
|
|
1634
|
+
const spec = parseTunnelSpec(argv[1]);
|
|
1635
|
+
if (spec.subdomain === void 0) return void 0;
|
|
1636
|
+
const protocolFlag = flagValue(argv, "--protocol");
|
|
1637
|
+
return {
|
|
1638
|
+
subdomain: spec.subdomain,
|
|
1639
|
+
port: spec.port,
|
|
1640
|
+
host: spec.host,
|
|
1641
|
+
zone,
|
|
1642
|
+
proto,
|
|
1643
|
+
protocol: protocolFlag ? parseTransportProtocol(protocolFlag) : void 0
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
} catch {
|
|
1647
|
+
return void 0;
|
|
1648
|
+
}
|
|
1649
|
+
return void 0;
|
|
1650
|
+
}
|
|
1651
|
+
function classifyDefinition(def, canonicalLabel, exists = existsSync6) {
|
|
1652
|
+
const [nodePath, scriptPath, ...argv] = def.command;
|
|
1653
|
+
const params = parseServiceArgv(argv);
|
|
1654
|
+
if (!params) return void 0;
|
|
1655
|
+
const fqdn = fqdnFor(params.subdomain, params.zone);
|
|
1656
|
+
const canonical = def.label.toLowerCase() === canonicalLabel(fqdn).toLowerCase();
|
|
1657
|
+
let issue;
|
|
1658
|
+
if (!canonical) issue = "duplicate";
|
|
1659
|
+
else if (!nodePath || !scriptPath || !exists(nodePath) || !exists(scriptPath)) issue = "broken";
|
|
1660
|
+
else if (def.schema < SERVICE_SCHEMA || !argv.includes("--keep-alive")) issue = "stale";
|
|
1661
|
+
else if (!def.running) issue = "dead";
|
|
1662
|
+
return { label: def.label, fqdn, params, canonical, issue };
|
|
1663
|
+
}
|
|
1664
|
+
function inventoryServices() {
|
|
1665
|
+
return listInstalledServices().map((def) => classifyDefinition(def, serviceName)).filter((s) => !!s);
|
|
1666
|
+
}
|
|
1667
|
+
function planRepair(services, opts = {}) {
|
|
1668
|
+
const byFqdn = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const s of services) byFqdn.set(s.fqdn, [...byFqdn.get(s.fqdn) ?? [], s]);
|
|
1670
|
+
const actions = [];
|
|
1671
|
+
for (const [fqdn, group] of byFqdn) {
|
|
1672
|
+
const main2 = group.find((s) => s.canonical);
|
|
1673
|
+
const extras = group.filter((s) => !s.canonical);
|
|
1674
|
+
const action = { fqdn, remove: extras.map((s) => s.label) };
|
|
1675
|
+
if (!main2) action.reinstall = { params: extras[0].params, reason: "duplicate" };
|
|
1676
|
+
else if (main2.issue) action.reinstall = { params: main2.params, reason: main2.issue };
|
|
1677
|
+
else if (opts.restartRunning) action.reinstall = { params: main2.params, reason: "outdated" };
|
|
1678
|
+
if (action.reinstall || action.remove.length) actions.push(action);
|
|
1679
|
+
}
|
|
1680
|
+
return actions;
|
|
1681
|
+
}
|
|
1682
|
+
function runRepair(actions) {
|
|
1683
|
+
const result = { fixed: [], failed: [] };
|
|
1684
|
+
if (actions.length === 0) return result;
|
|
1685
|
+
prepareServiceChanges();
|
|
1686
|
+
for (const a of actions) {
|
|
1687
|
+
try {
|
|
1688
|
+
for (const label4 of a.remove) uninstallServiceLabel(label4);
|
|
1689
|
+
if (a.reinstall) installServiceForSpec(a.reinstall.params);
|
|
1690
|
+
result.fixed.push(a.fqdn);
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
result.failed.push({ fqdn: a.fqdn, error: err.message });
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
function serviceIssuesByFqdn() {
|
|
1698
|
+
const map = /* @__PURE__ */ new Map();
|
|
1699
|
+
try {
|
|
1700
|
+
for (const a of planRepair(inventoryServices())) {
|
|
1701
|
+
map.set(a.fqdn, a.reinstall?.reason ?? "duplicate");
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
return map;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1391
1708
|
// src/core/unmanaged-scan-cache.ts
|
|
1392
|
-
import { readFileSync as
|
|
1709
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
1393
1710
|
function saveUnmanagedScan(rows) {
|
|
1394
1711
|
try {
|
|
1395
1712
|
ensureDirs();
|
|
@@ -1401,7 +1718,7 @@ function saveUnmanagedScan(rows) {
|
|
|
1401
1718
|
}
|
|
1402
1719
|
function lookupUnmanagedByIndex(num) {
|
|
1403
1720
|
try {
|
|
1404
|
-
const cache = JSON.parse(
|
|
1721
|
+
const cache = JSON.parse(readFileSync6(scanCacheFile, "utf8"));
|
|
1405
1722
|
return cache[String(num)];
|
|
1406
1723
|
} catch {
|
|
1407
1724
|
return void 0;
|
|
@@ -1485,8 +1802,8 @@ async function resolveRemoteTarget(cf, target) {
|
|
|
1485
1802
|
}
|
|
1486
1803
|
const tunnel = matches[0];
|
|
1487
1804
|
if (!tunnel) return null;
|
|
1488
|
-
const { listCargoCnames } = await import("./dns-
|
|
1489
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1805
|
+
const { listCargoCnames } = await import("./dns-VUPKRH46.js");
|
|
1806
|
+
const { listZones: listZones3 } = await import("./zones-CF4XFFZZ.js");
|
|
1490
1807
|
for (const zone of await listZones3(cf.token)) {
|
|
1491
1808
|
const rec = (await listCargoCnames(cf.token, zone.id)).find((r) => tunnelIdFromCname2(r.content) === tunnel.id);
|
|
1492
1809
|
if (rec) return { tunnel, fqdn: rec.name };
|
|
@@ -1507,25 +1824,29 @@ async function removeTunnelById(cf, tunnel, opts = {}) {
|
|
|
1507
1824
|
async function listAll(cf, opts = {}) {
|
|
1508
1825
|
const entries = await reconcile();
|
|
1509
1826
|
const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
|
|
1827
|
+
const issues = serviceIssuesByFqdn();
|
|
1510
1828
|
const rows = await Promise.all(entries.map(async (e) => {
|
|
1511
1829
|
const fqdn = entryFqdn(e);
|
|
1512
1830
|
const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
|
|
1513
1831
|
const svc = serviceState(fqdn);
|
|
1514
1832
|
const state4 = await probeEntryState(e, gone);
|
|
1833
|
+
const issue = issues.get(fqdn);
|
|
1834
|
+
const svcLabel = svc === "none" ? "-" : svc;
|
|
1515
1835
|
return {
|
|
1516
1836
|
num: e.index ? String(e.index) : "-",
|
|
1517
1837
|
url: `https://${fqdn}`,
|
|
1518
1838
|
target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
|
|
1519
1839
|
protocol: e.protocol ?? "auto",
|
|
1520
1840
|
state: state4,
|
|
1521
|
-
service:
|
|
1841
|
+
service: issue ? `${svcLabel} (${issue})` : svcLabel,
|
|
1522
1842
|
pid: e.state === "running" && e.pid ? String(e.pid) : "-",
|
|
1523
|
-
managed: true
|
|
1843
|
+
managed: true,
|
|
1844
|
+
serviceIssue: issue
|
|
1524
1845
|
};
|
|
1525
1846
|
}));
|
|
1526
1847
|
if (opts.all) {
|
|
1527
|
-
const { listCargoCnames } = await import("./dns-
|
|
1528
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1848
|
+
const { listCargoCnames } = await import("./dns-VUPKRH46.js");
|
|
1849
|
+
const { listZones: listZones3 } = await import("./zones-CF4XFFZZ.js");
|
|
1529
1850
|
const tracked = new Set(entries.map(entryFqdn));
|
|
1530
1851
|
const unmanaged = [];
|
|
1531
1852
|
for (const zone of await listZones3(cf.token)) {
|
|
@@ -1547,19 +1868,27 @@ async function listAll(cf, opts = {}) {
|
|
|
1547
1868
|
}
|
|
1548
1869
|
|
|
1549
1870
|
// src/core/watchdog.ts
|
|
1550
|
-
import { readFileSync as
|
|
1871
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
1551
1872
|
var activeWatchdogs = /* @__PURE__ */ new Map();
|
|
1552
1873
|
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1874
|
+
var EXIT_RESTART_GAP_MS = 5e3;
|
|
1553
1875
|
function resolveMetricsPort(logFile, fallback) {
|
|
1554
|
-
if (
|
|
1876
|
+
if (existsSync7(logFile)) {
|
|
1555
1877
|
try {
|
|
1556
|
-
const port = extractMetricsPort(
|
|
1878
|
+
const port = extractMetricsPort(readFileSync7(logFile, "utf8"));
|
|
1557
1879
|
if (port) return port;
|
|
1558
1880
|
} catch {
|
|
1559
1881
|
}
|
|
1560
1882
|
}
|
|
1561
1883
|
return fallback;
|
|
1562
1884
|
}
|
|
1885
|
+
async function freshMetricsPort() {
|
|
1886
|
+
try {
|
|
1887
|
+
return await pickFreePort();
|
|
1888
|
+
} catch {
|
|
1889
|
+
return void 0;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1563
1892
|
async function verifyRemoteTunnel(cf, tunnelId) {
|
|
1564
1893
|
try {
|
|
1565
1894
|
await getTunnel(cf, tunnelId);
|
|
@@ -1569,25 +1898,39 @@ async function verifyRemoteTunnel(cf, tunnelId) {
|
|
|
1569
1898
|
return true;
|
|
1570
1899
|
}
|
|
1571
1900
|
}
|
|
1572
|
-
async function
|
|
1573
|
-
|
|
1574
|
-
state4.tier1Restarts++;
|
|
1575
|
-
say.warn(`[watchdog] Health check failed for ${ctx.fqdn} -> restarting connector (Tier 1)...`);
|
|
1576
|
-
await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
|
|
1901
|
+
async function launchConnector(ctx) {
|
|
1902
|
+
const metricsPort = await freshMetricsPort();
|
|
1577
1903
|
const conn = startConnector({
|
|
1578
1904
|
bin: ctx.bin,
|
|
1579
1905
|
token: ctx.token,
|
|
1580
1906
|
detach: false,
|
|
1581
1907
|
logFile: ctx.logFile,
|
|
1582
1908
|
protocol: ctx.protocol,
|
|
1909
|
+
metricsPort,
|
|
1583
1910
|
onExit: () => {
|
|
1584
|
-
void
|
|
1911
|
+
void notifyConnectorExited(ctx.fqdn);
|
|
1585
1912
|
}
|
|
1586
1913
|
});
|
|
1914
|
+
if (activeWatchdogs.get(ctx.fqdn)?.ctx !== ctx) {
|
|
1915
|
+
await stopConnector({ pid: conn.pid, bootId: currentBootId() });
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1587
1918
|
ctx.pid = conn.pid;
|
|
1588
1919
|
await sleep3(1e3);
|
|
1589
|
-
ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1590
|
-
await
|
|
1920
|
+
ctx.metricsPort = metricsPort ?? resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1921
|
+
await markConnectorRunning(ctx.fqdn, {
|
|
1922
|
+
pid: conn.pid,
|
|
1923
|
+
metricsPort: ctx.metricsPort,
|
|
1924
|
+
bootId: currentBootId(),
|
|
1925
|
+
tunnelId: ctx.tunnelId
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
async function tier1Restart(ctx, state4) {
|
|
1929
|
+
state4.lastRestartAt = Date.now();
|
|
1930
|
+
state4.tier1Restarts++;
|
|
1931
|
+
say.warn(`[watchdog] Health check failed for ${ctx.fqdn} -> restarting connector (Tier 1)...`);
|
|
1932
|
+
await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
|
|
1933
|
+
await launchConnector(ctx);
|
|
1591
1934
|
state4.consecutiveFailures = 0;
|
|
1592
1935
|
}
|
|
1593
1936
|
async function tier2Reprovision(ctx, state4) {
|
|
@@ -1598,36 +1941,46 @@ async function tier2Reprovision(ctx, state4) {
|
|
|
1598
1941
|
const result = await createTunnelSubdomain(ctx.cf, { ...ctx.item, force: true, yes: true });
|
|
1599
1942
|
ctx.tunnelId = result.tunnelId;
|
|
1600
1943
|
ctx.token = result.token;
|
|
1601
|
-
|
|
1602
|
-
bin: ctx.bin,
|
|
1603
|
-
token: ctx.token,
|
|
1604
|
-
detach: false,
|
|
1605
|
-
logFile: ctx.logFile,
|
|
1606
|
-
protocol: ctx.protocol,
|
|
1607
|
-
onExit: () => {
|
|
1608
|
-
void onConnectorExited(ctx.fqdn);
|
|
1609
|
-
}
|
|
1610
|
-
});
|
|
1611
|
-
ctx.pid = conn.pid;
|
|
1612
|
-
await sleep3(1e3);
|
|
1613
|
-
ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1614
|
-
await patchEntry(ctx.fqdn, { pid: conn.pid, metricsPort: ctx.metricsPort, bootId: currentBootId(), tunnelId: ctx.tunnelId });
|
|
1944
|
+
await launchConnector(ctx);
|
|
1615
1945
|
state4.consecutiveFailures = 0;
|
|
1616
1946
|
state4.tier1Restarts = 0;
|
|
1617
1947
|
}
|
|
1618
|
-
async function
|
|
1948
|
+
async function guardedRestart(fqdn, run2) {
|
|
1949
|
+
const item = activeWatchdogs.get(fqdn);
|
|
1950
|
+
if (!item || item.state.restarting) return;
|
|
1951
|
+
item.state.restarting = true;
|
|
1952
|
+
try {
|
|
1953
|
+
await run2();
|
|
1954
|
+
} catch (err) {
|
|
1955
|
+
say.warn(`[watchdog] Recovery for ${fqdn} failed: ${err.message} \u2014 retrying on the next check`);
|
|
1956
|
+
} finally {
|
|
1957
|
+
item.state.restarting = false;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
async function notifyConnectorExited(fqdn) {
|
|
1619
1961
|
const item = activeWatchdogs.get(fqdn);
|
|
1620
1962
|
if (!item) return;
|
|
1621
1963
|
const { ctx, state: state4 } = item;
|
|
1622
|
-
if (Date.now() - state4.lastRestartAt <
|
|
1623
|
-
|
|
1964
|
+
if (state4.restarting || Date.now() - state4.lastRestartAt < EXIT_RESTART_GAP_MS) return;
|
|
1965
|
+
const run2 = state4.tier1Restarts >= 3 ? tier2Reprovision : tier1Restart;
|
|
1966
|
+
await guardedRestart(fqdn, () => run2(ctx, state4));
|
|
1624
1967
|
}
|
|
1625
1968
|
function startTunnelWatchdog(ctx, opts = {}) {
|
|
1626
1969
|
stopTunnelWatchdog(ctx.fqdn);
|
|
1627
1970
|
const intervalMs = opts.intervalMs ?? 15e3;
|
|
1628
1971
|
const cooldownMs = opts.cooldownMs ?? 5e3;
|
|
1629
|
-
const state4 = { consecutiveFailures: 0, tier1Restarts: 0, tier2Restarts: 0, lastRestartAt: 0 };
|
|
1972
|
+
const state4 = { consecutiveFailures: 0, tier1Restarts: 0, tier2Restarts: 0, lastRestartAt: 0, restarting: false };
|
|
1973
|
+
let checking = false;
|
|
1630
1974
|
const check = async () => {
|
|
1975
|
+
if (state4.restarting || checking) return;
|
|
1976
|
+
checking = true;
|
|
1977
|
+
try {
|
|
1978
|
+
await runCheck();
|
|
1979
|
+
} finally {
|
|
1980
|
+
checking = false;
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
const runCheck = async () => {
|
|
1631
1984
|
if (!ctx.metricsPort) ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1632
1985
|
let healthy = false;
|
|
1633
1986
|
if (ctx.metricsPort) {
|
|
@@ -1636,22 +1989,19 @@ function startTunnelWatchdog(ctx, opts = {}) {
|
|
|
1636
1989
|
}
|
|
1637
1990
|
if (healthy) {
|
|
1638
1991
|
state4.consecutiveFailures = 0;
|
|
1992
|
+
state4.tier1Restarts = 0;
|
|
1639
1993
|
return;
|
|
1640
1994
|
}
|
|
1641
1995
|
state4.consecutiveFailures++;
|
|
1642
1996
|
if (state4.consecutiveFailures >= 3 && Date.now() - state4.lastRestartAt >= cooldownMs) {
|
|
1643
1997
|
const remoteExists = await verifyRemoteTunnel(ctx.cf, ctx.tunnelId);
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
} else {
|
|
1647
|
-
await tier1Restart(ctx, state4);
|
|
1648
|
-
}
|
|
1998
|
+
const run2 = !remoteExists || state4.tier1Restarts >= 3 ? tier2Reprovision : tier1Restart;
|
|
1999
|
+
await guardedRestart(ctx.fqdn, () => run2(ctx, state4));
|
|
1649
2000
|
}
|
|
1650
2001
|
};
|
|
1651
2002
|
const timer = setInterval(() => {
|
|
1652
|
-
|
|
2003
|
+
check().catch((err) => say.debug(`[watchdog] check failed: ${err.message}`));
|
|
1653
2004
|
}, intervalMs);
|
|
1654
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
1655
2005
|
activeWatchdogs.set(ctx.fqdn, { timer, ctx, state: state4 });
|
|
1656
2006
|
}
|
|
1657
2007
|
function stopTunnelWatchdog(fqdn) {
|
|
@@ -1665,26 +2015,55 @@ function stopAllWatchdogs() {
|
|
|
1665
2015
|
for (const [fqdn] of activeWatchdogs) stopTunnelWatchdog(fqdn);
|
|
1666
2016
|
}
|
|
1667
2017
|
|
|
2018
|
+
// src/core/installed-version.ts
|
|
2019
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync2 } from "fs";
|
|
2020
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
2021
|
+
function readInstalledVersion() {
|
|
2022
|
+
try {
|
|
2023
|
+
const script = realpathSync2(process.argv[1] ?? "");
|
|
2024
|
+
const pkg2 = JSON.parse(readFileSync8(join8(dirname3(script), "..", "package.json"), "utf8"));
|
|
2025
|
+
return typeof pkg2.version === "string" ? pkg2.version : void 0;
|
|
2026
|
+
} catch {
|
|
2027
|
+
return void 0;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1668
2031
|
// src/core/up-runner.ts
|
|
1669
2032
|
var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2033
|
+
var UPGRADE_RESTART_EXIT_CODE = 75;
|
|
2034
|
+
var VERSION_CHECK_INTERVAL_MS = 6e4;
|
|
2035
|
+
var LOCK_WAIT_MS = 3e4;
|
|
2036
|
+
var MAX_CREATE_BACKOFF_MS = 6e4;
|
|
1670
2037
|
function logFileFor(fqdn) {
|
|
1671
|
-
return
|
|
2038
|
+
return join9(logDir, `${serviceSlug(fqdn)}.log`);
|
|
2039
|
+
}
|
|
2040
|
+
function knownFqdn(item) {
|
|
2041
|
+
if (item.hostname) return item.hostname;
|
|
2042
|
+
return item.name !== void 0 && item.zone ? fqdnFor(item.name, item.zone) : void 0;
|
|
1672
2043
|
}
|
|
1673
2044
|
async function createWithBootRetry(cf, item, keepAlive) {
|
|
1674
|
-
|
|
2045
|
+
if (!keepAlive) return createTunnelSubdomain(cf, item);
|
|
1675
2046
|
for (let attempt = 0; ; attempt++) {
|
|
1676
2047
|
try {
|
|
1677
|
-
return await createTunnelSubdomain(cf, item);
|
|
2048
|
+
return await createTunnelSubdomain(cf, { ...item, reuse: true });
|
|
1678
2049
|
} catch (err) {
|
|
1679
|
-
|
|
1680
|
-
say.
|
|
1681
|
-
await sleep4(
|
|
2050
|
+
const wait = Math.min(2 ** attempt * 1e3, MAX_CREATE_BACKOFF_MS);
|
|
2051
|
+
say.warn(`Could not create ${item.name ?? "tunnel"} (${err.message}) \u2014 retrying in ${wait / 1e3}s`);
|
|
2052
|
+
await sleep4(wait);
|
|
1682
2053
|
}
|
|
1683
2054
|
}
|
|
1684
2055
|
}
|
|
2056
|
+
async function freshMetricsPort2() {
|
|
2057
|
+
try {
|
|
2058
|
+
return await pickFreePort();
|
|
2059
|
+
} catch {
|
|
2060
|
+
return void 0;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
1685
2063
|
async function startTunnels(cf, bin, items, opts = {}) {
|
|
1686
2064
|
const started = [];
|
|
1687
2065
|
let tornDown = false;
|
|
2066
|
+
const supervised = !!opts.keepAlive && !opts.detach;
|
|
1688
2067
|
const teardownAll = async (code) => {
|
|
1689
2068
|
if (tornDown) return;
|
|
1690
2069
|
tornDown = true;
|
|
@@ -1692,7 +2071,8 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1692
2071
|
try {
|
|
1693
2072
|
for (const s of started) {
|
|
1694
2073
|
if (opts.keepAlive) {
|
|
1695
|
-
|
|
2074
|
+
const current = getEntry(s.fqdn);
|
|
2075
|
+
await stopConnector(current?.pid ? current : { pid: s.pid, bootId: currentBootId() });
|
|
1696
2076
|
} else {
|
|
1697
2077
|
try {
|
|
1698
2078
|
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
@@ -1707,6 +2087,12 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1707
2087
|
process.exit(code);
|
|
1708
2088
|
}
|
|
1709
2089
|
};
|
|
2090
|
+
for (const item of items) {
|
|
2091
|
+
const fqdn = knownFqdn(item);
|
|
2092
|
+
if (!fqdn) continue;
|
|
2093
|
+
if (opts.detach) await assertHostnameFree(fqdn);
|
|
2094
|
+
else await acquireHostnameLock(fqdn, { waitMs: opts.keepAlive ? LOCK_WAIT_MS : 0 });
|
|
2095
|
+
}
|
|
1710
2096
|
const spin = clack2.spinner();
|
|
1711
2097
|
spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
|
|
1712
2098
|
for (const item of items) {
|
|
@@ -1714,28 +2100,35 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1714
2100
|
const result = await createWithBootRetry(cf, item, opts.keepAlive);
|
|
1715
2101
|
const fqdn = result.host.hostname;
|
|
1716
2102
|
const logFile = logFileFor(fqdn);
|
|
2103
|
+
const knownPort = await freshMetricsPort2();
|
|
1717
2104
|
const conn = startConnector({
|
|
1718
2105
|
bin,
|
|
1719
2106
|
token: result.token,
|
|
1720
2107
|
detach: !!opts.detach,
|
|
1721
2108
|
logFile,
|
|
1722
2109
|
protocol: opts.protocol,
|
|
2110
|
+
metricsPort: knownPort,
|
|
1723
2111
|
onExit: opts.detach ? void 0 : (code) => {
|
|
1724
|
-
if (
|
|
1725
|
-
|
|
1726
|
-
void
|
|
2112
|
+
if (tornDown) return;
|
|
2113
|
+
if (opts.keepAlive) {
|
|
2114
|
+
void notifyConnectorExited(fqdn);
|
|
2115
|
+
return;
|
|
1727
2116
|
}
|
|
2117
|
+
say.warn(`Connector for ${fqdn} exited.`);
|
|
2118
|
+
void teardownAll(code ?? 1);
|
|
1728
2119
|
}
|
|
1729
2120
|
});
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2121
|
+
let metricsPort = knownPort;
|
|
2122
|
+
if (!metricsPort) {
|
|
2123
|
+
await sleep4(600);
|
|
2124
|
+
if (existsSync8(logFile)) {
|
|
2125
|
+
try {
|
|
2126
|
+
metricsPort = extractMetricsPort(readFileSync9(logFile, "utf8"));
|
|
2127
|
+
} catch {
|
|
2128
|
+
}
|
|
1736
2129
|
}
|
|
1737
2130
|
}
|
|
1738
|
-
await
|
|
2131
|
+
await markConnectorRunning(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile, metricsPort, protocol: opts.protocol });
|
|
1739
2132
|
started.push({
|
|
1740
2133
|
fqdn,
|
|
1741
2134
|
subdomain: result.host.subdomain,
|
|
@@ -1743,7 +2136,7 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1743
2136
|
target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
|
|
1744
2137
|
pid: conn.pid
|
|
1745
2138
|
});
|
|
1746
|
-
if (
|
|
2139
|
+
if (supervised) {
|
|
1747
2140
|
startTunnelWatchdog({
|
|
1748
2141
|
cf,
|
|
1749
2142
|
bin,
|
|
@@ -1768,6 +2161,7 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1768
2161
|
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
1769
2162
|
process.on(sig, () => void teardownAll(0));
|
|
1770
2163
|
}
|
|
2164
|
+
if (supervised) watchForUpgrade(() => void teardownAll(UPGRADE_RESTART_EXIT_CODE));
|
|
1771
2165
|
spin.message("Connecting to the Cloudflare edge\u2026");
|
|
1772
2166
|
const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
|
|
1773
2167
|
const live = healths.filter((h) => h === "healthy").length;
|
|
@@ -1776,6 +2170,20 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1776
2170
|
clack2.note(lines.join("\n"), `${live}/${started.length} live`);
|
|
1777
2171
|
say.dim(opts.keepAlive ? "Service running with auto-heal watchdog." : "Ctrl-C stops and releases them.");
|
|
1778
2172
|
}
|
|
2173
|
+
function watchForUpgrade(restart) {
|
|
2174
|
+
if (process.platform === "win32") return;
|
|
2175
|
+
const startedWith = readInstalledVersion();
|
|
2176
|
+
if (!startedWith) return;
|
|
2177
|
+
const timer = setInterval(() => {
|
|
2178
|
+
const now = readInstalledVersion();
|
|
2179
|
+
if (now && now !== startedWith) {
|
|
2180
|
+
clearInterval(timer);
|
|
2181
|
+
say.warn(`cloudtunnel ${startedWith} \u2192 ${now} installed \u2014 restarting onto the new version\u2026`);
|
|
2182
|
+
restart();
|
|
2183
|
+
}
|
|
2184
|
+
}, VERSION_CHECK_INTERVAL_MS);
|
|
2185
|
+
timer.unref();
|
|
2186
|
+
}
|
|
1779
2187
|
|
|
1780
2188
|
// src/core/resolve-domain.ts
|
|
1781
2189
|
async function resolveDomain(cf, opts, creds) {
|
|
@@ -1788,12 +2196,6 @@ async function resolveDomain(cf, opts, creds) {
|
|
|
1788
2196
|
throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
|
|
1789
2197
|
}
|
|
1790
2198
|
|
|
1791
|
-
// src/core/transport-protocol.ts
|
|
1792
|
-
function parseTransportProtocol(value) {
|
|
1793
|
-
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
1794
|
-
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
2199
|
// src/commands/up.ts
|
|
1798
2200
|
function promptOrExit(value) {
|
|
1799
2201
|
if (clack3.isCancel(value)) {
|
|
@@ -1857,7 +2259,7 @@ async function runUp(specArgs, opts) {
|
|
|
1857
2259
|
}
|
|
1858
2260
|
await startTunnels(cf, bin, items, { detach: opts.detach, protocol, keepAlive: opts.keepAlive });
|
|
1859
2261
|
}
|
|
1860
|
-
var SERVICE_UP_TIMEOUT_MS =
|
|
2262
|
+
var SERVICE_UP_TIMEOUT_MS = 3e4;
|
|
1861
2263
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1862
2264
|
async function registerServices(cf, items, domain, proto, protocol) {
|
|
1863
2265
|
assertServiceSupported();
|
|
@@ -1869,6 +2271,7 @@ async function registerServices(cf, items, domain, proto, protocol) {
|
|
|
1869
2271
|
for (const item of items) {
|
|
1870
2272
|
const subdomain = item.name;
|
|
1871
2273
|
const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
|
|
2274
|
+
await patchEntry(fqdn, { state: "provisioning", pid: void 0, metricsPort: void 0 });
|
|
1872
2275
|
installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
|
|
1873
2276
|
fqdns.push(fqdn);
|
|
1874
2277
|
}
|
|
@@ -1893,11 +2296,21 @@ async function waitServicesUp(fqdns, timeoutMs) {
|
|
|
1893
2296
|
while (pending.size > 0) {
|
|
1894
2297
|
for (const fqdn of [...pending]) {
|
|
1895
2298
|
const entry = getEntry(fqdn);
|
|
1896
|
-
if (entry?.state === "running" && entry.pid
|
|
2299
|
+
if (entry?.state === "running" && entry.pid && await isOurConnector(entry)) {
|
|
2300
|
+
if (!entry.metricsPort || (await checkLocalReady(entry.metricsPort, { timeoutMs: 500 })).healthy) {
|
|
2301
|
+
pending.delete(fqdn);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
1897
2304
|
}
|
|
1898
2305
|
if (pending.size === 0 || Date.now() >= deadline) break;
|
|
1899
2306
|
await delay(500);
|
|
1900
2307
|
}
|
|
2308
|
+
for (const fqdn of [...pending]) {
|
|
2309
|
+
const entry = getEntry(fqdn);
|
|
2310
|
+
if (entry?.state === "running" && entry.pid && await isOurConnector(entry)) {
|
|
2311
|
+
pending.delete(fqdn);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
1901
2314
|
return [...pending];
|
|
1902
2315
|
}
|
|
1903
2316
|
function registerUp(program) {
|
|
@@ -1918,6 +2331,8 @@ function registerLs(program) {
|
|
|
1918
2331
|
["#", "URL", "TARGET", "PROTOCOL", "STATE", "SERVICE", "PID"],
|
|
1919
2332
|
rows.map((r) => [r.num, r.url, r.target, r.protocol, r.state, r.service, r.pid])
|
|
1920
2333
|
);
|
|
2334
|
+
const needRepair = rows.filter((r) => r.serviceIssue).length;
|
|
2335
|
+
if (needRepair) say.warn(`${needRepair} boot service(s) need repair \u2192 cloudtunnel service repair`);
|
|
1921
2336
|
});
|
|
1922
2337
|
}
|
|
1923
2338
|
|
|
@@ -1976,19 +2391,19 @@ function registerDelete(program) {
|
|
|
1976
2391
|
}
|
|
1977
2392
|
|
|
1978
2393
|
// src/commands/logs.ts
|
|
1979
|
-
import { closeSync, existsSync as
|
|
2394
|
+
import { closeSync, existsSync as existsSync9, openSync as openSync2, readFileSync as readFileSync10, readSync, statSync as statSync2, watch } from "fs";
|
|
1980
2395
|
function printTail(file, n) {
|
|
1981
|
-
const lines =
|
|
2396
|
+
const lines = readFileSync10(file, "utf8").split("\n");
|
|
1982
2397
|
const tail = lines.slice(-n).join("\n");
|
|
1983
2398
|
process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
|
|
1984
2399
|
`);
|
|
1985
|
-
return
|
|
2400
|
+
return statSync2(file).size;
|
|
1986
2401
|
}
|
|
1987
2402
|
function follow(file, fromPos) {
|
|
1988
2403
|
let pos = fromPos;
|
|
1989
2404
|
say.dim("\u2014 following (Ctrl-C to stop) \u2014");
|
|
1990
2405
|
const watcher = watch(file, () => {
|
|
1991
|
-
const size =
|
|
2406
|
+
const size = statSync2(file).size;
|
|
1992
2407
|
if (size < pos) {
|
|
1993
2408
|
pos = 0;
|
|
1994
2409
|
return;
|
|
@@ -2010,7 +2425,7 @@ function follow(file, fromPos) {
|
|
|
2010
2425
|
function registerLogs(program) {
|
|
2011
2426
|
program.command("logs").argument("<target>", "subdomain name / hostname / id / #").description("Show the connector log for a subdomain (use -f to follow)").option("-f, --follow", "keep printing new log lines (like tail -f)").option("-n, --lines <n>", "number of lines to show", "50").action((name, opts) => {
|
|
2012
2427
|
const { fqdn, entry } = resolveTarget(name);
|
|
2013
|
-
if (!entry?.logFile || !
|
|
2428
|
+
if (!entry?.logFile || !existsSync9(entry.logFile)) {
|
|
2014
2429
|
throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
|
|
2015
2430
|
}
|
|
2016
2431
|
const n = Math.max(1, Number(opts.lines) || 50);
|
|
@@ -2022,7 +2437,7 @@ function registerLogs(program) {
|
|
|
2022
2437
|
// src/commands/relay.ts
|
|
2023
2438
|
import { openSync as openSync3 } from "fs";
|
|
2024
2439
|
import { spawn as spawn3 } from "child_process";
|
|
2025
|
-
import { join as
|
|
2440
|
+
import { join as join10 } from "path";
|
|
2026
2441
|
import pc from "picocolors";
|
|
2027
2442
|
|
|
2028
2443
|
// src/core/api-proxy-server.ts
|
|
@@ -2129,7 +2544,7 @@ function spawnDetachedRelay(sub, domain) {
|
|
|
2129
2544
|
const script = process.argv[1];
|
|
2130
2545
|
if (!script) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
2131
2546
|
ensureDirs();
|
|
2132
|
-
const logFile =
|
|
2547
|
+
const logFile = join10(logDir, `relay-${sub === "@" ? "root" : sub}.log`);
|
|
2133
2548
|
const fd = openSync3(logFile, "a", 384);
|
|
2134
2549
|
const args = [script, "relay", sub, "-d", domain, "-f", "-y"];
|
|
2135
2550
|
const child = spawn3(process.execPath, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
@@ -2187,11 +2602,68 @@ async function runRelay(sub, opts) {
|
|
|
2187
2602
|
force: opts.force,
|
|
2188
2603
|
yes: opts.yes
|
|
2189
2604
|
};
|
|
2190
|
-
await startTunnels(cf, bin, [item], {});
|
|
2605
|
+
await startTunnels(cf, bin, [item], { keepAlive: opts.keepAlive });
|
|
2191
2606
|
printRelayReady(fqdn, secret, "foreground");
|
|
2192
2607
|
}
|
|
2193
2608
|
function registerRelay(program) {
|
|
2194
|
-
program.command("relay [subdomain]").description("Expose a locked Cloudflare-API reverse proxy via a tunnel (for clients that can't reach api.cloudflare.com directly)").option("-d, --domain <domain>", "domain for the relay subdomain (prompted from a list if unset)").option("--detach", "run the relay in the background").option("--service", "register the relay as a boot service (systemd \xB7 launchd \xB7 Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt before replacing an existing record").action((subdomain, opts) => runRelay(subdomain ?? DEFAULT_SUB, opts));
|
|
2609
|
+
program.command("relay [subdomain]").description("Expose a locked Cloudflare-API reverse proxy via a tunnel (for clients that can't reach api.cloudflare.com directly)").option("-d, --domain <domain>", "domain for the relay subdomain (prompted from a list if unset)").option("--detach", "run the relay in the background").option("--service", "register the relay as a boot service (systemd \xB7 launchd \xB7 Task Scheduler)").option("--keep-alive", "supervised mode with auto-heal watchdog (used by the boot service)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt before replacing an existing record").action((subdomain, opts) => runRelay(subdomain ?? DEFAULT_SUB, opts));
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// src/commands/service.ts
|
|
2613
|
+
function describeAction(a) {
|
|
2614
|
+
const parts = [];
|
|
2615
|
+
if (a.reinstall) parts.push(`reinstall + restart (${a.reinstall.reason})`);
|
|
2616
|
+
if (a.remove.length) parts.push(`remove duplicate ${a.remove.join(", ")}`);
|
|
2617
|
+
return `${a.fqdn}: ${parts.join(" \xB7 ")}`;
|
|
2618
|
+
}
|
|
2619
|
+
async function repairWithPrompt(actions, opts = {}) {
|
|
2620
|
+
for (const a of actions) say.dim(` ${describeAction(a)}`);
|
|
2621
|
+
if (opts.dryRun) return false;
|
|
2622
|
+
const needsSudo = process.platform === "linux" ? " (uses sudo)" : "";
|
|
2623
|
+
if (!opts.yes && !await confirm(`Repair ${actions.length} boot service(s) now?${needsSudo}`)) return false;
|
|
2624
|
+
const res = runRepair(actions);
|
|
2625
|
+
for (const fqdn of res.fixed) say.ok(`Repaired ${fqdn}`);
|
|
2626
|
+
for (const f of res.failed) say.warn(`Could not repair ${f.fqdn}: ${f.error}`);
|
|
2627
|
+
if (res.failed.length) {
|
|
2628
|
+
throw new CliError(`${res.failed.length} boot service(s) could not be repaired.`, { hint: "re-run `cloudtunnel service repair`" });
|
|
2629
|
+
}
|
|
2630
|
+
return res.fixed.length > 0;
|
|
2631
|
+
}
|
|
2632
|
+
function registerService(program) {
|
|
2633
|
+
const service = program.command("service").description("Manage boot services (installed with `up --service`)");
|
|
2634
|
+
service.command("repair").description("Regenerate + restart boot services that are outdated, broken, stopped, or duplicated").option("-y, --yes", "don't ask for confirmation").option("--dry-run", "show what would be repaired without doing it").action(async (opts) => {
|
|
2635
|
+
assertServiceSupported();
|
|
2636
|
+
const services = inventoryServices();
|
|
2637
|
+
const actions = planRepair(services, { restartRunning: process.platform === "win32" });
|
|
2638
|
+
if (actions.length === 0) {
|
|
2639
|
+
say.ok(`All ${services.length} boot service(s) are up to date and running.`);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
say.info(`${actions.length} boot service(s) need repair:`);
|
|
2643
|
+
await repairWithPrompt(actions, opts);
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// src/core/service-upgrade-check.ts
|
|
2648
|
+
var INFO_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-v", "--version", "help"]);
|
|
2649
|
+
function remember(version) {
|
|
2650
|
+
saveConfig({ ...loadConfig(), serviceCheckVersion: version });
|
|
2651
|
+
}
|
|
2652
|
+
async function maybeRepairServicesAfterUpgrade(version, argv) {
|
|
2653
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
2654
|
+
const rest = argv.slice(2);
|
|
2655
|
+
if (rest[0] === "service" || rest[0] === "delete" || rest.some((a) => INFO_ARGS.has(a))) return;
|
|
2656
|
+
if (loadConfig().serviceCheckVersion === version) return;
|
|
2657
|
+
try {
|
|
2658
|
+
const actions = planRepair(inventoryServices(), { restartRunning: process.platform === "win32" });
|
|
2659
|
+
if (actions.length) {
|
|
2660
|
+
say.warn(`cloudtunnel ${version}: ${actions.length} boot service(s) need repair to stay up:`);
|
|
2661
|
+
await repairWithPrompt(actions);
|
|
2662
|
+
}
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
say.warn(`Boot-service check failed: ${err.message} \u2014 run \`cloudtunnel service repair\``);
|
|
2665
|
+
}
|
|
2666
|
+
remember(version);
|
|
2195
2667
|
}
|
|
2196
2668
|
|
|
2197
2669
|
// src/index.ts
|
|
@@ -2211,7 +2683,7 @@ function buildProgram() {
|
|
|
2211
2683
|
""
|
|
2212
2684
|
].join("\n")
|
|
2213
2685
|
);
|
|
2214
|
-
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs, registerRelay]) {
|
|
2686
|
+
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs, registerRelay, registerService]) {
|
|
2215
2687
|
register(program);
|
|
2216
2688
|
}
|
|
2217
2689
|
return program;
|
|
@@ -2225,6 +2697,7 @@ function shouldMigrate(argv) {
|
|
|
2225
2697
|
async function main() {
|
|
2226
2698
|
configureProxy();
|
|
2227
2699
|
if (shouldMigrate(process.argv)) await migrateLegacyProfiles();
|
|
2700
|
+
await maybeRepairServicesAfterUpgrade(pkg.version, process.argv);
|
|
2228
2701
|
const program = buildProgram();
|
|
2229
2702
|
try {
|
|
2230
2703
|
await program.parseAsync(process.argv);
|