@qpjoy/tunnel-cli 0.1.1 → 0.1.4

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
@@ -1,6 +1,7 @@
1
1
  # @qpjoy/tunnel-cli
2
2
 
3
- Global CLI wrapper for the QPJoy Linux `mihomo-client` server script.
3
+ Global CLI wrapper for the QPJoy Linux `mihomo-client` server script and
4
+ cross-platform headless HDO mesh enrollment.
4
5
 
5
6
  ```bash
6
7
  npm i -g @qpjoy/tunnel-cli
@@ -8,8 +9,62 @@ qp-tunnel-cli help
8
9
  ```
9
10
 
10
11
  The package ships the existing `scripts/mihomo-client.sh` file in the npm
11
- tarball. It does not reimplement the Linux systemd, proxy, SSH, daemon, or TUN
12
- orchestration in Node.
12
+ tarball. It also depends on `@qpjoy/electron-core-wireguard`, which resolves the
13
+ matching platform engine package such as darwin-arm64, linux-x64, or win32-x64.
14
+ The core WireGuard package is consumed as-is; this CLI does not modify its HDO
15
+ plugin behavior.
16
+
17
+ ## HDO Mesh Enrollment
18
+
19
+ For macOS, Windows, or a headless Linux machine such as an Internal/company
20
+ server, enroll into the HDO mesh without installing Electron:
21
+
22
+ ```bash
23
+ npm i -g @qpjoy/tunnel-cli
24
+ HDO_PASSWORD='<password>' qp-tunnel-cli hdo enroll \
25
+ --server-url 'https://domestic.example.com' \
26
+ --username 'internal-i' \
27
+ --device-id internal-i \
28
+ --label 'Internal I'
29
+ ```
30
+
31
+ On Linux, run the same command through `sudo -E` because it writes
32
+ `/etc/wireguard` and enables `wg-quick@hdo-internal`:
33
+
34
+ ```bash
35
+ HDO_PASSWORD='<password>' sudo -E qp-tunnel-cli hdo enroll \
36
+ --server-url 'https://domestic.example.com' \
37
+ --username 'internal-i'
38
+ ```
39
+
40
+ The command:
41
+
42
+ - logs in with username/password, or uses `--token` when provided
43
+ - uses `@qpjoy/electron-core-wireguard` to find/install the platform WireGuard engine
44
+ - registers the machine as an HDO device
45
+ - downloads the HDO manifest from `electron-server`
46
+ - writes a local WireGuard config
47
+ - stores local HDO state and refresh credentials
48
+ - starts a system-level tunnel at boot
49
+
50
+ Useful follow-up commands:
51
+
52
+ ```bash
53
+ qp-tunnel-cli hdo status
54
+ qp-tunnel-cli hdo refresh
55
+ qp-tunnel-cli hdo down
56
+ ```
57
+
58
+ Platform behavior:
59
+
60
+ - Linux: writes `/etc/wireguard/hdo-internal.conf` and enables `wg-quick@hdo-internal`
61
+ - macOS: installs a LaunchDaemon and may prompt for an administrator password
62
+ - Windows: installs a WireGuard tunnel service and may show a UAC prompt
63
+
64
+ Current MVP authentication accepts either username/password or the same bearer
65
+ token used by the HDO API. Production enrollment should move to short-lived
66
+ enrollment tokens and durable service tokens so external systems do not need to
67
+ handle user session JWTs.
13
68
 
14
69
  ## Server Usage
15
70
 
@@ -27,6 +82,18 @@ proxy and configures shell, SSH, Docker/containerd/buildkit proxy drop-ins witho
27
82
  enabling TUN route takeover. Reserve `tun-on` for machines that are not serving
28
83
  public inbound traffic.
29
84
 
85
+ Run any command through the active Mihomo local proxy:
86
+
87
+ ```bash
88
+ qp-tunnel-cli ./electron-server/scripts/manage.sh redeploy
89
+ qp-tunnel-cli -- docker compose build
90
+ ```
91
+
92
+ For host commands, `HTTP_PROXY` points at `127.0.0.1:<mixed-port>`. For
93
+ Docker/Compose build containers, the CLI also injects container-facing variables
94
+ such as `MARKET_CONTAINER_HTTP_PROXY` and `QP_TUNNEL_CONTAINER_HTTP_PROXY`
95
+ pointing at `host.docker.internal:<mixed-port>`.
96
+
30
97
  Install the bundled script as a normal Linux command:
31
98
 
