Repository Wiki
jason5ng32/MyIP

IP 地址与子网计算

本页介绍 MyIP 的 IPv4/IPv6 地址运算核心:严格解析、规范化表示、掩码与容量计算、子网拆分、网段聚合以及地址区间转换,并说明前端特殊地址分类如何复用这些能力。

目的与范围

本文面向维护计算逻辑的开发者,重点覆盖共享纯函数的输入契约、算法、结果结构与边界条件。前端计算器的输入解释、特殊地址注册表与展示格式也在范围内,但只描述已核实的实现。

公网 IP 获取、地理位置查询、RDAP 网络请求、MAC 厂商查询与页面交互不在本页展开。共享核心的文件注释说明它同时服务于前端 IP Calculator 和后端 RDAP 的 CIDR 包含判断;本文不据此推断后端调用细节。计算器入口 calculate(raw)、滑块调用 analyzeCidr 以及 IPv6 接口标识分析出现在模块说明中,其完整函数体未纳入本次读取范围,故不列出未经核实的详细契约。

依据:ip-math.js、ip-calc.js。

概述

实现把“地址算术”和“输入解释/展示”分开:

  • 地址算术层统一使用 BigInt。这样既避免 JavaScript 32 位位运算造成高位 IPv4 变为负数,也能精确表示 IPv6 地址及其地址数量。
  • 严格解析层只接受明确的地址语法,不自动去除空白,不把前导零 IPv4 当成普通十进制地址。
  • 网段层同时保存输入地址和清零主机位后的网络地址,既支持规范化,也能识别输入是否对齐。
  • 展示辅助层根据静态特殊地址表分类,并将大整数计数转换成精确十进制、分组字符串、二次幂和近似表示。

这套核心适用于检查地址是否合法、计算可用范围、验证包含或重叠关系、拆分地址规划以及压缩地址列表。所读取的共享核心没有外部依赖、网络请求、数据库写入或异步任务。

架构

Loading diagram...

Sources: ip-math.js、ip-math.js、ip-calc.js。

图中的关系是实际函数调用或数据访问,而不是网络服务之间的连接。前端分析模块通过本地 ip-math 桥接导入核心函数;测试逐项比较桥接导出与共享导出的函数身份,防止前后端出现两套不同的算法。

核心数据结构

结构字段含义
IPv4 解析结果family: 4、value: bigint32 位无符号地址数值
IPv6 解析结果family: 6、value: bigint、embeddedV4: boolean128 位数值;记录输入是否使用点分 IPv4 尾部
CIDR 解析结果family、prefix、address、network、aligned保留原地址、归一化网络地址及二者是否相同
网段详情上述相关字段以及 cidr、broadcast、first、last、lastAddress、mask、wildcard、count、usable供范围与容量展示使用
拆分结果family、prefix、total、subnets、truncated精确总数与有限长度的字符串列表分离
聚合结果v4、v6、invalid两个地址族分别输出,保留无法解析的原始项

这些都是内存对象,不是持久化实体。cidrInfo 的地址与数量字段仍为 BigInt,不要将它与前端已经格式化的展示对象混淆。

依据:ip-math.js、ip-math.js。

解析与规范化

IPv4:严格的四段十进制

parseIPv4(str) 首先检查字符串类型与四段结构,再要求每段符合 0 或无前导零的 1~3 位十进制,且数值不超过 255。地址通过左移 8 位和按位或逐段构建。

因此 01.2.3.4、127.1、0x7f.0.0.1、带空白的地址及带 /24 的字符串都不是该函数接受的 IPv4。计算器模块说明中提及对 inet_aton 风格混淆表示的宽松解释,那属于上层输入解释,不能据此放宽共享解析器。

IPv6:先处理 IPv4 尾部,再展开压缩段

