Skip to content

About

dns-opti:Go 编写的 DNS 服务器优选工具。并发测试 UDP/DoT/DoH/DoH3,IPv4/IPv6 双栈,按地区码与过滤策略筛选,四套评分公式排名,含可视化 TUI 与本地 Web 面板。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

dns-opti · DNS 优选工具

CI Release License Go

参考来源声明

本项目参考了以下两个开源项目,代码为完全重写,未复制其源代码:

  • xxnuo/dns-benchmark —— 并发调度模型(全局 worker pool、按 DNS 服务器分片)、 结果 JSON 的字段设计、以及 Web 端「批量可视化分析」的交互思路。
  • palemoky/dnspick —— 连接复用与预热策略(DoT 长连接、DoH keep-alive)、 按域名分类的 TUI 进度面板、系统 DNS 检测与「是否建议切换」的判定思路。

本仓库不包含上述项目的任何源代码文件,仅借鉴设计思路,因此不构成对其代码的再分发。 其中 dnspick 为 MIT 许可;dns-benchmark 未声明任何许可证(GitHub 上 license 字段为空), 故此处仅作学术性引用与致谢,不对其主张任何许可关系。若你是权利人并希望调整署名方式,请开 issue。 内置的 DNS 服务器列表与域名列表为本项目自行整理。


这是什么

dns-opti 是一款用 Go 编写的 DNS 服务器优选工具。它会并发测试一批公共 DNS 服务器, 逐条记录每一次查询,汇总出成功率、平均延迟、P95 延迟与标准差,再按四套评分公式给出排名。

支持 UDP / DoT / DoH / DoH3 四种传输,IPv4 与 IPv6 双栈(含 IPv6 字面量端点), 可按地区码筛选服务器,并提供 udp+doh 组合模式把同一服务商的明文与加密传输并排对比。

它提供两种界面:

界面 说明
可视化操作界面(TUI) 双击 exe(或运行 dns-opti ui)即可打开:全屏键盘菜单,选择域名范围 / 协议 / 地址族 / 地区筛选 / 公式 / 高级选项,实时查看测试进度,结束后查看排名报告、一键打开本地 Web 页面或保存报告。无需记任何参数
可视化 CLI 命令行子命令,测试过程有实时进度面板(按协议分类的进度条、成功率、平均延迟、正在测试的服务器),结束后输出分域名组的排名报告
本地 Web 端 绑定 127.0.0.1 的临时网页(界面参考 bench.dash.2020818.xyz),用于查看结果、切换四套评分公式、按地区码 / 地址族筛选、导入/导出 JSON、查看排名与延迟图表

所有数据都在本机处理:不提供公网服务、没有账号系统、没有远程 API、不使用数据库。


可视化操作界面(双击即用)

直接双击 dns-opti.exe(或在终端运行 dns-opti ui)即可打开全屏键盘界面,不需要记任何命令行参数:

 DNS 优选工具 · 主菜单                                                    v1.0.0

  使用 ↑/↓ 选择,回车确认;按 r 直接开始测试,Esc 退出

 ❯ 开始测试
    国内 + 国外域名 · UDP, DoT, DoH · 约 51 台服务器 · 139 个域名 · 共 21267 次查询
  测试域名范围
    当前: 国内 + 国外域名(回车在 国内 / 国外 / 分组统计 / 混合统计 间切换)
  测试协议
    当前: UDP, DoT, DoH(回车在 UDP / DoT / DoH / DoH3 / UDP+DoH 间切换)
  测试地址族
    当前: IPv4/IPv6(回车在 IPv4 / IPv6 / 两者间切换;主机名会按所选地址族解析)
  地区筛选
    当前: 全部地区(回车在 全部 → 中国 → 亚太 → 欧洲 → 美洲 → CDN → 内网/未知 间切换)
  过滤策略筛选
    当前: 全部策略(回车在 全部 → 原生 → 安全 间切换;不改 DNS 记录,只筛选服务器)
  评测公式
    当前: 综合体验 — 日常网页浏览与综合场景,也是默认公式…
  是否对比系统 DNS
    当前: 开(读取本机 DNS 配置并纳入排名)
  高级选项
    超时 2s · 每域名 3 次 · 并发 36 · 预热域名 example.com
  打开 Web 界面
    在浏览器中打开可视化页面(暂无数据,可在页面上导入结果 JSON)
  检测内置 DNS 类型
    逐个探测内置 DNS 对分类测试域的应答,判断其属「安全」还是「原生」(仅手动启动)
  查看公式说明
    四套评分公式的表达式、含义与适用场景
  退出
    关闭本窗口
界面 内容
主菜单 开始测试、域名范围、测试协议、测试地址族、地区筛选、过滤策略筛选、评测公式、系统 DNS 对比、高级选项、打开 Web 界面、检测内置 DNS 类型、公式说明、退出;每项都显示当前取值
高级选项 单次超时 / 每域名查询次数 / 并发数 / 预热域名的可视化调整(←/→ 改值)
测试进行中 整体进度条 + 按协议进度条 + 成功率 / 平均延迟 / 已失败 + 当前正在测试的服务器,可随时按 Esc 中止
测试结果 分域名组的排名表与推荐(组合模式下额外给出 UDP 与 DoH 对比),可滚动查看;o 打开本地 Web 页面,s 把报告另存为 .txt,回车返回菜单
公式说明 四套公式分页浏览:表达式、含义、适用场景
出错界面 明确显示失败原因,并给出可执行的排查建议(调大超时、关闭代理等)

