家里网络拓扑一直是MikroTik RouterOS做主路由,Debian旁路网关跑sing-box透明代理(以前使用Daed,但是最近发现Daed对anyTLS的支持并不是很好,所以换到了Sing-box)。DNS层之前用的MosDNS,负责国内/海外分流、DoH上游查询和缓存加速。整体跑了挺久,但有两个一直让我不太舒服的点:
第一,MosDNS的YAML配置太重了。 它的
sequence → parallel → fallback插件链模型虽然灵活,但想改一条分流规则,你得在几十行YAML里理清插件间的依赖关系。加了AdGuard Home做广告过滤后,MosDNS退化成纯转发器,但配置复杂度一点没降。第二,Go的GC抖动。 高并发查询时偶发延迟尖刺,p99有时候会飙到300ms以上。日常感知不强,但手机推送、IoT设备的心跳包偶尔会因为 DNS解析慢了半拍而重试。
第三,维护成本。我的分流需求其实很固定——国内域名走阿里/DNSPod DoH,海外域名走境外DoH,命中污染特征就重查。但用MosDNS实现,每种逻辑都要拆成
query_matcher + sequence + forward 插件组合,改一条上游地址要翻好几层缩进。对于"改完即忘、过两个月再看一脸懵"的家用场景,YAML插件链的认知负担太高,整体还是偏重了。我需要的东西其实很简单:能按GeoSite分流国内/海外,上游走DoH,有个靠谱的缓存,配置别太反人类,性能稳一点。
然后我在GitHub上翻到了KixDNS。

