跳过正文
  1. 博客文章/

一次 CGNAT 会话耗尽排查:为什么防火墙没丢包,客户端却上不了网

··1098 字·6 分钟·
运维实战 故障复盘 PfSense CGNAT 双 WAN 策略路由 网络排障 FortiGate 公网 IPv4 IPv6 Technitium DNS
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
高峰期,办公室终端访问网页时一会儿秒开,一会儿卡死。pfSense 本机访问同一站点正常,WAN 网关也没有丢包。几轮高峰期采到的数据都指向运营商侧:CGNAT 会话数或可用端口可能耗尽。

文中的地址、域名和设备唯一标识均已匿名化,监控指标名称和原始量级保留。不能用这些数据推算任何具体网络。

1. 现象与结论
#

办公终端先经过企业路由器,再从两条 WAN 选择出口。WAN1 后面是 pfSense,WAN2 后面是负责 PPPoE 和 NAT 的 FortiGate。两条线路分别拨号,各自建立上游映射。

flowchart LR
    C[办公终端与 VLAN] --> R[企业双 WAN 路由器]
    R -->|WAN1| P[pfSense]
    R -->|WAN2| F[FortiGate]
    P --> C1[运营商 CGNAT 出口 A]
    F --> C2[运营商 CGNAT 出口 B]
    C1 --> I[Internet]
    C2 --> I
    D[内部 DNS] --> P
    K[内部应用] --> P

一台固定走 WAN1 的 Linux 终端连续请求同一站点,10 次里只有 5 次成功。DNS 始终在 1 至 2 毫秒内返回,失败都发生在 TCP connect 阶段。相同时间,pfSense 本机 10 次请求全部成功,PPPoE 网关丢包为 0%。

失败连接大多停在 SYN_SENT。我为这件事加了四个指标:

指标含义
W4当前经过 WAN NAT 的 IPv4 逻辑会话
A4最近 120 秒内计数器有变化的会话
C4当前采样周期新出现的会话
F4_ratio失败握手状态数除以 W4

现场用 F4_ratio 观察失败握手状态在当前 WAN IPv4 会话中的占比。分母是 W4,不是 SYN 尝试总数,不能把它读成 SYN 丢包率。早期几次高峰里,W4 接近一万条时 F4_ratio 会迅速上升,终端也同时出现连接超时;后续样本没有重复出一个固定门槛,第 6 节再展开。

指标调整前稳定后
pfSense WAN IPv4 会话约 12,000约 6,000
F4_ratio17% 至 37%约 1.7% 至 2.5%
TP-Link 出口会话分布约 90:10约 50:50
长驻 WAN TCP DNS 会话约 1,0000
数据库 TIME_WAIT 峰值约 3,5000
WAN2 网关 CPU idle一度 1%40% 至 66%

PF 本地状态数不能当成运营商 CGNAT 映射数。两侧超时不同;PPPoE 重拨会使运营商侧旧映射失效,而 pfSense 本地状态可能继续保留。运营商没有给出真实配额,我没法直接证实 CGNAT 已耗尽,这个判断来自几轮高峰期同步采到的数据。

2. 排除的几个假线索
#

PF 状态表没有满
#

PF 状态一度从两万多涨到四万多,当时配置的 state limit 约为 161 万,峰值约占 2.5%。内存、mbuf、socket 和哈希表指标未见压力。

PF 状态数与运营商 CGNAT 映射数没有一一对应关系。提高 state limit 只增加本地容量,不能提高上游配额。

RRD 图表被多个采集器写坏
#

目标地址图曾在一分钟内从几百跳到几千。我最初以为是 CDN 突发,检查进程后发现系统同时运行了八个 RRD 采集器。七个孤儿进程的 PPID=1,它们与正常进程共同写同一个临时文件,没有单例锁。

处理时没有重启服务。我逐个核对 PID、父进程和完整命令,只停止七个孤儿。保留的采集器继续每分钟更新 RRD;文件修改时间连续十五分钟稳定推进后,图表才重新用于容量判断。

以后遇到同类尖峰,我会同时核对采集器数量和原始状态;两组数据一致后才调整防火墙。

一轮 WAN2 CPU 尖峰来自残留抓包进程
#

完成分流后,FortiGate CPU idle 一度只剩 1%,业务请求却很正常:约四千条会话,内存和吞吐都没到瓶颈。进程采样是这样的:

cmdbsvr    28% 至 76%
miglogd    18% 至 25%
snifferd   合计约 30% 至 40%

