Repository Wiki
jason5ng32/MyIP

ASN 画像、历史与自治系统互联关系

本能力通过三个互补视角描述自治系统:聚合多个来源的 ASN 画像、查询前缀的起源 ASN 历史,以及从 CAIDA 关系快照构建面向 Tier 1 的互联拓扑。实现强调来源隔离、可解释的数据筛选,以及名称补全失败时仍能返回核心网络信息。

目的与范围

本文覆盖 /api/asn-profile、/api/asn-history、/api/asn-connectivity 的处理逻辑,以及画像组合器的状态、时限与完整性契约。适用于排查画像缺项、历史记录被过滤、互联图与邻居计数不一致等问题。

边界如下:

  • 画像是以 ASN 为输入的多源聚合;历史是以 CIDR 前缀为输入的路由观测摘要,不是 ASN 注册资料变更历史。
  • 互联图是经筛选的上游及 Tier 1 对等关系视图,不是全网 BGP 拓扑,也不是实时探测结果。
  • Radar、RDAP、ASRank、PeeringDB、CAIDA 下载与索引内部实现,以及前端布局和全局缓存中间件,不在本页展开。相关主题应分别查阅对应集成、离线数据与前端页面。
  • 本页依据四个核心模块的已读实现;路由注册、输入校验实现、缓存实际 TTL 和测试断言未在本次源码范围内核验。

概述

视角输入主要来源输出价值
ASN 画像asnRadar、互联关系、RDAP、ASRank、私有信誉 API、PeeringDB一次请求获得多个独立区块及其状态
路由历史prefixRIPEstat routing-history、组织名称解析展示起源 ASN 的观测起止时间、前缀和相对可见度
互联关系asn本地 CAIDA 关系及组织数据;名称缺失时回退 RIPEstat生成有向关系图及直接 providers、peers、customers 列表

三个接口不是线性流水线:画像会直接复用 getAsnConnectivity,但没有把历史区块纳入 SECTIONS。历史需要单独提供前缀。

依据:asn-profile.js、asn-profile.js、asn-history.js。

架构

Loading diagram...

Sources: asn-profile.js、asn-profile.js、asn-connectivity.js、asn-connectivity.js、asn-history.js。

画像入口负责注入真实数据源;buildSectionLoaders 负责把来源返回值转换为统一状态;composeAsnProfile 负责并发、截止时限和最终组装。这种分工使数据获取、业务分类、失败隔离可以分别测试,而不是把所有异常集中到一个请求级失败分支。

画像聚合:从入口到分区结果

1. 注入来源与就绪检查

画像处理器将 req.query.asn 转为 Number,然后为七个区块建立 loader。接口注释说明 ASN 已经由 requireValidASN 规范化;处理器本身并未重新验证有效范围。

入口有两处容易影响冷启动行为的保护:

  • connectivityReadiness:as-rel 快照未加载时抛出 CAIDA as-rel snapshot not loaded;组织快照仍在下载时抛出 CAIDA as2org snapshot still loading。组织快照下载失败与“仍在加载”不同:注释说明名称可回退 RIPEstat,所以不会永远阻断拓扑。
  • peeringdbReadiness:没有 Radar API key 时返回 false,使区块成为 disabled;已经配置但索引未加载时抛错,使区块成为 error,避免把暂时不可用当成长期空结果。

这一保护仅在已读画像入口中显式装配;独立互联处理器直接调用 getAsnConnectivity,不能假定两条路径具备相同的就绪门禁。

源码示例:连接关系来源的就绪包装器。

javascript
1export const connectivityReadiness = ({ relLoaded = isAsRelLoaded, orgLoaded = isAsOrgLoaded, fetch = getAsnConnectivity } = {}) => 2 (...args) => { 3 if (!relLoaded()) throw new Error('CAIDA as-rel snapshot not loaded'); 4 if (isStillLoading(orgLoaded)) throw new Error('CAIDA as2org snapshot still loading'); 5 return fetch(...args); 6 };

Source: asn-profile.js。

2. 四种状态与“不完整”标记

响应包含 asn、status、incomplete,以及七个同名数据区块。只有 ok 区块持有数据;其他状态对应 null。

状态含义与故障的区别
ok有可展示的数据仍可能在 incomplete 中,不能只看 ok 判断完整性
empty来源成功回答,但没有该 ASN 的有效记录不是上游错误
error抛错或超过区块时限会记录区块级警告
disabled当前部署未配置该来源不参与“所有来源失败”的判断

