本文是系列第6篇。虚拟专线是SD-WAN最核心的功能之一——让指定的业务流量通过加密隧道传输,其余流量照常走本地网络。本文从业务需求出发,逐步拆解这个功能的完整实现链路。

一、要解决什么问题

假设一家企业有这样的需求:分支机构的员工需要访问总部的内部服务(如srv.example.com)和内部网段(IP段如10.0.0.0/24),这些流量需要通过加密隧道安全传输。同时,员工访问互联网(如浏览新闻、看视频)的流量不应经过隧道,直接走本地宽带即可。

这就是”分流”——将不同类型的流量引导到不同的出口。难点在于:

  1. 规则中同时包含IP地址(如10.0.0.0/24)和域名(如srv.example.com),但路由系统只能基于IP地址做决策
  2. 隧道可能断线重连,分流规则需要动态恢复
  3. 分流规则数量可能较多,匹配效率需要保证

二、整体方案架构

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

这里的逻辑是:

  1. 普通流量(无标记)→ 按默认路由表(main表)→ 走本地WAN
  2. 标记流量(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/目录下的脚本。

每个脚本执行时会收到环境变量:

  • $ACTIONifupifdown
  • $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侧(供局域网用户使用),需要确保:

  1. 隧道接口被加入到WAN zone或一个自定义zone
  2. LAN zone和隧道所在的zone之间配置了forwarding规则

下一篇将介绍嵌入式Agent开发——如何在一个资源受限的嵌入式OpenWrt设备上完成Agent程序的交叉编译、系统集成和稳定部署。