按键约定:↑/↓ 移动,回车 确认,←/→ 调整数值,r 从菜单直接开始测试,Esc 返回或退出。

  • 双击运行时程序会在退出前等待回车,窗口不会一闪而过。
  • 结果文件始终会导出为 JSON,路径以绝对路径显示,方便直接找到。
  • 如果标准输出不是交互式终端(管道、重定向、CI),程序会自动退回到普通命令行帮助,不会卡住。
  • 带任何参数运行(如 dns-opti test -d cn)仍然走原来的子命令路径,便于脚本调用。
  • 菜单里的协议切换包含 UDP+DoH 组合模式,选中它会同时打开对比报告; 切换协议 / 地址族 / 地区筛选 / 过滤策略都会立即重新统计待测服务器数量(「开始测试」那行的数字随之更新)。
  • 「打开 Web 界面」在主菜单里随时可用:测试完再打开会直接显示本次结果; 还没测试过也能打开,页面会显示导入按钮,可直接读取已有结果 JSON。

配置自动保存

交互界面里改动过的选项会自动写入程序所在目录下的 dns-opti.settings.json,下次打开自动读取:

{
  "domains": "mixed",
  "protocols": ["udp", "doh"],
  "combo": "udp+doh",
  "regions": ["CN", "HK"],
  "policies": ["native"],
  "ip_version": "ipv4",
  "timeout_ms": 2000,
  "attempts": 3,
  "system_dns": true,
  "formula": "comprehensive",
  "warmup_domain": "example.com"
}
  • 文件放在程序目录而不是 %APPDATA%:这是一个绿色便携工具,配置应当跟着程序走(U 盘、便携目录都能带走),也方便直接查看、编辑或删除。
  • 写入采用临时文件 + 原子重命名,中途崩溃不会留下半截文件。
  • 目录不可写时(例如装在 Program Files 下)会回退到当前工作目录;无法保存时只在状态栏提示,不会中断测试。
  • 文件损坏或字段非法时自动回落到默认值,不会让界面卡在一个跑不起来的配置上;删掉文件即可恢复出厂设置。

快速开始

# 0. 双击程序即可打开可视化操作界面;也可以在终端显式打开
dns-opti ui

# 1. 测试国内域名,UDP + DoT + DoH,结果导出到 result.json
dns-opti test --domains cn --protocols udp,dot,doh --output result.json

# 2. 打开结果页面(测试完成后自动启动 Web 端)
dns-opti test --domains all --web

# 3. 只看某个历史结果文件
dns-opti web --result result.json

# 4. 先看看会测什么,不实际发请求
dns-opti test --domains all --protocols udp,dot,doh,doh3 --dry-run

# 5. 只测 IPv6 端点(主机名端点会只查 AAAA 记录)
dns-opti test --ip-version ipv6 --protocols udp,doh

# 6. 只看国内 + 港台地区的服务器
dns-opti servers --regions cn,hk,tw

# 7. 同一服务商的明文 UDP 与加密 DoH 并排对比
dns-opti test --protocols udp+doh --domains cn --regions cn

# 8. 国内外域名合并成一个分组统计(不分组)
dns-opti test --domains mixed --protocols udp

# 9. 只测不过滤的原生解析器,或只看带广告拦截的解析器
dns-opti servers --policy native
dns-opti test --policy security --protocols udp

⚠️ 测试前请关闭系统代理与加速器(Clash / v2ray / Surge 等)。 否则 DoT / DoH / DoH3 的流量会绕经代理,测出来的延迟是代理的延迟,而不是 DNS 服务器的延迟。 本工具不会自动检测代理,请在运行前自行确认。


安装

下载预编译版本

到 Releases 下载对应平台的压缩包,解压后直接运行。 Windows 用户双击 dns-opti.exe 即可打开可视化操作界面,无需任何参数。

每个平台压缩包内附 LICENSE 与 NOTICE;官方发布的压缩包同时提供 checksums.txt 供校验完整性。

从源码构建

需要 Go 1.26 或更高版本(go.mod 声明 go 1.26.0)。

git clone https://github.com/DHA404/DNS-Finder.git dns-opti
cd dns-opti
go build -o dns-opti ./cmd/dns-opti        # Linux / macOS
go build -o dns-opti.exe .\cmd\dns-opti    # Windows (PowerShell)

⚠️ 暂不支持 go install。 go.mod 的模块名是 dns-opti 而非完整仓库路径, 因此 go install github.com/DHA404/DNS-Finder/cmd/dns-opti@latest 不会生效。 请使用上面的 git clone + go build,或直接下载预编译版本。 这是有意保留的取舍,而非疏漏。

使用 goreleaser 打包

goreleaser check
goreleaser release --snapshot --clean      # 输出到 dist/,不发布
goreleaser release --clean                 # 需要已配置 GITHUB_TOKEN,产出 draft release

优先平台为 windows/amd64,同时构建 darwin/arm64、darwin/amd64、linux/amd64。 若某平台的 DoH3(quic-go,纯 Go 无 cgo)编译失败,可用 nodoh3 构建标签去掉 QUIC 依赖:

go build -tags nodoh3 -o dns-opti ./cmd/dns-opti

nodoh3 构建会拒绝 --protocols doh3 并直接报错,而不是让每个 DoH3 服务器各失败一次:

错误: 本次构建未启用 DoH3(构建标签 nodoh3),请改用 --protocols doh 或 udp

构建产物命名、ldflags 版本注入与 .goreleaser.yml 保持一致(版本注入点为 dns-opti/internal/cli.Version)。