32
99
  ```bash
@@ -0,0 +1,16 @@
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
4
+
5
+ qp-tunnel-cli install --url 'http://user:pass@host:3434/peer_xxx.mihomo.yaml'
6
+
7
+ sudo qp-tunnel-cli install-script
8
+ sudo qp-tunnel-cli upgrade-systemd
9
+ sudo qp-tunnel-cli server-on
10
+
11
+ sudo qp-tunnel-cli tun-off
12
+ sudo qp-tunnel-cli server-on
13
+ sudo qp-tunnel-cli status
14
+
15
+ qp-tunnel-cli curl google.com
16
+ ```
package/dist/hdo.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ interface HdoCliContext {
2
+ isRoot(): boolean;
3
+ sudoSelf(args: string[]): never;
4
+ }
5
+ export declare function runHdoCli(args: string[], ctx: HdoCliContext): Promise<void>;
6
+ export {};
package/dist/hdo.js ADDED
@@ -0,0 +1,623 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runHdoCli = runHdoCli;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ const electron_core_wireguard_1 = require("@qpjoy/electron-core-wireguard");
9
+ const defaultInterfaceName = 'hdo-internal';
10
+ async function runHdoCli(args, ctx) {
11
+ const command = args[0] ?? 'help';
12
+ const rest = args.slice(1);
13
+ if (command === 'help' || command === '--help' || command === '-h') {
14
+ hdoHelp();
15
+ return;
16
+ }
17
+ if (process.platform === 'linux' && !ctx.isRoot()) {
18
+ ctx.sudoSelf(['hdo', ...args]);
19
+ }
20
+ switch (command) {
21
+ case 'enroll':
22
+ case 'refresh':
23
+ await enrollCommand(rest, command === 'refresh');
24
+ return;
25
+ case 'status':
26
+ statusCommand(parseStateFileArg(rest));
27
+ return;
28
+ case 'down':
29
+ await downCommand(parseStateFileArg(rest));
30
+ return;
31
+ default:
32
+ process.stderr.write(`Unknown hdo command: ${command}\n\n`);
33
+ hdoHelp();
34
+ process.exitCode = 1;
35
+ }
36
+ }
37
+ function hdoHelp() {
38
+ process.stdout.write(`QPJoy HDO CLI
39
+
40
+ Usage:
41
+ qp-tunnel-cli hdo enroll --server-url URL --username USER [options]
42
+ qp-tunnel-cli hdo enroll --server-url URL --token TOKEN [options]
43
+ qp-tunnel-cli hdo refresh [--server-url URL] [--username USER]
44
+ qp-tunnel-cli hdo status
45
+ qp-tunnel-cli hdo down
46
+
47
+ Enroll options:
48
+ --server-url URL HDO/electron-server base URL
49
+ --username USER Login username/email/phone
50
+ --password PASS Login password. Prefer HDO_PASSWORD or --password-file
51
+ --password-file PATH Read login password from a file
52
+ --token TOKEN Existing bearer token for the HDO API
53
+ --token-file PATH Read bearer token from a file
54
+ --device-id ID Stable device id. Default: hdo-<platform>-<hostname>
55
+ --label LABEL Device label. Default: <platform> <hostname>
56
+ --interface NAME WireGuard interface/config name. Default: hdo-internal
57
+ --config-path PATH WireGuard config path
58
+ --install-dir PATH WireGuard engine install/cache directory
59
+ --state-file PATH HDO client state file
60
+ --role ROLE Metadata role. Default: internal
61
+ --rotate-key Generate a new WireGuard keypair
62
+ --no-start Write config without starting the system tunnel
63
+
64
+ Environment:
65
+ HDO_SERVER_URL / QPJOY_HDO_SERVER_URL
66
+ HDO_USERNAME / QPJOY_HDO_USERNAME
67
+ HDO_PASSWORD / QPJOY_HDO_PASSWORD
68
+ HDO_TOKEN / QPJOY_HDO_TOKEN
69
+
70
+ Examples:
71
+ qp-tunnel-cli hdo enroll \\
72
+ --server-url https://domestic.example.com \\
73
+ --username user@example.com
74
+
75
+ HDO_PASSWORD='...' qp-tunnel-cli hdo enroll \\
76
+ --server-url https://domestic.example.com \\
77
+ --username internal-i
78
+
79
+ Notes:
80
+ Linux writes /etc/wireguard and enables wg-quick@<interface>.
81
+ macOS installs a LaunchDaemon and may prompt for an administrator password.
82
+ Windows installs a WireGuard tunnel service and may show a UAC prompt.
83
+ `);
84
+ }
85
+ async function enrollCommand(args, refreshOnly) {
86
+ const options = parseEnrollOptions(args);
87
+ const stateFile = resolveStateFile(options.stateFile);
88
+ const previous = readState(stateFile);
89
+ const interfaceName = sanitizeInterfaceName(options.interfaceName || previous.interfaceName || defaultInterfaceName);
90
+ const configPath = resolveConfigPath(options.configPath || previous.configPath, interfaceName);
91
+ const installDir = resolveInstallDir(options.installDir || previous.installDir);
92
+ const serverUrl = normalizeBaseUrl(options.serverUrl ??
93
+ process.env.HDO_SERVER_URL ??
94
+ process.env.QPJOY_HDO_SERVER_URL ??
95
+ previous.serverUrl);
96
+ const username = options.username ??
97
+ process.env.HDO_USERNAME ??
98
+ process.env.QPJOY_HDO_USERNAME ??
99
+ previous.username;
100
+ if (!serverUrl) {
101
+ throw new Error('Missing --server-url or HDO_SERVER_URL.');
102
+ }
103
+ const auth = await resolveAuth(serverUrl, options, previous, username);
104
+ const deviceId = options.deviceId ||
105
+ previous.deviceId ||
106
+ `hdo-${process.platform}-${sanitizeId((0, node_os_1.hostname)())}`;
107
+ const label = options.label || previous.label || `${process.platform} ${(0, node_os_1.hostname)()}`;
108
+ const keys = resolveKeypair(options, previous, installDir);
109
+ const registered = await apiJson(serverUrl, auth.accessToken, '/api/v1/hdo/devices/register', {
110
+ method: 'POST',
111
+ body: {
112
+ id: deviceId,
113
+ label,
114
+ platform: `${process.platform}-${process.arch}`,
115
+ publicKey: keys.publicKey,
116
+ status: 'online',
117
+ metadata: {
118
+ source: '@qpjoy/tunnel-cli',
119
+ role: options.role,
120
+ hostname: (0, node_os_1.hostname)(),
121
+ wireGuard: {
122
+ publicKey: keys.publicKey,
123
+ interfaceName,
124
+ updatedAt: new Date().toISOString(),
125
+ },
126
+ },
127
+ },
128
+ });
129
+ const manifest = await apiJson(serverUrl, auth.accessToken, `/api/v1/hdo/manifest/${encodeURIComponent(deviceId)}`, {
130
+ method: 'GET',
131
+ });
132
+ const runtime = hdoRuntimeFromManifest(manifest, registered, keys.privateKey);
133
+ writeWireGuardConfig(configPath, (0, electron_core_wireguard_1.renderHdoClientWireGuardConfig)({
134
+ privateKey: runtime.privateKey,
135
+ address: runtime.address,
136
+ domesticPublicKey: runtime.domesticPublicKey,
137
+ domesticEndpoint: runtime.domesticEndpoint,
138
+ allowedIps: runtime.allowedIps,
139
+ persistentKeepalive: 25,
140
+ }));
141
+ const now = new Date().toISOString();
142
+ writeState(stateFile, {
143
+ serverUrl,
144
+ bearerToken: auth.accessToken,
145
+ refreshToken: auth.refreshToken,
146
+ accessExpiresAt: auth.accessExpiresAt,
147
+ refreshExpiresAt: auth.refreshExpiresAt,
148
+ username: auth.username ?? username,
149
+ deviceId,
150
+ label,
151
+ interfaceName,
152
+ configPath,
153
+ installDir,
154
+ privateKey: keys.privateKey,
155
+ publicKey: keys.publicKey,
156
+ overlayIp: runtime.overlayIp,
157
+ lastManifestGeneration: runtime.generation,
158
+ enrolledAt: previous.enrolledAt || now,
159
+ updatedAt: now,
160
+ });
161
+ let startMessage = 'System tunnel not started (--no-start).';
162
+ if (options.start) {
163
+ startMessage = await startSystemTunnel(interfaceName, configPath, installDir);
164
+ }
165
+ process.stdout.write([
166
+ refreshOnly ? 'HDO config refreshed.' : 'HDO device enrolled.',
167
+ `Device: ${deviceId}`,
168
+ `Overlay IP: ${runtime.overlayIp}`,
169
+ `WireGuard config: ${configPath}`,
170
+ `State file: ${stateFile}`,
171
+ startMessage,
172
+ '',
173
+ ].join('\n'));
174
+ }
175
+ function statusCommand(stateFileInput) {
176
+ const stateFile = resolveStateFile(stateFileInput);
177
+ const state = readState(stateFile);
178
+ const interfaceName = sanitizeInterfaceName(state.interfaceName || defaultInterfaceName);
179
+ const configPath = resolveConfigPath(state.configPath, interfaceName);
180
+ const installDir = resolveInstallDir(state.installDir);
181
+ process.stdout.write(`State file: ${stateFile}\n`);
182
+ process.stdout.write(`Server URL: ${state.serverUrl || 'unset'}\n`);
183
+ process.stdout.write(`Device: ${state.deviceId || 'unset'}\n`);
184
+ process.stdout.write(`Overlay IP: ${state.overlayIp || 'unset'}\n`);
185
+ process.stdout.write(`WireGuard config: ${configPath}\n\n`);
186
+ if (process.platform === 'linux') {
187
+ inherit('systemctl', ['status', `wg-quick@${interfaceName}`, '--no-pager']);
188
+ process.stdout.write('\n');
189
+ inherit('wg', ['show', interfaceName]);
190
+ return;
191
+ }
192
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
193
+ installDir,
194
+ allowSystemFallback: true,
195
+ });
196
+ if (process.platform === 'darwin') {
197
+ const daemon = (0, electron_core_wireguard_1.getDarwinWireGuardLaunchDaemonStatus)({ runtime, configPath });
198
+ process.stdout.write(`LaunchDaemon: ${daemon.loaded ? 'loaded' : 'not loaded'}; running=${daemon.running}\n`);
199
+ if (daemon.plistPath)
200
+ process.stdout.write(`plist: ${daemon.plistPath}\n`);
201
+ }
202
+ const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
203
+ process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
204
+ process.stdout.write(`Runtime: ${runtime.method}\n`);
205
+ if (tunnel.realInterfaceName)
206
+ process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
207
+ if (tunnel.peers.length)
208
+ process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
209
+ if (tunnel.error)
210
+ process.stdout.write(`Status detail: ${tunnel.error}\n`);
211
+ }
212
+ async function downCommand(stateFileInput) {
213
+ const stateFile = resolveStateFile(stateFileInput);
214
+ const state = readState(stateFile);
215
+ const interfaceName = sanitizeInterfaceName(state.interfaceName || defaultInterfaceName);
216
+ const configPath = resolveConfigPath(state.configPath, interfaceName);
217
+ const installDir = resolveInstallDir(state.installDir);
218
+ if (process.platform === 'linux') {
219
+ inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
220
+ return;
221
+ }
222
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
223
+ installDir,
224
+ allowSystemFallback: true,
225
+ });
226
+ if (process.platform === 'darwin') {
227
+ const result = await (0, electron_core_wireguard_1.uninstallDarwinWireGuardLaunchDaemon)({ runtime, configPath });
228
+ if (!result.ok)
229
+ throw new Error(result.message);
230
+ process.stdout.write(`${result.message}\n`);
231
+ return;
232
+ }
233
+ const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'down' });
234
+ if (!result.ok)
235
+ throw new Error(result.message);
236
+ process.stdout.write(`${result.message}\n`);
237
+ }
238
+ function parseEnrollOptions(args) {
239
+ const options = {
240
+ interfaceName: defaultInterfaceName,
241
+ role: 'internal',
242
+ start: true,
243
+ rotateKey: false,
244
+ };
245
+ for (let index = 0; index < args.length; index += 1) {
246
+ const arg = args[index];
247
+ const readValue = () => {
248
+ const value = args[index + 1];
249
+ if (!value)
250
+ throw new Error(`Missing value for ${arg}`);
251
+ index += 1;
252
+ return value;
253
+ };
254
+ switch (arg) {
255
+ case '--server-url':
256
+ options.serverUrl = readValue();
257
+ break;
258
+ case '--token':
259
+ options.token = readValue();
260
+ break;
261
+ case '--token-file':
262
+ options.tokenFile = readValue();
263
+ break;
264
+ case '--username':
265
+ case '--user':
266
+ options.username = readValue();
267
+ break;
268
+ case '--password':
269
+ options.password = readValue();
270
+ break;
271
+ case '--password-file':
272
+ options.passwordFile = readValue();
273
+ break;
274
+ case '--device-id':
275
+ options.deviceId = readValue();
276
+ break;
277
+ case '--label':
278
+ options.label = readValue();
279
+ break;
280
+ case '--interface':
281
+ options.interfaceName = readValue();
282
+ break;
283
+ case '--config-path':
284
+ options.configPath = readValue();
285
+ break;
286
+ case '--state-file':
287
+ options.stateFile = readValue();
288
+ break;
289
+ case '--install-dir':
290
+ options.installDir = readValue();
291
+ break;
292
+ case '--role':
293
+ options.role = readValue();
294
+ break;
295
+ case '--rotate-key':
296
+ options.rotateKey = true;
297
+ break;
298
+ case '--no-start':
299
+ options.start = false;
300
+ break;
301
+ default:
302
+ throw new Error(`Unknown hdo enroll option: ${arg}`);
303
+ }
304
+ }
305
+ return options;
306
+ }
307
+ function parseStateFileArg(args) {
308
+ let stateFile;
309
+ for (let index = 0; index < args.length; index += 1) {
310
+ const arg = args[index];
311
+ if (arg === '--state-file') {
312
+ const value = args[index + 1];
313
+ if (!value)
314
+ throw new Error('Missing value for --state-file');
315
+ stateFile = value;
316
+ index += 1;
317
+ }
318
+ else {
319
+ throw new Error(`Unknown hdo option: ${arg}`);
320
+ }
321
+ }
322
+ return stateFile;
323
+ }
324
+ async function resolveAuth(serverUrl, options, previous, username) {
325
+ const token = resolveExplicitToken(options);
326
+ if (token) {
327
+ return {
328
+ accessToken: token,
329
+ refreshToken: previous.refreshToken,
330
+ accessExpiresAt: previous.accessExpiresAt,
331
+ refreshExpiresAt: previous.refreshExpiresAt,
332
+ username,
333
+ };
334
+ }
335
+ if (previous.refreshToken && !tokenExpired(previous.refreshExpiresAt)) {
336
+ try {
337
+ return await refreshAuth(serverUrl, previous.refreshToken, username ?? previous.username);
338
+ }
339
+ catch {
340
+ // Fall through to username/password login.
341
+ }
342
+ }
343
+ const identifier = username;
344
+ const password = resolvePassword(options);
345
+ if (identifier && password) {
346
+ return loginAuth(serverUrl, identifier, password);
347
+ }
348
+ if (previous.bearerToken && !tokenExpired(previous.accessExpiresAt)) {
349
+ return {
350
+ accessToken: previous.bearerToken,
351
+ refreshToken: previous.refreshToken,
352
+ accessExpiresAt: previous.accessExpiresAt,
353
+ refreshExpiresAt: previous.refreshExpiresAt,
354
+ username: username ?? previous.username,
355
+ };
356
+ }
357
+ if (!identifier) {
358
+ throw new Error('Missing --username, HDO_USERNAME, or --token.');
359
+ }
360
+ throw new Error('Missing --password, --password-file, or HDO_PASSWORD.');
361
+ }
362
+ function resolveExplicitToken(options) {
363
+ if (options.token)
364
+ return options.token;
365
+ if (options.tokenFile)
366
+ return (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(options.tokenFile), 'utf8').trim();
367
+ return process.env.HDO_TOKEN ?? process.env.QPJOY_HDO_TOKEN;
368
+ }
369
+ function resolvePassword(options) {
370
+ if (options.password)
371
+ return options.password;
372
+ if (options.passwordFile)
373
+ return (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(options.passwordFile), 'utf8').trim();
374
+ return process.env.HDO_PASSWORD ?? process.env.QPJOY_HDO_PASSWORD;
375
+ }
376
+ async function loginAuth(serverUrl, identifier, password) {
377
+ const raw = await apiJson(serverUrl, '', '/api/v1/auth/login', {
378
+ method: 'POST',
379
+ body: { identifier, password },
380
+ auth: false,
381
+ });
382
+ const root = requireRecord(raw, 'auth response');
383
+ const tokens = requireRecord(root.tokens, 'auth response tokens');
384
+ const accessToken = stringField(tokens.accessToken);
385
+ if (!accessToken)
386
+ throw new Error('Auth response did not include accessToken.');
387
+ return {
388
+ accessToken,
389
+ refreshToken: stringField(tokens.refreshToken) ?? undefined,
390
+ accessExpiresAt: stringField(tokens.accessExpiresAt) ?? undefined,
391
+ refreshExpiresAt: stringField(tokens.refreshExpiresAt) ?? undefined,
392
+ username: identifier,
393
+ };
394
+ }
395
+ async function refreshAuth(serverUrl, refreshToken, username) {
396
+ const raw = await apiJson(serverUrl, '', '/api/v1/auth/refresh', {
397
+ method: 'POST',
398
+ body: { refreshToken },
399
+ auth: false,
400
+ });
401
+ const root = requireRecord(raw, 'refresh response');
402
+ const accessToken = stringField(root.accessToken);
403
+ if (!accessToken)
404
+ throw new Error('Refresh response did not include accessToken.');
405
+ return {
406
+ accessToken,
407
+ refreshToken: stringField(root.refreshToken) ?? refreshToken,
408
+ accessExpiresAt: stringField(root.accessExpiresAt) ?? undefined,
409
+ refreshExpiresAt: stringField(root.refreshExpiresAt) ?? undefined,
410
+ username,
411
+ };
412
+ }
413
+ function resolveKeypair(options, previous, installDir) {
414
+ if (!options.rotateKey && previous.privateKey && previous.publicKey) {
415
+ return { privateKey: previous.privateKey, publicKey: previous.publicKey };
416
+ }
417
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardRuntime)({
418
+ installDir,
419
+ allowSystemFallback: true,
420
+ });
421
+ if (!runtime.command) {
422
+ throw new Error(runtime.error ?? 'WireGuard wg command unavailable.');
423
+ }
424
+ return (0, electron_core_wireguard_1.generateWireGuardKeyPairWithCli)(runtime.command);
425
+ }
426
+ async function startSystemTunnel(interfaceName, configPath, installDir) {
427
+ if (process.platform === 'linux') {
428
+ inheritRequired('systemctl', ['enable', `wg-quick@${interfaceName}`]);
429
+ inheritRequired('systemctl', ['restart', `wg-quick@${interfaceName}`]);
430
+ return `Enabled and restarted wg-quick@${interfaceName}.`;
431
+ }
432
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
433
+ installDir,
434
+ allowSystemFallback: true,
435
+ });
436
+ if (process.platform === 'darwin') {
437
+ const result = await (0, electron_core_wireguard_1.installDarwinWireGuardLaunchDaemon)({ runtime, configPath });
438
+ if (!result.ok)
439
+ throw new Error(result.message);
440
+ return result.message;
441
+ }
442
+ if (process.platform === 'win32') {
443
+ const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'restart' });
444
+ if (!result.ok)
445
+ throw new Error(result.message);
446
+ return result.message;
447
+ }
448
+ throw new Error(`Unsupported platform for HDO system tunnel: ${process.platform}`);
449
+ }
450
+ function readState(path) {
451
+ if (!(0, node_fs_1.existsSync)(path))
452
+ return {};
453
+ const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
454
+ const parsed = JSON.parse(raw);
455
+ if (!isRecord(parsed))
456
+ return {};
457
+ return parsed;
458
+ }
459
+ function writeState(path, state) {
460
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
461
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
462
+ chmodSyncSafe(path, 0o600);
463
+ }
464
+ function writeWireGuardConfig(path, content) {
465
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
466
+ (0, node_fs_1.writeFileSync)(path, content, { mode: 0o600 });
467
+ chmodSyncSafe(path, 0o600);
468
+ }
469
+ function hdoRuntimeFromManifest(manifest, registered, privateKey) {
470
+ const root = requireRecord(manifest, 'manifest');
471
+ const license = requireRecord(root.license, 'manifest.license');
472
+ if (license.active !== true) {
473
+ throw new Error('HDO mesh license is not active for this user/device.');
474
+ }
475
+ const wireGuard = requireRecord(root.wireGuard, 'manifest.wireGuard');
476
+ const client = requireRecord(wireGuard.client, 'manifest.wireGuard.client');
477
+ const domestic = requireRecord(wireGuard.domestic, 'manifest.wireGuard.domestic');
478
+ const overlayIp = stringField(client.overlayIp) ||
479
+ stringField(requireRecord(registered, 'registered device').overlayIp);
480
+ const domesticPublicKey = stringField(domestic.publicKey);
481
+ const domesticEndpoint = stringField(domestic.endpoint);
482
+ if (!overlayIp)
483
+ throw new Error('HDO manifest did not assign an overlay IP.');
484
+ if (!domesticPublicKey || !domesticEndpoint) {
485
+ throw new Error('HDO manifest is missing domestic WireGuard publicKey/endpoint.');
486
+ }
487
+ const routeCidrs = stringArray(wireGuard.routeCidrs);
488
+ const domesticOverlay = stringField(domestic.overlayIp);
489
+ const allowedIps = uniqueStrings([
490
+ ...routeCidrs,
491
+ ...(domesticOverlay ? [`${domesticOverlay}/32`] : []),
492
+ ]).filter((value) => value.includes('/'));
493
+ if (allowedIps.length === 0) {
494
+ throw new Error('HDO manifest did not include WireGuard AllowedIPs.');
495
+ }
496
+ return {
497
+ privateKey,
498
+ address: overlayIp.includes('/') ? overlayIp : `${overlayIp}/32`,
499
+ overlayIp: overlayIp.split('/')[0] || overlayIp,
500
+ domesticPublicKey,
501
+ domesticEndpoint,
502
+ allowedIps,
503
+ generation: numberField(root.generation),
504
+ };
505
+ }
506
+ async function apiJson(serverUrl, token, path, input) {
507
+ const headers = {};
508
+ if (input.auth !== false)
509
+ headers.authorization = `Bearer ${token}`;
510
+ if (input.body !== undefined)
511
+ headers['content-type'] = 'application/json';
512
+ const response = await fetch(`${serverUrl}${path}`, {
513
+ method: input.method,
514
+ headers,
515
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
516
+ });
517
+ const text = await response.text();
518
+ if (!response.ok) {
519
+ throw new Error(`HDO API ${input.method} ${path} failed: HTTP ${response.status} ${text}`);
520
+ }
521
+ return text ? JSON.parse(text) : null;
522
+ }
523
+ function resolveStateFile(input) {
524
+ return (0, node_path_1.resolve)(input || defaultStateFile());
525
+ }
526
+ function resolveConfigPath(input, interfaceName) {
527
+ return (0, node_path_1.resolve)(input || defaultConfigPath(interfaceName));
528
+ }
529
+ function resolveInstallDir(input) {
530
+ return (0, node_path_1.resolve)(input || defaultInstallDir());
531
+ }
532
+ function defaultStateFile() {
533
+ if (process.platform === 'linux')
534
+ return '/etc/qpjoy/hdo/client.json';
535
+ if (process.platform === 'win32')
536
+ return (0, node_path_1.join)(windowsUserDataDir(), 'client.json');
537
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.qpjoy', 'hdo', 'client.json');
538
+ }
539
+ function defaultConfigPath(interfaceName) {
540
+ if (process.platform === 'linux')
541
+ return `/etc/wireguard/${interfaceName}.conf`;
542
+ if (process.platform === 'win32')
543
+ return (0, node_path_1.join)(windowsUserDataDir(), `${interfaceName}.conf`);
544
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.qpjoy', 'hdo', `${interfaceName}.conf`);
545
+ }
546
+ function defaultInstallDir() {
547
+ if (process.platform === 'linux')
548
+ return '/usr/local/lib/qpjoy/hdo/bin';
549
+ if (process.platform === 'win32')
550
+ return (0, node_path_1.join)(windowsUserDataDir(), 'bin');
551
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.qpjoy', 'hdo', 'bin');
552
+ }
553
+ function windowsUserDataDir() {
554
+ return (0, node_path_1.join)(process.env.LOCALAPPDATA || (0, node_path_1.join)((0, node_os_1.homedir)(), 'AppData', 'Local'), 'QPJoy', 'HDO');
555
+ }
556
+ function inherit(command, args) {
557
+ (0, node_child_process_1.spawnSync)(command, args, { stdio: 'inherit' });
558
+ }
559
+ function inheritRequired(command, args) {
560
+ const result = (0, node_child_process_1.spawnSync)(command, args, { stdio: 'inherit' });
561
+ assertSpawnOk(command, args, result);
562
+ }
563
+ function assertSpawnOk(command, args, result) {
564
+ if (result.error) {
565
+ throw new Error(`${command} failed: ${result.error.message}`);
566
+ }
567
+ if (result.status && result.status !== 0) {
568
+ const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString('utf8') : result.stderr;
569
+ throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}${stderr ? `: ${stderr}` : ''}`);
570
+ }
571
+ }
572
+ function normalizeBaseUrl(value) {
573
+ if (!value)
574
+ return undefined;
575
+ return value.replace(/\/+$/, '');
576
+ }
577
+ function sanitizeInterfaceName(value) {
578
+ const safe = value.trim();
579
+ if (!/^[a-zA-Z0-9_.-]{1,64}$/.test(safe)) {
580
+ throw new Error(`Invalid WireGuard interface name: ${value}`);
581
+ }
582
+ return safe;
583
+ }
584
+ function sanitizeId(value) {
585
+ return value
586
+ .toLowerCase()
587
+ .replace(/[^a-z0-9._:-]+/g, '-')
588
+ .replace(/^-+|-+$/g, '')
589
+ .slice(0, 80) || 'device';
590
+ }
591
+ function tokenExpired(value) {
592
+ if (!value)
593
+ return false;
594
+ const time = Date.parse(value);
595
+ return Number.isFinite(time) && Date.now() > time - 60_000;
596
+ }
597
+ function chmodSyncSafe(path, mode) {
598
+ if (process.platform === 'win32')
599
+ return;
600
+ (0, node_fs_1.chmodSync)(path, mode);
601
+ }
602
+ function requireRecord(value, label) {
603
+ if (!isRecord(value))
604
+ throw new Error(`${label} is not an object.`);
605
+ return value;
606
+ }
607
+ function isRecord(value) {
608
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
609
+ }
610
+ function stringField(value) {
611
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
612
+ }
613
+ function numberField(value) {
614
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
615
+ }
616
+ function stringArray(value) {
617
+ if (!Array.isArray(value))
618
+ return [];
619
+ return value.map((item) => stringField(item)).filter((item) => Boolean(item));
620
+ }
621
+ function uniqueStrings(values) {
622
+ return [...new Set(values)];
623
+ }
package/dist/index.js CHANGED
@@ -5,11 +5,34 @@ const node_child_process_1 = require("node:child_process");
5
5
  const node_fs_1 = require("node:fs");
6
6
  const promises_1 = require("node:fs/promises");
7
7
  const node_path_1 = require("node:path");
8
+ const hdo_1 = require("./hdo");
8
9
  const args = process.argv.slice(2);
9
10
  const packageRoot = (0, node_path_1.resolve)(__dirname, '..');
10
11
  const bundledClientScript = (0, node_path_1.resolve)(packageRoot, 'resources/mihomo-client.sh');
11
12
  const repoClientScript = (0, node_path_1.resolve)(packageRoot, '../../../scripts/mihomo-client.sh');
12
13
  const defaultInstallTarget = '/usr/local/bin/mihomo-client';
14
+ const defaultMihomoConfigFile = '/etc/mihomo-client/config.yaml';
15
+ const defaultNoProxyEntries = [
16
+ 'localhost',
17
+ '127.0.0.1',
18
+ '::1',
19
+ 'postgres',
20
+ 'market',
21
+ 'db',
22
+ 'redis',
23
+ 'host.docker.internal',
24
+ 'docker.for.mac.host.internal',
25
+ 'docker.for.win.localhost',
26
+ 'kubernetes.docker.internal',
27
+ '10.0.0.0/8',
28
+ '172.16.0.0/12',
29
+ '192.168.0.0/16',
30
+ '100.64.0.0/10',
31
+ '100.88.0.0/16',
32
+ '100.89.0.0/16',
33
+ '100.90.0.0/16',
34
+ '.local',
35
+ ];
13
36
  const clientCommands = new Set([
14
37
  'setup',
15
38
  'install',
@@ -49,7 +72,10 @@ Usage:
49
72
  qp-tunnel-cli install-script [--target /usr/local/bin/mihomo-client]
50
73
  qp-tunnel-cli script-path
51
74
  qp-tunnel-cli client-help
75
+ qp-tunnel-cli hdo enroll --server-url https://domestic.example.com --username user
52
76
  qp-tunnel-cli <mihomo-client command> [options]
77
+ qp-tunnel-cli -- <command> [args...]
78
+ qp-tunnel-cli <command-path> [args...]
53
79
 
54
80
  Common commands:
55
81
  qp-tunnel-cli install --url http://IP:3434/peer_user01.mihomo.yaml --user download --password pass
@@ -59,14 +85,25 @@ Common commands:
59
85
  qp-tunnel-cli tun-on
60
86
  qp-tunnel-cli tun-off
61
87
  qp-tunnel-cli update-subscription
88
+ qp-tunnel-cli hdo status
62
89
  qp-tunnel-cli uninstall --purge
90
+ qp-tunnel-cli ./electron-server/scripts/manage.sh redeploy
63
91
 
64
- The npm package is a thin distributor for the Linux mihomo-client script. Client
65
- commands re-run through sudo when needed, then execute the bundled shell script.
92
+ The npm package distributes the Linux mihomo-client script and a cross-platform
93
+ HDO WireGuard enrollment command. Linux mihomo-client commands re-run through
94
+ sudo when needed, then execute the bundled shell script.
95
+
96
+ Unknown commands are executed with QPJoy proxy variables injected. Host commands
97
+ receive HTTP_PROXY=http://127.0.0.1:<mixed-port>; Docker/Compose build contexts
98
+ also receive container-facing variables such as MARKET_CONTAINER_HTTP_PROXY and
99
+ QP_TUNNEL_CONTAINER_HTTP_PROXY=http://host.docker.internal:<mixed-port>.
66
100
 
67
101
  Install the script as a normal server command:
68
102
  sudo qp-tunnel-cli install-script
69
103
  sudo mihomo-client status
104
+
105
+ Enroll this machine into an HDO mesh:
106
+ HDO_PASSWORD=... qp-tunnel-cli hdo enroll --server-url https://domestic.example.com --username user
70
107
  `);
