@qpjoy/electron-launcher 0.2.0

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.
@@ -0,0 +1,902 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { buildHdoRouteProbe, excludeLocalRoutesFromAllowedIps, getDarwinWireGuardLaunchDaemonStatus, getWireGuardTunnelStatus, installDarwinWireGuardLaunchDaemon, localCidrsForAllowedIpExclusion, repairWireGuardTunnelRoutes, renderWireGuardInterface, resolveWireGuardConnectionRuntime, setWireGuardTunnelState, uninstallDarwinWireGuardLaunchDaemon } from '@qpjoy/electron-core-wireguard';
5
+ export function prepareLauncherWireGuardPeer(input) {
6
+ const routePlan = input.routePlan;
7
+ const leaseIp = requiredString(routePlan.leaseIp, 'routePlan.leaseIp');
8
+ const privateKey = requiredString(input.privateKey, 'privateKey');
9
+ const selected = selectLauncherWireGuardPeers(routePlan, input.pathPreference ?? 'auto');
10
+ if (selected.peers.length === 0)
11
+ throw new Error('routePlan WireGuard peer is required');
12
+ const routeCidrs = uniqueStrings(selected.routeCidrs);
13
+ if (routeCidrs.length === 0)
14
+ throw new Error('routePlan.routeCidrs is required');
15
+ const routeProbe = buildHdoRouteProbe({ hdoCidrs: routeCidrs });
16
+ const exclusionCidrs = localCidrsForAllowedIpExclusion(routeProbe, routeCidrs);
17
+ const configPeers = selected.peers.map((peer) => {
18
+ const publicKey = requiredString(peer.publicKey, `${peer.fieldPrefix}PublicKey`);
19
+ const endpoint = requiredString(peer.endpoint, `${peer.fieldPrefix}Endpoint`);
20
+ let allowedIps = excludeLocalRoutesFromAllowedIps(uniqueStrings(peer.routeCidrs), exclusionCidrs);
21
+ if (allowedIps.length === 0 && process.platform === 'win32') {
22
+ allowedIps = uniqueStrings(peer.routeCidrs);
23
+ }
24
+ if (allowedIps.length === 0) {
25
+ if (selected.path === 'h2i-hybrid')
26
+ return null;
27
+ throw new Error(`${peer.name} AllowedIPs 与本机路由完全重叠,已拒绝生成会覆盖本地网络的配置。`);
28
+ }
29
+ return {
30
+ name: peer.name,
31
+ publicKey,
32
+ endpoint,
33
+ allowedIps,
34
+ persistentKeepalive: 25
35
+ };
36
+ }).filter((peer) => Boolean(peer));
37
+ if (configPeers.length === 0) {
38
+ throw new Error('服务端下发的 WireGuard AllowedIPs 与本机路由完全重叠,已拒绝生成会覆盖本地网络的配置。');
39
+ }
40
+ const allowedIps = uniqueStrings(configPeers.flatMap((peer) => peer.allowedIps));
41
+ const routePriorityCidrs = launcherRoutePriorityCidrs(routePlan, routeCidrs);
42
+ const suppressDns = input.suppressWireGuardDns === true;
43
+ const dns = suppressDns ? [] : wireGuardDnsServers(routePlan.dnsServer);
44
+ const splitDns = !suppressDns && Boolean(dns.length && input.dnsDomains?.length);
45
+ const config = renderWireGuardInterface({
46
+ privateKey,
47
+ addresses: [`${leaseIp}/32`],
48
+ dns: splitDns ? undefined : dns,
49
+ hdoDnsServers: splitDns ? dns : undefined,
50
+ hdoDnsDomains: splitDns ? input.dnsDomains : undefined,
51
+ hdoRoutePriorityCidrs: routePriorityCidrs.length ? routePriorityCidrs : undefined,
52
+ suppressInterfaceDns: splitDns,
53
+ mtu: input.mtu,
54
+ peers: configPeers
55
+ });
56
+ const configPath = writeLauncherWireGuardProfile(input, config);
57
+ const endpoints = configPeers.map((peer) => peer.endpoint).filter(Boolean);
58
+ const publicKeys = configPeers.map((peer) => peer.publicKey).filter(Boolean);
59
+ return {
60
+ address: `${leaseIp}/32`,
61
+ allowedIps,
62
+ config,
63
+ configPath,
64
+ dns,
65
+ endpoint: endpoints[0] ?? '',
66
+ endpoints,
67
+ path: selected.path,
68
+ publicKey: publicKeys[0] ?? '',
69
+ publicKeys,
70
+ routeCidrs,
71
+ routePriorityCidrs,
72
+ routeProbe
73
+ };
74
+ }
75
+ function selectLauncherWireGuardPeers(routePlan, preference) {
76
+ const directReady = routePlan.h2iDirectEnabled === true
77
+ && Boolean(routePlan.h2iDirectEndpoint)
78
+ && Boolean(routePlan.h2iDirectPublicKey);
79
+ if (preference === 'direct') {
80
+ return {
81
+ path: 'h2i-direct',
82
+ routeCidrs: routePlan.h2iDirectAllowedIps?.length ? routePlan.h2iDirectAllowedIps : routePlan.routeCidrs,
83
+ peers: [
84
+ {
85
+ name: 'MX H2I Internal Direct',
86
+ fieldPrefix: 'routePlan.h2iDirect',
87
+ endpoint: routePlan.h2iDirectEndpoint,
88
+ publicKey: routePlan.h2iDirectPublicKey,
89
+ routeCidrs: routePlan.h2iDirectAllowedIps?.length ? routePlan.h2iDirectAllowedIps : routePlan.routeCidrs
90
+ }
91
+ ]
92
+ };
93
+ }
94
+ if ((preference === 'auto' || preference === 'hybrid') && directReady) {
95
+ const directCidrs = hybridDirectCidrs(routePlan);
96
+ return {
97
+ path: 'h2i-hybrid',
98
+ routeCidrs: uniqueStrings([...routePlan.routeCidrs, ...directCidrs]),
99
+ peers: [
100
+ {
101
+ name: 'MX H2I Internal Direct',
102
+ fieldPrefix: 'routePlan.h2iDirect',
103
+ endpoint: routePlan.h2iDirectEndpoint,
104
+ publicKey: routePlan.h2iDirectPublicKey,
105
+ routeCidrs: directCidrs
106
+ },
107
+ {
108
+ name: 'MX HDI Domestic Relay',
109
+ fieldPrefix: 'routePlan.domesticRelay',
110
+ endpoint: routePlan.domesticRelayEndpoint,
111
+ publicKey: routePlan.domesticRelayPublicKey,
112
+ routeCidrs: routePlan.routeCidrs
113
+ }
114
+ ]
115
+ };
116
+ }
117
+ return {
118
+ path: 'hdi-relay',
119
+ routeCidrs: routePlan.routeCidrs,
120
+ peers: [
121
+ {
122
+ name: 'MX HDI Domestic Relay',
123
+ fieldPrefix: 'routePlan.domesticRelay',
124
+ endpoint: routePlan.domesticRelayEndpoint,
125
+ publicKey: routePlan.domesticRelayPublicKey,
126
+ routeCidrs: routePlan.routeCidrs
127
+ }
128
+ ]
129
+ };
130
+ }
131
+ function hybridDirectCidrs(routePlan) {
132
+ const directHosts = uniqueStrings([
133
+ routePlan.internalControlIp,
134
+ ipv4HostFromUrl(routePlan.internalBaseUrl)
135
+ ].filter((value) => Boolean(value)));
136
+ return directHosts.length ? directHosts.map((host) => `${host}/32`) : uniqueStrings(routePlan.h2iDirectAllowedIps ?? []);
137
+ }
138
+ function ipv4HostFromUrl(value) {
139
+ if (!value)
140
+ return null;
141
+ try {
142
+ const host = new URL(value).hostname;
143
+ return isIpv4(host) ? host : null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ function launcherRoutePriorityCidrs(routePlan, routeCidrs) {
150
+ const hosts = uniqueStrings([
151
+ routePlan.domesticGatewayIp,
152
+ routePlan.internalControlIp,
153
+ ipv4HostFromUrl(routePlan.internalBaseUrl),
154
+ wireGuardDnsServerHost(routePlan.dnsServer)
155
+ ].filter((value) => Boolean(value && isIpv4(value))));
156
+ return hosts
157
+ .filter((host) => routeCidrs.some((cidr) => cidrContainsHost(cidr, host)))
158
+ .map((host) => `${host}/32`);
159
+ }
160
+ function wireGuardDnsServers(value) {
161
+ const server = wireGuardDnsServerHost(value);
162
+ return server ? [server] : [];
163
+ }
164
+ function wireGuardDnsServerHost(value) {
165
+ const clean = value?.trim();
166
+ if (!clean)
167
+ return null;
168
+ const bracket = clean.match(/^\[([^\]]+)](?::\d{1,5})?$/);
169
+ if (bracket?.[1])
170
+ return bracket[1];
171
+ const ipv4WithPort = clean.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d{1,5}$/);
172
+ if (ipv4WithPort?.[1])
173
+ return ipv4WithPort[1];
174
+ return clean;
175
+ }
176
+ export function resolveLauncherWireGuardRuntime(input) {
177
+ return resolveWireGuardConnectionRuntime({
178
+ installDir: input.installDir ?? join(input.userDataDir, 'bin'),
179
+ bundledDir: input.bundledDir ?? defaultBundledWireGuardDir(),
180
+ allowSystemFallback: input.allowSystemFallback ?? false
181
+ });
182
+ }
183
+ export async function connectLauncherWireGuardPeer(input) {
184
+ const peer = prepareLauncherWireGuardPeer(input);
185
+ const runtime = resolveLauncherWireGuardRuntime(input);
186
+ const action = input.action ?? 'restart';
187
+ if (action !== 'down' && shouldUseDarwinLaunchDaemon(input, runtime)) {
188
+ const launchDaemon = await installDarwinWireGuardLaunchDaemon({
189
+ runtime,
190
+ configPath: peer.configPath,
191
+ serviceIdentity: launcherDarwinServiceIdentity(input)
192
+ });
193
+ const status = await waitForLauncherWireGuardStatus(runtime, peer.configPath);
194
+ const missingRoutes = Array.isArray(status?.missingRoutes) ? status.missingRoutes.length : 0;
195
+ const ok = launchDaemon.ok === true && status?.active === true && missingRoutes === 0;
196
+ if (launchDaemon.ok || isDarwinAuthorizationCancelled(launchDaemon) || input.fallbackToAppManaged === false) {
197
+ return {
198
+ ok,
199
+ action,
200
+ peer,
201
+ runtime,
202
+ status,
203
+ launchDaemon,
204
+ tunnel: {
205
+ ok: launchDaemon.ok,
206
+ configPath: peer.configPath,
207
+ routeLogPath: launchDaemon.routeLogPath,
208
+ routeLogTail: launchDaemon.routeLogTail,
209
+ message: launchDaemon.message
210
+ },
211
+ message: ok ? launchDaemon.message : launcherWireGuardNotReadyMessage(launchDaemon, status)
212
+ };
213
+ }
214
+ }
215
+ const tunnel = await setWireGuardTunnelState({
216
+ runtime,
217
+ configPath: peer.configPath,
218
+ action
219
+ });
220
+ const status = await waitForLauncherWireGuardStatus(runtime, peer.configPath);
221
+ const missingRoutes = Array.isArray(status?.missingRoutes) ? status.missingRoutes.length : 0;
222
+ const ok = tunnel.ok === true && status?.active === true && missingRoutes === 0;
223
+ return {
224
+ ok,
225
+ action,
226
+ peer,
227
+ runtime,
228
+ status,
229
+ tunnel,
230
+ message: ok ? tunnel.message : launcherWireGuardNotReadyMessage(tunnel, status)
231
+ };
232
+ }
233
+ async function waitForLauncherWireGuardStatus(runtime, configPath) {
234
+ const attempts = runtime.platform === 'win32'
235
+ ? 40
236
+ : runtime.platform === 'darwin' && runtime.method === 'darwin-userspace'
237
+ ? 12
238
+ : 1;
239
+ let status = null;
240
+ for (let index = 0; index < attempts; index += 1) {
241
+ status = safeWireGuardStatus(runtime, configPath);
242
+ const missingRoutes = Array.isArray(status?.missingRoutes) ? status.missingRoutes.length : 0;
243
+ if ((status?.active === true && missingRoutes === 0) || index === attempts - 1)
244
+ return status;
245
+ await delay(runtime.platform === 'darwin' && runtime.method === 'darwin-userspace' ? 350 : 500);
246
+ }
247
+ return status;
248
+ }
249
+ function delay(ms) {
250
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
251
+ }
252
+ export async function stopLauncherWireGuardPeer(input) {
253
+ const configPath = launcherWireGuardConfigPath(input);
254
+ if (!existsSync(configPath)) {
255
+ return {
256
+ ok: true,
257
+ skipped: true,
258
+ reason: 'missing-wireguard-config',
259
+ configPath
260
+ };
261
+ }
262
+ const runtime = resolveLauncherWireGuardRuntime(input);
263
+ if (shouldUseDarwinLaunchDaemon(input, runtime)) {
264
+ const launchDaemonStatus = getDarwinWireGuardLaunchDaemonStatus({
265
+ runtime,
266
+ configPath,
267
+ serviceIdentity: launcherDarwinServiceIdentity(input)
268
+ });
269
+ if (launchDaemonStatus.installed || launchDaemonStatus.loaded || launchDaemonStatus.running) {
270
+ const launchDaemon = await uninstallDarwinWireGuardLaunchDaemon({
271
+ runtime,
272
+ configPath,
273
+ serviceIdentity: launcherDarwinServiceIdentity(input)
274
+ });
275
+ const status = safeWireGuardStatus(runtime, configPath);
276
+ return {
277
+ ok: launchDaemon.ok === true,
278
+ skipped: false,
279
+ configPath,
280
+ runtime,
281
+ status,
282
+ launchDaemon,
283
+ tunnel: {
284
+ ok: launchDaemon.ok,
285
+ configPath,
286
+ routeLogPath: launchDaemon.routeLogPath,
287
+ routeLogTail: launchDaemon.routeLogTail,
288
+ message: launchDaemon.message
289
+ },
290
+ message: launchDaemon.message
291
+ };
292
+ }
293
+ }
294
+ const tunnel = await setWireGuardTunnelState({
295
+ runtime,
296
+ configPath,
297
+ action: 'down'
298
+ });
299
+ const status = safeWireGuardStatus(runtime, configPath);
300
+ return {
301
+ ok: tunnel.ok === true,
302
+ skipped: false,
303
+ configPath,
304
+ runtime,
305
+ status,
306
+ tunnel,
307
+ message: tunnel.message
308
+ };
309
+ }
310
+ export async function repairLauncherWireGuardPeerRoutes(input) {
311
+ const configPath = launcherWireGuardConfigPath(input);
312
+ if (!existsSync(configPath)) {
313
+ return {
314
+ ok: true,
315
+ skipped: true,
316
+ reason: 'missing-wireguard-config',
317
+ configPath
318
+ };
319
+ }
320
+ const runtime = resolveLauncherWireGuardRuntime(input);
321
+ const repaired = await repairWireGuardTunnelRoutes({ runtime, configPath });
322
+ const status = safeWireGuardStatus(runtime, configPath);
323
+ const missingRoutes = Array.isArray(status?.missingRoutes) ? status.missingRoutes : [];
324
+ const ok = repaired.ok === true && (status?.active !== true || missingRoutes.length === 0);
325
+ return {
326
+ ...repaired,
327
+ ok,
328
+ configPath,
329
+ runtime,
330
+ status,
331
+ missingRoutes,
332
+ message: ok
333
+ ? repaired.message
334
+ : `${repaired.message || 'WireGuard route repair completed, but route probes are still not ready.'}${missingRoutes.length ? ` missingRoutes=${missingRoutes.join(', ')}` : ''}`
335
+ };
336
+ }
337
+ export async function recoverLauncherWireGuardPeer(input) {
338
+ const configPath = launcherWireGuardConfigPath(input);
339
+ if (!existsSync(configPath)) {
340
+ return {
341
+ ok: true,
342
+ skipped: true,
343
+ reason: 'missing-wireguard-config',
344
+ recoveryReason: input.reason ?? null,
345
+ configPath
346
+ };
347
+ }
348
+ const runtime = resolveLauncherWireGuardRuntime(input);
349
+ const status = safeWireGuardStatus(runtime, configPath);
350
+ const missingRoutes = Array.isArray(status?.missingRoutes) ? status.missingRoutes : [];
351
+ if (status?.active === true && missingRoutes.length === 0) {
352
+ return {
353
+ ok: true,
354
+ skipped: true,
355
+ reason: 'already-active',
356
+ recoveryReason: input.reason ?? null,
357
+ configPath,
358
+ runtime,
359
+ status
360
+ };
361
+ }
362
+ if (status?.active === true && missingRoutes.length > 0) {
363
+ const repaired = await repairLauncherWireGuardPeerRoutes(input);
364
+ return {
365
+ ...repaired,
366
+ action: 'repair-routes',
367
+ recoveryReason: input.reason ?? null
368
+ };
369
+ }
370
+ if (shouldUseDarwinLaunchDaemon(input, runtime)) {
371
+ const launchDaemon = await installDarwinWireGuardLaunchDaemon({
372
+ runtime,
373
+ configPath,
374
+ serviceIdentity: launcherDarwinServiceIdentity(input)
375
+ });
376
+ const nextStatus = await waitForLauncherWireGuardStatus(runtime, configPath);
377
+ const nextMissingRouteCount = Array.isArray(nextStatus?.missingRoutes) ? nextStatus.missingRoutes.length : 0;
378
+ const ok = launchDaemon.ok === true && nextStatus?.active === true && nextMissingRouteCount === 0;
379
+ if (launchDaemon.ok || isDarwinAuthorizationCancelled(launchDaemon) || input.fallbackToAppManaged === false) {
380
+ return {
381
+ ok,
382
+ action: 'launchdaemon-recover',
383
+ recoveryReason: input.reason ?? null,
384
+ configPath,
385
+ runtime,
386
+ status: nextStatus,
387
+ launchDaemon,
388
+ tunnel: {
389
+ ok: launchDaemon.ok,
390
+ configPath,
391
+ routeLogPath: launchDaemon.routeLogPath,
392
+ routeLogTail: launchDaemon.routeLogTail,
393
+ message: launchDaemon.message
394
+ },
395
+ message: ok ? launchDaemon.message : launcherWireGuardNotReadyMessage(launchDaemon, nextStatus)
396
+ };
397
+ }
398
+ }
399
+ const tunnel = await setWireGuardTunnelState({
400
+ runtime,
401
+ configPath,
402
+ action: 'up'
403
+ });
404
+ const nextStatus = await waitForLauncherWireGuardStatus(runtime, configPath);
405
+ const nextMissingRouteCount = Array.isArray(nextStatus?.missingRoutes) ? nextStatus.missingRoutes.length : 0;
406
+ const ok = tunnel.ok === true && nextStatus?.active === true && nextMissingRouteCount === 0;
407
+ return {
408
+ ok,
409
+ action: 'recover-up',
410
+ recoveryReason: input.reason ?? null,
411
+ configPath,
412
+ runtime,
413
+ status: nextStatus,
414
+ tunnel,
415
+ message: ok ? tunnel.message : launcherWireGuardNotReadyMessage(tunnel, nextStatus)
416
+ };
417
+ }
418
+ export function getLauncherWireGuardPeerStatus(input) {
419
+ const configPath = launcherWireGuardConfigPath(input);
420
+ if (!existsSync(configPath)) {
421
+ return {
422
+ ok: false,
423
+ active: false,
424
+ configPath,
425
+ error: 'WireGuard config missing'
426
+ };
427
+ }
428
+ const runtime = resolveLauncherWireGuardRuntime(input);
429
+ const status = safeWireGuardStatus(runtime, configPath);
430
+ if (!shouldUseDarwinLaunchDaemon(input, runtime))
431
+ return status;
432
+ const launchDaemon = getDarwinWireGuardLaunchDaemonStatus({
433
+ runtime,
434
+ configPath,
435
+ serviceIdentity: launcherDarwinServiceIdentity(input)
436
+ });
437
+ return {
438
+ ...(status ?? {
439
+ ok: false,
440
+ active: false,
441
+ configPath,
442
+ error: 'WireGuard status unavailable'
443
+ }),
444
+ launchDaemon
445
+ };
446
+ }
447
+ export function probeLauncherWireGuardRoute(input) {
448
+ const targetIp = requiredString(input.targetIp, 'targetIp');
449
+ const route = readRouteToTarget(targetIp);
450
+ const interfaceName = route.interfaceName;
451
+ const gateway = route.gateway;
452
+ const viaLoopback = interfaceName === 'lo0' || gateway === '127.0.0.1' || gateway === '::1';
453
+ const expected = input.expectedInterfaceName?.trim() || null;
454
+ const expectedAddresses = uniqueStrings(input.expectedInterfaceAddresses ?? [])
455
+ .map((address) => address.split('/')[0]?.trim() ?? '')
456
+ .filter(isIpv4);
457
+ const interfaceMatches = routeInterfaceMatchesExpected(interfaceName, expected, expectedAddresses);
458
+ const viaWireGuard = Boolean(interfaceName && !viaLoopback && interfaceMatches);
459
+ return {
460
+ ok: viaWireGuard,
461
+ targetIp,
462
+ interfaceName,
463
+ gateway,
464
+ viaLoopback,
465
+ expectedInterfaceName: expected,
466
+ raw: route.raw,
467
+ error: route.error
468
+ };
469
+ }
470
+ export function probeLauncherWireGuardEndpoint(input) {
471
+ const endpoint = stringValue(input.endpoint);
472
+ const host = endpointHost(endpoint);
473
+ if (!endpoint || !host) {
474
+ return {
475
+ ok: false,
476
+ endpoint: endpoint ?? null,
477
+ host,
478
+ interfaceName: null,
479
+ gateway: null,
480
+ viaProxyTun: false,
481
+ raw: null,
482
+ error: 'WireGuard endpoint is empty or unsupported'
483
+ };
484
+ }
485
+ if (!isIpv4(host)) {
486
+ return {
487
+ ok: false,
488
+ endpoint,
489
+ host,
490
+ interfaceName: null,
491
+ gateway: null,
492
+ viaProxyTun: false,
493
+ raw: null,
494
+ error: `endpoint host is not an IPv4 address: ${host}`
495
+ };
496
+ }
497
+ const route = readRouteToTarget(host);
498
+ const viaProxyTun = isProxyTunGateway(route.gateway);
499
+ return {
500
+ ok: Boolean(route.interfaceName || route.gateway) && !viaProxyTun && !route.error,
501
+ endpoint,
502
+ host,
503
+ interfaceName: route.interfaceName,
504
+ gateway: route.gateway,
505
+ viaProxyTun,
506
+ raw: route.raw,
507
+ error: route.error ?? (viaProxyTun ? `endpoint route is captured by proxy TUN gateway ${route.gateway}` : null)
508
+ };
509
+ }
510
+ export function launcherWireGuardConfigPath(input) {
511
+ const profileName = sanitizeProfileName(input.profileName ?? 'mx-h2i.conf');
512
+ return join(input.userDataDir, 'wireguard', profileName);
513
+ }
514
+ export function defaultBundledWireGuardDir() {
515
+ const resourcesPath = process.resourcesPath ?? process.cwd();
516
+ const candidates = [
517
+ join(resourcesPath, 'qpjoy-wireguard-engine'),
518
+ join(resourcesPath, 'wireguard'),
519
+ resolve(process.cwd(), 'resources/qpjoy-wireguard-engine'),
520
+ resolve(process.cwd(), 'resources/wireguard')
521
+ ];
522
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
523
+ }
524
+ function writeLauncherWireGuardProfile(input, config) {
525
+ const configPath = launcherWireGuardConfigPath(input);
526
+ mkdirSync(join(input.userDataDir, 'wireguard'), { recursive: true });
527
+ writeFileSync(configPath, config, { mode: 0o600 });
528
+ return configPath;
529
+ }
530
+ function safeWireGuardStatus(runtime, configPath) {
531
+ try {
532
+ return getWireGuardTunnelStatus({ runtime, configPath });
533
+ }
534
+ catch {
535
+ return null;
536
+ }
537
+ }
538
+ function shouldUseDarwinLaunchDaemon(input, runtime) {
539
+ return input.darwinLaunchDaemon !== false
540
+ && runtime.platform === 'darwin'
541
+ && runtime.method === 'darwin-userspace';
542
+ }
543
+ function launcherDarwinServiceIdentity(input) {
544
+ return input.darwinServiceIdentity ?? {
545
+ displayName: 'Electron Launcher WireGuard',
546
+ darwinLaunchDaemonLabelPrefix: 'com.qpjoy.electron-launcher.wireguard',
547
+ darwinSupportRoot: '/Library/Application Support/QPJoy/Electron Launcher',
548
+ darwinLogDir: '/Library/Logs/QPJoy-Electron-Launcher',
549
+ darwinDaemonScriptName: 'electron-launcher-wireguard-daemon.sh',
550
+ staleDarwinLaunchDaemonLabelPrefixes: ['com.qpjoy.electron-launcher.wireguard']
551
+ };
552
+ }
553
+ function isDarwinAuthorizationCancelled(result) {
554
+ const message = stringValue(objectRecord(result).message) || stringValue(objectRecord(result).error) || '';
555
+ return /user canceled|用户已取消|-128/i.test(message);
556
+ }
557
+ function launcherWireGuardNotReadyMessage(tunnel, status) {
558
+ const tunnelRecord = objectRecord(tunnel);
559
+ const statusRecord = objectRecord(status);
560
+ const parts = uniqueStrings([
561
+ stringValue(tunnelRecord.message),
562
+ tunnelRecord.ok === true && statusRecord.active !== true ? 'WireGuard command succeeded but tunnel status is not active yet' : null,
563
+ stringValue(statusRecord.serviceState) ? `service=${stringValue(statusRecord.serviceState)}` : null,
564
+ stringValue(statusRecord.error),
565
+ stringValue(tunnelRecord.error)
566
+ ].filter((item) => Boolean(item)));
567
+ return parts.length ? parts.join('; ') : 'WireGuard tunnel is not active yet';
568
+ }
569
+ function objectRecord(value) {
570
+ return value && typeof value === 'object' ? value : {};
571
+ }
572
+ function stringValue(value) {
573
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
574
+ }
575
+ function endpointHost(value) {
576
+ if (!value)
577
+ return null;
578
+ const trimmed = value.trim();
579
+ const bracket = trimmed.match(/^\[([^\]]+)](?::\d+)?$/);
580
+ if (bracket?.[1])
581
+ return bracket[1];
582
+ if (/^https?:\/\//i.test(trimmed)) {
583
+ try {
584
+ return new URL(trimmed).hostname || null;
585
+ }
586
+ catch {
587
+ return null;
588
+ }
589
+ }
590
+ const parts = trimmed.split(':');
591
+ if (parts.length === 1)
592
+ return parts[0] || null;
593
+ if (parts.length === 2)
594
+ return parts[0] || null;
595
+ return null;
596
+ }
597
+ function isProxyTunGateway(value) {
598
+ const gateway = value ? ipv4ToInt(value) : null;
599
+ const start = ipv4ToInt('198.18.0.0');
600
+ const end = ipv4ToInt('198.19.255.255');
601
+ return gateway !== null && start !== null && end !== null && gateway >= start && gateway <= end;
602
+ }
603
+ function routeInterfaceMatchesExpected(interfaceName, expectedInterfaceName, expectedAddresses) {
604
+ if (!interfaceName)
605
+ return false;
606
+ if (expectedAddresses.includes(interfaceName))
607
+ return true;
608
+ if (!expectedInterfaceName) {
609
+ return expectedAddresses.length > 0
610
+ ? interfaceHasExpectedAddress(interfaceName, expectedAddresses)
611
+ : isWireGuardLikeInterface(interfaceName);
612
+ }
613
+ if (interfaceName === expectedInterfaceName)
614
+ return true;
615
+ if (/^wg/.test(interfaceName) && /^wg/.test(expectedInterfaceName)) {
616
+ return true;
617
+ }
618
+ return interfaceHasExpectedAddress(interfaceName, expectedAddresses);
619
+ }
620
+ function isWireGuardLikeInterface(interfaceName) {
621
+ return /^utun\d+$/.test(interfaceName) || /^wg/.test(interfaceName);
622
+ }
623
+ function interfaceHasExpectedAddress(interfaceName, expectedAddresses) {
624
+ const ips = expectedAddresses
625
+ .map((address) => address.split('/')[0]?.trim() ?? '')
626
+ .filter(isIpv4);
627
+ if (ips.length === 0)
628
+ return false;
629
+ if (process.platform !== 'darwin' && process.platform !== 'linux')
630
+ return false;
631
+ try {
632
+ const raw = execFileSync('ifconfig', [interfaceName], { encoding: 'utf8', timeout: 2000 });
633
+ return ips.some((ip) => raw.includes(`inet ${ip}`));
634
+ }
635
+ catch {
636
+ return false;
637
+ }
638
+ }
639
+ function readRouteToTarget(targetIp) {
640
+ try {
641
+ if (process.platform === 'darwin') {
642
+ const raw = execFileSync('route', ['-n', 'get', targetIp], { encoding: 'utf8', timeout: 2500 });
643
+ return {
644
+ interfaceName: matchRouteField(raw, 'interface'),
645
+ gateway: matchRouteField(raw, 'gateway'),
646
+ raw,
647
+ error: null
648
+ };
649
+ }
650
+ if (process.platform === 'linux') {
651
+ const raw = execFileSync('ip', ['route', 'get', targetIp], { encoding: 'utf8', timeout: 2500 });
652
+ const interfaceName = raw.match(/\bdev\s+(\S+)/)?.[1] ?? null;
653
+ const gateway = raw.match(/\bvia\s+(\S+)/)?.[1] ?? null;
654
+ return { interfaceName, gateway, raw, error: null };
655
+ }
656
+ if (process.platform === 'win32') {
657
+ return readWindowsRouteToTarget(targetIp);
658
+ }
659
+ return {
660
+ interfaceName: null,
661
+ gateway: null,
662
+ raw: null,
663
+ error: `route probe is not implemented on ${process.platform}`
664
+ };
665
+ }
666
+ catch (err) {
667
+ return {
668
+ interfaceName: null,
669
+ gateway: null,
670
+ raw: null,
671
+ error: err instanceof Error ? err.message : String(err)
672
+ };
673
+ }
674
+ }
675
+ function readWindowsRouteToTarget(targetIp) {
676
+ const script = [
677
+ "$ErrorActionPreference = 'Stop'",
678
+ '$ifaces = @{}',
679
+ "Get-NetIPInterface -AddressFamily IPv4 | ForEach-Object { $ifaces[[string]$_.InterfaceIndex] = $_.InterfaceAlias }",
680
+ 'Get-NetRoute -AddressFamily IPv4 | ForEach-Object {',
681
+ ' [pscustomobject]@{',
682
+ ' DestinationPrefix = $_.DestinationPrefix;',
683
+ ' NextHop = $_.NextHop;',
684
+ ' InterfaceIndex = $_.InterfaceIndex;',
685
+ ' InterfaceAlias = $ifaces[[string]$_.InterfaceIndex];',
686
+ ' RouteMetric = $_.RouteMetric;',
687
+ ' InterfaceMetric = $_.InterfaceMetric',
688
+ ' }',
689
+ '} | ConvertTo-Json -Compress'
690
+ ].join('; ');
691
+ try {
692
+ const raw = execFileSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
693
+ encoding: 'utf8',
694
+ timeout: 3500,
695
+ windowsHide: true
696
+ }).trim();
697
+ const rows = normalizeWindowsRouteRows(JSON.parse(raw));
698
+ const best = bestWindowsRouteForTarget(rows, targetIp);
699
+ if (best) {
700
+ return {
701
+ interfaceName: best.interfaceAlias || (best.interfaceIndex ? String(best.interfaceIndex) : null),
702
+ gateway: best.nextHop && best.nextHop !== '0.0.0.0' ? best.nextHop : null,
703
+ raw: JSON.stringify(best),
704
+ error: null
705
+ };
706
+ }
707
+ return {
708
+ interfaceName: null,
709
+ gateway: null,
710
+ raw,
711
+ error: `no Windows route matched ${targetIp}`
712
+ };
713
+ }
714
+ catch (err) {
715
+ return readWindowsRoutePrintToTarget(targetIp, err);
716
+ }
717
+ }
718
+ function readWindowsRoutePrintToTarget(targetIp, previousError) {
719
+ try {
720
+ const raw = execFileSync('route.exe', ['print', '-4'], {
721
+ encoding: 'utf8',
722
+ timeout: 3500,
723
+ windowsHide: true
724
+ });
725
+ const rows = raw.split(/\r?\n/)
726
+ .map((line) => line.trim())
727
+ .map(parseWindowsRoutePrintLine)
728
+ .filter((row) => Boolean(row));
729
+ const best = bestWindowsRouteForTarget(rows, targetIp);
730
+ if (best) {
731
+ return {
732
+ interfaceName: best.interfaceAlias || null,
733
+ gateway: best.nextHop && best.nextHop !== '0.0.0.0' ? best.nextHop : null,
734
+ raw: best.raw || raw,
735
+ error: null
736
+ };
737
+ }
738
+ return {
739
+ interfaceName: null,
740
+ gateway: null,
741
+ raw,
742
+ error: `PowerShell route probe failed (${errorText(previousError)}); route.exe found no route for ${targetIp}`
743
+ };
744
+ }
745
+ catch (err) {
746
+ return {
747
+ interfaceName: null,
748
+ gateway: null,
749
+ raw: null,
750
+ error: `PowerShell route probe failed (${errorText(previousError)}); route.exe failed (${errorText(err)})`
751
+ };
752
+ }
753
+ }
754
+ function normalizeWindowsRouteRows(input) {
755
+ const rows = Array.isArray(input) ? input : input ? [input] : [];
756
+ return rows.map((row) => {
757
+ const record = row && typeof row === 'object' ? row : {};
758
+ return {
759
+ destinationPrefix: stringField(record.DestinationPrefix),
760
+ nextHop: nullableField(record.NextHop),
761
+ interfaceAlias: nullableField(record.InterfaceAlias),
762
+ interfaceIndex: numericField(record.InterfaceIndex),
763
+ routeMetric: numericField(record.RouteMetric) ?? 0,
764
+ interfaceMetric: numericField(record.InterfaceMetric) ?? 0,
765
+ raw: JSON.stringify(record)
766
+ };
767
+ }).filter((row) => Boolean(row.destinationPrefix));
768
+ }
769
+ function parseWindowsRoutePrintLine(line) {
770
+ const columns = line.trim().split(/\s+/);
771
+ if (columns.length < 5 || !isIpv4(columns[0]) || !isIpv4(columns[1]))
772
+ return null;
773
+ const prefix = cidrFromAddressAndMask(columns[0], columns[1]);
774
+ if (!prefix)
775
+ return null;
776
+ return {
777
+ destinationPrefix: prefix,
778
+ nextHop: columns[2] === 'On-link' ? null : columns[2] ?? null,
779
+ interfaceAlias: columns[3] ?? null,
780
+ interfaceIndex: null,
781
+ routeMetric: Number(columns[4]) || 0,
782
+ interfaceMetric: 0,
783
+ raw: line
784
+ };
785
+ }
786
+ function bestWindowsRouteForTarget(rows, targetIp) {
787
+ const target = ipv4ToInt(targetIp);
788
+ if (target === null)
789
+ return null;
790
+ const candidates = rows
791
+ .map((row) => ({ row, parsed: parseCidr(row.destinationPrefix) }))
792
+ .filter((entry) => Boolean(entry.parsed && (target & entry.parsed.mask) === entry.parsed.network))
793
+ .sort((a, b) => {
794
+ if (b.parsed.prefix !== a.parsed.prefix)
795
+ return b.parsed.prefix - a.parsed.prefix;
796
+ return (a.row.routeMetric + a.row.interfaceMetric) - (b.row.routeMetric + b.row.interfaceMetric);
797
+ });
798
+ return candidates[0]?.row ?? null;
799
+ }
800
+ function matchRouteField(raw, field) {
801
+ const match = raw.match(new RegExp(`^\\s*${field}:\\s*(\\S+)`, 'm'));
802
+ return match?.[1] ?? null;
803
+ }
804
+ function requiredString(value, field) {
805
+ const trimmed = value?.trim();
806
+ if (!trimmed)
807
+ throw new Error(`${field} is required`);
808
+ return trimmed;
809
+ }
810
+ function uniqueStrings(values) {
811
+ return [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
812
+ }
813
+ function sanitizeProfileName(value) {
814
+ const trimmed = value.trim() || 'mx-h2i.conf';
815
+ return trimmed.replace(/[/\\]/g, '-');
816
+ }
817
+ function stringField(value) {
818
+ return typeof value === 'string' ? value.trim() : '';
819
+ }
820
+ function nullableField(value) {
821
+ const text = stringField(value);
822
+ return text || null;
823
+ }
824
+ function numericField(value) {
825
+ const number = typeof value === 'number' ? value : Number(value);
826
+ return Number.isFinite(number) ? number : null;
827
+ }
828
+ function errorText(err) {
829
+ return err instanceof Error ? err.message : String(err);
830
+ }
831
+ function isIpv4(value) {
832
+ if (!value)
833
+ return false;
834
+ return ipv4ToInt(value) !== null;
835
+ }
836
+ function ipv4ToInt(value) {
837
+ const parts = value.split('.');
838
+ if (parts.length !== 4)
839
+ return null;
840
+ let out = 0;
841
+ for (const part of parts) {
842
+ if (!/^\d+$/.test(part))
843
+ return null;
844
+ const number = Number(part);
845
+ if (!Number.isInteger(number) || number < 0 || number > 255)
846
+ return null;
847
+ out = ((out << 8) | number) >>> 0;
848
+ }
849
+ return out >>> 0;
850
+ }
851
+ function parseCidr(value) {
852
+ const [address, prefixText] = value.split('/');
853
+ const ip = ipv4ToInt(address ?? '');
854
+ const prefix = Number(prefixText);
855
+ if (ip === null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32)
856
+ return null;
857
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
858
+ return {
859
+ network: (ip & mask) >>> 0,
860
+ prefix,
861
+ mask
862
+ };
863
+ }
864
+ function cidrContainsHost(cidr, host) {
865
+ const parsed = parseCidr(cidr);
866
+ const ip = ipv4ToInt(host);
867
+ if (!parsed || ip === null)
868
+ return false;
869
+ return ((ip & parsed.mask) >>> 0) === parsed.network;
870
+ }
871
+ function cidrFromAddressAndMask(address, maskText) {
872
+ const ip = ipv4ToInt(address);
873
+ const mask = ipv4ToInt(maskText);
874
+ if (ip === null || mask === null)
875
+ return null;
876
+ const prefix = maskToPrefix(mask);
877
+ if (prefix === null)
878
+ return null;
879
+ return `${intToIpv4((ip & mask) >>> 0)}/${prefix}`;
880
+ }
881
+ function maskToPrefix(mask) {
882
+ let prefix = 0;
883
+ let seenZero = false;
884
+ for (let bit = 31; bit >= 0; bit -= 1) {
885
+ const one = Boolean(mask & (1 << bit));
886
+ if (one && seenZero)
887
+ return null;
888
+ if (one)
889
+ prefix += 1;
890
+ else
891
+ seenZero = true;
892
+ }
893
+ return prefix;
894
+ }
895
+ function intToIpv4(value) {
896
+ return [
897
+ (value >>> 24) & 255,
898
+ (value >>> 16) & 255,
899
+ (value >>> 8) & 255,
900
+ value & 255
901
+ ].join('.');
902
+ }