命令说明

dns-opti ui         打开可视化操作界面(等同于直接双击程序)
dns-opti test       测试 DNS 服务器并导出结果
dns-opti web        打开本地 Web 结果分析页面
dns-opti formulas   列出四套评分公式及其适用场景
dns-opti servers    列出内置的 DNS 服务器
dns-opti domains    列出内置的测试域名
dns-opti version    显示版本信息

dns-opti test 参数

参数 默认值 说明
-d, --domains all cn 仅国内域名 / intl 仅国外域名 / all 国内+国外(分组分别统计,不混用)/ mixed 国内外合并为一个分组统计 / 或逗号分隔的自定义域名
-p, --protocols udp,dot,doh 测试协议:udp、dot、doh、doh3;也接受组合模式 udp+doh(见下)
-s, --servers 空 手动指定服务器,覆盖内置列表。支持 1.1.1.1、2606:4700:4700::1111、[2606:4700:4700::1111]、udp://host、tls://host、https://host/dns-query、h3://host/dns-query
--server-class 全部 内置服务器范围:cn 国内 / intl 国外 / all
--regions 全部 按地区码筛选内置服务器:ISO 3166-1 两位国家码(cn、hk、tw、us…)或特殊码 cdn / private / unknown,逗号分隔
--policy 全部 按过滤策略筛选内置服务器:native 原生 / security 安全,逗号分隔(见 过滤策略)
--ip-version both 只测试指定地址族:ipv4 / ipv6 / both。主机名端点会按所选地址族解析(ipv6 时只查 AAAA)
-o, --output 自动命名 结果导出路径,默认 dns-opti_result_<时间>.json
-t, --timeout 2s 单次查询超时。公共 DNS 的正常应答在几百毫秒内,2s 已足够;调大只会让不可达的服务器拖长整轮测试
-a, --attempts 3 每个域名在每个服务器上的查询次数
-c, --concurrency CPU 线程数 × 3 同时测试的服务器数量
--system-dns true 检测系统 DNS 并纳入对比
--warmup-domain example.com 预热查询使用的域名(不计入结果)
-f, --formula comprehensive CLI 排名所用公式
--web / --no-web 关 测试完成后是否启动本地 Web 端
--json 关 以 JSON 输出结果摘要(进度信息走 stderr,便于管道处理)
--dry-run 关 只打印测试计划,不发起任何查询

IPv6 与地址族

内置清单里每个支持 IPv6 的服务商都同时提供了 IPv4 与 IPv6 端点(UDP 与 DoH 各有可直接寻址的 IPv6 字面量),--ip-version ipv6 即只测这些。

主机名端点(DoT / DoH)按所选地址族解析:指定 ipv6 时只向系统解析器查询 AAAA 记录,因此一个双栈名字不会被悄悄解析回 IPv4。

程序启动时会实际发一次查询来判定本机可用的地址族,而不是只看有没有路由:

有些网络(路由器通过 RA 下发全局 IPv6 地址、但上游没有 IPv6 出口)会让 udp6 拨号成功却每个包都被丢弃。只判断「能否建 socket」会把这种网络误判为支持 IPv6,然后为几十个 IPv6 服务器各等待一次超时。因此探测以「真实查询是否被应答」为准,并对每个地址族尝试多个目标——单个公共解析器被屏蔽是常态,只探测一个会把健康的 IPv4 主机误判为「没有 IPv4」。

判定为不可达的地址族会被自动跳过并给出提示。若你确定探测有误,用 --ip-version ipv6 显式指定会覆盖该探测(此时会如实测试并报告失败,而不是静默不测)。

几条围绕这个判定的具体行为,都做过实测:

行为 说明
同一判定作用于所有服务器 内置服务器、系统 DNS、--servers 自定义条目用同一条地址族规则。否则会出现「跳过 8 个内置 IPv6、却仍在测 IPv6 系统解析器」,白等一整个超时预算换来必然 0% 的行
判定会写进结果文件 meta.network 记录 {ipv4, ipv6, skipped, forced, label},报告里也有一行「地址族实测」。这样文件被分享或稍后重读时能自己解释「为什么没有 IPv6 服务器」,而不是只留下一句早已消失的控制台提示
探测只在必要时才做 列表里没有对应字面量、或已用 --ip-version 显式指定时,探测结果不可能改变筛选,此时完全跳过探测。实测:纯 IPv4 列表与纯主机名列表从 ~1.6s 降到 ~0.12s
首个应答即取消其余目标 一个地址族内任一目标应答后立即取消同族其余探测,而不是等全部目标超时。否则只要有一个目标被黑洞(真实网络里是常态),每轮都要白等满超时
提示只出现一次 运行前的提示(此刻才可操作)与结束后的报告不重复同一句话;报告改为给出事实性的判定行

完全离线(两族都探不出)时不会按可达性过滤任何服务器——宁可多测也不误杀,判定行会说明这一点。

国内外域名混合(--domains mixed)

dns-opti test --domains mixed --protocols udp

all 与 mixed 回答的是两个不同的问题:

模式 分组 回答的问题
all(默认) cn 与 intl 各自独立统计 「这台解析器对国内 / 国外域名分别表现如何」——能看出「国内快、国外慢」这种差异
mixed 合并为单一 mixed 组 「在国内外混合的真实负载下,这台解析器整体表现如何」——适合只挑一台日常主用解析器

