发生了什么

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"
    }
}

它的流程是:

  1. 读取当前 WAN IPv4;
  2. 清空 passwall_wan
  3. 将当前 WAN IPv4 重新插入;
  4. 读取当前 WAN IPv6;
  5. 清空 passwall_wan6
  6. 将当前 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主分支。欢迎对代码进行审查。

下载经过修改的正式版