/** * 主机信息与公网 IP 检测。 * * Patchbay 服务端和 mihomo 跑在同一台机器上,所以这两件事由它代答: * - 主机指标:mihomo 的管理接口只报自己的内存,CPU、主机内存、网关这些 * 从 os 模块和 /proc 里读(解析在 shared/host-info.ts,那边有测试) * - 公网 IP 检测:浏览器没法「经由某个入站端口」发请求,服务端可以——把本机 * 对应端口设成代理去访问 ip.sb,返回的就是那一路在外面的地址 */ import os from 'node:os' import { cpuUsageBetween, parseGateway, parseMemAvailable, parseMemTotal, parseNameservers, } from '../shared/host-info.ts' import type { CpuTimes, ExitProbe, HostInfo } from '../shared/host-info.ts' function cpuTimesNow(): CpuTimes { let busy = 0 let total = 0 for (const cpu of os.cpus()) { const t = cpu.times busy += t.user + t.nice + t.sys + t.irq total += t.user + t.nice + t.sys + t.irq + t.idle } return { busy, total } } /** 上一次请求时的 CPU 快照。使用率是两次请求之间的平均,第一次没有可比对象。 */ let lastCpu: CpuTimes | null = null async function readProc(path: string): Promise { try { return await Bun.file(path).text() } catch { return '' } } export async function hostInfo(): Promise { const now = cpuTimesNow() const cpuUsage = lastCpu ? cpuUsageBetween(lastCpu, now) : null lastCpu = now const lanAddresses: Array<{ iface: string; address: string }> = [] for (const [iface, addrs] of Object.entries(os.networkInterfaces())) { for (const addr of addrs ?? []) { if (addr.family === 'IPv4' && !addr.internal) lanAddresses.push({ iface, address: addr.address }) } } const meminfo = await readProc('/proc/meminfo') const load = os.loadavg()[0] ?? 0 return { cpuUsage, cpuCount: os.cpus().length, memTotal: parseMemTotal(meminfo) ?? os.totalmem(), memAvailable: parseMemAvailable(meminfo) ?? os.freemem(), // loadavg 在 Windows 上恒为 0,那不是「负载为零」而是「没有这个概念」 load1: process.platform === 'win32' ? null : load, uptimeSeconds: os.uptime(), platform: process.platform === 'linux' ? `Linux ${os.release()}` : `${os.type()} ${os.release()}`, release: os.release(), arch: os.arch(), lanAddresses, gateway: parseGateway(await readProc('/proc/net/route')), nameservers: parseNameservers(await readProc('/etc/resolv.conf')), } } /** 公网 IP 检测问谁。ip.sb 的这个接口返回 JSON,带 IP 和归属地。 */ const EXIT_URL = 'https://api.ip.sb/geoip' /** * 经由本机某个入站端口探测公网 IP。只对说 HTTP 的入站有效(http/mixed), * 纯 socks 端口这条路走不通。 */ export async function probeExit(port: number): Promise { const started = Date.now() try { const res = await fetch(EXIT_URL, { proxy: `http://127.0.0.1:${String(port)}`, signal: AbortSignal.timeout(10_000), }) if (!res.ok) return { ok: false, message: `探测目标返回 HTTP ${String(res.status)}` } const body = (await res.json()) as Record return { ok: true, ip: typeof body.ip === 'string' ? body.ip : '', country: typeof body.country === 'string' ? body.country : '', countryCode: typeof body.country_code === 'string' ? body.country_code : '', ms: Date.now() - started, } } catch (error) { return { ok: false, message: error instanceof Error ? error.message : String(error) } } }