@qpjoy/tunnel-cli 0.1.9 → 0.1.11

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 CHANGED
@@ -23,6 +23,8 @@ server, enroll into the HDO mesh without installing Electron:
23
23
  npm i -g @qpjoy/tunnel-cli
24
24
  HDO_PASSWORD='<password>' qp-tunnel-cli hdo enroll \
25
25
  --server-url 'https://domestic.example.com' \
26
+ --internal-url 'http://127.0.0.1:18090' \
27
+ --product-id h2o \
26
28
  --username 'internal-i' \
27
29
  --device-id internal-i \
28
30
  --label 'Internal I'
@@ -40,6 +42,8 @@ HDO_PASSWORD='<password>' sudo -E qp-tunnel-cli hdo enroll \
40
42
  The command:
41
43
 
42
44
  - logs in with username/password, or uses `--token` when provided
45
+ - requests an MX Launcher Internal product lease when `--internal-url` is set
46
+ - uses that lease as the WireGuard client address and adds the product route CIDR
43
47
  - uses `@qpjoy/electron-core-wireguard` to find/install the platform WireGuard engine
44
48
  - registers the machine as an HDO device
45
49
  - downloads the HDO manifest from `electron-server`
@@ -49,6 +53,22 @@ The command:
49
53
  installed by the OS, the CLI writes a compatible systemd unit that uses the
50
54
  bundled WireGuard tools from the npm package.
51
55
 
56
+ To test the new Internal allocator without applying WireGuard yet, run lease-only
57
+ mode. If `--server-url` is omitted and `--internal-url` is present, the command
58
+ automatically behaves as lease-only:
59
+
60
+ ```bash
61
+ qp-tunnel-cli hdo enroll \
62
+ --internal-url 'http://127.0.0.1:18090' \
63
+ --product-id h2o \
64
+ --identity-kind anonymous \
65
+ --lease-only
66
+ ```
67
+
68
+ For H2O, Internal assigns logged-in users from `10.90.0.1-10.90.99.254` and
69
+ anonymous users from `10.90.100.1-10.90.254.254`, based on the Product Network
70
+ Registry.
71
+
52
72
  Useful follow-up commands:
53
73
 
54
74
  ```bash
@@ -81,16 +101,25 @@ Run commands directly through the bundled script:
81
101
 
82
102
  ```bash
83
103
  qp-tunnel-cli status
84
- qp-tunnel-cli server-on
104
+ qp-tunnel-cli egress-on
85
105
  qp-tunnel-cli update-subscription
86
106
  ```
87
107
 
88
108
  For server commands, `qp-tunnel-cli` re-runs itself with `sudo` when root is needed.
89
- Use `server-on` for public VPS hosts: it keeps Mihomo running as a local outbound
109
+ Use `egress-on` for public VPS hosts: it keeps Mihomo running as a local outbound
90
110
  proxy and configures shell, SSH, Docker/containerd/buildkit proxy drop-ins without
91
111
  enabling TUN route takeover. Reserve `tun-on` for machines that are not serving
92
112
  public inbound traffic.
93
113
 
114
+ Domestic bootstrap can install from an Internal-pushed subscription file before
115
+ the WG relay can reach Internal:
116
+
117
+ ```bash
118
+ sudo qp-tunnel-cli install \
119
+ --file /opt/mx/current/qp-tunnel-cli/domestic-bootstrap-subscription.yaml
120
+ sudo qp-tunnel-cli egress-on
121
+ ```
122
+
94
123
  Run any command through the active Mihomo local proxy:
95
124
 
96
125
  ```bash
@@ -109,7 +138,7 @@ Install the bundled script as a normal Linux command:
109
138
  sudo qp-tunnel-cli install-script
110
139
  sudo qp-tunnel-cli upgrade-systemd
111
140
  sudo mihomo-client status
112
- sudo mihomo-client server-on
141
+ sudo mihomo-client egress-on
113
142
  ```
114
143
 
115
144
  Use a custom target when needed:
package/README.setup.md CHANGED
@@ -1,15 +1,18 @@
1
1
  ```bash
2
- npm i -g @qpjoy/tunnel-cli@0.1.4
3
- HDO_PASSWORD='...' qp-tunnel-cli hdo enroll --server-url 'https://domestic.example.com' --username internal-i
2
+ npm i -g @qpjoy/tunnel-cli@0.1.9
3
+ qp-tunnel-cli hdo enroll --internal-url 'http://127.0.0.1:18090' --product-id h2o --identity-kind anonymous --lease-only
4
+ HDO_PASSWORD='...' qp-tunnel-cli hdo enroll --server-url 'https://domestic.example.com' --internal-url 'http://127.0.0.1:18090' --product-id h2o --username internal-i
4
5
 
