代理分流规则验证
代理分流规则验证通过浏览器访问 8 个独立测试域名,展示各目标实际观察到的出口 IP、国家和 ISP,帮助用户对照代理客户端中的域名分流配置。它是出口观测工具,不是代理规则解析器,也不会自动判定某条规则是否正确。
目的与范围
本页覆盖 RuleTest 的工具注册、测试卡片初始化、网络请求、地理信息补充、串行调度、共享 IP 更新和完成事件,以及维护这些流程时必须注意的失败与并发边界。
本页不展开代理客户端的规则语法、DNS 泄漏检测、全球延迟测试、MTR、报告存储或成就判定。它们与本工具可能共享数据,但属于独立能力。当前证据来自工具注册和完整的 RuleTest.vue 实现;parseTrace、lookupMaxmind、共享状态与事件消费者的内部实现未读取,因此下文仅描述本组件可确认的调用契约。
概述
验证的是什么
每张卡片对应一个目标,从 ptest-1.ipcheck.ing 到 ptest-8.ipcheck.ing。浏览器向目标发出 HTTPS 请求,从 /cdn-cgi/trace 的文本响应中提取 IP 和国家代码,再以 IP 查询 MaxMind 补充 ISP。
因此,用户可以在代理客户端配置这些域名走不同策略,再对照卡片的出口信息检查实际效果。这里的关键区别是:页面展示目标请求的出口,而不是读取代理客户端的规则或节点名称。组件没有预期出口输入项、规则文件上传或自动通过/失败比较逻辑。相同出口不一定意味着规则没有生效,不同规则也可能最终汇聚到同一出口。
工具入口
工具注册项使用 ruletest 作为稳定标识,懒加载 RuleTest.vue,标题和说明分别取自 ruletest.Title、advancedtools.RuleTestNote。该项没有设置 requiresOriginalSite 或 requiresConfig。注册表约定同时服务于高级工具列表、抽屉与独立工具页面,并描述了 ?tool= 和 /tools/:slug 的入口形式。
{ slug: 'ruletest', emoji: '🚏', titleKey: 'ruletest.Title', noteKey: 'advancedtools.RuleTestNote', component: () => import('@/components/advanced-tools/RuleTest.vue') },Source: tools.js
这里没有进一步核验路由实现;入口说明依据注册表自身的契约,而不是推测额外的路由参数。
架构
Sources:
核心执行逻辑集中在 Vue 组件中。trace 请求直接由浏览器发往目标域名,这使观测对象保持为用户浏览器到测试目标的路径。MaxMind 只补充组织信息:组件明确保留 trace 提供的国家,不使用 MaxMind 的结果覆盖它。
ruleTests 负责当前界面,IPArray 负责向共享状态提交发现的 IP,ruletest:finished 则通知其他模块本轮结束。这三条数据通道语义不同,不应互相替代。
初始化与结果展示
卡片数据模型
createDefaultCard() 生成名称、等待状态和空国家代码;ruleTests 为每张卡片添加显示用编号与域名。
1const ruleTests = ref(Array.from({ length: 8 }, (_, index) => ({
2 id: index + 1,
3 url: `ptest-${index + 1}.ipcheck.ing`,
4 ...createDefaultCard(),
5})));Source: RuleTest.vue
| 字段 | 当前组件中的含义 | 生命周期 |
|---|---|---|
id | 卡片编号,1–8 | 初始化后不变 |
url | 不含协议的目标主机名 | 初始化后不变 |
name | ruletest.Name 的翻译结果 | 初始化时生成 |
ip | 等待/测试/错误文案,或 trace 提供的 IP | 每轮先重置,再更新 |
country_code | trace 的 loc 值 | 每轮先清空 |
country | 国家名称,或状态文案 | 成功时按当前语言转换 |
org | MaxMind 结果的 org,或状态文案 | 每轮重置,查询后更新 |
组件以字符串混合存放状态和业务值,而不是额外维护枚举状态。toneOf(test) 将等待和测试文案交给 ipFieldTone,错误文案也单独传入;isFieldPending 使用相同类别的文案处理 ISP 和国家字段。共享辅助函数的具体映射规则不在本页证据范围内。
UI 与交互
卡片网格在不同断点使用 1、2、4 列。每张卡展示目标域名、编号、IP 状态灯、ISP 与国家;国家代码存在时生成 circle-flags: 图标。FitText 使用 INLINE_TIERS 展示 IP。
底部刷新按钮以 !finishAll 禁用,并在未完成时显示 Spinner。移动端宽度由 store.isMobile 决定。注意,开始一轮测试时所有卡片都立即进入“测试中”,后面的卡片此时可能只是排队,并不代表 8 个网络请求同时执行。
核心执行流程
1. 自动开始与刷新
挂载后通过 setTimeout 延迟 300 毫秒调用 checkAllRuleTest();用户也可以在上一轮结束后点击刷新按钮调用同一函数。函数先设置 finishAll = false,将全部 IP、国家、ISP 设为测试状态,并清空国旗代码。
2. 获取目标出口
fetchTrace(id, url) 中的 id 实际是数组下标,而不是卡片上显示的 1–8 编号。请求成功返回文本后,组件将文本交给 parseTrace,并把 loc 别名为 country,默认空字符串。
1 const response = await fetch(`https://${url}/cdn-cgi/trace`);
2 const data = await response.text();
3 const { ip, loc: country = '' } = parseTrace(data);
4 if (ip) {
5 ruleTests.value[id].ip = ip;
6 IPArray.value = [...IPArray.value, { ip, country }];
7 }
8 if (country) {
9 ruleTests.value[id].country_code = country;
10 ruleTests.value[id].country = getCountryName(country, lang.value);
11 }Source: RuleTest.vue
组件只检查 ip 和 country 是否为真值;是否做 IP 格式校验取决于未读取的 parseTrace,不能据此宣称组件已严格验证响应。
3. 补充 ISP,不改写国家来源
IP 存在时才调用 lookupMaxmind(ip)。如果返回真值 geo,取其 org 展示;如果返回假值,则仅把 org 标记为错误。成功补充时,再向 IPArray 追加包含 location、asn、org 的对象。
这种先记录 IP、再补充信息的做法使共享状态不必等待 ISP 查询才知道出口,但也意味着同一目标可能追加两条记录。组件没有在这里合并或去重。
4. 串行推进并通知完成
Source: RuleTest.vue
一个目标的 trace 与 MaxMind 查询结束后才启动下一个目标。因此,本轮时间包含各目标的查询与补充耗时,而不是只取最慢的 trace 请求。某个目标抛出异常后,外层 finally 仍推进下一个目标。
异步完成语义需要特别注意:checkAllRuleTest 虽然声明为 async,却没有 await 或返回 processTest(0);递归推进也没有 await processTest(index + 1)。因此等待 checkAllRuleTest() 返回,并不等于等待所有测试完成。本组件使用 finishAll 和最终事件表示完成。
共享数据与完成事件
IPArray 的两阶段提交
首次解析出 IP 时追加 { ip, country };MaxMind 返回信息时追加增强记录。以下为原始补充逻辑:
1 const geo = await lookupMaxmind(ip);
2 ruleTests.value[id].org = geo ? geo.org : t('ruletest.StatusError');
3 if (geo) {
4 // Back-fill details for the Globalping picker + IP history.
5 IPArray.value = [...IPArray.value, {
6 ip,
7 country,
8 location: ruleTests.value[id].country || geo.country,
9 asn: geo.asn,
10 org: geo.org,
11 }];
12 }Source: RuleTest.vue
深度监听 IPArray 后,组件调用 store.updateAllIPs(IPArray.value),传递的是整个累积数组,而非本次增量。刷新并不清空 IPArray;当前组件实例内多轮运行会继续积累。共享 store 是否去重、如何记录历史、是否写入本地或远端存储,不能由本组件推断。
另一个细节是 location 使用当前卡片的 country || geo.country:如果 trace 缺失国家,卡片可能仍为非空的测试状态文案,该文案会优先于 geo.country。扩展这段逻辑时应区分“可显示的国家名称”和“非空字符串”。
ruletest:finished 载荷
最后一个数组下标完成后,组件先恢复 finishAll,再发送事件:
1 emitAppEvent('ruletest:finished', {
2 uniqueIPCount: new Set(ruleTests.value.map((test) => test.ip)).size,
3 workers: ruleTests.value.map((test) => ({
4 id: test.id,
5 ip: test.ip,
6 country_code: test.country_code,
7 org: test.org,
8 })),
9 });Source: RuleTest.vue
| 载荷字段 | 含义 | 使用限制 |
|---|---|---|
uniqueIPCount | 对所有卡片 ip 字符串做 Set 后的数量 | 没有过滤错误或测试文案,不等同于有效 IP 数 |
workers | 当前全部卡片的精简快照 | 包含失败卡片,不代表全成功 |
workers[].id | 1–8 的显示编号 | 不是 fetchTrace 的数组下标参数 |
workers[].ip | 当前 IP 字段 | 可能是状态文案 |
workers[].country_code | trace 国家代码 | 失败或缺失时可为空 |
workers[].org | 当前 ISP 字段 | 可能是状态文案 |
例如全部卡片变成相同错误文案时,uniqueIPCount 会是 1,而不是 0。源码注释说明报告构建器会重新从有效 IP 计算数量,但消费者的具体实现未在本次查阅中验证;事件使用者应自行确认有效性规则。
配置与内部接口
配置选项
本组件未声明供用户输入的目标、超时或并发配置,也未读取对应环境变量。下列是源码中的固定值与运行时状态,不是可直接设置的部署参数。
| 项目 | 类型 | 默认/固定值 | 行为 |
|---|---|---|---|
| 目标数量 | number | 8 | 初始化卡片数量 |
| 目标模板 | string | ptest-${index + 1}.ipcheck.ing | 编号从 1 开始 |
| 请求协议与路径 | string | HTTPS、/cdn-cgi/trace | 浏览器直接请求 |
| 挂载延迟 | number | 300 毫秒 | 仅用于自动启动 |
testCount | Vue ref,内部值为 number | 初始化时的卡片数 | 决定递归终止条件 |
finishAll | Vue ref,内部值为 boolean | false | 禁用刷新按钮与控制加载图标 |
lang | computed | store.lang | 转换 trace 国家代码 |
isMobile | computed | store.isMobile | 控制刷新按钮宽度 |
依据:RuleTest.vue、RuleTest.vue、RuleTest.vue。
函数参考
这些函数定义于 <script setup>,是组件内部函数,不是已公开的 HTTP API 或对外导出接口。源文件为 JavaScript,没有静态类型标注。
| 源码签名 | 参数 | 返回与副作用 |
|---|---|---|
const createDefaultCard = () => ({ ... }) | 无 | 返回包含翻译后状态文案的默认卡片对象 |
const toneOf = (test) => ipFieldTone(...) | 卡片对象 | 返回共享色调函数的结果;其值域由该辅助函数定义 |
const isFieldPending = (value) => isFieldPendingShared(...) | 待展示字段 | 返回共享状态检查结果,供模板决定占位显示 |
const fetchTrace = async (id, url) => { ... } | id 为零基数组下标,url 为主机名 | 无显式结果值;异步更新卡片与 IPArray,捕获请求/处理异常 |
const checkAllRuleTest = async () => { ... } | 无 | 重置卡片并启动链式任务;其 Promise 不代表整轮完成 |
const processTest = async (index) => { ... } | 零基数组下标 | checkAllRuleTest 内部函数;等待一个目标后推进下一个,并在最后发事件 |
fetchTrace 的正常调用路径会将异常转换为卡片错误,而不是向调用方提供结构化错误结果。外层 processTest 另有 catch 输出错误日志,并在 finally 继续调度。
依据:RuleTest.vue。
失败模式、边界与并发
| 场景 | 已验证的行为 | 排查与解读 |
|---|---|---|
| fetch、读取文本或解析抛错 | catch 将 IP、国家、ISP 全部改为错误,清空国家代码并 console.error | 页面不细分 DNS、TLS、CORS、网络等具体原因,应检查浏览器网络与控制台 |
| MaxMind 返回假值 | 保留已获取的 IP 和国家,仅 ISP 错误 | 不能把 ISP 查询失败直接当作分流失败 |
| MaxMind 抛错 | 与 trace 共用 catch,整张卡变为错误 | 此时 IP 可能已加入 IPArray,界面和共享状态可能不同步表达“成功” |
| 响应缺少 IP | 不查询 MaxMind,IP 可能保留测试文案 | 本轮仍能推进并最终完成,完成不意味着字段全部有效 |
| 响应缺少国家 | 不更新国家字段 | 国家可能保留测试文案,即使 IP 已成功 |
| HTTP 非成功状态 | 组件没有检查 response.ok 或状态码,仍读取文本并解析 | 不能认为 HTTP 错误一定进入 catch |
| 请求一直不结束 | 没有显式超时或 AbortController | 串行链可能停在当前目标,刷新按钮持续禁用 |
| 重复刷新 | UI 在 finishAll = false 时禁用按钮 | 函数本身没有重入锁,不应把 UI 禁用当作完整并发防护 |
| 离开页面 | 未见取消 300 毫秒定时器、终止请求或使旧结果失效的逻辑 | 后续扩展需要考虑组件卸载后的任务生命周期 |
上述边界直接来自 RuleTest.vue。
状态语义
finishAll 的含义是最后一个目标处理结束,而不是“所有目标成功”。全部卡片开始时统一置为测试中,最终不一定全部离开该文案:没有 IP 且未抛错的响应就是一种情况。
界面状态文案由 t() 写入字段,色调判断则在调用时再次取当前语言的文案。组件没有监听语言变化后重新翻译已有卡片字段;运行期间切换语言时,维护者应检查已有文案与当前状态判断的一致性。
性能、运维与扩展
性能特征
- 一轮最多发出 8 次 trace 请求;每个有 IP 的目标额外调用一次
lookupMaxmind。后者是否实际发起网络请求、是否命中缓存,在本次源码范围内无法确认。 - trace 与 ISP 补充都位于串行链中,因此 ISP 查询慢会延后下一张卡片的测试。
- 完整成功的一轮最多向
IPArray追加 16 条记录,重复刷新会继续增长;每次追加采用数组展开复制,并由 watcher 将累积数组提交给 store。 - 组件没有自动重试。刷新按钮启动的是一整轮,而不是仅重试失败目标。
运维排查顺序
- 确认浏览器实际请求了预期的
ptest-N.ipcheck.ing域名,而不是只检查应用服务器连通性。 - 检查 trace 响应中是否包含可解析的 IP 和国家字段。
- 区分 trace 失败与 MaxMind 补充失败;后者抛异常也会覆盖已经显示的 IP。
- 对照代理客户端策略核对出口,避免将
uniqueIPCount当作自动正确性判据。 - 若整轮停滞,先查当前请求是否长期未结束,再查后续任务;该实现没有单目标超时兜底。
安全扩展建议
以下是基于当前实现的维护建议,不是已经存在的功能:
- **增加目标:**同时核对初始化数量、域名模板和
testCount。后者只在初始化时读取长度,不会自动随动态列表变化。 - **增加超时与卸载清理:**让卡住的目标可明确结束,且避免旧实例继续写回数据。
- **提供可等待的整轮操作:**若其他调用方需要
await全部结果,应明确返回完整任务 Promise,不能沿用当前的启动即返回语义。 - **分离状态与值:**采用明确状态字段可避免翻译文案进入唯一 IP 计数,也便于处理缺字段和语言切换。
- **修改事件字段:**先核对报告与成就消费者,再更改事件契约。前端规范明确使用领域事件连接这些能力,而非让组件直接修改成就状态。
事件集成约定见 AGENTS.md。
测试证据与限制
本次有限源码查阅没有读取此功能的测试文件,也没有执行网络或自动化测试,因此不能宣称上述边界已受测试覆盖。仓库约定对可脱离网络执行的逻辑编写 Node 测试,UI 渲染和真实浏览器网络行为需要另行验证,见 AGENTS.md。
建议重点验证正常双阶段更新、缺 IP/国家、MaxMind 假值与异常、整轮完成时机、状态文案进入计数、重复刷新积累,以及组件卸载时未完成请求的行为。
相关链接
- 高级工具注册与入口约定:用于定位本工具及 DNS、延迟、MTR 等相邻能力,避免混淆测试用途。
- 代理分流请求与调度实现:排查出口结果、任务完成时机与错误处理。
- 前端事件、报告与成就集成约定:继续了解结果消费者的职责边界。