Featured image of post OpenWrt mwan3 + ipset:解决开机与 PPPoE 重拨后的分流失效

OpenWrt mwan3 + ipset:解决开机与 PPPoE 重拨后的分流失效

0. 前言

在 fw4(nftables)环境中,可以使用 ipset 保存电信、移动和境外地址表,再由 mwan3 根据目标地址选择不同的出口。

这种配置运行起来并不复杂,但实际使用中可能遇到两个时序问题:

  1. OpenWrt 启动时,ipset 地址表已经加载,而电信、移动的 PPPoE 还没有拨号完成。此时过早执行 mwan3 reload,mwan3 无法基于完整的接口和路由状态生成规则。
  2. 运营商定期断开 PPPoE,接口自动重拨并取得新地址后,mwan3 的运行状态没有及时刷新,最终出现分流错乱。

本文使用两个职责明确的脚本解决这些问题:

  • ipset-load.sh:开机加载地址表。
  • 99-mwan3-wan-reload:在电信或移动接口重新上线后,自动重载 mwan3。

ipset 负责地址数据,mwan3 负责策略与选路,OpenWrt hotplug 负责监听接口事件。

1. 网络环境与问题原因

本文使用三个 mwan3 逻辑接口:

  • ct:中国电信 PPPoE 出口。
  • cm:中国移动 PPPoE 出口。
  • glb:使用静态 IP 的代理 OpenWrt 虚拟机。

其中 ctcm 会因为开机拨号、运营商强制重拨或临时掉线而重新上线;glb 是本地静态接口,通常不会发生 PPPoE 重拨,所以只需要监控 ctcm

原来的开机流程如下:

1
2
3
4
5
6
7
OpenWrt 启动
  → rc.local 执行 ipset-load.sh
  → 地址表加载完成并重载 mwan3
  → 此时 PPPoE 可能尚未拨号成功
  → ct/cm 后续才获得地址和路由
  → mwan3 没有再次重载
  → 分流状态异常

增加接口 hotplug 钩子后,流程变为:

1
2
3
4
5
6
OpenWrt 启动
  → rc.local 加载 ipset 地址表
  → ct/cm 完成 PPPoE 拨号
  → OpenWrt 产生 ifup 事件
  → hotplug 钩子等待路由稳定
  → 自动执行 mwan3 reload

同一个钩子也能处理路由器运行期间发生的 PPPoE 自动重拨。

2. 地址表加载脚本

2.1 脚本代码

将下面的脚本保存为 /usr/bin/ipset-load.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/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 集合名称和对应的地址表文件。

以电信地址表为例:

1
load_set ct_ipv4 /etc/ipset/ct.txt

脚本通过 awk 把文本地址表转换成 ipset restore 能够批量执行的命令:

1
2
3
create ct_ipv4 hash:net family inet -exist
flush ct_ipv4
add ct_ipv4 1.0.1.0/24 -exist

参数 -exist 表示集合或条目已经存在时不报错。因此,同一脚本既可以在首次开机时运行,也可以在更新地址表后重复运行。

地址表应该分别放在:

1
2
3
/etc/ipset/ct.txt
/etc/ipset/cm.txt
/etc/ipset/glb.txt

文本文件一行写一个 IP 或 CIDR,可以包含空行和以 # 开头的注释:

1
2
3
# 中国电信地址示例
1.0.1.0/24
1.0.2.0/23

2.3 设置开机加载

赋予执行权限:

1
chmod +x /usr/bin/ipset-load.sh

编辑 /etc/rc.local,在 exit 0 前加入:

1
2
3
/usr/bin/ipset-load.sh

exit 0

rc.local 只在开机阶段执行一次,因此适合恢复 ipset 地址表,但不能用于持续监听 PPPoE 重拨。

3. PPPoE 上线监听脚本

3.1 为什么使用 hotplug

OpenWrt 会在网络接口上线或下线时执行 /etc/hotplug.d/iface/ 中的钩子,并向脚本传入环境变量:

  • ACTION:事件类型,例如 ifupifdown
  • INTERFACE/etc/config/network 中的逻辑接口名,例如 ctcm

需要注意,INTERFACE 不是 eth0pppoe-wan 等设备名称。

3.2 脚本代码

将下面的脚本保存为 /etc/hotplug.d/iface/99-mwan3-wan-reload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/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 接口筛选

下面的配置表示只监控电信和移动接口:

1
WAN_INTERFACES="ct cm"

LAN、loopback 和静态的 glb 接口事件都会被忽略。如果以后还需要监控其他动态 WAN,可以把对应逻辑接口名加入列表:

1
WAN_INTERFACES="ct cm wan2"

3.4 延迟与重复事件处理