各区块的分类并非简单的“非空即成功”:

区块成功、空与异常的判据
radarhasMeaningfulField(summary) 为真则 ok;同时有 failedSegments 则标记不完整。没有有效字段但存在失败段时抛错,否则 empty
prefixesbody.prefixes 必须是数组,否则抛出 malformed bgp-prefixes payload;非空为 ok,空为 empty;部分来源结果可标记不完整
connectivity有边,或 providers、peers、customers 计数之和非零,即为 ok;只有原点的孤立图可能为 empty
whoisRDAP 成功即 ok;被 isAutnumMissing 识别的错误为 empty,其他异常继续抛出
rankrecord.rank != null 为 ok,因此数值 0 不会被当成空
reputation未配置返回值为 disabled;必须 HTTP 200 且 data.found 为布尔值;found 决定 ok 或 empty
peeringdb未启用为 disabled;启用后按记录是否存在决定 ok 或 empty

hasMeaningfulField 对数字要求有限且非零,对字符串要求非空,并通过抽取数字判断是否为零。因此它是展示启发式,不是任意字符串或嵌套对象的通用有效性校验。

依据:asn-profile.js。

3. 并发与超时隔离

所有 loader 都通过 Promise.resolve().then(...) 启动,使同步抛错也成为 Promise 拒绝;每个 loader 独立套用 withDeadline,再由 Promise.allSettled 收集结果。某个区块失败不会取消其他区块。

javascript
1export const withDeadline = (promise, ms) => { 2 let timer; 3 const deadline = new Promise((_, reject) => { 4 timer = setTimeout(() => { 5 reject(Object.assign(new Error(`deadline of ${ms} ms exceeded`), { code: 'deadline' })); 6 }, ms); 7 }); 8 return Promise.race([promise, deadline]).finally(() => clearTimeout(timer)); 9};

Source: asn-profile.js。

超时不等于取消。 Promise.race 只停止等待该区块,底层工作仍可能继续;晚到结果不会进入本次响应。区块时限还依赖事件循环调度,不能视为硬实时保证。

Loading diagram...

Sources: asn-profile.js、asn-profile.js、asn-profile.js。

allSourcesFailed 先排除 disabled,再要求剩余状态非空且全部是 error。因此一个 empty 区块足以避免整体 502;全部为 disabled 也不会满足失败条件。502 返回 { error: 'All sources failed', status },不是完整画像对象。

4. 完整性与缓存边界

isCompleteProfile(body) 要求存在 status、其中没有 error,并且 incomplete 为空。empty 和 disabled 不破坏完整性。

组合器注释说明,这个判定用于区分完整响应的周级缓存与降级响应的日级缓存;本页未读取服务端缓存注册,因而不把该注释当作已核验的缓存头或部署保证。可以确定的设计约束是:暂时失败必须暴露为 error 或不完整,不能伪装成可长期缓存的 empty。

依据:asn-profile.js。

互联关系:有界 BFS、关系筛选与名称补全

拓扑生成顺序

buildTopology(origin, rel = asRelApi) 是无 I/O 的同步函数。默认适配器提供 providers、peers、customers、Tier 1 判定及客户数量查询。

  1. 用 Map 保存节点,用 Set 去重有向边;原点类型为 origin 或 origin-tier1。
  2. 从原点开始按层遍历,最多处理三层。
  3. 对当前节点,先加入全部 Tier 1 provider 边,类型为 transit;再加入全部 Tier 1 peer 边,类型为 peering。这些 Tier 1 目标不加入下一层。
  4. 如果 Tier 1 peer 数量或 Tier 1 总邻接数量达到可信阈值,跳过非 Tier 1 providers,避免把推断噪声当成上游。
  5. 否则,对非 Tier 1 providers 按 customerCountOf 降序排序,只保留前三个用于继续展开。
  6. 已存在节点仍可接收新边,但不会重复进入下一层;遍历结束后清理无出边的中间节点。

这里把 Tier 1 peering 与 transit 同时纳入图,是为了展示不购买 transit、主要通过对等互联连接骨干网络的大型网络。只遍历 customer → provider 会漏掉这类互联路径。

源码示例:中间上游的分支排序和限制。

