附:zapret 编译血泪史与 DPI 对抗工具链部署
零、敌情侦查:学校网络到底在干什么
先抛一个问题:当你在宿舍打开浏览器输入 bilibili.com 时,学校网络能知道你去了哪吗?
答案是:能,而且比你想象的详细。
第一招:DNS 劫持与重定向
校园网的标准配置里,DNS 服务器通常被 DHCP 强制指定为学校的递归 DNS(或者更直接的——在出口路由器上把 UDP 53 端口全部劫持重定向)。效果就是:
你的设备 → 学校 DNS 服务器 → 记录日志 → 上游查询 → 返回结果
你解析过的每一个域名,学校都有完整的日志:几点几分、哪个 IP、查了什么域名。不需要"监控"你,DNS 日志就是天然的上网行为画像。
更阴险的玩法是 DNS 污染——对特定域名返回假 IP,或者干脆返回空结果,让你以为"这个网站打不开"。你没报错,学校也没封你,只是你永远连不上。
第二招:DPI 流量指纹
DNS 日志告诉你"查了什么",但深度包检测(DPI)告诉你"在干什么"。
TLS 加密让 DPI 看不到 HTTP 请求的具体内容(URL、Cookie、POST 参数),但它能看到:
| DPI 能看到的 | 含义 |
|---|---|
| TLS SNI | ClientHello 中明文的域名(如 bilibili.com) |
| 目标 IP | 你连接的是哪个服务器 |
| 数据包大小/时序 | 流量模式指纹(视频流?API 调用?大文件下载?) |
| 连接持续时间 | 你在某个服务上停留了多久 |
| TLS 指纹(JA3/JA4) | 你用的什么客户端(浏览器类型、Python/Go 脚本等) |
组合起来能做很多事:
- 识别代理/VPN 工具:Shadowsocks、V2Ray、Trojan 的 TLS 指纹与正常浏览器完全不同
- 识别翻墙行为:连接到已知境外 IP 或 CDN 的模式特征
- 流量限速/阻断:检测到"视频流模式"→ 限速;检测到"VPN 模式"→ 直接 RST 断连
- 行为画像:几点到几点在看视频、几点在刷社交媒体、几点访问了"敏感"站点
第三招:IP 黑名单 + SNI 阻断
即使 DNS 换了,直接访问 IP 呢?
运营商的 DPI 设备维护着庞大的 IP 和 SNI 特征库。连接到某些 IP 段或 TLS 握手中出现某些 SNI 值时,直接在 TCP 层注入 RST 包强制断连。你连 TCP 三次握手都完成了,数据传输刚开始,一个 RST 过来全断了。
这就是为什么有时候同一个域名,有时能开有时不能、WiFi 能开流量不能——因为触发了不同网络路径上 DPI 设备的规则。
不防御的隐患
如果什么都不做,校园网环境下的真实处境:
┌──────────────────────────────────────────────┐
│ 学校 DPI 设备 │
│ │
│ 你的 DNS 查询日志 ← 永久留档 │
│ 你的 TLS SNI ← 明文可见 │
│ 你的连接目标 IP ← NAT 层必然暴露 │
│ 你的流量模式指纹 ← 可分类/识别/阻断 │
│ 你的客户端 TLS 指纹 ← 可识别工具类型 │
│ │
└──────────────────────────────────────────────┘
这不是在说"有人在看你"——这本身就是校园网基础设施的标准功能。你不上网则已,上网就必然经过这套系统。
更高阶的防御手段(简介)
本文的核心方案是 DNS 加密 + DPI 流量混淆,刚好覆盖了前三层的防御。但往上还有更激进的方案:
| 层级 | 方案 | 覆盖范围 | 难度 |
|---|---|---|---|
| DNS | DoH/DoT 加密 | 域名查询内容 | ⭐ 低(本文已实施) |
| DPI 指纹 | nfqws/zapret 流量混淆 | TLS 握手特征 | ⭐⭐ 中(本文已编译部署) |
| SNI 加密 | ECH (Encrypted Client Hello) | TLS SNI 明文 | ⭐⭐⭐ 高(需服务端支持,国内站基本无) |
| 全流量隧道 | WireGuard/OpenVPN | 所有 TCP/UDP 流量 | ⭐⭐⭐ 高(需境外服务器 + 足够带宽) |
| 协议混淆 | V2Ray XTLS/VLESS + 伪装 | 深度隐蔽 | ⭐⭐⭐⭐ 很高(法律风险,本文不涉及) |
| 物理隔离 | 独立 4G/5G 热点 | 完全绕过校园网 | 💰 取决于流量套餐 |
本文聚焦于不依赖境外服务器、不涉及法律灰色地带、在现有硬件条件下可实施的防御方案。
一、缘起:Go 语言的"倔脾气"
一切始于一条命令的失败。
在 OpenWrt 软路由上,AdGuardHome 作为主力 DNS 服务器运行了很久。它界面美观、功能齐全,唯一的毛病是:不认系统 CA 证书。
AdGuardHome 用 Go 语言编写。Go 的 TLS 证书验证走的是自己的逻辑——它内置了一套根证书,完全无视系统的 CA 存储。无论你把自签名 CA 证书放到 /etc/ssl/certs/,还是设置 SSL_CERT_FILE 环境变量,Go 程序通通看不见。之前折腾过的 dnsproxy(也是 Go)死在了同一个坑里。
这意味着:如果你的 DoH 服务端用的是自签名证书,Go 写的 DNS 工具全都连不上。
于是 DNS 查询一直以 UDP 明文飞向公共 DNS,在运营商 DPI 面前毫无遮掩。
二、换将:smartdns 上场
方案很明确:换一个走系统 CA 库的 DNS 服务器。
smartdns 是 C 语言写的,链接系统的 libcrypto/libssl,天然使用系统的 CA 证书链。而且它有一个 AdGuardHome 没有的特性:并发多上游查询,取最快响应——这种"多路归并"策略在延迟敏感的 DNS 场景下非常实用。安装很简单:
opkg install luci-app-smartdns
自动依赖安装 smartdns 1.2023.43
停掉 AdGuardHome,切换为 smartdns:
/etc/init.d/adguardhome stop
/etc/init.d/adguardhome disable
核心配置——上游 DNS 服务器(/etc/smartdns/custom.conf,非 UCI 配置):
# 主 DoH — 阿里云自建 dnsproxy(自签证书,跳过验证)
server-https https://<云服务器IP>:8443/dns-query -no-check-certificate
# 备用 DoH — 阿里公共 DNS(host-name 伪装 SNI)
server-https https://dns.alidns.com/dns-query -host-name www.aliyun.com
# 备用 DoH — DNSPod(host-name 伪装 SNI)
server-https https://doh.pub/dns-query -host-name www.dnspod.cn
# 100% DoH 加密 — 零 UDP 明文泄露
smartdns 对所有上游同时发起查询,取最快响应。三路 DoH 全部加密,-host-name 参数伪装 TLS SNI(握手中域名显示为 www.aliyun.com / www.dnspod.cn),规避 DPI 的 DoH 协议指纹。即使抓包看到 8443 端口的 TLS 流量,也无法判断是 DNS 查询。零 UDP 明文泄露。
验证流量是否加密:
tcpdump -i eth1 host <云服务器IP>
输出中看不到任何 DNS 明文——全是 TLS 加密流量,UDP 53 端口的明文字符串"baidu.com"消失了。
最后,开机自启:
/etc/rc.d/S19smartdns enable
三、架构升级:从裸奔到加密
升级前:客户端 → AdGuardHome(:53) → UDP 明文 → 公共 DNS
运营商 DPI 设备可以清清楚楚看到你在解析什么域名。
升级后: ┌─ DoH + SNI伪装 → 云服务器:8443 → 公共 DNS
客户端 → smartdns(:53) ──并发查询─┼─ DoH + SNI伪装 → dns.alidns.com
└─ DoH + SNI伪装 → doh.pub
三层防御,逐一击破运营商的 DPI 监控:
| 防御层 | 机制 | 效果 |
|---|---|---|
| 目标 IP 伪装 | 阿里云随机 IP | 学校/ISP 不认识这个地址,未被列入 DNS 特征库 |
| 端口混淆 | 8443 — 非标准 DNS 端口 | 不是 53,不是 853(DoT),DPI 认不出是 DNS |
| 内容加密 | TLS 隧道 | DNS 查询完全不可见,抓到包也只是加密载荷 |
DNS 层面,至此完全绕过 ISP DPI 的监控网。
但这才完成了一半。
四、血与泪:在 OpenWrt 上编译 nfqws
DNS 加密只是解决了"查什么域名被看到"的问题。但如果 ISP 对流量本身做深度包检测(DPI),通过 TLS SNI、数据包指纹、连接模式等识别你的流量,DNS 加密就远远不够了。
[nfqws](https://github.com/bol-van/zapret) 是目前最强的 DPI 绕过工具之一,通过 TCP 分片伪造、TLS 握手混淆、包重排等技术干扰 DPI 设备的特征匹配。但它没有现成的 OpenWrt 二进制包——需要自己编译。
目标平台:软路由,Intel Celeron N2840,OpenWrt 24.10.4。
第一回合:原生编译——希望破灭
最直觉的做法:在软路由上装 gcc,直接编译。
opkg install gcc make
cd zapret
make nfqws
22 个 .c 文件全部成功编译成 .o。一切顺利。然后——
ld: error adding symbols: file in wrong format
链接器炸了。不是某个 .o 的问题,是所有共享库都无法链接。OpenWrt 24.10.4 的 gcc 13.3.0 工具链存在一个系统性的 bug:可以编译,不能链接 .so。
第二回合:QEMU 虚拟机——同样的深渊
不死心,开一个同版本 OpenWrt 的 QEMU 虚拟机:
qemu-system-x86_64 -m 512M -hda openwrt-24.10.4.img
进到虚拟机里,opkg install gcc make,编译……同一个错误。
确认了:这不是个别现象,是 OpenWrt 官方固件中 gcc 包的结构性缺陷。包管理系统里装的 gcc 只是一个"残废版"——真正用来编译 OpenWrt 软件包的,是宿主机的交叉编译工具链(SDK),不是原生 gcc。
第三回合:musl-gcc 交叉编译——头文件地狱
把交叉编译工具链搬到笔记本上:
musl-gcc -std=gnu99 -Os -c *.c
__gnuc_va_list 冲突。glibc 和 musl 的头文件打架,互相定义同一符号。这需要在编译环境中做大量 patch 工作,不现实。
第四回合:跨机器搬运 .o——链接器不给面子
软路由上编译好的 22 个 .o 文件,scp 到 Ubuntu 笔记本上,用 musl-gcc 链接:
musl-gcc -o nfqws *.o -lnetfilter_queue -lnfnetlink
ld.bfd: error: cannot open .../libnfnetlink.so: file has no section headers
musl 的 .so 文件没有 section headers——这是 musl 的设计选择。但 GNU ld.bfd 又偏偏依赖 section headers 来解析符号表。两边的设计哲学冲突,链接器直接拒绝工作。
第五回合:静态编译——山重水复后的柳暗花明
既然动态链接和 musl 八字不合,那就绕开它——在 glibc 环境做静态编译。
Ubuntu 上没有 libnetfilter-queue 和 libnfnetlink 的静态 .a 文件,只能手动编译出静态库:
# 安装开发头文件
apt-get install libnetfilter-queue-dev libnfnetlink-dev \
libcap-dev libnftnl-dev
手动编译 libnfnetlink 静态库
cd libnfnetlink-1.0.2
./configure --enable-static && make
cp src/.libs/libnfnetlink.a /usr/local/lib/
静态编译 nfqws
cd zapret
gcc -std=gnu99 -Os -static \
-o nfqws .c crypto/.c \
-lnetfilter_queue -lnfnetlink -lz -lmnl -lnftnl
-rwxr-xr-x 1 root root 1.6M nfqws
1.6MB 的静态 ELF 二进制,拖到 OpenWrt 软路由上,直接运行。
成功了。这是一个 glibc 环境下静态编译的二进制,在 musl 系统上完整运行——静态链接让库依赖的差异完全消失,同 CPU 架构下天然跨 libc 兼容。
五条血的教训
1. OpenWrt 的 opkg install gcc 能编译不能链接,这不是 bug 是 feature——opkg 的 gcc 包就不是给你做原生编译用的。
2. 真正的包编译走 SDK 交叉工具链,在宿主机上,不是目标机上。
3. glibc 静态编译的二进制兼容 musl(同架构),这是绕过环境限制最干净的方案。
4. musl 的 .so 无 section headers,这是设计选择,但要和 GNU ld.bfd 配合就得碰壁。
5. 遇到瓶颈别死磕。第四回合失败后如果继续纠结交叉链接,可能再耗一个晚上也出不来。换个思路——静态编译——十分钟搞定。
五、工具链集结:zapret 全家桶
编译完成的成果不止一个 nfqws。zapret 是一套完整的 DPI 对抗工具集:
/etc/zapret/
├── config # 参数配置(策略、端口列表)
├── rules.sh # iptables 规则(一键启停)
/etc/init.d/zapret # OpenWrt procd 自启脚本
四个主力工具:
| 工具 | 大小 | 职责 |
|---|---|---|
| nfqws | 1.6MB | 主力 DPI 绕过,支持 fake/split/frag/desync 策略 |
| tpws | 1.5MB | 备选方案,TPROXY 透明代理 |
| mdig | 1.2MB | DNS 查询诊断工具 |
| ip2net | 990KB | IP/CIDR 范围工具 |
| blockcheck.sh | 53KB | 域名 DPI 拦截检测脚本 |
nfqws 的核心策略:
- split:将 TLS ClientHello 分成多个 TCP 段,让 DPI 无法完整抓取 SNI
- fake:在真实 ClientHello 前插入一个伪造的、无害的 ClientHello,DPI 取第一个就失效
- frag:IP 层分片,迫使 DPI 做分片重组,加大资源消耗和漏检概率
- desync:故意发送畸形数据包破坏 TCP 流的连续性,让 DPI 设备丢弃连接状态
规则部署采用安全模式——工具就位、配置写好、iptables 规则暂不激活。激活时只需两步:
# 1. 启动 nfqws 守护进程
/etc/init.d/zapret start
2. 添加 iptables NFQUEUE 规则(mangle 表 FORWARD 链)
iptables -t mangle -A FORWARD -p tcp --dport 443 \
-j NFQUEUE --queue-num 100 --queue-bypass
iptables -t mangle -A FORWARD -p udp --dport 443 \
-j NFQUEUE --queue-num 100 --queue-bypass
--queue-bypass 是关键——nfqws 进程万一挂了,数据包直接透传,网络不会断。紧急回滚只需 iptables -D 去掉这两条规则。
六、防御全景:从 DNS 到全流量
整合后的分层防御体系:
| 层面 | 方案 | 状态 |
|---|---|---|
| DNS 查询内容 | DoH 加密到云服务器 | ✅ 运行中 |
| DNS 53 端口劫持 | 8443 非标准端口绕过 | ✅ 天然免疫 |
| DPI 指纹识别 | nfqws 18重混淆 | ✅ 运行中 |
| TLS SNI 泄露 | nfqws split/fake 扰乱 | ✅ 运行中 |
| 设备指纹归一化 | MSS/QoS/TCP窗口 | ✅ 运行中 |
| 全流量隧道加密 | VPN/WireGuard 专线 | ❌ 云端带宽不够 |
整体拓扑:
┌─────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ 客户端 │───▶│ smartdns (:53) │───▶│ 三路 DoH 加密 │───▶│ 公共 DNS 上游 │
└─────────┘ │ ┌──────────────┐ │ │ 阿里云 ECS │ └─────────┘
│ │ nfqws(NFQUEUE)│ │ └──────────────┘
│ │ 18重混淆 ✅ │ │
└──────────────────┘
OpenWrt 软路由
Intel N2840
DNS 层的加密已经完成,流量混淆层全量激活。详见下一章实战验证。
七、安全部署:待激活的保险丝
nfqws 编译完成后,部署的策略是安全模式——工具就位、配置写好、但不加 iptables 规则。
/etc/zapret/
├── config # 参数配置(队列号、DPI 策略)
├── rules.sh # iptables 规则脚本(一键启停)
/etc/init.d/zapret # OpenWrt procd 自启管理
不直接激活的原因很简单:iptables NFQUEUE 规则配错一条,SSH 就可能断开——之前吃过 ebtables 锁死路由器的亏,这次必须稳一手。
激活流程设计为两步,随时可逆:
/etc/init.d/zapret start # 1. 启动 nfqws 守护进程
/etc/zapret/rules.sh start # 2. 添加 iptables 规则
/etc/zapret/rules.sh stop # 紧急回滚:移除所有规则
--queue-bypass 参数是最后的安全阀:即使 nfqws 进程崩溃,数据包也会直接透传而不丢。
八、插曲:巡检中发现的小问题
部署过程中顺便做了一次全面巡检,顺手修了三个掉线的服务:
- 笔记本 Ollama:没有自启,
systemctl start ollama恢复,3 个模型上线 - PostgreSQL 18:集群状态 down,
pg_ctlcluster 18 main start恢复 - 软路由 natfrp:内网穿透进程挂了,手动拉起
基础设施的熵增定律永恒成立——你不动它,它也会自己坏。
九、实战验证:开关按下之后
编译部署只是前半场,真正的考验是——激活后能不能扛住运营商 DPI 的正面检测?
踩坑:OpenWrt 24.10 的 iptables 陷阱
第一条 NFQUEUE 规则就炸了:
iptables -t mangle -A FORWARD -p tcp --dport 443 -j NFQUEUE --queue-num 100 --queue-bypass
iptables v1.8.10 (nf_tables): unknown option "--queue-num"
OpenWrt 24.10 默认用的是 iptables-nft(nftables 后端),不支持 NFQUEUE 扩展选项。需要额外安装 iptables-mod-nfqueue 包才能正常使用。
15 重混淆策略部署
解决语法问题后,nfqws 从最初的"空进程"升级为全策略混淆:
| 层面 | 策略 | 说明 |
|---|---|---|
| TCP 层 | badseq, badsum, datanoack, ts | 序列号/校验和/时间戳全维度混淆 |
| TLS 层 | fake, multisplit | 伪造 ClientHello + 分片传输 |
| 指纹 | md5sig, repeats×3, hopbyhop | 签名错误 + 重复注入 + IPv6 头 |
| TTL | autottl Δ2 (3-30) | 动态 TTL 跳动,隐藏跳数 |
| HTTP | hostcase, hostspell, domcase | 大小写/拼写/域名随机化 |
| 窗口 | wssize=64240:8, cutoff=n3 | TCP 窗口归一化 |
再加上 iptables 层面的 MSS 钳制 + DSCP QoS 抹除,共计 18 层混淆。
iptables 完整规则
四条规则,覆盖 TCP/UDP 443 的 NFQUEUE 劫持 + MSS 归一化 + QoS 标记抹除:
iptables -t mangle -A FORWARD -p tcp --dport 443 -j NFQUEUE --queue-num 100 --queue-bypass
iptables -t mangle -A FORWARD -p udp --dport 443 -j NFQUEUE --queue-num 100 --queue-bypass
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
iptables -t mangle -A FORWARD -j DSCP --set-dscp 0
实测结果
激活后立即进行全网测试,结果令人满意:
| 类别 | 实测 | 结论 |
|---|---|---|
| 🎮 游戏 | 原神 14ms / 王者 23ms / 和平 21ms | 🟢 0% 丢包 |
| 📺 视频 | 抖音峰值 73Mbps / 搜狐 80Mbps | 🟢 无卡顿 |
| 📁 下载 | 微信 77Mbps / 淘宝 76Mbps | 🟢 速度正常 |
| 🌐 网页 | 全站 0% 丢包 | 🟢 国内全通 |
为什么能 0% 丢包?
宿舍区专线经过了双层 DPI——学校数据中心的防火墙 IDS + 运营商的 GFW 联动。之前偶尔抽风就是某层 DPI 误判导致的丢包。
十八重混淆之后,原理解释:
学校 DPI → 收到乱码碎片 → 特征库匹配失败 → 放行
运营商 DPI → 同上 → 放行
→ 全链路无阻
两个 DPI 设备都认不出这种流量,自然一包不丢。
防御全景(最终版)
| 层面 | 方案 | 状态 |
|---|---|---|
| DNS 查询内容 | DoH 加密到云服务器 | ✅ |
| DNS 53 劫持 | 8443 绕过 | ✅ |
| DPI 指纹识别 | nfqws 18重混淆 | ✅ |
| TLS SNI 泄露 | split/fake 扰乱 | ✅ |
| 设备指纹 | MSS/QoS/窗口归一化 | ✅ |
| 全流量隧道 | VPN/WireGuard | ❌ 带宽不够 |
总结
从一个 Go TLS 证书兼容性的小问题出发,经历 smartdns 替换、DoH 加密、五回合编译血泪、18 重混淆部署,最终在 Celeron N2840 软路由上落地了一套完整的分层防御体系。实测结果:国内全站 0% 丢包,视频峰值 80Mbps,游戏延迟 14ms。
核心收获:用自签名证书跑私有 DoH 服务器,必须避开 Go 写的客户端。C 语言的 smartdns 是 OpenWrt 上的最优解——轻量、并发查询、系统 CA 集成,刚好弥补 AdGuardHome 的盲区。
编译 zapret 的过程则是一场标准的"嵌入式交叉编译"教学案例:原生编译行不通 → QEMU 验证问题 → musl 头文件冲突 → 动态链接 musl .so 失败 → 最终 glibc 静态编译一招破局。五次尝试,每一次都在削减问题的范围,直到触达那个干净的解法。
更新于 2026-05-27 · 第九章 实战验证已追加