@qpjoy/tunnel-cli 0.1.4 → 0.1.5
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 +7 -0
- package/dist/hdo.js +117 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,13 @@ qp-tunnel-cli hdo refresh
|
|
|
55
55
|
qp-tunnel-cli hdo down
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
If a tunnel was created by the Electron HDO plugin, its default interface is
|
|
59
|
+
`hdo-client`, so stop it with:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
qp-tunnel-cli hdo down --interface hdo-client
|
|
63
|
+
```
|
|
64
|
+
|
|
58
65
|
Platform behavior:
|
|
59
66
|
|
|
60
67
|
- Linux: writes `/etc/wireguard/hdo-internal.conf` and enables `wg-quick@hdo-internal`
|
package/dist/hdo.js
CHANGED
|
@@ -23,10 +23,10 @@ async function runHdoCli(args, ctx) {
|
|
|
23
23
|
await enrollCommand(rest, command === 'refresh');
|
|
24
24
|
return;
|
|
25
25
|
case 'status':
|
|
26
|
-
statusCommand(
|
|
26
|
+
statusCommand(parseCommandOptions(rest));
|
|
27
27
|
return;
|
|
28
28
|
case 'down':
|
|
29
|
-
await downCommand(
|
|
29
|
+
await downCommand(parseCommandOptions(rest));
|
|
30
30
|
return;
|
|
31
31
|
default:
|
|
32
32
|
process.stderr.write(`Unknown hdo command: ${command}\n\n`);
|
|
@@ -41,8 +41,8 @@ Usage:
|
|
|
41
41
|
qp-tunnel-cli hdo enroll --server-url URL --username USER [options]
|
|
42
42
|
qp-tunnel-cli hdo enroll --server-url URL --token TOKEN [options]
|
|
43
43
|
qp-tunnel-cli hdo refresh [--server-url URL] [--username USER]
|
|
44
|
-
qp-tunnel-cli hdo status
|
|
45
|
-
qp-tunnel-cli hdo down
|
|
44
|
+
qp-tunnel-cli hdo status [--interface NAME]
|
|
45
|
+
qp-tunnel-cli hdo down [--interface NAME]
|
|
46
46
|
|
|
47
47
|
Enroll options:
|
|
48
48
|
--server-url URL HDO/electron-server base URL
|
|
@@ -80,6 +80,10 @@ Notes:
|
|
|
80
80
|
Linux writes /etc/wireguard and enables wg-quick@<interface>.
|
|
81
81
|
macOS installs a LaunchDaemon and may prompt for an administrator password.
|
|
82
82
|
Windows installs a WireGuard tunnel service and may show a UAC prompt.
|
|
83
|
+
|
|
84
|
+
Tip:
|
|
85
|
+
If the Electron HDO plugin created the tunnel, stop it with:
|
|
86
|
+
qp-tunnel-cli hdo down --interface hdo-client
|
|
83
87
|
`);
|
|
84
88
|
}
|
|
85
89
|
async function enrollCommand(args, refreshOnly) {
|
|
@@ -172,12 +176,12 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
172
176
|
'',
|
|
173
177
|
].join('\n'));
|
|
174
178
|
}
|
|
175
|
-
function statusCommand(
|
|
176
|
-
const stateFile = resolveStateFile(
|
|
179
|
+
function statusCommand(input) {
|
|
180
|
+
const stateFile = resolveStateFile(input.stateFile);
|
|
177
181
|
const state = readState(stateFile);
|
|
178
|
-
const interfaceName = sanitizeInterfaceName(state.interfaceName || defaultInterfaceName);
|
|
179
|
-
const configPath = resolveConfigPath(state.configPath, interfaceName);
|
|
180
|
-
const installDir = resolveInstallDir(state.installDir);
|
|
182
|
+
const interfaceName = sanitizeInterfaceName(input.interfaceName || state.interfaceName || defaultInterfaceName);
|
|
183
|
+
const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
|
|
184
|
+
const installDir = resolveInstallDir(input.installDir || state.installDir);
|
|
181
185
|
process.stdout.write(`State file: ${stateFile}\n`);
|
|
182
186
|
process.stdout.write(`Server URL: ${state.serverUrl || 'unset'}\n`);
|
|
183
187
|
process.stdout.write(`Device: ${state.deviceId || 'unset'}\n`);
|
|
@@ -199,22 +203,27 @@ function statusCommand(stateFileInput) {
|
|
|
199
203
|
if (daemon.plistPath)
|
|
200
204
|
process.stdout.write(`plist: ${daemon.plistPath}\n`);
|
|
201
205
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
try {
|
|
207
|
+
const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
|
|
208
|
+
process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
|
|
209
|
+
process.stdout.write(`Runtime: ${runtime.method}\n`);
|
|
210
|
+
if (tunnel.realInterfaceName)
|
|
211
|
+
process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
|
|
212
|
+
if (tunnel.peers.length)
|
|
213
|
+
process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
|
|
214
|
+
if (tunnel.error)
|
|
215
|
+
process.stdout.write(`Status detail: ${tunnel.error}\n`);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
process.stdout.write(`WireGuard status detail: ${errorMessage(err)}\n`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function downCommand(input) {
|
|
222
|
+
const stateFile = resolveStateFile(input.stateFile);
|
|
214
223
|
const state = readState(stateFile);
|
|
215
|
-
const interfaceName = sanitizeInterfaceName(state.interfaceName || defaultInterfaceName);
|
|
216
|
-
const configPath = resolveConfigPath(state.configPath, interfaceName);
|
|
217
|
-
const installDir = resolveInstallDir(state.installDir);
|
|
224
|
+
const interfaceName = sanitizeInterfaceName(input.interfaceName || state.interfaceName || defaultInterfaceName);
|
|
225
|
+
const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
|
|
226
|
+
const installDir = resolveInstallDir(input.installDir || state.installDir);
|
|
218
227
|
if (process.platform === 'linux') {
|
|
219
228
|
inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
|
|
220
229
|
return;
|
|
@@ -224,6 +233,10 @@ async function downCommand(stateFileInput) {
|
|
|
224
233
|
allowSystemFallback: true,
|
|
225
234
|
});
|
|
226
235
|
if (process.platform === 'darwin') {
|
|
236
|
+
if (!canReadFile(configPath)) {
|
|
237
|
+
uninstallDarwinLaunchDaemonByInterface(interfaceName);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
227
240
|
const result = await (0, electron_core_wireguard_1.uninstallDarwinWireGuardLaunchDaemon)({ runtime, configPath });
|
|
228
241
|
if (!result.ok)
|
|
229
242
|
throw new Error(result.message);
|
|
@@ -304,22 +317,35 @@ function parseEnrollOptions(args) {
|
|
|
304
317
|
}
|
|
305
318
|
return options;
|
|
306
319
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
320
|
+
function parseCommandOptions(args) {
|
|
321
|
+
const options = {};
|
|
309
322
|
for (let index = 0; index < args.length; index += 1) {
|
|
310
323
|
const arg = args[index];
|
|
311
|
-
|
|
324
|
+
const readValue = () => {
|
|
312
325
|
const value = args[index + 1];
|
|
313
326
|
if (!value)
|
|
314
|
-
throw new Error(
|
|
315
|
-
stateFile = value;
|
|
327
|
+
throw new Error(`Missing value for ${arg}`);
|
|
316
328
|
index += 1;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
329
|
+
return value;
|
|
330
|
+
};
|
|
331
|
+
switch (arg) {
|
|
332
|
+
case '--state-file':
|
|
333
|
+
options.stateFile = readValue();
|
|
334
|
+
break;
|
|
335
|
+
case '--interface':
|
|
336
|
+
options.interfaceName = readValue();
|
|
337
|
+
break;
|
|
338
|
+
case '--config-path':
|
|
339
|
+
options.configPath = readValue();
|
|
340
|
+
break;
|
|
341
|
+
case '--install-dir':
|
|
342
|
+
options.installDir = readValue();
|
|
343
|
+
break;
|
|
344
|
+
default:
|
|
345
|
+
throw new Error(`Unknown hdo option: ${arg}`);
|
|
320
346
|
}
|
|
321
347
|
}
|
|
322
|
-
return
|
|
348
|
+
return options;
|
|
323
349
|
}
|
|
324
350
|
async function resolveAuth(serverUrl, options, previous, username) {
|
|
325
351
|
const token = resolveExplicitToken(options);
|
|
@@ -524,7 +550,12 @@ function resolveStateFile(input) {
|
|
|
524
550
|
return (0, node_path_1.resolve)(input || defaultStateFile());
|
|
525
551
|
}
|
|
526
552
|
function resolveConfigPath(input, interfaceName) {
|
|
527
|
-
|
|
553
|
+
if (input)
|
|
554
|
+
return (0, node_path_1.resolve)(input);
|
|
555
|
+
const launchDaemonConfig = darwinLaunchDaemonConfigPath(interfaceName);
|
|
556
|
+
if (launchDaemonConfig && (0, node_fs_1.existsSync)(launchDaemonConfig))
|
|
557
|
+
return launchDaemonConfig;
|
|
558
|
+
return (0, node_path_1.resolve)(defaultConfigPath(interfaceName));
|
|
528
559
|
}
|
|
529
560
|
function resolveInstallDir(input) {
|
|
530
561
|
return (0, node_path_1.resolve)(input || defaultInstallDir());
|
|
@@ -543,6 +574,11 @@ function defaultConfigPath(interfaceName) {
|
|
|
543
574
|
return (0, node_path_1.join)(windowsUserDataDir(), `${interfaceName}.conf`);
|
|
544
575
|
return (0, node_path_1.join)((0, node_os_1.homedir)(), '.qpjoy', 'hdo', `${interfaceName}.conf`);
|
|
545
576
|
}
|
|
577
|
+
function darwinLaunchDaemonConfigPath(interfaceName) {
|
|
578
|
+
if (process.platform !== 'darwin')
|
|
579
|
+
return null;
|
|
580
|
+
return `/Library/Application Support/QPJoy/HDO/${interfaceName}/${interfaceName}.conf`;
|
|
581
|
+
}
|
|
546
582
|
function defaultInstallDir() {
|
|
547
583
|
if (process.platform === 'linux')
|
|
548
584
|
return '/usr/local/lib/qpjoy/hdo/bin';
|
|
@@ -560,6 +596,32 @@ function inheritRequired(command, args) {
|
|
|
560
596
|
const result = (0, node_child_process_1.spawnSync)(command, args, { stdio: 'inherit' });
|
|
561
597
|
assertSpawnOk(command, args, result);
|
|
562
598
|
}
|
|
599
|
+
function uninstallDarwinLaunchDaemonByInterface(interfaceName) {
|
|
600
|
+
const component = sanitizeLaunchDaemonComponent(interfaceName);
|
|
601
|
+
const label = `com.qpjoy.hdo.wireguard.${component}`;
|
|
602
|
+
const plist = `/Library/LaunchDaemons/${label}.plist`;
|
|
603
|
+
const supportDir = `/Library/Application Support/QPJoy/HDO/${component}`;
|
|
604
|
+
const script = [
|
|
605
|
+
'set -e',
|
|
606
|
+
`LABEL=${shellQuote(label)}`,
|
|
607
|
+
`PLIST=${shellQuote(plist)}`,
|
|
608
|
+
`SUPPORT_DIR=${shellQuote(supportDir)}`,
|
|
609
|
+
`PID_FILE=${shellQuote(`/var/run/wireguard/${interfaceName}.pid`)}`,
|
|
610
|
+
`NAME_FILE=${shellQuote(`/var/run/wireguard/${interfaceName}.name`)}`,
|
|
611
|
+
`WIREGUARD_GO=${shellQuote(`${supportDir}/bin/wireguard-go`)}`,
|
|
612
|
+
'launchctl bootout "system/$LABEL" >/dev/null 2>&1 || launchctl bootout system "$PLIST" >/dev/null 2>&1 || true',
|
|
613
|
+
'if [ -s "$PID_FILE" ]; then WG_PID="$(cat "$PID_FILE" 2>/dev/null || true)"; if [ -n "$WG_PID" ]; then kill "$WG_PID" >/dev/null 2>&1 || true; sleep 0.2; kill -9 "$WG_PID" >/dev/null 2>&1 || true; fi; fi',
|
|
614
|
+
'if command -v pgrep >/dev/null 2>&1; then for stale_pid in $(pgrep -x wireguard-go 2>/dev/null || true); do stale_command="$(ps -p "$stale_pid" -o command= 2>/dev/null || true)"; printf "%s\\n" "$stale_command" | grep -F "$WIREGUARD_GO" >/dev/null 2>&1 && kill "$stale_pid" >/dev/null 2>&1 || true; done; fi',
|
|
615
|
+
'rm -f "$PLIST" "$PID_FILE" "$NAME_FILE"',
|
|
616
|
+
'rm -rf "$SUPPORT_DIR"'
|
|
617
|
+
].join('\n');
|
|
618
|
+
const appleScript = `do shell script ${appleScriptString(script)} with administrator privileges`;
|
|
619
|
+
const result = (0, node_child_process_1.spawnSync)('osascript', ['-e', appleScript], {
|
|
620
|
+
encoding: 'utf8'
|
|
621
|
+
});
|
|
622
|
+
assertSpawnOk('osascript', ['-e', '<uninstall-hdo-launchdaemon>'], result);
|
|
623
|
+
process.stdout.write(`Stopped and removed ${label}.\n`);
|
|
624
|
+
}
|
|
563
625
|
function assertSpawnOk(command, args, result) {
|
|
564
626
|
if (result.error) {
|
|
565
627
|
throw new Error(`${command} failed: ${result.error.message}`);
|
|
@@ -594,11 +656,32 @@ function tokenExpired(value) {
|
|
|
594
656
|
const time = Date.parse(value);
|
|
595
657
|
return Number.isFinite(time) && Date.now() > time - 60_000;
|
|
596
658
|
}
|
|
659
|
+
function canReadFile(path) {
|
|
660
|
+
try {
|
|
661
|
+
(0, node_fs_1.readFileSync)(path, 'utf8');
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
597
668
|
function chmodSyncSafe(path, mode) {
|
|
598
669
|
if (process.platform === 'win32')
|
|
599
670
|
return;
|
|
600
671
|
(0, node_fs_1.chmodSync)(path, mode);
|
|
601
672
|
}
|
|
673
|
+
function sanitizeLaunchDaemonComponent(value) {
|
|
674
|
+
return value.replace(/[^A-Za-z0-9.-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'hdo-client';
|
|
675
|
+
}
|
|
676
|
+
function shellQuote(value) {
|
|
677
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
678
|
+
}
|
|
679
|
+
function appleScriptString(value) {
|
|
680
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
681
|
+
}
|
|
682
|
+
function errorMessage(err) {
|
|
683
|
+
return err instanceof Error ? err.message : String(err);
|
|
684
|
+
}
|
|
602
685
|
function requireRecord(value, label) {
|
|
603
686
|
if (!isRecord(value))
|
|
604
687
|
throw new Error(`${label} is not an object.`);
|