Show device identity details and resolve hostnames
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { lookupService } from 'node:dns/promises';
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import { HarborError } from '../../shared/errors.js';
|
||||
@@ -163,6 +164,9 @@ const ONLINE_MS = 2 * 60 * 1000;
|
||||
const RECENT_MS = 24 * 60 * 60 * 1000;
|
||||
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
const TRAFFIC_HISTORY_LIMIT = 120;
|
||||
const HOSTNAME_LOOKUP_LIMIT = 8;
|
||||
const HOSTNAME_LOOKUP_TIMEOUT_MS = 800;
|
||||
const HOSTNAME_RETRY_MS = 5 * 60 * 1000;
|
||||
const COUNTER_PATTERN = /^\d+$/;
|
||||
const DEVICE_ID_PATTERN = /^dev_[a-f0-9]{16}$/;
|
||||
const MAC_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/;
|
||||
@@ -247,6 +251,31 @@ const validTimestamp = (value: unknown): value is string => (
|
||||
typeof value === 'string' && Number.isFinite(Date.parse(value))
|
||||
);
|
||||
|
||||
function normalizeHostname(value: unknown, ip: string) {
|
||||
const hostname = typeof value === 'string' ? value.trim().replace(/\.$/, '') : '';
|
||||
return hostname && hostname !== ip && /^[a-z0-9_](?:[a-z0-9_.-]{0,251}[a-z0-9_])?$/i.test(hostname)
|
||||
? hostname
|
||||
: null;
|
||||
}
|
||||
|
||||
async function resolveDeviceHostname(ip: string) {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
lookupService(ip, 0),
|
||||
new Promise<null>((resolve) => {
|
||||
timer = setTimeout(() => resolve(null), HOSTNAME_LOOKUP_TIMEOUT_MS);
|
||||
timer.unref();
|
||||
}),
|
||||
]);
|
||||
return normalizeHostname(result?.hostname, ip);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInventoryDevice(value: unknown): InventoryDevice | null {
|
||||
const device = record(value);
|
||||
const mac = normalizeMac(device.mac);
|
||||
@@ -267,7 +296,7 @@ function normalizeInventoryDevice(value: unknown): InventoryDevice | null {
|
||||
alias: typeof device.alias === 'string' ? device.alias : '',
|
||||
pinned: device.pinned === true,
|
||||
deprioritized: device.deprioritized === true && device.pinned !== true,
|
||||
hostname: typeof device.hostname === 'string' ? device.hostname : null,
|
||||
hostname: normalizeHostname(device.hostname, ip),
|
||||
manufacturer: typeof device.manufacturer === 'string' ? device.manufacturer : null,
|
||||
mac,
|
||||
ip,
|
||||
@@ -643,6 +672,7 @@ export function createDeviceInventoryService({
|
||||
observePolicy = null,
|
||||
applyPolicies = null,
|
||||
vendor = () => null,
|
||||
resolveHostname = resolveDeviceHostname,
|
||||
now = () => new Date(),
|
||||
}: {
|
||||
store: InventoryStore;
|
||||
@@ -652,6 +682,7 @@ export function createDeviceInventoryService({
|
||||
observePolicy?: (() => unknown | Promise<unknown>) | null;
|
||||
applyPolicies?: ((requested: DirectDevice[]) => unknown | Promise<unknown>) | null;
|
||||
vendor?: (mac: string) => string | null;
|
||||
resolveHostname?: (ip: string) => unknown | Promise<unknown>;
|
||||
now?: () => Date;
|
||||
}) {
|
||||
let refreshPromise: Promise<unknown> | null = null;
|
||||
@@ -660,6 +691,7 @@ export function createDeviceInventoryService({
|
||||
const trafficCursorByMac = new Map<string, TrafficCursor>();
|
||||
let globalTrafficHistory: TrafficSample[] = [];
|
||||
let globalTrafficCursor: TrafficCursor | null = null;
|
||||
const hostnameAttempts = new Map<string, number>();
|
||||
let domainTrafficSnapshot: Record<string, unknown> = {
|
||||
epoch: null,
|
||||
observedAt: null,
|
||||
@@ -995,6 +1027,34 @@ export function createDeviceInventoryService({
|
||||
identities.add(`${String(observation.ip)}|${observation.interface || ''}`);
|
||||
identitiesByMac.set(mac, identities);
|
||||
}
|
||||
const previousByMac = new Map(migrateDeviceInventoryState(store.read()).devices
|
||||
.map((device) => [device.mac, device]));
|
||||
const hostnameCandidates = new Map<string, DeviceObservation>();
|
||||
const hostnameKeys = new Set<string>();
|
||||
const refreshTime = new Date(observedAt).getTime();
|
||||
for (const observation of observations) {
|
||||
const key = `${observation.mac}|${observation.ip}`;
|
||||
hostnameKeys.add(key);
|
||||
const previous = previousByMac.get(observation.mac);
|
||||
if (!observation.active || (identitiesByMac.get(observation.mac)?.size || 0) !== 1
|
||||
|| (previous?.hostname && previous.ip === observation.ip)
|
||||
|| refreshTime - (hostnameAttempts.get(key) || 0) < HOSTNAME_RETRY_MS) continue;
|
||||
hostnameCandidates.set(observation.mac, observation);
|
||||
}
|
||||
for (const key of hostnameAttempts.keys()) {
|
||||
if (!hostnameKeys.has(key)) hostnameAttempts.delete(key);
|
||||
}
|
||||
// ponytail: resolve eight names per poll; add a queue only if large LANs need faster first-pass naming.
|
||||
const hostnameByMac = new Map<string, string>();
|
||||
await Promise.all([...hostnameCandidates].slice(0, HOSTNAME_LOOKUP_LIMIT).map(async ([mac, observation]) => {
|
||||
hostnameAttempts.set(`${mac}|${observation.ip}`, refreshTime);
|
||||
try {
|
||||
const hostname = normalizeHostname(await resolveHostname(observation.ip), observation.ip);
|
||||
if (hostname) hostnameByMac.set(mac, hostname);
|
||||
} catch {
|
||||
// Reverse lookup is best-effort and must not make inventory refresh stale.
|
||||
}
|
||||
}));
|
||||
return serializePolicy(async () => {
|
||||
if (typeof domainTrafficResult?.transportError === 'string') {
|
||||
domainTrafficSnapshot = {
|
||||
@@ -1021,7 +1081,7 @@ export function createDeviceInventoryService({
|
||||
alias: previous?.alias || '',
|
||||
pinned: previous?.pinned === true,
|
||||
deprioritized: previous?.deprioritized === true && previous?.pinned !== true,
|
||||
hostname: previous?.hostname || null,
|
||||
hostname: hostnameByMac.get(mac) || previous?.hostname || null,
|
||||
manufacturer: previous?.manufacturer || vendor(mac),
|
||||
mac,
|
||||
ip: replaceAddress ? observation.ip : previous.ip,
|
||||
|
||||
Reference in New Issue
Block a user