SD-WAN终端研发系列06 SD-WAN虚拟专线的流量分流实现
本文是系列第6篇。虚拟专线是SD-WAN最核心的功能之一——让指定的业务流量通过加密隧道传输,其余流量照常走本地网络。本文从业务需求出发,逐步拆解这个功能的完整实现链路。
一、要解决什么问题
假设一家企业有这样的需求:分支机构的员工需要访问总部的内部服务(如srv.example.com)和内部网段(IP段如10.0.0.0/24),这些流量需要通过加密隧道安全传输。同时,员工访问互联网(如浏览新闻、看视频)的流量不应经过隧道,直接走本地宽带即可。
这就是”分流”——将不同类型的流量引导到不同的出口。难点在于:
- 规则中同时包含IP地址(如
10.0.0.0/24)和域名(如srv.example.com),但路由系统只能基于IP地址做决策 - 隧道可能断线重连,分流规则需要动态恢复
- 分流规则数量可能较多,匹配效率需要保证
二、整体方案架构
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场景下尤其常见),所以不能用一次性DNS查询解决——需要持续监听DNS响应,将解析出的IP动态加入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规则
下一篇将介绍嵌入式Agent开发——如何在一个资源受限的嵌入式OpenWrt设备上完成Agent程序的交叉编译、系统集成和稳定部署。