引言

【老板定制需求】

在多条 PPPoE 接入线路的环境下,PCC(Per Connection Classifier) 是一种常用的负载均衡方案。它能够根据 IP 地址、端口等信息将连接均匀分布到多条线路上,实现带宽叠加和高可用。

然而,当某条 PPPoE 线路意外掉线时,静态 PCC 规则仍然会将新连接分配给已离线的接口,导致连接超时、网页打不开等问题。虽然 RouterOS 支持通过脚本监控接口状态,但手动调整 PCC 规则既繁琐又容易出错。

本文介绍一个 动态 PCC 调整脚本,它能自动检测所有 PPPoE 接口的在线状态,根据在线接口数量重新计算 PCC 的“分子/分母”,并启用或禁用对应的防火墙标记规则。通过定时运行此脚本,您的负载均衡配置将始终保持与当前在线线路数一致,有效提升网络的可靠性和用户体验。

脚本功能概览

  • 统计指定范围内的 PPPoE 接口(如 pppoe-out1pppoe-out20)的运行状态。
  • 如果没有任何接口在线,禁用所有相关的 PCC 规则,防止流量走无效线路。
  • 如果部分接口在线,则按接口编号顺序为每个在线接口分配一个递增的分子值(0 ~ N-1),分母为在线接口总数 N,并启用对应的防火墙标记规则。
  • 离线的接口,其对应的所有规则会被禁用,避免残留规则干扰流量。

使用环境要求

  • MikroTik RouterOS(版本需支持脚本和 PCC)。
  • 已配置多个 PPPoE 客户端接口(例如 pppoe-out1pppoe-out2……)。
  • 已根据静态 PCC 思路创建了基本的防火墙标记(mangle)规则(下文会给出模板)。
  • 有基本的 RouterOS 命令行或 WinBox 操作经验。

脚本内容

# 动态调整 PCC 负载均衡(基于 PPPoE 接口状态)
# 检测所有 pppoe-out 接口,根据在线数量重新分配 PCC 分子/分母
# 通过 scheduler 定期执行(例如每分钟一次)

:local N 0
# 第一次循环统计在线接口数
:for i from=1 to=20 do={
   :local intName "pppoe-out$i"
   :local status [/interface pppoe-client get [find name=$intName] running]
   :if ($status = true) do={ :set N ($N + 1) }
}