javascript
1 const intermediates = providers 2 .filter(p => !rel.isTier1(p)) 3 .sort((a, b) => rel.customerCountOf(b) - rel.customerCountOf(a)) 4 .slice(0, MAX_INTERMEDIATE_BRANCH); 5 6 for (const p of intermediates) { 7 addEdge(asn, p, 'transit'); 8 if (!nodes.has(p)) { 9 addNode(p, 'intermediate'); 10 nextLayer.push(p); 11 } 12 }

Source: asn-connectivity.js。

去重、剪枝与图的解释边界

  • 边按 from->to 去重,不把 kind 放进键。同一有向节点对第一次加入的关系获胜;代码先加入 transit,因此它优先于重复的 peering。
  • pruneLeafIntermediates 反复删除没有出边的 intermediate,再删除与被移除节点相连的边,直到达到不动点。父节点可能因为子节点被删除而成为下一轮叶子。
  • origin、origin-tier1、tier1 永不剪枝,所以空拓扑仍可保留原点。
  • 去重和有限层数避免重复扩展,却不构成环检测。互相指向的中间节点可能都有出边而被保留,不能把结果声明为严格 DAG,也不能声称每个幸存节点都能到达 Tier 1。
  • 三分支限制仅适用于非 Tier 1 providers;Tier 1 命中没有这一截断。图不是完整邻接列表。

依据:asn-connectivity.js、asn-connectivity.js。

邻居列表与组织名称

图生成后,buildGraph 对幸存节点并发调用 resolveAsnOrgName;单节点解析失败被捕获,名称保留 null,不破坏图。

直接邻居通过 countedRelationships(origin, rel) 取得。该调用复用关系筛选规则,使被视为误推断的非 Tier 1 上游不会重新出现在上游计数中。阈值常量由关系模块导入,本页未读取其数值定义,不推测具体门槛。

buildNeighbours 对每类邻居按客户数量降序排列,数量相同时按 ASN 升序,再截断到 50 条;counts 则使用完整数组长度。列表长度与计数不同是预期行为。

组织名称先查本地 lookupAsOrgName;随后 fillNeighbourNames 仅从已经解析的图节点补足缺失名称,不为所有列表项额外逐一发起网络查询。因此图外邻居可能一直为 null。

源码示例:独立接口和画像共享同一完整响应构建函数。

javascript
1export const getAsnConnectivity = async (asn) => { 2 const graph = await buildGraph(asn); 3 return { origin: asn, ...graph, neighbours: fillNeighbourNames(buildNeighbours(asn), graph.nodes) }; 4};

Source: asn-connectivity.js。

互联响应数据模型

字段结构或语义
origin请求 ASN,处理器转为整数
nodes[]{ asn, type, name };类型为 origin、origin-tier1、intermediate、tier1
edges[]{ from, to, kind };transit 指向 provider,peering 指向被选中的 Tier 1 peer
neighbours.countsproviders、peers、customers 三类未截断数量
neighbours.providers[] 等每项 { asn, name },每类最多 50 条

拓扑只向上游与 Tier 1 peer 展开,客户列表不会变成向下游递归的图。画像分类器同时检查图边和邻居数量,所以仅有 customers 的 ASN 也可能获得 connectivity: ok。

路由历史:前缀级起源摘要

过滤与时间聚合

历史处理器读取 req.query.prefix,通过是否包含 : 区分 IPv6 和 IPv4。注释说明此前经过 requireValidPrefix;本函数没有自行解析或校验完整 CIDR。

处理顺序如下:

  1. 调用 fetchRoutingHistory(prefix, { minPeersSeeing: 30 }),将 peer 门槛下推到 RIPEstat。
  2. 上游非成功响应直接返回 502;成功后读取 payload.data.by_origin,不存在时使用空数组。
  3. 对每个 origin,仅保留 IPv4 长度至少 /8、IPv6 至少 /19 的前缀,以排除过宽的默认路由或归属噪声。
  4. 合并该 origin 下所有接受前缀的 timelines;无有效前缀或无 timeline 则丢弃。
  5. 取最早 starttime、最晚 endtime 和最大的 full_peers_seeing。最大 peer 数小于 30 时丢弃;最终 peers 四舍五入。
  6. 按 lastSeen 字符串降序排列,再计算相对可见度。
  7. 去重 ASN 后并发补全组织名称,最终返回 { prefix, history }。
Loading diagram...

Source: asn-history.js。

