Add enabled local routing rules and gateway version reporting
This commit is contained in:
@@ -16,6 +16,9 @@ test('gateway deploy updates control without recreating dataplane', () => {
|
||||
assert.match(compose, /DATAPLANE_SOCKET: \/run\/vpn-proxy\/dataplane\.sock/);
|
||||
assert.match(deploy, /up -d --no-deps --wait[^\n]+vpn-proxy-control/);
|
||||
assert.match(workflow, /UPDATE_DATAPLANE="\$\{UPDATE_DATAPLANE\}"/);
|
||||
assert.match(workflow, /src\/server\/\(config\|dataplane\|gatewayRouting\|singboxRuntime\|version\)/);
|
||||
assert.match(workflow, /src\/shared\/errors/);
|
||||
assert.doesNotMatch(workflow, /dataplaneClient/);
|
||||
});
|
||||
|
||||
test('runtime images include shared server modules', () => {
|
||||
|
||||
@@ -24,8 +24,9 @@ const subscriptionConfig = {
|
||||
test('client exposes one local proxy and routes local exceptions before the selected VPN', () => {
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn', {
|
||||
routeRules: [
|
||||
{ type: 'domain', value: 'example.com' },
|
||||
{ type: 'domain_keyword', value: 'cdn' },
|
||||
{ type: 'domain_suffix', value: 'ru', enabled: true },
|
||||
{ type: 'domain', value: 'example.com', enabled: true },
|
||||
{ type: 'domain_keyword', value: 'cdn', enabled: false },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -34,7 +35,6 @@ test('client exposes one local proxy and routes local exceptions before the sele
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
{ domain: ['example.com'], outbound: 'direct' },
|
||||
{ domain_keyword: ['cdn'], outbound: 'direct' },
|
||||
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
|
||||
]);
|
||||
assert.equal(config.route.final, 'test-vpn');
|
||||
@@ -42,7 +42,10 @@ test('client exposes one local proxy and routes local exceptions before the sele
|
||||
});
|
||||
|
||||
test('client keeps its local proxy but routes directly when Harbor Gateway is ahead', () => {
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn', { clientDirect: true });
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn', {
|
||||
clientDirect: true,
|
||||
routeRules: [{ type: 'domain_suffix', value: 'ru', enabled: true }],
|
||||
});
|
||||
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
|
||||
@@ -22,7 +22,9 @@ const subscriptionConfig = {
|
||||
};
|
||||
|
||||
test('gateway routes .ru domains directly and other traffic through the selected VPN', () => {
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn');
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn', {
|
||||
routeRules: [{ type: 'domain_suffix', value: 'ru', enabled: true }],
|
||||
});
|
||||
|
||||
assert.deepEqual(config.route.rule_set, []);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
|
||||
@@ -169,10 +169,9 @@ setInterval(() => {}, 60_000);
|
||||
});
|
||||
assertStateSnapshot(initial);
|
||||
assert.equal(initial.selection.appliedServerId, 'test-vpn');
|
||||
assert.deepEqual(initial.route.localRules, {
|
||||
builtIn: [{ type: 'domain_suffix', value: 'ru' }],
|
||||
custom: [],
|
||||
});
|
||||
assert.deepEqual(initial.route.localRules, [
|
||||
{ type: 'domain_suffix', value: 'ru', enabled: true },
|
||||
]);
|
||||
assert.equal(JSON.stringify(initial).includes(subscriptionUrl), false);
|
||||
const stateKeys = Object.keys(initial).sort();
|
||||
let revision = initial.revision;
|
||||
@@ -242,18 +241,20 @@ setInterval(() => {}, 60_000);
|
||||
const routed = await mutation('/api/route-rules', 'PUT', {
|
||||
expectedRevision: rulesRevision,
|
||||
rules: [
|
||||
{ type: 'domain', value: 'https://Example.com/private?q=1' },
|
||||
{ type: 'domain_suffix', value: '*.Example.org' },
|
||||
{ type: 'domain_suffix', value: 'ru', enabled: false },
|
||||
{ type: 'domain', value: 'https://Example.com/private?q=1', enabled: true },
|
||||
{ type: 'domain_suffix', value: '*.Example.org', enabled: true },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(routed.state.route.localRules.custom, [
|
||||
{ type: 'domain', value: 'example.com' },
|
||||
{ type: 'domain_suffix', value: 'example.org' },
|
||||
assert.deepEqual(routed.state.route.localRules, [
|
||||
{ type: 'domain_suffix', value: 'ru', enabled: false },
|
||||
{ type: 'domain', value: 'example.com', enabled: true },
|
||||
{ type: 'domain_suffix', value: 'example.org', enabled: true },
|
||||
]);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'sing-box-config.json'))).route.rules.slice(0, 3), [
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
{ domain: ['example.com'], outbound: 'direct' },
|
||||
{ domain_suffix: ['example.org'], outbound: 'direct' },
|
||||
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
|
||||
]);
|
||||
|
||||
const invalidRules = await rawRequest(port, '/api/route-rules', 'PUT', {
|
||||
@@ -270,7 +271,7 @@ setInterval(() => {}, 60_000);
|
||||
});
|
||||
assert.equal(staleRules.response.status, 409);
|
||||
assert.equal(staleRules.payload.error.code, 'STATE_CONFLICT');
|
||||
assert.deepEqual((await request(port, '/api/state')).route.localRules.custom, routed.state.route.localRules.custom);
|
||||
assert.deepEqual((await request(port, '/api/state')).route.localRules, routed.state.route.localRules);
|
||||
|
||||
const workingConfig = fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8');
|
||||
fs.writeFileSync(singboxPath, `#!/usr/bin/env node
|
||||
@@ -291,7 +292,7 @@ setInterval(() => {}, 60_000);
|
||||
assert.equal(failedRules.response.status, 422);
|
||||
assert.equal(failedRules.payload.error.code, 'CONFIG_INVALID');
|
||||
const rolledBack = await request(port, '/api/state');
|
||||
assert.deepEqual(rolledBack.route.localRules.custom, routed.state.route.localRules.custom);
|
||||
assert.deepEqual(rolledBack.route.localRules, routed.state.route.localRules);
|
||||
assert.equal(rolledBack.connection.process, 'running');
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'), workingConfig);
|
||||
revision = rolledBack.revision;
|
||||
@@ -317,7 +318,7 @@ if (process.argv[2] === 'check') {
|
||||
const forgotten = await mutation('/api/subscription', 'DELETE');
|
||||
assert.equal(forgotten.state.subscription.status, 'missing');
|
||||
assert.equal(forgotten.state.servers.length, 0);
|
||||
assert.deepEqual(forgotten.state.route.localRules.custom, routed.state.route.localRules.custom);
|
||||
assert.deepEqual(forgotten.state.route.localRules, routed.state.route.localRules);
|
||||
assert.deepEqual((await stateResponse('/api/servers/ping-all')).results, []);
|
||||
|
||||
const missingConfig = await rawRequest(port, '/api/singbox/restart', 'POST');
|
||||
|
||||
@@ -33,10 +33,10 @@ test('a failure before rename preserves the last successful file', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('schema v1 state migrates to the current schema and keeps a backup', (t) => {
|
||||
test('schema v2 state migrates built-in .ru into a normal enabled rule', (t) => {
|
||||
const filePath = fixture(t);
|
||||
const legacy = {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
revision: 7,
|
||||
selectedTag: 'nl',
|
||||
servers: [{ tag: 'nl' }],
|
||||
@@ -49,8 +49,11 @@ test('schema v1 state migrates to the current schema and keeps a backup', (t) =>
|
||||
const migrated = store.read();
|
||||
|
||||
assert.equal(migrated.schemaVersion, STATE_SCHEMA_VERSION);
|
||||
assert.deepEqual(migrated.routeRules, [
|
||||
{ type: 'domain_suffix', value: 'ru', enabled: true },
|
||||
]);
|
||||
assert.equal(migrated.appliedTag, 'nl');
|
||||
assert.equal(store.migration.fromVersion, 1);
|
||||
assert.equal(store.migration.fromVersion, 2);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(store.migration.backupPath, 'utf8')), legacy);
|
||||
assert.equal(JSON.parse(fs.readFileSync(filePath, 'utf8')).schemaVersion, STATE_SCHEMA_VERSION);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { buildVersionInfo } from '../../src/server/version.js';
|
||||
import { buildGatewayVersionInfo, buildVersionInfo } from '../../src/server/version.js';
|
||||
import { HARBOR_VERSIONS, versionCompatibility } from '../../src/shared/versions.js';
|
||||
|
||||
test('component versions enforce one Harbor major and one Gateway major.minor', () => {
|
||||
@@ -22,6 +22,24 @@ test('component versions enforce one Harbor major and one Gateway major.minor',
|
||||
}).compatible, false);
|
||||
});
|
||||
|
||||
test('gateway reports control backend and deployed dataplane separately', () => {
|
||||
const control = {
|
||||
apiVersion: 1,
|
||||
location: 'gateway',
|
||||
components: { gatewayBackend: '0.4.0' },
|
||||
runtime: { singBox: '1.12.13' },
|
||||
};
|
||||
assert.deepEqual(buildGatewayVersionInfo(control, {
|
||||
gatewayBackendVersion: '0.3.0',
|
||||
singBoxVersion: '1.12.13',
|
||||
}), {
|
||||
apiVersion: 1,
|
||||
location: 'gateway',
|
||||
components: { gatewayBackend: '0.4.0' },
|
||||
runtime: { dataplaneVersion: '0.3.0', singBox: '1.12.13' },
|
||||
});
|
||||
});
|
||||
|
||||
test('runtime version info reports the installed sing-box binary', () => {
|
||||
const run = () => ({ stdout: 'sing-box version 1.12.13\n', stderr: '' });
|
||||
assert.deepEqual(buildVersionInfo('gateway', run), {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { normalizeRouteRules } from '../../src/shared/routingRules.js';
|
||||
import { canAppendRouteRule, normalizeRouteRules } from '../../src/shared/routingRules.js';
|
||||
|
||||
test('local route rules normalize URLs, suffixes and duplicates', () => {
|
||||
assert.deepEqual(normalizeRouteRules([
|
||||
@@ -9,12 +9,15 @@ test('local route rules normalize URLs, suffixes and duplicates', () => {
|
||||
{ type: 'domain_suffix', value: '*.Example.org' },
|
||||
{ type: 'domain_keyword', value: ' CDN ' },
|
||||
{ type: 'domain', value: 'example.com' },
|
||||
{ type: 'domain_suffix', value: '.ru' },
|
||||
{ type: 'domain_suffix', value: '.ru', enabled: false },
|
||||
], { strict: true }), [
|
||||
{ type: 'domain', value: 'example.com' },
|
||||
{ type: 'domain_suffix', value: 'example.org' },
|
||||
{ type: 'domain_keyword', value: 'cdn' },
|
||||
{ type: 'domain', value: 'example.com', enabled: true },
|
||||
{ type: 'domain_suffix', value: 'example.org', enabled: true },
|
||||
{ type: 'domain_keyword', value: 'cdn', enabled: true },
|
||||
{ type: 'domain_suffix', value: 'ru', enabled: false },
|
||||
]);
|
||||
assert.equal(canAppendRouteRule([{ value: 'filled' }]), true);
|
||||
assert.equal(canAppendRouteRule([{ value: '' }]), false);
|
||||
});
|
||||
|
||||
test('invalid local route rules fail at the strict boundary', () => {
|
||||
|
||||
Reference in New Issue
Block a user