@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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/hdo.js +117 -34
  3. 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(parseStateFileArg(rest));
26
+ statusCommand(parseCommandOptions(rest));
27
27
  return;
28
28
  case 'down':
29
- await downCommand(parseStateFileArg(rest));
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(stateFileInput) {
176
- const stateFile = resolveStateFile(stateFileInput);
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
- const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
203
- process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
204
- process.stdout.write(`Runtime: ${runtime.method}\n`);
205
- if (tunnel.realInterfaceName)
206
- process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
207
- if (tunnel.peers.length)
208
- process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
209
- if (tunnel.error)
210
- process.stdout.write(`Status detail: ${tunnel.error}\n`);
211
- }
212
- async function downCommand(stateFileInput) {
213
- const stateFile = resolveStateFile(stateFileInput);
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 parseStateFileArg(args) {
308
- let stateFile;
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
- if (arg === '--state-file') {
324
+ const readValue = () => {
312
325
  const value = args[index + 1];
313
326
  if (!value)
314
- throw new Error('Missing value for --state-file');
315
- stateFile = value;
327
+ throw new Error(`Missing value for ${arg}`);
316
328
  index += 1;
317
- }
318
- else {
319
- throw new Error(`Unknown hdo option: ${arg}`);
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 stateFile;
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
- return (0, node_path_1.resolve)(input || defaultConfigPath(interfaceName));
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.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qpjoy/tunnel-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Global QPJoy Tunnel CLI for mihomo-client and cross-platform HDO mesh enrollment.",
5
5
  "private": false,
6
6
  "type": "commonjs",