混合模式是显式选择,绝不会因为没写参数就发生:把两组分数平均掉恰好会抹平 all 想暴露的差异,所以它只能是用户主动要的结果。两个列表按域名去重后再合并,两边都出现的域名只测一次,不会被重复计入权重。

过滤策略:原生 / 安全

策略 含义
原生 不过滤任何域名,按权威记录如实返回
安全 会拦截域名:至少拦截恶意软件 / 钓鱼,通常也拦截广告与跟踪域名
未确认 来源文档未说明其过滤策略,且实测也未取得可用证据
dns-opti servers --policy native      # 只看不过滤的解析器
dns-opti servers --policy security    # 只看会拦截的解析器

为什么把「拦截广告」并入了「安全」? 二者在文档层面确实是不同策略,但在观测层面无法区分:只有部分厂商对拦截的应答是 sinkhole 地址(如 Cloudflare 返回 0.0.0.0),另一些厂商干脆让查询超时。超时的拦截与「不可达」在客户端完全一样,因此无法可靠地区分「只拦威胁」与「连广告一起拦」。报一个自己无法验证的细分,等于给猜测贴了标签——所以合并为一个「安全」。每条端点的 note 仍记录了文档所述的具体拦截范围,日后若要拆分有据可依。

为什么以人工核对文档为主? 解析器的过滤策略无法只靠查询它本身得出:没有任何握手会告诉你「我会拦广告」。探测在两个方向都不可靠:

  • 误判为有过滤:一个本来就已经下线的域名,和「被策略拦截」看起来完全一样。
  • 误判为无过滤:解析器可能异步更新名单,或名单里恰好没有你探测的那一个域名,因此探测成功什么也证明不了。
  • 本机网络会污染结论:若上游解析器抢先应答了探测域名,任何解析器看起来都「不过滤」。

而厂商文档是写明策略的,也正是用户选解析器时真正想知道的东西。所以这张表逐条核对厂商文档,每条都记录了判定所依据的原文;文档没有说明且实测也无证据的端点一律记为「未确认」,绝不猜测——一个错误的「安全」标签比诚实的「未知」更糟。

同一个厂商的不同地址可以是不同策略,因此这张表按端点而不是按厂商建立: Quad9 的 9.9.9.9 拦截恶意域名,而 9.9.9.10 明确不拦截;AdGuard 的 94.140.14.14 拦截广告,而 94.140.14.140 不拦截;114DNS 的 .114 是纯净版、.119 才是拦截版。按厂商归类对这些用户最可能手动填入的地址恰好是错的。

几个反直觉、容易搞错的对应关系(已在测试中固定):

端点 策略 容易搞错的地方
dns.mullvad.net 原生 裸域名才是无过滤版;名字看起来更「基础」的 base.dns.mullvad.net 反而拦截广告+跟踪+恶意域名
77.88.8.8 原生 Yandex 的 .8 是**基础(不过滤)**模式;安全模式是 .88、家庭模式是 .7,地址几乎一样
dnsforge.de 安全 裸域名是带广告拦截的 Normal 档;无过滤版是 blank.dnsforge.de
180.76.76.76 安全 百度公共 DNS 会拦截病毒/木马,并非「原生」
2620:fe::9 安全 与 2620:fe::fe 同属 Quad9 推荐(拦截)服务;不拦截的是 2620:fe::10

dns-opti servers 会在末尾给出内置清单的策略分布;--policy 筛选只作用于内置清单,手动 --servers 与系统 DNS 不受影响。

类型检测(交互界面手动启动)

主菜单的 「检测内置 DNS 类型」 会逐个探测内置清单里的解析器,把它判为「安全」或「原生」,并与硬编码的端点表对照,列出所有不一致项供人工复核。它只从交互界面手动启动,不参与正常测试流程。

判定口径:

观测结果 判定
任一测试域被拦截 安全
全部测试域都正常应答 原生
三者均无可用应答 未确认(无应答 ≠ 不过滤)

探测的域名分两类,这个区分是实测出来的、不是装饰:

类别 域名 作用
厂商自检域 malware.testcategory.com、phishing.testcategory.com、advertising.filterdns.net 各厂商官方文档给出的自检方式,权威但在本机不具区分度
广告域名 googleads.g.doubleclick.net、pagead2.googlesyndication.com、ad.doubleclick.net 实测可区分:拦截型返回 sinkhole,原生返回真实地址

⚠️ 实测结论,值得记下:只用前三个厂商自检域时,一次 72 个目标的全量扫描结果为 安全 0 个——连已被证实会拦截的 AdGuard(94.140.14.14)都判成了「原生」。原因是这些自检域只上了自己厂商的名单,在本机网络下连原生解析器也照常返回真实地址。补上广告域名后,AdGuard 的 4 个端点全部被正确判为「安全」。因此检测必须同时使用两类域名。

检测的结论只是证据,不是定论:它反映的是本机网络路径下观测到的行为,换网络可能不同。它也不能证明「只拦威胁不拦广告」——如前所述,那在客户端无法区分。新增内置 DNS 时应运行本项,并把结论写回 internal/policy 的端点表。

UDP + DoH 组合模式

dns-opti test --protocols udp+doh --regions cn

udp+doh 不是一种新协议,而是一个选择器:它展开为 UDP 与 DoH 两种传输,并把同一服务商的两种传输配成一对,在报告末尾额外给出「UDP 与 DoH 对比」一节。

两种传输分别统计,不合并成一个分数——UDP 是一次往返,DoH 还要额外付出 TLS 与 HTTP 握手,把它们平均掉恰好会掩盖这个模式想回答的问题:加密传输在本机要贵多少。