71
108
  }
72
109
  function clientHelp() {
@@ -129,6 +166,94 @@ function runClientCommand(scriptArgs) {
129
166
  }
130
167
  runScriptWithoutSudo(scriptArgs);
131
168
  }
169
+ function mixedPortFromConfig() {
170
+ const explicit = process.env.QP_TUNNEL_MIXED_PORT || process.env.MIHOMO_MIXED_PORT;
171
+ if (explicit && /^\d+$/.test(explicit)) {
172
+ return explicit;
173
+ }
174
+ const configFile = process.env.MIHOMO_CONFIG_FILE || defaultMihomoConfigFile;
175
+ if (!(0, node_fs_1.existsSync)(configFile)) {
176
+ process.stderr.write(`Mihomo config not found: ${configFile}\nRun: sudo qp-tunnel-cli install ... && sudo qp-tunnel-cli server-on\n`);
177
+ process.exit(1);
178
+ }
179
+ const content = (0, node_fs_1.readFileSync)(configFile, 'utf8');
180
+ const match = /^\s*mixed-port\s*:\s*(\d+)/m.exec(content);
181
+ if (!match) {
182
+ process.stderr.write(`Could not detect mixed-port from ${configFile}\n`);
183
+ process.exit(1);
184
+ }
185
+ return match[1];
186
+ }
187
+ function mergeCsvValues(...values) {
188
+ const seen = new Set();
189
+ const merged = [];
190
+ for (const value of values) {
191
+ for (const item of (value || '').split(',')) {
192
+ const trimmed = item.trim();
193
+ if (!trimmed || seen.has(trimmed))
194
+ continue;
195
+ seen.add(trimmed);
196
+ merged.push(trimmed);
197
+ }
198
+ }
199
+ return merged.join(',');
200
+ }
201
+ function proxyEnvironment() {
202
+ const port = mixedPortFromConfig();
203
+ const hostProxy = `http://127.0.0.1:${port}`;
204
+ const hostSocksProxy = `socks5://127.0.0.1:${port}`;
205
+ const containerHost = process.env.QP_TUNNEL_CONTAINER_HOST || 'host.docker.internal';
206
+ const containerProxy = `http://${containerHost}:${port}`;
207
+ const noProxy = mergeCsvValues(process.env.NO_PROXY, process.env.no_proxy, defaultNoProxyEntries.join(','));
208
+ const containerNoProxy = mergeCsvValues(process.env.MARKET_CONTAINER_NO_PROXY, process.env.QP_TUNNEL_CONTAINER_NO_PROXY, noProxy);
209
+ return {
210
+ ...process.env,
211
+ HTTP_PROXY: hostProxy,
212
+ HTTPS_PROXY: hostProxy,
213
+ ALL_PROXY: hostSocksProxy,
214
+ http_proxy: hostProxy,
215
+ https_proxy: hostProxy,
216
+ all_proxy: hostSocksProxy,
217
+ NO_PROXY: noProxy,
218
+ no_proxy: noProxy,
219
+ npm_config_proxy: hostProxy,
220
+ npm_config_https_proxy: hostProxy,
221
+ npm_config_noproxy: noProxy,
222
+ pnpm_config_proxy: hostProxy,
223
+ pnpm_config_https_proxy: hostProxy,
224
+ pnpm_config_noproxy: noProxy,
225
+ QP_TUNNEL_MIXED_PORT: port,
226
+ QP_TUNNEL_HOST_HTTP_PROXY: hostProxy,
227
+ QP_TUNNEL_HOST_HTTPS_PROXY: hostProxy,
228
+ QP_TUNNEL_HOST_ALL_PROXY: hostSocksProxy,
229
+ QP_TUNNEL_CONTAINER_HTTP_PROXY: containerProxy,
230
+ QP_TUNNEL_CONTAINER_HTTPS_PROXY: containerProxy,
231
+ QP_TUNNEL_CONTAINER_NO_PROXY: containerNoProxy,
232
+ CONTAINER_HTTP_PROXY: containerProxy,
233
+ CONTAINER_HTTPS_PROXY: containerProxy,
234
+ CONTAINER_NO_PROXY: containerNoProxy,
235
+ BUILD_CONTAINER_HTTP_PROXY: containerProxy,
236
+ BUILD_CONTAINER_HTTPS_PROXY: containerProxy,
237
+ BUILD_CONTAINER_NO_PROXY: containerNoProxy,
238
+ MARKET_CONTAINER_HTTP_PROXY: process.env.MARKET_CONTAINER_HTTP_PROXY || containerProxy,
239
+ MARKET_CONTAINER_HTTPS_PROXY: process.env.MARKET_CONTAINER_HTTPS_PROXY || containerProxy,
240
+ MARKET_CONTAINER_NO_PROXY: containerNoProxy,
241
+ };
242
+ }
243
+ function runExternalCommand(commandArgs) {
244
+ if (commandArgs.length === 0) {
245
+ process.stderr.write('Missing command after qp-tunnel-cli --\n');
246
+ process.exit(1);
247
+ }
248
+ const [rawCommand, ...rawArgs] = commandArgs;
249
+ const command = rawCommand === 'sudo' && !rawArgs.includes('-E') ? 'sudo' : rawCommand;
250
+ const commandArgsWithSudoEnv = rawCommand === 'sudo' && !rawArgs.includes('-E') ? ['-E', ...rawArgs] : rawArgs;
251
+ const result = (0, node_child_process_1.spawnSync)(command, commandArgsWithSudoEnv, {
252
+ stdio: 'inherit',
253
+ env: proxyEnvironment(),
254
+ });
255
+ exitFromSpawn(result);
256
+ }
132
257
  function parseInstallScriptArgs(scriptArgs) {
133
258
  let target = defaultInstallTarget;
134
259
  for (let index = 0; index < scriptArgs.length; index += 1) {
@@ -192,6 +317,9 @@ async function printScriptPath() {
192
317
  }
193
318
  async function main() {
194
319
  const command = args[0] ?? 'help';
320
+ if (command === '--') {
321
+ runExternalCommand(args.slice(1));
322
+ }
195
323
  if (command === 'help' || command === '--help' || command === '-h') {
196
324
  help();
197
325
  return;
@@ -211,13 +339,19 @@ async function main() {
211
339
  installClientScript(args.slice(1));
212
340
  return;
213
341
  }
342
+ if (command === 'hdo' || command === 'hdo-enroll' || command === 'hdo-refresh') {
343
+ const hdoArgs = command === 'hdo' ? args.slice(1) : [command.replace(/^hdo-/, ''), ...args.slice(1)];
344
+ await (0, hdo_1.runHdoCli)(hdoArgs, { isRoot, sudoSelf });
345
+ return;
346
+ }
214
347
  const passthroughCommand = command === '--verbose' ? args[1] : command;
215
348
  if (passthroughCommand && clientCommands.has(passthroughCommand)) {
216
349
  runClientCommand(args);
217
350
  }
218
- process.stderr.write(`Unknown command: ${command}\n`);
351
+ if (args.length > 0) {
352
+ runExternalCommand(args);
353
+ }
219
354
  help();
220
- process.exitCode = 1;
221
355
  }
222
356
  main().catch((error) => {
223
357
  const message = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qpjoy/tunnel-cli",
3
- "version": "0.1.1",
4
- "description": "Global QPJoy Tunnel CLI for installing and running the Linux mihomo-client script.",
3
+ "version": "0.1.4",
4
+ "description": "Global QPJoy Tunnel CLI for mihomo-client and cross-platform HDO mesh enrollment.",
5
5
  "private": false,
6
6
  "type": "commonjs",
7
7
  "main": "dist/index.js",
@@ -21,6 +21,9 @@
21
21
  "publishConfig": {
22
22
  "access": "public"
23
23
  },
24
+ "dependencies": {
25
+ "@qpjoy/electron-core-wireguard": "^0.1.18"
26
+ },
24
27
  "devDependencies": {
25
28
  "@types/node": "^22.10.7"
26
29
  },
@@ -97,6 +97,7 @@ Examples:
97
97
  sudo bash ./scripts/mihomo-client.sh ssh-proxy-on
98
98
  sudo bash ./scripts/mihomo-client.sh daemon-proxy-on
99
99
  sudo bash ./scripts/mihomo-client.sh run curl -I https://www.google.com/generate_204
100
+ sudo bash ./scripts/mihomo-client.sh run ./electron-server/scripts/manage.sh redeploy
100
101
  sudo bash ./scripts/mihomo-client.sh print-env
101
102
  EOF
102
103
  }
@@ -532,10 +533,10 @@ docker_proxy_env_lines() {
532
533
  cat <<EOF
533
534
  HTTP_PROXY=http://127.0.0.1:$port
534
535
  HTTPS_PROXY=http://127.0.0.1:$port
535
- NO_PROXY=localhost,127.0.0.1,::1
536
+ NO_PROXY=$MIHOMO_NO_PROXY
536
537
  http_proxy=http://127.0.0.1:$port
537
538
  https_proxy=http://127.0.0.1:$port
538
- no_proxy=localhost,127.0.0.1,::1
539
+ no_proxy=$MIHOMO_NO_PROXY
539
540
  EOF
540
541
  }
541
542
 
@@ -814,16 +815,38 @@ server_off_command() {
814
815
  }
815
816
 
816
817
  run_command() {
817
- local port
818
+ local port host_proxy host_socks_proxy container_host container_proxy
818
819
  [[ $# -gt 0 ]] || die "Usage: sudo bash ./scripts/mihomo-client.sh run <command> [args...]"
819
820
  port="$(mixed_port_from_config)"
820
821
  [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
821
- http_proxy="http://127.0.0.1:$port" \
822
- https_proxy="http://127.0.0.1:$port" \
823
- HTTP_PROXY="http://127.0.0.1:$port" \
824
- HTTPS_PROXY="http://127.0.0.1:$port" \
825
- all_proxy="socks5://127.0.0.1:$port" \
826
- ALL_PROXY="socks5://127.0.0.1:$port" \
822
+ host_proxy="http://127.0.0.1:$port"
823
+ host_socks_proxy="socks5://127.0.0.1:$port"
824
+ container_host="${QP_TUNNEL_CONTAINER_HOST:-host.docker.internal}"
825
+ container_proxy="http://${container_host}:$port"
826
+ http_proxy="$host_proxy" \
827
+ https_proxy="$host_proxy" \
828
+ HTTP_PROXY="$host_proxy" \
829
+ HTTPS_PROXY="$host_proxy" \
830
+ all_proxy="$host_socks_proxy" \
831
+ ALL_PROXY="$host_socks_proxy" \
832
+ no_proxy="$MIHOMO_NO_PROXY" \
833
+ NO_PROXY="$MIHOMO_NO_PROXY" \
834
+ QP_TUNNEL_MIXED_PORT="$port" \
835
+ QP_TUNNEL_HOST_HTTP_PROXY="$host_proxy" \
836
+ QP_TUNNEL_HOST_HTTPS_PROXY="$host_proxy" \
837
+ QP_TUNNEL_HOST_ALL_PROXY="$host_socks_proxy" \
838
+ QP_TUNNEL_CONTAINER_HTTP_PROXY="$container_proxy" \
839
+ QP_TUNNEL_CONTAINER_HTTPS_PROXY="$container_proxy" \
840
+ QP_TUNNEL_CONTAINER_NO_PROXY="$MIHOMO_NO_PROXY" \
841
+ CONTAINER_HTTP_PROXY="$container_proxy" \
842
+ CONTAINER_HTTPS_PROXY="$container_proxy" \
843
+ CONTAINER_NO_PROXY="$MIHOMO_NO_PROXY" \
844
+ BUILD_CONTAINER_HTTP_PROXY="$container_proxy" \
845
+ BUILD_CONTAINER_HTTPS_PROXY="$container_proxy" \
846
+ BUILD_CONTAINER_NO_PROXY="$MIHOMO_NO_PROXY" \
847
+ MARKET_CONTAINER_HTTP_PROXY="${MARKET_CONTAINER_HTTP_PROXY:-$container_proxy}" \
848
+ MARKET_CONTAINER_HTTPS_PROXY="${MARKET_CONTAINER_HTTPS_PROXY:-$container_proxy}" \
849
+ MARKET_CONTAINER_NO_PROXY="${MARKET_CONTAINER_NO_PROXY:-$MIHOMO_NO_PROXY}" \
827
850
  "$@"
828
851
  }
829
852