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。
架构与职责边界
下图仅展示从服务端实现直接确认的调用关系。前端执行按钮与该接口之间的具体请求构造尚未核实,因此不把未读脚本中的调用画成确定关系。
Source: dns-leak-test.js
这种薄代理分工保留了上游对认证、额度及会话结果的处理权。本地在网络请求之前拒绝无效 token,并从服务端环境加入 API key;上游 HTTP 失败状态不会自动被代理改写成统一的 502,只有代理捕获到异常时才进入该分支。
增强检测页面的可见生命周期
1. 登录与执行入口
执行按钮同时受 isBusy 与 store.user 控制:忙碌时或未登录时禁用,点击时调用 runTest;忙碌时图标替换为 Spinner。未登录用户还会看到 user.SignInToUse 提示。
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 泄露”。
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
会话结果查询的真实控制流
Source: dns-leak-test.js
步骤一:先校验,再发起网络请求
getSessionResult 首先要求 req.method === 'GET',否则返回 405。随后读取 req.params?.token,用 /^[0-9a-f]{32}$/ 校验,只接受 32 位小写十六进制 token。
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 缺失。
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。
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.ecsInfo | ECS 子网与地理信息 | 有 ECS 才显示子网明细;详情交给 ecsDetail(row) |
row.do、row.cd | DNSSEC 相关标志 | 按真值判断显示图标,不在模板中验证类型 |
dnssecSummary | DNSSEC 摘要文本与颜色 | 摘要算法未读取 |
依据: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。
| 参数 | 所在位置 | 要求 | 行为 |
|---|---|---|---|
token | req.params.token | 必填,32 位小写十六进制 | 格式无效时不访问上游 |
lang | req.query.lang | 可选,非空字符串 | 按原字符串值交由上游处理 |
| 请求头 | req.headers | 由调用请求提供 | 复制后传给 fetchUpstream |
| 响应状态 | 产生条件 | 响应内容 |
|---|---|---|
| 405 | 直接调用处理器时方法不是 GET | error: 'Method Not Allowed' |
| 400 | token 缺失或不符合正则 | error: 'Invalid token' |
| 500 | key 或 endpoint 缺失 | error: 'API key is missing' |
| 上游状态码 | 成功获得并处理上游响应 | 上游 JSON;解析失败为 {} |
| 502 | try 范围内异常进入 catch | error: 'Upstream fetch failed'、detail: error.message |
异常边界: new URL(...) 在 try 外执行。非空但无效的 endpoint 可能导致 URL 构造抛错,该错误不会被这里的 502 分支捕获;最终 HTTP 处理行为需查看应用级错误处理,本次未核实。处理器内部的 405 也不保证所有非 GET 的网络请求都会进入此函数,具体取决于路由匹配。
依据:dns-leak-test.js。
失败模式、边界与并发
优先区分四类失败
- 本地输入失败: 检查 token 长度、大小写与字符范围,不必先排查 DNS 采集链路。
- 配置失败: 500 的固定文案覆盖两个环境变量,排查时两者都要检查;格式错误的 endpoint 与变量缺失是不同路径。
- 上游 HTTP 拒绝: 有合法上游响应时保留原状态码。不能仅凭非 2xx 就认为请求转发抛出了异常,也不能臆测每个状态与前端额度提示的映射。
- 转发异常: 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 标志组合的展示。
- 后续读取脚本后,再验证去重、排序、取消、卸载清理和多轮运行结果隔离。
相关链接
- 普通检测入口:DnsLeaksTest.vue。本次仅定位,未读取内部实现;普通检测的探测器编排应在对应主题中展开。
- 增强检测交互与明细:EnhancedDnsLeakTest.vue。
- 会话结果接口契约:dns-leak-test.js。
以上链接用于进一步定位具体职责,不意味着未读实现已经在本页得到验证。