import { spawnSync } from 'node:child_process'; const ACTIVE_STATES = new Set(['REACHABLE', 'DELAY', 'PROBE', 'PERMANENT', 'NOARP']); const IGNORED_STATES = new Set(['FAILED', 'INCOMPLETE']); const MAC_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i; const INTERFACE_PATTERN = /^[a-z0-9_.:-]{1,15}$/i; interface NeighborEntry extends Record { state?: unknown; lladdr?: unknown; dev?: unknown; dst?: unknown; } export interface NeighborObservation { ip: string; mac: string; interface: string; active: boolean; observedAt: string; source: 'neighbor'; } function neighborEntry(value: unknown): NeighborEntry { return value && typeof value === 'object' && !Array.isArray(value) ? value as NeighborEntry : {}; } export function isDeviceInterface(value: unknown) { const name = String(value || ''); return INTERFACE_PATTERN.test(name) && name !== 'docker0' && !name.startsWith('br-') && !name.startsWith('veth'); } export function parseNeighborSnapshot(value: unknown, observedAt = new Date().toISOString()): NeighborObservation[] { if (!Array.isArray(value)) return []; return value.flatMap((value) => { const entry = neighborEntry(value); const states = (Array.isArray(entry.state) ? entry.state : [entry.state]) .filter(Boolean) .map((state: unknown) => String(state).toUpperCase()); const mac = String(entry.lladdr || '').toLowerCase(); const deviceInterface = String(entry.dev || ''); if (!entry.dst || !isDeviceInterface(deviceInterface) || !MAC_PATTERN.test(mac) || states.some((state) => IGNORED_STATES.has(state))) { return []; } return [{ ip: String(entry.dst), mac, interface: deviceInterface, active: states.some((state) => ACTIVE_STATES.has(state)), observedAt, source: 'neighbor', }]; }); } export function readNeighborSnapshot(run: typeof spawnSync = spawnSync, now = () => new Date()) { const observedAt = now().toISOString(); const result = run('ip', ['-j', 'neigh', 'show'], { encoding: 'utf8', timeout: 1500, }); if (result.error || result.status !== 0) { return { observedAt, observations: [], error: result.error?.message || String(result.stderr || 'ip neigh завершился с ошибкой').trim(), }; } try { return { observedAt, observations: parseNeighborSnapshot(JSON.parse(result.stdout || '[]'), observedAt), error: null, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { observedAt, observations: [], error: `ip neigh вернул невалидный JSON: ${message}` }; } }