From 4716cc98209b2fc0ed26820ed66e07a602c58ef7 Mon Sep 17 00:00:00 2001 From: stswangzhiping <59632378+stswangzhiping@users.noreply.github.com> Date: Sun, 22 Mar 2026 07:38:05 +0800 Subject: [PATCH] fix: replace unstable disk serial with wired MAC for hardware fingerprint Made-with: Cursor --- lib/fingerprint.js | 75 +++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/lib/fingerprint.js b/lib/fingerprint.js index 5a9c902..179c4fe 100644 --- a/lib/fingerprint.js +++ b/lib/fingerprint.js @@ -9,13 +9,14 @@ const { execSync } = require('child_process'); * 生成硬件唯一指纹作为 box_id。 * * 策略: - * 优先 将 machine-id + CPU serial + 磁盘 serial 拼接后取 SHA-256 前 32 字符, + * 将 machine-id + CPU serial + 有线网卡永久 MAC 拼接后取 SHA-256 前 32 字符。 * 三者至少能拿到一个即可用此方案,防止 ghost clone 场景下 machine-id 相同的问题。 * * 若均拿不到则依次退化: * DMI UUID → 持久化随机 UUID * - * 注意:MAC 地址故意排除,网卡更换/虚拟化/Docker 都会导致其变化。 + * 注意:磁盘序列号排除(lsblk 兜底不稳定,不同内核版本/驱动可能返回空值)。 + * 有线 MAC 适用于嵌入式设备(网卡焊在主板,由固件烧录,不会更换)。 */ const PERSIST_FILE = '/etc/clawd/.box_id'; @@ -42,37 +43,43 @@ function getCpuSerial() { return null; } -// ── 3. 主存储设备序列号 ──────────────────────────────────────────────────────── -function getDiskSerial() { - // 按优先级依次尝试常见块设备 - const candidates = [ - '/sys/block/sda/device/serial', - '/sys/block/sdb/device/serial', - '/sys/block/nvme0n1/device/serial', - '/sys/block/mmcblk0/device/serial', // SD 卡(RPi) - '/sys/block/vda/device/serial', // 虚拟磁盘 - ]; +// ── 3. 有线网卡永久 MAC 地址 ────────────────────────────────────────────────── +// 嵌入式设备网卡焊在主板,MAC 由固件烧录,比磁盘序列号更稳定。 +// 优先读 ethtool 永久 MAC,其次读 sysfs 且类型为 PERM(0) 的地址。 +function getEthMac() { + const iface = process.env.CLAWD_ETH_IFACE || 'eth0'; - for (const p of candidates) { - try { - const serial = fs.readFileSync(p, 'utf8').trim().replace(/\s+/g, ''); - if (serial && serial.length >= 4 && serial !== '0000000000000000') { - return crypto.createHash('sha256').update('disk:' + serial).digest('hex').slice(0, 32); - } - } catch (_) {} - } - - // 兜底:尝试 lsblk(需要 util-linux,大多数发行版自带) + // 1. ethtool 永久 MAC(最可信) try { - const out = execSync( - "lsblk --nodeps -o SERIAL --noheadings 2>/dev/null | head -1", - { timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] } - ).toString().trim(); - if (out && out.length >= 4) { - return crypto.createHash('sha256').update('disk:' + out).digest('hex').slice(0, 32); + const out = execSync(`ethtool -P ${iface} 2>/dev/null`, { + timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], + }).toString(); + const m = out.match(/Permanent address:\s*([0-9a-f:]{17})/i); + if (m) { + const mac = m[1].replace(/:/g, '').toLowerCase(); + if (mac && mac !== '000000000000' && mac !== 'ffffffffffff') return mac; } } catch (_) {} + // 2. sysfs:仅在 address_assign_type=0(NET_ADDR_PERM)时使用 + try { + const assignType = fs.readFileSync( + `/sys/class/net/${iface}/addr_assign_type`, 'utf8' + ).trim(); + if (assignType === '0') { + const mac = fs.readFileSync(`/sys/class/net/${iface}/address`, 'utf8') + .trim().replace(/:/g, '').toLowerCase(); + if (mac && mac.length === 12 && mac !== '000000000000') return mac; + } + } catch (_) {} + + // 3. 兜底:直接读 address(不验证是否随机化,总比无值强) + try { + const mac = fs.readFileSync(`/sys/class/net/${iface}/address`, 'utf8') + .trim().replace(/:/g, '').toLowerCase(); + if (mac && mac.length === 12 && mac !== '000000000000') return mac; + } catch (_) {} + return null; } @@ -112,13 +119,13 @@ function getPersistentUUID() { // ── 主函数 ──────────────────────────────────────────────────────────────────── function getBoxId() { - const machineId = getMachineId(); - const cpuSerial = getCpuSerial(); - const diskSerial = getDiskSerial(); + const machineId = getMachineId(); + const cpuSerial = getCpuSerial(); + const ethMac = getEthMac(); - // 只要能拿到其中任意一项,就把三者拼接后取哈希,避免 ghost clone 场景 - if (machineId || cpuSerial || diskSerial) { - const raw = [machineId || '', cpuSerial || '', diskSerial || ''].join(':'); + // 只要能拿到其中任意一项,就把三者拼接后取哈希 + if (machineId || cpuSerial || ethMac) { + const raw = [machineId || '', cpuSerial || '', ethMac || ''].join(':'); return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 32); }