SD-WAN终端研发系列06 SD-WAN虚拟专线的流量分流实现
本文实现基于域名和网段的选择性分流:命中策略的报文进入加密隧道,其余报文沿主路由表转发。
一、要解决什么问题
策略输入包含域名和 CIDR 网段,转发面最终只能依据 IP 地址、报文标记和路由表选路。实现需要处理以下问题:
- 域名规则需要在 DNS 应答阶段转换为动态 IP 集合,CIDR 规则可以直接写入集合
- 隧道可能断线重连,分流规则需要动态恢复
- 分流规则数量可能较多,匹配效率需要保证
二、整体方案架构
OpenWrt上实现上述需求的完整技术链路如下:
控制器下发分流规则(包含IP和域名两种格式)
↓
Agent接收规则,拆分为两部分
├── IP地址规则 → ipset 集合(直接存储)
└── 域名规则 → dnsmasq 配置(DNS解析后动态写入ipset)
↓
所有规则最终都转化为IP地址,存入同一个ipset集合
↓
防火墙(iptables)标记匹配ipset的流量包(fwmark 0x63)
↓
策略路由(ip rule + ip route)将标记流量引导到隧道接口
↓
hotplug脚本监控隧道接口状态,自动维护路由规则
下面按顺序说明每个环节。
三、第一步:用 ipset 匹配 IP 集合
3.1 为什么需要ipset
如果只有少量IP地址,可以通过iptables规则逐条匹配。但当规则数量达到数百甚至数千条时,逐条匹配的性能会急剧下降。
ipset是Linux内核提供的一种高效IP集合管理工具。它的核心优势是:
- 将大量IP地址/网段组织为一个集合,内核可以直接在集合中查找,时间复杂度接近O(1)
- 支持动态添加和删除IP,无需重新加载防火墙规则
- 支持存储IP地址、网段、MAC地址等多种格式
3.2 创建ipset集合
# 创建一个名为tunnel_acl的ipset集合,存储CIDR格式的IP/网段
ipset create tunnel_acl hash:net
hash:net是集合类型,表示使用哈希表存储网络地址段。其他常用类型包括hash:ip(单个IP地址)和list:set(集合的集合)。
3.3 添加和查询IP
# 添加单个IP
ipset add tunnel_acl 10.0.0.1
# 添加网段
ipset add tunnel_acl 10.0.0.0/24
# 查询IP是否在集合中
ipset test tunnel_acl 10.0.0.100
3.4 在iptables中引用ipset
# 匹配目的IP在tunnel_acl集合中的流量包
iptables -t mangle -A PREROUTING -m set --match-set tunnel_acl dst -j MARK --set-mark 0x63
这条规则的意思是:在路由前(PREROUTING链),如果数据包的目的地址在tunnel_acl集合中,就给这个包打上标记0x63。
四、第二步:用 dnsmasq 将域名转为 IP
4.1 问题的本质
控制器下发的分流规则中,域名(如srv.example.com)无法直接用于路由决策。路由器需要知道域名对应的IP地址是什么。
域名对应的 IP 可能变化,CDN 场景尤其常见。dnsmasq 需要持续处理 DNS 响应,并把解析结果动态加入 ipset。
4.2 dnsmasq与ipset的配合
OpenWrt内置的dnsmasq可以同时充当DNS服务器和DHCP服务器。dnsmasq-full版本(完整版)额外支持ipset功能:当dnsmasq解析一个域名得到IP地址后,可以自动将该IP加入指定的ipset集合。
配置方式如下:
在dnsmasq配置中添加规则:
# 当srv.example.com被解析时,将结果IP加入tunnel_acl ipset集合
ipset=/srv.example.com/tunnel_acl
# 当mail.example.com被解析时,同样处理
ipset=/mail.example.com/tunnel_acl
注意:这里用的是dnsmasq-full而不是标准版dnsmasq,因为标准版不支持ipset功能。需要在编译时选择安装dnsmasq-full并替换标准版。
4.3 通过UCI配置域名规则
Agent接收控制器下发的域名规则后,需要将其写入dnsmasq的配置中。可以通过UCI来实现:
uci add_list dhcp.@dnsmasq[0].ipset='/srv.example.com/tunnel_acl'
uci commit dhcp
然后调用脚本将UCI配置同步到dnsmasq的实际配置文件中。OpenWrt提供了一个脚本update_dns_domains来完成这个工作:
/usr/sbin/update_dns_domains
该脚本会读取/etc/config/dhcp中的ipset配置项,生成dnsmasq可识别的ipset=配置行,写入/tmp/resolv.conf.d/resolv.conf.auto或其他dnsmasq的配置文件中。
4.4 重启dnsmasq使配置生效
修改配置后需要重启dnsmasq:
/etc/init.d/dnsmasq restart
4.5 DNS解析流程
完整的数据流:
局域网PC发出DNS请求:srv.example.com
↓
请求到达路由器的dnsmasq(53端口)
↓
dnsmasq向上游DNS服务器(或本地hosts文件)查询
↓
获得解析结果(如:203.0.113.50)
↓
dnsmasq检查ipset配置规则
↓
发现srv.example.com匹配规则,将203.0.113.50加入tunnel_acl ipset集合
↓
返回解析结果给PC
↓
PC向203.0.113.50发起连接
↓
防火墙检查发现目的IP在tunnel_acl集合中 → 打标记 → 走隧道
五、第三步:防火墙标记与策略路由
5.1 防火墙标记(fwmark)
在所有分流规则(无论是IP直接加入还是域名解析后加入)都写入ipset集合后,接下来需要用防火墙规则对匹配流量打标记。
创建一条命名防火墙规则:
# 通过UCI添加
uci add firewall custom_rule
uci set firewall.@custom_rule[-1].name='tunnel_mark'
uci set firewall.@custom_rule[-1].src='*'
uci set firewall.@custom_rule[-1].dest='*'
uci set firewall.@custom_rule[-1].family='ipv4'
uci set firewall.@custom_rule[-1].proto='all'
uci set firewall.@custom_rule[-1].target='MARK'
uci set firewall.@custom_rule[-1].set_mark='0x63'
uci set firewall.@custom_rule[-1].extra='-m set --match-set tunnel_acl dst'
uci commit firewall
/etc/init.d/firewall restart
或者直接通过iptables命令:
iptables -t mangle -A PREROUTING -m set --match-set tunnel_acl dst -j MARK --set-mark 0x63
-t mangle指定操作mangle表(专门用于修改数据包标记的表)。PREROUTING链在路由决策之前执行,确保标记在路由时已经生效。--set-mark 0x63设置标记值为0x63。
5.2 策略路由
标记设置好后,需要配置策略路由,让被标记的流量走隧道:
# 1. 创建路由表(在/etc/iproute2/rt_tables中添加)
echo "200 tunnel_rt" >> /etc/iproute2/rt_tables
# 2. 添加策略路由规则
# 优先级499,匹配标记0x63的数据包,查tunnel_rt路由表
ip rule add fwmark 0x63 table tunnel_rt prio 499
# 3. 在tunnel_rt路由表中添加默认路由(走隧道接口)
ip route add default dev tunnel0 table tunnel_rt
这里的逻辑是:
- 普通流量(无标记)→ 按默认路由表(main表)→ 走本地WAN
- 标记流量(0x63)→ 按tunnel_rt路由表 → 走隧道接口(tunnel0)
prio 499是规则的优先级。OpenWrt的默认路由规则优先级通常是不同的数值(如mwan3使用较低的优先级),需要确保分流规则的优先级在合理范围内,不被其他规则覆盖。
5.3 防火墙与dnsmasq的启动顺序
有一个需要注意的细节:dnsmasq在启动时才会加载ipset配置规则,但ipset集合本身需要防火墙规则引用。因此必须确保先启动防火墙(创建ipset集合和iptables规则),再启动dnsmasq(加载ipset配置)。
OpenWrt的启动脚本编号中,S19firewall(防火墙)在S20network(网络)之前执行,而dnsmasq通常是network启动后才启动,所以默认顺序是正确的。但如果手动重启服务,需要注意顺序。
六、第四步:用 hotplug 恢复断线规则
6.1 问题场景
L2TP 隧道在系统中表现为虚拟网络接口。隧道断线后,tunnel0 等接口进入 DOWN 状态,相关路由规则随之消失。接口重新变为 UP 时,路由规则不会自行恢复。
如果不补回规则,隧道重连后即使 ipset 和防火墙标记还在,流量也无法进入隧道。
6.2 hotplug机制
OpenWrt的hotplug子系统可以在系统事件发生时自动执行脚本。对于网络接口,当接口状态变化(UP/DOWN)时,会触发/etc/hotplug.d/iface/目录下的脚本。
每个脚本执行时会收到环境变量:
$ACTION:ifup或ifdown$INTERFACE:接口名称(如tunnel0)
6.3 hotplug脚本示例
#!/bin/sh
# /etc/hotplug.d/iface/10-l2tp
case "$ACTION" in
ifup)
# 隧道接口上线时
if echo "$INTERFACE" | grep -q "l2tp"; then
# 获取隧道接口的默认网关
gateway=$(netstat -r | grep default | grep "$INTERFACE" | awk '{print $2}')
# 清空并重建隧道路由表
ip route flush table tunnel_rt
ip route add default via "$gateway" dev "$INTERFACE" table tunnel_rt
logger -t hotplug "L2TP tunnel $INTERFACE is up, route updated"
# 可选:重载SQoS配置
# service sqm reload
fi
;;
ifdown)
# 隧道接口断开时
# 通常不需要主动清理,路由规则会随接口DOWN自动消失
;;
esac
这段脚本的核心逻辑:当隧道接口上线时,自动查询该接口的网关地址,然后在vpl路由表中添加默认路由指向该网关。
6.4 智能组网的路由更新
对于智能组网(Hub-Spoke模式),路由更新逻辑更复杂一些。路由更新脚本需要:
#!/bin/sh
# 从UCI配置获取需要组网的目标网段
routes=$(uci get network.mesh.target)
[ -z "$routes" ] && exit 1
# 获取已建立的隧道接口
tunnels=$(ifconfig | grep l2tp | cut -d ' ' -f 1)
for tunnel in $tunnels; do
# 获取隧道接口的网关
gateway=$(netstat -r | grep default | grep "$tunnel" | awk '{print $2}')
# 根据接口名确定路由表名
tunnel_table=${tunnel:2:6} # 从接口名截取路由表名
# 清空路由表并添加路由
ip route flush table "$tunnel_table"
for target in $routes; do
ip route add "$target" via "$gateway" table "$tunnel_table"
done
done
七、完整流程串联
将所有环节串联起来,虚拟专线的完整数据流如下:
1. 控制器下发分流规则列表
├── IP规则:[10.0.0.0/24, 172.16.0.0/16, ...]
└── 域名规则:[srv.example.com, mail.example.com, ...]
2. Agent处理规则
a. IP规则 → 直接加入tunnel_acl ipset集合
b. 域名规则 → 写入UCI dhcp配置 → 调用update_dns_domains → 重启dnsmasq
3. 防火墙打标记
iptables在PREROUTING链匹配tunnel_acl集合中的目的IP → 打标记0x63
4. 策略路由
ip rule将标记0x63的流量 → 指向tunnel_rt路由表 → 默认路由走tunnel0隧道
5. 隧道建立
tunnel0接口UP → hotplug脚本自动更新路由表
6. 数据传输
员工访问srv.example.com
→ DNS解析(dnsmasq将解析结果加入tunnel_acl ipset)
→ 访问解析出的IP(防火墙匹配ipset → 打标记 → 策略路由 → 走隧道 → 到达总部)
八、关键配置要点总结
| 环节 | 关键配置 | 注意事项 |
|---|---|---|
| ipset | hash:net类型 |
集合名需要在防火墙和路由中一致 |
| dnsmasq | 必须用dnsmasq-full | 标准版不支持ipset功能 |
| 防火墙 | mangle表PREROUTING链 | 先于路由决策执行 |
| 策略路由 | ip rule + ip route |
路由表需要在rt_tables中注册 |
| hotplug | 接口事件触发 | 脚本需要有可执行权限 |
| 启动顺序 | 防火墙 → dnsmasq | 防火墙必须先创建ipset集合 |
九、一个容易忽略的细节:防火墙zone配置
隧道接口(如tunnel0)需要被正确分配到防火墙的zone中。如果隧道的流量需要转发到LAN侧(供局域网用户使用),需要确保:
- 隧道接口被加入到WAN zone或一个自定义zone
- LAN zone和隧道所在的zone之间配置了forwarding规则
下一篇介绍如何在资源受限的 OpenWrt 设备上交叉编译、集成和运行 Agent。