接口刚产生 ifup 事件时,地址已经取得,但路由状态仍可能处于更新阶段。因此脚本默认等待 5 秒:

1
RELOAD_DELAY="5"

如果设备启动较慢,或者开机后仍偶尔出现规则异常,可以提高到 10 秒:

1
RELOAD_DELAY="10"

PPPoE 重连期间可能连续产生多个相近事件。脚本使用 /var/lock/mwan3-wan-reload.lock 作为互斥锁:第一份脚本创建锁目录后继续执行,后续重复事件发现锁已存在便直接退出。

如果 ctcm 几乎同时上线,通常只会执行一次 mwan3 reload。这不会遗漏某一套规则,因为 reload 会重新加载完整的 mwan3 配置,而不是只刷新触发事件的单个接口。

3.5 为什么文件没有 .sh 后缀

Linux 脚本能否执行取决于第一行的解释器声明和文件执行权限,而不是扩展名:

1
#!/bin/sh

OpenWrt 的 hotplug 钩子通常使用 数字-名称 的格式命名,例如:

1
99-mwan3-wan-reload

前面的 99- 让该钩子在多数接口钩子之后运行。ipset-load.sh 是放在 /usr/bin 中供人主动调用的普通脚本,保留 .sh 后缀更容易辨认;两种命名方式都不影响 Shell 执行。

4. 安装方法

假设已经把两个文件上传到路由器的 /tmp 目录,执行:

1
2
3
4
5
6
7
8
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

最终文件结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/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 测试地址表加载

手动运行:

1
/usr/bin/ipset-load.sh

检查三个集合:

1
2
3
ipset list ct_ipv4 | head
ipset list cm_ipv4 | head
ipset list glb_ipv4 | head

5.2 模拟接口上线

模拟电信接口上线:

1
2
ACTION=ifup INTERFACE=ct \
    /etc/hotplug.d/iface/99-mwan3-wan-reload

模拟移动接口上线:

1
2
ACTION=ifup INTERFACE=cm \
    /etc/hotplug.d/iface/99-mwan3-wan-reload

等待超过 RELOAD_DELAY 设置的时间,然后查看日志:

1
logread | grep mwan3-hotplug

正常情况下可以看到:

1
mwan3-hotplug: interface ct is up; reloading mwan3

最后检查 mwan3 的接口和规则状态:

1
2
mwan3 status
mwan3 status rules

5.3 重启验证

完成手动测试后重启路由器:

1
reboot

重新登录后依次检查:

1
2
3
4
ipset list ct_ipv4 | head
logread | grep mwan3-hotplug
mwan3 status
mwan3 status rules

如果日志中出现 ctcm 上线后重载 mwan3 的记录,并且规则恢复正常,说明开机时序问题已经解决。

6. 常见问题

6.1 为什么不能只用 rc.local

rc.local 只在系统启动时运行一次。运营商在数小时或数天后强制断开 PPPoE 时,它不会再次执行,因此无法处理运行期间的自动重拨。

hotplug 是由接口事件驱动的:只要 ctcm 再次上线,脚本就会重新执行。

6.2 是否需要监控 glb

本文中的 glb 是使用静态 IP 的本地代理 OpenWrt 虚拟机,不参与 PPPoE 拨号。代理节点的外部连接最终仍通过 ctcm 出口,因此通常不需要监控 glb

即便代理虚拟机重启,主路由上的静态 glb 接口也不一定产生新的 ifup 事件。如果确实需要检测代理虚拟机是否在线,应使用独立的可达性检测方案,而不是简单地把 glb 加入 hotplug 列表。

6.3 如何确认逻辑接口名称

执行:

1
uci show network | grep '=interface'

确认 /etc/config/network 中的逻辑接口名是否确实为 ctcm。不要把 pppoe-ctpppoe-cm 或物理网卡名填入 WAN_INTERFACES

6.4 为什么重载而不是重启

本文执行的是:

1
/etc/init.d/mwan3 reload

它用于重新加载 mwan3 策略,不会主动重启 PPPoE 接口。相比重启整个网络服务,它影响更小,也不会为了修复分流而制造一次新的断线。

7. 总结

这套方案把开机初始化和接口事件处理拆成两个脚本:

1
2
3
4
5
6
7
8
ipset-load.sh
  → 恢复 ct/cm/glb 地址表
  → 适合由 rc.local 在开机时调用

99-mwan3-wan-reload
  → 监听 ct/cm 的 ifup 事件
  → 等待地址和路由稳定
  → 自动执行 mwan3 reload

最终既解决了 OpenWrt 开机时 PPPoE 尚未完成的问题,也处理了运营商定期重拨后 mwan3 分流失效的问题。两个脚本各自只负责一类任务,后续维护和排错也更直观。

使用 Hugo 构建
主题 StackJimmy 设计