@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.
Files changed (2) hide show
  1. package/dist/hdo.js +125 -2
  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.6",
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.18"
25
+ "@qpjoy/electron-core-wireguard": "^0.1.21"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.10.7"