import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import { buildTrafficRuleCommands, createDeviceTrafficService, parseTrafficCounters, selectTrafficDevices, } from '../../src/server/services/deviceTrafficService.js'; const uploadChain = 'VPN_PROXY_TRAFFIC_UP'; const downloadChain = 'VPN_PROXY_TRAFFIC_DOWN'; const dataplaneSource = fs.readFileSync( path.resolve(import.meta.dirname, '../../src/server/dataplane.js'), 'utf8', ); const observation = (ip, mac = '00:11:22:33:44:55', deviceInterface = 'eth0') => ({ ip, mac, interface: deviceInterface, }); 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]*setImmediate[\s\S]*traffic\.refresh\(\)/); assert.match(dataplaneSource, /traffic\.refresh\(\)\.catch/); }); test('traffic selection keeps only unambiguous IPv4 neighbors', () => { const selected = selectTrafficDevices([ observation('192.168.50.7'), observation('192.168.50.7'), observation('192.168.50.8', '00:11:22:33:44:66'), observation('192.168.50.9', '00:11:22:33:44:66'), observation('192.168.50.10', '00:11:22:33:44:77', 'bad interface'), observation('192.168.50.11', '00:11:22:33:44:88', 'br-docker0'), observation('2001:db8::7', '00:11:22:33:44:88'), ]); assert.equal(selected.length, 1); assert.deepEqual( { ip: selected[0].ip, mac: selected[0].mac, interface: selected[0].interface }, observation('192.168.50.7'), ); assert.match(selected[0].key, /^[a-f0-9]{16}$/); }); test('traffic rules mirror public upload and download semantics without routing targets', () => { const [device] = selectTrafficDevices([observation('192.168.50.7')]); const commands = buildTrafficRuleCommands({ devices: [device], bypassCidrs: ['10.0.0.0/8', '192.168.0.0/16'], uploadChain, downloadChain, slot: 'A', }); const args = commands.map(([, commandArgs]) => commandArgs); assert.ok(args.some((value) => value.join(' ') === '-w 1 -t raw -A VPN_PROXY_TRAFFIC_UP_A -d 10.0.0.0/8 -j RETURN')); assert.ok(args.some((value) => value.join(' ') === '-w 1 -t mangle -A VPN_PROXY_TRAFFIC_DOWN_A -s 10.0.0.0/8 -j RETURN')); assert.ok(args.some((value) => ( value.includes('--mac-source') && value.includes('00:11:22:33:44:55') && value.includes('-i') && value.includes('eth0') && value.includes('-s') ))); assert.ok(args.some((value) => ( value.some((part) => part.startsWith('harbor-traffic:')) && value.includes('-o') && value.includes('eth0') && value.includes('-d') && value.includes('192.168.50.7') ))); assert.ok(args.filter((value) => value.includes('-j')).every((value) => value[value.indexOf('-j') + 1] === 'RETURN')); const uploadDeviceIndex = args.findIndex((value) => value.some((part) => part.endsWith(':upload'))); const downloadDeviceIndex = args.findIndex((value) => value.some((part) => part.endsWith(':download'))); assert.ok(args.filter((value) => value.includes('-d') && value.includes('10.0.0.0/8')) .every((value) => args.indexOf(value) < uploadDeviceIndex)); assert.ok(args.filter((value) => value.includes('-s') && value.includes('10.0.0.0/8')) .every((value) => args.indexOf(value) < downloadDeviceIndex)); }); test('counter parser preserves exact uint64 byte strings', () => { const counters = parseTrafficCounters( '[7:9007199254740993] -A VPN_PROXY_TRAFFIC_UP_A -s 192.168.50.7/32 -m comment --comment "harbor-traffic:0123456789abcdef:upload" -j RETURN\n', 'VPN_PROXY_TRAFFIC_UP_A', ); assert.equal(counters.get('0123456789abcdef:upload'), '9007199254740993'); }); test('traffic service preserves active rules and snapshot when replacement fails', async () => { let observed = { observedAt: '2026-08-07T12:00:00.000Z', observations: [observation('192.168.50.7')], error: null, }; const firstDevice = selectTrafficDevices(observed.observations)[0]; const calls = []; let failReplacement = false; let failCounters = false; const generations = ['boot', 'rules-a', 'rules-b']; const run = (command, args, options) => { calls.push([command, args, options]); if (command === 'iptables-save') { if (failCounters) return { status: null, stdout: '', stderr: '', error: new Error('counter read timed out') }; const direction = args.includes('raw') ? 'upload' : 'download'; const chain = direction === 'upload' ? `${uploadChain}_A` : `${downloadChain}_A`; const bytes = direction === 'upload' ? '1200' : '3400'; return { status: 0, stdout: `[1:${bytes}] -A ${chain} -m comment --comment "harbor-traffic:${firstDevice.key}:${direction}" -j RETURN\n`, stderr: '', }; } if (failReplacement && args.includes('-R') && args.includes(downloadChain)) { return { status: 1, stdout: '', stderr: 'cannot switch download rules' }; } return { status: 0, stdout: '', stderr: '' }; }; const service = createDeviceTrafficService({ observe: async () => observed, uploadChain, downloadChain, bypassCidrs: ['10.0.0.0/8'], run, nextGeneration: () => generations.shift(), }); const first = await service.refresh(); assert.equal(first.epoch, 'boot'); assert.equal(first.generation, 'rules-a'); assert.deepEqual(first.devices, [{ ip: '192.168.50.7', mac: '00:11:22:33:44:55', interface: 'eth0', uploadBytes: '1200', downloadBytes: '3400', }]); const switchCallsBefore = calls.filter(([, args]) => args.includes('-R') || args.includes('-A')).length; await service.refresh(); const switchCallsAfter = calls.filter(([, args]) => args.includes('-R') || args.includes('-A')).length; assert.equal(switchCallsAfter, switchCallsBefore); observed = { observedAt: '2026-08-07T12:01:00.000Z', observations: [observation('192.168.50.8')], error: null, }; failReplacement = true; const failed = await service.refresh(); assert.equal(failed.generation, 'rules-a'); assert.match(failed.source.error, /cannot switch download rules/); assert.deepEqual(failed.devices, first.devices); const uploadSwitches = calls .filter(([, args]) => args.includes('-R') && args.includes(uploadChain)) .map(([, args]) => args.at(-1)); assert.deepEqual(uploadSwitches, [`${uploadChain}_B`, `${uploadChain}_A`]); failReplacement = false; observed = { observedAt: '2026-08-07T12:02:00.000Z', observations: [], error: 'neighbors unavailable', }; const stale = await service.refresh(); assert.equal(stale.generation, 'rules-a'); assert.equal(stale.source.error, 'neighbors unavailable'); assert.deepEqual(stale.devices, first.devices); failCounters = true; observed = { observedAt: '2026-08-07T12:03:00.000Z', observations: [observation('192.168.50.7')], error: null, }; const timedOut = await service.refresh(); assert.equal(timedOut.observedAt, '2026-08-07T12:02:00.000Z'); assert.match(timedOut.source.error, /counter read timed out/); assert.deepEqual(timedOut.devices, first.devices); assert.ok(calls.every(([, , options]) => options.timeout === 2_000)); }); test('traffic service finalizes a detached slot once and keeps epoch totals monotonic', async () => { const firstObservation = observation('192.168.50.7', '00:11:22:33:44:55'); const secondObservation = observation('192.168.50.8', '00:11:22:33:44:66'); const firstDevice = selectTrafficDevices([firstObservation])[0]; const secondDevice = selectTrafficDevices([secondObservation])[0]; let observed = { observedAt: '2026-08-07T12:00:00.000Z', observations: [firstObservation], error: null, }; const values = { A: { upload: '100', download: '200' }, B: { upload: '5', download: '7' }, }; const keys = { A: firstDevice.key, B: secondDevice.key }; let failNextCounterRead = false; const run = (command, args) => { if (command !== 'iptables-save') return { status: 0, stdout: '', stderr: '' }; if (failNextCounterRead) { failNextCounterRead = false; return { status: null, stdout: '', stderr: '', error: new Error('retired slot read failed') }; } const direction = args.includes('raw') ? 'upload' : 'download'; const tableChain = args.includes('raw') ? uploadChain : downloadChain; return { status: 0, stdout: ['A', 'B'].map((slot) => ( `[1:${values[slot][direction]}] -A ${tableChain}_${slot} -m comment --comment "harbor-traffic:${keys[slot]}:${direction}" -j RETURN` )).join('\n'), stderr: '', }; }; const generations = ['epoch-1', 'rules-a', 'rules-b']; const service = createDeviceTrafficService({ observe: async () => observed, uploadChain, downloadChain, bypassCidrs: [], run, nextGeneration: () => generations.shift(), }); const first = await service.refresh(); assert.equal(first.epoch, 'epoch-1'); assert.equal(first.generation, 'rules-a'); assert.deepEqual(first.devices.map(({ mac, uploadBytes, downloadBytes }) => ({ mac, uploadBytes, downloadBytes, })), [{ mac: firstObservation.mac, uploadBytes: '100', downloadBytes: '200', }]); values.A = { upload: '130', download: '240' }; observed = { observedAt: '2026-08-07T12:01:00.000Z', observations: [secondObservation], error: null, }; failNextCounterRead = true; const pending = await service.refresh(); assert.equal(pending.epoch, 'epoch-1'); assert.equal(pending.generation, 'rules-b'); assert.match(pending.source.error, /retired slot read failed/); assert.deepEqual(pending.devices.map(({ mac, uploadBytes, downloadBytes }) => ({ mac, uploadBytes, downloadBytes, })), [ { mac: firstObservation.mac, uploadBytes: '100', downloadBytes: '200' }, { mac: secondObservation.mac, uploadBytes: '5', downloadBytes: '7' }, ]); const finalized = await service.refresh(); assert.equal(finalized.generation, 'rules-b'); assert.equal(finalized.source.error, null); assert.deepEqual(finalized.devices.map(({ mac, uploadBytes, downloadBytes }) => ({ mac, uploadBytes, downloadBytes, })), [ { mac: firstObservation.mac, uploadBytes: '130', downloadBytes: '240' }, { mac: secondObservation.mac, uploadBytes: '5', downloadBytes: '7' }, ]); values.B = { upload: '15', download: '17' }; const polled = await service.refresh(); assert.deepEqual(polled.devices.map(({ mac, uploadBytes, downloadBytes }) => ({ mac, uploadBytes, downloadBytes, })), [ { mac: firstObservation.mac, uploadBytes: '130', downloadBytes: '240' }, { mac: secondObservation.mac, uploadBytes: '15', downloadBytes: '17' }, ]); });