:if ($N = 0) do={
   # 所有接口均掉线,禁用全部 PCC 规则
   :for i from=1 to=20 do={
      :local intName "pppoe-out$i"
      # 禁用 prerouting mark-connection
      :local connRule [/ip firewall mangle find where chain=prerouting and action=mark-connection and new-connection-mark="C$i"]
      :if ([:len $connRule] > 0) do={
         :local currDisabled [/ip firewall mangle get $connRule disabled]
         :if ($currDisabled != yes) do={
            /ip firewall mangle set $connRule disabled=yes
         }
      }
      # 禁用 prerouting mark-routing
      :local routeRule [/ip firewall mangle find where chain=prerouting and action=mark-routing and new-routing-mark="R$i"]
      :if ([:len $routeRule] > 0) do={
         :local currDisabled [/ip firewall mangle get $routeRule disabled]
         :if ($currDisabled != yes) do={
            /ip firewall mangle set $routeRule disabled=yes
         }
      }
      # 禁用 input mark-connection
      :local inRule [/ip firewall mangle find where chain=intput and action=mark-connection and in-interface=$intName]
      :if ([:len $inRule] > 0) do={
         :local currDisabled [/ip firewall mangle get $inRule disabled]
         :if ($currDisabled != yes) do={
            /ip firewall mangle set $inRule disabled=yes
         }
      }
      # 禁用 output mark-routing
      :local outRule [/ip firewall mangle find where chain=output and action=mark-routing and connection-mark="C$i"]
      :if ([:len $outRule] > 0) do={
         :local currDisabled [/ip firewall mangle get $outRule disabled]
         :if ($currDisabled != yes) do={
            /ip firewall mangle set $outRule disabled=yes
         }
      }
   }
   :log info "所有 PPPoE 接口离线,PCC 规则已禁用"
} else={
   :local order 0
   # 第二次循环按接口编号顺序分配分子(0 ~ N-1)
   :for i from=1 to=20 do={
      :local intName "pppoe-out$i"
      :local status [/interface pppoe-client get [find name=$intName] running]
      :if ($status = true) do={
         :local classifier "both-addresses-and-ports:$N/$order"
         
         # 处理 prerouting 链的 mark-connection 规则(设置分类器和启用)
         :local connRule [/ip firewall mangle find where chain=prerouting and action=mark-connection and new-connection-mark="C$i"]
         :if ([:len $connRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $connRule disabled]
            :local currClassifier [/ip firewall mangle get $connRule per-connection-classifier]
            :if ($currDisabled != no || $currClassifier != $classifier) do={
               /ip firewall mangle set $connRule disabled=no per-connection-classifier=$classifier
            }
         }
         
         # 处理 prerouting 链的 mark-routing 规则(只需启用)
         :local routeRule [/ip firewall mangle find where chain=prerouting and action=mark-routing and new-routing-mark="R$i"]
         :if ([:len $routeRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $routeRule disabled]
            :if ($currDisabled != no) do={
               /ip firewall mangle set $routeRule disabled=no
            }
         }
         
         # 处理 input 链的 mark-connection 规则(只需启用)
         :local inRule [/ip firewall mangle find where chain=intput and action=mark-connection and in-interface=$intName]
         :if ([:len $inRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $inRule disabled]
            :if ($currDisabled != no) do={
               /ip firewall mangle set $inRule disabled=no
            }
         }
         
         # 处理 output 链的 mark-routing 规则(只需启用)
         :local outRule [/ip firewall mangle find where chain=output and action=mark-routing and connection-mark="C$i"]
         :if ([:len $outRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $outRule disabled]
            :if ($currDisabled != no) do={
               /ip firewall mangle set $outRule disabled=no
            }
         }
         
         :set order ($order + 1)
      } else={
         # 接口离线,禁用所有相关规则(同样加入状态判断)
         :local intName "pppoe-out$i"
         :local connRule [/ip firewall mangle find where chain=prerouting and action=mark-connection and new-connection-mark="C$i"]
         :if ([:len $connRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $connRule disabled]
            :if ($currDisabled != yes) do={
               /ip firewall mangle set $connRule disabled=yes
            }
         }
         :local routeRule [/ip firewall mangle find where chain=prerouting and action=mark-routing and new-routing-mark="R$i"]
         :if ([:len $routeRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $routeRule disabled]
            :if ($currDisabled != yes) do={
               /ip firewall mangle set $routeRule disabled=yes
            }
         }
         :local inRule [/ip firewall mangle find where chain=intput and action=mark-connection and in-interface=$intName]
         :if ([:len $inRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $inRule disabled]
            :if ($currDisabled != yes) do={
               /ip firewall mangle set $inRule disabled=yes
            }
         }
         :local outRule [/ip firewall mangle find where chain=output and action=mark-routing and connection-mark="C$i"]
         :if ([:len $outRule] > 0) do={
            :local currDisabled [/ip firewall mangle get $outRule disabled]
            :if ($currDisabled != yes) do={
               /ip firewall mangle set $outRule disabled=yes
            }
         }
      }
   }
   :log info "PCC 已调整:$N 个接口在线,分子 0~$($N-1)"
}

脚本详解

  1. 统计在线接口数
    脚本第一个循环遍历 pppoe-out1pppoe-out20,通过 running 属性判断接口是否在线,得到在线总数 N
  2. 全离线处理
    N=0,说明所有线路断开,则将所有与 PCC 相关的规则禁用,并记录日志。这样可以避免流量因无可用出口而失败,后续由默认路由或其他机制处理。
  3. 部分在线处理

    • 重置 order 计数器为 0。
    • 再次遍历接口,对每个在线接口:

      • 构建 PCC 分类器参数 both-addresses-and-ports:$N/$order
      • 查找并更新 prerouting 链的 mark-connection 规则,设置 per-connection-classifier 并启用。
      • 启用 prerouting 链的 mark-routing 规则(无需分类器,依赖连接标记)。
      • 启用 input 链的 mark-connection 规则(注意原脚本链名为 intput,常见为 input,使用时请确认)。
      • 启用 output 链的 mark-routing 规则
      • 计数器 order 加 1。
    • 对离线接口,禁用其对应的所有四条规则。
  4. 日志输出
    每次调整后都会在系统日志中记录当前在线接口数和分配范围,便于排障。

如何修改脚本以适应自己的环境

1. 接口命名和数量范围

脚本默认处理 pppoe-out1pppoe-out20,如果你的接口名称不同或数量不同,请修改两处:

  • 循环的起始和结束数字:for i from=1 to=20 改为你的最大编号。
  • 接口名称模板:"pppoe-out$i" 改为你的实际前缀,例如 "pppoe-WAN$i""adsl$i"

2. 防火墙标记的名称

