Repository Wiki
jason5ng32/MyIP

MAC 地址与厂商识别

MAC 查询通过本地 IEEE 注册表,将完整 MAC 地址或地址前缀解析为厂商、注册地址、国家/地区、分配块范围及地址属性。它不仅回答“属于哪个厂商”,也能在厂商未命中时返回单播、组播、本地管理及可能随机化等标志。

目的与范围

本文覆盖 /api/macchecker 的请求校验、本地注册表解析、最长前缀匹配、响应结构和运行边界。主要面向维护查询 API、排查识别结果或扩展注册表支持的开发者。

离线数据下载、统一更新调度、通用 HTTP 缓存和前端交互属于相邻能力,本文仅说明已经核实的连接点,不将其完整机制并入本页。当前阅读范围未覆盖这些机制的实现,也未覆盖完整测试文件,因此不对更新周期、启动等待状态码或测试覆盖率作保证。

概述

实现将“输入是什么”和“注册表记录了什么”分开处理:

  • normalizeMacQuery 将输入转换为 6~12 位大写十六进制字符串。
  • createMacChecker 执行 HTTP 参数检查,调用可注入的查询函数并返回 JSON。
  • describeMac 在注册块 Map 中按 36、28、24 位顺序查询,生成厂商信息及范围。
  • macFlags 从原始输入的首字节计算地址属性;这些属性不依赖是否找到厂商。

因此,found: false 是有效查询结果,并非请求失败;isRand 也只是基于地址位的可能随机化判断,不是设备行为的直接证据。

依据:mac-input.js、mac-checker.js、oui-db.js。

架构与入口

Loading diagram...

Sources: backend-server.js、backend-server.js、mac-checker.js、oui-db.js

图中上半部分是实际路由与处理器关系,下半部分是数据构建和纯匹配函数。lookupMac 的导入及注入已核实,但其函数体不在本次已读取片段中,因此图中没有补画它与加载器之间的内部调用。

服务将 needsOui 定义为 requireOfflineData([isOuiLoaded]),随后按 needsOui → cacheable(THIRTY_DAYS_CACHE) → macChecker 注册路由。路由附近注释把 IEEE 注册信息归入 30 天缓存类别;缓存中间件具体如何设置响应头、如何处理错误响应,不在本次核验范围。

输入规范化

接受的是十六进制内容,而非严格分组格式

规范化器先移除空白、冒号、连字符和句点,再转换大写,最后检查长度和字符集。它支持完整 48 位 MAC,也支持至少 24 位的前缀;7、9 等奇数位长度同样合法,能够表达 MA-M、MA-S 等非整字节边界。

下面是完整的实际规范化函数:

javascript
1export const normalizeMacQuery = (input) => { 2 if (typeof input !== 'string') return null; 3 const hex = input.replace(SEPARATORS, '').toUpperCase(); 4 if (hex.length < MAC_PREFIX_MIN_HEX || hex.length > MAC_HEX_LENGTH) return null; 5 return /^[0-9A-F]+$/.test(hex) ? hex : null; 6};

Source: mac-input.js

这里的 SEPARATORS 为 /[\s:.-]/g,MAC_PREFIX_MIN_HEX 为 6,MAC_HEX_LENGTH 为 12。分隔符出现的位置没有单独校验,所以不能将它描述为“严格验证六组冒号分隔字节”。输入允许格式化差异,但移除分隔符后必须全部为十六进制字符。

另一个细节是 HTTP 处理器会先执行 String(mac),而直接调用 normalizeMacQuery 时,非字符串会返回 null。两者的入参边界并不完全相同。

IEEE 数据模型与解析

五种注册表,共用一个索引

每条解析记录是以大写 assignment 为键的二元组,值包含 registry、company、address、country。加载器将这些记录合并到一个 Map 中,不按注册表建立独立查询接口。

注册类别CSV 文件名前缀位数十六进制位数分配块地址数最低有效记录数
MA-Loui.csv24616,777,21628,000
MA-Mmam.csv2871,048,5764,500
MA-Soui36.csv3694,0965,000
IABiab.csv3694,0963,200
CIDcid.csv24616,777,216150

文件名为注册表配置中的数据资源名,而不是已核验的仓库内 CSV 文件链接。配置定义见 oui-db.js。地址数由实现中的 16 ** (12 - assignment.length) 得出。

MA-M、MA-S 和 IAB 可能位于登记给 IEEE Registration Authority 的 MA-L 范围内,因此若仅按前三字节识别,就可能返回注册机构而不是更具体的分配对象。最长前缀优先正是为了解决这类覆盖关系。IAB 已停止新的分配,但历史记录仍被保留;CID 表示本地管理空间中的 Company ID。

