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 画像 | asn | Radar、互联关系、RDAP、ASRank、私有信誉 API、PeeringDB | 一次请求获得多个独立区块及其状态 |
| 路由历史 | prefix | RIPEstat routing-history、组织名称解析 | 展示起源 ASN 的观测起止时间、前缀和相对可见度 |
| 互联关系 | asn | 本地 CAIDA 关系及组织数据;名称缺失时回退 RIPEstat | 生成有向关系图及直接 providers、peers、customers 列表 |
三个接口不是线性流水线:画像会直接复用 getAsnConnectivity,但没有把历史区块纳入 SECTIONS。历史需要单独提供前缀。
依据:asn-profile.js、asn-profile.js、asn-history.js。
架构
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,不能假定两条路径具备相同的就绪门禁。
源码示例:连接关系来源的就绪包装器。
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 | 当前部署未配置该来源 | 不参与“所有来源失败”的判断 |
各区块的分类并非简单的“非空即成功”:
| 区块 | 成功、空与异常的判据 |
|---|---|
radar | hasMeaningfulField(summary) 为真则 ok;同时有 failedSegments 则标记不完整。没有有效字段但存在失败段时抛错,否则 empty |
prefixes | body.prefixes 必须是数组,否则抛出 malformed bgp-prefixes payload;非空为 ok,空为 empty;部分来源结果可标记不完整 |
connectivity | 有边,或 providers、peers、customers 计数之和非零,即为 ok;只有原点的孤立图可能为 empty |
whois | RDAP 成功即 ok;被 isAutnumMissing 识别的错误为 empty,其他异常继续抛出 |
rank | record.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 收集结果。某个区块失败不会取消其他区块。
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 只停止等待该区块,底层工作仍可能继续;晚到结果不会进入本次响应。区块时限还依赖事件循环调度,不能视为硬实时保证。
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 判定及客户数量查询。
- 用
Map保存节点,用Set去重有向边;原点类型为origin或origin-tier1。 - 从原点开始按层遍历,最多处理三层。
- 对当前节点,先加入全部 Tier 1 provider 边,类型为
transit;再加入全部 Tier 1 peer 边,类型为peering。这些 Tier 1 目标不加入下一层。 - 如果 Tier 1 peer 数量或 Tier 1 总邻接数量达到可信阈值,跳过非 Tier 1 providers,避免把推断噪声当成上游。
- 否则,对非 Tier 1 providers 按
customerCountOf降序排序,只保留前三个用于继续展开。 - 已存在节点仍可接收新边,但不会重复进入下一层;遍历结束后清理无出边的中间节点。
这里把 Tier 1 peering 与 transit 同时纳入图,是为了展示不购买 transit、主要通过对等互联连接骨干网络的大型网络。只遍历 customer → provider 会漏掉这类互联路径。
源码示例:中间上游的分支排序和限制。
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。
源码示例:独立接口和画像共享同一完整响应构建函数。
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.counts | providers、peers、customers 三类未截断数量 |
neighbours.providers[] 等 | 每项 { asn, name },每类最多 50 条 |
拓扑只向上游与 Tier 1 peer 展开,客户列表不会变成向下游递归的图。画像分类器同时检查图边和邻居数量,所以仅有 customers 的 ASN 也可能获得 connectivity: ok。
路由历史:前缀级起源摘要
过滤与时间聚合
历史处理器读取 req.query.prefix,通过是否包含 : 区分 IPv6 和 IPv4。注释说明此前经过 requireValidPrefix;本函数没有自行解析或校验完整 CIDR。
处理顺序如下:
- 调用
fetchRoutingHistory(prefix, { minPeersSeeing: 30 }),将 peer 门槛下推到 RIPEstat。 - 上游非成功响应直接返回 502;成功后读取
payload.data.by_origin,不存在时使用空数组。 - 对每个 origin,仅保留 IPv4 长度至少
/8、IPv6 至少/19的前缀,以排除过宽的默认路由或归属噪声。 - 合并该 origin 下所有接受前缀的
timelines;无有效前缀或无 timeline 则丢弃。 - 取最早
starttime、最晚endtime和最大的full_peers_seeing。最大 peer 数小于 30 时丢弃;最终peers四舍五入。 - 按
lastSeen字符串降序排列,再计算相对可见度。 - 去重 ASN 后并发补全组织名称,最终返回
{ prefix, history }。
Source: asn-history.js。
firstSeen 和 lastSeen 是多个区间的外包络,并不证明 ASN 在两者之间连续发布。实现直接比较时间字符串而非解析日期,也没有输出中间空档;维护上游适配时需要保留可比较的时间格式。
相对可见度不是全网覆盖率
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.autnum | number,毫秒 | 5000 | 传给 RDAP autnum |
SOURCE_TIMEOUTS.rank | number,毫秒 | 4000 | 传给 ASRank |
SOURCE_TIMEOUTS.reputation | number,毫秒 | 4000 | 传给私有信誉接口 |
DEADLINES.radar / prefixes | number,毫秒 | 各 10000 | 两个 Radar 区块的外层时限 |
DEADLINES.connectivity | number,毫秒 | 5000 | 互联区块外层时限 |
DEADLINES.whois | number,毫秒 | 14000 | 覆盖 bootstrap 与 autnum 的区块预算 |
DEADLINES.rank / reputation | number,毫秒 | 各 5000 | 两个区块的外层时限 |
DEADLINES.peeringdb | number,毫秒 | 1000 | 本地索引区块时限 |
composeAsnProfile 的 deadlines | object | DEADLINES | 可注入替代预算 |
MAX_DEPTH | number | 3 | 互联 BFS 处理层数 |
MAX_INTERMEDIATE_BRANCH | number | 3 | 每个节点展开的非 Tier 1 provider 上限 |
NEIGHBOUR_LIST_CAP | number | 50 | 每类邻居列表上限,不影响计数 |
MIN_PREFIX | object | { v4: 8, v6: 19 } | 历史前缀长度下限 |
MIN_PEERS | number | 30 | 上游查询与本地历史筛选共用门槛 |
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-profile | asn,按入口约定为规范化数字字符串 | 数值 asn、七区块、status、incomplete | 所有已启用来源均失败时 502,返回错误及状态表 |
/api/asn-connectivity | asn,处理器用 parseInt(..., 10) | origin、nodes、edges、neighbours | 捕获异常后 500,返回 error.message |
/api/asn-history | prefix,经校验的 CIDR | prefix、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。
故障、并发与运维注意事项
排障优先级
- 画像返回 200 但缺区块:先检查对应
status;disabled查来源配置,empty查数据语义,error查asn-profile: section failed日志。 - 画像数据可见却不完整:检查
incomplete,尤其是 Radar 分段失败、prefixes 部分结果;不要只看 HTTP 状态或ok。 - 冷启动互联或 PeeringDB 错误:检查离线快照就绪状态,不应通过返回空数组掩盖加载失败。
- 互联图名称缺失:名称失败本来就允许降级;该路径没有历史接口的逐项回退警告钩子。
- 历史为空:可能确实无
by_origin,也可能所有记录被前缀下限、缺少 timeline 或 peer 门槛过滤;空数组并不等同于上游不可用。 - 历史 504:处理器将外层捕获的
AbortError识别为上游超时。名称补全内部失败通常被内层 catch 降级,不应与路由历史主查询超时混为一谈。
并发与性能
- 画像来源并发执行,等待时间主要由最慢尚未完成的区块决定,而不是七个来源耗时相加;默认最大区块预算为 14 秒。
- 互联 BFS 同步读取本地索引,分支限制和叶子清理控制图规模;名称查询只针对剪枝后节点并发执行。
- 邻居列表在截断前排序,因此输出上限 50 不代表仅处理 50 个邻居。
- 历史组织查询先去重 ASN;互联名称查询和历史批量查询均未在已读函数中设置并发池上限。
- 这些核心函数未实现请求间去重、重试、持久化写入或自己的响应缓存。底层来源是否具备这些机制,需继续核验对应模块,不能从这里推断为不存在。
- 超时后底层任务继续运行,因此外层时限限制的是响应等待,而非整个系统的上游工作量。
扩展与测试边界
增加画像来源时,需要同时维护 SECTIONS、loader、区块预算和返回状态分类,并评估前端是否认识新增字段。来源异常应进入现有隔离机制;有部分结果时保留 ok 并设置 incomplete,而不是无条件丢弃可用数据。
互联逻辑通过 rel 和 lookupName 注入支持无网络 fixture;画像通过 deps、loaders 和 deadlines 注入支持确定性测试;就绪包装器也暴露了依赖覆盖参数。这些是源码明确提供的测试入口。
本次发现了相关测试文件名称,但未读取测试正文,因此不宣称已有断言覆盖。建议重点核验:重复边 transit 优先、循环节点与叶子剪枝、50 条截断但计数完整、部分 Radar 结果、所有来源失败、超时后迟到结果、历史门槛边界及名称批次拒绝。
相关链接
- 画像入口与来源装配:继续追踪来源配置、就绪检查和请求头传递。
- 画像状态与完整性契约:扩展来源或排查降级行为时的主要参考。
- 互联图与直接邻居算法:理解图筛选与列表截断的边界。
- 历史过滤与观测语义:解释历史空结果、可见度和错误响应。