parseIPv6(str) 的顺序如下:

  1. 限定字符为十六进制、冒号和点,排除方括号、zone ID 与空白。
  2. 如含点号,只允许最后一个冒号后的点分 IPv4;交给严格 IPv4 解析器校验,再换成两个十六进制组。
  3. 按 :: 分割,最多允许一次压缩。
  4. 无压缩时必须有八组;有压缩时显式组最多七组,保证 :: 确实替代至少一组。
  5. 验证每组为 1~4 个十六进制字符,逐组左移 16 位构建数值。

embeddedV4 记录输入形式,不改变地址族:::ffff:192.0.2.1 依然是 IPv6。

依据:ip-math.js。

CIDR:保留主机位,计算网络位

parseCidr(str) 要求恰好一个 /,左侧必须是严格 IP。右侧接受 1~3 位数字前缀;仅 IPv4 还可使用点分掩码。点分掩码必须是连续的高位 1 后接低位 0,否则 maskToPrefix 返回 null。

成功时,network = address & mask,并以 aligned 标记原地址是否就是网络地址。数字前缀检查与 IPv4 段检查不同:数字前缀的正则允许前导零,然后通过 Number 转换并验证范围。

formatCidr 默认输出网络地址;指定 network: false 才使用保留的原始地址。这种区分避免在标准化结果时丢失输入是否对齐的信息。

IPv6 输出规则

formatIPv6 默认使用小写、去除每组前导零,并压缩最长的连续零组;同长度时选择第一段,单个零组不压缩。expanded: true 输出八组、每组四位的完整形式。点分 IPv4 尾部输入在默认输出中变为十六进制组,而不是保留原始写法。

依据:ip-math.js。

掩码、容量与网段详情

地址族决定位宽:IPv4 为 32,IPv6 为 128。prefixToMask 先构造 prefix 个 1,再左移主机位数;wildcardMask 使用地址族的全 1 最大值与掩码异或,避免产生无限宽的有符号补码。

addressCount(prefix, family) 返回 2^(位宽 - prefix)。usableCount 对 IPv4 的 /0~/30 扣除网络和广播地址,对 /31、/32 不扣除;IPv6 则返回全部地址数。这里是实现中的容量口径,不应解释为所有地址在具体网络策略下均可实际分配。

smallestPrefixFor(count, family) 从最大前缀向零遍历,返回能容纳给定地址总数的最小块对应前缀。它不按 IPv4 可用主机数补足网络与广播地址。内部通过 BigInt(count) 转换并捕获转换异常,小于 1 或超过整个地址族容量则返回 null。

cidrInfo 执行过程

Loading diagram...

Source: ip-math.js。

范围字段需要区分:

  • network 到 lastAddress 是完整地址块,cidrToRange 返回的正是这两个边界,且包含端点。
  • first 到 last 是实现定义的可用范围:仅 IPv4 且前缀小于 31 时去掉首尾。
  • broadcast 对 IPv6 为 null;对所有 IPv4 前缀都填入 lastAddress,包括 /31 与 /32。因此不能仅凭该字段非空判断链路实际采用广播语义。

依据:ip-math.js、ip-math.js。

子网集合算法

拆分:精确总量与有限输出分离

splitCidr(cidrStr, newPrefix, options = {}) 只允许新前缀不小于原前缀,且不超过地址族位宽。输入不对齐时,从解析得到的网络地址开始拆分。

总子网数为 2^(newPrefix - 原前缀),每块大小由 addressCount(newPrefix, family) 给出。循环按 network + i * size 输出子网,但只输出 min(total, limit) 项。total 始终保留为精确 BigInt,truncated 明确标记列表是否不完整。

同前缀返回标准化后的原块;limit: 0 可以只获取总数而不生成任何地址字符串。默认 1024 是输出保护,而不是子网总数的上限。

聚合:按地址族排序,用栈逐级合并

aggregateCidrs(list) 对每个字符串先 trim(),依次尝试 CIDR 与裸 IP;裸 IPv4 转为 /32,裸 IPv6 转为 /128。无法解析的项原样放入 invalid。

