Repository Wiki
jason5ng32/MyIP

DNS 泄露检测与增强检测流程

DNS 泄露检测用于观察 DNS 解析相关信息;本页重点介绍已核实的增强检测结果查询接口、登录入口、查询明细展示与错误处理。增强检测的服务端实现是面向 IPCheck.ing API 的薄代理,而不是在本地执行 DNS 抓包或解析日志分析。

目的与范围

本页覆盖以下已验证部分:

  • 增强检测页面的登录限制、执行按钮、进度、额度提示及结果表格。
  • GET /api/dnsleaktest/session/:token 的本地校验、环境配置、请求转发与响应处理。
  • 解析器 IP、ECS、DNSSEC 标志等展示字段及其解读边界。
  • 代理接口的失败路径、缓存行为和运维排查入口。

普通 DNS 检测与增强检测不应视为同一套已验证的执行算法。本次在限定源码读取范围内,未读取普通检测实现,也未读取增强组件的脚本部分,因此不描述会话 token 生成、探测域名构造、探测轮数、轮询间隔、取消逻辑或具体去重算法。身份认证、通用刷新编排、IP 地理信息查询与报告导出属于相关能力,本页只说明它们在已读代码中显露的边界,不扩展其内部实现。

概述

增强检测页面以一次检测的会话结果为展示中心:用户登录后可点击执行,运行中展示阶段和百分比;获得结果后展示解析器数量、原始记录数量、DNSSEC 摘要以及查询明细。页面还区分普通错误、月度额度耗尽和无查询记录三种反馈。

服务端的职责更窄:验证会话 token 和环境配置,调用上游的同名会话结果接口,再把上游状态码与 JSON 数据返回给调用方。它不对查询结果做本地归并,也不重新计算解析器数量或安全结论。

前端注释说明登录限制与 NS 采集侧按会话保存日志有关;但采集服务、日志存储及保留周期的实现不在本次已读源码中,不能据此推断本地存在 DNS 日志数据库。

依据:EnhancedDnsLeakTest.vue、dns-leak-test.js。

架构与职责边界

下图仅展示从服务端实现直接确认的调用关系。前端执行按钮与该接口之间的具体请求构造尚未核实,因此不把未读脚本中的调用画成确定关系。

Loading diagram...

Source: dns-leak-test.js

这种薄代理分工保留了上游对认证、额度及会话结果的处理权。本地在网络请求之前拒绝无效 token,并从服务端环境加入 API key;上游 HTTP 失败状态不会自动被代理改写成统一的 502,只有代理捕获到异常时才进入该分支。

增强检测页面的可见生命周期

1. 登录与执行入口

执行按钮同时受 isBusy 与 store.user 控制:忙碌时或未登录时禁用,点击时调用 runTest;忙碌时图标替换为 Spinner。未登录用户还会看到 user.SignInToUse 提示。

vue
1 <Button variant="action" :disabled="isBusy || !store.user" @click="runTest" class="cursor-pointer"> 2 <Spinner v-if="isBusy" /> 3 <template v-else> 4 <Play class="size-4 shrink-0" /> 5 </template> 6 <span class="ml-1.5">{{ runLabel }}</span> 7 </Button>

Source: EnhancedDnsLeakTest.vue

这是 UI 入口限制,不等同于服务端认证实现。代理函数自身没有验证用户身份或 Firebase token 的代码,只转发请求头;具体身份判定依赖上游或本次未核实的其他请求处理层。

2. 错误、额度与运行进度

  • errorMsg 非空时显示破坏性颜色的错误文本。
  • quotaExceeded 为真时显示月度额度耗尽提示,并提供调用 openUsageDialog 的按钮。源码注释明确把这种情况作为独立提示,而不是普通错误。
  • showFlow 控制进度卡片显示,stageText 展示阶段,progressValue 同时用于百分比和进度条。
  • isBusy 控制运行中的动画;它为假时,进度状态点使用成功色。因此颜色本身不能代替完整结果判断。

