import "server-only"; import { readFile } from "node:fs/promises"; import os from "node:os"; // Everything the metrics snapshot needs about the network is already in /proc // and /sys. systeminformation's networkInterfaces() and networkStats() gather // it by spawning ~120 commands (cat, grep, nmcli, ip) on every snapshot. export type NetworkCounters = { rxBytes: number; txBytes: number }; export type NetworkRate = { rxBytesPerSec: number; txBytesPerSec: number }; export type InterfaceAddresses = { ipv4: string | null; ipv6: string | null }; // Interface names are at most 15 characters and never contain a slash; this // keeps a name from walking out of /sys/class/net. const INTERFACE_NAME = /^[\w.:@-]{1,15}$/; /** * The interface carrying the IPv4 default route, lowest metric first, from * /proc/net/route. Columns: Iface Destination Gateway Flags RefCnt Use Metric Mask ... */ export function parseDefaultRouteInterface(text: string): string | null { let best: { iface: string; metric: number } | null = null; for (const line of text.split("\n").slice(1)) { const fields = line.trim().split(/\s+/); if (fields.length < 8) continue; const [iface, destination, , , , , metricField, mask] = fields; if (destination !== "00000000" || mask !== "00000000") continue; const metric = Number(metricField); if (!best || metric < best.metric) best = { iface, metric }; } return best?.iface ?? null; } export async function readDefaultRouteInterface(): Promise { try { return parseDefaultRouteInterface(await readFile("/proc/net/route", "utf8")); } catch { // Not Linux: there is no /proc/net/route. return null; } } /** Bytes received and sent by one interface, from /sys/class/net//statistics. */ export async function readInterfaceCounters(iface: string): Promise { if (!INTERFACE_NAME.test(iface)) return null; try { const statistics = `/sys/class/net/${iface}/statistics`; const [rx, tx] = await Promise.all([ readFile(`${statistics}/rx_bytes`, "utf8"), readFile(`${statistics}/tx_bytes`, "utf8"), ]); const rxBytes = Number(rx); const txBytes = Number(tx); if (!Number.isFinite(rxBytes) || !Number.isFinite(txBytes)) return null; return { rxBytes, txBytes }; } catch { // The interface went away, or this is not Linux. return null; } } /** * Turns successive readings of an interface into rates. Switching interface * (Wi-Fi to Ethernet, say) starts a new baseline instead of reporting a * nonsense difference between two unrelated counters. */ export function createNetworkRateTracker(maxGapMs = 60_000) { let previous: { iface: string; at: number; counters: NetworkCounters } | null = null; return function update(iface: string, counters: NetworkCounters, at: number): NetworkRate | null { const before = previous; previous = { iface, at, counters }; if (!before || before.iface !== iface || at <= before.at || at - before.at > maxGapMs) { return null; } const seconds = (at - before.at) / 1000; const rx = counters.rxBytes - before.counters.rxBytes; const tx = counters.txBytes - before.counters.txBytes; // A counter that went backwards (interface re-created) gives no rate this round. if (rx < 0 || tx < 0) return null; return { rxBytesPerSec: rx / seconds, txBytesPerSec: tx / seconds }; }; } /** The interface's IPv4 address, and its IPv6 address preferring a global one over link-local. */ export function readInterfaceAddresses( iface: string, interfaces: NodeJS.Dict = os.networkInterfaces(), ): InterfaceAddresses { const addresses = (interfaces[iface] ?? []).filter((address) => !address.internal); const ipv4 = addresses.find((address) => address.family === "IPv4")?.address ?? null; const ipv6Addresses = addresses.filter((address) => address.family === "IPv6"); const ipv6 = (ipv6Addresses.find((address) => !address.address.startsWith("fe80:")) ?? ipv6Addresses[0]) ?.address ?? null; return { ipv4, ipv6 }; }