设备里残留了十个 snifferd。它们是验证 WAN 分流时启动的无限抓包;本地 SSH 超时后,远端抓包没有跟着退出。

我逐个核对 PID 和完整过滤条件,确认不是业务进程后才发送 SIGTERM。每个 snifferd 的配对 newcli 会跟着退出:

diagnose sys kill 15 <PID>
fnsysctl ps | grep snifferd

清理后会话数没有明显变化,CPU idle 回到 40% 至 66%。新建 PPPoE 会话仍显示 no_ofld_reason: non-npu-intf。降低日志量只能稍微减轻 CPU 占用,PPPoE 虚拟接口依旧无法使用 NP4Lite。

这次清理只能解释当时那一轮尖峰。后面没有 snifferd 时,cmdbsvr 仍出现过高占用;get system performance status 本身也会启动 CLI 查询,放大单核设备上的 CPU 读数。WAN2 采集后来改成每 5 分钟只执行一次 diagnose sys session stat,不再抓 CPU、内存和吞吐。

后面再做远端抓包,我会限制包数,结束后顺手查一次进程:

diagnose sniffer packet any 'host 192.0.2.10' 2 1000 l
fnsysctl ps | grep snifferd

3. 减少应用产生的连接
#

DNS 转发
#

Technitium DNS 集群最初强制使用 TCP、并发数为 10,同时查询四个上游。forwarderTimeout=10000msforwarderRetries=3、并发转发开启,DNSSEC 验证关闭。再叠加 pfSense 的 24 小时 tcp.established,WAN 侧慢慢攒出约 1,035 条 TCP DNS 状态。

防火墙 timeout 尝试失败
#

我先给 pfSense 的 DNS 规则设置短 timeout,随后发现选错了规则:allow2dns 只匹配客户端访问内部 DNS,不匹配 DNS 服务器访问公网。只改 LAN 入方向也不会缩短 PPPoE 出方向状态。

补上的 WAN 浮动规则又漏了 Disable Reply-To,pfSense 自动生成:

reply-to (pppoe0 gateway)

新的 TCP DNS 查询开始卡在 SYN_SENT:CLOSED,只有一包 SYN。规则随即回滚,没有清状态,也没有继续调整全局 timeout。

协议切换,并发 10 降到 2
#

Technitium 集群设置会同时作用于两个节点,不能做单节点 canary。变更前分别导出两个节点的配置备份,并完成 16 次无副作用查询:两个节点、四个上游,每个上游测试普通 A 记录和根 DNSKEY,全部返回 NoError

这轮只改两项:

forwarderProtocol:    Tcp → Udp
forwarderConcurrency: 10  → 2

这里的 2 只是过渡值。四分钟后,WAN TCP/53 状态从 1,035 降到 854,TIME_WAIT 从 231 降到 129,同时出现约 330 条 UDP/53 状态。TCP 状态后来降到 328,再自然归零;我没有执行 pfctl -k

并发 2 降到 1
#

改用 UDP 后,映射不再保留那么久。把并发从 2 降到 1,减少的是同一时刻发往上游的查询。DNS 公网状态从约 2,399 降到 1,390,70 秒后稳定在 1,321,减少 44.9%;普通 UDP 查询和 TCP fallback 都返回 NoError

少一条并行上游路径后,缓存未命中的尾延迟可能上升。现场没看出延迟变化,我就把并发保留在 1。

上游从 4 个减到 2 个
#

我又删掉两个低优先级上游,会话却从约 1,286 变成 1,337,完全落在正常波动里。这一步没省下什么。两个上游最后还是保留了,只为故障冗余。

参数初始值最终值结果
forwarderProtocolTCPUDP长驻 TCP 映射降到 0
forwarderConcurrency101DNS 会话下降约 45%
上游数量42对总会话影响很小
forwarderTimeout10000ms10000ms未调整
forwarderRetries33未调整

全局 UDP timeout 没有调整,避免影响视频会议、VoIP、QUIC 和游戏;也没有强制放大 CDN 域名 TTL。UDP 仍占用 CGNAT 映射,但空闲状态不会像 TCP established 那样在 pfSense 里保留一天。

数据库连接池
#

另一个大尖峰来自内部 MySQL。后来确认这些连接来自 Kubernetes 工作负载,连接池配置如下:

DB_MAX_OPEN=100
DB_MAX_IDLE=10
ConnMaxLifetime=5m
ConnMaxIdleTime=10m