这些条件来自模板,不足以确定状态变量何时初始化、进度如何计算、额度由哪个 HTTP 状态映射而来。依据:EnhancedDnsLeakTest.vue。

3. 结果与空状态

hasResult 决定是否展示三个摘要项:result.resolverCount、result.rawCount 和 dnssecSummary.text。DNSSEC 摘要依据 dnssecSummary.tone 分别应用成功色、警告色或弱化文本色。

有结果且 result.queries.length === 0 时显示空状态;有结果且数组非空时显示结果表格。这意味着“查询成功但没有捕获记录”是独立的 UI 状态,不能写成“确认没有 DNS 泄露”。

vue
1 <Card v-if="hasResult && result.queries.length === 0"> 2 <CardContent class="p-6 text-center text-sm text-muted-foreground"> 3 {{ t('enhanceddnsleaktest.Empty') }} 4 </CardContent> 5 </Card>

Source: EnhancedDnsLeakTest.vue

会话结果查询的真实控制流

Loading diagram...

Source: dns-leak-test.js

步骤一:先校验,再发起网络请求

getSessionResult 首先要求 req.method === 'GET',否则返回 405。随后读取 req.params?.token,用 /^[0-9a-f]{32}$/ 校验,只接受 32 位小写十六进制 token。

javascript
1 const token = req.params?.token; 2 if (!token || !TOKEN_RE.test(token)) { 3 return res.status(400).json({ error: 'Invalid token' }); 4 }

Source: dns-leak-test.js

缺失 token、长度错误、包含非十六进制字符或大写字母都会失败。该校验只检查格式,不验证会话存在性、归属或有效期;这些行为不能从正则推导出来。

步骤二:读取服务端配置并构造上游 URL

IPCHECKING_API_KEY 或 IPCHECKING_API_ENDPOINT 任意一个缺失时,返回 500,错误消息统一为 API key is missing。这条消息并不意味着一定只有 key 缺失。

javascript
1 const url = new URL(`${apiEndpoint}/dnsleaktest/session/${token}`); 2 url.searchParams.set('apikey', apiKey); 3 // The upstream resolves any tag onto the languages it has, so the caller's 4 // value rides along untouched; omitting it lets the upstream default apply. 5 if (typeof req.query.lang === 'string' && req.query.lang) { 6 url.searchParams.set('lang', req.query.lang); 7 }

Source: dns-leak-test.js

lang 仅在非空字符串时加入 URL;本地不做语言标签转换,缺省语言由上游决定。函数仅显式追加 apikey 和 lang,并非透传全部查询参数。

步骤三:转发请求头并保留响应语义

调用 fetchUpstream(url, { headers: { ...req.headers } }) 时复制全部请求头。文件注释特别指出 Authorization: Bearer <Firebase ID> 的转发用途,但这里没有实际解析 Bearer token。

上游响应体解析失败时回退为 {},仍保留上游状态码;只有发生其他被捕获异常时才记录错误并返回 502。

javascript
1 const data = await apiResponse.json().catch(() => ({})); 2 3 res.set('Cache-Control', 'no-store'); 4 res.status(apiResponse.status).json(data); 5 } catch (error) { 6 logger.error({ err: error }, 'dnsleaktest upstream fetch failed'); 7 res.status(502).json({ error: 'Upstream fetch failed', detail: error.message }); 8 }

Source: dns-leak-test.js

“透传”在这里指状态码与解析后的 JSON 值,不是原始字节或全部响应头透传;上游返回非 JSON 时,原始响应体会丢失。

结果数据与展示规则

下表描述模板实际读取的字段,不是经完整验证的上游响应 Schema。当前代理没有进行结果结构校验。