5
6
  qp-tunnel-cli install --url 'http://user:pass@host:3434/peer_xxx.mihomo.yaml'
7
+ # Domestic bootstrap can use an Internal-pushed local YAML before WG relay reaches Internal.
8
+ qp-tunnel-cli install --file '/opt/mx/current/qp-tunnel-cli/domestic-bootstrap-subscription.yaml'
6
9
 
7
10
  sudo qp-tunnel-cli install-script
8
11
  sudo qp-tunnel-cli upgrade-systemd
9
- sudo qp-tunnel-cli server-on
12
+ sudo qp-tunnel-cli egress-on
10
13
 
11
14
  sudo qp-tunnel-cli tun-off
12
- sudo qp-tunnel-cli server-on
15
+ sudo qp-tunnel-cli egress-on
13
16
  sudo qp-tunnel-cli status
14
17
 
15
18
  qp-tunnel-cli curl google.com
package/dist/hdo.js CHANGED
@@ -46,6 +46,11 @@ Usage:
46
46
 
47
47
  Enroll options:
48
48
  --server-url URL HDO/electron-server base URL
49
+ --internal-url URL MX Launcher Internal URL for product IP lease allocation
50
+ --product-id ID Product network id. Default: h2o
51
+ --mode MODE Launcher mode: embed or standalone
52
+ --identity-kind KIND user or anonymous. Default: user when --username is set, else anonymous
53
+ --lease-only Only request/store the Internal lease; skip legacy HDO manifest/WireGuard apply
49
54
  --username USER Login username/email/phone
50
55
  --password PASS Login password. Prefer HDO_PASSWORD or --password-file
51
56
  --password-file PATH Read login password from a file
@@ -69,6 +74,8 @@ Enroll options:
69
74
 
70
75
  Environment:
71
76
  HDO_SERVER_URL / QPJOY_HDO_SERVER_URL
77
+ HDO_INTERNAL_URL / QPJOY_HDO_INTERNAL_URL / MX_INTERNAL_URL
78
+ HDO_PRODUCT_ID / QPJOY_HDO_PRODUCT_ID
72
79
  HDO_USERNAME / QPJOY_HDO_USERNAME
73
80
  HDO_PASSWORD / QPJOY_HDO_PASSWORD
74
81
  HDO_TOKEN / QPJOY_HDO_TOKEN
@@ -109,20 +116,87 @@ async function enrollCommand(args, refreshOnly) {
109
116
  process.env.HDO_SERVER_URL ??
110
117
  process.env.QPJOY_HDO_SERVER_URL ??
111
118
  previous.serverUrl);
119
+ const internalUrl = normalizeBaseUrl(options.internalUrl ??
120
+ process.env.HDO_INTERNAL_URL ??
121
+ process.env.QPJOY_HDO_INTERNAL_URL ??
122
+ process.env.MX_INTERNAL_URL ??
123
+ previous.internalUrl);
112
124
  const username = options.username ??
113
125
  process.env.HDO_USERNAME ??
114
126
  process.env.QPJOY_HDO_USERNAME ??
115
127
  previous.username;