一个服务商可能有多个 UDP 端点但只有一个 DoH 端点(AliDNS 就是如此),此时该侧会聚合多个端点并标注「N 个端点」,避免把两台服务器悄悄并成一行。只有两侧都至少成功一次才会给出差值——否则缺失侧的全 0 会被误读成「非常快」。

dns-opti servers 参数

参数 说明
-p, --protocols 按协议筛选:udp,dot,doh,doh3
--server-class 按来源筛选:cn / intl / all
--regions 按地区码筛选,逗号分隔;如 cn,hk,tw,us,cdn
--policy 按过滤策略筛选:native / security
--ip-version 按地址族筛选:ipv4 / ipv6 / both

输出的每一行形如 协议 地址族 地址 策略 名称,并在末尾给出内置清单覆盖的地区码与策略分布:

UDP    IPv4      114.114.114.114                                原生         114DNS 1
UDP    IPv4      114.114.114.119                                安全         114DNS 安全版
UDP    IPv6      2400:3200::1                                   原生         AliDNS 1 (IPv6)

共 106 个服务器。
内置清单包含以下地区码: CN, CDN, US, CY, DE, SE, RU, TW, ID
内置清单的过滤策略分布: 原生=68 安全=33 未确认=5

dns-opti web 参数

参数 说明
-r, --result 要展示的结果 JSON(本工具格式,或 xxnuo/dns-benchmark / palemoky/dnspick 格式)
--port 本地端口,默认随机分配
--no-open 不自动打开浏览器,只打印地址

服务只监听 127.0.0.1,并在 HTTP 层再校验一次 Host 头,非本机来源一律 403。 退出方式:在终端按 Ctrl+C。

Web 界面说明

界面结构参考 https://bench.dash.2020818.xyz/:左侧筛选栏 + 右侧「指标标签页 → 大卡片横向柱状图 → 分页」。在此之上做了以下扩展:

区域 功能
顶部导航 读取分析(导入 JSON)、导出 JSON、深浅色主题切换
左侧 · 服务器类型 按 UDP / DoT / DoH / DoH3 筛选
左侧 · 域名组 按域名分组筛选:国内 / 国外 / 国内外混合——不同分组的分数不可直接比较
左侧 · 过滤策略 按解析器的过滤行为筛选:原生 / 安全(外加「未确认」)。与地区无关的独立维度,说明文字由服务端下发
左侧 · 地区码 按解析器所属国家/地区筛选(ISO 3166-1 国家码)。含快捷分组(中国 / 亚太 / 美洲 / 欧洲 / CDN 任播 / 内网·未知)、地区码搜索、全选 / 清除 / 反选,以及可逐个勾选的地区码胶囊(带记录数)
左侧 · 地址族 按 IPv4 / IPv6 筛选。主机名端点显示的是实际解析并使用的地址族,而不是「理论上支持」
公式栏 一键切换四套评分公式,排名立即重算;「公式说明」展开公式、说明与适用场景
指标标签页 综合得分 / 平均延迟 / P95 延迟 / 延迟标准差 / 成功率,各自独立排序,成功率为 0 的服务器始终排在最后(避免「没有数据」被误读成「0ms 最快」)
柱状图 横向柱状图,按当前指标排序,颜色越深名次越前;点击柱子复制服务器地址
UDP 与 DoH 对比 组合模式(--protocols udp+doh)专有:同一服务商两种传输并排展示,给出 DoH − UDP 延迟差;聚合了多个端点的一侧会标注端点数。普通运行不显示该面板
排名变化卡片 列出同一 DNS 在其它公式下位次变化最大的几项——直观展示「快 / 稳」取舍
系统 DNS 卡片 给出是否建议更换的结论;内网 / 环回 / 链路本地地址额外警告
延迟分布 成功查询落在各延迟区间的堆叠柱状图(仅本次测试结果可用,导入格式不含每次查询延迟)
明细数据 完整表格,含过滤策略、地区、地址族、成功率、平均 / P95 / 标准差与当前公式得分

服务器以 IP 标识

Web 端与 CLI 报告里的服务器标识统一为 IP 地址,并且同一 IP 会以「IP + 协议 + 域名组」作为行标识:

119.28.28.28 · UDP · 国内外混合

原因是一个厂商名对应多个端点——AliDNS 在 4 种传输、2 个地址族下共有 8 个端点——只有地址才是唯一的;而且地址正是用户要填进系统设置的东西,可以直接从报告里复制出来用。图表标签、明细表首列都是这个规则。

主机名端点(DoH / DoT)在解析出地址后显示该 IP,同时把配置用的端点地址以小字保留在下一行:证书签发给域名,只给 IP 的话用户没法配置。同理,「点击复制」对明文协议复制 IP,对加密协议复制完整端点 URL。

厂商名仍然保留在导出文件的 meta.dns_servers[].name 里作为来源信息,界面不再用它做标识。

排名由服务端 /api/rank 计算,与 CLI 及导出文件使用同一套评分代码,不存在两处实现不一致的可能。 地区名称、快捷分组、过滤策略标签与 UDP/DoH 配对同样由服务端下发(/api/result 的 regions、region_groups、policies、comparisons 字段),页面不自行推导,因此与 CLI 的措辞和归类完全一致。


评分公式

同一份测试数据,在不同公式下的排名可能不同——这正是本工具提供多套公式的原因。 Web 端切换公式会立即重新排名。