firstSeen 和 lastSeen 是多个区间的外包络,并不证明 ASN 在两者之间连续发布。实现直接比较时间字符串而非解析日期,也没有输出中间空档;维护上游适配时需要保留可比较的时间格式。

相对可见度不是全网覆盖率

javascript
1 const peersMax = history.reduce((m, r) => Math.max(m, r.peers), 0); 2 for (const row of history) { 3 row.peersPct = peersMax > 0 ? Math.round((row.peers / peersMax) * 100) : 0; 4 }

Source: asn-history.js。

peers 是本行 timeline 中的峰值 RIS peer 观测数,不是与该 ASN 建立商业互联的 peer 数量;它与互联接口的 neighbours.peers 含义不同。peersPct 使用本次过滤后响应的最大值为基准,最高行是 100%,不能跨请求直接解释为互联网覆盖率。

名称降级与历史数据结构

每行包含:asn(字符串)、org、firstSeen、lastSeen、peers、prefixes、peersPct。prefixes 是通过长度过滤的前缀列表,并不是仅保留贡献最大 peer 数的前缀。

名称补全采用一次 Promise.all 收集全部 ASN,再统一写回。如果解析器有未被内部处理的拒绝,批次进入 catch;由于写回尚未开始,本批历史可能全部保持 org: null。历史仍成功返回,日志为 as-overview batch failed; returning ASN-only history。单项回退错误另有 as-overview lookup failed 警告钩子。

文件开头注释提及前端会将 IP 量化到 IPv4 /24、IPv6 /48 以共享边缘缓存;已读后端实现只是接受传入的前缀,并不执行这一量化。不要把此前端约定与 /8、/19 的历史过滤下限混淆。

依据:asn-history.js、asn-history.js、asn-history.js。

参数、时限与配置边界

下表均为源码常量或注入参数,并非已确认可通过环境变量修改的设置。

参数类型默认值作用
SOURCE_TIMEOUTS.autnumnumber,毫秒5000传给 RDAP autnum
SOURCE_TIMEOUTS.ranknumber,毫秒4000传给 ASRank
SOURCE_TIMEOUTS.reputationnumber,毫秒4000传给私有信誉接口
DEADLINES.radar / prefixesnumber,毫秒各 10000两个 Radar 区块的外层时限
DEADLINES.connectivitynumber,毫秒5000互联区块外层时限
DEADLINES.whoisnumber,毫秒14000覆盖 bootstrap 与 autnum 的区块预算
DEADLINES.rank / reputationnumber,毫秒各 5000两个区块的外层时限
DEADLINES.peeringdbnumber,毫秒1000本地索引区块时限
composeAsnProfile 的 deadlinesobjectDEADLINES可注入替代预算
MAX_DEPTHnumber3互联 BFS 处理层数
MAX_INTERMEDIATE_BRANCHnumber3每个节点展开的非 Tier 1 provider 上限
NEIGHBOUR_LIST_CAPnumber50每类邻居列表上限,不影响计数
MIN_PREFIXobject{ v4: 8, v6: 19 }历史前缀长度下限
MIN_PEERSnumber30上游查询与本地历史筛选共用门槛

INNER_TIMEOUTS 是区块预算设计的参考表,不是 withDeadline 实际读取的配置。它记录 Radar 和 prefixes 8000 ms、whois 13000 ms、connectivity 3000 ms、rank 与 reputation 4000 ms、PeeringDB 0 ms;实际组合器读取 DEADLINES。

Radar 与 prefixes 通过 hasRadarApiKey() 判断是否启用;PeeringDB 就绪包装器也使用同一配置判定。信誉来源是否配置由 requestAsnReputation 的返回值体现,调用时会传递 req.headers。具体密钥环境变量、转发头筛选和认证实现不在已读范围内,不能据此认定任何客户端请求头都安全可信。

依据:asn-profile.js、asn-profile.js、asn-connectivity.js、asn-history.js。

API 与函数参考

HTTP 处理入口

路径来自各模块注释;本页未核验路由注册时允许的 HTTP 方法。

路径查询参数成功响应显式错误分支
/api/asn-profileasn,按入口约定为规范化数字字符串数值 asn、七区块、status、incomplete所有已启用来源均失败时 502,返回错误及状态表
/api/asn-connectivityasn,处理器用 parseInt(..., 10)origin、nodes、edges、neighbours捕获异常后 500,返回 error.message
/api/asn-historyprefix,经校验的 CIDRprefix、history上游非成功 502;AbortError 504;其他捕获异常 500

