Add Gateway connectivity diagnostics and traffic chart improvements
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createConnectivityDiagnosticsService,
|
||||
CURL_META_MARKER,
|
||||
} from '../../src/server/services/connectivityDiagnosticsService.js';
|
||||
|
||||
function response(body = '', overrides = {}) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
error: '',
|
||||
stderr: '',
|
||||
stdout: `${body}${CURL_META_MARKER}${JSON.stringify({
|
||||
exitcode: 0,
|
||||
http_code: 204,
|
||||
time_starttransfer: 0.12,
|
||||
time_total: 0.15,
|
||||
...overrides,
|
||||
})}`,
|
||||
};
|
||||
}
|
||||
|
||||
test('connectivity diagnostics force separate direct and VPN paths', async () => {
|
||||
const calls = [];
|
||||
const execute = async (args) => {
|
||||
calls.push(args);
|
||||
const vpn = args.includes('--proxy');
|
||||
const url = args.at(-1);
|
||||
if (url.includes('cloudflare')) return response(`ip=${vpn ? '203.0.113.20' : '198.51.100.10'}\n`);
|
||||
if (url.includes('api6')) return response(vpn ? '2001:db8::20' : '2001:db8::10');
|
||||
if (url.includes('ipify')) return response(vpn ? '203.0.113.20' : '198.51.100.10');
|
||||
return response();
|
||||
};
|
||||
const service = createConnectivityDiagnosticsService({
|
||||
proxyPort: 18080,
|
||||
execute,
|
||||
now: () => '2026-08-07T12:00:00.000Z',
|
||||
});
|
||||
|
||||
const result = await service.run({ vpnAvailable: true });
|
||||
|
||||
assert.deepEqual(result.direct.ipv4.addresses, ['198.51.100.10']);
|
||||
assert.deepEqual(result.vpn.ipv4.addresses, ['203.0.113.20']);
|
||||
assert.equal(result.direct.ipv6, '2001:db8::10');
|
||||
assert.equal(result.vpn.ipv6, '2001:db8::20');
|
||||
assert.equal(result.assessment.summary, 'available');
|
||||
assert.ok(calls.some((args) => args.includes('--noproxy') && args.includes('*')));
|
||||
assert.ok(calls.some((args) => args.includes('--proxy') && args.includes('http://127.0.0.1:18080')));
|
||||
});
|
||||
|
||||
test('connectivity diagnostics reports a likely direct restriction without claiming its owner', async () => {
|
||||
const attempts = new Map();
|
||||
const execute = async (args) => {
|
||||
const vpn = args.includes('--proxy');
|
||||
const url = args.at(-1);
|
||||
if (url.includes('cloudflare')) return response(`ip=${vpn ? '203.0.113.20' : '198.51.100.10'}\n`);
|
||||
if (url.includes('api6')) return response('', { exitcode: 6, http_code: 0, errormsg: 'resolve failed' });
|
||||
if (url.includes('ipify')) return response(vpn ? '203.0.113.20' : '198.51.100.10');
|
||||
const key = `${vpn}:${url}`;
|
||||
attempts.set(key, (attempts.get(key) || 0) + 1);
|
||||
if (!vpn && url.includes('youtube')) {
|
||||
return response('', { exitcode: 28, http_code: 0, errormsg: 'timeout' });
|
||||
}
|
||||
return response();
|
||||
};
|
||||
const result = await createConnectivityDiagnosticsService({ proxyPort: 18080, execute })
|
||||
.run({ vpnAvailable: true });
|
||||
|
||||
assert.equal(result.assessment.summary, 'likely-direct-restriction');
|
||||
assert.equal(
|
||||
result.assessment.comparisons.find((item) => item.id === 'youtube').assessment,
|
||||
'likely-direct-restriction',
|
||||
);
|
||||
assert.equal(attempts.get('false:https://www.youtube.com/generate_204'), 2);
|
||||
});
|
||||
|
||||
test('connectivity diagnostics skips VPN probes when sing-box is off', async () => {
|
||||
const calls = [];
|
||||
const execute = async (args) => {
|
||||
calls.push(args);
|
||||
const url = args.at(-1);
|
||||
if (url.includes('cloudflare')) return response('ip=198.51.100.10\n');
|
||||
if (url.includes('api6')) return response('', { exitcode: 6, http_code: 0, errormsg: 'resolve failed' });
|
||||
if (url.includes('ipify')) return response('198.51.100.10');
|
||||
return response();
|
||||
};
|
||||
const result = await createConnectivityDiagnosticsService({ proxyPort: 18080, execute })
|
||||
.run({ vpnAvailable: false });
|
||||
|
||||
assert.equal(result.vpn.available, false);
|
||||
assert.equal(result.assessment.summary, 'vpn-off');
|
||||
assert.equal(calls.some((args) => args.includes('--proxy')), false);
|
||||
});
|
||||
|
||||
test('connectivity diagnostics keeps a partial snapshot and falls back when one IP source fails', async () => {
|
||||
const execute = async (args) => {
|
||||
const url = args.at(-1);
|
||||
if (url.includes('cloudflare')) {
|
||||
return response('', { exitcode: 28, http_code: 0, errormsg: 'timeout' });
|
||||
}
|
||||
if (url.includes('api6')) return response('', { exitcode: 6, http_code: 0, errormsg: 'resolve failed' });
|
||||
if (url.includes('ipify')) return response('198.51.100.10');
|
||||
if (url.includes('amazonaws')) return response('198.51.100.10');
|
||||
return response();
|
||||
};
|
||||
const result = await createConnectivityDiagnosticsService({ proxyPort: 18080, execute })
|
||||
.run({ vpnAvailable: false });
|
||||
|
||||
assert.equal(result.direct.internetAvailable, true);
|
||||
assert.deepEqual(result.direct.ipv4.addresses, ['198.51.100.10']);
|
||||
assert.deepEqual(result.direct.ipv4.sources.map((source) => source.source), [
|
||||
'cloudflare',
|
||||
'ipify',
|
||||
'aws',
|
||||
]);
|
||||
});
|
||||
@@ -26,6 +26,7 @@ test('control uses the dataplane socket protocol', async () => {
|
||||
assert.equal(traffic.running, true);
|
||||
await client.observeDevicePolicy();
|
||||
await client.applyDevicePolicies([{ id: 'dev_0011223344556677' }]);
|
||||
await client.runConnectivityDiagnostics();
|
||||
assert.equal(client.running, true);
|
||||
await client.restart();
|
||||
assert.equal((await client.stop()).running, false);
|
||||
@@ -36,6 +37,7 @@ test('control uses the dataplane socket protocol', async () => {
|
||||
'GET /device-traffic /run/dataplane.sock',
|
||||
'GET /device-policy /run/dataplane.sock',
|
||||
'PUT /device-policy /run/dataplane.sock',
|
||||
'POST /diagnostics/connectivity /run/dataplane.sock',
|
||||
'POST /restart /run/dataplane.sock',
|
||||
'POST /stop /run/dataplane.sock',
|
||||
]);
|
||||
|
||||
@@ -27,7 +27,21 @@ test('gateway routes .ru domains directly and other traffic through the selected
|
||||
});
|
||||
|
||||
assert.deepEqual(config.route.rule_set, []);
|
||||
assert.deepEqual(config.inbounds.map((inbound) => inbound.tag), [
|
||||
'tproxy-in',
|
||||
'mixed-in',
|
||||
'diagnostics-vpn-in',
|
||||
]);
|
||||
assert.deepEqual(config.inbounds[2], {
|
||||
type: 'mixed',
|
||||
tag: 'diagnostics-vpn-in',
|
||||
listen: '127.0.0.1',
|
||||
listen_port: 18080,
|
||||
sniff: true,
|
||||
set_system_proxy: false,
|
||||
});
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ['diagnostics-vpn-in'], outbound: 'test-vpn' },
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
{ inbound: ['tproxy-in'], outbound: 'test-vpn' },
|
||||
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
|
||||
|
||||
Reference in New Issue
Block a user