@qpjoy/tunnel-cli 0.1.6 → 0.1.8
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 +125 -2
- 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
|
},
|
|
@@ -135,13 +152,17 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
135
152
|
const manifest = await apiJson(serverUrl, auth.accessToken, `/api/v1/hdo/manifest/${encodeURIComponent(deviceId)}`, {
|
|
136
153
|
method: 'GET',
|
|
137
154
|
});
|
|
138
|
-
const runtime = hdoRuntimeFromManifest(manifest, registered, keys.privateKey
|
|
155
|
+
const runtime = hdoRuntimeFromManifest(manifest, registered, keys.privateKey, {
|
|
156
|
+
allowEndpointlessDirectPeers: direct.directListener,
|
|
157
|
+
});
|
|
139
158
|
writeWireGuardConfig(configPath, (0, electron_core_wireguard_1.renderHdoClientWireGuardConfig)({
|
|
140
159
|
privateKey: runtime.privateKey,
|
|
141
160
|
address: runtime.address,
|
|
161
|
+
listenPort: direct.listenPort,
|
|
142
162
|
domesticPublicKey: runtime.domesticPublicKey,
|
|
143
163
|
domesticEndpoint: runtime.domesticEndpoint,
|
|
144
164
|
allowedIps: runtime.allowedIps,
|
|
165
|
+
directPeers: runtime.directPeers,
|
|
145
166
|
persistentKeepalive: 25,
|
|
146
167
|
}));
|
|
147
168
|
const now = new Date().toISOString();
|
|
@@ -160,6 +181,10 @@ async function enrollCommand(args, refreshOnly) {
|
|
|
160
181
|
privateKey: keys.privateKey,
|
|
161
182
|
publicKey: keys.publicKey,
|
|
162
183
|
overlayIp: runtime.overlayIp,
|
|
184
|
+
directListener: direct.directListener,
|
|
185
|
+
preferDirectPeers: direct.preferDirectPeers,
|
|
186
|
+
endpointHost: direct.endpointHost,
|
|
187
|
+
listenPort: direct.listenPort,
|
|
163
188
|
lastManifestGeneration: runtime.generation,
|
|
164
189
|
enrolledAt: previous.enrolledAt || now,
|
|
165
190
|
updatedAt: now,
|
|
@@ -272,6 +297,8 @@ function parseEnrollOptions(args) {
|
|
|
272
297
|
role: 'internal',
|
|
273
298
|
start: true,
|
|
274
299
|
rotateKey: false,
|
|
300
|
+
directListener: false,
|
|
301
|
+
preferDirectPeers: false,
|
|
275
302
|
};
|
|
276
303
|
for (let index = 0; index < args.length; index += 1) {
|
|
277
304
|
const arg = args[index];
|
|
@@ -323,6 +350,23 @@ function parseEnrollOptions(args) {
|
|
|
323
350
|
case '--role':
|
|
324
351
|
options.role = readValue();
|
|
325
352
|
break;
|
|
353
|
+
case '--direct-listener':
|
|
354
|
+
options.directListener = true;
|
|
355
|
+
break;
|
|
356
|
+
case '--try-direct-peers':
|
|
357
|
+
case '--prefer-direct-peers':
|
|
358
|
+
options.preferDirectPeers = true;
|
|
359
|
+
break;
|
|
360
|
+
case '--public-endpoint':
|
|
361
|
+
options.publicEndpoint = readValue();
|
|
362
|
+
break;
|
|
363
|
+
case '--endpoint-host':
|
|
364
|
+
case '--public-host':
|
|
365
|
+
options.endpointHost = readValue();
|
|
366
|
+
break;
|
|
367
|
+
case '--listen-port':
|
|
368
|
+
options.listenPort = parsePort(readValue(), arg);
|
|
369
|
+
break;
|
|
326
370
|
case '--rotate-key':
|
|
327
371
|
options.rotateKey = true;
|
|
328
372
|
break;
|
|
@@ -467,6 +511,59 @@ function resolveKeypair(options, previous, installDir) {
|
|
|
467
511
|
}
|
|
468
512
|
return (0, electron_core_wireguard_1.generateWireGuardKeyPairWithCli)(runtime.command);
|
|
469
513
|
}
|
|
514
|
+
function resolveDirectEndpoint(options, previous) {
|
|
515
|
+
const publicEndpoint = options.publicEndpoint ??
|
|
516
|
+
process.env.HDO_PUBLIC_ENDPOINT ??
|
|
517
|
+
process.env.QPJOY_HDO_PUBLIC_ENDPOINT;
|
|
518
|
+
const parsedEndpoint = publicEndpoint ? parseEndpoint(publicEndpoint) : {};
|
|
519
|
+
const endpointHost = options.endpointHost ??
|
|
520
|
+
process.env.HDO_ENDPOINT_HOST ??
|
|
521
|
+
process.env.QPJOY_HDO_ENDPOINT_HOST ??
|
|
522
|
+
parsedEndpoint.host ??
|
|
523
|
+
previous.endpointHost;
|
|
524
|
+
const listenPort = options.listenPort ??
|
|
525
|
+
parseOptionalPort(process.env.HDO_LISTEN_PORT ?? process.env.QPJOY_HDO_LISTEN_PORT) ??
|
|
526
|
+
parsedEndpoint.port ??
|
|
527
|
+
previous.listenPort;
|
|
528
|
+
const directListener = Boolean(options.directListener ||
|
|
529
|
+
publicEndpoint ||
|
|
530
|
+
options.endpointHost ||
|
|
531
|
+
options.listenPort ||
|
|
532
|
+
previous.directListener);
|
|
533
|
+
const preferDirectPeers = Boolean(options.preferDirectPeers || previous.preferDirectPeers);
|
|
534
|
+
return {
|
|
535
|
+
directListener,
|
|
536
|
+
preferDirectPeers,
|
|
537
|
+
endpointHost,
|
|
538
|
+
listenPort,
|
|
539
|
+
endpoint: endpointHost && listenPort ? `${endpointHost}:${listenPort}` : undefined,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function directPeersFromManifest(wireGuard, ownOverlayIp, options = {}) {
|
|
543
|
+
const ownIp = ownOverlayIp.split('/')[0] || ownOverlayIp;
|
|
544
|
+
const rows = Array.isArray(wireGuard.directPeers) ? wireGuard.directPeers : [];
|
|
545
|
+
return rows.flatMap((item) => {
|
|
546
|
+
const row = plainObject(item);
|
|
547
|
+
if (!row)
|
|
548
|
+
return [];
|
|
549
|
+
const publicKey = stringField(row.publicKey);
|
|
550
|
+
const overlayIp = stringField(row.overlayIp);
|
|
551
|
+
if (!publicKey || !overlayIp || overlayIp === ownIp)
|
|
552
|
+
return [];
|
|
553
|
+
const allowedIps = stringArray(row.allowedIps);
|
|
554
|
+
const endpoint = stringField(row.endpoint);
|
|
555
|
+
if (!endpoint && options.allowEndpointlessDirectPeers !== true)
|
|
556
|
+
return [];
|
|
557
|
+
const peer = {
|
|
558
|
+
name: `HDO Direct ${stringField(row.label) ?? stringField(row.id) ?? overlayIp}`,
|
|
559
|
+
publicKey,
|
|
560
|
+
allowedIps: allowedIps.length ? allowedIps : [`${overlayIp}/32`],
|
|
561
|
+
endpoint,
|
|
562
|
+
persistentKeepalive: 25,
|
|
563
|
+
};
|
|
564
|
+
return [peer];
|
|
565
|
+
});
|
|
566
|
+
}
|
|
470
567
|
async function startSystemTunnel(interfaceName, configPath, installDir) {
|
|
471
568
|
if (process.platform === 'linux') {
|
|
472
569
|
const runtime = await ensureLinuxWireGuardRuntime(installDir);
|
|
@@ -519,7 +616,7 @@ function writeWireGuardConfig(path, content) {
|
|
|
519
616
|
(0, node_fs_1.writeFileSync)(path, content, { mode: 0o600 });
|
|
520
617
|
chmodSyncSafe(path, 0o600);
|
|
521
618
|
}
|
|
522
|
-
function hdoRuntimeFromManifest(manifest, registered, privateKey) {
|
|
619
|
+
function hdoRuntimeFromManifest(manifest, registered, privateKey, options = {}) {
|
|
523
620
|
const root = requireRecord(manifest, 'manifest');
|
|
524
621
|
const license = requireRecord(root.license, 'manifest.license');
|
|
525
622
|
if (license.active !== true) {
|
|
@@ -553,6 +650,7 @@ function hdoRuntimeFromManifest(manifest, registered, privateKey) {
|
|
|
553
650
|
domesticPublicKey,
|
|
554
651
|
domesticEndpoint,
|
|
555
652
|
allowedIps,
|
|
653
|
+
directPeers: directPeersFromManifest(wireGuard, overlayIp, options),
|
|
556
654
|
generation: numberField(root.generation),
|
|
557
655
|
};
|
|
558
656
|
}
|
|
@@ -809,6 +907,28 @@ function sanitizeId(value) {
|
|
|
809
907
|
.replace(/^-+|-+$/g, '')
|
|
810
908
|
.slice(0, 80) || 'device';
|
|
811
909
|
}
|
|
910
|
+
function parseEndpoint(value) {
|
|
911
|
+
const trimmed = value.trim();
|
|
912
|
+
const index = trimmed.lastIndexOf(':');
|
|
913
|
+
if (index <= 0 || index === trimmed.length - 1) {
|
|
914
|
+
throw new Error(`Invalid --public-endpoint, expected HOST:PORT: ${value}`);
|
|
915
|
+
}
|
|
916
|
+
const host = trimmed.slice(0, index).replace(/^\[|\]$/g, '');
|
|
917
|
+
const port = parsePort(trimmed.slice(index + 1), '--public-endpoint');
|
|
918
|
+
return { host, port };
|
|
919
|
+
}
|
|
920
|
+
function parsePort(value, label) {
|
|
921
|
+
const port = Number(value);
|
|
922
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
923
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
924
|
+
}
|
|
925
|
+
return port;
|
|
926
|
+
}
|
|
927
|
+
function parseOptionalPort(value) {
|
|
928
|
+
if (!value)
|
|
929
|
+
return undefined;
|
|
930
|
+
return parsePort(value, 'listen port');
|
|
931
|
+
}
|
|
812
932
|
function tokenExpired(value) {
|
|
813
933
|
if (!value)
|
|
814
934
|
return false;
|
|
@@ -849,6 +969,9 @@ function requireRecord(value, label) {
|
|
|
849
969
|
throw new Error(`${label} is not an object.`);
|
|
850
970
|
return value;
|
|
851
971
|
}
|
|
972
|
+
function plainObject(value) {
|
|
973
|
+
return isRecord(value) ? value : null;
|
|
974
|
+
}
|
|
852
975
|
function isRecord(value) {
|
|
853
976
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
854
977
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qpjoy/tunnel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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.21"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.10.7"
|