公式 表达式 适用场景
极速优先 speed Score = 1000 × 成功率 ÷ 平均延迟(ms) 对延迟极度敏感、能接受偶尔失败:快速打开网页、短连接小请求
稳定优先 stable Score = 1000 × 成功率² ÷ 平均延迟(ms) × 1 ÷ (1 + 标准差 ÷ 平均延迟) 视频会议、在线游戏、长时间保持连接
综合体验 comprehensive Score = 1000 × 成功率 ÷ (0.5 × 平均延迟(ms) + 0.5 × P95延迟(ms)) 日常网页浏览与综合场景(默认)
抗抖动优先 jitter Score = 1000 × 成功率 ÷ (平均延迟(ms) + 2 × 标准差(ms)) 移动网络、Wi-Fi 不稳定、网络波动大

用 dns-opti formulas 可以查看每套公式的完整说明。


工作原理

并发模型

  • 任务粒度 = 一个 DNS 服务器 + 一种协议,全局 worker pool 调度。
  • 默认并发数 = runtime.NumCPU() × 3(CPU 线程数 × 3)。 每个 worker 独占一个 DNS 服务器,所以延迟是「一次往返」而不是「排队等待」; 每线程 3 个 worker 足以把网络等待藏起来,又不至于让 CPU 在解析响应时互相抢占。
  • 每个 worker 独占一个 DNS 服务器,服务器的域名 × 试探次数查询拆到 2 条连接(DefaultPerServerConcurrency = 2)上,每条连接由独立 goroutine 独占, 查询互相重叠而不是排队。
  • 不同 DNS 服务器并行测试;同一台服务器最多 2 条连接并发——实测(19 台服务器交错对照) 与全串行的成功率、延迟没有差别,但墙钟时间约减半。再调高会开始丢应答 (单服务器 ≥16 并发时成功率跌到 88~96%),因此固定为 2。

连接复用与预热

协议 连接处理
UDP 无连接,直接查询
DoT 每条连接维护一条持久 TCP+TLS 连接,复用
DoH 每条连接一个独立 http.Transport,启用 keep-alive,复用连接
DoH3 每条连接一个 HTTP/3 (QUIC) 连接,复用

正式计时前,每条连接都会先用 --warmup-domain(默认 example.com)做一次预热查询 (所以一台服务器默认预热 2 次),结果不计入统计,只用于建立连接、完成 TLS 握手、 并让服务器的 hostname 解析进入缓存。连接中断时自动重建,该次查询记为失败。

服务器地址若是域名,会在测试前解析为 IP 并缓存,系统 DNS 的解析耗时不会被算进查询延迟。 同时保留原域名用于 TLS SNI 与证书校验。

连续失败早停

若某个服务器连续失败 5 次(失败的预热算作第一次),则判定为不可达, 剩余查询直接记为失败,不再逐次等待超时。这可以避免一个被防火墙拦截的 DoH3 端点 拖慢整轮测试。

数据流式写入

  • 每一条原始记录(raw)在产生时立即写入临时 JSON Lines 文件,不在内存里堆积。
  • 汇总数据(summary)在内存中聚合——它的规模只与「服务器数 × 协议数 × 域名组数」有关,是有界的。
  • 导出时把 meta、raw(从临时文件流式拷贝)、summary 拼成一个 JSON 文件。

结果文件格式

{
  "meta": {
    "version": "1.0",
    "timestamp": "2026-09-25T12:00:00Z",
    "platform": "windows/amd64",
    "concurrency": 12,
    "warmup_domain": "example.com",
    "domain_groups": ["cn", "intl"],
    "domains": "all",                // 域名选择:cn / intl / all / mixed / 自定义
    "protocols": ["udp", "doh"],
    "combo": "udp+doh",              // 仅组合模式存在
    "regions": ["CN"],               // 仅 --regions 时存在
    "policies": ["native"],          // 仅 --policy 时存在
    "ip_version": "ipv6",            // 仅 --ip-version 指定时存在
    "network": {                     // 地址族实测判定(见上文「IPv6 与地址族」)
      "ipv4": true, "ipv6": false, "skipped": 10,
      "label": "本机 IPv6 实测无响应(已发真实查询确认),已跳过 8 个 IPv6 服务器…"
    },
    "dns_servers": [
      { "name": "AliDNS 1", "address": "223.5.5.5", "protocol": "udp",
        "ip": "223.5.5.5", "region": "CN", "policy": "native",
        "family": "ipv4", "is_system": false }
    ],
    "system_dns": ["192.168.1.1"]
  },
  "raw": [
    { "dns": "223.5.5.5", "protocol": "udp", "domain": "baidu.com", "group": "cn",
      "attempt": 1, "success": true, "latency_ms": 12.3, "ts": 1758801600 }
  ],
  "summary": [
    { "dns": "223.5.5.5", "protocol": "udp", "group": "cn", "ip": "223.5.5.5",
      "region": "CN", "policy": "native", "family": "ipv4",
      "total": 71, "success": 70,
      "success_rate": 0.9859, "avg_ms": 15.2, "p95_ms": 28.1, "stddev_ms": 4.3,
      "combo": "alidns|CN" }        // 仅组合模式存在
  ]
}

失败统一记为 success: false,不区分超时与错误码(原因对「选哪个 DNS」没有指导意义, 只会让数据文件膨胀)。

ip 是该服务器实际被访问到的字面地址:裸 IP 端点就是它自己,主机名端点则是解析结果。 它是界面上的服务器标识(见上文「服务器以 IP 标识」),name 只作为来源信息保留。 该字段是新增的,旧版本导出的文件没有它,导入时会从 dns 中取出主机作为回落值。

