使用Technitium DNS Server自建公网DNS服务器

该文章只探讨技术实现,具体能否使用请遵守当地法律法规

背景

中国大陆的所有正规运行的公共DNS服务器,只要后端处于大陆境内,则必然有DNS污染,并且这个污染可能是地域性的,即部分省市运营商DNS实现的污染。而公共加密DNS污染程度可能会小点,但仍存在污染可能性,例如当年的dockerhub污染是全国范围内的污染。

理论上依靠境内外分流可以实现去污染,即使用代理软件分流DNS请求,境内DNS只负责解析已知境内域名(可依靠geosite:cn实现),已知被完全阻断的域名通过远端节点解析,未知的域名由境内外并发比对geoip:cn判断对端是否是国内服务器或直接采用境外服务器解析实现。

但为了防止DNS泄露或实现其他目的,自建DNS并非一个坏选择。但自建DNS需要注意在部分国家或地区向外提供DNS解析是非法的,因此53端口的传统DNS和853端口的DoT或DoQ并非是个好选择,前者是明文传输更容易被劫持篡改,后者端口特征明显非常容易被阻断。

由此背景,我们可知,如果想自建公网DNS服务器,最好选用DoH这种毫无特征的加密请求,因为从第三者看来,DoH的查询请求与普通的网站访问无异。

Technitium服务器的安装

服务器选型

硬件资源

其实Technitium DNS Server服务器的资源需求并不高,以我个人的实践看来,1核心1GiB内存20GiB硬盘足够搭建一个Technitium DNS Server了,甚至你想还能再套娃一个MosDNS在上游分流,而且完美支持ECS。

网络资源

网络最好是三网优化线路,比如高富帅专用三网CN2GIA。当然如果你个人使用的网络运营商比较单一,那使用单网优化线路也可以,例如联通可以选用日本软银或9929乃至4837线路,移动可选用CMIN2。至于流量一个月500GiB左右差不多了,除非你还有其他用途,只用作个人DNS一个月500GiB绰绰有余。

安装前准备

个人选用的是Debian12最小化安装,相比Ubuntu更节省资源。

如果使用RedHat系Linux可参考步骤和思路,命令除了防火墙一般大差不差。

开启BBR+FQ

BBR能有效提升网络吞吐量并增强高延迟下TCP的表现。

Debian12内核默认带有BBR但是没有开启,通过以下命令开启BBR:

echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf

echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf

通过sysctl -p应用变更。

root@dns:~# sysctl -p
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

通过lsmod | grep bbr验证变更是否生效。

root@dns:~# lsmod | grep bbr
tcp_bbr                20480  47

放行防火墙

Debian12默认未安装防火墙,但是强烈建议安装一个,系统防火墙是设备的最后一道防线,希望各位对自己的设备负责。当然你就是不想装或者像国内云有面板防火墙那就跳过下一个吧

Debian12安装ufw防火墙:

root@dns:~# apt install -y ufw

ufw防火墙放通必要端口:

root@dns:~# ufw allow 22/tcp
Rules updated
Rules updated (v6)
root@dns:~# ufw allow 80/tcp
Rules updated
Rules updated (v6)
root@dns:~# ufw allow 443
Rules updated
Rules updated (v6)

请确保ssh端口放开后再启用ufw防火墙:

root@dns:~# ufw enable 
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

查看ufw防火墙状态:

root@dns:~# ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
80/tcp                     ALLOW       Anywhere                  
443                        ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)             
80/tcp (v6)                ALLOW       Anywhere (v6)             
443 (v6)                   ALLOW       Anywhere (v6)             

安装步骤

说实话不是很想重复造轮子了,可参考我另外一篇文章进行安装和初始化。

如果使用docker安装初学者推荐host模式安装,与宿主机共享网络栈会减少很多问题,老手随意。

安装完毕后,会提示你使用http://vps_ip:5380/进行登入,但是此时应该是无法登入的,因为被系统防火墙拦截了。

