Refactor VPN proxy components and update related behavior
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-11 01:27:46 +03:00
parent c89e56942a
commit aa9c959368
58 changed files with 4234 additions and 2755 deletions
+74 -27
View File
@@ -15,6 +15,30 @@ import { createGatewayAutoRoute } from '../../dist/server/http/routes/gatewayAut
const subscriptionUrl = 'https://subscription.example/0123456789abcdef0123456789abcdef';
const server = { id: 'server', label: 'Server', host: 'server.example', port: 443, protocol: 'vless' };
const storedProfile = (servers = [server]) => ({
id: 'primary',
label: 'Основной',
subscriptionUrl,
subscriptionConfig: { outbounds: [] },
servers,
userInfo: {},
fetchedAt: null,
desiredServerId: servers[0]?.id || '',
lastRefreshAttemptAt: null,
lastRefreshErrorCode: null,
});
const canonicalState = (profiles = [storedProfile()]) => ({
revision: 10,
profiles,
desiredProfileId: profiles[0]?.id || '',
appliedProfileId: profiles[0]?.id || '',
appliedServerId: profiles[0]?.desiredServerId || '',
appliedServerSnapshot: profiles[0]?.servers[0] || null,
routeRules: [],
appliedRouteRules: [],
routeRulesRevision: 0,
gatewayAutoEnabled: true,
});
const networkA = {
gateway: '192.168.50.111',
interface: 'en0',
@@ -51,17 +75,7 @@ function deferred() {
}
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? {
revision: 10,
servers: [server],
selectedServerId: server.id,
appliedServerId: server.id,
routeRules: [],
appliedRouteRules: [],
routeRulesRevision: 0,
subscriptionUrl,
gatewayAutoEnabled: true,
});
let state = structuredClone(overrides.state ?? canonicalState());
let config = Object.hasOwn(overrides, 'config') ? overrides.config : 'old-config';
let network = Object.hasOwn(overrides, 'network') ? overrides.network : networkA;
let tail = Promise.resolve();
@@ -109,11 +123,14 @@ function createHarness(overrides = {}) {
readConfig: () => overrides.missingSubscription ? null : { outbounds: [] },
},
config: {
build: (_subscription, selectedServerId, routeRules, gatewayAuto) => ({
selectedServerId,
routeRules,
clientDirect: gatewayAuto.mode === 'gateway-direct',
}),
build: (_subscription, selectedServerId, routeRules, gatewayAuto) => {
if (failures.configBuild) throw failures.configBuild;
return {
selectedServerId,
routeRules,
clientDirect: gatewayAuto.mode === 'gateway-direct',
};
},
read: () => config,
write: (value) => {
events.push('config.write');
@@ -144,6 +161,10 @@ function createHarness(overrides = {}) {
events.push('runtime.restore');
if (failures.runtimeRestore) throw failures.runtimeRestore;
},
stopCommand: async () => {
events.push('runtime.stop');
return overrides.stopCommandResult || { ok: true, mutationStarted: true };
},
},
discovery: {
readHostNetwork: () => structuredClone(network),
@@ -224,16 +245,8 @@ test('gateway-auto handles no-op and metadata-only discovery without config/runt
test('gateway-auto mode changes separate state-only, stopped and running paths', async () => {
const stateOnly = createHarness({
gatewayAuto: directState(),
state: {
revision: 1,
servers: [],
selectedServerId: '',
appliedServerId: '',
routeRules: [],
appliedRouteRules: [],
routeRulesRevision: 0,
gatewayAutoEnabled: true,
},
state: { ...canonicalState([]), revision: 1 },
running: false,
});
await stateOnly.service.setEnabled(false);
assert.equal(stateOnly.snapshot().gatewayAuto.mode, 'local-vpn');
@@ -266,6 +279,38 @@ test('gateway-auto startup writes candidate config before publication without ap
]);
});
test('gateway loss safely stops when the applied server disappeared from refreshed config', async () => {
const state = canonicalState();
state.profiles[0].servers = [];
state.profiles[0].desiredServerId = '';
const buildError = Object.assign(new Error('server removed'), { code: 'SERVER_NOT_FOUND' });
const harness = createHarness({
state,
gatewayAuto: directState(),
network: networkB,
failures: { configBuild: buildError },
});
await assert.rejects(harness.service.refresh(), (error) => error === buildError);
const after = harness.snapshot();
assert.equal(after.gatewayAuto.mode, 'local-vpn');
assert.equal(after.state.connectionDesired, 'stopped');
assert.equal(after.state.appliedProfileId, '');
assert.equal(after.state.appliedServerId, '');
assert.equal(harness.events.includes('runtime.stop'), true);
});
test('Gateway discovery preserves a stale applied local runtime when direct config cannot build', async () => {
const state = canonicalState();
state.profiles[0].servers = [];
state.profiles[0].desiredServerId = '';
const buildError = Object.assign(new Error('server removed'), { code: 'SERVER_NOT_FOUND' });
const harness = createHarness({ state, failures: { configBuild: buildError } });
const before = harness.snapshot();
await assert.rejects(harness.service.refresh(), (error) => error === buildError);
assert.deepEqual(withoutRevision(harness.snapshot()), withoutRevision(before));
assert.equal(harness.events.includes('runtime.stop'), false);
});
test('disable and re-enable preserve verified Gateway identity and persisted preference commits', async () => {
const harness = createHarness({ gatewayAuto: directState() });
await harness.service.setEnabled(false);
@@ -382,7 +427,9 @@ test('gateway-auto discards stale subscription and route probe results', async (
staleSubscription.service.set(directState());
const refresh = staleSubscription.service.refresh();
await new Promise((resolve) => setImmediate(resolve));
staleSubscription.patchState({ subscriptionUrl: `${subscriptionUrl}-new` });
const changedState = staleSubscription.snapshot().state;
changedState.profiles[0].subscriptionUrl = `${subscriptionUrl}-new`;
staleSubscription.setState(changedState);
gate.resolve(verifiedA);
await refresh;
assert.deepEqual(staleSubscription.snapshot().gatewayAuto, createGatewayAutoState());