Add per-device VPN and direct routing policies
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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/);
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user