128 lines
5.0 KiB
JavaScript
128 lines
5.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
buildDevicePolicyRestore,
|
|
createDevicePolicyService,
|
|
fingerprintDirectDevices,
|
|
normalizeDirectDevices,
|
|
} from '../../src/server/services/devicePolicyService.js';
|
|
|
|
const directDevice = {
|
|
id: 'dev_0011223344556677',
|
|
ip: '192.168.50.7',
|
|
mac: '00:11:22:33:44:55',
|
|
interface: 'eth0',
|
|
};
|
|
|
|
test('device policy rules match the full identity before the TPROXY fallback', () => {
|
|
assert.deepEqual(normalizeDirectDevices([{ ...directDevice, mac: directDevice.mac.toUpperCase() }]), [directDevice]);
|
|
assert.throws(() => normalizeDirectDevices([directDevice, directDevice]), /повторяющаяся/);
|
|
assert.throws(() => normalizeDirectDevices([{ ...directDevice, interface: 'br-user' }]), /identity/);
|
|
assert.throws(() => normalizeDirectDevices([{ ...directDevice, interface: 'docker0' }]), /identity/);
|
|
assert.throws(() => normalizeDirectDevices([{ ...directDevice, interface: 'veth1234' }]), /identity/);
|
|
|
|
const restore = buildDevicePolicyRestore({
|
|
devices: [directDevice],
|
|
chain: 'VPN_PROXY_DEVICE_POLICY',
|
|
slot: 'B',
|
|
tproxyPort: 7895,
|
|
tproxyMark: '1',
|
|
});
|
|
assert.equal(restore, [
|
|
'*mangle',
|
|
'-F VPN_PROXY_DEVICE_POLICY_B',
|
|
'-A VPN_PROXY_DEVICE_POLICY_B -i eth0 -s 192.168.50.7 -m mac --mac-source 00:11:22:33:44:55 -m comment --comment harbor-policy:dev_0011223344556677:direct -j RETURN',
|
|
'-A VPN_PROXY_DEVICE_POLICY_B -p tcp -j TPROXY --on-port 7895 --tproxy-mark 1/1',
|
|
'-A VPN_PROXY_DEVICE_POLICY_B -p udp -j TPROXY --on-port 7895 --tproxy-mark 1/1',
|
|
'COMMIT',
|
|
'',
|
|
].join('\n'));
|
|
assert.match(fingerprintDirectDevices([directDevice]), /^[a-f0-9]{64}$/);
|
|
});
|
|
|
|
test('device policy swaps one prepared slot and preserves the active slot on failure', async () => {
|
|
const commands = [];
|
|
let failSwap = false;
|
|
const generations = ['epoch-a', 'rules-b', 'rules-a'];
|
|
const service = createDevicePolicyService({
|
|
chain: 'VPN_PROXY_DEVICE_POLICY',
|
|
tproxyPort: 7895,
|
|
tproxyMark: '1',
|
|
nextGeneration: () => generations.shift(),
|
|
now: () => new Date('2026-08-07T12:00:00.000Z'),
|
|
run: (command, args, options) => {
|
|
commands.push({ command, args, options });
|
|
if (failSwap && args.includes('-R')) return { status: 1, stderr: 'swap failed' };
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
},
|
|
});
|
|
|
|
const initial = service.snapshot();
|
|
assert.equal((await service.apply([])).generation, initial.generation);
|
|
assert.equal(commands.length, 0);
|
|
|
|
const applied = await service.apply([directDevice]);
|
|
assert.deepEqual(applied.appliedIds, [directDevice.id]);
|
|
assert.equal(applied.changed, true);
|
|
assert.deepEqual(commands.at(-1).args, [
|
|
'-w', '1', '-t', 'mangle', '-R', 'VPN_PROXY_DEVICE_POLICY', '1', '-j', 'VPN_PROXY_DEVICE_POLICY_B',
|
|
]);
|
|
assert.equal(commands[0].command, 'iptables-restore');
|
|
assert.match(commands[0].options.input, /--mac-source 00:11:22:33:44:55/);
|
|
assert.equal(commands.length, 2);
|
|
assert.equal(commands.every(({ options }) => options.timeout === 2_000), true);
|
|
|
|
const commandCount = commands.length;
|
|
assert.equal((await service.apply([directDevice])).changed, false);
|
|
assert.equal(commands.length, commandCount);
|
|
|
|
failSwap = true;
|
|
await assert.rejects(service.apply([]), /swap failed/);
|
|
assert.deepEqual(service.snapshot().appliedIds, [directDevice.id]);
|
|
assert.equal(service.snapshot().generation, applied.generation);
|
|
|
|
const prepareCommands = [];
|
|
const prepareFailure = createDevicePolicyService({
|
|
chain: 'VPN_PROXY_DEVICE_POLICY',
|
|
tproxyPort: 7895,
|
|
tproxyMark: '1',
|
|
nextGeneration: () => 'prepare-epoch',
|
|
run: (command, args, options) => {
|
|
prepareCommands.push({ command, args, options });
|
|
return command === 'iptables-restore'
|
|
? { status: 1, stderr: 'prepare failed' }
|
|
: { status: 0, stdout: '', stderr: '' };
|
|
},
|
|
});
|
|
await assert.rejects(prepareFailure.apply([directDevice]), /prepare failed/);
|
|
assert.equal(prepareCommands.some(({ args }) => args.includes('-R')), false);
|
|
assert.deepEqual(prepareFailure.snapshot().appliedIds, []);
|
|
});
|
|
|
|
test('the maximum policy set stays inside the control request deadline', async () => {
|
|
const devices = Array.from({ length: 512 }, (_, index) => {
|
|
const mac = index.toString(16).padStart(12, '0').match(/../g).join(':');
|
|
return {
|
|
id: `dev_${index.toString(16).padStart(16, '0')}`,
|
|
ip: `192.168.${50 + Math.floor(index / 254)}.${(index % 254) + 1}`,
|
|
mac,
|
|
interface: 'eth0',
|
|
};
|
|
});
|
|
const calls = [];
|
|
const service = createDevicePolicyService({
|
|
chain: 'VPN_PROXY_DEVICE_POLICY',
|
|
tproxyPort: 7895,
|
|
tproxyMark: '1',
|
|
nextGeneration: () => 'generation',
|
|
run: (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
},
|
|
});
|
|
|
|
await service.apply(devices);
|
|
assert.deepEqual(calls.map(({ command }) => command), ['iptables-restore', 'iptables']);
|
|
assert.equal(calls.reduce((sum, { options }) => sum + options.timeout, 0), 4_000);
|
|
});
|