84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
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<string, unknown> {
|
|
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}` };
|
|
}
|
|
}
|