未确认校验中间件的拒绝状态码,因此这里不列假设的 400 契约。

可复用函数

以下签名保留 JavaScript 原始形式;类型说明来自已读调用和返回结构,并非 TypeScript 声明。

函数签名参数及返回错误或副作用
buildSectionLoaders(deps)注入来源函数;返回七个异步 loader分类或来源失败可拒绝,由组合器隔离
composeAsnProfile(asn, loaders, { deadlines = DEADLINES } = {})ASN、区块函数集合、可选预算;异步返回画像对象loader 失败转为 error 和 null,记录警告
withDeadline(promise, ms)Promise 与毫秒;返回竞争结果的 Promise超时拒绝,错误 code 为 deadline;不取消原任务
allSourcesFailed(body)返回 boolean排除 disabled 后要求至少一个状态且全为 error
isCompleteProfile(body)返回 boolean检查状态存在、无 error、无不完整区块
buildTopology(origin, rel = asRelApi)数值 ASN 与可选关系适配器;返回 { nodes, edges }同步,无名称查询;依赖异常不在函数内捕获
buildNeighbours(origin, rel = asRelApi, lookupName = lookupAsOrgName)ASN、关系适配器、名称函数;返回计数和三类列表同步本地查询
fillNeighbourNames(neighbours, nodes)邻居对象和已解析图节点;返回邻居对象原地更新列表项名称
getAsnConnectivity(asn)数值 ASN;异步返回完整互联响应单节点名称失败被吞掉,其他失败可向上传播

依据:asn-profile.js、asn-connectivity.js。

故障、并发与运维注意事项

排障优先级

  1. 画像返回 200 但缺区块:先检查对应 status;disabled 查来源配置,empty 查数据语义,error 查 asn-profile: section failed 日志。
  2. 画像数据可见却不完整:检查 incomplete,尤其是 Radar 分段失败、prefixes 部分结果;不要只看 HTTP 状态或 ok。
  3. 冷启动互联或 PeeringDB 错误:检查离线快照就绪状态,不应通过返回空数组掩盖加载失败。
  4. 互联图名称缺失:名称失败本来就允许降级;该路径没有历史接口的逐项回退警告钩子。
  5. 历史为空:可能确实无 by_origin,也可能所有记录被前缀下限、缺少 timeline 或 peer 门槛过滤;空数组并不等同于上游不可用。
  6. 历史 504:处理器将外层捕获的 AbortError 识别为上游超时。名称补全内部失败通常被内层 catch 降级,不应与路由历史主查询超时混为一谈。

并发与性能

  • 画像来源并发执行,等待时间主要由最慢尚未完成的区块决定,而不是七个来源耗时相加;默认最大区块预算为 14 秒。
  • 互联 BFS 同步读取本地索引,分支限制和叶子清理控制图规模;名称查询只针对剪枝后节点并发执行。
  • 邻居列表在截断前排序,因此输出上限 50 不代表仅处理 50 个邻居。
  • 历史组织查询先去重 ASN;互联名称查询和历史批量查询均未在已读函数中设置并发池上限。
  • 这些核心函数未实现请求间去重、重试、持久化写入或自己的响应缓存。底层来源是否具备这些机制,需继续核验对应模块,不能从这里推断为不存在。
  • 超时后底层任务继续运行,因此外层时限限制的是响应等待,而非整个系统的上游工作量。

扩展与测试边界

增加画像来源时,需要同时维护 SECTIONS、loader、区块预算和返回状态分类,并评估前端是否认识新增字段。来源异常应进入现有隔离机制;有部分结果时保留 ok 并设置 incomplete,而不是无条件丢弃可用数据。

互联逻辑通过 rel 和 lookupName 注入支持无网络 fixture;画像通过 deps、loaders 和 deadlines 注入支持确定性测试;就绪包装器也暴露了依赖覆盖参数。这些是源码明确提供的测试入口。

本次发现了相关测试文件名称,但未读取测试正文,因此不宣称已有断言覆盖。建议重点核验:重复边 transit 优先、循环节点与叶子剪枝、50 条截断但计数完整、部分 Radar 结果、所有来源失败、超时后迟到结果、历史门槛边界及名称批次拒绝。

相关链接