Repository Wiki
jason5ng32/MyIP

代理分流规则验证

代理分流规则验证通过浏览器访问 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 的入口形式。

javascript
{ slug: 'ruletest', emoji: '🚏', titleKey: 'ruletest.Title', noteKey: 'advancedtools.RuleTestNote', component: () => import('@/components/advanced-tools/RuleTest.vue') },

Source: tools.js

这里没有进一步核验路由实现;入口说明依据注册表自身的契约,而不是推测额外的路由参数。

架构

Loading diagram...

Sources:

核心执行逻辑集中在 Vue 组件中。trace 请求直接由浏览器发往目标域名,这使观测对象保持为用户浏览器到测试目标的路径。MaxMind 只补充组织信息:组件明确保留 trace 提供的国家,不使用 MaxMind 的结果覆盖它。

ruleTests 负责当前界面,IPArray 负责向共享状态提交发现的 IP,ruletest:finished 则通知其他模块本轮结束。这三条数据通道语义不同,不应互相替代。

初始化与结果展示

卡片数据模型

createDefaultCard() 生成名称、等待状态和空国家代码;ruleTests 为每张卡片添加显示用编号与域名。

javascript
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不含协议的目标主机名初始化后不变
nameruletest.Name 的翻译结果初始化时生成
ip等待/测试/错误文案,或 trace 提供的 IP每轮先重置,再更新
country_codetrace 的 loc 值每轮先清空
country国家名称,或状态文案成功时按当前语言转换
orgMaxMind 结果的 org,或状态文案每轮重置,查询后更新

组件以字符串混合存放状态和业务值,而不是额外维护枚举状态。toneOf(test) 将等待和测试文案交给 ipFieldTone,错误文案也单独传入;isFieldPending 使用相同类别的文案处理 ISP 和国家字段。共享辅助函数的具体映射规则不在本页证据范围内。

UI 与交互

卡片网格在不同断点使用 1、2、4 列。每张卡展示目标域名、编号、IP 状态灯、ISP 与国家;国家代码存在时生成 circle-flags: 图标。FitText 使用 INLINE_TIERS 展示 IP。

底部刷新按钮以 !finishAll 禁用,并在未完成时显示 Spinner。移动端宽度由 store.isMobile 决定。注意,开始一轮测试时所有卡片都立即进入“测试中”,后面的卡片此时可能只是排队,并不代表 8 个网络请求同时执行。

依据:RuleTest.vue、RuleTest.vue。

核心执行流程

1. 自动开始与刷新

挂载后通过 setTimeout 延迟 300 毫秒调用 checkAllRuleTest();用户也可以在上一轮结束后点击刷新按钮调用同一函数。函数先设置 finishAll = false,将全部 IP、国家、ISP 设为测试状态,并清空国旗代码。

2. 获取目标出口

fetchTrace(id, url) 中的 id 实际是数组下标,而不是卡片上显示的 1–8 编号。请求成功返回文本后,组件将文本交给 parseTrace,并把 loc 别名为 country,默认空字符串。

javascript
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. 串行推进并通知完成

Loading diagram...

Source: RuleTest.vue

一个目标的 trace 与 MaxMind 查询结束后才启动下一个目标。因此,本轮时间包含各目标的查询与补充耗时,而不是只取最慢的 trace 请求。某个目标抛出异常后,外层 finally 仍推进下一个目标。

异步完成语义需要特别注意:checkAllRuleTest 虽然声明为 async,却没有 await 或返回 processTest(0);递归推进也没有 await processTest(index + 1)。因此等待 checkAllRuleTest() 返回,并不等于等待所有测试完成。本组件使用 finishAll 和最终事件表示完成。

共享数据与完成事件

IPArray 的两阶段提交

首次解析出 IP 时追加 { ip, country };MaxMind 返回信息时追加增强记录。以下为原始补充逻辑:

javascript
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,再发送事件:

javascript
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[].id1–8 的显示编号不是 fetchTrace 的数组下标参数
workers[].ip当前 IP 字段可能是状态文案
workers[].country_codetrace 国家代码失败或缺失时可为空
workers[].org当前 ISP 字段可能是状态文案

例如全部卡片变成相同错误文案时,uniqueIPCount 会是 1,而不是 0。源码注释说明报告构建器会重新从有效 IP 计算数量,但消费者的具体实现未在本次查阅中验证;事件使用者应自行确认有效性规则。

配置与内部接口

配置选项

本组件未声明供用户输入的目标、超时或并发配置,也未读取对应环境变量。下列是源码中的固定值与运行时状态,不是可直接设置的部署参数。

项目类型默认/固定值行为
目标数量number8初始化卡片数量
目标模板stringptest-${index + 1}.ipcheck.ing编号从 1 开始
请求协议与路径stringHTTPS、/cdn-cgi/trace浏览器直接请求
挂载延迟number300 毫秒仅用于自动启动
testCountVue ref,内部值为 number初始化时的卡片数决定递归终止条件
finishAllVue ref,内部值为 booleanfalse禁用刷新按钮与控制加载图标
langcomputedstore.lang转换 trace 国家代码
isMobilecomputedstore.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。
  • 组件没有自动重试。刷新按钮启动的是一整轮,而不是仅重试失败目标。

运维排查顺序

  1. 确认浏览器实际请求了预期的 ptest-N.ipcheck.ing 域名,而不是只检查应用服务器连通性。
  2. 检查 trace 响应中是否包含可解析的 IP 和国家字段。
  3. 区分 trace 失败与 MaxMind 补充失败;后者抛异常也会覆盖已经显示的 IP。
  4. 对照代理客户端策略核对出口,避免将 uniqueIPCount 当作自动正确性判据。
  5. 若整轮停滞,先查当前请求是否长期未结束,再查后续任务;该实现没有单目标超时兜底。

安全扩展建议

以下是基于当前实现的维护建议,不是已经存在的功能:

  • **增加目标:**同时核对初始化数量、域名模板和 testCount。后者只在初始化时读取长度,不会自动随动态列表变化。
  • **增加超时与卸载清理:**让卡住的目标可明确结束,且避免旧实例继续写回数据。
  • **提供可等待的整轮操作:**若其他调用方需要 await 全部结果,应明确返回完整任务 Promise,不能沿用当前的启动即返回语义。
  • **分离状态与值:**采用明确状态字段可避免翻译文案进入唯一 IP 计数,也便于处理缺字段和语言切换。
  • **修改事件字段:**先核对报告与成就消费者,再更改事件契约。前端规范明确使用领域事件连接这些能力,而非让组件直接修改成就状态。

事件集成约定见 AGENTS.md。

测试证据与限制

本次有限源码查阅没有读取此功能的测试文件,也没有执行网络或自动化测试,因此不能宣称上述边界已受测试覆盖。仓库约定对可脱离网络执行的逻辑编写 Node 测试,UI 渲染和真实浏览器网络行为需要另行验证,见 AGENTS.md。

建议重点验证正常双阶段更新、缺 IP/国家、MaxMind 假值与异常、整轮完成时机、状态文案进入计数、重复刷新积累,以及组件卸载时未完成请求的行为。

相关链接

Sources

(4 files)
(root)
frontend
frontend/components/advanced-tools
frontend/data