/** * celilo discovering the network of the box it is installed on. * * These tests moved here with the code, from * `modules/celilo-mgmt/scripts/discovery.test.ts`. They were always claims about * CELILO — that it reflects whatever network it finds and bakes in no range of * its own — being made in a module's test file because that is where the code * happened to live * (openspec/changes/networks-are-declared-not-written/specs/network-declaration/spec.md). * * The headline test feeds a RANDOM RFC1918 /24 and asserts celilo uses exactly * that: the product-level guarantee that no specific address range is baked in. */ import { beforeEach, describe, expect, test } from 'bun:test'; import { eq } from 'drizzle-orm'; import type { DbClient } from '../db/client'; import { modules, systemConfig } from '../db/schema'; import { setupTestDatabase } from '../test-utils/database'; import { CONTROL_PLANE_MODULE_ID, upsertDeployedSystem } from './deployed-systems'; import { discoverAndRecordNetwork, discoveredNetworkKeys, parseInternalNetwork, } from './network-discovery'; /** Build an `ip route` output for a given /24 base, gateway, and host IP. */ function ipRouteFor(base: string, gateway: string, hostIp: string, dev = 'eth0'): string { return [ `default via ${gateway} dev ${dev} proto dhcp src ${hostIp} metric 100`, `${base}/24 dev ${dev} proto kernel scope link src ${hostIp} metric 100`, '', ].join('\n'); } /** Pick a random RFC1918 /24 base + a host IP + gateway within it. */ function randomRfc1918Slash24(): { base: string; gateway: string; hostIp: string } { const r = Math.random(); let a: number; let b: number; if (r < 0.34) { a = 10; b = Math.floor(Math.random() * 256); } else if (r < 0.67) { a = 172; b = 16 + Math.floor(Math.random() * 16); // 172.16–172.31 } else { a = 192; b = 168; } const c = Math.floor(Math.random() * 256); return { base: `${a}.${b}.${c}.0`, gateway: `${a}.${b}.${c}.1`, hostIp: `${a}.${b}.${c}.${10 + Math.floor(Math.random() * 200)}`, }; } describe('parseInternalNetwork', () => { test('uses whatever /24 the host is actually on — no hard-coded range', () => { for (let i = 0; i < 50; i++) { const { base, gateway, hostIp } = randomRfc1918Slash24(); const result = parseInternalNetwork(ipRouteFor(base, gateway, hostIp)); expect(result).toEqual({ subnet: `${base}/24`, gateway }); // Guard against any latent assumption of the old default. if (base !== '192.168.0.0') { expect(result?.subnet).not.toBe('192.168.0.0/24'); } } }); test('parses a typical dhcp default route + connected subnet', () => { const out = ipRouteFor('10.37.42.0', '10.37.42.1', '10.37.42.50'); expect(parseInternalNetwork(out)).toEqual({ subnet: '10.37.42.0/24', gateway: '10.37.42.1' }); }); test('honors a non-/24 prefix length', () => { const out = [ 'default via 172.20.0.1 dev ens3', '172.20.0.0/16 dev ens3 proto kernel scope link src 172.20.5.9', ].join('\n'); expect(parseInternalNetwork(out)).toEqual({ subnet: '172.20.0.0/16', gateway: '172.20.0.1' }); }); test('returns null when there is no default route', () => { expect(parseInternalNetwork('10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.5')).toBe( null, ); }); test('returns null when the connected subnet route is missing', () => { expect(parseInternalNetwork('default via 10.0.0.1 dev eth0')).toBe(null); }); }); describe('discoveredNetworkKeys (issue #300 redeploy idempotency)', () => { const internal = { subnet: '192.168.1.0/24', gateway: '192.168.1.1' }; test('writes discovered subnet+gateway on first install (unset)', () => { expect(discoveredNetworkKeys(internal, false)).toEqual({ 'network.internal.subnet': '192.168.1.0/24', 'network.internal.gateway': '192.168.1.1', }); }); test('first install with control plane in secure-mgmt records there (celilo#1356)', () => { // The default topology (ce-8mxz) puts celilo-mgr on secure-mgmt, so a fresh // segmented bootstrap must not file the control-plane subnet under // `network.internal.*` — celilo would otherwise be blind to its own // internal zone. expect(discoveredNetworkKeys(internal, false, { controlPlaneZone: 'secure-mgmt' })).toEqual({ 'network.secure-mgmt.subnet': '192.168.1.0/24', 'network.secure-mgmt.gateway': '192.168.1.1', }); }); test('first install with control plane in internal is unchanged', () => { expect(discoveredNetworkKeys(internal, false, { controlPlaneZone: 'internal' })).toEqual({ 'network.internal.subnet': '192.168.1.0/24', 'network.internal.gateway': '192.168.1.1', }); }); test('first install never clobbers an already-set control-plane zone value', () => { expect( discoveredNetworkKeys(internal, false, { controlPlaneZone: 'secure-mgmt', secureMgmtAlreadySet: true, }), ).toEqual({}); }); test('preserves an already-set subnet on redeploy (no override)', () => { // The regression: a redeploy on a box off the internal network discovers the // WRONG subnet — it must not clobber the operator's value. expect(discoveredNetworkKeys(internal, true)).toEqual({}); }); test('box IS on the configured internal network → nothing to record', () => { expect(discoveredNetworkKeys(internal, true, { internalSubnet: '192.168.1.0/24' })).toEqual({}); }); test('box is OFF the internal network → records it as secure-mgmt, not discarded', () => { // #300 discarded this value to protect `internal`. Correct, but it left celilo // blind to its own network: untrusted by the firewall, and absent from the // resolver's split-horizon views (NOERROR/0-records → public DNS → hairpin). const mgmtBox = { subnet: '10.0.120.0/24', gateway: '10.0.120.1' }; expect(discoveredNetworkKeys(mgmtBox, true, { internalSubnet: '192.168.0.0/24' })).toEqual({ 'network.secure-mgmt.subnet': '10.0.120.0/24', 'network.secure-mgmt.gateway': '10.0.120.1', }); }); test('never clobbers an already-set secure-mgmt subnet', () => { const mgmtBox = { subnet: '10.0.120.0/24', gateway: '10.0.120.1' }; expect( discoveredNetworkKeys(mgmtBox, true, { internalSubnet: '192.168.0.0/24', secureMgmtAlreadySet: true, }), ).toEqual({}); }); test('internal set but its value unreadable → stays conservative, writes nothing', () => { // Without knowing the configured subnet we cannot tell whether this box is on // it; guessing wrong would mislabel the LAN. Preserve #300's behaviour. expect(discoveredNetworkKeys(internal, true, {})).toEqual({}); }); test('writes nothing when discovery failed and nothing is set', () => { expect(discoveredNetworkKeys(null, false)).toEqual({}); }); }); describe('discoverAndRecordNetwork', () => { let db: DbClient; beforeEach(async () => { db = await setupTestDatabase(); }); function read(key: string): string | undefined { return db.select().from(systemConfig).where(eq(systemConfig.key, key)).get()?.value; } test('records the discovered network on a box that has none', () => { const result = discoverAndRecordNetwork(db, () => ipRouteFor('10.31.7.0', '10.31.7.1', '10.31.7.20'), ); expect(result.applied).toHaveLength(2); expect(read('network.internal.subnet')).toBe('10.31.7.0/24'); expect(read('network.internal.gateway')).toBe('10.31.7.1'); }); test('first install for a control plane deployed in secure-mgmt records secure-mgmt (celilo#1356)', () => { // Seed the module_systems row the deploy pipeline writes before // bootstrapControlPlane runs, placing celilo-mgmt in secure-mgmt. db.insert(modules) .values({ id: CONTROL_PLANE_MODULE_ID, name: 'Celilo Management Server', version: '0.9.0', manifestData: {}, sourcePath: '/tmp/modules/celilo-mgmt', }) .run(); upsertDeployedSystem(db, CONTROL_PLANE_MODULE_ID, { name: 'main', hostname: 'celilo-mgr', ipv4Address: '10.226.120.10', zone: 'secure-mgmt', infraType: 'machine', machineId: null, serviceId: null, vmid: null, }); const result = discoverAndRecordNetwork(db, () => ipRouteFor('10.226.120.0', '10.226.120.1', '10.226.120.10'), ); expect(result.applied).toEqual([ 'network.secure-mgmt.subnet = 10.226.120.0/24', 'network.secure-mgmt.gateway = 10.226.120.1', ]); expect(read('network.internal.subnet')).toBeUndefined(); expect(read('network.secure-mgmt.subnet')).toBe('10.226.120.0/24'); }); test('a redeploy on the same box changes nothing and says so', () => { const routes = () => ipRouteFor('10.31.7.0', '10.31.7.1', '10.31.7.20'); discoverAndRecordNetwork(db, routes); const second = discoverAndRecordNetwork(db, routes); expect(second.applied).toEqual([]); expect(second.skipped).toContain('already accounts for'); expect(read('network.internal.subnet')).toBe('10.31.7.0/24'); }); test('a box off the internal LAN records its own network as secure-mgmt', () => { db.insert(systemConfig) .values({ key: 'network.internal.subnet', value: '192.168.0.0/24' }) .run(); discoverAndRecordNetwork(db, () => ipRouteFor('10.0.120.0', '10.0.120.1', '10.0.120.10')); // The operator's internal value is untouched, and celilo is no longer blind // to the network its own control plane occupies. expect(read('network.internal.subnet')).toBe('192.168.0.0/24'); expect(read('network.secure-mgmt.subnet')).toBe('10.0.120.0/24'); }); test('an unreadable routing table is reported, not guessed at', () => { const result = discoverAndRecordNetwork(db, () => null); expect(result.applied).toEqual([]); expect(result.skipped).toContain('ip route'); expect(read('network.internal.subnet')).toBeUndefined(); }); });