Refactor VPN proxy client implementation
Build and Deploy Gateway / build-and-push (push) Failing after 2s
Build and Deploy Gateway / deploy (push) Has been skipped

This commit is contained in:
2026-08-09 00:41:52 +03:00
parent 32be4380a3
commit 34d8b681ad
190 changed files with 20064 additions and 9761 deletions
@@ -0,0 +1,292 @@
import { isDeepStrictEqual } from 'node:util';
import type { GatewayAutoState, StoredState } from '../../../shared/contracts/state.js';
import type { RuntimeCommandResult } from '../connection/index.js';
import { finishRollback } from '../../services/rollback.js';
interface HostNetworkState {
gateway: string;
interface: string;
mac: string;
observedAt?: number;
}
interface VerifiedGateway {
gatewayId: string;
uiOrigin?: string;
verifiedAt?: string;
}
type TimerHandle = NodeJS.Timeout;
interface GatewayAutoServiceDependencies {
appMode: string;
state: {
read(): StoredState;
update(mutator: (state: StoredState) => Record<string, unknown>): StoredState;
};
subscription: { readConfig(): unknown | null };
config: {
build(
subscriptionConfig: unknown,
selectedServerId: string,
routeRules: StoredState['routeRules'],
gatewayAuto: GatewayAutoState,
): unknown;
read(): string | null;
write(value: unknown): void;
restore(value: string): void;
remove(): void;
};
runtime: {
isRunning(): boolean;
applyCommand(): Promise<RuntimeCommandResult>;
restoreRunning(): Promise<unknown>;
};
discovery: {
readHostNetwork(): HostNetworkState | null;
probeGateway(input: {
gateway: string;
subscriptionUrl: string;
}): Promise<VerifiedGateway>;
};
transition: {
createInitial(): GatewayAutoState;
applyPreference(state: GatewayAutoState, enabled: boolean): GatewayAutoState;
next(
current: GatewayAutoState,
input: {
network: HostNetworkState | null;
verifiedGateway?: VerifiedGateway | null;
error?: string;
},
): GatewayAutoState;
sameRoute(
previous: GatewayAutoState['gateway'] | HostNetworkState | null | undefined,
current: HostNetworkState | null | undefined,
): boolean;
};
serialize<T>(operation: () => Promise<T>): Promise<T>;
scheduler: {
setInterval(callback: () => void, intervalMs: number): TimerHandle;
clearInterval(timer: TimerHandle): void;
};
onRouteChange(state: GatewayAutoState): void;
onDiscoveryWarning(reason: string): void;
onTimerError(error: unknown): void;
}
interface CommitOptions {
reconfigure?: boolean;
persistEnabled?: boolean;
}
interface RefreshOptions {
reconfigure?: boolean;
}
function errorMessage(error: unknown) {
return error && typeof error === 'object' && 'message' in error && error.message
? String(error.message)
: 'Gateway presence check failed';
}
export function createGatewayAutoService(dependencies: GatewayAutoServiceDependencies) {
let current = dependencies.transition.createInitial();
let refreshPromise: Promise<GatewayAutoState> | null = null;
let discoveryTimer: TimerHandle | null = null;
const restoreConfig = (previous: string | null) => {
if (previous === null) dependencies.config.remove();
else dependencies.config.restore(previous);
};
const commitCandidate = async (
candidate: GatewayAutoState,
{ reconfigure = true, persistEnabled }: CommitOptions = {},
) => {
const previousGatewayAuto = current;
const stateChanged = !isDeepStrictEqual(previousGatewayAuto, candidate);
const modeChanged = previousGatewayAuto.mode !== candidate.mode;
if (!stateChanged && persistEnabled === undefined) return current;
const previousState = dependencies.state.read();
const subscriptionConfig = modeChanged
? dependencies.subscription.readConfig()
: null;
const candidateConfig = modeChanged && previousState.selectedServerId && subscriptionConfig
? dependencies.config.build(
subscriptionConfig,
previousState.selectedServerId,
previousState.routeRules,
candidate,
)
: null;
const previousConfig = candidateConfig === null ? null : dependencies.config.read();
const wasRunning = candidateConfig === null ? false : dependencies.runtime.isRunning();
let configMutationStarted = false;
let runtimeMutationStarted = false;
let gatewayAutoPublished = false;
let stateCommitStarted = false;
try {
if (candidateConfig !== null) {
configMutationStarted = true;
dependencies.config.write(candidateConfig);
if (reconfigure && wasRunning) {
const command = await dependencies.runtime.applyCommand();
runtimeMutationStarted = command.mutationStarted;
if (!command.ok) throw command.error;
}
}
if (stateChanged) {
current = candidate;
gatewayAutoPublished = true;
stateCommitStarted = true;
dependencies.state.update((state) => state);
}
if (persistEnabled !== undefined) {
stateCommitStarted = true;
dependencies.state.update((state) => ({
...state,
gatewayAutoEnabled: persistEnabled,
}));
}
} catch (error) {
await finishRollback(error, [
...(gatewayAutoPublished ? [{ run: () => { current = previousGatewayAuto; } }] : []),
...(configMutationStarted ? [{ run: () => restoreConfig(previousConfig) }] : []),
...(wasRunning && runtimeMutationStarted ? [{
run: () => dependencies.runtime.restoreRunning(),
runtime: true,
}] : []),
...(stateCommitStarted ? [{ run: () => dependencies.state.update(() => previousState) }] : []),
], 'Gateway auto rollback failed');
}
if (modeChanged) dependencies.onRouteChange(candidate);
return current;
};
const runRefresh = async ({ reconfigure = true }: RefreshOptions) => {
const state = dependencies.state.read();
const network = state.subscriptionUrl
? dependencies.discovery.readHostNetwork()
: null;
if (!network) {
const discoveryError = 'macOS default gateway недоступен или устарел';
const discoveredState = dependencies.transition.next(current, {
network: null,
error: discoveryError,
});
const candidate = dependencies.transition.applyPreference(
state.subscriptionUrl
? { ...discoveredState, lastError: discoveryError }
: discoveredState,
state.gatewayAutoEnabled !== false,
);
return commitCandidate(candidate, { reconfigure });
}
if (
current.mode === 'gateway-direct' &&
!dependencies.transition.sameRoute(current.gateway, network)
) {
await commitCandidate(
dependencies.transition.next(current, { network }),
{ reconfigure },
);
}
let verifiedGateway: VerifiedGateway;
try {
verifiedGateway = await dependencies.discovery.probeGateway({
gateway: network.gateway,
subscriptionUrl: String(state.subscriptionUrl),
});
} catch (error) {
const reason = errorMessage(error);
const latestState = dependencies.state.read();
const latestNetwork = latestState.subscriptionUrl
? dependencies.discovery.readHostNetwork()
: null;
if (
latestState.subscriptionUrl !== state.subscriptionUrl ||
!dependencies.transition.sameRoute(network, latestNetwork)
) {
return commitCandidate(dependencies.transition.createInitial(), { reconfigure });
}
if (current.lastError !== reason) dependencies.onDiscoveryWarning(reason);
return commitCandidate(
dependencies.transition.applyPreference(
dependencies.transition.next(current, {
network: latestNetwork,
error: reason,
}),
latestState.gatewayAutoEnabled !== false,
),
{ reconfigure },
);
}
const latestState = dependencies.state.read();
const latestNetwork = latestState.subscriptionUrl
? dependencies.discovery.readHostNetwork()
: null;
if (
latestState.subscriptionUrl !== state.subscriptionUrl ||
!dependencies.transition.sameRoute(network, latestNetwork)
) {
return commitCandidate(dependencies.transition.createInitial(), { reconfigure });
}
return commitCandidate(
dependencies.transition.applyPreference(
dependencies.transition.next(current, { network: latestNetwork, verifiedGateway }),
latestState.gatewayAutoEnabled !== false,
),
{ reconfigure },
);
};
const refresh = (options: RefreshOptions = {}) => {
if (dependencies.appMode !== 'client') return Promise.resolve(current);
if (refreshPromise) return refreshPromise;
refreshPromise = dependencies.serialize(() => runRefresh(options)).finally(() => {
refreshPromise = null;
});
return refreshPromise;
};
const setEnabled = (enabled: boolean) => dependencies.serialize(() => commitCandidate(
dependencies.transition.applyPreference(current, enabled),
{ persistEnabled: enabled },
));
const startDiscovery = (intervalMs: number) => {
if (discoveryTimer) return;
discoveryTimer = dependencies.scheduler.setInterval(() => {
void refresh().catch(dependencies.onTimerError);
}, intervalMs);
discoveryTimer.unref();
};
const stopDiscovery = () => {
if (!discoveryTimer) return;
dependencies.scheduler.clearInterval(discoveryTimer);
discoveryTimer = null;
};
return {
read: () => current,
set: (value: GatewayAutoState) => { current = value; },
createInitial: dependencies.transition.createInitial,
setEnabled,
refresh,
startDiscovery,
stopDiscovery,
};
}
export type GatewayAutoService = ReturnType<typeof createGatewayAutoService>;