发生了什么
ImmortalWRT 24.10.5 / 其它固件
PassWall 2026.4.15 / 2025.12.x 直到最新插件
在开启 IPv6 透明代理并设置分流规则后,无论使用 sing-box 还是 Xray 作为分流核心,都可能在随机时间发生死机问题。具体表现为:
SSH 无法连接或连接极其缓慢、网页管理界面几乎无法加载、CPU 满载、内存占满但没有触发 OOM、产生大量连接。
这种问题是完全随机发生的:可能几个小时后出现,也可能半个月都不出现。
目前的推断
这是为什么呢?
目前的结论是:原本 PassWall 的 nftables.sh 中,update_wan_sets() 对 WAN6 nft set 的刷新方式存在风险。
update_wan_sets() {
local WAN_IP=$(get_wan_ips ip4)
[ -n "$WAN_IP" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
echo "$WAN_IP" | insert_nftset $NFTSET_WAN "-1"
}
local WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6 "-1"
}
}
它的流程是:
- 读取当前 WAN IPv4;
- 清空
passwall_wan; - 将当前 WAN IPv4 重新插入;
- 读取当前 WAN IPv6;
- 清空
passwall_wan6; - 将当前 WAN IPv6 重新插入。
在静态、低并发场景下,这通常没什么问题。
但问题出在开启 IPv6 透明代理的场景中。
PassWall 后面会使用这个 WAN6 set 做 IPv6 目的地址豁免:
nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
passwall_wan6 里的地址会被直接返回,不进入后续代理链。
这个 set 相当于 WAN6 本机地址豁免表,也可以理解为出口地址豁免表。
原版每次更新时都会执行:
nft flush set $NFTABLE_NAME $NFTSET_WAN6
这个动作会产生一个短暂窗口:passwall_wan6 被清空,但新地址还没插入完成。这个窗口通常很短,但在以下情况下会变得危险:
- IPv6 地址频繁变化,例如 DHCPv6/PD、RA、热插拔、接口 flap;
- PassWall 热更新规则时,同时触发网络 hotplug;
- 多个
update_wan_sets进程并发执行; - nft set 插入过程被打断,或执行顺序交错;
- IPv6 TProxy 正在工作,流量持续经过
PSW_MANGLE_V6。
此时,ip6 daddr @$NFTSET_WAN6 return 暂时匹配不到 WAN6 地址,本应直连或豁免的 IPv6 流量可能继续进入 TProxy 规则,结果可能导致:
- WAN6 本地地址流量被误代理;
- 出现 IPv6 连接异常或断流;
- 更严重时,可能产生代理回环,或导致访问路由器自身服务异常;
- 并发刷新时,后完成的旧进程还可能扰乱,甚至覆盖新进程的结果。
这里做了什么
第一步:把原来的主体函数改名为 _update_wan_sets(),外面新增一个带锁的 update_wan_sets():
update_wan_sets() {
local log=$1
[ -z "$(command -v get_wan_ips)" ] && . "$UTILS_PATH"
[ -d "$LOCK_PATH" ] || mkdir -p "$LOCK_PATH"
(
flock -x 9 || exit 1
_update_wan_sets "$log"
) 9>"${LOCK_PATH}/${CONFIG}_update_wan_sets.lock"
}
这解决了并发问题。无论是 hotplug、PassWall reload、手动调用,还是多个网络事件同时触发,最终同一时间只有一个进程能进入真正的 WAN set 更新逻辑。
第二步,也是最关键的行为变化:IPv6 TProxy 开启时,不再 flush WAN6 set。
local WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
if [ "${PROXY_IPV6:-$(config_t_get global_forwarding ipv6_tproxy 0)}" != "1" ]; then
nft flush set $NFTABLE_NAME $NFTSET_WAN6
fi
echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6 "-1"
}
也就是说:
- 如果
ipv6_tproxy != 1,行为和原版一样:清空passwall_wan6,再插入当前 WAN6; - 如果
ipv6_tproxy == 1,跳过 flush,直接将当前 WAN6 插入 set。
这背后的判断很明确:IPv6 TProxy 开启时,passwall_wan6 不是普通缓存,而是正在被 nft 规则实时引用的关键豁免集。清空它会导致瞬时错误转发,所以宁可保留旧值,也不要让集合短暂为空。
两者的本质差异
原版是“强一致刷新”:
清空旧 WAN6 -> 插入新 WAN6
优点是集合干净,旧地址不会残留;缺点是中间存在空窗期。
MitaHill 版在 IPv6 TProxy 开启时改成“不中断补充更新”:
保留现有 WAN6 -> 插入新 WAN6
优点是 passwall_wan6 始终有内容,不会在 TProxy 运行中失去豁免能力;缺点是旧 WAN6 地址可能暂时残留在 set 里。但相比“清空导致实时流量误进代理”,这是更小的风险。
再加上 flock 后,MitaHill 版的实际流程变成:
获取锁
- 更新 IPv4:仍然 flush + insert
- 更新 IPv6:
- 如果没开 IPv6 TProxy:flush + insert
- 如果开了 IPv6 TProxy:不 flush,只 insert
释放锁
实验结果
目前运行了11天,主路由ImmortalWRT J1800
目前是稳定运行的,包含着singbox的分流和URL-Test
贡献和引用
最初的想法来源于nodeseek的用户Shikanoko在我发布的主题帖中回答了我的问题,我认为可能有关系,于是使用CodeX对Passwall的源码进行了编辑和编译,最终才进行了这样的结果。
以及,在Passwall2的Github issues中,同样有人遇到了此问题,可能是同一问题。
提供经过我修改的Passwall插件以及源码在Github上基于Passwall 2026.4.28 提交 进行修改,核心改动为passwall: avoid WAN6 set flush during IPv6 TProxy updates
插件的稳定性仍然需要进行测试,在测试3个月,也就是2026.7.x -> 2026.8.x左右仍然运行稳定,我将考虑合并到Passwall主分支。欢迎对代码进行审查。
发表评论