116
- if (!serverUrl) {
117
- throw new Error('Missing --server-url or HDO_SERVER_URL.');
128
+ const productId = sanitizeId(options.productId ??
129
+ process.env.HDO_PRODUCT_ID ??
130
+ process.env.QPJOY_HDO_PRODUCT_ID ??
131
+ previous.productId ??
132
+ 'h2o');
133
+ const launcherMode = options.mode ?? previous.launcherMode ?? (productId === 'launcher' ? 'standalone' : 'embed');
134
+ const identityKind = options.identityKind ?? previous.identityKind ?? (username ? 'user' : 'anonymous');
135
+ const leaseOnly = Boolean(options.leaseOnly || (internalUrl && !serverUrl));
136
+ if (!serverUrl && !leaseOnly) {
137
+ throw new Error('Missing --server-url/HDO_SERVER_URL or use --internal-url with --lease-only.');
118
138
  }
119
- const auth = await resolveAuth(serverUrl, options, previous, username);
120
139
  const deviceId = options.deviceId ||
121
140
  previous.deviceId ||
122
141
  `hdo-${process.platform}-${sanitizeId((0, node_os_1.hostname)())}`;
123
142
  const label = options.label || previous.label || `${process.platform} ${(0, node_os_1.hostname)()}`;
124
143
  const keys = resolveKeypair(options, previous, installDir);
125
144
  const direct = resolveDirectEndpoint(options, previous);
145
+ const launcherNetworkLease = internalUrl
146
+ ? await enrollLauncherNetworkLease(internalUrl, {
147
+ productId,
148
+ mode: launcherMode,
149
+ identityKind,
150
+ installId: deviceId,
151
+ deviceId,
152
+ siteId: previous.launcherNetworkLease?.siteId,
153
+ userId: identityKind === 'user' ? username : undefined,
154
+ publicKey: keys.publicKey,
155
+ deviceLabel: label,
156
+ platform: `${process.platform}-${process.arch}`,
157
+ requestedBy: '@qpjoy/tunnel-cli',
158
+ requestId: `qp-tunnel-cli-hdo-enroll-${Date.now()}`,
159
+ })
160
+ : undefined;
161
+ if (leaseOnly) {
162
+ const now = new Date().toISOString();
163
+ writeState(stateFile, {
164
+ ...previous,
165
+ internalUrl,
166
+ productId,
167
+ launcherMode,
168
+ identityKind,
169
+ username,
170
+ deviceId,
171
+ label,
172
+ interfaceName,
173
+ configPath,
174
+ installDir,
175
+ privateKey: keys.privateKey,
176
+ publicKey: keys.publicKey,
177
+ overlayIp: launcherNetworkLease?.leaseIp ?? previous.overlayIp,
178
+ launcherNetworkLease: launcherNetworkLease ?? previous.launcherNetworkLease,
179
+ enrolledAt: previous.enrolledAt || now,
180
+ updatedAt: now,
181
+ });
182
+ process.stdout.write([
183
+ refreshOnly ? 'HDO Internal lease refreshed.' : 'HDO Internal lease enrolled.',
184
+ `Product: ${productId}`,
185
+ `Mode: ${launcherMode}`,
186
+ `Identity: ${identityKind}`,
187
+ `Device: ${deviceId}`,
188
+ `Lease IP: ${launcherNetworkLease?.leaseIp ?? previous.overlayIp ?? 'unassigned'}`,
189
+ `Lease CIDR: ${launcherNetworkLease?.cidr ?? 'unassigned'}`,
190
+ `State file: ${stateFile}`,
191
+ 'System tunnel not started (lease-only).',
192
+ '',
193
+ ].join('\n'));
194
+ return;
195
+ }
196
+ if (!serverUrl) {
197
+ throw new Error('Missing --server-url or HDO_SERVER_URL.');
198
+ }
199
+ const auth = await resolveAuth(serverUrl, options, previous, username);
126
200
  const registered = await apiJson(serverUrl, auth.accessToken, '/api/v1/hdo/devices/register', {
127
201
  method: 'POST',
128
202
  body: {
@@ -154,6 +228,8 @@ async function enrollCommand(args, refreshOnly) {
154
228
  });
155
229
  const runtime = hdoRuntimeFromManifest(manifest, registered, keys.privateKey, {
156
230
  allowEndpointlessDirectPeers: direct.directListener,
231
+ launcherNetworkLease,
232
+ ownPublicKey: keys.publicKey,
157
233
  });
158
234
  writeWireGuardConfig(configPath, (0, electron_core_wireguard_1.renderHdoClientWireGuardConfig)({
159
235
  privateKey: runtime.privateKey,
@@ -168,11 +244,15 @@ async function enrollCommand(args, refreshOnly) {
168
244
  const now = new Date().toISOString();
169
245
  writeState(stateFile, {
170
246
  serverUrl,
247
+ internalUrl,
171
248
  bearerToken: auth.accessToken,
172
249
  refreshToken: auth.refreshToken,
173
250
  accessExpiresAt: auth.accessExpiresAt,
174
251
  refreshExpiresAt: auth.refreshExpiresAt,
175
252
  username: auth.username ?? username,
253
+ productId,
254
+ launcherMode,
255
+ identityKind,
176
256
  deviceId,
177
257
  label,
178
258
  interfaceName,
@@ -181,6 +261,7 @@ async function enrollCommand(args, refreshOnly) {
181
261
  privateKey: keys.privateKey,
182
262
  publicKey: keys.publicKey,
183
263
  overlayIp: runtime.overlayIp,
264
+ launcherNetworkLease: launcherNetworkLease ?? previous.launcherNetworkLease,
184
265
  directListener: direct.directListener,
185
266
  preferDirectPeers: direct.preferDirectPeers,
186
267
  endpointHost: direct.endpointHost,
@@ -211,12 +292,17 @@ function statusCommand(input) {
211
292
  const installDir = resolveInstallDir(input.installDir || state.installDir);
212
293
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
213
294
  installDir,
214
- allowSystemFallback: true,
295
+ allowSystemFallback: false,
215
296
  });
216
297
  process.stdout.write(`State file: ${stateFile}\n`);
217
298
  process.stdout.write(`Server URL: ${state.serverUrl || 'unset'}\n`);
299
+ process.stdout.write(`Internal URL: ${state.internalUrl || 'unset'}\n`);
300
+ process.stdout.write(`Product: ${state.productId || 'unset'} (${state.launcherMode || 'unset'} / ${state.identityKind || 'unset'})\n`);
218
301
  process.stdout.write(`Device: ${state.deviceId || 'unset'}\n`);
219
302
  process.stdout.write(`Overlay IP: ${state.overlayIp || 'unset'}\n`);
303
+ if (state.launcherNetworkLease) {
304
+ process.stdout.write(`Internal lease: ${state.launcherNetworkLease.leaseIp} ${state.launcherNetworkLease.cidr} (${state.launcherNetworkLease.leaseId})\n`);
305
+ }
220
306
  process.stdout.write(`WireGuard config: ${configPath}\n\n`);
221
307
  if (process.platform === 'linux') {
222
308
  if (commandAvailable('systemctl')) {
@@ -259,7 +345,7 @@ async function downCommand(input) {
259
345
  if (process.platform === 'linux') {
260
346
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
261
347
  installDir,
262
- allowSystemFallback: true,
348
+ allowSystemFallback: false,
263
349
  });
264
350
  if (commandAvailable('systemctl') && systemdUnitExists('wg-quick@.service')) {
265
351
  inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
@@ -273,7 +359,7 @@ async function downCommand(input) {
273
359
  }
274
360
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
275
361
  installDir,
276
- allowSystemFallback: true,
362
+ allowSystemFallback: false,
277
363
  });
278
364
  if (process.platform === 'darwin') {
279
365
  if (!canReadFile(configPath)) {
@@ -296,6 +382,7 @@ function parseEnrollOptions(args) {
296
382
  interfaceName: defaultInterfaceName,
297
383
  role: 'internal',
298
384
  start: true,
385
+ leaseOnly: false,
299
386
  rotateKey: false,
300
387
  directListener: false,
301
388
  preferDirectPeers: false,
@@ -313,6 +400,39 @@ function parseEnrollOptions(args) {
313
400
  case '--server-url':
314
401
  options.serverUrl = readValue();
315
402
  break;
403
+ case '--internal-url':
404
+ case '--mx-internal-url':
405
+ options.internalUrl = readValue();
406
+ break;
407
+ case '--product-id':
408
+ case '--app-id':
409
+ options.productId = readValue();
410
+ break;
411
+ case '--mode': {
412
+ const value = readValue();
413
+ if (value !== 'standalone' && value !== 'embed')
414
+ throw new Error(`Invalid --mode: ${value}`);
415
+ options.mode = value;
416
+ break;
417
+ }
418
+ case '--identity-kind': {
419
+ const value = readValue();
420
+ if (value !== 'user' && value !== 'anonymous')
421
+ throw new Error(`Invalid --identity-kind: ${value}`);
422
+ options.identityKind = value;
423
+ break;
424
+ }
425
+ case '--anonymous':
426
+ options.identityKind = 'anonymous';
427
+ break;
428
+ case '--user-identity':
429
+ case '--employee':
430
+ options.identityKind = 'user';
431
+ break;
432
+ case '--lease-only':
433
+ options.leaseOnly = true;
434
+ options.start = false;
435
+ break;
316
436
  case '--token':
317
437
  options.token = readValue();
318
438
  break;
@@ -498,13 +618,75 @@ async function refreshAuth(serverUrl, refreshToken, username) {
498
618
  username,
499
619
  };
500
620
  }
621
+ async function enrollLauncherNetworkLease(internalUrl, input) {
622
+ const raw = await apiJson(internalUrl, '', '/internal/v1/launcher-network/enrollments', {
623
+ method: 'POST',
624
+ auth: false,
625
+ body: input,
626
+ });
627
+ const root = requireRecord(raw, 'launcher network enroll response');
628
+ const lease = requireRecord(root.lease, 'launcher network lease');
629
+ return parseLauncherNetworkLease(lease);
630
+ }
631
+ function parseLauncherNetworkLease(lease) {
632
+ const leaseId = stringField(lease.leaseId);
633
+ const productId = stringField(lease.productId);
634
+ const launcherMode = stringField(lease.launcherMode);
635
+ const identityKind = stringField(lease.identityKind);
636
+ const installId = stringField(lease.installId);
637
+ const deviceId = stringField(lease.deviceId);
638
+ const siteId = stringField(lease.siteId);
639
+ const cidr = stringField(lease.cidr);
640
+ const leaseIp = stringField(lease.leaseIp);
641
+ if (!leaseId || !productId || !installId || !deviceId || !siteId || !cidr || !leaseIp) {
642
+ throw new Error('Launcher Network lease response is missing required fields.');
643
+ }
644
+ if (launcherMode !== 'standalone' && launcherMode !== 'embed') {
645
+ throw new Error(`Launcher Network lease response has invalid launcherMode: ${launcherMode ?? 'missing'}`);
646
+ }
647
+ if (identityKind !== 'user' && identityKind !== 'anonymous') {
648
+ throw new Error(`Launcher Network lease response has invalid identityKind: ${identityKind ?? 'missing'}`);
649
+ }
650
+ return {
651
+ leaseId,
652
+ leaseKey: stringField(lease.leaseKey) ?? undefined,
653
+ productId,
654
+ launcherMode,
655
+ identityKind,
656
+ sequence: numberField(lease.sequence),
657
+ installId,
658
+ deviceId,
659
+ siteId,
660
+ userId: stringField(lease.userId),
661
+ cidr,
662
+ leaseIp,
663
+ serviceVip: stringField(lease.serviceVip) ?? undefined,
664
+ internalControlIp: stringField(lease.internalControlIp) ?? undefined,
665
+ domesticGatewayIp: stringField(lease.domesticGatewayIp) ?? undefined,
666
+ domesticSiteId: stringField(lease.domesticSiteId) ?? undefined,
667
+ overseaSiteId: stringField(lease.overseaSiteId) ?? undefined,
668
+ publicKey: stringField(lease.publicKey),
669
+ status: stringField(lease.status) ?? undefined,
670
+ updatedAt: stringField(lease.updatedAt) ?? undefined,
671
+ };
672
+ }
673
+ function allowedIpsFromLauncherNetworkLease(lease) {
674
+ if (!lease)
675
+ return [];
676
+ return uniqueStrings([
677
+ lease.cidr,
678
+ lease.serviceVip ? `${lease.serviceVip}/32` : '',
679
+ lease.internalControlIp ? `${lease.internalControlIp}/32` : '',
680
+ lease.domesticGatewayIp ? `${lease.domesticGatewayIp}/32` : '',
681
+ ]).filter((value) => value.includes('/'));
682
+ }
501
683
  function resolveKeypair(options, previous, installDir) {
502
684
  if (!options.rotateKey && previous.privateKey && previous.publicKey) {
503
685
  return { privateKey: previous.privateKey, publicKey: previous.publicKey };
504
686
  }
505
687
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardRuntime)({
506
688
  installDir,
507
- allowSystemFallback: true,
689
+ allowSystemFallback: false,
508
690
  });
509
691
  if (!runtime.command) {
510
692
  throw new Error(runtime.error ?? 'WireGuard wg command unavailable.');
@@ -550,6 +732,8 @@ function directPeersFromManifest(wireGuard, ownOverlayIp, options = {}) {
550
732
  const overlayIp = stringField(row.overlayIp);
551
733
  if (!publicKey || !overlayIp || overlayIp === ownIp)
552
734
  return [];
735
+ if (options.ownPublicKey && publicKey === options.ownPublicKey)
736
+ return [];
553
737
  const allowedIps = stringArray(row.allowedIps);
554
738
  const endpoint = stringField(row.endpoint);
555
739
  if (!endpoint && options.allowEndpointlessDirectPeers !== true)
@@ -581,7 +765,7 @@ async function startSystemTunnel(interfaceName, configPath, installDir) {
581
765
  }
582
766
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
583
767
  installDir,
584
- allowSystemFallback: true,
768
+ allowSystemFallback: false,
585
769
  });
586
770
  if (process.platform === 'darwin') {
587
771
  const result = await (0, electron_core_wireguard_1.installDarwinWireGuardLaunchDaemon)({ runtime, configPath });
@@ -625,8 +809,9 @@ function hdoRuntimeFromManifest(manifest, registered, privateKey, options = {})
625
809
  const wireGuard = requireRecord(root.wireGuard, 'manifest.wireGuard');
626
810
  const client = requireRecord(wireGuard.client, 'manifest.wireGuard.client');
627
811
  const domestic = requireRecord(wireGuard.domestic, 'manifest.wireGuard.domestic');
628
- const overlayIp = stringField(client.overlayIp) ||
812
+ const manifestOverlayIp = stringField(client.overlayIp) ||
629
813
  stringField(requireRecord(registered, 'registered device').overlayIp);
814
+ const overlayIp = options.launcherNetworkLease?.leaseIp || manifestOverlayIp;
630
815
  const domesticPublicKey = stringField(domestic.publicKey);
631
816
  const domesticEndpoint = stringField(domestic.endpoint);
632
817
  if (!overlayIp)
@@ -638,6 +823,7 @@ function hdoRuntimeFromManifest(manifest, registered, privateKey, options = {})
638
823
  const domesticOverlay = stringField(domestic.overlayIp);
639
824
  const allowedIps = uniqueStrings([
640
825
  ...routeCidrs,
826
+ ...allowedIpsFromLauncherNetworkLease(options.launcherNetworkLease),
641
827
  ...(domesticOverlay ? [`${domesticOverlay}/32`] : []),
642
828
  ]).filter((value) => value.includes('/'));
643
829
  if (allowedIps.length === 0) {
@@ -743,14 +929,14 @@ function printWireGuardRuntimeStatus(runtime, configPath) {
743
929
  async function ensureLinuxWireGuardRuntime(installDir) {
744
930
  let runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
745
931
  installDir,
746
- allowSystemFallback: true,
932
+ allowSystemFallback: false,
747
933
  });
748
934
  if (runtime.available)
749
935
  return runtime;
750
936
  const installed = installLinuxWireGuardTools();
751
937
  runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
752
938
  installDir,
753
- allowSystemFallback: true,
939
+ allowSystemFallback: false,
754
940
  });
755
941
  if (runtime.available)
756
942
  return runtime;
package/dist/index.js CHANGED
@@ -91,9 +91,10 @@ Usage:
91
91
 
92
92
  Common commands:
93
93
  qp-tunnel-cli install --url http://IP:3434/peer_user01.mihomo.yaml --user download --password pass
94
+ qp-tunnel-cli install --file /opt/mx/current/qp-tunnel-cli/domestic-bootstrap-subscription.yaml
94
95
  qp-tunnel-cli status
95
96
  qp-tunnel-cli start
96
- qp-tunnel-cli server-on
97
+ qp-tunnel-cli egress-on
97
98
  qp-tunnel-cli tun-on
98
99
  qp-tunnel-cli tun-off
99
100
  qp-tunnel-cli update-subscription
@@ -113,6 +114,7 @@ QP_TUNNEL_CONTAINER_HTTP_PROXY=http://host.docker.internal:<mixed-port>.
113
114
  Install the script as a normal server command:
114
115
  sudo qp-tunnel-cli install-script
115
116
  sudo mihomo-client status
117
+ sudo mihomo-client egress-on
116
118
 
117
119
  Enroll this machine into an HDO mesh:
118
120
  HDO_PASSWORD=... qp-tunnel-cli hdo enroll --server-url https://domestic.example.com --username user
@@ -185,7 +187,7 @@ function mixedPortFromConfig() {
185
187
  }
186
188
  const configFile = process.env.MIHOMO_CONFIG_FILE || defaultMihomoConfigFile;
187
189
  if (!(0, node_fs_1.existsSync)(configFile)) {
188
- process.stderr.write(`Mihomo config not found: ${configFile}\nRun: sudo qp-tunnel-cli install ... && sudo qp-tunnel-cli server-on\n`);
190
+ process.stderr.write(`Mihomo config not found: ${configFile}\nRun: sudo qp-tunnel-cli install ... && sudo qp-tunnel-cli egress-on\n`);
189
191
  process.exit(1);
190
192
  }
191
193
  const content = (0, node_fs_1.readFileSync)(configFile, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qpjoy/tunnel-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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.28"
25
+ "@qpjoy/electron-core-wireguard": "^0.1.29"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.10.7"
@@ -45,8 +45,8 @@ Commands:
45
45
  upgrade-systemd Refresh the installed systemd unit from this script
46
46
  server-on Enable persistent server-safe outbound proxy mode, without TUN
47
47
  server-off Disable server-safe proxy integrations, keeping the service installed
48
- egress-on Alias for server-on
49
- egress-off Alias for server-off
48
+ egress-on Preferred server-safe outbound proxy mode for Domestic bootstrap
49
+ egress-off Disable server-safe outbound proxy integrations
50
50
  proxy-on Write /etc/profile.d proxy exports for login shells
51
51
  proxy-off Remove /etc/profile.d proxy exports
52
52
  tun-on Enable persistent Mihomo TUN mode with cn-direct and local/private route bypasses
@@ -65,6 +65,7 @@ Commands:
65
65
 
66
66
  Install options:
67
67
  --url URL Subscription URL, e.g. http://IP:3434/peer_user01.mihomo.yaml
68
+ --file FILE Local subscription YAML file, for Internal-pushed bootstrap
68
69
  --user USER Basic Auth username for subscription
69
70
  --password PASS Basic Auth password for subscription
70
71
  --version TAG Mihomo version tag. Default: latest stable release
@@ -73,6 +74,7 @@ Install options:
73
74
 
74
75
  Update options:
75
76
  --url URL Override saved subscription URL
77
+ --file FILE Replace subscription from a local YAML file
76
78
  --user USER Override saved subscription username
77
79
  --password PASS Override saved subscription password
78
80
 
@@ -89,9 +91,13 @@ Examples:
89
91
  --url http://IP:3434/peer_user01.mihomo.yaml \
90
92
  --binary-path /tmp/mihomo
91
93
 
94
+ sudo bash ./scripts/mihomo-client.sh install \
95
+ --file /opt/mx/current/qp-tunnel-cli/domestic-bootstrap-subscription.yaml
96
+
92
97
  sudo bash ./scripts/mihomo-client.sh update-subscription
98
+ sudo bash ./scripts/mihomo-client.sh update-subscription --file /opt/mx/current/qp-tunnel-cli/domestic-bootstrap-subscription.yaml
93
99
  sudo bash ./scripts/mihomo-client.sh start
94
- sudo bash ./scripts/mihomo-client.sh server-on
100
+ sudo bash ./scripts/mihomo-client.sh egress-on
95
101
  sudo bash ./scripts/mihomo-client.sh proxy-on
96
102
  sudo bash ./scripts/mihomo-client.sh tun-on
97
103
  sudo bash ./scripts/mihomo-client.sh ssh-proxy-on
@@ -575,6 +581,20 @@ fetch_subscription() {
575
581
  render_runtime_config
576
582
  }
577
583
 
584
+ install_subscription_file() {
585
+ local file_path="$1"
586
+
587
+ [[ -n "$file_path" ]] || die "Subscription file is required."
588
+ [[ -f "$file_path" ]] || die "Subscription file not found: $file_path"
589
+ [[ -s "$file_path" ]] || die "Subscription file is empty: $file_path"
590
+
591
+ ensure_dirs
592
+ log "Installing local subscription file: $file_path"
593
+ cp "$file_path" "$MIHOMO_SUBSCRIPTION_FILE"
594
+ chmod 600 "$MIHOMO_SUBSCRIPTION_FILE"
595
+ render_runtime_config
596
+ }
597
+
578
598
  mixed_port_from_config() {
579
599
  if [[ -f "$MIHOMO_CONFIG_FILE" ]]; then
580
600
  awk -F: '/^[[:space:]]*mixed-port[[:space:]]*:/ {gsub(/[[:space:]]/, "", $2); print $2; exit}' "$MIHOMO_CONFIG_FILE"
@@ -682,10 +702,29 @@ update_subscription_command() {
682
702
  local url="${1:-}"
683
703
  local username="${2:-}"
684
704
  local password="${3:-}"
705
+ local file_path="${4:-}"
685
706
  local -a normalized=()
686
707
 
687
708
  load_env
688
709
 
710
+ if [[ -n "$file_path" ]]; then
711
+ install_subscription_file "$file_path"
712
+ set_env_value MIHOMO_SUBSCRIPTION_SOURCE "local-file"
713
+ set_env_value MIHOMO_SUBSCRIPTION_LOCAL_FILE "$file_path"
714
+ set_env_value MIHOMO_SUBSCRIPTION_URL ""
715
+ set_env_value MIHOMO_SUBSCRIPTION_USER ""
716
+ set_env_value MIHOMO_SUBSCRIPTION_PASSWORD ""
717
+
718
+ if service_is_active; then
719
+ log "Restarting Mihomo service after local subscription update"
720
+ systemctl restart "$MIHOMO_SERVICE_NAME"
721
+ echo "Subscription updated from local file and service restarted."
722
+ else
723
+ echo "Subscription updated from local file."
724
+ fi
725
+ return
726
+ fi
727
+
689
728
  url="${url:-${MIHOMO_SUBSCRIPTION_URL:-}}"
690
729
  username="${username:-${MIHOMO_SUBSCRIPTION_USER:-}}"
691
730
  password="${password:-${MIHOMO_SUBSCRIPTION_PASSWORD:-}}"
@@ -697,6 +736,8 @@ update_subscription_command() {
697
736
  [[ -n "$url" ]] || die "No subscription URL configured. Use install or pass --url."
698
737
 
699
738
  fetch_subscription "$url" "$username" "$password"
739
+ set_env_value MIHOMO_SUBSCRIPTION_SOURCE "url"
740
+ set_env_value MIHOMO_SUBSCRIPTION_LOCAL_FILE ""
700
741
  set_env_value MIHOMO_SUBSCRIPTION_URL "$url"
701
742
  set_env_value MIHOMO_SUBSCRIPTION_USER "$username"
702
743
  set_env_value MIHOMO_SUBSCRIPTION_PASSWORD "$password"
@@ -941,24 +982,27 @@ install_command() {
941
982
  local version="${4:-latest}"
942
983
  local autostart="${5:-true}"
943
984
  local binary_path="${6:-}"
985
+ local file_path="${7:-}"
944
986
  local -a normalized=()
945
987
 
946
988
  ensure_dirs
947
989
  load_env
948
990
  log "Starting Mihomo client install/setup"
949
991
 
950
- if [[ -z "$url" ]]; then
992
+ if [[ -z "$file_path" && -z "$url" ]]; then
951
993
  url="$(prompt_default "Subscription URL" "${MIHOMO_SUBSCRIPTION_URL:-}")"
952
994
  fi
953
- mapfile -t normalized < <(normalize_subscription_inputs "$url" "$username" "$password")
954
- url="${normalized[0]}"
955
- username="${normalized[1]}"
956
- password="${normalized[2]}"
957
- if [[ -z "$username" ]]; then
958
- username="$(prompt_default "Subscription username (empty if none)" "${MIHOMO_SUBSCRIPTION_USER:-}")"
959
- fi
960
- if [[ -z "$password" ]]; then
961
- password="$(prompt_password "Subscription password (empty if none)")"
995
+ if [[ -z "$file_path" ]]; then
996
+ mapfile -t normalized < <(normalize_subscription_inputs "$url" "$username" "$password")
997
+ url="${normalized[0]}"
998
+ username="${normalized[1]}"
999
+ password="${normalized[2]}"
1000
+ if [[ -z "$username" ]]; then
1001
+ username="$(prompt_default "Subscription username (empty if none)" "${MIHOMO_SUBSCRIPTION_USER:-}")"
1002
+ fi
1003
+ if [[ -z "$password" ]]; then
1004
+ password="$(prompt_password "Subscription password (empty if none)")"
1005
+ fi
962
1006
  fi
963
1007
 
964
1008
  if [[ -n "$binary_path" ]]; then
@@ -978,8 +1022,8 @@ install_command() {
978
1022
  set_env_value MIHOMO_SUBSCRIPTION_USER "$username"
979
1023
  set_env_value MIHOMO_SUBSCRIPTION_PASSWORD "$password"
980
1024
 
981
- log "Downloading initial subscription and rendering runtime config"
982
- update_subscription_command "$url" "$username" "$password"
1025
+ log "Installing initial subscription and rendering runtime config"
1026
+ update_subscription_command "$url" "$username" "$password" "$file_path"
983
1027
 
984
1028
  systemctl enable "$MIHOMO_SERVICE_NAME" >/dev/null 2>&1 || true
985
1029
  if [[ "$autostart" == "true" ]]; then
@@ -1080,9 +1124,11 @@ main() {
1080
1124
  local version="latest"
1081
1125
  local autostart="true"
1082
1126
  local binary_path=""
1127
+ local file_path=""
1083
1128
  while [[ $# -gt 0 ]]; do
1084
1129
  case "$1" in
1085
1130
  --url) url="$2"; shift 2 ;;
1131
+ --file) file_path="$2"; shift 2 ;;
1086
1132
  --user) username="$2"; shift 2 ;;
1087
1133
  --password) password="$2"; shift 2 ;;
1088
1134
  --version) version="$2"; shift 2 ;;
@@ -1091,21 +1137,23 @@ main() {
1091
1137
  *) die "Unknown install option: $1" ;;
1092
1138
  esac
1093
1139
  done
1094
- install_command "$url" "$username" "$password" "$version" "$autostart" "$binary_path"
1140
+ install_command "$url" "$username" "$password" "$version" "$autostart" "$binary_path" "$file_path"
1095
1141
  ;;
1096
1142
  update-subscription)
1097
1143
  local url=""
1098
1144
  local username=""
1099
1145
  local password=""
1146
+ local file_path=""
1100
1147
  while [[ $# -gt 0 ]]; do
1101
1148
  case "$1" in
1102
1149
  --url) url="$2"; shift 2 ;;
1150
+ --file) file_path="$2"; shift 2 ;;
1103
1151
  --user) username="$2"; shift 2 ;;
1104
1152
  --password) password="$2"; shift 2 ;;
1105
1153
  *) die "Unknown update-subscription option: $1" ;;
1106
1154
  esac
1107
1155
  done
1108
- update_subscription_command "$url" "$username" "$password"
1156
+ update_subscription_command "$url" "$username" "$password" "$file_path"
1109
1157
  ;;
1110
1158
  start)
1111
1159
  start_command