📝 KixDNS是什么
一个用Rust写的高性能非递归DNS转发器。核心特点:
- Pipeline架构:不是MosDNS那种插件链,而是声明式的管道选择(
pipeline_select)+ 两阶段处理(请求匹配 + 响应匹配)
- 零拷贝UDP处理:基于
BytesMut,最小化内存拷贝
- moka缓存引擎:Rust生态里最快的并发缓存,支持后台刷新和stale兜底
- 自适应流控:
PermitManager根据上游延迟动态调整并发许可数
- 热重载配置:改完
pipeline.json自动生效,不用重启服务
- GeoSite原生支持:直接读
.dat格式的geosite数据库做分流
说白了,它把我需要的功能全做了,而且用Rust的方式做——零开销抽象、无 GC、编译期安全。
📝 同类DNS转发器横向对比
在决定换之前,我把KixDNS和之前考虑过的两个方案做了系统对比,分别是之前使用过的MosDNS以及还有不少人在用的SmartDNS。
1. 架构哲学
ㅤ | MosDNS | SmartDNS | KixDNS |
语言 | Go | C | Rust |
配置格式 | YAML(插件链) | conf(类 dnsmasq) | JSON(声明式管道) |
核心模型 | sequence → parallel → fallback | 多上游测速取最快IP | pipeline_select → 两阶段匹配,也可实现多上游使用最快IP |
分流方式 | 插件组合,需手写逻辑 | server-group + domain-rule | GeoSite匹配 + 响应CIDR检测 |
缓存 | 内置(可持久化) | 内置 | moka(后台刷新 + stale 兜底) |
上游协议 | UDP/TCP/DoT/DoH | UDP/TCP/DoT/DoH/DoQ | UDP/TCP/DoT/DoH/DoQ |
热重载 | 不支持,需重启 | 不支持,需重启 | 支持,文件监听自动生效 |
Web UI | 无 | 有 Dashboard | 无 |
2. 适用场景
MosDNS适合那种喜欢"编程式"控制 DNS 每一步流程的用户。它的
sequence 插件本质上是一个迷你的DSL,你可以写出非常复杂的条件分支:if 广告域名 → reject,if 国内域名 → forward_local,else → forward_remote with fast_fallback。代价是配置文件动辄上百行,可读性差,团队协作时交接成本高。SmartDNS的核心差异化是"测速选 IP"——它会对上游返回的多个A记录做TCP连接测速,把访问最快的IP返回给客户端。这个功能在国内运营商DNS污染 + CDN调度不精准的场景下确实有用。但它的分流逻辑依赖
server-group 和 domain-rule,想实现GeoSite级别的国内外分流需要写很多规则,不如直接读geosite.dat 来得干净。另外C语言的内存管理虽然性能好,但在高并发下的稳定性不如Rust的所有权模型。KixDNS走的是另一条路:声明式管道。你不需要"编程",只需要"声明"——域名匹配什么条件就走哪个管道,管道里定义上游和响应处理逻辑。配置文件就是一棵JSON树,一目了然(但是写起来太屎了,JSON作为配置文件真的写起来像吃屎。)。它的杀手锏是响应匹配(
response_matchers):不仅能在请求阶段按域名分流,还能在响应阶段检查返回的 IP 是否命中污染特征(比如 0.0.0.0/32、240.0.0.0/4),一旦命中就自动jump到备用管道重查。MosDNS可以做到类似的效果,但是不如Kix的配置简洁。3. 性能体感
我没有做严格的benchmark(三个软件跑在不同硬件上比不公平),但从生产环境的实际体感来说:
- KixDNS:日常p50 = 16ms(国内)/ 50ms(海外),p99 = 90ms,内存19MB,零GC抖动
- MosDNS(之前的生产数据):p50差不多,但p99偶尔飙到300ms+,内存40-60MB
- SmartDNS(短期测试):测速选IP功能会增加首次解析延迟(多了一次TCP握手测速),缓存命中后和前两者差不多。
📝 KixDNS安装与配置
0. 主机环境
1. 安装KixDNS
从GitHub Releases下载预编译二进制:
2. 配置pipeline.json
pipeline.json是kixdns配置文件的名称,你可以根据自己的需要更改成自己喜欢的名称,例如config.json,但需要注意,后面的systemctl启动及管理脚本使用的是pipeline.json这个名称,所以你也需要进行替换。
配置要点:
缓存策略。
cache_background_refresh: true 是关键——缓存条目在TTL剩余 15%(cache_refresh_threshold_percent)时会触发后台异步刷新,客户端永远拿到的是新鲜数据,不会因为缓存过期而等待。serve_stale: true 确保即使上游全部超时,也能返回上一次的旧缓存(保留30秒),而不是直接报错。这两个特性组合起来,意味着客户端几乎永远不会经历DNS解析失败。反污染跳转。
cn_doh 管道的 response_matchers 列了6个CIDR,覆盖了国内DNS投毒的典型特征 IP:0.0.0.0/32(黑洞)、240.0.0.0/4(保留地址段)、以及几个已知的污染节点。一旦国内DoH返回的IP命中这些CIDR,KixDNS会自动 jump_to_pipeline: global_doh,用境外DoH重查。这个机制比MosDNS手写fallback逻辑优雅得多——它是声明式的,你只需要列出"什么样的答案是脏的",而不是写"如果脏了怎么办"的控制流。DoH连接池。
doh_pool_size: 32 意味着KixDNS会维护32个到每个上游DoH服务器的复用连接。DNS查询包只有几十字节,但DoH跑在HTTPS上,如果没有连接复用,每次查询都要走TLS握手(~100ms+)。连接池把这个开销摊薄到接近零。自适应流控。
flow_control 的三个参数定义了一个动态窗口:初始500个并发许可,根据上游延迟在100-800之间自动伸缩。上游响应快就放大窗口提高吞吐,上游变慢就收缩窗口防止雪崩。这个机制让KixDNS在上游波动时不会因为排队过多请求而加剧延迟。upstream:你需要替换
global_doh 中的upstream 为你自己使用的国外DOH服务,例如Google、Cloudflare、NextDNS、Adguard DNS等等。我个人推荐NovaXNS。3. systemd 服务
LimitNOFILE=65536 是必须的——DoH连接池 + UDP pool + TCP pool加起来,默认的1024文件描述符限制根本不够用。4. 接入sing-box
sing-box的DNS配置指向KixDNS:
注意这里用的是
"type": "tcp" 而不是 "udp"——这是一个踩坑后的结论,下面详细说。5. 完整DNS链路
6. 踩坑:sing-box的UDP DNS黑洞
部署完KixDNS后过了一天,手机部分页面打不开、加载很慢。第一反应是查KixDNS日志:
KixDNS完全健康。问题出在上游—sing-box:
sing-box的DNS配置是
type: "udp" 指向 127.0.0.1:5353,KixDNS也确实回了,但sing-box那边27%的查询 context deadline exceeded(10秒超时)。翻GitHub找到了根因——Issue #1918:sing-box在Linux旁路网关 +
strict_route: true + TUN system 栈的环境下,DNS server使用UDP协议时,会持有一个长期固定源端口的UDP socket。这个socket对应的conntrack flow会逐渐变成stale/blackhole状态——包发出去了,回包却被TUN的strict_route规则吞掉。这是sing-box的已知bug,上游至今未修。解决方法很简单:把sing-boxDNS server 的
type 从 "udp" 改成 "tcp"。KixDNS同时绑了UDP和TCP(bind_tcp: "0.0.0.0:5353"),零成本切换。改完后做了个压力测试——80个并发TCP DNS查询:
实际峰值才7 QPS,TCP的322QPS 承载能力是实际负载的46倍。本地环回的TCP握手开销在 0.1ms级别,在50-85ms的DoH上游延迟面前完全可以忽略。
运行状态
换到KixDNS + TCP模式后稳定运行,几个关键指标:
🤗 经验总结
- sing-box旁路网关 + strict_route的UDP DNS有坑。如果你遇到间歇性DNS超时,先检查sing-box的DNS server type,改成TCP可能直接解决。参考Issue #1918。
- DNS分层缓存不是多余的。AdGuard Home做第一层缓存(高频域名 + 广告过滤),KixDNS 做第二层(分流 + DoH + stale 兜底)。两层配合下,即使KixDNS的上游全部挂了,AdGuard Home的stale缓存还能扛一阵。
- 选DNS工具别只看功能列表。MosDNS功能最全,但配置复杂度也是最高的。KixDNS功能精简,但每一个功能都做到了生产级。对于家庭网络,稳定和简单比功能丰富更重要。
- Rust 不是万能的,但在网络基础设施场景下确实有优势。无GC意味着延迟可预测,所有权模型意味着内存安全,编译期检查意味着配置错误在启动时就暴露而不是运行时炸。
对大多数旁路网关用户来说,DNS转发器的核心价值就三件事:分流准、解析快、配置简单。在这三个维度上,KixDNS是我目前用过的最优解。
📎 参考文章
有关KixDNS安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!