$ pwd: ~ / 技术分享 / article/kixdns-bypass-gateway-dns-guide

KixDNS:用Rust重塑旁路网关的DNS分流——从选型到踩坑的记录

// 为什么放弃Singbox自身的DNS,为什么不使用MosDNS或SmartDNS,而选择一个名不见经传的Rust DNS转发器?这不是一篇软件测评,而是一次真实家庭网络的生产环境完整落地记录。

git-status.logreadonly
$ git log --oneline --stat
📁 category: 技术分享📅 updated: 2026-06-28🏷️ tags: 旁路由, DNS, 家庭网络
article/kixdns-bypass-gateway-dns-guide.mdreadonly
家里网络拓扑一直是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
notion image

📝 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(声明式管道)
核心模型
sequenceparallelfallback
多上游测速取最快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 广告域名 → rejectif 国内域名 → forward_localelse → 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/32240.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模式后稳定运行,几个关键指标:

🤗 经验总结

  1. sing-box旁路网关 + strict_route的UDP DNS有坑。如果你遇到间歇性DNS超时,先检查sing-box的DNS server type,改成TCP可能直接解决。参考Issue #1918
  1. DNS分层缓存不是多余的。AdGuard Home做第一层缓存(高频域名 + 广告过滤),KixDNS 做第二层(分流 + DoH + stale 兜底)。两层配合下,即使KixDNS的上游全部挂了,AdGuard Home的stale缓存还能扛一阵。
  1. 选DNS工具别只看功能列表。MosDNS功能最全,但配置复杂度也是最高的。KixDNS功能精简,但每一个功能都做到了生产级。对于家庭网络,稳定和简单比功能丰富更重要。
  1. Rust 不是万能的,但在网络基础设施场景下确实有优势。无GC意味着延迟可预测,所有权模型意味着内存安全,编译期检查意味着配置错误在启动时就暴露而不是运行时炸。
对大多数旁路网关用户来说,DNS转发器的核心价值就三件事:分流准、解析快、配置简单。在这三个维度上,KixDNS是我目前用过的最优解。

📎 参考文章

有关KixDNS安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~ 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
follow-me.shreadonly
$ cat ./notice.txt

// 喜欢这篇文章?在 X 上看更多日常更新

comments.logreadonly