连接池回落时会关闭超过 10 条的空闲连接;超过 5 分钟生命周期的连接在归还连接池后也会陆续关闭。现场一度出现约 3,500 条 TIME_WAIT。

修改后,DB_MAX_IDLE 为 25,ConnMaxLifetime 默认为 15 分钟,ConnMaxIdleTime 保持 10 分钟。稳定采样中,连接维持在 20 至 26 条 ESTABLISHED,TIME_WAIT 降到 0。

这段流量发生在内部 VLAN,不经过 PPPoE。它应该修,但不会占用运营商 CGNAT 配额。

4. pfSense 的两个 TCP timeout 调整
#

参数原值新值原因
tcp.established86,400 秒43,200 秒回收连续空闲超过 12 小时的状态,仍高于 RFC 5382 的 7,440 秒下限
tcp.closing900 秒450 秒更快回收 FIN_WAIT/CLOSING,仍高于 240 秒量级的兼容性边界

tcp.established 调整后,约三千多条连续空闲超过 12 小时的状态被回收。它减少的是 pfSense 本地历史状态,不会通知运营商删除 CGNAT 映射。

UDP、adaptive timeout、state limit 和静态端口 NAT 均保持不变。全程没有清空状态表。

5. 双 WAN 为什么没有按权重分流
#

隐藏的 ISP 选路覆盖了负载均衡
#

企业路由器已经启用双 WAN,界面权重为 4:6,但二十秒新 SYN 抓包显示,WAN1 拿走了接近九成连接。切换连接均衡并把权重改成 3:7 后,结果仍然接近 90:10。

TP-Link 双 WAN 负载均衡配置,界面权重与实际分流并不一致
界面设置了双 WAN 权重,但实际新连接仍集中在 WAN1

我又让两台终端访问一组从未使用过的测试地址,免得旧连接干扰判断。一台始终走 WAN1,另一台始终走 WAN2。“特殊应用程序选路”确实在按主机保持出口,改权重不会迁走已经固定的主机。

接着查 传输控制 → 路由设置 → ISP 选路,才看到一条历史规则 dianxing。它把某运营商地址组强制指向 WAN1,而两条宽带恰好来自同一运营商。关掉这条规则后,新连接先从 90:10 改善到约 75:25。

这条 dianxing 是现场以前加的,不是 TP-Link 默认配置。其他设备没有这条规则就直接跳过,别照着文章新建。

TP-Link 设置最终状态原因
流量均衡开启按连接数参与双 WAN 分流
智能均衡关闭现场瓶颈是会话数,不是带宽占用
特殊应用程序选路开启保持视频会议、SIP、FTP 等相关连接使用同一出口
dianxing ISP 选路关闭两条线路来自同一 ISP,规则会把大部分目标压到 WAN1

截图记录的是测试阶段,“特殊应用程序选路”当时曾临时关闭;生产恢复后重新开启。

先建内部目的地址组
#

这台 TP-Link 的 PBR 比普通静态路由更早参与选路,生效接口又只有 WAN1 和 WAN2。我不敢直接写“源 VLAN → ANY → WAN2”,那样可能连内部服务器流量也一起送走。

我先从 系统路由静态路由 页面抄出所有必须经 WAN1 到达的网络:

WAN1_TRANSIT     10.0.0.0/24
SERVER_NETS_A    10.20.0.0/16
SERVER_NETS_B    10.30.0.0/16
REMOTE_NETS      10.40.0.0/16
OVERLAY_NETS     172.20.0.0/16

菜单路径是 对象管理 → 地址管理 → 地址。每段网络单独建对象,再到 对象管理 → 地址管理 → 地址组,组合成 INTERNAL_NETS

TP-Link 地址对象示例,真实名称与 CIDR 已替换
WAN1 内部网络、canary 和待分流网段使用独立地址对象

策略路由里选不到 LAN/VLAN 接口,本地直连 VLAN 会不会被影响,配置页给不了答案。我只敢先放一台 canary,等真实流量验证通过再扩。

/32 canary 验证两条规则
#

我先在 对象管理 → 地址管理 建一个低连接终端:

CANARY_HOST = 192.168.50.171/32

然后打开 传输控制 → 路由设置 → 策略路由,按下面的顺序建两条规则:

字段内部流量规则公网流量规则
规则名称CANARY_INTERNAL_WAN1CANARY_PUBLIC_WAN2
服务类型ALLALL
源地址CANARY_HOSTCANARY_HOST
目的地址INTERNAL_NETSIPGROUP_ANY
生效接口WAN1WAN2
生效时间AnyAny
强制
顺序12

两条规则的顺序不能换。公网规则不要勾“强制”。设备还要启用 WAN2 在线检测并配置备用出口;WAN2 被判定离线后,新连接才会回退 WAN1。

保存后我没有清状态,旧连接继续走原出口。canary 检查四件事:

  1. 公网 IP 与已知 WAN2 测试机一致;
  2. 内部 DNS 和内部服务端口正常;
  3. 从其他 VLAN 新建连接到 canary 能正常返回;
  4. WAN2 断开后,新公网连接能够回退 WAN1。

第三项是我最担心的。它用真实流量确认 IPGROUP_ANY 没有破坏本地直连 VLAN;这项没过,我不会把整段 VLAN 放进来。

从单机扩到地址组
#

canary 通过后,我没有给每个网段再复制一套规则,而是建了 WAN2_PBR_SOURCES。两条生产规则共用这个源地址组:

SRC_INTERNAL_WAN1: WAN2_PBR_SOURCES → INTERNAL_NETS → WAN1,强制=是
SRC_PUBLIC_WAN2:   WAN2_PBR_SOURCES → IPGROUP_ANY   → WAN2,强制=否

TP-Link 地址组示例,成员已匿名化
canary、内部目的地址和 WAN2 分流源地址分别成组

TP-Link 双规则 PBR,真实对象名已替换
内部规则在前且强制走 WAN1,公网规则在后并允许 WAN2 故障回退

每次扩容只往 WAN2_PBR_SOURCES 加一个地址对象。我会先确认内部规则已经覆盖它,再放公网流量去 WAN2,避免内部豁免尚未生效。

flowchart TD
    S[WAN2_PBR_SOURCES 新连接] --> R1{内部目的地址?}
    R1 -->|是| W1[WAN1 / 强制]
    R1 -->|否| R2[ANY / WAN2 / 不强制]
    R2 --> H{在线检测判定 WAN2 正常?}
    H -->|是| W2[从 WAN2 建立新会话]
    H -->|否| W1
    L[本地直连 VLAN] --> D[设备直连路由
canary 已验证]

当时 WAN1/WAN2 约为 4,726/2,482,要接近均衡需迁移约 1,122 条。选择一个约 1,083 条连接的地址范围后,实际稳定在 3,977/3,914。已有连接不会迁移,新连接才进入 WAN2。

每次加完先看十五分钟,最后至少覆盖一个完整高峰。我盯着 F4_ratio、WAN2 CPU 和会话数,并用真实终端测试内部访问与故障回退。

要回滚也不用删规则或清状态。从 WAN2_PBR_SOURCES 移除刚加入的对象即可;新连接恢复原来的均衡策略,旧连接自己结束。

6. 这套网络接下来怎么扩
#

先把几个数字分开
#

现场高峰有 210 台左右的终端,TP-Link 显示 5,506 条终端连接,最高的一台是 379/400。pfSense 上的 W4 则是经过 WAN NAT 的 IPv4 逻辑会话。三个数字看起来都在说“连接数”,统计对象并不相同,更不能直接拿来推算运营商的映射表。

故障后,单终端默认上限从 300 收紧到 200。它主要用来拦异常下载、P2P 和失控程序。真有终端反复触顶,就查这台机器的进程和目的端口。如果大家都在正常增长,继续往下压单机上限只会让正常用户更早碰壁。

后来采样多了,连“5000 条左右开始卡”也站不住了。Grafana 记录的本地 WAN 会话有时跑到 6000 多条还正常,有时 3000 多、4000 多条就开始连不上;这些数不是运营商 CGNAT 映射数。卡顿时 F4_ratio 往往会升高,但单看本地会话总数猜不出下一次什么时候出问题。

双 WAN 会话数、握手失败率与 TP-Link 终端连接监控
同一时段两条 WAN 的会话数和握手失败率

截图这一刻,WAN1/WAN2 分别有 5,019 和 6,770 条本地会话,握手失败率为 3.7% 和 4.6%;当天峰值则到过 18.26% 和 14.79%。

这种漂移可能与 PPPoE 重拨后落到不同运营商节点有关,也可能只是当时共享地址池的负载不同。现场没有运营商侧数据,这两种解释都还不能确认。

