本文实现基于域名和网段的选择性分流:命中策略的报文进入加密隧道,其余报文沿主路由表转发。

一、要解决什么问题

策略输入包含域名和 CIDR 网段,转发面最终只能依据 IP 地址、报文标记和路由表选路。实现需要处理以下问题:

  1. 域名规则需要在 DNS 应答阶段转换为动态 IP 集合,CIDR 规则可以直接写入集合
  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 场景尤其常见。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

这里的逻辑是:

  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规则

下一篇介绍如何在资源受限的 OpenWrt 设备上交叉编译、集成和运行 Agent。