字段或派生值页面用途已验证的边界
result.resolverCount解析器数量摘要计算方式未读取
result.rawCount原始记录数量摘要与去重后行数的具体关系未核实
result.queries判断空结果或显示明细模板直接访问 .length
visibleRows实际渲染的明细行集合模板注释说明排序和可选去重,但算法未读取
row.ip解析器 IP缺失时显示 —
row.ipInfo国家旗帜、国家、地区、城市及 ASN国家码或 ASN 为 N/A 时不展示相应正常内容
row.queryType查询类型徽章缺失时显示 —
row.transport传输方式徽章展示前调用 .toUpperCase()
row.ecs、row.ecsInfoECS 子网与地理信息有 ECS 才显示子网明细;详情交给 ecsDetail(row)
row.do、row.cdDNSSEC 相关标志按真值判断显示图标,不在模板中验证类型
dnssecSummaryDNSSEC 摘要文本与颜色摘要算法未读取

依据:EnhancedDnsLeakTest.vue、EnhancedDnsLeakTest.vue。

排序与去重的展示约定

结果表格注释注明排序顺序为 ECS 优先、国家、IP 数值顺序,并说明可以按“解析器 IP、ECS”组合去重。开关通过 dedupe 绑定,表格循环读取 visibleRows。这是已读模板表达的约定,不能据此补写 IPv6 排序方式、空值优先级或重复项保留策略。

结果中的 ECS 子网与解析器 IP 是两个不同的展示维度;不要把解析器地理位置直接当作 ECS 所表示的位置。模板为两者分别提供 ipInfo 与 ecsInfo。依据:EnhancedDnsLeakTest.vue、EnhancedDnsLeakTest.vue。

DNSSEC 图标不是完整安全裁决

模板对 row.do 为真显示绿色勾,为假显示黄色叉;对 row.cd 则取反,未置位显示绿色勾,置位显示黄色叉。源码注释明确说明不使用红色,因为单独一个标志状态不必然危险。

因此应把这些值理解为观测与提示,不要把单个图标升级为“连接绝对安全”“已发生泄露”或“端到端 DNSSEC 验证一定成功”的结论。依据:EnhancedDnsLeakTest.vue。

配置选项

配置或输入类型默认值作用与校验
IPCHECKING_API_KEY环境变量字符串未提供默认值必需,以 apikey 查询参数加入上游 URL
IPCHECKING_API_ENDPOINT环境变量字符串未提供默认值必需,用于拼接上游 /dnsleaktest/session/:token
lang请求查询字符串本地不设置,使用上游默认行为仅非空字符串被转发,不做语言映射
Authorization请求头本地不生成作为 req.headers 的一部分转发;注释说明其 Bearer 身份令牌用途
dedupe页面开关状态本次未核实控制可选去重展示,不是已证实的服务端配置

代理没有显式传入超时或重试选项。fetchUpstream 的默认超时、重试机制以及底层请求细节本次未读取,不能把“没有显式配置”写成“没有超时”。配置依据:dns-leak-test.js。

API 参考

GET /api/dnsleaktest/session/:token

获取给定会话的增强 DNS 检测结果。该路径由处理器文件注释声明,路由注册检索也命中了同一路径。

实现签名: export async function getSessionResult(req, res)。这是无 TypeScript 类型声明的异步 JavaScript 处理器,通过 res 写出响应;调用方不应将其返回值当作结果 DTO。

参数所在位置要求行为
tokenreq.params.token必填,32 位小写十六进制格式无效时不访问上游
langreq.query.lang可选,非空字符串按原字符串值交由上游处理
请求头req.headers由调用请求提供复制后传给 fetchUpstream
响应状态产生条件响应内容
405直接调用处理器时方法不是 GETerror: 'Method Not Allowed'
400token 缺失或不符合正则error: 'Invalid token'
500key 或 endpoint 缺失error: 'API key is missing'
上游状态码成功获得并处理上游响应上游 JSON;解析失败为 {}
502try 范围内异常进入 catcherror: 'Upstream fetch failed'、detail: error.message

异常边界: new URL(...) 在 try 外执行。非空但无效的 endpoint 可能导致 URL 构造抛错,该错误不会被这里的 502 分支捕获;最终 HTTP 处理行为需查看应用级错误处理,本次未核实。处理器内部的 405 也不保证所有非 GET 的网络请求都会进入此函数,具体取决于路由匹配。