region、policy 与 family 分别是地区码、过滤策略与地址族(见上文「地区码」「过滤策略」)。 family 记录的是实际使用的地址族:主机名端点只有在解析之后才知道,因此它反映的是这次测试真正走的路径, 而不是「理论上支持 IPv6」。从未连通的服务器该字段为空,页面上显示为「双栈 / 未定」。 policy 为 unknown(界面显示「未确认」)表示该端点没有可核实的文档依据,不是「不过滤」的意思。

domains 记录本次的域名选择,因此一个文件能自己说明它是分组(all)还是混合(mixed)统计的, 不必从行里的分组去反推。

combo 只在 --protocols udp+doh 时出现:它把同一服务商的两种传输标成同一个配对标识, Web 端据此重建「UDP 与 DoH 对比」面板。该字段是新增的,旧版本导出的文件仍可正常导入。

network 记录地址族实测判定,让文件能自己解释「为什么没有 IPv6 服务器」—— skipped 是被该判定排除的服务器总数(含系统 DNS),forced 表示这是 --ip-version 的显式指令而非探测结论。旧版本导出的文件没有该字段,导入时会被忽略。

兼容导入

Web 端 导入 JSON 支持三种格式:

  1. 本工具的导出文件(summary 字段为标志);
  2. 只有 raw 没有 summary 的导出文件——会按记录重新聚合;
  3. xxnuo/dns-benchmark(以服务器地址为 key 的 map,含 latencyStats / totalRequests)与 palemoky/dnspick(含 results 数组)的结果文件。

后两种格式不含国内/国外域名分组,导入后统一归入「导入数据」组,并在页面顶部说明。 它们的 ip 与 region 由文件名 / 地址推导,policy 则按同样的端点表查得—— 查不到就是「未确认」,不会因为「没听说过」而当成原生。


内置数据

DNS 服务器列表与域名列表都以代码 / 文本形式硬编码进二进制, 不提供外部动态配置,更新列表就是改代码后重新编译。