随后分别处理 IPv4 和 IPv6:

  1. 按网络地址升序排列;同地址时较短前缀在前。
  2. 若当前栈顶已经覆盖新块,跳过新块。
  3. 否则入栈,检查最后两个块是否前缀相同、数值相邻且前一个块在父前缀边界对齐。
  4. 满足条件就替换为父块,并继续向上合并。

对齐检查是关键:相邻、大小相同的两个地址块并不一定能表示为一个不额外覆盖地址的父 CIDR。实现只合并严格的兄弟块,不生成带空洞的超网。两种地址族始终分别返回。

区间转 CIDR:选择当前能容纳的最大对齐块

rangeToCidrs(startStr, endStr) 拒绝无效地址、跨地址族和反向区间。对于闭区间中的当前起点 cur,从 /0 向更长前缀扫描,找到第一个同时满足“起点对齐”和“不超过结束地址”的块,然后将 cur 增加该块大小。函数按块推进,不逐地址枚举。

反向转换 cidrToRange(cidrStr) 返回 { family, start, end },端点为 BigInt;它不是仅返回可用主机范围。

依据:ip-math.js。

包含判断与排序

函数签名输入要求与返回值
prefixContains(network, prefix, family, value)数值须为本地址族范围内的 BigInt,前缀须为合法整数。比较右移主机位后的值;非法输入返回 false
cidrContains(cidrStr, ipStr)解析 CIDR 和地址;合法同族时返回布尔值;解析失败或跨族返回 null
cidrOverlaps(aStr, bStr)判断任一网络地址是否被另一网段包含;非法或跨族返回 null
compareIps(a, b)接收已有的解析对象;IPv4 排在 IPv6 前,同族按 value 排序,返回 -1、0 或 1

prefixContains 不要求传入的 network 已经清零主机位,因为右移比较会忽略这些位。compareIps 则没有参数校验,应只对已解析成功的对象使用。

尤其要注意 false 和 null 的差异:前者可以表示合法输入但不包含,后者表示无法比较。模块顶部关于谓词失败值的概括不能替代每个函数的实际分支。

依据:ip-math.js。

前端地址分类与展示

最长前缀优先的特殊地址匹配

静态的 IPV4_SPECIAL_BLOCKS 与 IPV6_SPECIAL_BLOCKS 保存 cidr、id、英文 label、RFC 编号数组、scope 和 global。compileBlocks 在模块初始化时解析 CIDR,为每行添加 family、network 和 prefix。

lookupBlocks(family, value) 返回所有包含该地址的记录,按前缀长度降序排列。因此更具体的例外记录排在广义保留块前面。内部 classify 取第一项作为主分类,同时保留完整匹配列表:

  • IPv4 未匹配任何记录时,默认 scope: 'global'、isGlobal: true。
  • IPv6 未匹配任何记录时,插入 Reserved by IETF 分类,而不是默认视为全球单播。

这是静态地址属性分类,不是联网可达性探测。记录中的 global 不代表当前网络一定能访问该地址。

依据:ip-calc.js。

大数展示与反向解析名称

formatCount(value) 只接受非负 BigInt,输出:

字段含义
exact完整十进制字符串
grouped每三位加逗号的字符串
pow2若为正的二次幂,返回指数;否则 null
approx十进制位数超过 15 时生成近似科学计数表示;否则 null

countLabel(count) 在 pow2 >= 10 时附加二次幂表达,其余情况只显示分组字符串。近似值只用于可读性,精确值仍保留在 exact。

ptrName(parsed) 把 IPv4 字节倒序后拼接 .in-addr.arpa,把 IPv6 的 32 个十六进制字符倒序逐位拼接 .ip6.arpa。它生成名称,不发起 DNS 查询。ptrZone 的完整实现未读取,不在此推断其返回契约。

依据:ip-calc.js。

源码使用示例

以下片段均直接摘自仓库;测试片段保留其测试环境上下文,算法片段是实际实现而非新增示例。

