@qpjoy/tunnel-cli 0.1.5 → 0.1.7
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 +3 -1
- package/README.setup.md +3 -0
- package/dist/hdo.js +288 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ The command:
|
|
|
45
45
|
- downloads the HDO manifest from `electron-server`
|
|
46
46
|
- writes a local WireGuard config
|
|
47
47
|
- stores local HDO state and refresh credentials
|
|
48
|
-
- starts a system-level tunnel at boot
|
|
48
|
+
- starts a system-level tunnel at boot. On Linux, if `wg-quick@.service` is not
|
|
49
|
+
installed by the OS, the CLI writes a compatible systemd unit that uses the
|
|
50
|
+
bundled WireGuard tools from the npm package.
|
|
49
51
|
|
|
50
52
|
Useful follow-up commands:
|
|
51
53
|
|
package/README.setup.md
CHANGED
package/dist/hdo.js
CHANGED
|
@@ -58,6 +58,12 @@ Enroll options:
|
|
|
58
58
|
--install-dir PATH WireGuard engine install/cache directory
|
|
59
59
|
--state-file PATH HDO client state file
|
|
60
60
|
--role ROLE Metadata role. Default: internal
|
|
61
|
+
--direct-listener Accept direct WireGuard peers from other HDO devices
|
|
62
|
+
--try-direct-peers Try direct HDO device peers from observed NAT endpoints
|
|
63
|
+
--public-endpoint HOST:PORT
|
|
64
|
+
Publish this device as a direct peer endpoint
|
|
65
|
+
--endpoint-host HOST Direct peer endpoint host
|
|
66
|
+
--listen-port PORT Local WireGuard listen port for direct peers
|
|
61
67
|
--rotate-key Generate a new WireGuard keypair
|
|
62
68
|
--no-start Write config without starting the system tunnel
|
|
63
69
|
|
|
@@ -66,6 +72,7 @@ Environment:
|
|
|
66
72
|
HDO_USERNAME / QPJOY_HDO_USERNAME
|
|
67
73
|
HDO_PASSWORD / QPJOY_HDO_PASSWORD
|
|
68
74
|
HDO_TOKEN / QPJOY_HDO_TOKEN
|
|
75
|
+
HDO_PUBLIC_ENDPOINT / QPJOY_HDO_PUBLIC_ENDPOINT
|
|
69
76
|
|
|
70
77
|
Examples:
|
|
71
78
|
qp-tunnel-cli hdo enroll \\
|
|
@@ -78,6 +85,11 @@ Examples:
|
|
|
78
85
|
|
|
79
86
|
Notes:
|
|
80
87
|
Linux writes /etc/wireguard and enables wg-quick@<interface>.
|
|
88
|
+
If Linux does not provide wg-quick@.service, this CLI installs a compatible
|
|
89
|
+
systemd unit that uses the bundled WireGuard tools from npm.
|
|
90
|
+
Use --direct-listener --public-endpoint HOST:PORT on reachable Internal
|
|
91
|
+
machines. Use --try-direct-peers only when both devices are managed by this
|
|
92
|
+
CLI/plugin and you accept NAT hole-punching fallback risk.
|
|
81
93
|
macOS installs a LaunchDaemon and may prompt for an administrator password.
|
|
82
94
|
Windows installs a WireGuard tunnel service and may show a UAC prompt.
|
|
83
95
|
|
|
@@ -110,6 +122,7 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
110
122
|
`hdo-${process.platform}-${sanitizeId((0, node_os_1.hostname)())}`;
|
|
111
123
|
const label = options.label || previous.label || `${process.platform} ${(0, node_os_1.hostname)()}`;
|
|
112
124
|
const keys = resolveKeypair(options, previous, installDir);
|
|
125
|
+
const direct = resolveDirectEndpoint(options, previous);
|
|
113
126
|
const registered = await apiJson(serverUrl, auth.accessToken, '/api/v1/hdo/devices/register', {
|
|
114
127
|
method: 'POST',
|
|
115
128
|
body: {
|
|
@@ -125,6 +138,12 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
125
138
|
wireGuard: {
|
|
126
139
|
publicKey: keys.publicKey,
|
|
127
140
|
interfaceName,
|
|
141
|
+
preferDirectPeers: direct.preferDirectPeers,
|
|
142
|
+
acceptDirectPeers: direct.directListener,
|
|
143
|
+
directListener: direct.directListener,
|
|
144
|
+
endpointHost: direct.endpointHost,
|
|
145
|
+
listenPort: direct.listenPort,
|
|
146
|
+
endpoint: direct.endpoint,
|
|
128
147
|
updatedAt: new Date().toISOString(),
|
|
129
148
|
},
|
|
130
149
|
},
|
|
@@ -137,9 +156,11 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
137
156
|
writeWireGuardConfig(configPath, (0, electron_core_wireguard_1.renderHdoClientWireGuardConfig)({
|
|
138
157
|
privateKey: runtime.privateKey,
|
|
139
158
|
address: runtime.address,
|
|
159
|
+
listenPort: direct.listenPort,
|
|
140
160
|
domesticPublicKey: runtime.domesticPublicKey,
|
|
141
161
|
domesticEndpoint: runtime.domesticEndpoint,
|
|
142
162
|
allowedIps: runtime.allowedIps,
|
|
163
|
+
directPeers: runtime.directPeers,
|
|
143
164
|
persistentKeepalive: 25,
|
|
144
165
|
}));
|
|
145
166
|
const now = new Date().toISOString();
|
|
@@ -158,6 +179,10 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
158
179
|
privateKey: keys.privateKey,
|
|
159
180
|
publicKey: keys.publicKey,
|
|
160
181
|
overlayIp: runtime.overlayIp,
|
|
182
|
+
directListener: direct.directListener,
|
|
183
|
+
preferDirectPeers: direct.preferDirectPeers,
|
|
184
|
+
endpointHost: direct.endpointHost,
|
|
185
|
+
listenPort: direct.listenPort,
|
|
161
186
|
lastManifestGeneration: runtime.generation,
|
|
162
187
|
enrolledAt: previous.enrolledAt || now,
|
|
163
188
|
updatedAt: now,
|
|
@@ -182,21 +207,26 @@ function statusCommand(input) {
|
|
|
182
207
|
const interfaceName = sanitizeInterfaceName(input.interfaceName || state.interfaceName || defaultInterfaceName);
|
|
183
208
|
const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
|
|
184
209
|
const installDir = resolveInstallDir(input.installDir || state.installDir);
|
|
210
|
+
const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
|
|
211
|
+
installDir,
|
|
212
|
+
allowSystemFallback: true,
|
|
213
|
+
});
|
|
185
214
|
process.stdout.write(`State file: ${stateFile}\n`);
|
|
186
215
|
process.stdout.write(`Server URL: ${state.serverUrl || 'unset'}\n`);
|
|
187
216
|
process.stdout.write(`Device: ${state.deviceId || 'unset'}\n`);
|
|
188
217
|
process.stdout.write(`Overlay IP: ${state.overlayIp || 'unset'}\n`);
|
|
189
218
|
process.stdout.write(`WireGuard config: ${configPath}\n\n`);
|
|
190
219
|
if (process.platform === 'linux') {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
220
|
+
if (commandAvailable('systemctl')) {
|
|
221
|
+
inherit('systemctl', ['status', `wg-quick@${interfaceName}`, '--no-pager']);
|
|
222
|
+
process.stdout.write('\n');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
process.stdout.write('systemctl unavailable; showing WireGuard runtime status only.\n\n');
|
|
226
|
+
}
|
|
227
|
+
printWireGuardRuntimeStatus(runtime, configPath);
|
|
194
228
|
return;
|
|
195
229
|
}
|
|
196
|
-
const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
|
|
197
|
-
installDir,
|
|
198
|
-
allowSystemFallback: true,
|
|
199
|
-
});
|
|
200
230
|
if (process.platform === 'darwin') {
|
|
201
231
|
const daemon = (0, electron_core_wireguard_1.getDarwinWireGuardLaunchDaemonStatus)({ runtime, configPath });
|
|
202
232
|
process.stdout.write(`LaunchDaemon: ${daemon.loaded ? 'loaded' : 'not loaded'}; running=${daemon.running}\n`);
|
|
@@ -225,7 +255,18 @@ async function downCommand(input) {
|
|
|
225
255
|
const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
|
|
226
256
|
const installDir = resolveInstallDir(input.installDir || state.installDir);
|
|
227
257
|
if (process.platform === 'linux') {
|
|
228
|
-
|
|
258
|
+
const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
|
|
259
|
+
installDir,
|
|
260
|
+
allowSystemFallback: true,
|
|
261
|
+
});
|
|
262
|
+
if (commandAvailable('systemctl') && systemdUnitExists('wg-quick@.service')) {
|
|
263
|
+
inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'down' });
|
|
267
|
+
if (!result.ok)
|
|
268
|
+
throw new Error(result.message);
|
|
269
|
+
process.stdout.write(`${result.message}\n`);
|
|
229
270
|
return;
|
|
230
271
|
}
|
|
231
272
|
const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
|
|
@@ -254,6 +295,8 @@ function parseEnrollOptions(args) {
|
|
|
254
295
|
role: 'internal',
|
|
255
296
|
start: true,
|
|
256
297
|
rotateKey: false,
|
|
298
|
+
directListener: false,
|
|
299
|
+
preferDirectPeers: false,
|
|
257
300
|
};
|
|
258
301
|
for (let index = 0; index < args.length; index += 1) {
|
|
259
302
|
const arg = args[index];
|
|
@@ -305,6 +348,23 @@ function parseEnrollOptions(args) {
|
|
|
305
348
|
case '--role':
|
|
306
349
|
options.role = readValue();
|
|
307
350
|
break;
|
|
351
|
+
case '--direct-listener':
|
|
352
|
+
options.directListener = true;
|
|
353
|
+
break;
|
|
354
|
+
case '--try-direct-peers':
|
|
355
|
+
case '--prefer-direct-peers':
|
|
356
|
+
options.preferDirectPeers = true;
|
|
357
|
+
break;
|
|
358
|
+
case '--public-endpoint':
|
|
359
|
+
options.publicEndpoint = readValue();
|
|
360
|
+
break;
|
|
361
|
+
case '--endpoint-host':
|
|
362
|
+
case '--public-host':
|
|
363
|
+
options.endpointHost = readValue();
|
|
364
|
+
break;
|
|
365
|
+
case '--listen-port':
|
|
366
|
+
options.listenPort = parsePort(readValue(), arg);
|
|
367
|
+
break;
|
|
308
368
|
case '--rotate-key':
|
|
309
369
|
options.rotateKey = true;
|
|
310
370
|
break;
|
|
@@ -449,8 +509,67 @@ function resolveKeypair(options, previous, installDir) {
|
|
|
449
509
|
}
|
|
450
510
|
return (0, electron_core_wireguard_1.generateWireGuardKeyPairWithCli)(runtime.command);
|
|
451
511
|
}
|
|
512
|
+
function resolveDirectEndpoint(options, previous) {
|
|
513
|
+
const publicEndpoint = options.publicEndpoint ??
|
|
514
|
+
process.env.HDO_PUBLIC_ENDPOINT ??
|
|
515
|
+
process.env.QPJOY_HDO_PUBLIC_ENDPOINT;
|
|
516
|
+
const parsedEndpoint = publicEndpoint ? parseEndpoint(publicEndpoint) : {};
|
|
517
|
+
const endpointHost = options.endpointHost ??
|
|
518
|
+
process.env.HDO_ENDPOINT_HOST ??
|
|
519
|
+
process.env.QPJOY_HDO_ENDPOINT_HOST ??
|
|
520
|
+
parsedEndpoint.host ??
|
|
521
|
+
previous.endpointHost;
|
|
522
|
+
const listenPort = options.listenPort ??
|
|
523
|
+
parseOptionalPort(process.env.HDO_LISTEN_PORT ?? process.env.QPJOY_HDO_LISTEN_PORT) ??
|
|
524
|
+
parsedEndpoint.port ??
|
|
525
|
+
previous.listenPort;
|
|
526
|
+
const directListener = Boolean(options.directListener ||
|
|
527
|
+
publicEndpoint ||
|
|
528
|
+
options.endpointHost ||
|
|
529
|
+
options.listenPort ||
|
|
530
|
+
previous.directListener);
|
|
531
|
+
const preferDirectPeers = Boolean(options.preferDirectPeers || previous.preferDirectPeers);
|
|
532
|
+
return {
|
|
533
|
+
directListener,
|
|
534
|
+
preferDirectPeers,
|
|
535
|
+
endpointHost,
|
|
536
|
+
listenPort,
|
|
537
|
+
endpoint: endpointHost && listenPort ? `${endpointHost}:${listenPort}` : undefined,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function directPeersFromManifest(wireGuard, ownOverlayIp) {
|
|
541
|
+
const ownIp = ownOverlayIp.split('/')[0] || ownOverlayIp;
|
|
542
|
+
const rows = Array.isArray(wireGuard.directPeers) ? wireGuard.directPeers : [];
|
|
543
|
+
return rows.flatMap((item) => {
|
|
544
|
+
const row = plainObject(item);
|
|
545
|
+
if (!row)
|
|
546
|
+
return [];
|
|
547
|
+
const publicKey = stringField(row.publicKey);
|
|
548
|
+
const overlayIp = stringField(row.overlayIp);
|
|
549
|
+
if (!publicKey || !overlayIp || overlayIp === ownIp)
|
|
550
|
+
return [];
|
|
551
|
+
const allowedIps = stringArray(row.allowedIps);
|
|
552
|
+
const peer = {
|
|
553
|
+
name: `HDO Direct ${stringField(row.label) ?? stringField(row.id) ?? overlayIp}`,
|
|
554
|
+
publicKey,
|
|
555
|
+
allowedIps: allowedIps.length ? allowedIps : [`${overlayIp}/32`],
|
|
556
|
+
endpoint: stringField(row.endpoint),
|
|
557
|
+
persistentKeepalive: 25,
|
|
558
|
+
};
|
|
559
|
+
return [peer];
|
|
560
|
+
});
|
|
561
|
+
}
|
|
452
562
|
async function startSystemTunnel(interfaceName, configPath, installDir) {
|
|
453
563
|
if (process.platform === 'linux') {
|
|
564
|
+
const runtime = await ensureLinuxWireGuardRuntime(installDir);
|
|
565
|
+
if (!commandAvailable('systemctl')) {
|
|
566
|
+
const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'restart' });
|
|
567
|
+
if (!result.ok)
|
|
568
|
+
throw new Error(result.message);
|
|
569
|
+
return `${result.message} systemctl is unavailable, so this tunnel is not installed as a boot service.`;
|
|
570
|
+
}
|
|
571
|
+
ensureLinuxWgQuickSystemdUnit(runtime);
|
|
572
|
+
inheritRequired('systemctl', ['daemon-reload']);
|
|
454
573
|
inheritRequired('systemctl', ['enable', `wg-quick@${interfaceName}`]);
|
|
455
574
|
inheritRequired('systemctl', ['restart', `wg-quick@${interfaceName}`]);
|
|
456
575
|
return `Enabled and restarted wg-quick@${interfaceName}.`;
|
|
@@ -526,6 +645,7 @@ function hdoRuntimeFromManifest(manifest, registered, privateKey) {
|
|
|
526
645
|
domesticPublicKey,
|
|
527
646
|
domesticEndpoint,
|
|
528
647
|
allowedIps,
|
|
648
|
+
directPeers: directPeersFromManifest(wireGuard, overlayIp),
|
|
529
649
|
generation: numberField(root.generation),
|
|
530
650
|
};
|
|
531
651
|
}
|
|
@@ -596,6 +716,138 @@ function inheritRequired(command, args) {
|
|
|
596
716
|
const result = (0, node_child_process_1.spawnSync)(command, args, { stdio: 'inherit' });
|
|
597
717
|
assertSpawnOk(command, args, result);
|
|
598
718
|
}
|
|
719
|
+
function printWireGuardRuntimeStatus(runtime, configPath) {
|
|
720
|
+
process.stdout.write(`Runtime: ${runtime.method}\n`);
|
|
721
|
+
if (runtime.warnings.length) {
|
|
722
|
+
process.stdout.write(`Runtime warnings: ${runtime.warnings.join('; ')}\n`);
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
|
|
726
|
+
process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
|
|
727
|
+
if (tunnel.realInterfaceName)
|
|
728
|
+
process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
|
|
729
|
+
if (tunnel.peers.length)
|
|
730
|
+
process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
|
|
731
|
+
if (tunnel.error)
|
|
732
|
+
process.stdout.write(`Status detail: ${tunnel.error}\n`);
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
process.stdout.write(`WireGuard status detail: ${errorMessage(err)}\n`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function ensureLinuxWireGuardRuntime(installDir) {
|
|
739
|
+
let runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
|
|
740
|
+
installDir,
|
|
741
|
+
allowSystemFallback: true,
|
|
742
|
+
});
|
|
743
|
+
if (runtime.available)
|
|
744
|
+
return runtime;
|
|
745
|
+
const installed = installLinuxWireGuardTools();
|
|
746
|
+
runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
|
|
747
|
+
installDir,
|
|
748
|
+
allowSystemFallback: true,
|
|
749
|
+
});
|
|
750
|
+
if (runtime.available)
|
|
751
|
+
return runtime;
|
|
752
|
+
throw new Error(installed
|
|
753
|
+
? runtime.error ?? 'WireGuard runtime unavailable after installing wireguard-tools.'
|
|
754
|
+
: `${runtime.error ?? 'WireGuard runtime unavailable'}. Install wireguard-tools or use an npm package with the matching @qpjoy/electron-core-wireguard-engine package.`);
|
|
755
|
+
}
|
|
756
|
+
function installLinuxWireGuardTools() {
|
|
757
|
+
const installers = [
|
|
758
|
+
{ probe: 'apt-get', label: 'apt-get', commands: [['apt-get', 'update'], ['apt-get', 'install', '-y', 'wireguard-tools']] },
|
|
759
|
+
{ probe: 'dnf', label: 'dnf', commands: [['dnf', 'install', '-y', 'wireguard-tools']] },
|
|
760
|
+
{ probe: 'yum', label: 'yum', commands: [['yum', 'install', '-y', 'epel-release'], ['yum', 'install', '-y', 'wireguard-tools']] },
|
|
761
|
+
{ probe: 'apk', label: 'apk', commands: [['apk', 'add', '--no-cache', 'wireguard-tools']] },
|
|
762
|
+
{ probe: 'zypper', label: 'zypper', commands: [['zypper', '--non-interactive', 'install', 'wireguard-tools']] },
|
|
763
|
+
{ probe: 'pacman', label: 'pacman', commands: [['pacman', '-Sy', '--noconfirm', 'wireguard-tools']] },
|
|
764
|
+
];
|
|
765
|
+
for (const installer of installers) {
|
|
766
|
+
if (!commandAvailable(installer.probe))
|
|
767
|
+
continue;
|
|
768
|
+
process.stdout.write(`WireGuard tools are missing; installing wireguard-tools with ${installer.label}.\n`);
|
|
769
|
+
for (const command of installer.commands) {
|
|
770
|
+
const [name, ...args] = command;
|
|
771
|
+
const result = (0, node_child_process_1.spawnSync)(name, args, { stdio: 'inherit' });
|
|
772
|
+
if (result.status !== 0) {
|
|
773
|
+
process.stdout.write(`wireguard-tools install step failed: ${command.join(' ')}\n`);
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
function ensureLinuxWgQuickSystemdUnit(runtime) {
|
|
782
|
+
if (systemdUnitUsable())
|
|
783
|
+
return;
|
|
784
|
+
const wgQuick = runtime.wgQuick?.command;
|
|
785
|
+
if (!wgQuick) {
|
|
786
|
+
throw new Error(runtime.error ?? 'wg-quick unavailable; cannot install systemd boot service.');
|
|
787
|
+
}
|
|
788
|
+
const unitPath = '/etc/systemd/system/wg-quick@.service';
|
|
789
|
+
if ((0, node_fs_1.existsSync)(unitPath)) {
|
|
790
|
+
throw new Error(`${unitPath} exists but wg/wg-quick is not available in PATH. Install wireguard-tools or fix the existing unit.`);
|
|
791
|
+
}
|
|
792
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(unitPath), { recursive: true });
|
|
793
|
+
(0, node_fs_1.writeFileSync)(unitPath, renderLinuxWgQuickSystemdUnit(runtime, wgQuick), { mode: 0o644 });
|
|
794
|
+
chmodSyncSafe(unitPath, 0o644);
|
|
795
|
+
process.stdout.write(`Installed ${unitPath} using bundled WireGuard tools.\n`);
|
|
796
|
+
}
|
|
797
|
+
function renderLinuxWgQuickSystemdUnit(runtime, wgQuick) {
|
|
798
|
+
const pathDirs = uniqueStrings([
|
|
799
|
+
runtime.wg.command ? (0, node_path_1.dirname)(runtime.wg.command) : '',
|
|
800
|
+
runtime.wgQuick?.command ? (0, node_path_1.dirname)(runtime.wgQuick.command) : '',
|
|
801
|
+
runtime.wireGuardGo?.command ? (0, node_path_1.dirname)(runtime.wireGuardGo.command) : '',
|
|
802
|
+
'/usr/local/sbin',
|
|
803
|
+
'/usr/local/bin',
|
|
804
|
+
'/usr/sbin',
|
|
805
|
+
'/usr/bin',
|
|
806
|
+
'/sbin',
|
|
807
|
+
'/bin',
|
|
808
|
+
].filter(Boolean));
|
|
809
|
+
return `[Unit]
|
|
810
|
+
Description=WireGuard via wg-quick(8) for %I
|
|
811
|
+
Documentation=man:wg-quick(8) man:wg(8)
|
|
812
|
+
Wants=network-online.target
|
|
813
|
+
After=network-online.target nss-lookup.target
|
|
814
|
+
ConditionPathExists=/etc/wireguard/%i.conf
|
|
815
|
+
|
|
816
|
+
[Service]
|
|
817
|
+
Type=oneshot
|
|
818
|
+
RemainAfterExit=yes
|
|
819
|
+
Environment=${systemdQuote(`PATH=${pathDirs.join(':')}`)}
|
|
820
|
+
Environment=WG_ENDPOINT_RESOLUTION_RETRIES=infinity
|
|
821
|
+
ExecStart=${systemdQuote(wgQuick)} up %i
|
|
822
|
+
ExecStop=${systemdQuote(wgQuick)} down %i
|
|
823
|
+
|
|
824
|
+
[Install]
|
|
825
|
+
WantedBy=multi-user.target
|
|
826
|
+
`;
|
|
827
|
+
}
|
|
828
|
+
function systemdUnitUsable() {
|
|
829
|
+
return systemdUnitExists('wg-quick@.service') && commandAvailable('wg') && commandAvailable('wg-quick');
|
|
830
|
+
}
|
|
831
|
+
function systemdUnitExists(unitName) {
|
|
832
|
+
const paths = [
|
|
833
|
+
`/etc/systemd/system/${unitName}`,
|
|
834
|
+
`/run/systemd/system/${unitName}`,
|
|
835
|
+
`/lib/systemd/system/${unitName}`,
|
|
836
|
+
`/usr/lib/systemd/system/${unitName}`,
|
|
837
|
+
];
|
|
838
|
+
if (paths.some((path) => (0, node_fs_1.existsSync)(path)))
|
|
839
|
+
return true;
|
|
840
|
+
if (!commandAvailable('systemctl'))
|
|
841
|
+
return false;
|
|
842
|
+
const result = (0, node_child_process_1.spawnSync)('systemctl', ['cat', unitName], { stdio: 'ignore' });
|
|
843
|
+
return result.status === 0;
|
|
844
|
+
}
|
|
845
|
+
function commandAvailable(command) {
|
|
846
|
+
const result = (0, node_child_process_1.spawnSync)('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command], {
|
|
847
|
+
stdio: 'ignore',
|
|
848
|
+
});
|
|
849
|
+
return result.status === 0;
|
|
850
|
+
}
|
|
599
851
|
function uninstallDarwinLaunchDaemonByInterface(interfaceName) {
|
|
600
852
|
const component = sanitizeLaunchDaemonComponent(interfaceName);
|
|
601
853
|
const label = `com.qpjoy.hdo.wireguard.${component}`;
|
|
@@ -650,6 +902,28 @@ function sanitizeId(value) {
|
|
|
650
902
|
.replace(/^-+|-+$/g, '')
|
|
651
903
|
.slice(0, 80) || 'device';
|
|
652
904
|
}
|
|
905
|
+
function parseEndpoint(value) {
|
|
906
|
+
const trimmed = value.trim();
|
|
907
|
+
const index = trimmed.lastIndexOf(':');
|
|
908
|
+
if (index <= 0 || index === trimmed.length - 1) {
|
|
909
|
+
throw new Error(`Invalid --public-endpoint, expected HOST:PORT: ${value}`);
|
|
910
|
+
}
|
|
911
|
+
const host = trimmed.slice(0, index).replace(/^\[|\]$/g, '');
|
|
912
|
+
const port = parsePort(trimmed.slice(index + 1), '--public-endpoint');
|
|
913
|
+
return { host, port };
|
|
914
|
+
}
|
|
915
|
+
function parsePort(value, label) {
|
|
916
|
+
const port = Number(value);
|
|
917
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
918
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
919
|
+
}
|
|
920
|
+
return port;
|
|
921
|
+
}
|
|
922
|
+
function parseOptionalPort(value) {
|
|
923
|
+
if (!value)
|
|
924
|
+
return undefined;
|
|
925
|
+
return parsePort(value, 'listen port');
|
|
926
|
+
}
|
|
653
927
|
function tokenExpired(value) {
|
|
654
928
|
if (!value)
|
|
655
929
|
return false;
|
|
@@ -676,6 +950,9 @@ function sanitizeLaunchDaemonComponent(value) {
|
|
|
676
950
|
function shellQuote(value) {
|
|
677
951
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
678
952
|
}
|
|
953
|
+
function systemdQuote(value) {
|
|
954
|
+
return `"${String(value).replace(/%/g, '%%').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
955
|
+
}
|
|
679
956
|
function appleScriptString(value) {
|
|
680
957
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
681
958
|
}
|
|
@@ -687,6 +964,9 @@ function requireRecord(value, label) {
|
|
|
687
964
|
throw new Error(`${label} is not an object.`);
|
|
688
965
|
return value;
|
|
689
966
|
}
|
|
967
|
+
function plainObject(value) {
|
|
968
|
+
return isRecord(value) ? value : null;
|
|
969
|
+
}
|
|
690
970
|
function isRecord(value) {
|
|
691
971
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
692
972
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qpjoy/tunnel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Global QPJoy Tunnel CLI for mihomo-client and cross-platform HDO mesh enrollment.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "commonjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@qpjoy/electron-core-wireguard": "^0.1.
|
|
25
|
+
"@qpjoy/electron-core-wireguard": "^0.1.19"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.10.7"
|