CSV 解析和完整性校验

解析分为两层:

  1. parseCsv(text) 逐字符处理 CSV,允许引号字段内出现逗号、换行及双引号转义。文件结束时若仍处于引号字段内,抛出 CSV ends inside a quoted field。
  2. parseRegistry(text, spec) 去除开头 BOM,核对固定表头,并要求原始文本以换行结尾;不满足时抛错。随后逐行筛选注册类别、assignment 长度和十六进制字符。

无效数据行被跳过,不是逐行抛错;有效行的公司名称去除首尾空白,地址再将连续空白折叠为一个空格。记录数下限由下一层 readOuiRegistries 验证,而非 parseRegistry 自身验证。

javascript
1export const readOuiRegistries = (pathOf) => { 2 const blocks = new Map(); 3 for (const spec of OUI_REGISTRIES) { 4 const entries = parseRegistry(fs.readFileSync(pathOf(spec.file), 'utf8'), spec); 5 if (entries.length < spec.min) { 6 throw new Error(`${spec.registry} has only ${entries.length} assignments (expected ≥ ${spec.min})`); 7 } 8 for (const [hex, block] of entries) blocks.set(hex, block); 9 } 10 return blocks; 11};

Source: oui-db.js

这个函数只有在所有文件都成功读取并达到阈值后才返回新 Map。最低记录数按解析后的条目数计算,而不是按最终 Map 的去重键数计算。若存在相同 assignment,后执行的 blocks.set 会覆盖前面的值。

国家/地区来自地址启发式解析

CSV 没有独立的国家列,countryFromAddress 只检查地址末尾三个空白分隔词:

  • 使用两位大写字母及 Intl.DisplayNames(['en'], { type: 'region', fallback: 'none' }) 判断候选代码。
  • 优先检查“国家代码后跟包含数字的邮编”模式。
  • 若邮编之后还有国家代码,通常采用后者,以避免把州代码误当国家。
  • 对 NL 和 IT 特殊处理,避免把邮编尾部字母或省份缩写误当另一国家。
  • 没有上述模式时,从右向左选择可识别国家代码;仍无匹配则返回 null。

该字段是注册地址的推断结果,不是设备的实时地理位置。源码中的例子及逻辑见 oui-db.js。

最长前缀匹配与属性计算

查找键和显示前缀有意不同

describeMac 的查找过程先清除首字节的最低位,即组播位,再按 9、7、6 位十六进制前缀尝试匹配:

javascript
1 const firstOctet = (parseInt(hex.slice(0, 2), 16) & 0xfe).toString(16).toUpperCase().padStart(2, '0'); 2 const key = firstOctet + hex.slice(2); 3 let assignment = null; 4 for (const length of HEX_LENGTHS) { 5 if (key.length >= length && blocks.has(key.slice(0, length))) { 6 assignment = key.slice(0, length); 7 break; 8 } 9 }

Source: oui-db.js

清除组播位用于查找所属分配块,但不会清除本地管理位。命中后,显示前缀通过 hex.slice(0, 2) + assignment.slice(2) 恢复查询的原始首字节。因此组播查询获得的是对应组播形式的地址范围,而不是一个不包含查询地址的单播范围。

源码以 01:00:5E:… 为例:它按 00:00:5E 查找 IANA 记录,显示范围则为 01:00:5E:00:00:00 至 01:00:5E:FF:FF:FF。见 oui-db.js。

前缀查询不会猜测更细分的厂商

只有 key.length >= length 时才尝试该长度的匹配。6 位查询无法区分同一 MA-L 下的多个 MA-M 或 MA-S 分配块,因此不会枚举子块或任意选择厂商。结果始终是当前输入能够确定的最长已登记前缀。

属性标志及其含义

字段计算依据应如何解读
isMulticast首字节 & 0x01 非零组播位已设置
isUnicast!isMulticast与组播互补
isLocal首字节 & 0x02 非零本地管理位已设置
isGlobal!isLocal与本地管理互补,不等同于公网可达
isRand初始为 isLocal && !isMulticast可能为随机化地址;命中 CID 后置为 false
isPrivate公司名称小写后严格等于 private注册条目标记,与 isLocal 含义不同

没有命中厂商时仍计算首字节标志,但 isPrivate 固定为 false。CID 命中会取消随机化推断,因为这是登记过的本地地址空间,而非仅凭本地管理位判断的未知地址。