1. 基础解析与无效值处理

javascript
1 it('dispatches by family', () => { 2 assert.equal(parseIp('10.0.0.1').family, 4); 3 assert.equal(parseIp('::1').family, 6); 4 assert.equal(parseIp('nope'), null); 5 assert.equal(ipToBigInt('10.0.0.1'), 0x0a000001n); 6 assert.equal(ipToBigInt('junk'), null); 7 });

Source: ip-math.test.js。

该测试明确区分解析对象与单独的数值返回,并验证非法字符串返回 null。

2. 可用地址数量的协议边界

javascript
1export const usableCount = (prefix, family) => { 2 const count = addressCount(prefix, family); 3 if (count === null) return null; 4 if (family === 6 || prefix >= 31) return count; 5 return count - 2n; 6};

Source: ip-math.js。

不能简单地对所有网段执行“总数减二”;上层展示应复用该分支而非重复实现。

3. 受输出限制保护的拆分循环

javascript
1 const total = 1n << BigInt(newPrefix - cidr.prefix); 2 const size = addressCount(newPrefix, family); 3 const emit = total < BigInt(limit) ? Number(total) : limit; 4 const subnets = []; 5 for (let i = 0n; i < BigInt(emit); i += 1n) { 6 subnets.push(`${formatIp({ family, value: network + i * size })}/${newPrefix}`); 7 } 8 return { family, prefix: newPrefix, total, subnets, truncated: BigInt(emit) < total };

Source: ip-math.js。

调用方应同时展示 total 和 truncated,不能把输出列表长度当作全部子网数量。

4. 前端桥接一致性测试

javascript
1 it('re-exports every common export unchanged', () => { 2 for (const [name, fn] of Object.entries(common)) { 3 assert.equal(bridge[name], fn, `bridge is missing or diverges on ${name}`); 4 } 5 });

Source: ip-math.test.js。

这不只是比较计算结果,而是要求导出引用相同,从而保护共享实现的统一性。

参数与配置

已读取的算法代码使用函数参数与静态表,没有环境变量、服务端配置项或存储配置。

选项类型默认值约束及用途
formatIPv6 的 expandedbooleanfalse输出完整八组十六进制形式
formatCidr 的 networkbooleantruefalse 时使用原始 address 而非 network
splitCidr 的 options.limitnumber1024非负安全整数;限制生成列表长度,不改变精确总数
familynumber无核心校验接受 4 或 6
prefix/newPrefixnumber无整数,IPv4 范围 0~32,IPv6 范围 0~128

特殊地址表是代码级规则,不是运行时远程注册表。修改规则需同时考虑宽泛地址块和更具体例外项之间的最长前缀优先关系。

依据:ip-math.js、ip-math.js、ip-math.js。

API 速查

以下是已读取的 JavaScript 参数签名;返回类型按实现说明,不表示仓库存在额外的 TypeScript 声明。

签名返回结果
parseIPv4(str)、parseIPv6(str)、parseIp(str)解析对象或 null
ipToBigInt(str)bigint 或 null
parseCidr(str)CIDR 对象或 null
toOctets(value)、toHextets(value)数值数组或 null
formatIPv4(value)、formatIPv6(value, { expanded = false } = {})字符串或 null
formatIp(parsed)、formatCidr(cidr, { network = true } = {})字符串或 null
prefixToMask(prefix, family)、wildcardMask(prefix, family)掩码 bigint 或 null
maskToPrefix(mask, family)连续掩码对应前缀数值或 null
addressCount(prefix, family)、usableCount(prefix, family)数量 bigint 或 null
smallestPrefixFor(count, family)满足地址总量的前缀数值或 null
cidrInfo(str)网段详情或 null
splitCidr(cidrStr, newPrefix, options = {})拆分结果或 null
aggregateCidrs(list){ v4, v6, invalid };非数组输入返回三个空数组
rangeToCidrs(startStr, endStr){ family, cidrs } 或 null
cidrToRange(cidrStr){ family, start, end } 或 null
lookupBlocks(family, value)按前缀降序排列的匹配记录数组
formatCount(value)展示计数对象或 null
countLabel(count)展示字符串;空值返回空字符串
ptrName(parsed)反向 DNS 名称字符串或 null

