0. 前言
在 fw4(nftables)环境中,可以使用 ipset 保存电信、移动和境外地址表,再由 mwan3 根据目标地址选择不同的出口。
这种配置运行起来并不复杂,但实际使用中可能遇到两个时序问题:
- OpenWrt 启动时,ipset 地址表已经加载,而电信、移动的 PPPoE 还没有拨号完成。此时过早执行
mwan3 reload,mwan3 无法基于完整的接口和路由状态生成规则。 - 运营商定期断开 PPPoE,接口自动重拨并取得新地址后,mwan3 的运行状态没有及时刷新,最终出现分流错乱。
本文使用两个职责明确的脚本解决这些问题:
ipset-load.sh:开机加载地址表。99-mwan3-wan-reload:在电信或移动接口重新上线后,自动重载 mwan3。
ipset 负责地址数据,mwan3 负责策略与选路,OpenWrt hotplug 负责监听接口事件。
1. 网络环境与问题原因
本文使用三个 mwan3 逻辑接口:
ct:中国电信 PPPoE 出口。cm:中国移动 PPPoE 出口。glb:使用静态 IP 的代理 OpenWrt 虚拟机。
其中 ct 和 cm 会因为开机拨号、运营商强制重拨或临时掉线而重新上线;glb 是本地静态接口,通常不会发生 PPPoE 重拨,所以只需要监控 ct 和 cm。
原来的开机流程如下:
OpenWrt 启动
→ rc.local 执行 ipset-load.sh
→ 地址表加载完成并重载 mwan3
→ 此时 PPPoE 可能尚未拨号成功
→ ct/cm 后续才获得地址和路由
→ mwan3 没有再次重载
→ 分流状态异常
增加接口 hotplug 钩子后,流程变为:
OpenWrt 启动
→ rc.local 加载 ipset 地址表
→ ct/cm 完成 PPPoE 拨号
→ OpenWrt 产生 ifup 事件
→ hotplug 钩子等待路由稳定
→ 自动执行 mwan3 reload
同一个钩子也能处理路由器运行期间发生的 PPPoE 自动重拨。
2. 地址表加载脚本
2.1 脚本代码
将下面的脚本保存为 /usr/bin/ipset-load.sh:
#!/bin/sh
set -eu
load_set() {
SET="$1"
FILE="$2"
[ -f "$FILE" ] || return 0
awk -v SET="$SET" '
BEGIN {
print "create " SET " hash:net family inet -exist"
print "flush " SET
}
NF && $1 !~ /^#/ {
print "add " SET " " $1 " -exist"
}' "$FILE" | ipset restore
}
load_set ct_ipv4 /etc/ipset/ct.txt
load_set cm_ipv4 /etc/ipset/cm.txt
load_set glb_ipv4 /etc/ipset/glb.txt
# 让刚更新的地址表立即参与 mwan3 分流,不会重启网络接口。
/etc/init.d/mwan3 reload
exit 0
2.2 脚本原理
load_set 函数接收两个参数:ipset 集合名称和对应的地址表文件。
以电信地址表为例:
load_set ct_ipv4 /etc/ipset/ct.txt
脚本通过 awk 把文本地址表转换成 ipset restore 能够批量执行的命令:
create ct_ipv4 hash:net family inet -exist
flush ct_ipv4
add ct_ipv4 1.0.1.0/24 -exist
参数 -exist 表示集合或条目已经存在时不报错。因此,同一脚本既可以在首次开机时运行,也可以在更新地址表后重复运行。
地址表应该分别放在:
/etc/ipset/ct.txt
/etc/ipset/cm.txt
/etc/ipset/glb.txt
文本文件一行写一个 IP 或 CIDR,可以包含空行和以 # 开头的注释:
# 中国电信地址示例
1.0.1.0/24
1.0.2.0/23
2.3 设置开机加载
赋予执行权限:
chmod +x /usr/bin/ipset-load.sh
编辑 /etc/rc.local,在 exit 0 前加入:
/usr/bin/ipset-load.sh
exit 0
rc.local 只在开机阶段执行一次,因此适合恢复 ipset 地址表,但不能用于持续监听 PPPoE 重拨。
3. PPPoE 上线监听脚本
3.1 为什么使用 hotplug
OpenWrt 会在网络接口上线或下线时执行 /etc/hotplug.d/iface/ 中的钩子,并向脚本传入环境变量:
ACTION:事件类型,例如ifup或ifdown。INTERFACE:/etc/config/network中的逻辑接口名,例如ct、cm。
需要注意,INTERFACE 不是 eth0、pppoe-wan 等设备名称。
3.2 脚本代码
将下面的脚本保存为 /etc/hotplug.d/iface/99-mwan3-wan-reload:
#!/bin/sh
# 需要监听的 OpenWrt 逻辑接口名称。
WAN_INTERFACES="ct cm"
# 接口上线后等待路由稳定的时间,单位为秒。
RELOAD_DELAY="5"
# 防止短时间内重复执行 mwan3 reload。
LOCK_DIR="/var/lock/mwan3-wan-reload.lock"
# 只处理接口上线事件,忽略 ifdown 等其他事件。
[ "${ACTION:-}" = "ifup" ] || exit 0
# 只处理 WAN_INTERFACES 列表中的完整接口名称。
case " $WAN_INTERFACES " in
*" ${INTERFACE:-} "*) ;;
*) exit 0 ;;
esac
# 后台等待并重载 mwan3,避免阻塞 OpenWrt hotplug 流程。
(
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
sleep "$RELOAD_DELAY"
logger -t mwan3-hotplug "interface $INTERFACE is up; reloading mwan3"
if ! /etc/init.d/mwan3 reload; then
logger -t mwan3-hotplug -p daemon.err \
"failed to reload mwan3 after interface $INTERFACE came up"
exit 1
fi
) &
exit 0
3.3 接口筛选
下面的配置表示只监控电信和移动接口:
WAN_INTERFACES="ct cm"
LAN、loopback 和静态的 glb 接口事件都会被忽略。如果以后还需要监控其他动态 WAN,可以把对应逻辑接口名加入列表:
WAN_INTERFACES="ct cm wan2"
3.4 延迟与重复事件处理
接口刚产生 ifup 事件时,地址已经取得,但路由状态仍可能处于更新阶段。因此脚本默认等待 5 秒:
RELOAD_DELAY="5"
如果设备启动较慢,或者开机后仍偶尔出现规则异常,可以提高到 10 秒:
RELOAD_DELAY="10"
PPPoE 重连期间可能连续产生多个相近事件。脚本使用 /var/lock/mwan3-wan-reload.lock 作为互斥锁:第一份脚本创建锁目录后继续执行,后续重复事件发现锁已存在便直接退出。
如果 ct 和 cm 几乎同时上线,通常只会执行一次 mwan3 reload。这不会遗漏某一套规则,因为 reload 会重新加载完整的 mwan3 配置,而不是只刷新触发事件的单个接口。
3.5 为什么文件没有 .sh 后缀
Linux 脚本能否执行取决于第一行的解释器声明和文件执行权限,而不是扩展名:
#!/bin/sh
OpenWrt 的 hotplug 钩子通常使用 数字-名称 的格式命名,例如:
99-mwan3-wan-reload
前面的 99- 让该钩子在多数接口钩子之后运行。ipset-load.sh 是放在 /usr/bin 中供人主动调用的普通脚本,保留 .sh 后缀更容易辨认;两种命名方式都不影响 Shell 执行。
4. 安装方法
假设已经把两个文件上传到路由器的 /tmp 目录,执行:
cp /tmp/ipset-load.sh /usr/bin/ipset-load.sh
mkdir -p /etc/hotplug.d/iface
cp /tmp/99-mwan3-wan-reload \
/etc/hotplug.d/iface/99-mwan3-wan-reload
chmod +x /usr/bin/ipset-load.sh
chmod +x /etc/hotplug.d/iface/99-mwan3-wan-reload
最终文件结构如下:
/etc/
├── hotplug.d/
│ └── iface/
│ └── 99-mwan3-wan-reload
├── ipset/
│ ├── ct.txt
│ ├── cm.txt
│ └── glb.txt
└── rc.local
/usr/bin/
└── ipset-load.sh
两个脚本不要放在同一个位置,也不要把 hotplug 钩子加入 rc.local:
| 文件 | 放置位置 | 调用方式 |
|---|---|---|
ipset-load.sh | /usr/bin/ipset-load.sh | /etc/rc.local 在开机时调用 |
99-mwan3-wan-reload | /etc/hotplug.d/iface/99-mwan3-wan-reload | OpenWrt 在接口上线时自动调用 |
5. 测试与验证
5.1 测试地址表加载
手动运行:
/usr/bin/ipset-load.sh
检查三个集合:
ipset list ct_ipv4 | head
ipset list cm_ipv4 | head
ipset list glb_ipv4 | head
5.2 模拟接口上线
模拟电信接口上线:
ACTION=ifup INTERFACE=ct \
/etc/hotplug.d/iface/99-mwan3-wan-reload
模拟移动接口上线:
ACTION=ifup INTERFACE=cm \
/etc/hotplug.d/iface/99-mwan3-wan-reload
等待超过 RELOAD_DELAY 设置的时间,然后查看日志:
logread | grep mwan3-hotplug
正常情况下可以看到:
mwan3-hotplug: interface ct is up; reloading mwan3
最后检查 mwan3 的接口和规则状态:
mwan3 status
mwan3 status rules
5.3 重启验证
完成手动测试后重启路由器:
reboot
重新登录后依次检查:
ipset list ct_ipv4 | head
logread | grep mwan3-hotplug
mwan3 status
mwan3 status rules
如果日志中出现 ct 或 cm 上线后重载 mwan3 的记录,并且规则恢复正常,说明开机时序问题已经解决。
6. 常见问题
6.1 为什么不能只用 rc.local
rc.local 只在系统启动时运行一次。运营商在数小时或数天后强制断开 PPPoE 时,它不会再次执行,因此无法处理运行期间的自动重拨。
hotplug 是由接口事件驱动的:只要 ct 或 cm 再次上线,脚本就会重新执行。
6.2 是否需要监控 glb
本文中的 glb 是使用静态 IP 的本地代理 OpenWrt 虚拟机,不参与 PPPoE 拨号。代理节点的外部连接最终仍通过 ct 或 cm 出口,因此通常不需要监控 glb。
即便代理虚拟机重启,主路由上的静态 glb 接口也不一定产生新的 ifup 事件。如果确实需要检测代理虚拟机是否在线,应使用独立的可达性检测方案,而不是简单地把 glb 加入 hotplug 列表。
6.3 如何确认逻辑接口名称
执行:
uci show network | grep '=interface'
确认 /etc/config/network 中的逻辑接口名是否确实为 ct 和 cm。不要把 pppoe-ct、pppoe-cm 或物理网卡名填入 WAN_INTERFACES。
6.4 为什么重载而不是重启
本文执行的是:
/etc/init.d/mwan3 reload
它用于重新加载 mwan3 策略,不会主动重启 PPPoE 接口。相比重启整个网络服务,它影响更小,也不会为了修复分流而制造一次新的断线。
7. 总结
这套方案把开机初始化和接口事件处理拆成两个脚本:
ipset-load.sh
→ 恢复 ct/cm/glb 地址表
→ 适合由 rc.local 在开机时调用
99-mwan3-wan-reload
→ 监听 ct/cm 的 ifup 事件
→ 等待地址和路由稳定
→ 自动执行 mwan3 reload
最终既解决了 OpenWrt 开机时 PPPoE 尚未完成的问题,也处理了运营商定期重拨后 mwan3 分流失效的问题。两个脚本各自只负责一类任务,后续维护和排错也更直观。
