@qpjoy/tunnel-cli 0.1.8 → 0.1.10
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 +32 -3
- package/README.setup.md +7 -4
- package/dist/hdo.js +190 -4
- package/dist/index.js +16 -2
- package/package.json +3 -2
- package/resources/mihomo-client.sh +144 -22
- package/scripts/sync-mx-launcher-fallback.mjs +65 -0
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
|
|
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 `
|
|
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
|
|
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.
|
|
3
|
-
|
|
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
|
|
12
|
+
sudo qp-tunnel-cli egress-on
|
|
10
13
|
|
|
11
14
|
sudo qp-tunnel-cli tun-off
|
|
12
|
-
sudo qp-tunnel-cli
|
|
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
|
-
|
|
117
|
-
|
|
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,
|
|
@@ -215,8 +296,13 @@ function statusCommand(input) {
|
|
|
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')) {
|
|
@@ -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,6 +618,68 @@ 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 };
|
|
@@ -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)
|
|
@@ -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
|
|
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) {
|
package/dist/index.js
CHANGED
|
@@ -24,14 +24,26 @@ const defaultNoProxyEntries = [
|
|
|
24
24
|
'docker.for.mac.host.internal',
|
|
25
25
|
'docker.for.win.localhost',
|
|
26
26
|
'kubernetes.docker.internal',
|
|
27
|
+
'kubernetes.default.svc',
|
|
28
|
+
'.cluster.local',
|
|
27
29
|
'10.0.0.0/8',
|
|
28
30
|
'172.16.0.0/12',
|
|
29
31
|
'192.168.0.0/16',
|
|
32
|
+
'169.254.0.0/16',
|
|
33
|
+
'169.254.169.254',
|
|
34
|
+
'169.254.169.254/32',
|
|
35
|
+
'100.100.100.200',
|
|
36
|
+
'100.100.100.200/32',
|
|
30
37
|
'100.64.0.0/10',
|
|
31
38
|
'100.88.0.0/16',
|
|
32
39
|
'100.89.0.0/16',
|
|
33
40
|
'100.90.0.0/16',
|
|
41
|
+
'10.88.0.0/16',
|
|
42
|
+
'10.89.0.0/16',
|
|
43
|
+
'10.90.0.0/16',
|
|
44
|
+
'10.91.0.0/16',
|
|
34
45
|
'.local',
|
|
46
|
+
'.lan',
|
|
35
47
|
];
|
|
36
48
|
const clientCommands = new Set([
|
|
37
49
|
'setup',
|
|
@@ -79,9 +91,10 @@ Usage:
|
|
|
79
91
|
|
|
80
92
|
Common commands:
|
|
81
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
|
|
82
95
|
qp-tunnel-cli status
|
|
83
96
|
qp-tunnel-cli start
|
|
84
|
-
qp-tunnel-cli
|
|
97
|
+
qp-tunnel-cli egress-on
|
|
85
98
|
qp-tunnel-cli tun-on
|
|
86
99
|
qp-tunnel-cli tun-off
|
|
87
100
|
qp-tunnel-cli update-subscription
|
|
@@ -101,6 +114,7 @@ QP_TUNNEL_CONTAINER_HTTP_PROXY=http://host.docker.internal:<mixed-port>.
|
|
|
101
114
|
Install the script as a normal server command:
|
|
102
115
|
sudo qp-tunnel-cli install-script
|
|
103
116
|
sudo mihomo-client status
|
|
117
|
+
sudo mihomo-client egress-on
|
|
104
118
|
|
|
105
119
|
Enroll this machine into an HDO mesh:
|
|
106
120
|
HDO_PASSWORD=... qp-tunnel-cli hdo enroll --server-url https://domestic.example.com --username user
|
|
@@ -173,7 +187,7 @@ function mixedPortFromConfig() {
|
|
|
173
187
|
}
|
|
174
188
|
const configFile = process.env.MIHOMO_CONFIG_FILE || defaultMihomoConfigFile;
|
|
175
189
|
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
|
|
190
|
+
process.stderr.write(`Mihomo config not found: ${configFile}\nRun: sudo qp-tunnel-cli install ... && sudo qp-tunnel-cli egress-on\n`);
|
|
177
191
|
process.exit(1);
|
|
178
192
|
}
|
|
179
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.
|
|
3
|
+
"version": "0.1.10",
|
|
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,13 +22,14 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@qpjoy/electron-core-wireguard": "^0.1.
|
|
25
|
+
"@qpjoy/electron-core-wireguard": "^0.1.28"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.10.7"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"sync:mx-launcher-fallback": "node scripts/sync-mx-launcher-fallback.mjs",
|
|
32
33
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
34
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
34
35
|
}
|
|
@@ -16,7 +16,7 @@ MIHOMO_SERVICE_FILE="${MIHOMO_SERVICE_FILE:-/etc/systemd/system/$MIHOMO_SERVICE_
|
|
|
16
16
|
MIHOMO_PROFILE_PROXY_FILE="${MIHOMO_PROFILE_PROXY_FILE:-/etc/profile.d/mihomo-client-proxy.sh}"
|
|
17
17
|
MIHOMO_DAEMON_PROXY_SERVICES="${MIHOMO_DAEMON_PROXY_SERVICES:-docker.service containerd.service buildkit.service}"
|
|
18
18
|
MIHOMO_DAEMON_PROXY_DROPIN_NAME="${MIHOMO_DAEMON_PROXY_DROPIN_NAME:-mihomo-proxy.conf}"
|
|
19
|
-
MIHOMO_NO_PROXY="${MIHOMO_NO_PROXY:-localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,100.88.0.0/16,100.89.0.0/16,100.90.0.0/16,host.docker.internal,.local}"
|
|
19
|
+
MIHOMO_NO_PROXY="${MIHOMO_NO_PROXY:-localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,169.254.169.254,169.254.169.254/32,100.64.0.0/10,100.88.0.0/16,100.89.0.0/16,100.90.0.0/16,10.88.0.0/16,10.89.0.0/16,10.90.0.0/16,10.91.0.0/16,100.100.100.200,100.100.100.200/32,host.docker.internal,docker.for.mac.host.internal,docker.for.win.localhost,kubernetes.docker.internal,kubernetes.default.svc,.cluster.local,.local,.lan}"
|
|
20
20
|
MIHOMO_SSH_PROXY_HELPER="${MIHOMO_SSH_PROXY_HELPER:-/usr/local/bin/mihomo-ssh-proxy}"
|
|
21
21
|
MIHOMO_SSH_CONFIG_DIR="${MIHOMO_SSH_CONFIG_DIR:-/etc/ssh/ssh_config.d}"
|
|
22
22
|
MIHOMO_SSH_CONFIG_FILE="${MIHOMO_SSH_CONFIG_FILE:-$MIHOMO_SSH_CONFIG_DIR/99-mihomo-proxy.conf}"
|
|
@@ -45,11 +45,11 @@ 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
|
|
49
|
-
egress-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
|
-
tun-on Enable Mihomo TUN mode and
|
|
52
|
+
tun-on Enable persistent Mihomo TUN mode with cn-direct and local/private route bypasses
|
|
53
53
|
tun-off Disable Mihomo TUN mode and also turn proxy-on off
|
|
54
54
|
ssh-proxy-on Configure OpenSSH client to use local Mihomo SOCKS for common Git hosts
|
|
55
55
|
ssh-proxy-off Remove OpenSSH proxy override
|
|
@@ -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
|
|
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
|
|
@@ -399,8 +405,43 @@ ensure_subscription_source() {
|
|
|
399
405
|
[[ -f "$MIHOMO_SUBSCRIPTION_FILE" ]] || die "Subscription source not found. Please run install or update-subscription first."
|
|
400
406
|
}
|
|
401
407
|
|
|
408
|
+
detect_tun_proxy_group_name() {
|
|
409
|
+
local configured="${MIHOMO_TUN_PROXY_GROUP:-}"
|
|
410
|
+
local detected=""
|
|
411
|
+
if [[ -n "$configured" ]]; then
|
|
412
|
+
echo "$configured"
|
|
413
|
+
return
|
|
414
|
+
fi
|
|
415
|
+
if [[ -f "$MIHOMO_SUBSCRIPTION_FILE" ]]; then
|
|
416
|
+
detected="$(awk '
|
|
417
|
+
/^[[:space:]]*proxy-groups:[[:space:]]*$/ { in_groups=1; next }
|
|
418
|
+
in_groups && /^[^[:space:]-]/ { in_groups=0 }
|
|
419
|
+
in_groups && /^[[:space:]]*-[[:space:]]*name:[[:space:]]*/ {
|
|
420
|
+
line=$0
|
|
421
|
+
sub(/^[[:space:]]*-[[:space:]]*name:[[:space:]]*/, "", line)
|
|
422
|
+
gsub(/^[[:space:]"]+/, "", line)
|
|
423
|
+
gsub(/[[:space:]"]+$/, "", line)
|
|
424
|
+
print line
|
|
425
|
+
exit
|
|
426
|
+
}
|
|
427
|
+
in_groups && /^[[:space:]]*name:[[:space:]]*/ {
|
|
428
|
+
line=$0
|
|
429
|
+
sub(/^[[:space:]]*name:[[:space:]]*/, "", line)
|
|
430
|
+
gsub(/^[[:space:]"]+/, "", line)
|
|
431
|
+
gsub(/[[:space:]"]+$/, "", line)
|
|
432
|
+
print line
|
|
433
|
+
exit
|
|
434
|
+
}
|
|
435
|
+
' "$MIHOMO_SUBSCRIPTION_FILE" || true)"
|
|
436
|
+
fi
|
|
437
|
+
echo "${detected:-PROXY}"
|
|
438
|
+
}
|
|
439
|
+
|
|
402
440
|
write_tun_overlay() {
|
|
403
|
-
|
|
441
|
+
ensure_subscription_source
|
|
442
|
+
local proxy_group
|
|
443
|
+
proxy_group="$(detect_tun_proxy_group_name)"
|
|
444
|
+
cat > "$MIHOMO_TUN_OVERLAY_FILE" <<EOF
|
|
404
445
|
tun:
|
|
405
446
|
enable: true
|
|
406
447
|
stack: system
|
|
@@ -408,6 +449,17 @@ tun:
|
|
|
408
449
|
auto-redirect: true
|
|
409
450
|
auto-detect-interface: true
|
|
410
451
|
strict-route: true
|
|
452
|
+
route-exclude-address:
|
|
453
|
+
- 10.0.0.0/8
|
|
454
|
+
- 172.16.0.0/12
|
|
455
|
+
- 192.168.0.0/16
|
|
456
|
+
- 169.254.0.0/16
|
|
457
|
+
- 100.64.0.0/10
|
|
458
|
+
- 10.88.0.0/16
|
|
459
|
+
- 10.89.0.0/16
|
|
460
|
+
- 10.90.0.0/16
|
|
461
|
+
- 10.91.0.0/16
|
|
462
|
+
- 100.100.100.200/32
|
|
411
463
|
dns-hijack:
|
|
412
464
|
- any:53
|
|
413
465
|
- tcp://any:53
|
|
@@ -425,6 +477,7 @@ dns:
|
|
|
425
477
|
- 223.5.5.5
|
|
426
478
|
- 119.29.29.29
|
|
427
479
|
- 1.1.1.1
|
|
480
|
+
- 8.8.8.8
|
|
428
481
|
nameserver:
|
|
429
482
|
- https://dns.alidns.com/dns-query
|
|
430
483
|
- https://doh.pub/dns-query
|
|
@@ -436,6 +489,31 @@ dns:
|
|
|
436
489
|
geoip-code: CN
|
|
437
490
|
geosite:
|
|
438
491
|
- gfw
|
|
492
|
+
|
|
493
|
+
rules:
|
|
494
|
+
- DOMAIN-SUFFIX,local,DIRECT
|
|
495
|
+
- DOMAIN-SUFFIX,lan,DIRECT
|
|
496
|
+
- DOMAIN-SUFFIX,internal,DIRECT
|
|
497
|
+
- DOMAIN-SUFFIX,cluster.local,DIRECT
|
|
498
|
+
- DOMAIN,metadata.google.internal,DIRECT
|
|
499
|
+
- DOMAIN,kubernetes.default.svc,DIRECT
|
|
500
|
+
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
|
501
|
+
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
|
|
502
|
+
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
|
|
503
|
+
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
|
504
|
+
- IP-CIDR,169.254.0.0/16,DIRECT,no-resolve
|
|
505
|
+
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
|
|
506
|
+
- IP-CIDR,10.88.0.0/16,DIRECT,no-resolve
|
|
507
|
+
- IP-CIDR,10.89.0.0/16,DIRECT,no-resolve
|
|
508
|
+
- IP-CIDR,10.90.0.0/16,DIRECT,no-resolve
|
|
509
|
+
- IP-CIDR,10.91.0.0/16,DIRECT,no-resolve
|
|
510
|
+
- IP-CIDR,100.100.100.200/32,DIRECT,no-resolve
|
|
511
|
+
- IP-CIDR6,::1/128,DIRECT,no-resolve
|
|
512
|
+
- IP-CIDR6,fc00::/7,DIRECT,no-resolve
|
|
513
|
+
- IP-CIDR6,fe80::/10,DIRECT,no-resolve
|
|
514
|
+
- GEOSITE,CN,DIRECT
|
|
515
|
+
- GEOIP,CN,DIRECT
|
|
516
|
+
- MATCH,$proxy_group
|
|
439
517
|
EOF
|
|
440
518
|
chmod 600 "$MIHOMO_TUN_OVERLAY_FILE"
|
|
441
519
|
}
|
|
@@ -503,6 +581,20 @@ fetch_subscription() {
|
|
|
503
581
|
render_runtime_config
|
|
504
582
|
}
|
|
505
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
|
+
|
|
506
598
|
mixed_port_from_config() {
|
|
507
599
|
if [[ -f "$MIHOMO_CONFIG_FILE" ]]; then
|
|
508
600
|
awk -F: '/^[[:space:]]*mixed-port[[:space:]]*:/ {gsub(/[[:space:]]/, "", $2); print $2; exit}' "$MIHOMO_CONFIG_FILE"
|
|
@@ -610,10 +702,29 @@ update_subscription_command() {
|
|
|
610
702
|
local url="${1:-}"
|
|
611
703
|
local username="${2:-}"
|
|
612
704
|
local password="${3:-}"
|
|
705
|
+
local file_path="${4:-}"
|
|
613
706
|
local -a normalized=()
|
|
614
707
|
|
|
615
708
|
load_env
|
|
616
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
|
+
|
|
617
728
|
url="${url:-${MIHOMO_SUBSCRIPTION_URL:-}}"
|
|
618
729
|
username="${username:-${MIHOMO_SUBSCRIPTION_USER:-}}"
|
|
619
730
|
password="${password:-${MIHOMO_SUBSCRIPTION_PASSWORD:-}}"
|
|
@@ -625,6 +736,8 @@ update_subscription_command() {
|
|
|
625
736
|
[[ -n "$url" ]] || die "No subscription URL configured. Use install or pass --url."
|
|
626
737
|
|
|
627
738
|
fetch_subscription "$url" "$username" "$password"
|
|
739
|
+
set_env_value MIHOMO_SUBSCRIPTION_SOURCE "url"
|
|
740
|
+
set_env_value MIHOMO_SUBSCRIPTION_LOCAL_FILE ""
|
|
628
741
|
set_env_value MIHOMO_SUBSCRIPTION_URL "$url"
|
|
629
742
|
set_env_value MIHOMO_SUBSCRIPTION_USER "$username"
|
|
630
743
|
set_env_value MIHOMO_SUBSCRIPTION_PASSWORD "$password"
|
|
@@ -759,10 +872,12 @@ tun_on_command() {
|
|
|
759
872
|
daemon_proxy_on_command
|
|
760
873
|
if service_is_active; then
|
|
761
874
|
systemctl restart "$MIHOMO_SERVICE_NAME"
|
|
762
|
-
|
|
875
|
+
systemctl enable "$MIHOMO_SERVICE_NAME" >/dev/null 2>&1 || true
|
|
876
|
+
echo "Mihomo TUN mode enabled, persisted, and service restarted."
|
|
763
877
|
else
|
|
764
878
|
systemctl start "$MIHOMO_SERVICE_NAME"
|
|
765
|
-
|
|
879
|
+
systemctl enable "$MIHOMO_SERVICE_NAME" >/dev/null 2>&1 || true
|
|
880
|
+
echo "Mihomo TUN mode enabled, persisted, and service started."
|
|
766
881
|
fi
|
|
767
882
|
}
|
|
768
883
|
|
|
@@ -867,24 +982,27 @@ install_command() {
|
|
|
867
982
|
local version="${4:-latest}"
|
|
868
983
|
local autostart="${5:-true}"
|
|
869
984
|
local binary_path="${6:-}"
|
|
985
|
+
local file_path="${7:-}"
|
|
870
986
|
local -a normalized=()
|
|
871
987
|
|
|
872
988
|
ensure_dirs
|
|
873
989
|
load_env
|
|
874
990
|
log "Starting Mihomo client install/setup"
|
|
875
991
|
|
|
876
|
-
if [[ -z "$url" ]]; then
|
|
992
|
+
if [[ -z "$file_path" && -z "$url" ]]; then
|
|
877
993
|
url="$(prompt_default "Subscription URL" "${MIHOMO_SUBSCRIPTION_URL:-}")"
|
|
878
994
|
fi
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
888
1006
|
fi
|
|
889
1007
|
|
|
890
1008
|
if [[ -n "$binary_path" ]]; then
|
|
@@ -904,8 +1022,8 @@ install_command() {
|
|
|
904
1022
|
set_env_value MIHOMO_SUBSCRIPTION_USER "$username"
|
|
905
1023
|
set_env_value MIHOMO_SUBSCRIPTION_PASSWORD "$password"
|
|
906
1024
|
|
|
907
|
-
log "
|
|
908
|
-
update_subscription_command "$url" "$username" "$password"
|
|
1025
|
+
log "Installing initial subscription and rendering runtime config"
|
|
1026
|
+
update_subscription_command "$url" "$username" "$password" "$file_path"
|
|
909
1027
|
|
|
910
1028
|
systemctl enable "$MIHOMO_SERVICE_NAME" >/dev/null 2>&1 || true
|
|
911
1029
|
if [[ "$autostart" == "true" ]]; then
|
|
@@ -1006,9 +1124,11 @@ main() {
|
|
|
1006
1124
|
local version="latest"
|
|
1007
1125
|
local autostart="true"
|
|
1008
1126
|
local binary_path=""
|
|
1127
|
+
local file_path=""
|
|
1009
1128
|
while [[ $# -gt 0 ]]; do
|
|
1010
1129
|
case "$1" in
|
|
1011
1130
|
--url) url="$2"; shift 2 ;;
|
|
1131
|
+
--file) file_path="$2"; shift 2 ;;
|
|
1012
1132
|
--user) username="$2"; shift 2 ;;
|
|
1013
1133
|
--password) password="$2"; shift 2 ;;
|
|
1014
1134
|
--version) version="$2"; shift 2 ;;
|
|
@@ -1017,21 +1137,23 @@ main() {
|
|
|
1017
1137
|
*) die "Unknown install option: $1" ;;
|
|
1018
1138
|
esac
|
|
1019
1139
|
done
|
|
1020
|
-
install_command "$url" "$username" "$password" "$version" "$autostart" "$binary_path"
|
|
1140
|
+
install_command "$url" "$username" "$password" "$version" "$autostart" "$binary_path" "$file_path"
|
|
1021
1141
|
;;
|
|
1022
1142
|
update-subscription)
|
|
1023
1143
|
local url=""
|
|
1024
1144
|
local username=""
|
|
1025
1145
|
local password=""
|
|
1146
|
+
local file_path=""
|
|
1026
1147
|
while [[ $# -gt 0 ]]; do
|
|
1027
1148
|
case "$1" in
|
|
1028
1149
|
--url) url="$2"; shift 2 ;;
|
|
1150
|
+
--file) file_path="$2"; shift 2 ;;
|
|
1029
1151
|
--user) username="$2"; shift 2 ;;
|
|
1030
1152
|
--password) password="$2"; shift 2 ;;
|
|
1031
1153
|
*) die "Unknown update-subscription option: $1" ;;
|
|
1032
1154
|
esac
|
|
1033
1155
|
done
|
|
1034
|
-
update_subscription_command "$url" "$username" "$password"
|
|
1156
|
+
update_subscription_command "$url" "$username" "$password" "$file_path"
|
|
1035
1157
|
;;
|
|
1036
1158
|
start)
|
|
1037
1159
|
start_command
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const repoRoot = resolve(packageRoot, '../../..');
|
|
10
|
+
const mxLauncherRoot = resolve(repoRoot, 'electron-dock/mx-launcher');
|
|
11
|
+
const refreshScript = resolve(mxLauncherRoot, 'server/scripts/site-slot-refresh-tunnel-cli.mjs');
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const tarball = optionValue('--from-tarball') || optionValue('--tarball') || latestPreviewTarball();
|
|
14
|
+
|
|
15
|
+
if (!tarball) {
|
|
16
|
+
die('Missing @qpjoy/tunnel-cli tarball. Pass --from-tarball FILE or run pnpm pack first.');
|
|
17
|
+
}
|
|
18
|
+
if (!existsSync(tarball)) {
|
|
19
|
+
die(`Tarball not found: ${tarball}`);
|
|
20
|
+
}
|
|
21
|
+
if (!existsSync(refreshScript)) {
|
|
22
|
+
die(`MX Launcher refresh script not found: ${refreshScript}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const passThrough = [];
|
|
26
|
+
for (const name of ['--target-dir', '--temp-dir']) {
|
|
27
|
+
const value = optionValue(name);
|
|
28
|
+
if (value) passThrough.push(name, value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
execFileSync(process.execPath, [
|
|
32
|
+
refreshScript,
|
|
33
|
+
'--from-tarball',
|
|
34
|
+
tarball,
|
|
35
|
+
...passThrough
|
|
36
|
+
], {
|
|
37
|
+
cwd: mxLauncherRoot,
|
|
38
|
+
stdio: 'inherit'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function optionValue(name) {
|
|
42
|
+
const index = args.indexOf(name);
|
|
43
|
+
if (index < 0) return null;
|
|
44
|
+
const value = args[index + 1];
|
|
45
|
+
if (!value || value.startsWith('--')) die(`Missing value for ${name}`);
|
|
46
|
+
return resolve(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function latestPreviewTarball() {
|
|
50
|
+
const previewDir = '/tmp/qpjoy-publish-preview';
|
|
51
|
+
if (!existsSync(previewDir)) return null;
|
|
52
|
+
const tarballs = readdirSync(previewDir)
|
|
53
|
+
.filter((entry) => entry.endsWith('.tgz') && entry.includes('tunnel-cli'))
|
|
54
|
+
.map((entry) => {
|
|
55
|
+
const path = join(previewDir, entry);
|
|
56
|
+
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
57
|
+
})
|
|
58
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
59
|
+
return tarballs[0]?.path ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function die(message) {
|
|
63
|
+
console.error(message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|