Default new devices to Direct routing
Build and Deploy Gateway / build-and-push (push) Successful in 24s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-31 01:07:59 +03:00
parent f977874da6
commit 7e15cc199f
11 changed files with 276 additions and 22 deletions
+181 -6
View File
@@ -809,6 +809,25 @@ test('device policy is independent from pinning, persists, and keeps the last ap
});
const observedAt = '2026-08-07T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
store.update((state) => ({
...state,
revision: state.revision + 1,
devices: [{
id: deviceId(mac),
alias: '',
pinned: false,
deprioritized: false,
hostname: null,
manufacturer: null,
mac,
ip: '192.168.50.7',
interface: 'eth0',
firstSeenAt: observedAt,
lastSeenAt: observedAt,
source: 'neighbor',
confidence: 'high',
}],
}));
let observations = [{
ip: '192.168.50.7',
mac,
@@ -900,14 +919,9 @@ test('device policy is independent from pinning, persists, and keeps the last ap
observedAt,
active: true,
});
failApply = true;
snapshot = await service.refresh();
const secondId = snapshot.devices.find((device) => device.mac === secondMac).id;
failApply = true;
await assert.rejects(
service.setPolicy(secondId, 'direct', snapshot.revision),
(error) => error.code === 'DEVICE_POLICY_APPLY_FAILED',
);
snapshot = service.snapshot();
const byId = new Map(snapshot.devices.map((device) => [device.id, device]));
assert.equal(byId.get(id).desiredPolicy, 'direct');
assert.equal(byId.get(id).appliedPolicy, 'direct');
@@ -931,6 +945,167 @@ test('device policy is independent from pinning, persists, and keeps the last ap
);
});
test('new devices default to Direct while known and ambiguous devices keep truthful applied routes', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-new-device-policy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const observedAt = '2026-08-31T12:00:00.000Z';
const knownMac = '00:11:22:33:44:10';
const newMac = '00:11:22:33:44:20';
const ambiguousMac = '00:11:22:33:44:30';
const retainedMac = '00:11:22:33:44:40';
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
initializeMissing: true,
});
store.update((state) => ({
...state,
revision: state.revision + 1,
devices: [{
id: deviceId(knownMac),
alias: '',
pinned: false,
deprioritized: false,
hostname: null,
manufacturer: null,
mac: knownMac,
ip: '192.168.50.10',
interface: 'eth0',
firstSeenAt: '2026-08-30T12:00:00.000Z',
lastSeenAt: '2026-08-30T12:00:00.000Z',
source: 'neighbor',
confidence: 'high',
}],
policy: {
...state.policy,
byMac: {
[retainedMac]: {
desired: 'direct',
applied: 'direct',
status: 'failed',
appliedAt: '2026-08-29T12:00:00.000Z',
error: 'cleanup failed',
operationId: null,
},
},
},
}));
let observations = [
{ ip: '192.168.50.10', mac: knownMac, interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.20', mac: newMac, interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.30', mac: ambiguousMac, interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.31', mac: ambiguousMac, interface: 'eth1', observedAt, active: true },
{ ip: '192.168.50.40', mac: retainedMac, interface: 'eth0', observedAt, active: true },
];
let activeDevices = [];
let generation = 0;
let failApply = false;
const appliedSets = [];
const policySnapshot = () => ({
epoch: 'policy-epoch',
generation: `policy-rules-${generation}`,
fingerprint: fingerprintDirectDevices(activeDevices),
observedAt,
appliedIds: activeDevices.map(({ id }) => id),
});
const createService = () => createDeviceInventoryService({
store,
observe: () => ({ observedAt, observations, error: null }),
observePolicy: policySnapshot,
applyPolicies: async (devices) => {
appliedSets.push(structuredClone(devices));
if (failApply) throw new Error('iptables unavailable');
activeDevices = structuredClone(devices);
generation += 1;
return policySnapshot();
},
});
let service = createService();
let snapshot = await service.refresh();
const byMac = new Map(snapshot.devices.map((device) => [device.mac, device]));
assert.equal(byMac.get(knownMac).desiredPolicy, 'vpn');
assert.equal(byMac.get(knownMac).appliedPolicy, 'vpn');
assert.equal(byMac.get(newMac).desiredPolicy, 'direct');
assert.equal(byMac.get(newMac).appliedPolicy, 'direct');
assert.equal(byMac.get(ambiguousMac).desiredPolicy, 'direct');
assert.equal(byMac.get(ambiguousMac).appliedPolicy, 'vpn');
assert.equal(byMac.get(ambiguousMac).policyStatus, 'pending');
assert.equal(byMac.get(retainedMac).appliedPolicy, 'direct');
assert.deepEqual(appliedSets.at(-1).map(({ mac }) => mac).sort(), [newMac, retainedMac].sort());
observations = observations.filter(({ mac, interface: deviceInterface }) => (
mac !== ambiguousMac || deviceInterface === 'eth0'
));
snapshot = await service.refresh();
assert.equal(snapshot.devices.find(({ mac }) => mac === ambiguousMac).appliedPolicy, 'direct');
const newId = snapshot.devices.find(({ mac }) => mac === newMac).id;
snapshot = await service.setPolicy(newId, 'vpn', snapshot.revision);
assert.equal(snapshot.devices.find(({ mac }) => mac === newMac).appliedPolicy, 'vpn');
service = createService();
snapshot = await service.refresh();
assert.equal(snapshot.devices.find(({ mac }) => mac === newMac).appliedPolicy, 'vpn');
const reappearedMac = '00:11:22:33:44:50';
observations.push({ ip: '192.168.50.50', mac: reappearedMac, interface: 'eth0', observedAt, active: true });
failApply = true;
snapshot = await service.refresh();
const failed = snapshot.devices.find(({ mac }) => mac === reappearedMac);
assert.equal(failed.desiredPolicy, 'direct');
assert.equal(failed.appliedPolicy, 'vpn');
assert.equal(failed.policyStatus, 'failed');
failApply = false;
service = createService();
snapshot = await service.refresh();
assert.equal(snapshot.devices.find(({ mac }) => mac === reappearedMac).appliedPolicy, 'direct');
});
test('reappearing device preserves retained applied Direct when cleanup acknowledgement failed', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-retained-device-policy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const observedAt = '2026-08-31T12:00:00.000Z';
const mac = '00:11:22:33:44:60';
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
initializeMissing: true,
});
store.update((state) => ({
...state,
policy: {
...state.policy,
byMac: {
[mac]: {
desired: 'direct',
applied: 'direct',
status: 'failed',
appliedAt: '2026-08-30T12:00:00.000Z',
error: 'cleanup failed',
operationId: null,
},
},
},
}));
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.60', mac, interface: 'eth0', observedAt, active: true }],
}),
applyPolicies: async () => { throw new Error('iptables unavailable'); },
});
const snapshot = await service.refresh();
assert.equal(snapshot.devices[0].desiredPolicy, 'direct');
assert.equal(snapshot.devices[0].appliedPolicy, 'direct');
assert.equal(snapshot.devices[0].policyStatus, 'failed');
assert.equal(store.read().policy.byMac[mac].appliedAt, '2026-08-30T12:00:00.000Z');
});
test('device inventory v1 migration creates a versioned backup', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-migration-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));