来源:ip-math.js、ip-calc.js。包含判断和排序函数见前述专节。

失败模式、边界与并发

不要把“通常返回空值”解释成任意参数都不会抛错

共享模块注释将导出概括为纯函数、不可用输入返回空值,但实际调用仍须遵守对象形状与选项契约:

  • compareIps 直接读取对象字段,未对 null 等值做防御。
  • formatIPv6、formatCidr 使用选项对象解构;省略选项与显式传入 null 不等价,后者可能触发 JavaScript TypeError。
  • splitCidr 对选项有显式检查:null、非对象、数组、负数或非安全整数 limit 返回 null。
  • aggregateCidrs 对非数组输入返回空结果,而不是 null;对无效数组元素则保留在 invalid 中。
  • smallestPrefixFor 捕获 BigInt 转换异常,但输入若已经是失去精度的 number,转换并不能恢复原值;大计数应保持 BigInt。

依据:ip-math.js、ip-math.js、ip-math.js。

并发与运行成本

核心函数同步执行,工作数组在调用内创建,没有共享请求状态、锁、重试、超时或后台队列。多个调用之间不存在异步结果覆盖问题,但大规模同步计算仍可能占用调用线程。

  • 拆分成本主要随实际生成项数增长。默认上限保护内存;调用方若传入极大的合法 limit,仍可能生成巨量字符串,代码没有额外的固定硬上限。
  • 聚合需要按地址族排序,成本主要由输入数量决定,随后通过栈合并,不遍历地址块内部的每个 IP。
  • 区间转换按生成 CIDR 块推进,每次至多扫描地址族位宽对应的前缀范围。
  • maskToPrefix 最多尝试 IPv4 的 33 个或 IPv6 的 129 个前缀,不涉及外部查询。
  • BigInt 结果应先转换成合适的字符串展示结构,再交给需要 JSON 数据的边界;前端 formatCount 已提供精确字符串通道。

以上为源码控制流分析,不是基准测试结果。

扩展与测试

扩展原则

  1. 新增基础运算优先放在共享算术层,保持前后端复用;桥接测试会检查新增导出是否保持一致。
  2. 对宽松输入的解释不要直接修改严格 IPv4 解析规则,否则会破坏现有拒绝用例。
  3. 新增特殊地址规则时保留 scope、global、RFC 引用与 CIDR,并验证最长前缀匹配的例外覆盖关系。compileBlocks 直接使用解析结果,错误的静态 CIDR 可能导致初始化失败。
  4. 在调用边界区分非法输入、合法但不包含、部分生成列表这三类结果,不统一吞成空数组或 false。

已核实的测试覆盖与证据限制

读取的测试片段使用 Node.js node:test 与 node:assert/strict,明确覆盖:

  • 前端桥接与共享导出引用一致。
  • IPv4 极值、标准地址及前导零、空白、短写、非字符串输入的拒绝。
  • IPv6 压缩、大小写、嵌入 IPv4 尾部及 embeddedV4 标记。
  • 多重 ::、zone ID、方括号、错误组数和非十六进制字符的拒绝。
  • parseIp 地址族分发与 ipToBigInt 失败返回值。

格式化测试片段还列出了最长零段压缩、同长度择首、单零不压缩和 IPv4 尾部转十六进制等用例。未读取完整测试文件,也未执行测试,因此本文不宣称完整覆盖率或测试已通过。UI 路由、查询参数同步、完整 IPv6 接口分析与实际 RDAP 调用路径的实现细节未在已读取源码中核实。

依据:ip-math.test.js。

相关链接

Sources

(3 files)
frontend/utils