最近我帮朋友整理一台软路由,现象很有意思:平时跑满千兆都没压力的设备,最近网页频繁打不开,CPU 动不动就飙到 90%,内存也在持续上涨。打开进程一看,负责流量识别的服务正在后台一遍一遍地遍历一张百万行级别的域名库。这类方案在社区里很常见:把已知应用和网站的域名全部收进一张大表,再对每个连接做字符串匹配。它听起来很“全”,实际上却把软路由拖进了泥潭。后来我换了个思路:把“全量域名匹配”改成“少量特征动态识别”,用 eBPF 动态 DPI 来做流量分类,最终只维护了一百多条规则,就覆盖住了需要识别和管控的绝大多数场景。这篇文章会把这次的实战路径、踩坑点和判断逻辑写清楚。
我的核心判断是:流量识别的瓶颈从来不是“规则够不够多”,而是“规则够不够聪明”。真正适合软路由的方案,不是百万域名库,而是能用少量特征稳定复用的动态 DPI。
1. 百万域名库不是“保险箱”,而是“慢性毒药”
1.1 一张大表带来的三个连锁问题
很多软路由教程会让你导入“全量域名库”,理由很简单:库全,识别就全。这个理由在逻辑上说得通,但放到真实环境里,问题很快就会出现。
第一是内存。百万行级别的域名表,即使经过压缩和索引,也要占掉几百 MB 内存。软路由的内存往往只有 1GB 到 4GB,还要分配给 DHCP、DNS 缓存、流控、连接跟踪。结果流量识别模块反而成了内存大户,挤占了核心业务。
第二是 CPU。每个新建连接都要和这张大表做匹配。如果匹配算法没有做好索引,就是全表遍历,CPU 会随着连接数增长迅速吃满。更麻烦的是,很多域名库还会附带 IP 段规则,这些规则在 iptables/nftables 里展开后,规则条数可能翻好几倍。每当建连时,系统就要在这些规则里跑一遍。
第三是更新。域名库不是静态数据,维护方可能每周更新几次。每次更新都要重新加载,加载期间容易出现表不一致、重复规则、误命中。如果你手动给库里加过自定义规则,下次更新时可能直接被覆盖掉。
从工程角度看,这种方案本质上是在用“空间”和“部署复杂度”换“省事”。可一旦规则规模到了百万级,它就不省事了,反而成了一种运维负担。
| 对比维度 | 百万域名库方案 | eBPF 动态 DPI 方案 |
|---|---|---|
| 规则数量 | 数十万到数百万行 | 几十到几百条特征规则 |
| 匹配位置 | 用户态进程遍历大表 | 内核态 eBPF 程序查 map |
| 内存占用 | 高,随库线性增长 | 低,主要受 map 容量影响 |
| 更新方式 | 整库替换或整体重载 | map 动态增删,不中断流量 |
| 维护成本 | 依赖第三方库和更新节奏 | 依赖自己梳理的少量稳定特征 |
| 误判风险 | 域名归属与实际业务可能错位 | 特征与业务强相关,误判容易定位 |
1.2 为什么“全量覆盖”在真实网络里是个伪命题
域名和 IP 的映射关系,在真实网络里比想象中要松散得多。同一个域名背后可能有几十个 CDN 节点,不同区域访问到不同 IP;同一个 IP 上也可能跑着大量虚拟主机;很多动态解析甚至几分钟变一次。所以“把域名放进名单”这种方式,本质上是在和一个不断变化的映射表赛跑。
更关键的是,域名本身不能完全回答“这是什么业务”。一个网盘域名,可能同时承载网页登录、客户端同步、视频播放和下载任务。域名库只能告诉你“哪个域名属于哪家公司”,但流量识别需要回答“这条连接到底在跑什么”。这两者之间差了一层抽象。
所以,百万域名库最大的问题不是“占空间”,而是“它解决了一个不太对的问题”。它看起来全,但漏掉新域名、新 IP 段的速度比你更新库的速度还快。真正出现一条新游戏的流量时,域名库里大概率没有,你又得手动加。
1.3 从百万行到 256 条,不是取舍,而是换思路
这里需要解释为什么是“256 条规则”。这不是什么官方标准,更像是一种刻意设计:通过限制规则数量,逼迫自己不再“看到域名就加”,而是先问“这个特征是不是稳定、是不是能覆盖一类流量”。
当规则规模被限制住之后,你做的事情就从“堆数据”变成了“做抽象”。不再追求认识全网所有域名,而是只定义一小批能够区分业务类别的稳定特征。比如:
- TLS 握手里的 SNI 后缀:
*.examplevideo.com - 证书里的 CN 或 O 字段
- HTTP 请求里的 Host 字段
- 固定的端口段
- 特定的 IP 段或 ASN 段
- 连接建立后表现出的包大小序列、方向比例等行为特征
用这些特征,每个业务类别用 1 到 3 条规则就能覆盖。而在家里或小型办公网里,真正需要重点管控的业务类别通常不会超过 50 类,256 条规则带给 eBPF map 的查找压力也很小,哈希查找基本是常数级开销。
注意:256 条规则的价值不是“少”,而是让你从规则库里跳出来,真正去理解流量。规则越少,交叉误判越容易排查。
2. eBPF 动态 DPI 为什么能用“小规则”干大事
2.1 DPI 不做“拉网排查”,只做“关键位置取样”
很多人听到“深度包检测”,会以为要把数据包从第一字节到最后一字节全部拆开,甚至还原整个应用流。实际上 DPI 不是这么工作的。它只需要在协议栈的几个关键位置取样,就能拿到足够区分业务的字段。
以 HTTPS 为例:
- 看到 TCP 目的端口 443,先假定是 TLS。
- 拿到 TLS ClientHello 里的 SNI 字段,就知道客户端在访问哪个域名。
- 如果 SNI 命中了某条通配符规则,直接打标。
- 如果 SNI 缺失或因为加密扩展被隐藏,就换成看证书指纹、看连接行为。
这种取样思维,才是 DPI 能在少量规则下工作的基础。它不需要看全每一个包,只需要在几个关键节点拿到“这个包属于哪类业务”的证据。再配合 eBPF 把取样逻辑放进内核,识别过程就像给每个连接贴标签。
2.2 eBPF 把动态能力带进了内核
eBPF 的价值,是让你能把上面这套“取样逻辑”编译成字节码程序,挂载到内核的 TC hook 或 XDP hook 上。每个数据包经过时,eBPF 程序会执行解析逻辑,查询 map,再把分类结果写进skb->mark,或者直接在内核侧根据策略执行放行、丢弃、限速动作。
这里最值得强调的一点是“动态”。规则不是编译在程序里的常量,而是放在 eBPF map 里。用户态程序可以通过bpftool map update,或者自己写的控制程序,随时往 map 里插入新规则、删除旧规则。整个过程不需要重新编译和加载 eBPF 程序,连接不会中断,规则却能热更新。
下面是一段用于理解流程的伪代码,不要照抄到生产环境,因为具体头部偏移和辅助函数接口要按你的内核版本和 libbpf 写法来处理:
// eBPF DPI 程序框架,伪代码结构用于理解 int dpi_prog(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; // 1. 解析以太网/IPv4/IPv6/TCP/UDP 头 // 2. 如果是 TCP 443 或 UDP 443,尝试解析 TLS ClientHello // 3. 如果拿到 SNI,查询 map 中的规则表 // 4. 命中后把分类结果写入 skb->mark 或 map // 5. 按策略返回 TC_ACT_OK / TC_ACT_SHOT return TC_ACT_OK; }和传统“在用户态定期解析日志”相比,eBPF 把识别动作放到了包路径上,延迟更低,也不需要一个常驻的高 CPU 用户态进程去轮询连接跟踪表。
2.3 为什么少量规则能兜住大多数场景
从流量构成来看,普通网络里的业务类别非常集中:视频、游戏、办公、下载、即时通讯、系统更新,以及未知流量。而每个类别只要稳定识别出其中一个或两个关键字段,就能给整条流打上标签。
举例:
- 视频:识别
*.googlevideo.com或*.bilivideo.com这类稳定的 CDN 域名后缀。 - 办公:识别
*.office.com、*.zoom.us、企业自建系统的专属域。 - 下载:识别某些固定下载服务器 IP 段,以及来自特定应用的长连接特征。
- 系统更新:识别微软、苹果、Linux 发行版的更新域名并按 CIDR 匹配。
这些规则加起来,可能不到五十条。剩下的海量域名,根本不需要进规则表,直接归为“默认流量”。这比维护百万库清醒得多:你不需要认识所有人,只需要在关键路口识别特定几个目标。
3. 软路由实战落地路径
3.1 落地前先定义问题:你到底要识别哪些流量
不要急着写 eBPF 程序。先花半天回答三个问题:
- 你要识别哪几类流量?视频、游戏、办公、下载、未知?
- 识别出来要做什么?放行、限速、阻断、标记、统计?
- 识别不到时怎么办?默认放行,还是默认阻断?
这个环节决定了后续的规则表怎么设计。建议先写一个规则描述文件,方便维护和审查。比如:
rules: - name: office365 type: tls_sni value: "*.office365.com" action: accept priority: 10 - name: video_bili type: tls_sni value: "*.bilibili.com" action: mark_video priority: 20 - name: game_common type: cidr value: "203.0.113.0/24" action: mark_game priority: 30真实规则可以包含tls_sni、http_host、cidr、port、proto等类型。在一开始,我建议只定义 10 条以内的规则,先把流程跑通。
3.2 最小可运行流程:从零规则起步到少量规则生效
推荐按下面的顺序操作,不要一上来就想接主链路。
- 准备环境。一台 Linux 软路由,内核建议 5.10 以上并开启 BTF。安装 clang、llvm、libbpf-dev、bpftool。
- 编译加载空程序。确认 eBPF 能加载、能卸载。用
bpftool prog list和bpftool map list检查。 - 抓真实流量。用
tcpdump抓几种目标业务的前几个包,重点关注 TLS ClientHello 的 SNI、HTTP 的 Host,以及固定连接的 IP 段。 - 生成规则。把抓到的字段做聚合。不要为一个网站单独建规则,优先找共同后缀或共同证书,做成通配规则。
- 写解析逻辑。在空程序里解析 TCP/UDP 头。如果目的端口是 443,就尝试提取 SNI;如果是 HTTP,就提取 Host。
- 单任务验证。放出一条测试流量,确认 eBPF 程序把连接打上了预期 mark,用户态能读到命中次数增加。
- 扩展到几类流量。加 10 条左右规则,逐类验证。不要急着加到 256 条,先把规则模板和更新链路验证扎实。
- 接上策略。根据 mark 去执行 nftables/TC 限速,或者只做统计展示。
注意:eBPF 解析数据包时,必须用
data_end做边界检查,否则验证器会拒绝加载。这不是在限制你,而是在保护内核安全。
3.3 动态更新规则:真正的“动态”体现在哪里
动态更新的核心是 map。把规则表设计成一个哈希 map,key 是规则编号,value 是特征、动作和优先级。用户态程序可以:
- 从抓包工具或日志中提取新的特征;
- 通过
bpftool map update写入新规则; - 删除过期规则;
- 读取命中统计来辅助判断。
命令示例:
bpftool map update name dpi_rules \ key 0x01 0x00 0x00 0x00 \ value 0x01 0x00 0x00 0x00 0x00 ...这里 key/value 的具体布局取决于你自己定义的结构体。重点在于,eBPF 程序不用重新加载,规则表可以像数据库记录一样增删改查。你甚至可以从日志系统读入规则变更事件,自动写进 map,这时候“动态 DPI”才开始真正运转。
4. 性能、排查与适用边界
4.1 性能观察:先看基线,再看增量
软路由上做 DPI,最怕的不是理论峰值,而是影响正常转发。建议按三步观察:
- 在未加载 eBPF 程序时,记录 CPU idle、软中断、PPS、丢包率作为基线。
- 加载一个只放行的空程序,再记录一次,算出 eBPF 固定开销。
- 加入 DPI 规则和动作,记录增量。如果新增规则后 CPU 明显上涨,优先怀疑规则匹配逻辑是否对每个包都做了大范围遍历。
| 检查项 | 正常信号 | 异常信号 |
|---|---|---|
| CPU idle | 加载后下降 5%~10% 以内 | 下降超过 20%,说明解析或匹配太重 |
| PPS | 吞吐不下降或下降可接受 | 丢包率明显增加 |
| map 命中率 | 目标流量大多能命中 | 命中率很低,规则特征不够稳定 |
| 内存 | map 条目稳定 | map 持续增长,可能是规则没有淘汰策略 |
这些数字不要当成固定阈值,应该在你自己环境中对比观察。
4.2 一套可复用的排查链路
遇到问题不要第一时间怀疑 eBPF 慢,按顺序排查:
- 先看现象:丢包、卡顿、CPU 高,还是识别不准。
- 再看输入:抓包确认流量里真的有你要匹配的特征吗?比如 TLS SNI 是否可能被分段,是否使用了 ECH 导致 SNI 被隐藏。
- 再看环境:
uname -r确认内核版本,检查 BTF,确认程序挂载在哪个接口、哪个方向,是 TC ingress 还是 egress。 - 再看规则:是否有多条规则优先级相同且互相覆盖,通配符是不是写得太宽,CIDR 规则是否和 SNI 规则冲突。
- 最后看工具边界:eBPF 验证器对循环次数有限制,某些复杂解析无法通过;XDP hook 上能拿到的字段和 TC hook 不完全一样;多队列网卡还需要看 RSS 是否均衡。
这套链路在软路由流量识别问题里基本够用。很多“识别不到”的问题,最后都出在输入抓包不完整或规则优先级冲突上。
4.3 适用边界:它擅长什么,不擅长什么
eBPF 动态 DPI 最适合的场景是中小型软路由、网关设备、旁路流量分类器,以及零信任网关。它们共同的特点是:规则数量可控,业务类别清晰,对实时性有要求,又不想背着超大规则库运维。
不适合的场景也比较明确:
- 超大规模全量 DPI 审计,比如 100Gbps 网关级别的全量还原和分析。eBPF 在普通软路由上做不到,需要结合专用硬件或更底层的数据平面技术。
- 需要识别全部长尾应用,且规则量不断膨胀到几十万条。这不是 eBPF 的问题,而是 DPI 目标定错了。这种场景应该考虑离线分析与云侧流量分类。
- 内核版本过旧,BTF 和 eBPF 特性受限的设备。这时候强行上 eBPF 会一直踩坑,不如先用 nftables 把关键端口和 IP 段管住,再逐步迁移。
我在这次实践里感受最深的一点是:流量识别的真正门槛,不是把规则库堆得有多大,而是能不能在正确的位置取样,用稳定的特征把问题抽象出来。256 条规则看起来少,但它逼着你把混乱的域名清单,整理成一套能直接指导限速和标记策略的模型。
如果你接下来要动软路由,我的建议是先别急着导入大库。先用抓包工具把自己常用的业务跑一遍,整理出前 20 条规则,再考虑用 eBPF 动态加载。跑通之后,你会明显感觉到百万域名库的笨重,也会理解少即是多这条经验在流量识别里同样成立。