脚本中使用的标记名称遵循 C$i(连接标记)、R$i(路由标记),并且与接口编号绑定。如果你已有规则使用不同的命名风格(例如 conn-mark-pppoe1route-mark-1),你需要修改脚本中对应的查找条件:

  • 查找 new-connection-mark="C$i" 的地方改为你的连接标记名,如 "conn-mark-pppoe$i"
  • 查找 new-routing-mark="R$i" 的地方改为你的路由标记名,如 "route-mark-pppoe$i"
  • 查找 connection-mark="C$i" 的地方(output 链)也要同步修改。

3. 防火墙链的名称

脚本中出现了 chain=intput,这很可能是笔误,标准的 RouterOS 链名为 inputoutputprerouting。请根据你实际规则所在的链名进行调整。如果你在 input 链有规则,就将脚本中的 intput 改为 input

4. PCC 分类器类型

脚本使用了 both-addresses-and-ports,即根据源/目的地址和端口进行哈希。你可以根据需求改为:

  • src-address:仅源地址
  • dst-address:仅目的地址
  • src-port:仅源端口
  • dst-port:仅目的端口
  • both-addresses:仅源和目的地址

修改 :local classifier "both-addresses-and-ports:$N/$order" 中的分类器名称即可。

5. 规则存在性判断

脚本在修改规则前会查找规则是否存在,若找不到则跳过。你可以根据实际需要,预先创建好所有可能用到的规则(即使接口编号不存在),脚本只负责启用/禁用和修改分类器。这样可以避免规则缺失导致脚本报错。

预置 PCC 规则模板

为了让脚本正确管理,你需要提前创建好以下四种规则(每条线一组)。以接口 pppoe-out1 为例:

# 1. 入口连接标记(prerouting)
/ip firewall mangle add chain=prerouting action=mark-connection new-connection-mark=C1 \
    passthrough=yes per-connection-classifier=both-addresses-and-ports:1/0 disabled=yes

# 2. 入口路由标记(prerouting)
/ip firewall mangle add chain=prerouting action=mark-routing new-routing-mark=R1 \
    connection-mark=C1 passthrough=no disabled=yes

# 3. 本地上行连接标记(input)
/ip firewall mangle add chain=input action=mark-connection new-connection-mark=C1 \
    in-interface=pppoe-out1 passthrough=yes disabled=yes

# 4. 本地上行路由标记(output)
/ip firewall mangle add chain=output action=mark-routing new-routing-mark=R1 \
    connection-mark=C1 passthrough=no disabled=yes
  • 初始状态全部设为 disabled=yes,脚本会根据接口状态自动启用。
  • 对于第 1 条规则,per-connection-classifier 的分母/分子会被脚本动态修改,初始值可以随便填(如 1/0)。
  • 第 2、4 条规则依赖连接标记,因此无需分类器。
  • 第 3 条规则用于处理路由器自身发出的连接(如 DNS 查询、更新等),确保它们也能正确走对应线路。

请为每个接口(pppoe-out1 ~ pppoe-out20)重复上述四条规则,并将标记名中的数字改为接口编号。

设置定时调度

将脚本添加到 System → Scheduler,设置间隔(例如每 60 秒执行一次):

/system scheduler add name="dynamic-pcc" interval=1m on-event="/system script run dynamic-pcc-script" start-time=startup

也可以将脚本直接写在 scheduler 的 on-event 中,但为了便于维护,建议先将脚本保存为脚本(System → Scripts),然后在 scheduler 中调用。

测试与验证

  1. 手动断开某条 PPPoE 线路,观察脚本是否将该线路的规则禁用,其他线路的分类器是否重新调整。
  2. 检查日志(/log print)查看脚本输出信息。
  3. 使用 ip firewall mangle print 查看相关规则的 disabled 状态和 per-connection-classifier 值是否与预期一致。
  4. 进行实际的上网测试,确认负载均衡按在线线路数正常工作。

注意事项

  • 脚本依赖于规则存在,如果某个编号的规则不存在,find 可能返回空值,set 命令不会执行。建议确保所有可能用到的编号规则都已提前创建。
  • 如果接口数量很多(例如 50 条),脚本循环会稍慢,但每分钟执行一次对路由器性能影响极小。
  • 脚本只管理了标记规则,路由表或 NAT 规则需要单独配置(通常只需为每个接口添加一条默认网关,并设置路由标记即可)。

结语

通过上述动态 PCC 脚本,你的 RouterOS 负载均衡系统将具备自适应能力,在线路波动时自动调整,无需人工干预。只需根据实际环境调整接口范围、标记名称和链名,并创建好基础规则,就能大幅提升多线接入的稳定性。

如果你在部署过程中遇到问题,欢迎在评论区交流讨论。

最后修改:2026 年 03 月 28 日
如果觉得我的文章对你有用,请随意赞赏