@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.
- package/README.md +164 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +62 -0
- package/dist/network-diagnostics.d.ts +42 -0
- package/dist/network-diagnostics.js +254 -0
- package/dist/network-ownership-registry.d.ts +68 -0
- package/dist/network-ownership-registry.js +286 -0
- package/dist/release-updater.d.ts +129 -0
- package/dist/release-updater.js +195 -0
- package/dist/standalone-data-plane.d.ts +119 -0
- package/dist/standalone-data-plane.js +441 -0
- package/dist/system-domain-proxy.d.ts +91 -0
- package/dist/system-domain-proxy.js +2729 -0
- package/dist/wireguard.d.ts +289 -0
- package/dist/wireguard.js +902 -0
- package/package.json +61 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { LauncherRoutePlan } from '@qpjoy/mx-launcher-core';
|
|
2
|
+
import { type ElectronLauncherNetworkOwnershipClaim, type ElectronLauncherNetworkOwnershipRegistry, type ElectronLauncherNetworkOwnershipRoute } from './network-ownership-registry.js';
|
|
3
|
+
import { connectLauncherWireGuardPeer, stopLauncherWireGuardPeer, type ElectronLauncherWireGuardPathPreference, type ElectronLauncherWireGuardRuntimeOptions } from './wireguard.js';
|
|
4
|
+
export type ElectronLauncherStandaloneDataPlaneState = 'lease-missing' | 'lease-active' | 'data-plane-pending' | 'proxy-tun-captured' | 'ownership-conflict' | 'route-mismatch' | 'service-unreachable' | 'network-ready';
|
|
5
|
+
export type ElectronLauncherStandaloneDataPlaneSeverity = 'ok' | 'info' | 'warning' | 'error';
|
|
6
|
+
export type ElectronLauncherStandaloneDataPlaneProbeTarget = 'lease-ip' | 'internal-control' | 'domestic-gateway' | 'service-vip' | 'dns-server';
|
|
7
|
+
export interface ElectronLauncherStandaloneDataPlaneInput {
|
|
8
|
+
routePlan?: LauncherRoutePlan | null;
|
|
9
|
+
leaseIp?: string | null;
|
|
10
|
+
serviceVip?: string | null;
|
|
11
|
+
dnsServer?: string | null;
|
|
12
|
+
internalControlIp?: string | null;
|
|
13
|
+
domesticGatewayIp?: string | null;
|
|
14
|
+
endpoint?: string | null;
|
|
15
|
+
requiredProbeTargets?: ElectronLauncherStandaloneDataPlaneProbeTarget[] | null;
|
|
16
|
+
expectedInterfaceName?: string | null;
|
|
17
|
+
expectedInterfaceAddresses?: string[] | null;
|
|
18
|
+
wireGuardActive?: boolean | null;
|
|
19
|
+
}
|
|
20
|
+
export interface ElectronLauncherStandaloneOwnershipInput {
|
|
21
|
+
ownerId?: string | null;
|
|
22
|
+
productId?: string | null;
|
|
23
|
+
instanceId?: string | null;
|
|
24
|
+
displayName?: string | null;
|
|
25
|
+
priority?: number | null;
|
|
26
|
+
metadata?: Record<string, unknown> | null;
|
|
27
|
+
dnsHosts?: string[] | null;
|
|
28
|
+
dnsZones?: string[] | null;
|
|
29
|
+
routeCidrs?: string[] | null;
|
|
30
|
+
reverseProxyRoutes?: ElectronLauncherNetworkOwnershipRoute[] | null;
|
|
31
|
+
existingClaims?: ElectronLauncherNetworkOwnershipClaim[] | null;
|
|
32
|
+
}
|
|
33
|
+
export interface ElectronLauncherStandaloneOwnershipState {
|
|
34
|
+
statePath: string;
|
|
35
|
+
claims: ElectronLauncherNetworkOwnershipClaim[];
|
|
36
|
+
registry: ElectronLauncherNetworkOwnershipRegistry;
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ElectronLauncherStandaloneDataPlaneApplyInput extends ElectronLauncherWireGuardRuntimeOptions, ElectronLauncherStandaloneOwnershipInput {
|
|
40
|
+
routePlan: LauncherRoutePlan;
|
|
41
|
+
privateKey: string;
|
|
42
|
+
dnsDomains?: string[] | null;
|
|
43
|
+
suppressWireGuardDns?: boolean | null;
|
|
44
|
+
mtu?: number | null;
|
|
45
|
+
pathPreference?: ElectronLauncherWireGuardPathPreference;
|
|
46
|
+
action?: 'up' | 'restart';
|
|
47
|
+
requiredProbeTargets?: ElectronLauncherStandaloneDataPlaneProbeTarget[] | null;
|
|
48
|
+
failOnOwnershipConflicts?: boolean | null;
|
|
49
|
+
dataPlaneProbeAttempts?: number | null;
|
|
50
|
+
dataPlaneProbeIntervalMs?: number | null;
|
|
51
|
+
ownershipStatePath?: string | null;
|
|
52
|
+
skipOwnershipState?: boolean | null;
|
|
53
|
+
}
|
|
54
|
+
export interface ElectronLauncherStandaloneDataPlaneApplyResult {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
state: ElectronLauncherStandaloneDataPlaneState;
|
|
57
|
+
diagnostics: ElectronLauncherStandaloneDataPlaneDiagnostics;
|
|
58
|
+
ownershipClaim: ElectronLauncherNetworkOwnershipClaim;
|
|
59
|
+
ownershipRegistry: ElectronLauncherNetworkOwnershipRegistry;
|
|
60
|
+
wireGuard: Awaited<ReturnType<typeof connectLauncherWireGuardPeer>> | null;
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ElectronLauncherStandaloneDataPlaneStopInput extends ElectronLauncherWireGuardRuntimeOptions {
|
|
64
|
+
routePlan?: LauncherRoutePlan | null;
|
|
65
|
+
ownerId?: string | null;
|
|
66
|
+
ownershipStatePath?: string | null;
|
|
67
|
+
skipOwnershipState?: boolean | null;
|
|
68
|
+
}
|
|
69
|
+
export interface ElectronLauncherStandaloneDataPlaneStopResult {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
wireGuard: Awaited<ReturnType<typeof stopLauncherWireGuardPeer>>;
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
export interface ElectronLauncherStandaloneRouteProbe {
|
|
75
|
+
target: ElectronLauncherStandaloneDataPlaneProbeTarget;
|
|
76
|
+
label: string;
|
|
77
|
+
address: string;
|
|
78
|
+
required: boolean;
|
|
79
|
+
ok: boolean;
|
|
80
|
+
viaWireGuard: boolean;
|
|
81
|
+
viaLoopback: boolean;
|
|
82
|
+
viaProxyTun: boolean;
|
|
83
|
+
interfaceName: string | null;
|
|
84
|
+
gateway: string | null;
|
|
85
|
+
expectedInterfaceName: string | null;
|
|
86
|
+
error: string | null;
|
|
87
|
+
raw: string | null;
|
|
88
|
+
}
|
|
89
|
+
export interface ElectronLauncherStandaloneEndpointProbe {
|
|
90
|
+
endpoint: string;
|
|
91
|
+
host: string | null;
|
|
92
|
+
ok: boolean;
|
|
93
|
+
viaProxyTun: boolean;
|
|
94
|
+
interfaceName: string | null;
|
|
95
|
+
gateway: string | null;
|
|
96
|
+
error: string | null;
|
|
97
|
+
}
|
|
98
|
+
export interface ElectronLauncherStandaloneDataPlaneDiagnostics {
|
|
99
|
+
ok: boolean;
|
|
100
|
+
state: ElectronLauncherStandaloneDataPlaneState;
|
|
101
|
+
severity: ElectronLauncherStandaloneDataPlaneSeverity;
|
|
102
|
+
message: string;
|
|
103
|
+
productId: string | null;
|
|
104
|
+
leaseIp: string | null;
|
|
105
|
+
serviceVip: string | null;
|
|
106
|
+
dnsServer: string | null;
|
|
107
|
+
internalControlIp: string | null;
|
|
108
|
+
domesticGatewayIp: string | null;
|
|
109
|
+
probes: ElectronLauncherStandaloneRouteProbe[];
|
|
110
|
+
endpoint: ElectronLauncherStandaloneEndpointProbe | null;
|
|
111
|
+
updatedAt: string;
|
|
112
|
+
}
|
|
113
|
+
export declare function diagnoseElectronLauncherStandaloneDataPlane(input: ElectronLauncherStandaloneDataPlaneInput): ElectronLauncherStandaloneDataPlaneDiagnostics;
|
|
114
|
+
export declare function applyElectronLauncherStandaloneDataPlane(input: ElectronLauncherStandaloneDataPlaneApplyInput): Promise<ElectronLauncherStandaloneDataPlaneApplyResult>;
|
|
115
|
+
export declare function stopElectronLauncherStandaloneDataPlane(input: ElectronLauncherStandaloneDataPlaneStopInput): Promise<ElectronLauncherStandaloneDataPlaneStopResult>;
|
|
116
|
+
export declare function readElectronLauncherStandaloneOwnershipState(statePath?: string | null): ElectronLauncherStandaloneOwnershipState;
|
|
117
|
+
export declare function upsertElectronLauncherStandaloneOwnershipClaim(claim: ElectronLauncherNetworkOwnershipClaim, statePath?: string | null): ElectronLauncherStandaloneOwnershipState;
|
|
118
|
+
export declare function releaseElectronLauncherStandaloneOwnershipClaim(ownerId: string, statePath?: string | null): ElectronLauncherStandaloneOwnershipState;
|
|
119
|
+
export declare function buildElectronLauncherStandaloneOwnershipClaim(routePlan: LauncherRoutePlan, input?: ElectronLauncherStandaloneOwnershipInput): ElectronLauncherNetworkOwnershipClaim;
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { isIP } from 'node:net';
|
|
5
|
+
import { classifyLauncherIpv4Address } from './network-diagnostics.js';
|
|
6
|
+
import { buildElectronLauncherNetworkOwnershipRegistry } from './network-ownership-registry.js';
|
|
7
|
+
import { connectLauncherWireGuardPeer, getLauncherWireGuardPeerStatus, stopLauncherWireGuardPeer, probeLauncherWireGuardEndpoint, probeLauncherWireGuardRoute } from './wireguard.js';
|
|
8
|
+
export function diagnoseElectronLauncherStandaloneDataPlane(input) {
|
|
9
|
+
const routePlan = input.routePlan ?? null;
|
|
10
|
+
const leaseIp = ipv4OrNull(input.leaseIp) ?? ipv4OrNull(routePlan?.leaseIp);
|
|
11
|
+
const serviceVip = ipv4OrNull(input.serviceVip) ?? ipv4OrNull(routePlan?.serviceVip);
|
|
12
|
+
const dnsServer = dnsServerHost(input.dnsServer) ?? dnsServerHost(routePlan?.dnsServer);
|
|
13
|
+
const internalControlIp = ipv4OrNull(input.internalControlIp) ?? ipv4OrNull(routePlan?.internalControlIp);
|
|
14
|
+
const domesticGatewayIp = ipv4OrNull(input.domesticGatewayIp) ?? ipv4OrNull(routePlan?.domesticGatewayIp);
|
|
15
|
+
const endpoint = stringValue(input.endpoint)
|
|
16
|
+
?? stringValue(routePlan?.domesticRelayEndpoint)
|
|
17
|
+
?? stringValue(routePlan?.h2iDirectEndpoint);
|
|
18
|
+
if (!leaseIp && !routePlan) {
|
|
19
|
+
return buildDiagnostics({
|
|
20
|
+
state: 'lease-missing',
|
|
21
|
+
severity: 'info',
|
|
22
|
+
message: 'Launcher lease has not been issued yet.',
|
|
23
|
+
productId: null,
|
|
24
|
+
leaseIp,
|
|
25
|
+
serviceVip,
|
|
26
|
+
dnsServer,
|
|
27
|
+
internalControlIp,
|
|
28
|
+
domesticGatewayIp,
|
|
29
|
+
probes: [],
|
|
30
|
+
endpoint: null
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const expectedInterfaceName = stringValue(input.expectedInterfaceName);
|
|
34
|
+
const expectedInterfaceAddresses = input.expectedInterfaceAddresses ?? (leaseIp ? [leaseIp] : []);
|
|
35
|
+
const requiredTargets = input.requiredProbeTargets
|
|
36
|
+
? new Set(input.requiredProbeTargets)
|
|
37
|
+
: null;
|
|
38
|
+
const requiredProbe = (target, address) => Boolean(address) && (!requiredTargets || requiredTargets.has(target));
|
|
39
|
+
const probes = [
|
|
40
|
+
probeRouteTarget({
|
|
41
|
+
target: 'lease-ip',
|
|
42
|
+
label: 'Lease IP self route',
|
|
43
|
+
address: leaseIp,
|
|
44
|
+
required: requiredProbe('lease-ip', leaseIp)
|
|
45
|
+
}, expectedInterfaceName, expectedInterfaceAddresses),
|
|
46
|
+
probeRouteTarget({
|
|
47
|
+
target: 'internal-control',
|
|
48
|
+
label: 'Internal control plane',
|
|
49
|
+
address: internalControlIp,
|
|
50
|
+
required: requiredProbe('internal-control', internalControlIp)
|
|
51
|
+
}, expectedInterfaceName, expectedInterfaceAddresses),
|
|
52
|
+
probeRouteTarget({
|
|
53
|
+
target: 'domestic-gateway',
|
|
54
|
+
label: 'Domestic DNS relay',
|
|
55
|
+
address: domesticGatewayIp,
|
|
56
|
+
required: requiredProbe('domestic-gateway', domesticGatewayIp)
|
|
57
|
+
}, expectedInterfaceName, expectedInterfaceAddresses),
|
|
58
|
+
probeRouteTarget({
|
|
59
|
+
target: 'service-vip',
|
|
60
|
+
label: 'Product service VIP',
|
|
61
|
+
address: serviceVip,
|
|
62
|
+
required: requiredProbe('service-vip', serviceVip)
|
|
63
|
+
}, expectedInterfaceName, expectedInterfaceAddresses),
|
|
64
|
+
probeRouteTarget({
|
|
65
|
+
target: 'dns-server',
|
|
66
|
+
label: 'WireGuard DNS server',
|
|
67
|
+
address: dnsServer,
|
|
68
|
+
required: requiredProbe('dns-server', dnsServer)
|
|
69
|
+
}, expectedInterfaceName, expectedInterfaceAddresses)
|
|
70
|
+
].filter((probe) => Boolean(probe));
|
|
71
|
+
const endpointProbe = endpoint ? normalizeEndpointProbe(probeLauncherWireGuardEndpoint({ endpoint })) : null;
|
|
72
|
+
const requiredProbes = probes.filter((probe) => probe.required);
|
|
73
|
+
const failedProbes = requiredProbes.filter((probe) => !probe.ok);
|
|
74
|
+
const proxyProbe = requiredProbes.find((probe) => probe.viaProxyTun) ?? null;
|
|
75
|
+
const endpointProxyCaptured = endpointProbe?.viaProxyTun === true;
|
|
76
|
+
const wireGuardActive = input.wireGuardActive === true;
|
|
77
|
+
const state = dataPlaneState({
|
|
78
|
+
leaseIp,
|
|
79
|
+
failedProbes,
|
|
80
|
+
proxyCaptured: Boolean(proxyProbe || endpointProxyCaptured),
|
|
81
|
+
wireGuardActive
|
|
82
|
+
});
|
|
83
|
+
return buildDiagnostics({
|
|
84
|
+
state,
|
|
85
|
+
severity: dataPlaneSeverity(state),
|
|
86
|
+
message: dataPlaneMessage({
|
|
87
|
+
state,
|
|
88
|
+
proxyProbe,
|
|
89
|
+
endpointProbe,
|
|
90
|
+
failedProbes
|
|
91
|
+
}),
|
|
92
|
+
productId: stringValue(routePlan?.productId),
|
|
93
|
+
leaseIp,
|
|
94
|
+
serviceVip,
|
|
95
|
+
dnsServer,
|
|
96
|
+
internalControlIp,
|
|
97
|
+
domesticGatewayIp,
|
|
98
|
+
probes,
|
|
99
|
+
endpoint: endpointProbe
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export async function applyElectronLauncherStandaloneDataPlane(input) {
|
|
103
|
+
const ownershipClaim = buildElectronLauncherStandaloneOwnershipClaim(input.routePlan, input);
|
|
104
|
+
const stateClaims = input.skipOwnershipState === true ? [] : readStandaloneOwnershipClaims(input.ownershipStatePath);
|
|
105
|
+
const ownershipRegistry = buildElectronLauncherNetworkOwnershipRegistry([
|
|
106
|
+
...stateClaims.filter((claim) => claim.ownerId !== ownershipClaim.ownerId),
|
|
107
|
+
...(input.existingClaims ?? []),
|
|
108
|
+
ownershipClaim
|
|
109
|
+
]);
|
|
110
|
+
if (ownershipRegistry.conflicts.length > 0 && input.failOnOwnershipConflicts !== false) {
|
|
111
|
+
const diagnostics = ownershipConflictDiagnostics(input.routePlan, ownershipRegistry);
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
state: diagnostics.state,
|
|
115
|
+
diagnostics,
|
|
116
|
+
ownershipClaim,
|
|
117
|
+
ownershipRegistry,
|
|
118
|
+
wireGuard: null,
|
|
119
|
+
message: diagnostics.message
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (input.skipOwnershipState !== true) {
|
|
123
|
+
writeStandaloneOwnershipClaim(input.ownershipStatePath, ownershipClaim);
|
|
124
|
+
}
|
|
125
|
+
const wireGuard = await connectLauncherWireGuardPeer({
|
|
126
|
+
...input,
|
|
127
|
+
action: input.action ?? 'restart',
|
|
128
|
+
dnsDomains: input.dnsDomains ?? [],
|
|
129
|
+
suppressWireGuardDns: input.suppressWireGuardDns,
|
|
130
|
+
pathPreference: input.pathPreference
|
|
131
|
+
});
|
|
132
|
+
const diagnostics = await waitForStandaloneDataPlaneDiagnostics(input, wireGuard);
|
|
133
|
+
const finalOwnershipClaim = {
|
|
134
|
+
...ownershipClaim,
|
|
135
|
+
state: diagnostics.ok ? 'active' : 'connecting',
|
|
136
|
+
updatedAt: new Date().toISOString()
|
|
137
|
+
};
|
|
138
|
+
if (input.skipOwnershipState !== true) {
|
|
139
|
+
writeStandaloneOwnershipClaim(input.ownershipStatePath, finalOwnershipClaim);
|
|
140
|
+
}
|
|
141
|
+
const finalOwnershipRegistry = buildElectronLauncherNetworkOwnershipRegistry([
|
|
142
|
+
...stateClaims.filter((claim) => claim.ownerId !== ownershipClaim.ownerId),
|
|
143
|
+
...(input.existingClaims ?? []),
|
|
144
|
+
finalOwnershipClaim
|
|
145
|
+
]);
|
|
146
|
+
return {
|
|
147
|
+
ok: wireGuard.ok === true && diagnostics.ok,
|
|
148
|
+
state: diagnostics.state,
|
|
149
|
+
diagnostics,
|
|
150
|
+
ownershipClaim: finalOwnershipClaim,
|
|
151
|
+
ownershipRegistry: finalOwnershipRegistry,
|
|
152
|
+
wireGuard,
|
|
153
|
+
message: diagnostics.ok ? 'Launcher standalone data plane is ready.' : diagnostics.message
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export async function stopElectronLauncherStandaloneDataPlane(input) {
|
|
157
|
+
const wireGuard = await stopLauncherWireGuardPeer(input);
|
|
158
|
+
if (input.skipOwnershipState !== true && input.ownerId) {
|
|
159
|
+
releaseStandaloneOwnershipClaim(input.ownershipStatePath, input.ownerId);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
ok: wireGuard.ok === true,
|
|
163
|
+
wireGuard,
|
|
164
|
+
message: wireGuard.message ?? (wireGuard.ok ? 'Launcher standalone data plane stopped.' : 'Launcher standalone data plane stop failed.')
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function readElectronLauncherStandaloneOwnershipState(statePath) {
|
|
168
|
+
const file = statePath || defaultStandaloneOwnershipStatePath();
|
|
169
|
+
const claims = readStandaloneOwnershipClaims(file);
|
|
170
|
+
return {
|
|
171
|
+
statePath: file,
|
|
172
|
+
claims,
|
|
173
|
+
registry: buildElectronLauncherNetworkOwnershipRegistry(claims),
|
|
174
|
+
updatedAt: new Date().toISOString()
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function upsertElectronLauncherStandaloneOwnershipClaim(claim, statePath) {
|
|
178
|
+
writeStandaloneOwnershipClaim(statePath, claim);
|
|
179
|
+
return readElectronLauncherStandaloneOwnershipState(statePath);
|
|
180
|
+
}
|
|
181
|
+
export function releaseElectronLauncherStandaloneOwnershipClaim(ownerId, statePath) {
|
|
182
|
+
releaseStandaloneOwnershipClaim(statePath, ownerId);
|
|
183
|
+
return readElectronLauncherStandaloneOwnershipState(statePath);
|
|
184
|
+
}
|
|
185
|
+
export function buildElectronLauncherStandaloneOwnershipClaim(routePlan, input = {}) {
|
|
186
|
+
const productId = stringValue(input.productId) ?? stringValue(routePlan.productId) ?? 'launcher';
|
|
187
|
+
const instanceId = stringValue(input.instanceId) ?? stringValue(routePlan.leaseIp);
|
|
188
|
+
const ownerId = stringValue(input.ownerId) ?? `${productId}:${instanceId ?? 'local'}`;
|
|
189
|
+
const dnsHosts = uniqueStrings(input.dnsHosts ?? []);
|
|
190
|
+
const dnsZones = uniqueStrings(input.dnsZones ?? []);
|
|
191
|
+
const routeCidrs = uniqueStrings(input.routeCidrs ?? []);
|
|
192
|
+
const productRouteCidrs = routeCidrs.length
|
|
193
|
+
? routeCidrs
|
|
194
|
+
: routePlan.leaseCidr
|
|
195
|
+
? [routePlan.leaseCidr]
|
|
196
|
+
: uniqueStrings(routePlan.routeCidrs ?? []);
|
|
197
|
+
return {
|
|
198
|
+
ownerId,
|
|
199
|
+
productId,
|
|
200
|
+
instanceId,
|
|
201
|
+
displayName: stringValue(input.displayName) ?? productId,
|
|
202
|
+
state: 'connecting',
|
|
203
|
+
priority: normalizePriority(input.priority),
|
|
204
|
+
leaseIp: ipv4OrNull(routePlan.leaseIp),
|
|
205
|
+
gatewayIp: ipv4OrNull(routePlan.internalControlIp) ?? ipv4OrNull(routePlan.domesticGatewayIp),
|
|
206
|
+
dnsHosts,
|
|
207
|
+
dnsZones,
|
|
208
|
+
routeCidrs: productRouteCidrs,
|
|
209
|
+
reverseProxyRoutes: input.reverseProxyRoutes ?? [],
|
|
210
|
+
metadata: {
|
|
211
|
+
launcherMode: routePlan.launcherMode,
|
|
212
|
+
serviceVip: routePlan.serviceVip,
|
|
213
|
+
snapshotId: routePlan.snapshotId,
|
|
214
|
+
dnsServer: routePlan.dnsServer,
|
|
215
|
+
...(input.metadata ?? {})
|
|
216
|
+
},
|
|
217
|
+
updatedAt: new Date().toISOString()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function probeRouteTarget(target, expectedInterfaceName, expectedInterfaceAddresses) {
|
|
221
|
+
const address = ipv4OrNull(target.address);
|
|
222
|
+
if (!address)
|
|
223
|
+
return null;
|
|
224
|
+
const route = probeLauncherWireGuardRoute({
|
|
225
|
+
targetIp: address,
|
|
226
|
+
expectedInterfaceName,
|
|
227
|
+
expectedInterfaceAddresses
|
|
228
|
+
});
|
|
229
|
+
const viaProxyTun = isProxyTunAddress(route.gateway);
|
|
230
|
+
const selfRouteOk = target.target === 'lease-ip' && route.viaLoopback && !viaProxyTun && !route.error;
|
|
231
|
+
return {
|
|
232
|
+
target: target.target,
|
|
233
|
+
label: target.label,
|
|
234
|
+
address,
|
|
235
|
+
required: target.required,
|
|
236
|
+
ok: selfRouteOk || (route.ok && !viaProxyTun && !route.error),
|
|
237
|
+
viaWireGuard: route.ok,
|
|
238
|
+
viaLoopback: route.viaLoopback,
|
|
239
|
+
viaProxyTun,
|
|
240
|
+
interfaceName: route.interfaceName,
|
|
241
|
+
gateway: route.gateway,
|
|
242
|
+
expectedInterfaceName: route.expectedInterfaceName,
|
|
243
|
+
error: route.error,
|
|
244
|
+
raw: route.raw
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function normalizeEndpointProbe(probe) {
|
|
248
|
+
return {
|
|
249
|
+
endpoint: probe.endpoint ?? '',
|
|
250
|
+
host: probe.host,
|
|
251
|
+
ok: probe.ok,
|
|
252
|
+
viaProxyTun: probe.viaProxyTun,
|
|
253
|
+
interfaceName: probe.interfaceName,
|
|
254
|
+
gateway: probe.gateway,
|
|
255
|
+
error: probe.error
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function dataPlaneState(input) {
|
|
259
|
+
if (!input.leaseIp)
|
|
260
|
+
return 'lease-missing';
|
|
261
|
+
if (input.proxyCaptured)
|
|
262
|
+
return 'proxy-tun-captured';
|
|
263
|
+
if (input.failedProbes.length === 0)
|
|
264
|
+
return 'network-ready';
|
|
265
|
+
return input.wireGuardActive ? 'route-mismatch' : 'data-plane-pending';
|
|
266
|
+
}
|
|
267
|
+
function dataPlaneSeverity(state) {
|
|
268
|
+
if (state === 'network-ready')
|
|
269
|
+
return 'ok';
|
|
270
|
+
if (state === 'lease-missing' || state === 'lease-active')
|
|
271
|
+
return 'info';
|
|
272
|
+
if (state === 'ownership-conflict' || state === 'proxy-tun-captured' || state === 'route-mismatch' || state === 'service-unreachable')
|
|
273
|
+
return 'error';
|
|
274
|
+
return 'warning';
|
|
275
|
+
}
|
|
276
|
+
function dataPlaneMessage(input) {
|
|
277
|
+
if (input.state === 'lease-missing')
|
|
278
|
+
return 'Launcher lease has not been issued yet.';
|
|
279
|
+
if (input.state === 'network-ready') {
|
|
280
|
+
return 'Launcher lease and local data-plane routes are both ready.';
|
|
281
|
+
}
|
|
282
|
+
if (input.state === 'ownership-conflict') {
|
|
283
|
+
return 'Another launcher owner already claims the same DNS, route, or reverse-proxy resource.';
|
|
284
|
+
}
|
|
285
|
+
if (input.state === 'service-unreachable') {
|
|
286
|
+
return 'Launcher route proof is ready, but the product service VIP is not reachable yet.';
|
|
287
|
+
}
|
|
288
|
+
if (input.state === 'proxy-tun-captured') {
|
|
289
|
+
if (input.proxyProbe) {
|
|
290
|
+
return `${input.proxyProbe.label} is captured by proxy TUN gateway ${input.proxyProbe.gateway ?? 'unknown'}; keep Launcher internal CIDRs on the WireGuard/direct route.`;
|
|
291
|
+
}
|
|
292
|
+
return `WireGuard endpoint is captured by proxy TUN gateway ${input.endpointProbe?.gateway ?? 'unknown'}; endpoint traffic must stay outside Clash/mihomo TUN.`;
|
|
293
|
+
}
|
|
294
|
+
const names = input.failedProbes.map((probe) => probe.label).join(', ');
|
|
295
|
+
if (input.state === 'route-mismatch') {
|
|
296
|
+
return `WireGuard is active but route proof is not ready: ${names || 'no required routes'}.`;
|
|
297
|
+
}
|
|
298
|
+
return `Launcher lease is active, but privileged WireGuard/data-plane apply is still pending: ${names || 'no route proof'}.`;
|
|
299
|
+
}
|
|
300
|
+
async function waitForStandaloneDataPlaneDiagnostics(input, wireGuard) {
|
|
301
|
+
const attempts = Math.max(1, Math.min(30, Math.floor(input.dataPlaneProbeAttempts ?? 12)));
|
|
302
|
+
const intervalMs = Math.max(250, Math.min(5000, Math.floor(input.dataPlaneProbeIntervalMs ?? 1000)));
|
|
303
|
+
let diagnostics = standaloneDiagnosticsForWireGuard(input, wireGuard);
|
|
304
|
+
for (let index = 1; index < attempts && !diagnostics.ok; index += 1) {
|
|
305
|
+
await delay(intervalMs);
|
|
306
|
+
const status = getLauncherWireGuardPeerStatus(input);
|
|
307
|
+
diagnostics = standaloneDiagnosticsForWireGuard(input, {
|
|
308
|
+
...wireGuard,
|
|
309
|
+
ok: wireGuard.ok === true || status?.active === true,
|
|
310
|
+
status
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return diagnostics;
|
|
314
|
+
}
|
|
315
|
+
function standaloneDiagnosticsForWireGuard(input, wireGuard) {
|
|
316
|
+
const status = objectRecord(wireGuard.status);
|
|
317
|
+
return diagnoseElectronLauncherStandaloneDataPlane({
|
|
318
|
+
routePlan: input.routePlan,
|
|
319
|
+
leaseIp: input.routePlan.leaseIp,
|
|
320
|
+
serviceVip: input.routePlan.serviceVip,
|
|
321
|
+
dnsServer: input.routePlan.dnsServer,
|
|
322
|
+
endpoint: wireGuard.peer?.endpoint,
|
|
323
|
+
requiredProbeTargets: input.requiredProbeTargets,
|
|
324
|
+
expectedInterfaceName: stringValue(status?.realInterfaceName) || stringValue(status?.interfaceName),
|
|
325
|
+
expectedInterfaceAddresses: Array.isArray(status?.addresses) ? status.addresses.filter((item) => typeof item === 'string') : [input.routePlan.leaseIp],
|
|
326
|
+
wireGuardActive: wireGuard.ok === true || status?.active === true
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function ownershipConflictDiagnostics(routePlan, registry) {
|
|
330
|
+
const conflict = registry.conflicts[0];
|
|
331
|
+
return buildDiagnostics({
|
|
332
|
+
state: 'ownership-conflict',
|
|
333
|
+
severity: 'error',
|
|
334
|
+
message: conflict
|
|
335
|
+
? `Launcher ownership conflict on ${conflict.resource}:${conflict.key} (${conflict.reason}).`
|
|
336
|
+
: 'Launcher ownership conflict detected.',
|
|
337
|
+
productId: stringValue(routePlan.productId),
|
|
338
|
+
leaseIp: ipv4OrNull(routePlan.leaseIp),
|
|
339
|
+
serviceVip: ipv4OrNull(routePlan.serviceVip),
|
|
340
|
+
dnsServer: dnsServerHost(routePlan.dnsServer),
|
|
341
|
+
internalControlIp: ipv4OrNull(routePlan.internalControlIp),
|
|
342
|
+
domesticGatewayIp: ipv4OrNull(routePlan.domesticGatewayIp),
|
|
343
|
+
probes: [],
|
|
344
|
+
endpoint: null
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function defaultStandaloneOwnershipStatePath() {
|
|
348
|
+
if (process.platform === 'darwin') {
|
|
349
|
+
return join(homedir(), 'Library', 'Application Support', 'QPJoy', 'Electron Launcher', 'standalone-ownership.json');
|
|
350
|
+
}
|
|
351
|
+
if (process.platform === 'win32') {
|
|
352
|
+
return join(process.env.APPDATA || homedir(), 'QPJoy', 'Electron Launcher', 'standalone-ownership.json');
|
|
353
|
+
}
|
|
354
|
+
return join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'qpjoy-electron-launcher', 'standalone-ownership.json');
|
|
355
|
+
}
|
|
356
|
+
function readStandaloneOwnershipClaims(statePath) {
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(readFileSync(statePath || defaultStandaloneOwnershipStatePath(), 'utf8'));
|
|
359
|
+
return Array.isArray(parsed.claims)
|
|
360
|
+
? parsed.claims.filter((claim) => Boolean(claim?.ownerId))
|
|
361
|
+
: [];
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function writeStandaloneOwnershipClaim(statePath, claim) {
|
|
368
|
+
const file = statePath || defaultStandaloneOwnershipStatePath();
|
|
369
|
+
const claims = [
|
|
370
|
+
...readStandaloneOwnershipClaims(file).filter((row) => row.ownerId !== claim.ownerId),
|
|
371
|
+
claim
|
|
372
|
+
];
|
|
373
|
+
writeStandaloneOwnershipState(file, claims);
|
|
374
|
+
}
|
|
375
|
+
function releaseStandaloneOwnershipClaim(statePath, ownerId) {
|
|
376
|
+
const file = statePath || defaultStandaloneOwnershipStatePath();
|
|
377
|
+
const claims = readStandaloneOwnershipClaims(file).filter((row) => row.ownerId !== ownerId);
|
|
378
|
+
writeStandaloneOwnershipState(file, claims);
|
|
379
|
+
}
|
|
380
|
+
function writeStandaloneOwnershipState(file, claims) {
|
|
381
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
382
|
+
writeFileSync(file, JSON.stringify({
|
|
383
|
+
version: 1,
|
|
384
|
+
claims,
|
|
385
|
+
updatedAt: new Date().toISOString()
|
|
386
|
+
}, null, 2), { mode: 0o600 });
|
|
387
|
+
}
|
|
388
|
+
function delay(ms) {
|
|
389
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
390
|
+
}
|
|
391
|
+
function buildDiagnostics(input) {
|
|
392
|
+
return {
|
|
393
|
+
ok: input.state === 'network-ready',
|
|
394
|
+
state: input.state,
|
|
395
|
+
severity: input.severity,
|
|
396
|
+
message: input.message,
|
|
397
|
+
productId: input.productId,
|
|
398
|
+
leaseIp: input.leaseIp,
|
|
399
|
+
serviceVip: input.serviceVip,
|
|
400
|
+
dnsServer: input.dnsServer,
|
|
401
|
+
internalControlIp: input.internalControlIp,
|
|
402
|
+
domesticGatewayIp: input.domesticGatewayIp,
|
|
403
|
+
probes: input.probes,
|
|
404
|
+
endpoint: input.endpoint,
|
|
405
|
+
updatedAt: new Date().toISOString()
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function isProxyTunAddress(value) {
|
|
409
|
+
return Boolean(value && classifyLauncherIpv4Address(value) === 'proxy-fake-ip');
|
|
410
|
+
}
|
|
411
|
+
function normalizePriority(value) {
|
|
412
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
413
|
+
return Number.isFinite(number) ? Math.trunc(number) : 100;
|
|
414
|
+
}
|
|
415
|
+
function uniqueStrings(values) {
|
|
416
|
+
return values
|
|
417
|
+
.map((value) => String(value || '').trim())
|
|
418
|
+
.filter((value, index, rows) => Boolean(value) && rows.indexOf(value) === index);
|
|
419
|
+
}
|
|
420
|
+
function objectRecord(value) {
|
|
421
|
+
return value && typeof value === 'object' ? value : {};
|
|
422
|
+
}
|
|
423
|
+
function dnsServerHost(value) {
|
|
424
|
+
const clean = stringValue(value);
|
|
425
|
+
if (!clean)
|
|
426
|
+
return null;
|
|
427
|
+
const bracket = clean.match(/^\[([^\]]+)](?::\d{1,5})?$/);
|
|
428
|
+
if (bracket?.[1])
|
|
429
|
+
return ipv4OrNull(bracket[1]);
|
|
430
|
+
const ipv4WithPort = clean.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d{1,5}$/);
|
|
431
|
+
if (ipv4WithPort?.[1])
|
|
432
|
+
return ipv4OrNull(ipv4WithPort[1]);
|
|
433
|
+
return ipv4OrNull(clean);
|
|
434
|
+
}
|
|
435
|
+
function ipv4OrNull(value) {
|
|
436
|
+
const clean = stringValue(value);
|
|
437
|
+
return clean && isIP(clean) === 4 ? clean : null;
|
|
438
|
+
}
|
|
439
|
+
function stringValue(value) {
|
|
440
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
441
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type ElectronLauncherNetworkOwnershipClaim, type ElectronLauncherNetworkOwnershipRegistry } from './network-ownership-registry.js';
|
|
2
|
+
export type ElectronLauncherPacMatchMode = 'direct' | 'proxy';
|
|
3
|
+
export type ElectronLauncherSystemResolverMode = 'off' | 'dynamic' | 'file';
|
|
4
|
+
export interface ElectronLauncherSystemDomainProxyPolicy {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
domains?: string[] | null;
|
|
7
|
+
pacUrl?: string | null;
|
|
8
|
+
proxy?: string | null;
|
|
9
|
+
matchMode?: ElectronLauncherPacMatchMode | null;
|
|
10
|
+
fallbackProxy?: string | null;
|
|
11
|
+
pacPort?: number | null;
|
|
12
|
+
dnsServers?: string[] | null;
|
|
13
|
+
dnsFallbackTarget?: string | null;
|
|
14
|
+
systemResolver?: boolean | ElectronLauncherSystemResolverMode | null;
|
|
15
|
+
reverseProxyRoutes?: ElectronLauncherSystemDomainProxyRoute[] | null;
|
|
16
|
+
ownershipClaim?: ElectronLauncherNetworkOwnershipClaim | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ElectronLauncherSystemDomainProxyOptions {
|
|
19
|
+
userDataDir: string;
|
|
20
|
+
statePath?: string;
|
|
21
|
+
pacPort?: number | null;
|
|
22
|
+
log?: Pick<Console, 'warn'> | null;
|
|
23
|
+
}
|
|
24
|
+
export interface ElectronLauncherSystemDomainProxyStatus {
|
|
25
|
+
supported: boolean;
|
|
26
|
+
applied: boolean;
|
|
27
|
+
platform: NodeJS.Platform;
|
|
28
|
+
pacUrl?: string | null;
|
|
29
|
+
proxy?: string | null;
|
|
30
|
+
matchMode?: ElectronLauncherPacMatchMode | null;
|
|
31
|
+
fallbackProxy?: string | null;
|
|
32
|
+
pacPort?: number | null;
|
|
33
|
+
sharedPac?: boolean;
|
|
34
|
+
dnsServers?: string[];
|
|
35
|
+
dnsFallbackTarget?: string | null;
|
|
36
|
+
systemResolver?: boolean;
|
|
37
|
+
systemResolverMode?: ElectronLauncherSystemResolverMode;
|
|
38
|
+
resolverDomains?: string[];
|
|
39
|
+
resolverPort?: number | null;
|
|
40
|
+
resolverApplied?: boolean;
|
|
41
|
+
resolverError?: string | null;
|
|
42
|
+
domains?: string[];
|
|
43
|
+
reverseProxyRoutes?: ElectronLauncherSystemDomainProxyRoute[];
|
|
44
|
+
ownershipRegistry?: ElectronLauncherNetworkOwnershipRegistry | null;
|
|
45
|
+
updatedAt?: string | null;
|
|
46
|
+
changed?: boolean;
|
|
47
|
+
verified?: boolean;
|
|
48
|
+
actual?: unknown;
|
|
49
|
+
reason?: string;
|
|
50
|
+
restored?: boolean;
|
|
51
|
+
skipped?: boolean;
|
|
52
|
+
staleState?: boolean;
|
|
53
|
+
orphanCleanup?: boolean;
|
|
54
|
+
pending?: boolean;
|
|
55
|
+
externalApply?: boolean;
|
|
56
|
+
darwinApplyShell?: string | null;
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ElectronLauncherPacProxy {
|
|
60
|
+
address: string;
|
|
61
|
+
directive: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ElectronLauncherSystemDomainProxyRoute {
|
|
64
|
+
routeId?: string | null;
|
|
65
|
+
host: string;
|
|
66
|
+
dnsTarget?: string | null;
|
|
67
|
+
targetUrl?: string | null;
|
|
68
|
+
tlsMode?: 'internal' | 'passthrough' | 'edge-terminated' | null;
|
|
69
|
+
authRequired?: boolean | null;
|
|
70
|
+
enabled?: boolean | null;
|
|
71
|
+
}
|
|
72
|
+
export interface ElectronLauncherSystemDomainProxyManager {
|
|
73
|
+
apply(policy: ElectronLauncherSystemDomainProxyPolicy): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
74
|
+
disable(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
75
|
+
restoreStale(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
76
|
+
darwinPrepareApply?(policy: ElectronLauncherSystemDomainProxyPolicy): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
77
|
+
completeExternalApply?(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
78
|
+
darwinRestoreScript?(): string | null;
|
|
79
|
+
completeExternalRestore?(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
80
|
+
status(): ElectronLauncherSystemDomainProxyStatus;
|
|
81
|
+
statusVerified(): Promise<ElectronLauncherSystemDomainProxyStatus>;
|
|
82
|
+
close(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export declare function createElectronLauncherSystemDomainProxy(options: ElectronLauncherSystemDomainProxyOptions): ElectronLauncherSystemDomainProxyManager;
|
|
85
|
+
export declare function renderElectronLauncherPacScript(input: {
|
|
86
|
+
domains: string[];
|
|
87
|
+
proxy?: ElectronLauncherPacProxy | null;
|
|
88
|
+
matchMode?: ElectronLauncherPacMatchMode | null;
|
|
89
|
+
fallbackProxy?: ElectronLauncherPacProxy | null;
|
|
90
|
+
ownershipClaims?: ElectronLauncherNetworkOwnershipClaim[] | null;
|
|
91
|
+
}): string;
|