Add native traffic inspection to Harbor Connect and Gateway

This commit is contained in:
2026-08-31 05:19:15 +03:00
parent 116686a138
commit 4d066cb879
62 changed files with 10975 additions and 220 deletions
+27 -12
View File
@@ -152,22 +152,37 @@ test('production paths use only the compiled dispatcher', () => {
assert.match(workflow, /command -v npm[^']+command -v git[^']+test -x \/bin\/bash/);
assert.doesNotMatch(workflow, /docker image inspect "\$\{\{ env\.NODE_BUILD_IMAGE \}\}"/);
assert.match(legacyBuild, /npm run build:production && docker build/);
assert.match(legacyBuild, /docker run --rm[^;]+sing-box version[^;]+grep -Fx/);
assert.match(legacyBuild, /docker run --rm --entrypoint sing-box[^;]+version[^;]+grep -Fx/);
assert.match(dockerignore, /^dist$/m);
});
test('every shipped build defaults to sing-box 1.13.18', () => {
for (const file of [
'.env.example',
'.gitea/workflows/gateway-build.yml',
'Dockerfile',
'Dockerfile.client',
'Dockerfile.runtime-base',
'docker-compose.client.yml',
'docker-compose.gateway.yml',
'scripts/build-on-107-deploy-111.sh',
'scripts/build-runtime-base.sh',
test('Mac client builds default to exact sing-box 1.14.0-rc.5', () => {
assert.match(
fs.readFileSync(path.join(root, 'Dockerfile.client'), 'utf8'),
/^ARG SINGBOX_VERSION=1\.14\.0-rc\.5$/m,
);
assert.match(
fs.readFileSync(path.join(root, 'docker-compose.client.yml'), 'utf8'),
/SINGBOX_VERSION: \$\{SINGBOX_VERSION:-1\.14\.0-rc\.5\}/,
);
});
test('Gateway builds use the Mac-qualified exact sing-box 1.14.0-rc.5', () => {
for (const [file, pin] of [
['.env.example', /^SINGBOX_VERSION=1\.14\.0-rc\.5$/m],
['.gitea/workflows/gateway-build.yml', /^\s*SINGBOX_VERSION: 1\.14\.0-rc\.5$/m],
['Dockerfile', /^ARG SINGBOX_VERSION=1\.14\.0-rc\.5$/m],
['Dockerfile.runtime-base', /^ARG SINGBOX_VERSION=1\.14\.0-rc\.5$/m],
['docker-compose.gateway.yml', /SINGBOX_VERSION: \$\{SINGBOX_VERSION:-1\.14\.0-rc\.5\}/],
['scripts/build-on-107-deploy-111.sh', /^SINGBOX_VERSION="\$\{SINGBOX_VERSION:-1\.14\.0-rc\.5\}"$/m],
['scripts/build-runtime-base.sh', /^SINGBOX_VERSION="\$\{SINGBOX_VERSION:-1\.14\.0-rc\.5\}"$/m],
]) {
assert.match(fs.readFileSync(path.join(root, file), 'utf8'), /SINGBOX_VERSION[^\n]*1\.13\.18/);
assert.match(fs.readFileSync(path.join(root, file), 'utf8'), pin);
}
const gatewayDockerfile = fs.readFileSync(path.join(root, 'Dockerfile'), 'utf8');
for (const dependency of ['@bufbuild/protobuf', '@connectrpc/connect', '@connectrpc/connect-node']) {
assert.match(gatewayDockerfile, new RegExp(`/src/node_modules/${dependency.replace('/', '\\/')}`));
}
});
+10 -7
View File
@@ -26,6 +26,8 @@ test('control uses the dataplane socket protocol', async () => {
assert.equal(traffic.running, true);
const domainTraffic = await client.observeDomainTraffic();
assert.equal(domainTraffic.running, true);
const liveTraffic = await client.observeLiveTraffic();
assert.equal(liveTraffic.running, true);
await client.observeDevicePolicy();
await client.applyDevicePolicies([{ id: 'dev_0011223344556677' }]);
await client.runConnectivityDiagnostics(
@@ -47,6 +49,7 @@ test('control uses the dataplane socket protocol', async () => {
'GET /devices /run/dataplane.sock',
'GET /device-traffic /run/dataplane.sock',
'GET /domain-traffic /run/dataplane.sock',
'GET /traffic/live /run/dataplane.sock',
'GET /device-policy /run/dataplane.sock',
'PUT /device-policy /run/dataplane.sock',
'POST /diagnostics/connectivity /run/dataplane.sock',
@@ -59,16 +62,16 @@ test('control uses the dataplane socket protocol', async () => {
'POST /restart /run/dataplane.sock',
'POST /stop /run/dataplane.sock',
]);
assert.deepEqual(requests[6].body, { devices: [{ id: 'dev_0011223344556677' }] });
assert.deepEqual(requests[7].body, {
assert.deepEqual(requests[7].body, { devices: [{ id: 'dev_0011223344556677' }] });
assert.deepEqual(requests[8].body, {
services: [{ id: 'custom-test', url: 'https://example.com' }],
target: 'site:custom-test',
});
assert.equal(requests[7].timeoutMs, 25_000);
assert.deepEqual(requests[8].body, { config: { outbounds: [] } });
assert.deepEqual(requests[9].body, { role: 'primary', services: [], target: 'site:youtube', timeoutMs: 9_000 });
assert.equal(requests[9].timeoutMs, 19_000);
assert.deepEqual(requests[11].body, { role: 'reserve' });
assert.equal(requests[8].timeoutMs, 25_000);
assert.deepEqual(requests[9].body, { config: { outbounds: [] } });
assert.deepEqual(requests[10].body, { role: 'primary', services: [], target: 'site:youtube', timeoutMs: 9_000 });
assert.equal(requests[10].timeoutMs, 19_000);
assert.deepEqual(requests[12].body, { role: 'reserve' });
});
test('connectivity diagnostics expose a retryable domain error', async () => {
+10
View File
@@ -33,6 +33,16 @@ test('gateway deploy updates control without recreating dataplane', () => {
assert.doesNotMatch(workflow, /grep -Eq/);
});
test('Gateway native API credentials remain private to the dataplane volume', () => {
assert.match(compose, /SING_BOX_TRAFFIC_SOURCE: \$\{SING_BOX_TRAFFIC_SOURCE:-snapshot\}/);
assert.match(compose, /vpn-proxy-dataplane:[\s\S]*SING_BOX_API_SECRET: \/var\/lib\/sing-box\/api\.secret[\s\S]*sing-box-cache:\/var\/lib\/sing-box/);
assert.doesNotMatch(
compose.match(/vpn-proxy-control:[\s\S]*?(?=\nvolumes:)/)?.[0] || '',
/SING_BOX_API_SECRET|sing-box-cache|19091/,
);
assert.doesNotMatch(compose.match(/ports:[\s\S]*?volumes:/)?.[0] || '', /19091/);
});
test('manual hard deploy safely forces the existing full Gateway path', () => {
assert.match(workflow, /workflow_dispatch:\s*\n\s+inputs:\s*\n\s+hard_deploy:[\s\S]*default: false[\s\S]*type: boolean/);
assert.match(workflow, /env:\s*\n\s+HARD_DEPLOY_INPUT: \$\{\{ inputs\.hard_deploy \}\}/);
+3 -2
View File
@@ -29,8 +29,9 @@ const observation = (ip, mac = '00:11:22:33:44:55', deviceInterface = 'eth0') =>
test('dataplane exposes cached traffic snapshots without making accounting a readiness dependency', () => {
assert.match(dataplaneSource, /req\.method === 'GET' && req\.url === '\/device-traffic'[\s\S]*traffic\.snapshot\(\)/);
assert.match(dataplaneSource, /ready = true;[\s\S]*deviceTrafficAccountingEnabled[\s\S]*setImmediate[\s\S]*traffic\.refresh\(\)/);
assert.match(dataplaneSource, /traffic\.refresh\(\)\.catch/);
assert.match(dataplaneSource, /async function refreshDeviceTraffic\(\)[\s\S]*traffic\.refresh\(\)/);
assert.match(dataplaneSource, /ready = true;[\s\S]*deviceTrafficAccountingEnabled[\s\S]*setImmediate[\s\S]*refreshDeviceTraffic\(\)/);
assert.match(dataplaneSource, /refreshDeviceTraffic\(\)\.catch/);
});
test('traffic selection keeps only unambiguous IPv4 neighbors', () => {
+104
View File
@@ -16,6 +16,40 @@ const connection = (connectionId, type, host, upload, download, sourceIP = devic
download,
chains,
});
const nativeConnection = (connectionId, uploadBytes, downloadBytes, overrides = {}) => ({
id: connectionId,
startedAt: '2026-08-31T10:00:00.000Z',
closedAt: null,
inbound: { tag: 'tproxy-in', type: 'tproxy' },
network: 'tcp',
protocol: 'tls',
source: { ip: device.ip, port: 54_000 },
destination: { domain: 'example.com', ip: null, port: 443, provenance: 'sing-box' },
origin: { kind: 'device', id, label: 'MacBook', provenance: 'source-ip' },
route: {
kind: 'vpn',
scope: 'local-sing-box',
outbound: 'channel-selector',
outboundType: 'selector',
chain: ['channel-primary', 'channel-selector'],
rule: 'final',
},
traffic: {
uploadBytes,
downloadBytes,
uploadBytesPerSecond: '0',
downloadBytesPerSecond: '0',
},
...overrides,
});
const nativeBatch = (connections, overrides = {}) => ({
epoch: 'sing-box-100',
observedAt: '2026-08-31T10:00:00.000Z',
reset: false,
connections,
closedIds: [],
...overrides,
});
test('sing-box route traffic keeps vpn, direct and unknown deltas separate', async () => {
let response = { connections: [
@@ -238,3 +272,73 @@ test('failover activity is zero-work while disabled and uses the existing connec
service.disableActivity();
assert.equal(service.activitySnapshot(500), null);
});
test('native lifecycle batches keep decimal precision and dedupe a same-epoch reconnect reset and final tail', () => {
let now = new Date('2026-08-31T10:00:00.000Z');
const service = createDomainTrafficService({
observe: () => ({ connections: [] }),
devices: () => [],
now: () => now,
});
const initial = nativeConnection('native', '9007199254740993', '10');
service.ingestNative(nativeBatch([initial], { reset: true }));
now = new Date('2026-08-31T10:00:01.000Z');
service.ingestNative(nativeBatch([initial], {
reset: true,
observedAt: now.toISOString(),
}));
assert.equal(service.snapshot().series[0].uploadBytes, '9007199254740993');
assert.equal(service.snapshot().source.activeConnections, 1);
service.enableActivity();
now = new Date('2026-08-31T10:00:02.000Z');
service.ingestNative(nativeBatch([
nativeConnection('native', '9007199254740998', '15', { closedAt: now.toISOString() }),
], {
observedAt: now.toISOString(),
closedIds: ['native'],
}));
assert.equal(service.snapshot().series[0].uploadBytes, '9007199254740998');
assert.equal(service.snapshot().series[0].downloadBytes, '15');
assert.equal(service.snapshot().source.activeConnections, 0);
assert.equal(service.activitySnapshot(0).state, 'active');
now = new Date('2026-08-31T10:00:03.000Z');
service.ingestNative(nativeBatch([
nativeConnection('native', '9007199254740998', '15'),
], {
reset: true,
observedAt: now.toISOString(),
}));
assert.equal(service.snapshot().series[0].uploadBytes, '9007199254740998');
assert.equal(service.snapshot().series[0].downloadBytes, '15');
assert.equal(service.snapshot().source.activeConnections, 1);
now = new Date('2026-08-31T10:00:14.000Z');
service.ingestNative(nativeBatch([], { observedAt: now.toISOString() }));
assert.equal(service.snapshot().observedAt, now.toISOString());
assert.equal(service.snapshot().source.activeConnections, 1);
assert.equal(service.activitySnapshot(0).state, 'quiet');
});
test('one native batch accounts every final tail before lifecycle and UI caps', () => {
const service = createDomainTrafficService({
observe: () => ({ connections: [] }),
devices: () => [],
});
const connections = Array.from({ length: 2_049 }, (_, index) => (
nativeConnection(`closed-${index}`, '1', '1', { closedAt: '2026-08-31T10:00:00.000Z' })
));
service.ingestNative(nativeBatch(connections, {
reset: true,
closedIds: connections.map(({ id: connectionId }) => connectionId),
}));
assert.equal(service.snapshot().series[0].uploadBytes, '2049');
assert.equal(service.snapshot().series[0].downloadBytes, '2049');
assert.equal(service.snapshot().source.activeConnections, 0);
});
+99
View File
@@ -0,0 +1,99 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import test from 'node:test';
const singboxUrl = pathToFileURL(path.resolve('dist/server/singbox.js')).href;
const subscriptionConfig = {
outbounds: [{
type: 'vless',
tag: 'vpn',
server: 'vpn.example.test',
server_port: 443,
uuid: '00000000-0000-4000-8000-000000000000',
tls: { enabled: true },
}],
};
function run(source, { component = 'dataplane', socket = '/tmp/harbor-test.sock' } = {}) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-gateway-native-config-'));
const script = `
const { buildGatewayConfig, buildDualChannelGatewayConfig } = await import(${JSON.stringify(singboxUrl)});
const subscription = ${JSON.stringify(subscriptionConfig)};
process.stdout.write(JSON.stringify({
single: buildGatewayConfig(subscription, 'vpn'),
dual: buildDualChannelGatewayConfig({
primary: { subscriptionConfig: subscription, selectedServerId: 'vpn' },
reserve: { subscriptionConfig: subscription, selectedServerId: 'vpn' },
}),
}));
`;
try {
return spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
cwd: path.resolve('.'),
encoding: 'utf8',
env: {
...process.env,
APP_MODE: 'gateway',
APP_COMPONENT: component,
DATA_DIR: directory,
SING_BOX_CACHE: path.join(directory, 'cache.db'),
SING_BOX_TRAFFIC_SOURCE: source,
DATAPLANE_SOCKET: socket,
},
});
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
}
test('Gateway snapshot keeps the 1.13-compatible config and Clash traffic API', () => {
const result = run('snapshot', { component: '', socket: '' });
assert.equal(result.status, 0, result.stderr);
const configs = JSON.parse(result.stdout);
for (const config of Object.values(configs)) {
assert.equal(config.services, undefined);
assert.deepEqual(config.dns, { independent_cache: true });
assert.deepEqual(config.experimental.clash_api, { external_controller: '127.0.0.1:19090' });
}
});
test('Gateway shadow and native add one secret-free loopback API to single and dual configs', () => {
for (const source of ['shadow', 'native']) {
const result = run(source);
assert.equal(result.status, 0, result.stderr);
const configs = JSON.parse(result.stdout);
for (const config of Object.values(configs)) {
assert.deepEqual(config.services, [{
type: 'api',
listen: '127.0.0.1',
listen_port: 19091,
dashboard: false,
}]);
assert.equal(JSON.stringify(config).includes('secret'), false);
assert.deepEqual(config.dns, {});
assert.deepEqual(config.experimental.clash_api, { external_controller: '127.0.0.1:19090' });
}
}
});
test('Gateway shadow and native reject combined or socket-less topology', () => {
for (const options of [
{ component: '', socket: '' },
{ component: 'control', socket: '' },
{ component: 'dataplane', socket: '' },
]) {
const result = run('native', options);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /require split control\/dataplane topology/);
}
});
test('Gateway rejects unknown traffic sources', () => {
const result = run('disabled');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /must be snapshot, shadow or native/);
});
+185
View File
@@ -0,0 +1,185 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
ensureGatewayNativeApiSecret,
materializeGatewayNativeConfig,
} from '../../dist/server/gatewayNativeRuntime.js';
import { createSingboxRuntime } from '../../dist/server/singboxRuntime.js';
const apiService = {
type: 'api',
listen: '127.0.0.1',
listen_port: 19091,
dashboard: false,
};
function mode(filePath) {
return fs.statSync(filePath).mode & 0o777;
}
async function waitForJson(filePath) {
for (let attempt = 0; attempt < 100; attempt += 1) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error(`Timed out waiting for ${filePath}`);
}
test('Gateway native materialization keeps a stable 0600 secret out of shared config', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-native-secret-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const secretPath = path.join(directory, 'api.secret');
const runtimeConfigPath = path.join(directory, 'runtime-config.json');
const config = { services: [apiService], inbounds: [], outbounds: [] };
const first = materializeGatewayNativeConfig(config, {
apiPort: 19091,
secretPath,
runtimeConfigPath,
});
const second = materializeGatewayNativeConfig(config, {
apiPort: 19091,
secretPath,
runtimeConfigPath,
});
assert.match(first.secret, /^[0-9a-f]{64}$/);
assert.equal(second.secret, first.secret);
assert.equal(first.warning, null);
assert.equal(mode(secretPath), 0o600);
assert.equal(mode(runtimeConfigPath), 0o600);
assert.equal(JSON.stringify(config).includes(first.secret), false);
assert.equal(JSON.parse(fs.readFileSync(runtimeConfigPath, 'utf8')).services[0].secret, first.secret);
});
test('Gateway native secret rejects symlinks and repairs regular-file permissions', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-native-secret-mode-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const regularPath = path.join(directory, 'regular.secret');
fs.writeFileSync(regularPath, 'a'.repeat(64), { mode: 0o644 });
assert.equal(ensureGatewayNativeApiSecret(regularPath), 'a'.repeat(64));
assert.equal(mode(regularPath), 0o600);
const linkPath = path.join(directory, 'linked.secret');
fs.symlinkSync(regularPath, linkPath);
assert.throws(() => ensureGatewayNativeApiSecret(linkPath));
});
test('materialization strips every API service and returns a warning on unsafe input', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-native-safe-config-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const config = {
services: [
apiService,
{ type: 'api', listen: '0.0.0.0', listen_port: 19092, dashboard: false },
{ type: 'resolved' },
],
};
const result = materializeGatewayNativeConfig(config, {
apiPort: 19091,
secretPath: path.join(directory, 'api.secret'),
runtimeConfigPath: path.join(directory, 'runtime-config.json'),
});
const runtimeConfig = JSON.parse(fs.readFileSync(result.configPath, 'utf8'));
assert.equal(result.secret, null);
assert.match(result.warning, /expected exactly one native API service/);
assert.deepEqual(runtimeConfig.services, [{ type: 'resolved' }]);
});
test('runtime starts the VPN-safe config and reports native materialization warnings', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-native-runtime-'));
const binDirectory = path.join(directory, 'bin');
const configPath = path.join(directory, 'shared.json');
const capturedPath = path.join(directory, 'captured.json');
fs.mkdirSync(binDirectory);
fs.writeFileSync(configPath, JSON.stringify({
services: [{ ...apiService, listen: '0.0.0.0' }],
inbounds: [],
outbounds: [],
}));
fs.writeFileSync(path.join(binDirectory, 'sing-box'), `#!/usr/bin/env node
const fs = require('node:fs');
const configPath = process.argv[process.argv.indexOf('-c') + 1];
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (config.services?.some((service) => service.type === 'api')) process.exit(7);
if (process.argv[2] === 'check') process.exit(0);
fs.writeFileSync(process.env.HARBOR_CAPTURED_CONFIG, JSON.stringify(config));
process.on('SIGTERM', () => process.exit(0));
setInterval(() => {}, 60_000);
`);
fs.chmodSync(path.join(binDirectory, 'sing-box'), 0o755);
const previousPath = process.env.PATH;
process.env.PATH = `${binDirectory}:${previousPath}`;
process.env.HARBOR_CAPTURED_CONFIG = capturedPath;
const runtime = createSingboxRuntime({
configPath,
nativeApi: {
apiPort: 19091,
secretPath: path.join(directory, 'api.secret'),
runtimeConfigPath: path.join(directory, 'runtime-config.json'),
},
});
t.after(async () => {
await runtime.stop();
process.env.PATH = previousPath;
delete process.env.HARBOR_CAPTURED_CONFIG;
fs.rmSync(directory, { recursive: true, force: true });
});
const checked = runtime.checkConfig(JSON.parse(fs.readFileSync(configPath, 'utf8')));
assert.match(checked.warning, /must be unauthenticated base config/);
const state = await runtime.apply();
assert.equal(state.running, true);
assert.match(state.nativeApiWarning, /must be unauthenticated base config/);
assert.equal(runtime.nativeApiSecret, null);
assert.equal((await waitForJson(capturedPath)).services, undefined);
});
test('snapshot runtime strips a native API left by the previous mode before starting sing-box', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-snapshot-runtime-'));
const binDirectory = path.join(directory, 'bin');
const configPath = path.join(directory, 'shared.json');
const runtimeConfigPath = path.join(directory, 'runtime-config.json');
const capturedPath = path.join(directory, 'captured.json');
fs.mkdirSync(binDirectory);
fs.writeFileSync(configPath, JSON.stringify({
services: [apiService],
inbounds: [],
outbounds: [],
}));
fs.writeFileSync(path.join(binDirectory, 'sing-box'), `#!/usr/bin/env node
const fs = require('node:fs');
const configPath = process.argv[process.argv.indexOf('-c') + 1];
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (config.services?.some((service) => service.type === 'api')) process.exit(7);
if (process.argv[2] === 'check') process.exit(0);
fs.writeFileSync(process.env.HARBOR_CAPTURED_CONFIG, JSON.stringify(config));
process.on('SIGTERM', () => process.exit(0));
setInterval(() => {}, 60_000);
`);
fs.chmodSync(path.join(binDirectory, 'sing-box'), 0o755);
const previousPath = process.env.PATH;
process.env.PATH = `${binDirectory}:${previousPath}`;
process.env.HARBOR_CAPTURED_CONFIG = capturedPath;
const runtime = createSingboxRuntime({ configPath, gatewayRuntimeConfigPath: runtimeConfigPath });
t.after(async () => {
await runtime.stop();
process.env.PATH = previousPath;
delete process.env.HARBOR_CAPTURED_CONFIG;
fs.rmSync(directory, { recursive: true, force: true });
});
const state = await runtime.apply();
assert.equal(state.running, true);
assert.equal(mode(runtimeConfigPath), 0o600);
assert.equal((await waitForJson(capturedPath)).services, undefined);
});
+692
View File
@@ -0,0 +1,692 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { ConnectionEventType } from '../../dist/server/generated/daemon/started_service_pb.js';
import { createLiveTrafficLedger } from '../../dist/server/services/liveTrafficService.js';
function connection(id, overrides = {}) {
return {
id,
inbound: 'mixed-in',
inboundType: 'mixed',
network: 'tcp',
source: '127.0.0.1:54000',
destination: '203.0.113.10:443',
domain: 'example.test',
protocol: 'tls',
createdAt: 1_700_000_000_000n,
closedAt: 0n,
uplinkTotal: 0n,
downlinkTotal: 0n,
outbound: 'test-vpn',
outboundType: 'vless',
rule: 'final',
chainList: ['test-vpn'],
...overrides,
};
}
function event(type, id, overrides = {}) {
return {
type,
id,
uplinkDelta: 0n,
downlinkDelta: 0n,
closedAt: 0n,
...overrides,
};
}
function status(connectionsIn, uplinkTotal, downlinkTotal) {
return { connectionsIn, uplinkTotal, downlinkTotal };
}
test('ledger applies NEW, UPDATE and only the final CLOSED tail once', () => {
let now = new Date('2026-08-31T10:00:00.000Z');
const ledger = createLiveTrafficLedger({ now: () => now });
ledger.beginEpoch(1_700_000_000_000n, '1.14.0-rc.5', 4);
ledger.applyConnections({ reset: true, events: [] });
ledger.applyStatus(status(0, 0n, 0n));
const opened = connection('a', { uplinkTotal: 10n, downlinkTotal: 20n });
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'a', { connection: opened })],
});
let snapshot = ledger.snapshot();
assert.deepEqual(snapshot.connections[0].traffic, {
uploadBytes: '10',
downloadBytes: '20',
uploadBytesPerSecond: '0',
downloadBytesPerSecond: '0',
});
assert.equal(snapshot.connections[0].origin.label, 'Этот Mac');
assert.equal(snapshot.connections[0].closedAt, null);
assert.equal(snapshot.connections[0].route.kind, 'vpn');
now = new Date('2026-08-31T10:00:01.000Z');
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_UPDATE, 'a', {
uplinkDelta: 5n,
downlinkDelta: 7n,
})],
});
snapshot = ledger.snapshot();
assert.deepEqual(snapshot.connections[0].traffic, {
uploadBytes: '15',
downloadBytes: '27',
uploadBytesPerSecond: '5',
downloadBytesPerSecond: '7',
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'a', {
connection: connection('a', {
closedAt: BigInt(now.getTime()),
uplinkTotal: 18n,
downlinkTotal: 30n,
}),
})],
});
const firstClosed = ledger.snapshot().connections[0];
now = new Date('2026-08-31T10:00:10.000Z');
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'a', {
connection: connection('a', { uplinkTotal: 999n, downlinkTotal: 999n }),
})],
});
ledger.applyStatus(status(0, 18n, 30n));
snapshot = ledger.snapshot();
assert.equal(snapshot.source.state, 'live');
assert.equal(snapshot.summary.active, 0);
assert.equal(snapshot.summary.recent, 1);
assert.equal(snapshot.connections[0].closedAt, firstClosed.closedAt);
assert.deepEqual(snapshot.connections[0].traffic, {
uploadBytes: '18',
downloadBytes: '30',
uploadBytesPerSecond: '0',
downloadBytesPerSecond: '0',
});
assert.equal(snapshot.source.unattributedUploadBytes, '0');
assert.equal(snapshot.source.unattributedDownloadBytes, '0');
ledger.markStopped();
snapshot = ledger.snapshot();
assert.equal(snapshot.source.state, 'stopped');
assert.equal(snapshot.summary.recent, 0);
});
test('connection churn does not clear rates before the next update tick', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'a', {
connection: connection('a'),
})],
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_UPDATE, 'a', {
uplinkDelta: 5n,
downlinkDelta: 7n,
})],
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'b', {
connection: connection('b'),
})],
});
const active = new Map(ledger.snapshot().connections.map((item) => [item.id, item]));
assert.equal(active.get('a').traffic.uploadBytesPerSecond, '5');
assert.equal(active.get('a').traffic.downloadBytesPerSecond, '7');
assert.equal(active.get('b').traffic.uploadBytesPerSecond, '0');
});
test('a connection completed between polls remains visible until the exact 30 second boundary', () => {
let now = new Date('2026-08-31T10:00:00.000Z');
const ledger = createLiveTrafficLedger({ now: () => now });
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: false,
events: [
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'quick', {
connection: connection('quick', {
createdAt: BigInt(now.getTime()),
uplinkTotal: 2n,
downlinkTotal: 3n,
}),
}),
event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'quick', {
uplinkDelta: 5n,
downlinkDelta: 7n,
}),
],
});
let snapshot = ledger.snapshot();
assert.deepEqual(snapshot.summary, {
active: 0,
recent: 1,
visible: 1,
recognized: 0,
unresolved: 0,
unresolvedOrigin: 0,
truncated: false,
});
assert.equal(snapshot.connections[0].closedAt, '2026-08-31T10:00:00.000Z');
assert.deepEqual(snapshot.connections[0].traffic, {
uploadBytes: '7',
downloadBytes: '10',
uploadBytesPerSecond: '0',
downloadBytesPerSecond: '0',
});
now = new Date('2026-08-31T10:00:29.999Z');
assert.equal(ledger.snapshot().summary.recent, 1);
now = new Date('2026-08-31T10:00:30.000Z');
snapshot = ledger.snapshot();
assert.equal(snapshot.summary.recent, 0);
assert.equal(snapshot.connections.length, 0);
});
test('a connection already closed in an RC5 reset remains visible as recent', () => {
const ledger = createLiveTrafficLedger({ now: () => new Date('2026-08-31T10:00:02.000Z') });
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'reset-closed', {
connection: connection('reset-closed', {
createdAt: BigInt(Date.parse('2026-08-31T10:00:00.000Z')),
closedAt: BigInt(Date.parse('2026-08-31T10:00:01.000Z')),
uplinkTotal: 3n,
downlinkTotal: 7n,
}),
})],
});
const snapshot = ledger.snapshot();
assert.equal(snapshot.summary.active, 0);
assert.equal(snapshot.summary.recent, 1);
assert.equal(snapshot.connections[0].id, 'reset-closed');
assert.equal(snapshot.connections[0].closedAt, '2026-08-31T10:00:01.000Z');
});
test('a genuinely newer lifecycle with the same UUID supersedes its recent tombstone', () => {
let now = new Date('2026-08-31T10:00:00.000Z');
const ledger = createLiveTrafficLedger({ now: () => now });
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({ reset: true, events: [] });
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'revived', {
connection: connection('revived', { createdAt: BigInt(now.getTime()), uplinkTotal: 3n }),
})],
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'revived')],
});
const firstClosedAt = ledger.snapshot().connections[0].closedAt;
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'revived', {
connection: connection('revived', { createdAt: BigInt(now.getTime()), uplinkTotal: 999n }),
})],
});
assert.equal(ledger.snapshot().summary.active, 0);
assert.equal(ledger.snapshot().connections[0].closedAt, firstClosedAt);
now = new Date('2026-08-31T10:00:01.000Z');
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'revived', {
connection: connection('revived', { createdAt: BigInt(now.getTime()), uplinkTotal: 4n }),
})],
});
const snapshot = ledger.snapshot();
assert.equal(snapshot.summary.active, 1);
assert.equal(snapshot.summary.recent, 0);
assert.equal(snapshot.connections[0].closedAt, null);
assert.equal(snapshot.connections[0].traffic.uploadBytes, '4');
});
test('reset reconciles active deltas without treating earlier closed traffic as a gap', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: true,
events: [
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'closed-a', {
connection: connection('closed-a', { uplinkTotal: 100n }),
}),
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'active-b', {
connection: connection('active-b', { uplinkTotal: 10n }),
}),
],
});
ledger.applyStatus(status(2, 110n, 0n));
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'closed-a')],
});
ledger.applyStatus(status(1, 110n, 0n));
ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'active-b', {
connection: connection('active-b', { uplinkTotal: 20n }),
})],
});
ledger.applyStatus(status(1, 120n, 0n));
const snapshot = ledger.snapshot();
assert.equal(snapshot.source.state, 'live');
assert.equal(snapshot.source.unattributedUploadBytes, '0');
assert.equal(snapshot.summary.active, 1);
assert.equal(snapshot.summary.recent, 1);
assert.equal(snapshot.connections.find(({ id }) => id === 'active-b').traffic.uploadBytes, '20');
});
test('reset can revive the same lifecycle tombstone without counting its totals twice', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'same', {
connection: connection('same', { uplinkTotal: 10n }),
})],
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'same')],
});
ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'same', {
connection: connection('same', { uplinkTotal: 10n }),
})],
});
ledger.applyStatus(status(1, 10n, 0n));
ledger.applyStatus(status(1, 10n, 0n));
ledger.applyStatus(status(1, 10n, 0n));
const snapshot = ledger.snapshot();
assert.equal(snapshot.source.state, 'live');
assert.equal(snapshot.source.unattributedUploadBytes, '0');
assert.equal(snapshot.summary.active, 1);
assert.equal(snapshot.summary.recent, 0);
});
test('destination hostnames remain recognized without protocol sniffing', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'hostname', {
connection: connection('hostname', {
destination: 'Example.COM:443',
domain: '',
protocol: '',
}),
})],
});
const snapshot = ledger.snapshot();
assert.equal(snapshot.summary.recognized, 1);
assert.deepEqual(snapshot.connections[0].destination, {
domain: 'example.com',
ip: null,
port: 443,
provenance: 'sing-box',
});
});
test('CLOSED keeps newly available native domain, protocol and route metadata', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'late-metadata', {
connection: connection('late-metadata', {
domain: '',
protocol: '',
uplinkTotal: 1n,
downlinkTotal: 2n,
}),
})],
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'late-metadata', {
connection: connection('late-metadata', {
domain: 'recognized.example',
protocol: 'http2',
outbound: 'direct',
outboundType: 'direct',
chainList: ['direct'],
rule: 'domain-final',
uplinkTotal: 3n,
downlinkTotal: 5n,
}),
})],
});
const closed = ledger.snapshot().connections[0];
assert.equal(closed.destination.domain, 'recognized.example');
assert.equal(closed.protocol, 'http2');
assert.deepEqual(closed.route, {
kind: 'direct',
scope: 'local-sing-box',
outbound: 'direct',
outboundType: 'direct',
chain: ['direct'],
rule: 'domain-final',
});
assert.equal(closed.traffic.uploadBytes, '3');
assert.equal(closed.traffic.downloadBytes, '5');
});
test('CLOSED partial route metadata keeps route kind consistent with its outbound', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'partial-route', {
connection: connection('partial-route'),
})],
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'partial-route', {
connection: connection('partial-route', {
outbound: '',
outboundType: '',
chainList: ['late-hop'],
rule: 'late-rule',
}),
})],
});
const route = ledger.snapshot().connections[0].route;
assert.equal(route.kind, 'vpn');
assert.equal(route.outbound, 'test-vpn');
assert.equal(route.outboundType, 'vless');
assert.deepEqual(route.chain, ['late-hop']);
assert.equal(route.rule, 'late-rule');
});
test('transport errors preserve the last observed data timestamp', () => {
let now = new Date('2026-08-31T10:00:00.000Z');
const ledger = createLiveTrafficLedger({ now: () => now });
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({ reset: true, events: [] });
ledger.applyStatus(status(0, 0n, 0n));
const lastGood = ledger.snapshot();
now = new Date('2026-08-31T10:01:00.000Z');
ledger.markTransportError(new Error('stream ended'));
const firstStale = ledger.snapshot();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.markTransportError(new Error('stream ended again'));
const stale = ledger.snapshot();
assert.equal(stale.source.state, 'stale');
assert.equal(stale.observedAt, lastGood.observedAt);
assert.equal(firstStale.observedAt, lastGood.observedAt);
assert.equal(stale.sequence, lastGood.sequence + 3);
});
test('reset is idempotent, a restart creates a clean epoch, and settled UUIDs are bounded', () => {
const ledger = createLiveTrafficLedger();
const active = connection('active', { uplinkTotal: 10n, downlinkTotal: 20n });
const closed = connection('closed', {
closedAt: 1_700_000_001_000n,
uplinkTotal: 5n,
downlinkTotal: 7n,
});
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
const reset = {
reset: true,
events: [
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'active', { connection: active }),
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'closed', { connection: closed }),
],
};
const firstReset = ledger.applyConnections(reset);
ledger.applyStatus(status(1, 15n, 27n));
const repeatedReset = ledger.applyConnections(reset);
ledger.applyStatus(status(1, 15n, 27n));
assert.equal(ledger.snapshot().source.state, 'live');
assert.equal(ledger.snapshot().source.unattributedUploadBytes, '0');
assert.deepEqual(ledger.snapshot().connections.map(({ id }) => id), ['active']);
assert.equal(ledger.snapshot().summary.recent, 0);
assert.deepEqual(firstReset.connections.map(({ id }) => id), ['active', 'closed']);
assert.deepEqual(repeatedReset.connections.map(({ id }) => id), ['active']);
const settledEvents = Array.from({ length: 2_049 }, (_, index) => {
const id = `settled-${String(index).padStart(4, '0')}`;
return event(ConnectionEventType.CONNECTION_EVENT_CLOSED, id);
});
ledger.applyConnections({ reset: false, events: settledEvents });
ledger.applyConnections({
reset: false,
events: [
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'settled-0000', {
connection: connection('settled-0000'),
}),
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'settled-2048', {
connection: connection('settled-2048'),
}),
],
});
assert.deepEqual(
ledger.snapshot().connections.map(({ id }) => id).sort(),
['active', 'settled-0000'],
);
ledger.applyConnections({
reset: false,
events: [
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'epoch-recent', {
connection: connection('epoch-recent'),
}),
event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'epoch-recent'),
],
});
assert.equal(ledger.snapshot().summary.recent, 1);
ledger.beginEpoch(101n, '1.14.0-rc.5', 4);
const restarted = ledger.snapshot();
assert.equal(restarted.epoch, 'sing-box-101');
assert.equal(restarted.source.state, 'connecting');
assert.equal(restarted.summary.active, 0);
assert.equal(restarted.summary.recent, 0);
assert.equal(restarted.source.unattributedUploadBytes, '0');
});
test('three consecutive status mismatches degrade without assigning the byte gap', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'a', {
connection: connection('a', { uplinkTotal: 10n, downlinkTotal: 20n }),
})],
});
assert.ok(ledger.applyStatus(status(1, 15n, 27n)));
assert.ok(ledger.applyStatus(status(1, 15n, 27n)));
assert.equal(ledger.snapshot().source.state, 'live');
assert.equal(ledger.applyStatus(status(1, 15n, 27n)), null);
let snapshot = ledger.snapshot();
assert.equal(snapshot.source.state, 'degraded');
assert.equal(snapshot.source.unattributedUploadBytes, '5');
assert.equal(snapshot.source.unattributedDownloadBytes, '7');
assert.deepEqual(snapshot.connections[0].traffic, {
uploadBytes: '10',
downloadBytes: '20',
uploadBytesPerSecond: '0',
downloadBytesPerSecond: '0',
});
assert.ok(ledger.applyStatus(status(1, 10n, 20n)));
snapshot = ledger.snapshot();
assert.equal(snapshot.source.state, 'live');
assert.equal(snapshot.source.unattributedUploadBytes, '0');
assert.equal(snapshot.source.unattributedDownloadBytes, '0');
});
test('a missing NEW does not double-count UPDATE before an absolute CLOSED total', () => {
const ledger = createLiveTrafficLedger({ now: () => new Date('2023-11-14T22:13:21.000Z') });
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({ reset: true, events: [] });
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_UPDATE, 'missed', {
uplinkDelta: 10n,
downlinkDelta: 20n,
})],
});
ledger.applyStatus(status(0, 10n, 20n));
assert.equal(ledger.snapshot().source.unattributedUploadBytes, '10');
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'missed', {
connection: connection('missed', {
closedAt: 1_700_000_001_000n,
uplinkTotal: 15n,
downlinkTotal: 27n,
}),
})],
});
ledger.applyStatus(status(0, 15n, 27n));
let snapshot = ledger.snapshot();
assert.equal(snapshot.source.unattributedUploadBytes, '15');
assert.equal(snapshot.source.unattributedDownloadBytes, '27');
assert.equal(snapshot.summary.recent, 1);
assert.equal(snapshot.connections[0].closedAt, '2023-11-14T22:13:21.000Z');
assert.deepEqual(snapshot.connections[0].traffic, {
uploadBytes: '15',
downloadBytes: '27',
uploadBytesPerSecond: '0',
downloadBytesPerSecond: '0',
});
ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'metadata-less', {
uplinkDelta: 3n,
downlinkDelta: 4n,
})],
});
ledger.applyStatus(status(0, 18n, 31n));
snapshot = ledger.snapshot();
assert.equal(snapshot.source.unattributedUploadBytes, '18');
assert.equal(snapshot.source.unattributedDownloadBytes, '31');
});
test('snapshot caps visibility at 256 while summary covers every active connection', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
ledger.applyConnections({
reset: false,
events: [
event(ConnectionEventType.CONNECTION_EVENT_NEW, 'recent', { connection: connection('recent') }),
event(ConnectionEventType.CONNECTION_EVENT_CLOSED, 'recent'),
],
});
const events = Array.from({ length: 257 }, (_, index) => {
const id = index === 255 ? 'z-tie' : index === 256 ? 'a-tie' : `id-${String(index).padStart(3, '0')}`;
const createdAt = index >= 255 ? 1_700_000_000_255n : 1_700_000_000_000n + BigInt(index);
return event(ConnectionEventType.CONNECTION_EVENT_NEW, id, {
connection: connection(id, {
createdAt,
domain: index % 2 === 0 ? `service-${index}.example` : '',
destination: `203.0.113.${index % 255}:443`,
outbound: index % 3 === 0 ? 'direct' : 'test-vpn',
outboundType: index % 3 === 0 ? 'direct' : 'vless',
}),
});
});
ledger.applyConnections({ reset: true, events });
const snapshot = ledger.snapshot();
assert.deepEqual(snapshot.summary, {
active: 257,
recent: 1,
visible: 256,
recognized: 129,
unresolved: 128,
unresolvedOrigin: 0,
truncated: true,
});
assert.equal(snapshot.connections.length, 256);
assert.deepEqual(snapshot.connections.slice(0, 2).map(({ id }) => id), ['a-tie', 'z-tie']);
assert.equal(snapshot.connections.some(({ id }) => id === 'id-000'), false);
assert.equal(snapshot.connections.some(({ id }) => id === 'recent'), false);
});
test('gateway projection uses selector chains and resolves every active origin from the current device map', () => {
let deviceVisible = false;
const ledger = createLiveTrafficLedger({
gateway: true,
resolveOrigin: (sourceIp) => deviceVisible
? { kind: 'device', id: 'device-a', label: 'MacBook', provenance: 'source-ip' }
: { kind: 'unknown', id: null, label: sourceIp, provenance: 'unknown' },
});
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
const first = ledger.applyConnections({
reset: true,
events: [event(ConnectionEventType.CONNECTION_EVENT_NEW, 'gateway', {
connection: connection('gateway', {
inbound: 'tproxy-in',
inboundType: 'tproxy',
source: '192.168.50.7:54000',
outbound: 'channel-selector',
outboundType: 'selector',
chainList: ['channel-primary', 'channel-selector'],
}),
})],
});
assert.equal(first.connections[0].route.kind, 'vpn');
assert.equal(ledger.snapshot().summary.unresolvedOrigin, 1);
deviceVisible = true;
assert.equal(ledger.snapshot().summary.unresolvedOrigin, 0);
assert.equal(ledger.snapshot().connections[0].origin.id, 'device-a');
const second = ledger.applyConnections({
reset: false,
events: [event(ConnectionEventType.CONNECTION_EVENT_UPDATE, 'gateway', { uplinkDelta: 1n })],
});
assert.equal(second.connections[0].origin.id, 'device-a');
assert.equal(second.connections[0].traffic.uploadBytes, '1');
assert.equal(ledger.snapshot().capabilities.deviceAttribution, true);
});
test('recent closed storage is bounded independently from the 256-row response cap', () => {
const ledger = createLiveTrafficLedger();
ledger.beginEpoch(100n, '1.14.0-rc.5', 4);
const events = Array.from({ length: 2_049 }, (_, index) => {
const id = `recent-${String(index).padStart(4, '0')}`;
return event(ConnectionEventType.CONNECTION_EVENT_CLOSED, id, { connection: connection(id) });
});
const projection = ledger.applyConnections({ reset: false, events });
const snapshot = ledger.snapshot();
assert.equal(projection.connections.length, 2_049);
assert.equal(projection.closedIds.length, 2_049);
assert.equal(snapshot.summary.active, 0);
assert.equal(snapshot.summary.recent, 2_048);
assert.equal(snapshot.summary.visible, 256);
assert.equal(snapshot.summary.truncated, true);
assert.equal(snapshot.connections[0].id, 'recent-0001');
assert.equal(snapshot.connections.some(({ id }) => id === 'recent-0000'), false);
});
+208
View File
@@ -0,0 +1,208 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
createLiveTrafficRoute,
enrichLiveTrafficDeviceLabels,
} from '../../dist/server/http/routes/liveTrafficRoute.js';
function response() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(payload) {
this.rawPayload = payload;
this.payload = JSON.parse(payload);
},
};
}
const snapshot = {
apiVersion: 1,
epoch: 'sing-box-1700000000000',
sequence: 7,
observedAt: '2026-08-31T10:00:00.000Z',
capabilities: {
lifecycle: true,
deviceAttribution: false,
applicationAttribution: false,
},
source: {
transport: 'native',
state: 'live',
completeness: 'lifecycle',
singBoxVersion: '1.14.0-rc.5',
singBoxApiVersion: 4,
error: null,
unattributedUploadBytes: '0',
unattributedDownloadBytes: '0',
},
summary: {
active: 0,
recent: 0,
visible: 0,
recognized: 0,
unresolved: 0,
unresolvedOrigin: 0,
truncated: false,
},
connections: [],
};
test('split Gateway control reads the cached socket while combined Gateway keeps no collector', () => {
const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(index, /const liveTraffic = clientLiveTraffic \|\| \(remoteDataplane \? \{[\s\S]*observeLiveTraffic\(\)[\s\S]*\} : null\)/);
assert.match(index, /createLiveTrafficRoute\(\{[\s\S]*traffic: liveTraffic,[\s\S]*deviceInventory: remoteDataplane \? deviceInventory : null/);
assert.match(index, /clientLiveTraffic\?\.start\(\)/);
assert.doesNotMatch(index, /liveTraffic\?\.start\(\)/);
});
test('GET returns the injected cached snapshot without another data-source operation', async () => {
let snapshots = 0;
const route = createLiveTrafficRoute({
traffic: {
snapshot() {
snapshots += 1;
return snapshot;
},
},
});
const res = response();
assert.equal(await route.handle({ method: 'GET', url: '/api/traffic/live?ignored=1' }, res), true);
assert.equal(snapshots, 1);
assert.equal(res.status, 200);
assert.deepEqual(res.headers, { 'content-type': 'application/json; charset=utf-8' });
assert.deepEqual(res.payload, snapshot);
});
test('async Gateway snapshots are validated and known dev labels are enriched without changing summary', async () => {
const gatewaySnapshot = {
...snapshot,
capabilities: { ...snapshot.capabilities, deviceAttribution: true },
summary: {
active: 300,
recent: 0,
visible: 256,
recognized: 300,
unresolved: 0,
unresolvedOrigin: 299,
truncated: true,
},
connections: Array.from({ length: 256 }, (_, index) => ({
id: `connection-${String(index).padStart(3, '0')}`,
startedAt: new Date(Date.parse(snapshot.observedAt) - index * 1000).toISOString(),
closedAt: null,
inbound: { tag: 'tproxy-in', type: 'tproxy' },
network: 'tcp',
protocol: 'tls',
source: { ip: index === 0 ? '192.168.50.7' : '192.168.50.8', port: 50_000 + index },
destination: { domain: 'example.com', ip: '203.0.113.1', port: 443, provenance: 'sing-box' },
origin: index === 0
? { kind: 'device', id: 'dev_0011223344556677', label: '192.168.50.7', provenance: 'source-ip' }
: { kind: 'unknown', id: null, label: 'Неизвестное устройство', provenance: 'unknown' },
route: {
kind: 'vpn',
scope: 'local-sing-box',
outbound: 'proxy',
outboundType: 'selector',
chain: ['proxy'],
rule: 'default',
},
traffic: {
uploadBytes: '10',
downloadBytes: '20',
uploadBytesPerSecond: '1',
downloadBytesPerSecond: '2',
},
})),
};
const original = structuredClone(gatewaySnapshot);
const route = createLiveTrafficRoute({
traffic: { snapshot: async () => gatewaySnapshot },
deviceInventory: {
snapshot: () => ({
devices: [{
id: 'dev_0011223344556677',
alias: 'Гостиная',
hostname: 'tv.local',
ip: '192.168.50.7',
}],
}),
},
});
const res = response();
assert.equal(await route.handle({ method: 'GET', url: '/api/traffic/live' }, res), true);
assert.equal(res.payload.connections[0].origin.label, 'Гостиная');
assert.equal(res.payload.connections[1].origin.kind, 'unknown');
assert.deepEqual(res.payload.summary, gatewaySnapshot.summary);
assert.deepEqual(gatewaySnapshot, original);
});
test('device label enrichment follows alias, hostname and IP without identifying unknown origins', () => {
const connection = {
id: 'connection-1',
startedAt: snapshot.observedAt,
closedAt: null,
inbound: { tag: 'tproxy-in', type: 'tproxy' },
network: 'tcp',
protocol: 'tls',
source: { ip: '192.168.50.7', port: 50_000 },
destination: { domain: 'example.com', ip: '203.0.113.1', port: 443, provenance: 'sing-box' },
origin: { kind: 'device', id: 'dev_0011223344556677', label: '192.168.50.7', provenance: 'source-ip' },
route: { kind: 'vpn', scope: 'local-sing-box', outbound: 'proxy', outboundType: 'selector', chain: ['proxy'], rule: 'default' },
traffic: { uploadBytes: '1', downloadBytes: '2', uploadBytesPerSecond: '0', downloadBytesPerSecond: '0' },
};
const source = {
...snapshot,
capabilities: { ...snapshot.capabilities, deviceAttribution: true },
summary: { ...snapshot.summary, active: 1, visible: 1, recognized: 1 },
connections: [connection],
};
const labels = (device) => enrichLiveTrafficDeviceLabels(source, { devices: [device] })
.connections[0].origin.label;
assert.equal(labels({ id: connection.origin.id, alias: ' ТВ ', hostname: 'tv.local', ip: '192.168.50.7' }), 'ТВ');
assert.equal(labels({ id: connection.origin.id, alias: '', hostname: 'tv.local', ip: '192.168.50.7' }), 'tv.local');
assert.equal(labels({ id: connection.origin.id, alias: '', hostname: null, ip: '192.168.50.7' }), '192.168.50.7');
assert.equal(labels({ id: 'dev_ffffffffffffffff', alias: 'Чужой', ip: connection.source.ip }), connection.origin.label);
const unknown = { ...source, connections: [{ ...connection, origin: { kind: 'unknown', id: null, label: 'Неизвестно', provenance: 'unknown' } }] };
assert.equal(enrichLiveTrafficDeviceLabels(unknown, { devices: [{ ...connection.origin, id: 'dev_0011223344556677', alias: 'Не угадывать' }] }).connections[0].origin.label, 'Неизвестно');
});
test('route rejects malformed cached snapshots before responding', async () => {
const route = createLiveTrafficRoute({
traffic: { snapshot: async () => ({ ...snapshot, apiVersion: 2 }) },
});
await assert.rejects(
route.handle({ method: 'GET', url: '/api/traffic/live' }, response()),
/apiVersion 1/,
);
});
test('route ignores other paths and rejects mutation methods without reading the cache', async () => {
let snapshots = 0;
const route = createLiveTrafficRoute({
traffic: { snapshot: () => { snapshots += 1; return snapshot; } },
});
assert.equal(await route.handle({ method: 'GET', url: '/api/traffic/history' }, response()), false);
await assert.rejects(
route.handle({ method: 'POST', url: '/api/traffic/live' }, response()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
assert.equal(snapshots, 0);
});
test('route is unavailable when no traffic collector exists', async () => {
const route = createLiveTrafficRoute({ traffic: null });
await assert.rejects(
route.handle({ method: 'GET', url: '/api/traffic/live' }, response()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
});
+46
View File
@@ -40,6 +40,28 @@ const snapshot = {
},
domainTraffic: {
observedAt,
source: {
error: null,
mode: 'shadow',
writer: 'snapshot',
activeConnections: 4,
native: {
state: 'degraded',
epoch: 'epoch-1',
sequence: 7,
observedAt,
active: 5,
unattributedUploadBytes: '11',
unattributedDownloadBytes: '22',
},
shadow: {
activeDifference: 1,
uploadDifferenceBytes: '-30',
downloadDifferenceBytes: '40',
routeMismatches: 2,
deviceMismatches: 3,
},
},
overflowConnections: '2',
attributionEvents: {
unresolved_host: '3',
@@ -97,6 +119,15 @@ test('Prometheus exposition keeps exact counters, stable identity and escaped na
assert.match(output, /harbor_domain_traffic_attribution_events_total\{outcome="unresolved_host"\} 3/);
assert.match(output, /harbor_domain_traffic_attribution_events_total\{outcome="unknown_device"\} 4/);
assert.match(output, /harbor_domain_traffic_attribution_events_total\{outcome="unsupported_source"\} 5/);
assert.match(output, /harbor_traffic_collector_info\{mode="shadow",writer="snapshot"\} 1/);
assert.match(output, /harbor_traffic_collector_state\{state="degraded"\} 1/);
assert.match(output, /harbor_traffic_collector_unattributed_bytes\{direction="download"\} 22/);
assert.match(output, /harbor_traffic_collector_unattributed_bytes\{direction="upload"\} 11/);
assert.match(output, /harbor_traffic_shadow_active_difference 1/);
assert.match(output, /harbor_traffic_shadow_difference_bytes\{direction="download"\} 40/);
assert.match(output, /harbor_traffic_shadow_difference_bytes\{direction="upload"\} -30/);
assert.match(output, /harbor_traffic_shadow_route_mismatches 2/);
assert.match(output, /harbor_traffic_shadow_device_mismatches 3/);
assert.doesNotMatch(output, /harbor_device_domain_traffic_bytes_total\{[^\n]*name=/);
assert.doesNotMatch(output, /harbor_device_(?:direct_ipv4_packet|singbox_tracked)_bytes_total\{[^\n]*(?:name|ip|mac|server)=/);
assert.equal(output.endsWith('\n'), true);
@@ -122,6 +153,15 @@ test('Prometheus response uses the negotiated legacy text contract without mutat
assert.deepEqual(snapshot, before);
});
test('legacy combined Gateway snapshots keep existing metrics without collector diagnostics', () => {
const legacy = structuredClone(snapshot);
legacy.domainTraffic.source = { error: null, activeConnections: 0 };
const output = renderPrometheusMetrics(legacy);
assert.match(output, /harbor_singbox_tracked_bytes_total/);
assert.doesNotMatch(output, /harbor_traffic_collector_info/);
});
test('invalid canonical counters fail the scrape instead of publishing corrupt values', () => {
const invalid = { traffic: { gatewayBytes: 'broken', proxyBytes: '0' }, devices: [] };
assert.throws(
@@ -135,6 +175,12 @@ test('invalid canonical counters fail the scrape instead of publishing corrupt v
const invalidRoute = structuredClone(snapshot);
invalidRoute.domainTraffic.routes[0].outbound = 'vpn-server-tag';
assert.throws(() => renderPrometheusMetrics(invalidRoute), /Invalid sing-box outbound labels/);
const invalidCollector = structuredClone(snapshot);
invalidCollector.domainTraffic.source.mode = 'future';
assert.throws(() => renderPrometheusMetrics(invalidCollector), /Invalid traffic collector labels/);
const invalidShadow = structuredClone(snapshot);
invalidShadow.domainTraffic.source.shadow.uploadDifferenceBytes = '1.5';
assert.throws(() => renderPrometheusMetrics(invalidShadow), /Invalid Prometheus gauge/);
});
function routeResponse() {
+270
View File
@@ -1,12 +1,28 @@
import assert from 'node:assert/strict';
import { create } from '@bufbuild/protobuf';
import { connectNodeAdapter } from '@connectrpc/connect-node';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import http2 from 'node:http2';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
ConnectionEventsSchema,
StartedAtSchema,
StartedService,
StatusSchema,
VersionSchema,
} from '../../dist/server/generated/daemon/started_service_pb.js';
const root = path.resolve(import.meta.dirname, '../..');
process.env.APP_MODE = 'gateway';
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'vpn-proxy-gateway-test-'));
process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, 'cache.db');
process.env.SING_BOX_TRAFFIC_SOURCE = 'snapshot';
const {
buildDualChannelGatewayConfig,
@@ -124,3 +140,257 @@ test('cached dual-channel outbounds must match the applied provider fingerprints
config.outbounds[2].outbounds = ['channel-primary'];
assert.equal(dualChannelConfigMatchesApplied(config, applied, 'primary'), false);
});
function request(socketPath, pathname, method = 'GET', body) {
return new Promise((resolve, reject) => {
const payload = body === undefined ? null : JSON.stringify(body);
const value = http.request({
socketPath,
path: pathname,
method,
...(payload ? { headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } } : {}),
}, (response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))));
});
value.on('error', reject);
value.end(payload);
});
}
async function waitForSocket(socketPath, child, stderr) {
for (let attempt = 0; attempt < 100; attempt += 1) {
if (child.exitCode !== null) throw new Error(`dataplane exited: ${stderr()}`);
try {
const status = await request(socketPath, '/status');
if (status.ready) return;
} catch {}
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error(`dataplane did not start: ${stderr()}`);
}
async function startDataplane(mode, apiPort) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), `harbor-${mode}-`));
const socketPath = path.join(directory, 'dataplane.sock');
const configPath = path.join(directory, 'sing-box.json');
const binDirectory = path.join(directory, 'bin');
fs.mkdirSync(binDirectory);
fs.writeFileSync(configPath, JSON.stringify({
services: [{
type: 'api',
listen: '127.0.0.1',
listen_port: 19091,
dashboard: false,
}],
inbounds: [],
outbounds: [],
}));
for (const [name, source] of [
['sing-box', `#!/bin/sh
if [ "$1" = check ]; then exit 0; fi
trap 'exit 0' TERM INT
while :; do sleep 1; done
`],
['ip', `#!/bin/sh
printf '%s\n' '[{"dst":"192.168.50.7","lladdr":"00:11:22:33:44:55","dev":"en0","state":["REACHABLE"]}]'
`],
['iptables', '#!/bin/sh\nexit 0\n'],
['iptables-restore', '#!/bin/sh\nexit 0\n'],
]) {
const executable = path.join(binDirectory, name);
fs.writeFileSync(executable, source);
fs.chmodSync(executable, 0o755);
}
const child = spawn(process.execPath, ['dist/server/main.js'], {
cwd: root,
env: {
...process.env,
APP_MODE: 'gateway',
APP_COMPONENT: 'dataplane',
DATA_DIR: directory,
DATAPLANE_SOCKET: socketPath,
DEVICE_TRAFFIC_ACCOUNTING_ENABLED: 'false',
SING_BOX_API_PORT: String(apiPort),
SING_BOX_CACHE: path.join(directory, 'cache.db'),
SING_BOX_CONFIG: configPath,
SING_BOX_API_SECRET: path.join(directory, 'api.secret'),
SING_BOX_RUNTIME_CONFIG: path.join(directory, 'runtime-config.json'),
SING_BOX_TRAFFIC_SOURCE: mode,
PATH: `${binDirectory}:${process.env.PATH || ''}`,
},
stdio: ['ignore', 'ignore', 'pipe'],
});
let error = '';
child.stderr.on('data', (chunk) => { error += chunk; });
const stop = async () => {
if (child.exitCode === null) {
const exited = new Promise((resolve) => child.once('exit', resolve));
child.kill('SIGTERM');
await exited;
}
fs.rmSync(directory, { recursive: true, force: true });
};
try {
await waitForSocket(socketPath, child, () => error);
return { socketPath, child, stop };
} catch (reason) {
await stop();
throw reason;
}
}
function waitForAbort(signal) {
return new Promise((resolve) => {
if (signal.aborted) resolve();
else signal.addEventListener('abort', resolve, { once: true });
});
}
async function waitForSnapshot(socketPath, pathname, predicate, method = 'GET', body) {
const deadline = Date.now() + 3_000;
while (Date.now() < deadline) {
const snapshot = await request(socketPath, pathname, method, body);
if (predicate(snapshot)) return snapshot;
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error(`timed out waiting for ${pathname}`);
}
function nativeRoutes(router, source) {
router.service(StartedService, {
getVersion: () => create(VersionSchema, { version: '1.14.0-rc.5', apiVersion: 4 }),
getStartedAt: () => create(StartedAtSchema, { startedAt: 1_700_000_000_000n }),
async *subscribeConnections(_request, context) {
yield create(ConnectionEventsSchema, {
reset: true,
events: [{
type: 0,
id: 'native-1',
connection: {
id: 'native-1',
inbound: 'tproxy-in',
inboundType: 'tproxy',
network: 'tcp',
source: '192.168.50.7:54000',
destination: '203.0.113.10:443',
domain: 'native.example',
protocol: 'tls',
createdAt: 1_700_000_000_000n,
uplinkTotal: 101n,
downlinkTotal: 202n,
outbound: 'channel-selector',
outboundType: 'selector',
chainList: ['channel-primary', 'channel-selector'],
},
}],
});
await waitForAbort(context.signal);
},
async *subscribeStatus(_request, context) {
while (!context.signal.aborted) {
yield create(StatusSchema, {
connectionsIn: source.mismatch ? 2 : 1,
uplinkTotal: source.mismatch ? 999n : 101n,
downlinkTotal: source.mismatch ? 999n : 202n,
});
await Promise.race([
waitForAbort(context.signal),
new Promise((resolve) => setTimeout(resolve, 100)),
]);
}
},
});
}
test('Gateway modes keep snapshot compare-only and make native the sole canonical writer', async (t) => {
let connectionReads = 0;
const nativeSource = { mismatch: false };
const clash = http.createServer((req, res) => {
if (req.url === '/connections') connectionReads += 1;
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ connections: [{
id: 'legacy-1',
upload: 11,
download: 22,
metadata: {
type: 'tproxy/tproxy-in',
host: 'legacy.example',
sourceIP: '192.168.50.7',
},
chains: ['channel-primary'],
}] }));
});
await new Promise((resolve) => clash.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise((resolve) => clash.close(resolve)));
const apiPort = clash.address().port;
const native = http2.createServer(connectNodeAdapter({
routes: (router) => nativeRoutes(router, nativeSource),
}));
await new Promise((resolve) => native.listen(19091, '127.0.0.1', resolve));
t.after(() => new Promise((resolve) => native.close(resolve)));
for (const mode of ['snapshot', 'shadow', 'native']) {
connectionReads = 0;
const dataplane = await startDataplane(mode, apiPort);
try {
const live = mode === 'snapshot'
? await request(dataplane.socketPath, '/traffic/live')
: await waitForSnapshot(
dataplane.socketPath,
'/traffic/live',
(snapshot) => snapshot.source.state === 'live',
);
const domain = await waitForSnapshot(
dataplane.socketPath,
'/domain-traffic',
(snapshot) => snapshot.tracked[0]?.uploadBytes === (mode === 'native' ? '101' : '11'),
);
assert.equal(domain.source.mode, mode);
assert.equal(domain.source.writer, mode === 'native' ? 'native' : 'snapshot');
assert.equal(domain.source.native === null, mode === 'snapshot');
assert.equal(domain.source.shadow === null, mode !== 'shadow');
assert.equal(live.source.state, mode === 'snapshot' ? 'disabled' : 'live');
assert.equal(connectionReads > 0, mode !== 'native');
if (mode === 'shadow') {
assert.equal(domain.source.native.active, 1);
assert.equal(domain.source.shadow.uploadDifferenceBytes, '90');
assert.equal(domain.source.shadow.downloadDifferenceBytes, '180');
}
if (mode === 'native') {
assert.equal(connectionReads, 0);
assert.equal(live.connections[0].id, 'native-1');
assert.deepEqual(domain.tracked, [{
source: 'gateway',
outbound: 'vpn',
uploadBytes: '101',
downloadBytes: '202',
}]);
await request(dataplane.socketPath, '/failover/activity', 'PUT', { enabled: true });
await waitForSnapshot(
dataplane.socketPath,
'/failover/activity/read',
(response) => response.activity?.state === 'quiet',
'POST',
{ thresholdBytesPerSecond: 0 },
);
nativeSource.mismatch = true;
await waitForSnapshot(
dataplane.socketPath,
'/traffic/live',
(snapshot) => snapshot.source.state === 'degraded',
);
const degradedActivity = await request(
dataplane.socketPath,
'/failover/activity/read',
'POST',
{ thresholdBytesPerSecond: 0 },
);
assert.equal(degradedActivity.activity, null);
}
} finally {
await dataplane.stop();
}
}
});
+355
View File
@@ -0,0 +1,355 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import test from 'node:test';
import { createLiveTrafficService } from '../../dist/server/services/liveTrafficService.js';
const subscriptionConfig = {
outbounds: [{
type: 'vless',
tag: 'test-vpn',
server: 'vpn.example.test',
server_port: 443,
uuid: '00000000-0000-4000-8000-000000000000',
tls: { enabled: true },
}],
};
function buildClientConfig(trafficSource) {
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), `harbor-native-${trafficSource}-`));
const singboxUrl = pathToFileURL(path.resolve('dist/server/singbox.js')).href;
const script = `
const { buildGatewayConfig } = await import(${JSON.stringify(singboxUrl)});
const config = buildGatewayConfig(${JSON.stringify(subscriptionConfig)}, 'test-vpn');
process.stdout.write(JSON.stringify(config));
`;
try {
return JSON.parse(execFileSync(process.execPath, ['--input-type=module', '--eval', script], {
cwd: path.resolve('.'),
encoding: 'utf8',
env: {
...process.env,
APP_MODE: 'client',
DATA_DIR: dataDir,
SING_BOX_CACHE: path.join(dataDir, 'cache.db'),
SING_BOX_TRAFFIC_SOURCE: trafficSource,
},
}));
} finally {
fs.rmSync(dataDir, { recursive: true, force: true });
}
}
function abortableStream(value, signal) {
return (async function* stream() {
yield value;
await new Promise((_, reject) => {
const abort = () => reject(signal.reason || new Error('aborted'));
if (signal.aborted) abort();
else signal.addEventListener('abort', abort, { once: true });
});
}());
}
async function waitFor(check, timeout = 1_000) {
const deadline = Date.now() + timeout;
while (!check()) {
if (Date.now() >= deadline) throw new Error('Timed out waiting for native traffic collector');
await new Promise((resolve) => setTimeout(resolve, 5));
}
}
test('client native mode adds the private API service and removes the retired DNS option', () => {
const config = buildClientConfig('native');
assert.deepEqual(config.services, [{
type: 'api',
listen: '127.0.0.1',
listen_port: 19091,
dashboard: false,
}]);
assert.deepEqual(config.dns, {});
assert.equal(config.experimental.clash_api, undefined);
});
test('client disabled mode omits the API service and keeps the 1.13-compatible DNS option', () => {
const config = buildClientConfig('disabled');
assert.equal(config.services, undefined);
assert.deepEqual(config.dns, { independent_cache: true });
assert.equal(config.experimental.clash_api, undefined);
});
test('native collector authenticates all RC5 lifecycle calls and uses one-second Go duration intervals', async () => {
const requests = [];
const connection = {
id: 'connection-1',
inbound: 'mixed-in',
inboundType: 'mixed',
network: 'tcp',
source: '127.0.0.1:54000',
destination: '203.0.113.10:443',
domain: 'example.test',
protocol: 'tls',
createdAt: 1_700_000_000_000n,
closedAt: 0n,
uplinkTotal: 12n,
downlinkTotal: 34n,
outbound: 'test-vpn',
outboundType: 'vless',
rule: 'final',
chainList: ['test-vpn'],
};
const clientFactory = (port) => {
requests.push(['factory', port]);
return {
async getVersion(_input, { signal, headers }) {
requests.push(['version', signal instanceof AbortSignal, headers?.authorization]);
return { version: '1.14.0-rc.5', apiVersion: 4 };
},
async getStartedAt(_input, { signal, headers }) {
requests.push(['started-at', signal instanceof AbortSignal, headers?.authorization]);
return { startedAt: 1_700_000_000_000n };
},
subscribeConnections({ interval }, { signal, headers }) {
requests.push(['connections', interval, headers?.authorization]);
return abortableStream({
reset: true,
events: [{ type: 0, id: connection.id, connection }],
}, signal);
},
subscribeStatus({ interval }, { signal, headers }) {
requests.push(['status', interval, headers?.authorization]);
return abortableStream({
connectionsIn: 1,
uplinkTotal: 12n,
downlinkTotal: 34n,
}, signal);
},
};
};
const service = createLiveTrafficService({
port: 19091,
enabled: true,
isRuntimeRunning: () => true,
authorization: () => 'test-secret',
clientFactory,
});
service.start();
await waitFor(() => service.snapshot().source.state === 'live');
const snapshot = service.snapshot();
assert.equal(snapshot.epoch, 'sing-box-1700000000000');
assert.equal(snapshot.source.singBoxVersion, '1.14.0-rc.5');
assert.equal(snapshot.source.singBoxApiVersion, 4);
assert.equal(snapshot.connections[0].destination.domain, 'example.test');
assert.deepEqual(requests, [
['factory', 19091],
['version', true, 'Bearer test-secret'],
['started-at', true, 'Bearer test-secret'],
['connections', 1_000_000_000n, 'Bearer test-secret'],
['status', 1_000_000_000n, 'Bearer test-secret'],
]);
await service.stop();
});
test('disabled collector stays cached and never constructs a native client', async () => {
let factoryCalls = 0;
const service = createLiveTrafficService({
port: 19091,
enabled: false,
isRuntimeRunning: () => true,
clientFactory: () => {
factoryCalls += 1;
throw new Error('must not connect');
},
});
service.start();
assert.equal(service.snapshot().source.state, 'disabled');
assert.equal(factoryCalls, 0);
await service.stop();
});
test('a native runtime materialization error is cached as incompatible without constructing a client', async () => {
let factoryCalls = 0;
const service = createLiveTrafficService({
port: 19091,
enabled: false,
unavailableError: 'secret file unavailable at https://private.example/path',
isRuntimeRunning: () => true,
clientFactory: () => {
factoryCalls += 1;
throw new Error('must not connect');
},
});
service.start();
assert.equal(service.snapshot().source.state, 'incompatible');
assert.equal(service.snapshot().source.error, 'secret file unavailable at [endpoint]');
assert.equal(factoryCalls, 0);
await service.stop();
});
for (const incompatible of [
{ version: '1.13.18', apiVersion: 4 },
{ version: '1.14.0-rc.5', apiVersion: 5 },
]) {
test(`collector rejects unqualified sing-box ${incompatible.version} API ${incompatible.apiVersion}`, async () => {
let startedAtCalls = 0;
const service = createLiveTrafficService({
port: 19091,
enabled: true,
isRuntimeRunning: () => true,
clientFactory: () => ({
async getVersion() { return incompatible; },
async getStartedAt() {
startedAtCalls += 1;
return { startedAt: 1n };
},
subscribeConnections() { throw new Error('must not subscribe'); },
subscribeStatus() { throw new Error('must not subscribe'); },
}),
});
service.start();
await waitFor(() => service.snapshot().source.state === 'incompatible');
assert.equal(service.snapshot().source.singBoxVersion, incompatible.version);
assert.equal(service.snapshot().source.singBoxApiVersion, incompatible.apiVersion);
assert.equal(startedAtCalls, 0);
await service.stop();
});
}
test('collector reconnects when either RC5 stream ends and cancels its sibling', async () => {
let factoryCalls = 0;
let authorizationCalls = 0;
const attachedSecrets = [];
let firstStatusSignal;
const pending = (signal) => (async function* stream() {
await new Promise((_, reject) => {
const abort = () => reject(signal.reason || new Error('aborted'));
if (signal.aborted) abort();
else signal.addEventListener('abort', abort, { once: true });
});
}());
const clientFactory = () => {
factoryCalls += 1;
const attempt = factoryCalls;
return {
async getVersion(_input, { headers }) {
attachedSecrets.push(headers.authorization);
return { version: '1.14.0-rc.5', apiVersion: 4 };
},
async getStartedAt() {
return { startedAt: 1_700_000_000_000n };
},
subscribeConnections(_input, { signal }) {
return attempt === 1 ? (async function* ended() {})() : pending(signal);
},
subscribeStatus(_input, { signal }) {
if (attempt === 1) firstStatusSignal = signal;
return pending(signal);
},
};
};
const service = createLiveTrafficService({
port: 19091,
enabled: true,
isRuntimeRunning: () => true,
authorization: () => `secret-${++authorizationCalls}`,
clientFactory,
});
service.start();
await waitFor(() => factoryCalls >= 2, 2_000);
assert.equal(firstStatusSignal.aborted, true);
assert.equal(factoryCalls, 2);
assert.deepEqual(attachedSecrets, ['Bearer secret-1', 'Bearer secret-2']);
await service.stop();
});
test('a failed lifecycle projection is retained across one collector reconnect', async () => {
let factoryCalls = 0;
const projected = [];
const idleStream = (signal) => (async function* stream() {
await new Promise((_, reject) => {
const abort = () => reject(signal.reason || new Error('aborted'));
if (signal.aborted) abort();
else signal.addEventListener('abort', abort, { once: true });
});
}());
const connection = {
id: 'projection',
inbound: 'tproxy-in',
inboundType: 'tproxy',
network: 'tcp',
source: '192.168.50.7:54000',
destination: '203.0.113.10:443',
domain: 'example.test',
protocol: 'tls',
createdAt: 1_700_000_000_000n,
closedAt: 0n,
uplinkTotal: 1n,
downlinkTotal: 0n,
outbound: 'channel-selector',
outboundType: 'selector',
rule: 'final',
chainList: ['channel-primary', 'channel-selector'],
};
const clientFactory = () => {
factoryCalls += 1;
const attempt = factoryCalls;
return {
async getVersion() {
return { version: '1.14.0-rc.5', apiVersion: 4 };
},
async getStartedAt() {
return { startedAt: 1_700_000_000_000n };
},
subscribeConnections(_input, { signal }) {
return (async function* stream() {
yield { reset: true, events: [{ type: 0, id: connection.id, connection }] };
await new Promise((_, reject) => {
signal.addEventListener('abort', () => reject(signal.reason || new Error('aborted')), { once: true });
});
}());
},
subscribeStatus(_input, { signal }) {
return attempt === 1
? idleStream(signal)
: (async function* stream() {
while (!signal.aborted) {
yield { connectionsIn: 1, uplinkTotal: 1n, downlinkTotal: 0n };
await new Promise((resolve) => setTimeout(resolve, 20));
}
}());
},
};
};
const service = createLiveTrafficService({
port: 19091,
enabled: true,
gateway: true,
isRuntimeRunning: () => true,
clientFactory,
onProjection: async (batch) => {
projected.push(batch.connections[0]?.traffic.uploadBytes || 'heartbeat');
if (projected.length === 1) throw new Error('writer unavailable');
},
});
service.start();
await waitFor(() => factoryCalls >= 2
&& projected.includes('heartbeat')
&& service.snapshot().source.state === 'live', 2_000);
assert.equal(projected[0], '1');
assert.ok(projected.filter((value) => value === '1').length >= 2);
assert.ok(projected.includes('heartbeat'));
assert.equal(factoryCalls, 2);
assert.equal(service.snapshot().source.error, null);
assert.equal(service.snapshot().connections[0].route.kind, 'vpn');
await service.stop();
});
+59
View File
@@ -11,6 +11,35 @@ import { normalizeSubscriptionConfig } from '../../dist/server/subscription.js';
const root = path.resolve(import.meta.dirname, '../..');
test('startup recovery rejects a materialized native API secret as shared config truth', async (t) => {
const server = {
id: 'server-a',
label: 'Server A',
host: 'a.example',
port: 443,
protocol: 'vless',
};
const fixture = await startClientFixture(t, {
state: profileState(server),
config: {
...generatedConfig(server),
services: [{
type: 'api',
listen: '127.0.0.1',
listen_port: 19091,
dashboard: false,
secret: 'a'.repeat(64),
}],
},
trafficSource: 'native',
});
assert.equal(fixture.state.connection.desired, 'stopped');
assert.equal(fixture.state.connection.process, 'stopped');
assert.equal(fs.existsSync(fixture.markerPath), false);
assert.equal(fs.existsSync(path.join(fixture.directory, 'sing-box-config.json')), false);
});
function listen(server, ...args) {
return new Promise((resolve, reject) => {
server.once('error', reject);
@@ -121,6 +150,7 @@ async function startClientFixture(t, {
config,
hostNetwork,
gatewayPresencePort,
trafficSource,
}) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-startup-recovery-'));
const { binDirectory, markerPath } = fakeSingbox(directory);
@@ -148,6 +178,7 @@ async function startClientFixture(t, {
: hostNetworkPath,
...(gatewayPresencePort ? { HARBOR_GATEWAY_CONTROL_PORT: String(gatewayPresencePort) } : {}),
HARBOR_TEST_RUN_MARKER: markerPath,
...(trafficSource ? { SING_BOX_TRAFFIC_SOURCE: trafficSource } : {}),
},
stdio: ['ignore', 'ignore', 'pipe'],
});
@@ -363,6 +394,34 @@ test('boot rejects an existing config owned by a different applied target', asyn
assert.equal(fs.existsSync(path.join(fixture.directory, 'sing-box-config.json')), false);
});
test('disabled traffic source rejects an existing config with any API service', async (t) => {
const server = {
id: 'server-a',
label: 'Server A',
host: 'a.example',
port: 443,
protocol: 'vless',
};
const fixture = await startClientFixture(t, {
state: profileState(server),
config: {
...generatedConfig(server),
services: [{
type: 'api',
listen: '0.0.0.0',
listen_port: 19091,
dashboard: false,
}],
},
trafficSource: 'disabled',
});
assert.equal(fixture.state.connection.desired, 'stopped');
assert.equal(fixture.state.connection.process, 'stopped');
assert.equal(fs.existsSync(fixture.markerPath), false);
assert.equal(fs.existsSync(path.join(fixture.directory, 'sing-box-config.json')), false);
});
test('stopped Gateway boot explicitly stops an already running remote dataplane', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-stopped-remote-'));
const socketPath = path.join(directory, 'dataplane.sock');