文件 内容
data/dns_servers.go 国内公共 DNS、国外公共 DNS、DoT / DoH / DoH3 服务器(每条同时标注地区码与来源分类)
data/cn_domains.txt 国内域名列表(一行一条,# 为注释)
data/intl_domains.txt 国外域名列表(一行一条,# 为注释)

国内与国外域名严格分离,任何时候都不会把两者混在一起算一个分数—— 一个 DNS 完全可能对国内域名很快、对国外域名很慢。

地区码

每条内置服务器都手工标注了一个地区码:ISO 3166-1 两位国家码(CN、US、DE…), 或三个特殊码 CDN(全球任播,无法归属单一国家)、PRIVATE(内网 / 环回 / 链路本地)、UNKNOWN。

为什么不用 GeoIP 数据库:本工具测的绝大多数服务器都是任播,同一个地址在世界各地由不同机房应答, GeoIP 查出来的是厂商注册地,而不是真正应答你的那个节点所在的国家——对「该选哪个 DNS」这个问题没有意义。 真正有用的是另外两件事,而它们都能手工标注:哪些网络只属于一个国家,哪些是全球任播。 这样既不需要内置一个 10MB 的数据库,也不需要联网查询。

对不在内置清单里的地址(--servers 自定义、系统 DNS、导入的结果文件), 地区码由一个手工核对的网段表推导(见 internal/region/region.go), 覆盖了本工具内置的解析器网络以及其他常见公共解析器;推导不出来的一律记为 UNKNOWN, 而不是猜一个看起来合理的国家。

导入 xxnuo/dns-benchmark 的结果文件时会优先采用该文件自带的 geocode 字段。 该字段除了国家码,还会出现厂商名(CLOUDFLARE、GOOGLE、TWITTER 等), 这些都会被归并到 CDN,因此筛选栏不会冒出无法标注的伪「国家」。


系统 DNS 检测

测试时会读取本机当前配置的 DNS(Windows 走 Get-DnsClientServerAddress,类 Unix 读 /etc/resolv.conf), 把它作为一个普通服务器纳入对比,并在结果中标记「当前系统 DNS」。

对于 RFC 1918 / RFC 4193 私有地址、环回地址、IPv6 链路本地地址(通常是路由器、公司内网或 VPN 的解析器), 会额外标记为「内网地址」并给出提醒:换成公共 DNS 可能导致内部域名无法解析。

只有在差距足够明显时(延迟差 ≥ 15ms,或成功率差 > 5%)才会给出「建议切换」的结论; 差距在误差范围内则明确建议保留现有设置。


目录结构

dns-opti/
  cmd/dns-opti/main.go     程序入口
  internal/cli/            cobra 命令、参数解析、报告输出
  internal/tui/            可视化操作界面(双击 exe 打开的键盘菜单)
  internal/console/        双击运行时保持窗口不关闭(Windows)
  internal/engine/         把配置、任务列表、调度器、记录器串成一次完整运行
  internal/dnsclient/      UDP / DoT / DoH / DoH3 查询、连接复用与预热、地址族探测、系统 DNS 检测
  internal/region/         地区码:内置网段/主机名表、快捷分组、中文名称
  internal/policy/         过滤策略:原生 / 安全 的端点表、分类探测与说明
  internal/settings/       交互界面的配置持久化(程序目录下的 JSON)
  internal/scheduler/      全局 worker pool,按 DNS 服务器分片
  internal/scorer/         四套评分公式与排名
  internal/store/          流式 JSONL 记录、汇总聚合、结果导出与统计
  internal/web/            本地回环 Web 服务、静态资源(go:embed)、格式导入
  internal/web/static/     前端页面(index.html / style.css / app.js / echarts.min.js)
  internal/config/         运行选项与参数解析
  internal/domainlist/     测试域名选择
  internal/model/          共享数据模型(meta / raw / summary)
  internal/ui/             TUI 进度面板与 CLI 报告
  data/                    内置 DNS 列表与域名列表
  .goreleaser.yml          发布配置

与 PLAN.md 第十一节的一处偏差:计划里把前端放在顶层 web/static/, 本项目放在 internal/web/static/。原因是 go:embed 只能嵌入当前包目录及其子目录下的文件, 顶层目录无法被 internal/web 嵌入。放在包内是既能单二进制分发、又不依赖外部文件的唯一做法。 前端为纯静态 HTML + 原生 JS(无构建步骤),图表库 ECharts 已随二进制嵌入,离线可用。


已知限制

  • 不检测代理。 请在运行前自行关闭系统代理 / 加速器。
  • 本地 DNS 缓存会影响结果。 预热统一使用 example.com,但操作系统或上游仍可能缓存。 域名越多、次数越多,结果越稳定。
  • 不同协议的分数不可直接比较。 UDP 没有握手开销,DoT/DoH/DoH3 有;请在同一协议内比较, 或用协议筛选后分别查看。
  • 单次测量噪声。 建议 --attempts 至少为 3;只测一次时,几毫秒的差异不具参考意义。
  • DoH3 依赖网络放行 QUIC。 若 UDP 443 被拦截,DoH3 会全部失败——这是网络环境的结果,不是工具的问题。 可用 -tags nodoh3 构建不含 DoH3 的版本;该版本会直接拒绝 --protocols doh3,不会产生一片 0% 的假象。
  • 地区码是「服务商归属地」,不是「应答节点所在地」。 任播服务(CDN)在世界各地由不同机房应答, 无法归属单一国家,因此统一标为 CDN。这也是不使用 GeoIP 数据库的原因(见上文「地区码」)。
  • 地址族探测需要一次真实查询。 启动时会向每个地址族尝试少量目标(每个地址族多个目标、并发进行,首个应答即取消其余), 且只在探测结果可能改变筛选时才做;完全离线时两者都会判为不可用, 此时不会过滤任何服务器(宁可多测也不误杀),可用 --ip-version 显式指定。
  • 探测只回答「本机能不能走这个地址族」,不回答「这些服务器好不好」。被判定不可用的地址族会被整体跳过; 若你想在这种机器上强测 IPv6,--ip-version ipv6 会覆盖判定并如实报告失败。
  • UDP + DoH 组合模式只配对 UDP 与 DoH。 DoT / DoH3 不参与配对;一个服务商若有多个 UDP 端点, 该侧会聚合展示并标注端点数,因此那一行代表的是「该服务商的 UDP 传输整体」,不是单台服务器。
  • 过滤策略以人工核对厂商文档为主,类型检测为辅。 内置表的策略反映的是文档所述的行为, 可能滞后于厂商的实际改动;交互界面的「检测内置 DNS 类型」提供实测校验,但如该节所述, 它只能证明「会拦截」,且结论依赖本机网络路径。 内置清单里目前有 5 个端点因缺少可核实的文档依据而标为「未确认」: OneDNS 的 117.50.10.10 / 52.80.52.52(官网为纯 JS 单页应用,无法确认是拦截版还是纯净版)、 HiNet 的 168.95.1.1、360 的 dot.360.cn / doh.360.cn。它们不会被当成「原生」。
  • 「安全」不区分「只拦威胁」与「连广告一起拦」。 该细分在客户端无法观测 (超时的拦截与不可达完全一样),详见「过滤策略」一节的说明。
  • 类型检测只能证明「会拦截」,不能证明「不过滤」。 判为「原生」只表示本次探测未观测到拦截; 若本机上游抢先应答了探测域名,任何解析器都会看起来不过滤。三个测试域均无可用应答时判为「未确认」。
  • 过滤策略筛选只作用于内置清单。 手动 --servers 指定的条目与系统 DNS 不受 --policy 影响, 与 --regions 的处理方式一致。
  • 配置文件写在程序目录。 若程序装在只读目录(如 Program Files),会自动回退到当前工作目录, 因此同一份程序从不同目录启动时可能读到不同的配置文件。

许可证

本项目以 Apache License 2.0 发布,详见 LICENSE。

Copyright 2026 DHA404

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

第三方组件

二进制内嵌了 Apache ECharts 5.6.0(Apache-2.0,与本项目同许可),用于本地页面的图表, 其许可证头保留在 internal/web/static/echarts.min.js 内。

Go 依赖(bubbletea / lipgloss / miekg/dns / quic-go / cobra / x/term)不随本仓库分发, 各自遵循其原有许可证。完整清单、版本与许可证见 NOTICE。

参考项目

本项目代码为完全重写,不包含参考项目的任何源代码文件:

具体借鉴的设计点见文件开头的「参考来源声明」与 NOTICE。 内置的 DNS 服务器列表与域名列表为本项目自行整理。

About

dns-opti:Go 编写的 DNS 服务器优选工具。并发测试 UDP/DoT/DoH/DoH3,IPv4/IPv6 双栈,按地区码与过滤策略筛选,四套评分公式排名,含可视化 TUI 与本地 Web 面板。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages