zfl9 / chinadns-ng

chinadns 重构增强版,支持域名分流、ipset/nftset、UDP/TCP/DoT
GNU Affero General Public License v3.0
1.13k stars 188 forks source link
chinadns chinadns-ng dns dns-over-tls dnsmasq dot gfwlist ipset iptables netlink nftables ss-tproxy

简介

ChinaDNS 的个人重构版本,功能简述:


对于常规的使用模式,大致原理和流程可总结为:

chinadns-ng 根据域名 tag 来执行不同逻辑,包括 ipset/nftset 的逻辑(test、add),见 原理

编译

请前往 releases 页面下载可执行文件,添加可执行权限,放到 PATH 路径下(如 /usr/local/bin/)。

点我展开编译说明

--- **zig 工具链** - 从 2024.03.07 版本起,程序使用 Zig + C 语言编写,`zig` 是唯一需要的工具链。 - 从 [ziglang.org](https://ziglang.org/download/) 下载 zig 0.10.1,请根据当前(编译)主机的架构来选择合适的版本。 - 将解压后的目录加入 PATH 环境变量,执行 `zig version`,检查是否有输出 `0.10.1`。 - 注意,目前必须使用 zig 0.10.1 版本,因为 0.11、master 版本暂时不支持 async 特性。 --- 如果要构建 DoT 支持,请带上 `-Dwolfssl` 参数,构建过程需要以下依赖: - `wget` 或 `curl` 用于下载 wolfssl 源码包;`tar` 用于解压缩 - `autoconf`、`automake`、`libtool`、`make` 用于构建 wolfssl 针对 x86_64(v3/v4)、aarch64 的 wolfssl 构建已默认启用硬件指令加速,若目标硬件(CPU)不支持相关指令(部分树莓派阉割了 aes 相关指令),请指定 `-Dwolfssl-noasm` 选项,避免运行 chinadns-ng 时出现 SIGILL 非法指令异常。 --- 如果遇到编译错误,请先执行 `zig build clean-all`,然后重新执行相关构建命令。 可执行文件在 `./zig-out/bin` 目录,将文件安装(复制)到目标主机 PATH 路径下即可。 --- ```bash git clone https://github.com/zfl9/chinadns-ng cd chinadns-ng # 本机 (若构建失败,请手动指定"-Dtarget"和"-Dcpu") zig build # 链接到glibc zig build -Dtarget=native-native-musl # 静态链接到musl # x86 zig build -Dtarget=i386-linux-musl -Dcpu=i686 zig build -Dtarget=i386-linux-musl -Dcpu=pentium4 # x86_64 zig build -Dtarget=x86_64-linux-musl -Dcpu=x86_64 # v1 zig build -Dtarget=x86_64-linux-musl -Dcpu=x86_64_v2 zig build -Dtarget=x86_64-linux-musl -Dcpu=x86_64_v3 zig build -Dtarget=x86_64-linux-musl -Dcpu=x86_64_v4 # arm zig build -Dtarget=arm-linux-musleabi -Dcpu=generic+v5t+soft_float zig build -Dtarget=arm-linux-musleabi -Dcpu=generic+v5te+soft_float zig build -Dtarget=arm-linux-musleabi -Dcpu=generic+v6+soft_float zig build -Dtarget=arm-linux-musleabi -Dcpu=generic+v6t2+soft_float zig build -Dtarget=arm-linux-musleabi -Dcpu=generic+v7a # soft_float zig build -Dtarget=arm-linux-musleabihf -Dcpu=generic+v7a # hard_float # aarch64 zig build -Dtarget=aarch64-linux-musl -Dcpu=generic+v8a zig build -Dtarget=aarch64-linux-musl -Dcpu=generic+v9a # mips + soft_float # 请先阅读 https://www.zfl9.com/zig-mips.html ARCH=mips32 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH+soft_float ARCH=mips32r2 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH+soft_float ARCH=mips32r3 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH+soft_float ARCH=mips32r5 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH+soft_float # mipsel + soft_float # 请先阅读 https://www.zfl9.com/zig-mips.html ARCH=mips32 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH+soft_float ARCH=mips32r2 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH+soft_float ARCH=mips32r3 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH+soft_float ARCH=mips32r5 && MIPS_M_ARCH=$ARCH MIPS_SOFT_FP=1 zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH+soft_float # mips + hard_float # 请先阅读 https://www.zfl9.com/zig-mips.html ARCH=mips32 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH ARCH=mips32r2 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH ARCH=mips32r3 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH ARCH=mips32r5 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mips-linux-musl -Dcpu=$ARCH # mipsel + hard_float # 请先阅读 https://www.zfl9.com/zig-mips.html ARCH=mips32 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH ARCH=mips32r2 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH ARCH=mips32r3 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH ARCH=mips32r5 && MIPS_M_ARCH=$ARCH zig build -Dtarget=mipsel-linux-musl -Dcpu=$ARCH # mips64/mips64el 请前往 releases 页面下载预编译的可执行文件 # 如果想自己编译,请先前往 zig 根目录,按顺序 apply 这两个补丁 # - https://github.com/ziglang/zig/pull/14541.patch # - https://github.com/ziglang/zig/pull/14556.patch ```

Docker

因为要访问内核的 ipset/nftset,docker run 时请带上 --privileged 参数。

请前往 releases 页面下载可执行文件(无依赖),cp 至目标容器,运行即可。

OpenWrt

配置示例


chnroute 分流

点我展开

```shell # 监听地址和端口 bind-addr 0.0.0.0 bind-port 53 # 国内上游、可信上游 china-dns 223.5.5.5 trust-dns tcp://8.8.8.8 # 域名列表,用于分流 chnlist-file /etc/chinadns/chnlist.txt gfwlist-file /etc/chinadns/gfwlist.txt # chnlist-first # 收集 tag:chn、tag:gfw 域名的 IP (可选) add-tagchn-ip chnip,chnip6 add-taggfw-ip gfwip,gfwip6 # 测试 tag:none 域名的 IP (针对国内上游) ipset-name4 chnroute ipset-name6 chnroute6 # dns 缓存 cache 4096 cache-stale 86400 cache-refresh 20 # verdict 缓存 (用于 tag:none 域名) verdict-cache 4096 # 详细日志 # verbose ```


gfwlist 分流

点我展开

```shell # 监听地址和端口 bind-addr 0.0.0.0 bind-port 53 # 国内上游、可信上游 china-dns 223.5.5.5 trust-dns tcp://8.8.8.8 # 域名列表,用于分流 # 未被 gfwlist.txt 匹配的归为 tag:chn gfwlist-file /etc/chinadns/gfwlist.txt default-tag chn # 收集 tag:gfw 域名的 IP (可选) add-taggfw-ip gfwip,gfwip6 # dns 缓存 cache 4096 cache-stale 86400 cache-refresh 20 # 详细日志 # verbose ```


chnlist 分流

点我展开

```shell # 监听地址和端口 bind-addr 0.0.0.0 bind-port 53 # 国内上游、可信上游 china-dns 223.5.5.5 trust-dns tcp://8.8.8.8 # 域名列表,用于分流 # 未被 chnlist.txt 匹配的归为 tag:gfw chnlist-file /etc/chinadns/chnlist.txt default-tag gfw # 收集 tag:chn 域名的 IP (可选) add-tagchn-ip chnip,chnip6 # dns 缓存 cache 4096 cache-stale 86400 cache-refresh 20 # 详细日志 # verbose ```


DNS 转发器

# 127.0.0.1:53(udp/tcp) => 1.1.1.1(udp/tcp/tls)
# 允许指定任意多个 upstream,多次给出 -c 选项即可
# 使用 -d gfw 也是一样的,只不过将 -c 改为 -t 选项
chinadns-ng -b 127.0.0.1 -l 53 -d chn -c 1.1.1.1
chinadns-ng -b 127.0.0.1 -l 53 -d chn -c udp://1.1.1.1
chinadns-ng -b 127.0.0.1 -l 53 -d chn -c tcp://1.1.1.1
chinadns-ng -b 127.0.0.1 -l 53 -d chn -c tls://1.1.1.1

命令选项

点我展开所有选项

```console $ chinadns-ng --help usage: chinadns-ng . the existing options are as follows: -C, --config format similar to the long option -b, --bind-addr listen address, default: 127.0.0.1 -l, --bind-port listen port number, default: 65353 -c, --china-dns china dns server, default: <114 DNS> -t, --trust-dns trust dns server, default: -m, --chnlist-file path(s) of chnlist, '-' indicate stdin -g, --gfwlist-file path(s) of gfwlist, '-' indicate stdin -M, --chnlist-first match chnlist first, default gfwlist first -d, --default-tag chn or gfw or or none(default) -a, --add-tagchn-ip [set4,set6] add the ip of name-tag:chn to ipset/nftset use '--ipset-name4/6' setname if no value -A, --add-taggfw-ip add the ip of name-tag:gfw to ipset/nftset -4, --ipset-name4 ip test for tag:none, default: chnroute -6, --ipset-name6 ip test for tag:none, default: chnroute6 if setname contains @, then use nftset format: family_name@table_name@set_name --group define rule group: {dnl, upstream, ipset} --group-dnl domain name list for the current group --group-upstream upstream dns server for the current group --group-ipset add the ip of the current group to ipset -N, --no-ipv6 [rules] rule: tag:, ip:china, ip:non_china if no rules, then filter all AAAA queries --filter-qtype filter queries with the given qtype (u16) --cache enable dns caching, size 0 means disabled --cache-stale use stale cache: expired time <= N(second) --cache-refresh pre-refresh the cached data if TTL <= N(%) --cache-nodata-ttl TTL of the NODATA response, default is 60 --cache-ignore ignore the dns cache for this domain(suffix) --cache-db dns cache persistence (from/to db file) --verdict-cache enable verdict caching for tag:none domains --verdict-cache-db verdict cache persistence (from/to db file) --hosts [path] load hosts file, default path is /etc/hosts --dns-rr-ip = define local resource records of type A/AAAA --cert-verify enable SSL certificate validation, default: no --ca-certs CA certs path for SSL certificate validation --no-ipset-blacklist add-ip: don't enable built-in ip blacklist blacklist: 127.0.0.0/8, 0.0.0.0/8, ::1, :: -o, --timeout-sec response timeout of upstream, default: 5 -p, --repeat-times num of packets to trustdns, default:1, max:5 -n, --noip-as-chnip allow no-ip reply from chinadns (tag:none) -f, --fair-mode enable fair mode (nop, only fair mode now) -r, --reuse-port enable SO_REUSEPORT, default: -v, --verbose print the verbose log, default: -V, --version print `chinadns-ng` version number and exit -h, --help print `chinadns-ng` help information and exit bug report: https://github.com/zfl9/chinadns-ng. email: zfl9.com@gmail.com (Otokaze) ```


config

bind-addr、bind-port

china-dns、trust-dns

上游服务器的地址格式

chnlist-file、gfwlist-file、chnlist-first

default-tag

add-tagchn-ip、add-taggfw-ip

ipset-name4、ipset-name6

ipset/nftset 相关说明

相关配置/命令行选项:

ipset 相关说明:

nftset 相关说明:

group、group-*

以配置文件举例:

# 声明自定义组 "foo"
group foo
group-dnl foo.txt
group-upstream 1.1.1.1,8.8.8.8
group-ipset fooip,fooip6

# 声明自定义组 "bar"
group bar
group-dnl bar.txt
group-upstream 192.168.1.1
# 没有 group-ipset,表示不需要 add ip

no-ipv6

filter-qtype

cache、cache-*

verdict-cache

hosts、dns-rr-ip

cert-verify、ca-certs

no-ipset-blacklist

其他杂项配置

域名列表

文件格式

域名列表是一个纯文本文件(不支持注释),每行都是一个 域名后缀,如baidu.comwww.google.comwww.google.com.hk,不要以.开头或结尾,出于性能考虑,域名label数量做了人为限制,最多只能4个,过长的会被截断,如test.www.google.com.hk截断为www.google.com.hk


加载顺序

所有组的域名列表都被 加载 到同一个数据结构,一个 域名后缀 一旦被加载,其内部属性就不会被修改。因此,当一个 域名后缀 存在于多个组的域名列表时,优先加载的那个组将“获胜”。举个例子:假设 foo.com 同时存在于 tag:chn、tag:gfw 组的域名列表内,且优先加载 tag:gfw 组,则 foo.com 属于 tag:gfw 组。

先加载“自定义组”的域名列表,然后再加载“内置组”的域名列表(chn 和 gfw 谁先,取决于--chnlist-first)。


匹配顺序

收到 dns query 时,会对 qname 进行 最长后缀匹配。举个例子,若 qname 为 x.y.z.d.c.b.a,则匹配顺序为:

一旦其中某个 域名后缀 匹配成功,匹配就结束,并获取该 域名后缀 所属的 tag(group),并将 tag 信息记录到该 dns query 的相关数据结构,后续所有逻辑(分流、ipset/nftset)都基于这个 tag 信息,与 qname 无关。

如果都匹配失败,则该 dns query 的 tag 被设为 default-tag 选项的值,默认情况下,default-tag 是 none。global 分流、gfwlist 分流都基于此机制实现。你可以将 default-tag 设为不同的 tag,来实现各种目的。


性能、内存开销

chinadns-ng 在编码时特意考虑了性能和内存占用,并进行了深度优化,因此不必担心查询效率和内存开销。域名条目数量只会影响一点儿内存占用,对查询速度没影响,也不必担心内存占用,这是在Linux x86-64的实测数据:

简单测试

导入 chnroute/chnroute6 大陆 IP 数据库(用于 tag:none 域名的 IP 测试):

# ipset (chnroute, chnroute6)
ipset -R <res/chnroute.ipset
ipset -R <res/chnroute6.ipset

# nftset (inet@global@chnroute, inet@global@chnroute6)
nft -f res/chnroute.nftset
nft -f res/chnroute6.nftset

# 只需导入一次,除非对应集合已从内核移除(比如重启了)

运行 chinadns-ng,我自己配了全局透明代理,所以访问 8.8.8.8 会走代理。

# ipset
chinadns-ng -m res/chnlist.txt -g res/gfwlist.txt -v

# nftset
chinadns-ng -m res/chnlist.txt -g res/gfwlist.txt -v -4 inet@global@chnroute -6 inet@global@chnroute6

chinadns-ng 默认监听 127.0.0.1:65353,可以给 chinadns-ng 加上 -v 参数,使用 dig 测试,观察其日志。

常见问题

tag:chn、tag:gfw、tag:none 是指什么

这是 chinadns-ng 对域名的一个简单分类:

域名分流ipset/nftset 的核心流程,可以用这几句话来描述:

tag:chntag:gfw自定义组不存在任何判定/过滤;tag:none的判定/过滤也仅限于 china 上游的响应结果


如何以守护进程形式在后台运行 chinadns-ng

# 纯 shell 语法:
(chinadns-ng 参数... </dev/null &>>/var/log/chinadns-ng.log &)

# 也可借助 systemd 的 service 来实现,此处不展开叙述

如何更新 chnroute.ipset、chnroute6.ipset

cd res
./update-chnroute.sh
./update-chnroute6.sh
ipset -F chnroute
ipset -F chnroute6
ipset -R -exist <chnroute.ipset
ipset -R -exist <chnroute6.ipset
# 支持运行时重载 ipset/nftset,无需操作 chinadns-ng 进程

如何更新 chnroute.nftset、chnroute6.nftset

cd res
./update-chnroute-nft.sh
./update-chnroute6-nft.sh
nft flush set inet global chnroute
nft flush set inet global chnroute6
nft -f chnroute.nftset
nft -f chnroute6.nftset
# 支持运行时重载 ipset/nftset,无需操作 chinadns-ng 进程

如何更新 chnlist.txt、gfwlist.txt

cd res
./update-chnlist.sh
./update-gfwlist.sh
# pkill chinadns-ng # 关闭旧的 chinadns-ng 进程
chinadns-ng -m chnlist.txt -g gfwlist.txt 其他参数... # 重新运行 chinadns-ng

如何使用 TCP 协议与 DNS 上游进行通信

从 2024.03.07 版本开始,有以下更改:


为什么不内置 TCPDoT、DoH 等协议的支持

我想让代码保持简单,只做真正必要的事,其他事让专业工具去做。

换句话说,让程序保持简单和愚蠢,只做一件事,并认真做好这件事。


chinadns-ng 并不读取 chnroute.ipset、chnroute6.ipset

启动时也不会检查这些 ipset/nftset 集合是否已存在,程序只在收到来自国内上游的 DNS 响应时(仅针对 tag:none 域名),通过 netlink 查询给定 ipset/nftset 集合,对应 IP 是否存在。这种机制使得我们可以在 chinadns-ng 运行时直接更新 chnroute、chnroute6 列表,它立即生效,不需要重启 chinadns-ng。

只有 tag:none 域名存在 ipset/nftset 判断&&过滤。


接受 china 上游返回的 IP为保留地址 的解析记录

将对应的地址(段)加入到 chnroutechnroute6 集合即可。chinadns-ng 判断是否为"大陆 IP"的核心依据就是查询 chnroute、chnroute6 集合,程序内部并没有其他隐含的判断规则。

注意:只有 tag:none 域名需要这么做;对于 tag:chn 域名,chinadns-ng 只是转发,不涉及 ipset/nftset 判定;所以你也可以将相关域名加入到 chnlist.txt 列表(支持从多个文件加载域名列表)。

为什么没有默认将保留地址加入 chnroute*.ipset/nftset?因为我担心 GFW/ISP 会给受污染域名返回保留地址,所以没放到 chnroute 去。不过现在受污染域名都走 gfwlist.txt 机制了,只会走 trust 上游,加进去应该是没问题的。


received an error code from kernel: (-2) No such file or directory


trust上游存在一定的丢包,怎么缓解

推荐方法2,因为 QoS 等因素,TCP 流量的优先级通常比 UDP 高,且 TCP 本身就提供丢包重传等机制,比重复发包策略更可靠。另外,很多代理程序的 UDP 实现效率较低,很有可能出现 TCP 查询总体耗时低于 UDP 查询的情况。


为何选择 ipset/nftset 来处理 chnroute 查询

因为使用 ipset/nftset 可以与 iptables/nftables 规则共用一份 chnroute;达到联动的效果。


是否打算支持 geoip.dat 等格式的 chnroute

目前没有这个计划,因为 chinadns-ng 通常与 iptables/nftables 一起使用(配合透明代理),若使用非 ipset/nftset 实现,会导致两份重复的 chnroute,且无法与 iptables/nftables 规则实现联动。


是否打算支持 geosite.dat 等格式的 gfwlist/chnlist

目前没有这个计划,这些二进制格式需要引入 protobuf 等库,我不想引入依赖,而且 geosite.dat 本身也很大。


chinadns-ng 也可用于 gfwlist 透明代理分流

# 创建 ipset,用于存储 tag:gfw 域名的 IP (nftset 同理)
ipset create gfwlist hash:net family inet # ipv4
ipset create gfwlist6 hash:net family inet6 # ipv6

# 指定 gfwlist.txt,default-tag,add-taggfw-ip 选项
chinadns-ng -g gfwlist.txt -d chn -A gfwlist,gfwlist6

传统上,这是通过 dnsmasq 来实现的,但 dnsmasq 的 server/ipset/nftset 功能不擅长处理大量域名,影响性能,只是 gfwlist.txt 域名数量比 chnlist.txt 少,所以影响较小。如果你在意性能,如低端路由器,可使用 chinadns-ng 来实现。


使用 chinadns-ng 替代 dnsmasq 的注意事项

chinadns-ng 2.0 已经足以替代经典用例下的 dnsmasq:

对于路由器等场景,你可能仍然需要 dnsmasq 的 DHCP 等功能,这种情况下,建议关闭 dnsmasq 的 DNS:


--noip-as-chnip 选项的作用

此选项只作用于 tag:none 域名 && qtype=A/AAAA && china 上游,trust 上游不存在过滤。

chinadns-ng 对 tag:none 域名的 A/AAAA 查询有特殊处理逻辑:对 china 上游返回的 reply 进行 ip test (chnroute),如果测试结果是 china IP,则采纳 china 上游的结果,否则采纳 trust 上游的结果(为了减少重复判定,可启用 verdict-cache 来缓存该测试结果)。

要进行 ip test,显然要求 reply 中有 IP 地址;如果没有 IP(如 NODATA 响应),就没办法 test 了。

默认拒绝 china 上游的 no-ip 结果是为了减少 GFW/ISP 污染,防止其故意对某些域名返回空 answer (no-ip)。


如何以普通用户身份运行 chinadns-ng

向内核查询 ipset/nftset 需要 CAP_NET_ADMIN 权限,使用非 root 用户身份运行 chinadns-ng 时将产生 Operation not permitted 错误。解决方法有很多,这里介绍其中一种:

# 授予 CAP_NET_ADMIN 权限
# 用于执行 ipset/nftset 操作
sudo setcap cap_net_admin+ep /usr/local/bin/chinadns-ng

# 授予 CAP_NET_ADMIN + CAP_NET_BIND_SERVICE 权限
# 用于执行 ipset/nftset 操作、监听小于 1024 的端口
sudo setcap cap_net_bind_service,cap_net_admin+ep /usr/local/bin/chinadns-ng