此时你需要一个中间件进行反向代理,推荐NginxProxyManager或者Caddy。

域名解析记录配置

请先在DNS托管平台增加以下的DNS配置:

  • dns.xxx.com IN A VPS_IP

  • nginx.xxx.com IN A VPS_IP(仅NginxProxyManager需要)

中间件配置

Caddy

Debian源中默认带有caddy,可以使用apt install -y caddy直接安装:

root@dns:~# apt update
…………
…………
root@dns:~# apt install -y caddy
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
caddy is already the newest version (2.9.1).

使用nano编辑caddy配置文件,添加以下配置:

root@dns:~# nano /etc/caddy/Caddyfile

dns.xxx.com {
        log {
                output file /var/log/caddy/dns.xxx.com/access.log {
                        roll_size 100MiB
                        roll_local_time
                        roll_keep 10
                        roll_keep_for 2160h
                }
        }
        encode gzip zstd
        header {
                Strict-Transport-Security "max-age=31536000; preload"
        }
        @static {
                path /js/* /css/* /img/* /images/* /fonts/* /static/* /favicon.ico
        }
        @api {
                path /api/*
        }
        @myself_query {
                path /test-query*
        }
        @dns_query {
                path /dns-query*
        }

        handle @test_query {
                rewrite * /dns-query
                reverse_proxy 127.0.0.1:8880 {
                        header_up Host {upstream_hostport}
                        header_up X-Real-IP {remote_host}
                }
        }

        handle @dns_query {
                respond "Not Found" 404 {
                        close
                }
        }

        handle_path /dashboard* {
                reverse_proxy 127.0.0.1:5380 {
                        header_up Host {upstream_hostport}
                        header_up X-Real-IP {remote_host}
                }
        }

        handle @static {
                reverse_proxy 127.0.0.1:5380 {
                        header_up Host {upstream_hostport}
                        header_up X-Real-IP {remote_host}
                }
        }

        handle @api {
                reverse_proxy 127.0.0.1:5380 {
                        header_up Host {upstream_hostport}
                        header_up X-Real-IP {remote_host}
                }
        }

        handle {
                respond "Not Found" 404 {
                        close
                }
        }
}

解释一下代码,上述的配置文件将访问后缀/dns-query的请求和根路径/的请求全部返回404,以防止针对DNS服务器的主动探测,如果不加这两条配置,很可能被他人主动探测出来。我知道这个配置代码写的很shit,但是能用就行了。

配置修改完后使用systemctl reload caddy重载配置后即可生效。

在上述的配置中后续要访问Technitium DNS Server的WebUI需要使用https://dns.xxx.com/dashboard来进行访问,而DoH请求要发送到https://dns.xxx.com/test-query才能正常解析。

NginxProxyManager

NginxProxyManager推荐使用docker compose安装,docker compose配置文件如下:

services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    network_mode: "host"
    #ports:
    #  - '80:80'
    #  - '81:81'
    #  - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

创建docker-compose.yaml文件后使用docker compose up -d命令启动容器:

root@gc:~/docker/nginxproxymanager# docker compose up -d
[+] Running 1/1
 ✔ Container nginxproxymanager-app-1  Started                                                           0.2s 

使用netstat -ntlp命令查看监听情况:

root@gc:~/docker/nginxproxymanager# netstat -ntlp | grep 443
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      3869096/nginx: mast 
tcp6       0      0 :::443                  :::*                    LISTEN      3869096/nginx: mast

此时需要使用ufw allow 81/tcp放通81端口,使用http://vps_ip:81的方式访问WebUI,默认用户名是[email protected],默认密码是changeme

登入后会要求你更改用户名和密码,修改完毕后将进入以下界面。

请先添加一个针对NginxProxyManager本身的反代,点击Hosts-Proxy Hosts-Add Proxy Host

反代配置参考如下,点击save后会自动申请证书:

等待反代配置生效后,尝试访问https://nginx.xxx.com/测试能否访问NginxProxyManager的WebUI,能访问即可ufw delete allow 81/tcp取消防火墙配置。

通过账户名密码登入后继续按照下图添加反代配置,并且在Advanced中添加下列代码:

        location = / {
            return 404;
        }

        # myself-query 配置
        location /test-query {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://127.0.0.1:8880/dns-query;
        }

        # pannel 配置
        location /dashboard/ {
            proxy_pass http://127.0.0.1:5380/; # 自定义修改
            #proxy_redirect / /pannel/;    # 同步修改uri
            #proxy_cookie_path / /pannel/;    # 同步修改uri
            proxy_set_header Host $host;
            proxy_ssl_server_name on;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Host $host;
            proxy_set_header X-Forwarded-Server $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # 按需配置
            client_max_body_size 1m; 
            client_body_buffer_size 256k;
            client_body_timeout 60s;
            send_timeout 60s;
            proxy_connect_timeout 60s; 
            proxy_read_timeout 60s; 
            proxy_send_timeout 60s;
            proxy_buffer_size 32k; 
            proxy_buffers 4 16k; 
            proxy_busy_buffers_size 32k;
            proxy_temp_file_write_size 32k; 
            proxy_ignore_client_abort on; 
        }

        # api 配置
        location /api {
            proxy_pass http://127.0.0.1:5380/api;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # robots.txt 配置
        location = /robots.txt {
            default_type text/html;
            add_header Content-Type "text/plain; charset=UTF-8";
            return 200 "User-Agent: *\nDisallow: /";
        }

        # 禁止扫描探测隐藏文件
        location ~ /\.(?!well-known) {
            deny all;
        }

此时可以通过https://dns.xxx.com/dashboard 来访问Technitium DNS Server的WebUI了。

Technitium DNS Server具体配置

大致的配置步骤与文章基本类似,在上游添加8.8.8.8的DoT和9.9.9.11的DoT即可正常使用,但仍然需要注意以下配置:

(推荐)打开ECS支持

上游选用8.8.8.8和9.9.9.11(不可选用9.9.9.9)的理由就是因为这两个DNS拥有完整的ECS支持,即Technitium DNS Server会将请求携带的ECS信息透传至上游,上游将根据ECS信息进行最优的DNS解析回复,如果你的ECS是在大陆的,就会给你解析回复大陆IP,如果在境外则解析回复境外IP,这个功能对在境内没有后端的DNS提升解析境内域名准确性有极大帮助。其他的例如CloudFlare DNS不支持ECS,OpenDNS对于ECS的支持也并不完全,只根据请求的来源IP进行ECS缓存而忽视透传的ECS信息。

对于ECS支持的测试,V2ex中有大佬做过完整测试,可以参考该文章

Technitium DNS Server可以在Settings-General中打开EDNS Client Subnet (ECS)支持,IPv4前缀长度和IPv6前缀长度保持默认即可,/24和/56保证了隐私性的同时也保证了准确性。

(必须)启用DNS-over-HTTP

有心急的小伙伴在服务启动后就尝试使用DoH进行递归解析了,但是这个时候应该是不通的,因为DNS-over-HTTP是非加密协议默认不开启。

Settings-Optional Protocols中必须勾选Enable DNS-over-HTTP,随后修改DNS-over-HTTP Port8880,当然你也可以修改到其他端口,但反代配置也需要配合修改。

(必须)添加DNS-over-HTTP的ACL

在上述配置添加完后,DNS-over-HTTP协议已经启用了,但是依旧无法通过https://dns.xxx.com/test-query递归域名解析,是因为必须将127.0.0.1放入Reverse Proxy Network ACL中,不然连VPS本地都无法访问DNS-over-HTTP协议。

Settings-Optional Protocols中,将127.0.0.1填入Reverse Proxy Network ACL即可。

(必须)允许公网递归

必须打开公网递归功能才能正常从公网解析域名,不然请求会被DROP掉。

Settings-Recursion中选择Allow Recursion 并保存即可。