@qpjoy/tunnel-cli 0.1.6 → 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/dist/hdo.js +118 -0
- package/package.json +2 -2
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 \\
|
|
@@ -80,6 +87,9 @@ Notes:
|
|
|
80
87
|
Linux writes /etc/wireguard and enables wg-quick@<interface>.
|
|
81
88
|
If Linux does not provide wg-quick@.service, this CLI installs a compatible
|
|
82
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.
|
|
83
93
|
macOS installs a LaunchDaemon and may prompt for an administrator password.
|
|
84
94
|
Windows installs a WireGuard tunnel service and may show a UAC prompt.
|
|
85
95
|
|
|
@@ -112,6 +122,7 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
112
122
|
`hdo-${process.platform}-${sanitizeId((0, node_os_1.hostname)())}`;
|
|
113
123
|
const label = options.label || previous.label || `${process.platform} ${(0, node_os_1.hostname)()}`;
|
|
114
124
|
const keys = resolveKeypair(options, previous, installDir);
|
|
125
|
+
const direct = resolveDirectEndpoint(options, previous);
|
|
115
126
|
const registered = await apiJson(serverUrl, auth.accessToken, '/api/v1/hdo/devices/register', {
|
|
116
127
|
method: 'POST',
|
|
117
128
|
body: {
|
|
@@ -127,6 +138,12 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
127
138
|
wireGuard: {
|
|
128
139
|
publicKey: keys.publicKey,
|
|
129
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,
|
|
130
147
|
updatedAt: new Date().toISOString(),
|
|
131
148
|
},
|
|
132
149
|
},
|
|
@@ -139,9 +156,11 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
139
156
|
writeWireGuardConfig(configPath, (0, electron_core_wireguard_1.renderHdoClientWireGuardConfig)({
|
|
140
157
|
privateKey: runtime.privateKey,
|
|
141
158
|
address: runtime.address,
|
|
159
|
+
listenPort: direct.listenPort,
|
|
142
160
|
domesticPublicKey: runtime.domesticPublicKey,
|
|
143
161
|
domesticEndpoint: runtime.domesticEndpoint,
|
|
144
162
|
allowedIps: runtime.allowedIps,
|
|
163
|
+
directPeers: runtime.directPeers,
|
|
145
164
|
persistentKeepalive: 25,
|
|
146
165
|
}));
|
|
147
166
|
const now = new Date().toISOString();
|
|
@@ -160,6 +179,10 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
160
179
|
privateKey: keys.privateKey,
|
|
161
180
|
publicKey: keys.publicKey,
|
|
162
181
|
overlayIp: runtime.overlayIp,
|
|
182
|
+
directListener: direct.directListener,
|
|
183
|
+
preferDirectPeers: direct.preferDirectPeers,
|
|
184
|
+
endpointHost: direct.endpointHost,
|
|
185
|
+
listenPort: direct.listenPort,
|
|
163
186
|
lastManifestGeneration: runtime.generation,
|
|
164
187
|
enrolledAt: previous.enrolledAt || now,
|
|
165
188
|
updatedAt: now,
|
|
@@ -272,6 +295,8 @@ function parseEnrollOptions(args) {
|
|
|
272
295
|
role: 'internal',
|
|
273
296
|
start: true,
|
|
274
297
|
rotateKey: false,
|
|
298
|
+
directListener: false,
|
|
299
|
+
preferDirectPeers: false,
|
|
275
300
|
};
|
|
276
301
|
for (let index = 0; index < args.length; index += 1) {
|
|
277
302
|
const arg = args[index];
|
|
@@ -323,6 +348,23 @@ function parseEnrollOptions(args) {
|
|
|
323
348
|
case '--role':
|
|
324
349
|
options.role = readValue();
|
|
325
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;
|
|
326
368
|
case '--rotate-key':
|
|
327
369
|
options.rotateKey = true;
|
|
328
370
|
break;
|
|
@@ -467,6 +509,56 @@ function resolveKeypair(options, previous, installDir) {
|
|
|
467
509
|
}
|
|
468
510
|
return (0, electron_core_wireguard_1.generateWireGuardKeyPairWithCli)(runtime.command);
|
|
469
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
|
+
}
|
|
470
562
|
async function startSystemTunnel(interfaceName, configPath, installDir) {
|
|
471
563
|
if (process.platform === 'linux') {
|
|
472
564
|
const runtime = await ensureLinuxWireGuardRuntime(installDir);
|
|
@@ -553,6 +645,7 @@ function hdoRuntimeFromManifest(manifest, registered, privateKey) {
|
|
|
553
645
|
domesticPublicKey,
|
|
554
646
|
domesticEndpoint,
|
|
555
647
|
allowedIps,
|
|
648
|
+
directPeers: directPeersFromManifest(wireGuard, overlayIp),
|
|
556
649
|
generation: numberField(root.generation),
|
|
557
650
|
};
|
|
558
651
|
}
|
|
@@ -809,6 +902,28 @@ function sanitizeId(value) {
|
|
|
809
902
|
.replace(/^-+|-+$/g, '')
|
|
810
903
|
.slice(0, 80) || 'device';
|
|
811
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
|
+
}
|
|
812
927
|
function tokenExpired(value) {
|
|
813
928
|
if (!value)
|
|
814
929
|
return false;
|
|
@@ -849,6 +964,9 @@ function requireRecord(value, label) {
|
|
|
849
964
|
throw new Error(`${label} is not an object.`);
|
|
850
965
|
return value;
|
|
851
966
|
}
|
|
967
|
+
function plainObject(value) {
|
|
968
|
+
return isRecord(value) ? value : null;
|
|
969
|
+
}
|
|
852
970
|
function isRecord(value) {
|
|
853
971
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
854
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"
|