163 lines
6.6 KiB
JavaScript
163 lines
6.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import {
|
|
createConnectivityDiagnosticsService,
|
|
CURL_META_MARKER,
|
|
} from '../../src/server/services/connectivityDiagnosticsService.js';
|
|
|
|
const server = fs.readFileSync(path.resolve(import.meta.dirname, '../../src/server/index.js'), 'utf8');
|
|
|
|
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 endpoint is available in Connect and Gateway', () => {
|
|
assert.match(server, /const localConnectivityDiagnostics = !remoteDataplane/);
|
|
assert.doesNotMatch(server, /settings\.appMode !== 'gateway'[\s\S]{0,120}ENDPOINT_NOT_FOUND/);
|
|
});
|
|
|
|
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 every IP source in a partial snapshot', 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',
|
|
'icanhazip',
|
|
'ifconfig-me',
|
|
'yandex-internet',
|
|
]);
|
|
});
|
|
|
|
test('connectivity diagnostics pins public custom services and rejects private destinations', 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 lookup = async (hostname) => hostname === 'router.local'
|
|
? [{ address: '192.168.50.1', family: 4 }]
|
|
: [{ address: '93.184.216.34', family: 4 }];
|
|
const result = await createConnectivityDiagnosticsService({ proxyPort: 18080, execute, lookup })
|
|
.run({
|
|
vpnAvailable: false,
|
|
services: [
|
|
{ id: 'custom-public', label: 'Example', url: 'https://example.com/status' },
|
|
{ id: 'custom-private', label: 'Router', url: 'https://router.local/' },
|
|
],
|
|
});
|
|
|
|
const publicCall = calls.find((args) => args.at(-1) === 'https://example.com/status');
|
|
assert.ok(publicCall);
|
|
assert.equal(publicCall.includes('--location'), false);
|
|
assert.deepEqual(publicCall.slice(publicCall.indexOf('--resolve'), publicCall.indexOf('--resolve') + 2), [
|
|
'--resolve',
|
|
'example.com:443:93.184.216.34',
|
|
]);
|
|
assert.equal(calls.some((args) => args.at(-1) === 'https://router.local/'), false);
|
|
assert.equal(result.direct.sites.find((site) => site.id === 'custom-private').stage, 'validation');
|
|
assert.equal(result.assessment.comparisons.some((item) => item.id === 'custom-public'), true);
|
|
});
|