依据:dns-leak-test.js。

失败模式、边界与并发

优先区分四类失败

  1. 本地输入失败: 检查 token 长度、大小写与字符范围,不必先排查 DNS 采集链路。
  2. 配置失败: 500 的固定文案覆盖两个环境变量,排查时两者都要检查;格式错误的 endpoint 与变量缺失是不同路径。
  3. 上游 HTTP 拒绝: 有合法上游响应时保留原状态码。不能仅凭非 2xx 就认为请求转发抛出了异常,也不能臆测每个状态与前端额度提示的映射。
  4. 转发异常: 502 对应带有 dnsleaktest upstream fetch failed 消息的日志,并向客户端返回异常消息作为 detail。

成功状态与数据完整性应分开检查

非 JSON 上游响应会变成 {},但保留原状态码,因此可能出现“HTTP 成功但缺少业务字段”。代理没有结果 Schema 校验,而模板依赖 result.queries 等字段。前端是否对空对象进行归一化,本次未读取,不能假定页面一定能安全处理所有上游返回。

并发限制的证据边界

前端按钮在 isBusy 时禁用,可减少 UI 重复点击;但未读取 runTest,无法确认它是否也有重入检查、请求取消或旧结果隔离。

代理函数没有可变的模块级会话状态:每次请求分别读取配置、构造 URL 并获取结果。函数内没有本地锁、共享 Promise、限流或请求合并逻辑;这不代表系统其他层不存在限制。多次请求不会在此处理器内自动合并。

依据:dns-leak-test.js、EnhancedDnsLeakTest.vue。

性能、安全与运维说明

  • 缓存: 获得上游响应后设置 Cache-Control: no-store。本地校验失败与 502 分支没有在该函数内设置此头,不能笼统说所有响应都设置了禁用缓存。
  • 请求成本: 每次进入正常转发路径都会调用一次 fetchUpstream。处理器没有本地结果缓存;底层函数是否重试未核实。
  • 敏感信息: API key 位于上游 URL 查询参数中,身份信息可能位于被复制的请求头中。运维上应注意 URL、请求头及异常采集的脱敏;这是基于数据流的建议,不是本次已验证的日志脱敏保证。
  • 错误披露: 502 将 error.message 发给客户端,排障有帮助,但需审查底层错误是否可能包含不宜公开的信息。
  • 持久化: 此代理没有数据库写入或会话日志落盘操作。前端注释提到的 NS 会话日志属于采集侧背景信息,存储结构、生命周期和删除策略未找到实现证据。
  • 结果解释: 无查询记录、缺少地理信息和单个 DNSSEC 警告都不能直接替代泄露判定。应保留页面当前对空状态、缺失字段和警告颜色的区分。

扩展与验证建议

扩展时应保持薄代理与展示层的职责边界:语言标签交由上游处理;若增加新的查询字段,应同时定义缺失值展示与结构校验;若更改响应语义,不应无意抹去上游身份或额度相关状态。将 URL 构造纳入受控异常处理、收窄请求头转发范围、审查错误详情披露,都是可评估的改进方向,并非当前已实现的功能。

本次未读取 DNS 检测单元测试内容,也未运行测试,因此不声明已有覆盖或测试通过。建议优先验证以下边界:

  • 非 GET、缺失 token、大写 token、长度错误 token 均不触发上游请求。
  • 任一环境变量缺失,以及 endpoint 非空但格式无效时的处理差异。
  • 空、重复或非字符串 lang 是否按预期忽略,合法语言字符串是否保留。
  • 上游非 2xx JSON、非 JSON 响应及网络异常是否分别保留正确语义。
  • 前端未登录、忙碌、额度耗尽、空结果、缺失 IP 信息和 DNSSEC 标志组合的展示。
  • 后续读取脚本后,再验证去重、排序、取消、卸载清理和多轮运行结果隔离。

相关链接

以上链接用于进一步定位具体职责,不意味着未读实现已经在本页得到验证。

Sources

(2 files)
frontend/components/advanced-tools