请求执行流程与实际用法

Loading diagram...

Source: mac-checker.js

此图从处理器开始;上游离线数据守卫可能先处理请求,其内部行为不在图中展开。

基础用法:生产处理器绑定本地查询函数

仓库直接通过工厂创建默认导出的处理器:

javascript
export default createMacChecker(lookupMac);

Source: mac-checker.js

服务端将这一默认导出命名为 macChecker 并用于 GET 路由。它不是每次请求重新下载 IEEE 数据的在线厂商代理:已核实的匹配核心是对传入 Map 的本地查找。

扩展用法:可注入查询函数的处理器

以下工厂也是实际测试接缝,源代码注释明确说明 lookup 可替换为基于 fixture 注册表的查询函数:

javascript
1export const createMacChecker = (lookup) => async (req, res) => { 2 const { mac } = req.query; 3 if (!mac) { 4 return res.status(400).json({ error: 'No MAC address provided' }); 5 } 6 const hex = normalizeMacQuery(String(mac)); 7 if (!hex) { 8 return res.status(400).json({ error: 'Invalid MAC address' }); 9 } 10 11 // Past the boot window (requireOfflineData) only a failed download leaves this empty. 12 const result = lookup(hex); 13 if (!result) { 14 return res.status(503).json({ error: 'MAC database unavailable' }); 15 } 16 res.json(result); 17};

Source: mac-checker.js

虽然返回的处理器是 async 函数,内部 lookup(hex) 并没有 await。因此扩展时应保持查询函数同步返回结果对象或假值的契约,不能仅凭处理器声明为 async 就替换成异步远程查询。

HTTP API 参考

GET /api/macchecker

参数位置要求说明
macQuery string必填完整地址或前缀;规范化后必须为 6~12 位十六进制字符串

处理器先检查参数是否为假值,再执行字符串转换和规范化。空白或仅有分隔符的非空字符串会进入格式校验,然后得到 Invalid MAC address,而不是缺少参数错误。

状态码与错误体

情况处理器响应JSON 内容
mac 为假值400error: 'No MAC address provided'
规范化失败400error: 'Invalid MAC address'
lookup(hex) 返回假值503error: 'MAC database unavailable'
匹配到注册块正常 JSON 响应,通常 200success: true, found: true 及详细字段
没有注册块覆盖正常 JSON 响应,通常 200success: true, found: false 及标志字段

这里列出的是处理器自身的分支,不包含全局中间件可能生成的响应。依据:mac-checker.js。

成功响应字段

字段命中时未命中时
successtruetrue
foundtruefalse
macPrefix分配块前缀,保留查询首字节,每两位以冒号分隔查询前 6 位,每两位以冒号分隔
company公司名,空值回退为 'N/A''N/A'
address规范化后的注册地址,空值回退为 'N/A''N/A'
country推断国家代码,空值回退为 'N/A''N/A'
blockStart前缀后补 0 至 12 位,再格式化'N/A'
blockEnd前缀后补 F 至 12 位,再格式化'N/A'
blockSize数字,16 ** hostDigits字符串 'N/A'
blockTypeMA-L、MA-M、MA-S、IAB 或 CID'N/A'
isPrivate公司名是否为 privatefalse
isMulticast、isUnicast、isLocal、isGlobal首字节标志首字节标志
isRand首字节推断,但 CID 命中时为 false首字节推断

消费者应注意 blockSize 不是固定数字类型。MA-M、MA-S 的前缀包含奇数位十六进制数字,pairs 允许末尾只有一位,因此 macPrefix 不保证全部分组都为两个字符,也不保证是完整 MAC 地址。

依据:oui-db.js。

内部函数参考

源码为 JavaScript,以下参数和返回说明来自函数体,并非仓库中的静态类型声明。

实际声明参数与返回失败方式/调用约束
normalizeMacQuery(input)输入字符串;返回大写裸十六进制字符串或 null非字符串、长度错误、非十六进制内容返回 null
createMacChecker(lookup)接受查询函数;返回异步 Express 风格处理器查询函数应同步返回;处理器没有局部 try/catch
parseCsv(text)CSV 字符串 → 字段行数组未闭合引号抛出 Error
countryFromAddress(address)地址 → 国家代码或 null基于末尾词的启发式推断
parseRegistry(text, { registry, hexLength })CSV 与注册表规格 → [assignment, block] 数组错误表头、未以换行结束或 CSV 引号错误会抛出异常
readOuiRegistries(pathOf)文件路径解析函数 → 合并后的 Map同步读取错误、解析错误、记录数不足会向调用者抛出
macFlags(hex)至少能表达首字节的十六进制字符串 → 五个布尔属性不负责完整输入校验
describeMac(blocks, hex)注册表 Map 与规范化输入 → 完整结果对象约定输入已经过规范化,不额外验证参数

实现参考:mac-input.js、mac-checker.js、oui-db.js。lookupMac 仅核实了导入和使用,未读取函数体,不在此补写内部返回或异常约定。

配置与数据生命周期

这里的 MAC 专用参数是代码常量,而不是已经核实可由环境变量覆盖的选项。

配置/常量类型当前值作用
MAC_HEX_LENGTHnumber12完整 MAC 十六进制长度和范围补齐长度
MAC_PREFIX_MIN_HEXnumber6最小查询前缀
OUI_DB_DIRstring模块目录下的 oui-db五个本地 CSV 的加载目录
IEEE_BASEstringhttps://standards-oui.ieee.org注册表下载地址的基础域名
OUI_REGISTRIESarray五种注册表规格文件、URL、前缀长度及最低记录数
HEX_LENGTHSnumber[]从注册表推导为 [9, 7, 6]匹配顺序,不是独立人工维护列表
LETTERED_POSTCODESetNL、IT国家解析时的邮编特例

可见的加载代码先检查所有文件是否存在。如果任一文件缺失,会记录 info 级日志,将模块级 blocks 置为 null,然后返回;注释说明首次启动会随后下载。文件齐全时,使用 readOuiRegistries 同步读取,并在成功后记录 assignment 数量和耗时。

这一片段不够证明加载失败后是否保留旧快照、何时热重载或如何监听文件变化。数据更新系统虽在服务入口被导入,具体调度与故障恢复实现未在本页核验。依据:oui-db.js、backend-server.js。

故障、边界与并发

应区分三种“无结果”

  1. 输入无效:规范化失败,处理器返回 400。
  2. 数据库不可用:查询返回假值,处理器返回 503;这不等同于厂商未登记。
  3. 查询有效但未登记:返回 success: true, found: false,仍保留地址属性。

这一区分对前端提示与监控非常重要:将 found: false 计作后端故障会把正常的未知或本地管理地址误报为服务异常。

数据损坏的防护边界

加载层防护包括固定表头、CSV 引号完整性、末尾换行和最低有效记录数。它们可捕获一部分截断或错误下载,但并不是文件内容的真实性校验:有换行且条目数达标的错误内容仍可能通过这些检查。逐行非法记录会被跳过,因此需要结合最终有效数量理解加载错误。

异常和一致性

readOuiRegistries 在局部新建 Map,不会在每读一行时修改模块级 blocks;但不能据此推断整个下载和热更新流程具有跨文件原子性。describeMac 只读取传入索引,没有可见写操作。处理器本身没有重试或异常捕获,查询函数抛出的异常如何变成 HTTP 响应,需查看上层错误处理,本文不作推断。

性能、运维与扩展

  • 查询成本固定且较小:最多尝试三种前缀长度,执行 Map.has/Map.get,不会逐条遍历厂商记录。
  • 加载成本集中:readOuiRegistries 使用 fs.readFileSync;CSV 解析和合并均为同步执行。加载期间会占用 Node.js 执行线程,不应将该读取路径放入每次请求。
  • 内存使用随数据量增长:解析会构造行数组、条目数组和最终索引;当前证据不足以给出具体内存数值。
  • 排查顺序:先检查 HTTP 状态,再区分 found,随后检查输入长度是否足以定位子分配块,最后检查本地五表是否齐全及加载日志。
  • 扩展注册表:新增规格会参与 OUI_FILES 与 HEX_LENGTHS 推导,但还需要验证下载器兼容性、CSV 格式、最小记录数和特殊地址语义,不能只添加 URL 就宣称端到端支持。
  • 扩展国家推断:应针对地址尾部格式增加有依据的规则,而不是把推断值当作源 CSV 的权威国家字段。

以上性能结论来自本地匹配及同步加载代码,不是基准测试结果。参考:oui-db.js、oui-db.js、oui-db.js。

测试接缝与验证重点

已核实 createMacChecker(lookup) 为 fixture 测试保留了依赖注入入口;本次没有读取完整测试实现,不声明以下情况已有测试覆盖。维护时建议重点验证:6~12 位边界、混合分隔符、MA-L 与子块重叠、组播范围转换、CID 取消随机化标志、未命中响应、CSV 截断及最低记录数失败。

相关链接