校园网/企业网的 DPI 设备通常不会放过 UDP 53 端口的明文 DNS 查询。这篇教程教你用 非标准端口 + DoH 加密 把 DNS 流量伪装成普通 HTTPS,让运营商 DPI 彻底认不出。
架构一览
客户端 → smartdns(:53) → DoH TLS :8443 → 云服务器 dnsproxy → 223.5.5.5
↑
非标准端口
绕过了 DPI 的 53/853 端口特征库
核心思路三句话:
- 端口伪装:不用 53(明文 DNS)也不用 853(DoT 标准端口),用
8443——看起来就是普通 HTTPS - 内容加密:DoH 把 DNS 查询装进 HTTP/2 TLS 隧道,抓到包也只能看到加密载荷
- IP 无特征:云服务器 IP 不在任何 DNS 特征库里,DPI 不认识这个地址
第一步:在云服务器上部署 dnsproxy
1.1 下载 dnsproxy
# 云服务器 (本例:阿里云 ECS, Ubuntu 26.04)
mkdir -p /usr/local/dnsproxy
cd /usr/local/dnsproxy
# 下载预编译二进制 (AdGuardTeam/dnsproxy)
wget https://github.com/AdguardTeam/dnsproxy/releases/download/v0.73.4/dnsproxy-linux-amd64-v0.73.4.tar.gz
tar xzf dnsproxy-linux-amd64-*.tar.gz
1.2 生成自签名证书
⚠️ 自签名证书的 CN 用 IP 地址,不是域名。
mkdir -p /etc/dnsproxy/ssl
cd /etc/dnsproxy/ssl
# 生成 CA 私钥
openssl genrsa -out ca.key 2048
# 生成自签名 CA 证书(CN 填服务器公网 IP)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN="
# 生成服务端证书(同样用 IP)
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN="
# 签发
openssl x509 -req -days 3650 -in server.csr \
-CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
# 合并证书链
cat server.crt ca.crt > cert.pem
1.3 创建 systemd 服务
cat > /etc/systemd/system/dnsproxy.service << 'EOF'
[Unit]
Description=DNS Proxy (DoH)
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/dnsproxy/dnsproxy \
--https-port=8443 \
--tls-crt=/etc/dnsproxy/ssl/cert.pem \
--tls-key=/etc/dnsproxy/ssl/server.key \
--port=5353 \
--listen=0.0.0.0 \
--upstream=223.5.5.5 \
--upstream=119.29.29.29 \
--bootstrap=223.5.5.5
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now dnsproxy
参数说明:
--https-port=8443:DoH 监听端口(非标,避开 DPI)
--port=5353:本地 DNS 转发端口(不对外)
--upstream:上游 DNS,阿里 + DNSPod 双保险
--bootstrap:启动时解析 bootstrap DNS 用的初始上游
1.4 开放防火墙
# UFW
ufw allow 8443/tcp
ufw reload
# 或者 iptables
iptables -A INPUT -p tcp --dport 8443 -j ACCEPT
1.5 验证 DoH 端点
# 从任意能访问公网的机器测试
curl -sk "https://:8443/dns-query?name=baidu.com&type=A"
# 应返回 DNS JSON 响应
# 或测试 POST 格式(RFC 8484)
curl -sk -H "Content-Type: application/dns-message" \
"https://:8443/dns-query?dns=AAABAAABAAAAAAAABGJhaWQDY29tAAABAAE"
第二步:在 OpenWrt 软路由上配置 smartdns
2.1 安装 smartdns
# OpenWrt 软路由 (SSH)
opkg update
opkg install luci-app-smartdns
# 自动依赖 smartdns
2.2 停掉 AdGuardHome(如有)
⚠️ AdGuardHome 是 Go 写的,不认系统 CA 证书。自签名证书 + Go 程序 = 一定翻车。用 C 写的 smartdns 替代。
/etc/init.d/adguardhome stop
/etc/init.d/adguardhome disable
2.3 配置 smartdns 上游
编辑 /etc/smartdns/custom.conf(不是 UCI!UCI 改了可能不生效):
# ===== 主上游:自建 DoH 服务器 =====
server-https https://:8443/dns-query \
-no-check-certificate \
-host-name cloud-dns.aliyun.com
# ===== 备用上游:公共 DoH =====
server-https https://dns.alidns.com/dns-query \
-host-name www.aliyun.com
server-https https://doh.pub/dns-query \
-host-name www.dnspod.cn
# ===== 优化参数 =====
speed-check-mode ping,tcp:443
cache-size 4096
prefetch-domain yes
serve-expired yes
force-AAAA-SOA yes
dualstack-ip-selection yes
rr-ttl-min 60
edns-client-subnet 0.0.0.0/0
关键参数:
-no-check-certificate:跳过自签名证书验证(smartdns 支持这个,不像 Go 程序那样认死理)
-host-name:伪装 TLS SNI 为阿里云/CDN 域名,DPI 看到的是cloud-dns.aliyun.com而不是你的 IP
2.4 重启生效
/etc/init.d/smartdns restart
# 测试
nslookup baidu.com 127.0.0.1
第三步:验证与加固
3.1 确认 DNS 走 DoH 而非明文
# 抓 WAN 口的 UDP 53 包(DNS 明文)
tcpdump -i eth1 -c 5 "udp port 53"
# 如果输出为空 → 全走 DoH 加密 ✅
# 如果有包 → 存在明文泄露,检查 smartdns 配置
3.2 确认 DoH 连接正常
# 抓 WAN 口到云服务器的 TLS 流量
tcpdump -i eth1 "host and port 8443"
# 触发几次 DNS 查询
nslookup baidu.com 127.0.0.1
nslookup qq.com 127.0.0.1
# 应该看到 TLS 握手 + 加密数据传输
3.3 持久化:确保自启
# smartdns 开机自启
/etc/init.d/smartdns enable
# dnsmasq 只做 DHCP(端口 5353,不处理上游 DNS)
uci set dhcp.@dnsmasq[0].port='5353'
uci commit dhcp
/etc/init.d/dnsmasq restart
第四步:可选——加上 DPI 流量混淆
DoH 保护了 DNS 查询内容,但 TLS SNI 仍然是明文的。ISP 的 DPI 设备可以知道你连接了哪个 IP(虽然不知道查询内容)。
如果网络环境特别严苛,可以在软路由上部署 nfqws(zapret 工具链)对 TCP 流量做 DPI 混淆:
# nfqws 示例策略(TLS split + 随机 TTL)
nfqws --qnum=100 \
--filter-l7=tls \
--dpi-desync=split2 \
--dpi-desync-fooling=md5sig \
--dpi-desync-autottl=1:3-30
# iptables 劫持流量到 NFQUEUE
iptables -t mangle -I FORWARD 1 -p tcp --dport 443 \
-j NFQUEUE --queue-num 100 --queue-bypass
详细编译部署步骤见《DNS 安全升级 + DPI 对抗实录》。
踩坑记录
坑 #1:Go 程序 + 自签名证书 = 翻车
AdGuardHome、早期版本 dnsproxy 等 Go 写的 DNS 客户端不走系统 CA 路径。即使把自签名证书加到 /etc/ssl/certs/ 并运行 update-ca-certificates,Go 的 TLS 栈依然报 bad certificate。解决方案:换 C 写的(smartdns),或使用 -no-check-certificate 选项。
坑 #2:dnsproxy 只接受 POST
浏览器地址栏直接访问 https://IP:8443/dns-query?name=baidu.com 会返回 400 Bad Request。这不代表服务挂了——dnsproxy 的 DoH 端点只接受 application/dns-message 格式的 POST 请求(RFC 8484),浏览器 GET 自然不行。
坑 #3:OpenWrt 的 busybox grep 没有 -P
如果在 OpenWrt 上写脚本解析路由表,不要用 grep -oP(Perl 正则),用 awk 代替。BusyBox 的 grep 不支持 PCRE。
坑 #4:校园网 DHCP 可能推送多个死网关
部分校园网 DHCP 会同时推送一个可用网关和一个不可达的备用网关。Linux 会在两者间负载均衡,导致一半流量丢包。需要写 hotplug 脚本在 WAN 上线后清理 ARP 不可达的网关。用 awk 解析路由,不要用 grep -P。
总结
这套方案的核心优势:
- 零额外成本:一台已有的云服务器即可,dnsproxy 内存不到 20MB
- 完全绕过 DPI DNS 监控:端口、内容、IP 三重伪装
- 高可用:smartdns 多上游并发查询 + 自动故障切换
- 无法律风险:不涉及 VPN/翻墙,纯 DNS 加密
唯一的前提:有一台能跑 dnsproxy 的云服务器。