import { describe, expect, test } from 'bun:test'; import { removeAlienInterface } from './container-manager'; import type { ExecResult } from './types'; // A faithful `ip -o addr show scope global` line: "N: nameinet addr/prefix // scope global name\..." — the same shape the real container reports, so the // orphan regex under test matches it the way it matches the real thing. function tableLine(iface: string, cidr: string): string { return `5: ${iface}\t inet ${cidr} scope global ${iface}\\ valid_lft forever preferred_lft forever`; } /** Fakes exec. Interface-table reads return `tables` in sequence, holding the * last one once exhausted; `ip link del` returns `delExitCode`. Every command * is recorded for reach assertions. */ function fakeExec(tables: string[], delExitCode = 0) { const cmds: string[] = []; let read = 0; const exec = (cmd: string): ExecResult => { cmds.push(cmd); if (cmd.includes('ip -o addr show scope global')) { const stdout = tables[Math.min(read, tables.length - 1)]; read++; return { stdout, stderr: '', exitCode: 0 }; } if (cmd.startsWith('ip link del ')) { return { stdout: '', stderr: delExitCode === 0 ? '' : 'RTNETLINK answers: Operation not permitted', exitCode: delExitCode, }; } throw new Error(`fakeExec: unexpected command: ${cmd}`); }; return { exec, cmds }; } const SUBNET = '172.31.99.0/24'; const ORPHAN_TABLE = `${tableLine('eth0', '10.226.20.5/24')}\n${tableLine('eth4', '172.31.99.2/24')}`; const CLEAN_TABLE = tableLine('eth0', '10.226.20.5/24'); describe('removeAlienInterface', () => { test('deletes the orphan interface carrying the alien subnet', () => { const { exec, cmds } = fakeExec([ORPHAN_TABLE, CLEAN_TABLE]); removeAlienInterface({ service: 'fw-main', subnet: SUBNET, exec }); expect(cmds).toContain('ip link del eth4'); }); test('does nothing when no interface carries the alien subnet', () => { const { exec, cmds } = fakeExec([CLEAN_TABLE]); removeAlienInterface({ service: 'fw-main', subnet: SUBNET, exec }); expect(cmds).toEqual(['ip -o addr show scope global']); }); test('throws naming the surviving interface and container when the repair does not hold', () => { // Both reads report eth4: the delete claimed to run, the interface is // still there. This is exactly the state that made stage 3 of // firewall-interface-classification fail twelve seconds and one converge // away from the real event (celilo#1261). const { exec } = fakeExec([ORPHAN_TABLE, ORPHAN_TABLE]); expect(() => removeAlienInterface({ service: 'fw-main', subnet: SUBNET, exec })).toThrow( /eth4.*fw-main|fw-main.*eth4/s, ); }); test('the throw carries the disconnect failure when docker refused to detach', () => { const { exec } = fakeExec([ORPHAN_TABLE, ORPHAN_TABLE]); expect(() => removeAlienInterface({ service: 'fw-main', subnet: SUBNET, exec, disconnectError: 'Error response from daemon: endpoint not found', }), ).toThrow(/endpoint not found/); }); test('does not throw when the interface is gone after the repair, even if the delete reported failure', () => { // docker's exit code is not to be believed for veth surgery (see // attachAlienSegment), so a failed delete over a table that is actually // clean is surfaced, not fatal. const { exec } = fakeExec([ORPHAN_TABLE, CLEAN_TABLE], 1); expect(() => removeAlienInterface({ service: 'fw-main', subnet: SUBNET, exec })).not.toThrow(); }); });