装机师傅给的说法是:普通家庭宽带约 4096 条,有公网 IPv4 后约 6 万条。他没有说明这是 CGNAT 映射数、接入策略还是设备会话容量,所以我先把它当作口头经验记录。4096 与几次卡顿的量级接近,却解释不了 6000 多条仍然正常的情况。

按 250 台设备、每台平均 40 条活动连接算,平时就可能超过 10,000 条,系统更新时还会再冲一截。运营商到底给了多少配额,还是得从晚高峰复测里慢慢确认。

公网 IPv4 怎么验
#

下一步准备向运营商申请动态公网 IPv4。换好后先看 WAN 地址,确认它不在 100.64.0.0/10 或 RFC 1918 私网范围;再从外部查询出口 IPv4,看结果是否与 WAN 地址一致。

这两项对得上,只能说明 CGNAT 已经绕开。能扛多少会话,我现在没有数据,还得等一个完整高峰,看新连接成功率和 F4_ratio。出口路由器、防火墙和运营商接入侧也都有自己的上限。

套餐从 500M 升到 1G 只会增加带宽。拨号账号的会话资源不变,卡顿照样可能回来。

现场已经更换过一次宽带接入设备,但具体更换部件没有记清,暂时不把这项变化算进结论。新配置要在重启后重新确认;公网 IPv4 还在申请中,地址下发前先做 CGNAT 线路的晚高峰基线,下来后再用相同时间段对照。

公网 IPv4 还没下来时
#

DNS、数据库连接池和 TP-Link 分流都已经改过了,单终端上限也从 300 收紧到 200。公网 IPv4 下来之前,先让这些改动跑过几个高峰,再看还剩多少失败连接。

真要继续加容量,我会接另一条独立 PPPoE 或换一家运营商。新的拨号通常会让运营商侧重新分配映射。应用也得继续查,HTTP 客户端、数据库驱动和代理程序能复用连接的地方,少开一条就是少占一条。

提高 PF state limit 或 Linux 的 nf_conntrack_max 只会扩大本地表。清掉 pfSense 状态也不会通知运营商立即释放映射。我曾看到把全局 tcp.established 直接降到 600 秒的建议,差点照搬;RFC 5382 给出的下限是 7,440 秒,线上会议、WebSocket 和低频长连接都可能被误伤。现场最后保留 43,200 秒,只把 tcp.closing 从 900 秒降到 450 秒。

IPv6 先测再分流
#

原生 IPv6 流量不占 IPv4 CGNAT 映射,双栈站点可以分走一部分连接。先别急着全开:实际路径、PMTU 和客户端的地址族选择都可能出问题。Happy Eyeballs 也没有固定的 300 毫秒等待值,不同实现会有差异。

mihomo 里如果已经有独立的 IPv4、IPv6 出口节点,可以让 url-test 定期访问同一个探测地址,按延迟选择。只有一个出口时,加策略组并不会改善路由。我也不会一次把所有站点切到 IPv6,先从普通网页开始,视频会议和业务域名单独验证。

什么时候停手
#

WAN1、WAN2 和 TP-Link 的数据已经进了 Grafana,Prometheus 负责告警。我平时看会话数、建连速率、失败握手比例、出口资源和单终端连接数。

晚高峰复测使用的 WAN1 吞吐与双 WAN 建连速率
设备重启和公网 IPv4 下发前后的对照指标

这张图只记录 WAN1 吞吐和两条线路的建连速率,不拿其中某一条曲线单独解释卡顿。判断仍以真实终端连接测试、F4_ratio 和本地 WAN 会话变化为主。

高峰期 F4_ratio 能持续低于 5%,终端请求成功率超过 95%,出口设备也没有资源压力,就先停在这里。没必要为了图表上的 50:50 继续调权重。如果仍然反复越线,再申请公网 IPv4 或增加独立拨号,不再从全局 timeout 里挤空间。

7. 参考资料
#

相关文章

pfSense 翻车记:一次防火墙重启引发的故障与恢复全过程
·729 字·4 分钟
运维实战 故障复盘 防火墙 故障排查 ZFS +5
香橙派 Zero 3 上的 Gitea 防护:OpenResty、CrowdSec 与 OpenAppSec
··2554 字·12 分钟
运维实战 安全 Gitea ARM64 OpenResty +3
在 RDP 远程桌面中运行 PADS:从 API Hook 到 AppInit_DLLs 的工程实践
·1165 字·6 分钟
逆向工程 Pads Rdp Ci-Cd Windows +3