一次IPSec VPN隧道因MTU不匹配导致ERP系统远程访问异常的排查实录

问题背景

公司在全国有8个分支机构,均通过FortiGate防火墙与总部建立IPSec VPN隧道,日常办公依赖总部的SAP ERP系统进行订单录入、库存查询和财务审批。周一上午9点,华南分公司的30多名员工集中登录ERP系统时,纷纷反馈页面加载极慢——登录界面要等2-3分钟才能显示,进入后点击菜单经常卡死超时,只能反复刷新碰运气。

奇怪的是,Ping总部的服务器完全正常,延迟稳定在15ms左右,traceroute也每一跳都通畅。其他通过VPN访问的服务(如邮件OA)虽然也有点慢,但不像ERP这样完全不可用。网络监控平台上VPN隧道状态显示绿灯”Connected”,流量统计正常,没有任何告警。问题持续了整个上午,严重影响了华南分公司的业务运转,紧急程度极高——财务月结审批被迫延迟,客户订单无法及时录入。

故障现象

故障的表现非常”诡异”,具有典型的MTU问题特征——小包通、大包不通:

现象一:Ping正常但ERP不可用

从华南分公司Ping总部ERP服务器10.10.1.100,100字节的小包100%成功,延迟15ms;但当Ping包大小超过1400字节时:

1
2
3
4
5
6
7
8
C:\Users\zhao> ping 10.10.1.100 -l 100 -n 10
Reply from 10.10.1.100: bytes=100 time=15ms TTL=62

C:\Users\zhao> ping 10.10.1.100 -l 1500 -n 10
Request timed out.
Request timed out.
Ping statistics for 10.10.1.100:
Packets: Sent = 10, Received = 0, Lost = 10 (100% loss)

现象二:ERP登录页面加载极慢或超时

浏览器访问https://erp.company.com时,登录页的HTML框架能加载出来(小数据包),但包含CSS/JS资源的大响应体经常卡在”传输中”状态,最终浏览器报ERR_CONNECTION_TIMED_OUT

现象三:SSH大文件传输失败

通过SSH从华南服务器向总部scp一个50MB的备份文件,传输开始后几秒就卡死:

1
2
3
$ scp backup.tar.gz [email protected]:/tmp/
backup.tar.gz 0% 0KB --:-- ETA
# 卡住不动,最终Connection reset by peer

但小文件(几KB的配置文件)可以正常传输。

现象四:TCP连接建立正常但数据传输中断

用Wireshark在华南分公司的客户端抓包,可以看到TCP三次握手顺利完成,客户端发送HTTP GET请求后,服务器回复了第一个数据包(SYN+ACK后的初始窗口),但当服务器连续发送多个满载(满1460字节)的TCP段时,客户端迟迟收不到后续数据,最终触发TCP重传,重传若干次后连接被reset。

关键发现:抓包中看不到任何ICMP Fragmentation Needed消息,说明IP分片所需的通知被某处拦截了。

排查过程

第一步:确认问题范围——是否只影响华南分公司

第一时间联系其他7个分支,发现华东分公司(同样通过IPSec VPN接入)也反馈ERP访问缓慢,但华北分公司(通过专线接入,不走VPN)完全没有问题。这个对比非常关键——专线正常、VPN异常,问题锁定在VPN隧道层面。

第二步:排除ERP服务器本身的问题

直接在总部内网访问ERP服务器,响应迅速,没有任何异常。检查ERP服务器的系统负载、数据库连接池、Web服务器线程池,全部在正常范围内。确认问题不在服务器端。

第三步:排除VPN隧道连通性问题

VPN隧道状态在FortiGate监控面板上显示为绿色”Up”,路由表正确,Ping小包100%通过。初步排除隧道本身断裂的可能。但注意到隧道接口的MTU配置——FortiGate上VPN隧道接口默认MTU值:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 总部 FortiGate (FGT-HQ)
FGT-HQ # diagnose netlink interface list vpn-tunnel-HN
Name: vpn-tunnel-HN
MTU: 1436
Type: IPSec
Status: up

# 华南 FortiGate (FGT-HN)
FGT-HN # diagnose netlink interface list vpn-tunnel-HQ
Name: vpn-tunnel-HQ
MTU: 1436
Type: IPSec
Status: up

隧道MTU为1436,这是因为IPSec ESP封装会占用额外头部空间(ESP头8字节 + ESP尾2字节 + ICV 12字节 + 外层IP头20字节 = 42字节,1500-42=1458…但实际FortiGate的默认值是1436,预留了更多空间给可能的UDP封装和NAT-T)。隧道MTU本身看起来没有异常。

第四步:用路径MTU探测定位关键问题

使用Linux的tracepath命令进行路径MTU探测:

1
2
3
4
5
6
7
# 从华南Linux服务器探测到总部ERP
$ tracepath 10.10.1.100
1?: [LOCAL] 10.20.1.10 mtu=1500
2: 10.20.1.1 (华南FG内网口) mtu=1500
3: no reply
4: 10.10.1.1 (总部FG内网口) mtu=1436 ← 链路MTU突降!
5: 10.10.1.100 (ERP服务器) mtu=1436

关键发现:数据包经过VPN隧道后,路径MTU从1500降到了1436。这意味着从ERP服务器发往华南客户端的TCP数据包如果满载1460字节,在经过VPN隧道时会被封装成超过1500字节的IP包,需要分片。

第五步:检查TCP MSS协商

查看FortiGate防火墙的TCP MSS配置:

1
2
3
4
5
# 华南 FortiGate
FGT-HN # show full-configuration | grep -i mss
set tcp-mss-sender 0
set tcp-mss-receiver 0
# 隧道接口未配置MSS协商

MSS值为0表示”未设置”——FortiGate不会在TCP SYN/SYN-ACK中主动修改MSS值。这意味着TCP连接建立时,ERP服务器(在1500 MTU的内网环境中)会在SYN-ACK中通告MSS=1460(MTU 1500 - 40字节IP+TCP头),华南客户端同理也在SYN中通告MSS=1460。

这就是问题所在:双方协商的MSS=1460,但数据实际要经过MTU=1436的VPN隧道。ERP服务器按MSS=1460发送满载TCP段,封装后IP包总长 = 1460 + 40(IP头) + 42(IPSec封装) = 1542字节,超过了出接口MTU 1500!这要么需要IP分片,要么被丢弃。

第六步:验证——为什么Ping小包通但大包不通

ICMP Echo Request小包(100字节)加上IPSec封装后仍远小于1500,无需分片,顺利通过。但1500字节的Ping包(IP层已达1500)加上IPSec封装后变成1542字节,超过了物理接口MTU,需要分片。

第七步:为什么没有ICMP Fragmentation Needed反馈

正常情况下,当路由器发现出接口MTU不足以转发不分片的包,且包的DF(Don’t Fragment)位为1时,应该返回ICMP Type 3 Code 4(Fragmentation Needed and DF Set)消息,通知发送方降低包大小。但我们在抓包中完全没看到这类ICMP消息!

进一步排查发现两个原因:

  1. FortiGate的IPS策略拦截了ICMP Fragmentation Needed消息——查看IPS Sensor配置,发现有一条默认规则ICMP.DOS.FRGNeeded将其标记为”Drop”,这是误将路径MTU发现的必要ICMP消息当作DOS攻击了。

  2. ERP服务器的Linux内核默认在TCP包中设置DF位——Linux的net.ipv4.tcp_df默认行为倾向于设置DF位以避免分片带来的性能损失。当DF=1的TCP包在VPN封装后超MTU且被FortiGate丢弃时,本应返回ICMP Fragmentation Needed,但被IPS拦截了。

双重打击:服务器设置了DF位禁止分片 → FortiGate需要返回ICMP通知 → IPS策略把ICMP通知拦截了 → 服务器永远不知道自己发的包太大 → 只能不断重传同样大小的包 → 连接卡死。

第八步:确认根因链路

完整的因果链路:

  1. IPSec隧道封装增加42字节开销 → 隀道MTU从1500降到1436
  2. TCP MSS协商未适配隧道MTU → 双方协商MSS=1460(应为1396)
  3. ERP服务器按MSS=1460发送满载TCP段 → IPSec封装后超物理接口MTU
  4. TCP包DF位=1 → 不能分片,由FortiGate丢弃
  5. FortiGate IPS策略拦截ICMP Fragmentation Needed → 发送方收不到PMTU通知
  6. 服务器不断重传超大的TCP段 → 连接永远无法完成数据传输
  7. 小包(<1436-40=1396字节)不受影响 → Ping小包正常、ERP小响应体能加载

解决方案

紧急止血(5分钟内生效)

方案一:在FortiGate隧道接口上启用TCP MSS协商

这是最标准的修复方式,在VPN隧道接口上设置MSS Sender和MSS Receiver值,让FortiGate在TCP SYN/SYN-ACK经过隧道时自动修改MSS值,将其限制在隧道MTU允许的范围内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 华南 FortiGate
config system interface
edit "vpn-tunnel-HQ"
set tcp-mss-sender 1396
set tcp-mss-receiver 1396
next
end

# 总部 FortiGate
config system interface
edit "vpn-tunnel-HN"
set tcp-mss-sender 1396
set tcp-mss-receiver 1396
next
end

MSS值计算:隧道MTU 1436 - 40(IP头+TCP头) = 1396。设置后,FortiGate会在TCP SYN经过隧道接口时将MSS Option从1460修改为1396,确保双方协商出的最大段大小不超过隧道承载能力。

修改后立即生效,无需重启隧道,已有连接会在下次TCP握手时使用新MSS值。

方案二:释放IPS对ICMP Fragmentation Needed的拦截

同时修复IPS策略,放行必要的PMTU Discovery ICMP消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 总部 FortiGate IPS Sensor配置
config firewall ips-policy
edit "default-ips"
config rule
edit 1
set name "allow-pmtu-icmp"
set signature "ICMP.DOS.FRGNeeded"
set status disable
set log enable
set action pass
next
end
next
end

ICMP.DOS.FRGNeeded规则从Drop改为Pass,确保路径MTU发现的ICMP消息能正常传递。这是TCP PMTU Discovery机制正常工作的必要条件。

验证修复效果

修改后从华南分公司测试:

1
2
3
4
5
6
7
8
9
10
11
12
# Ping大包测试
C:\Users\zhao> ping 10.10.1.100 -l 1500 -n 10
Reply from 10.10.1.100: bytes=1500 time=16ms TTL=62
Reply from 10.10.1.100: bytes=1500 time=16ms TTL=62
# 100%成功!

# ERP访问测试——登录页面3秒内完整加载,菜单点击秒级响应

# SCP大文件传输测试
$ scp backup.tar.gz [email protected]:/tmp/
backup.tar.gz 100% 50MB 12.8MB/s 00:03
# 正常完成!

全量修复——所有分支机构VPN隧道

紧急修复华南后,立即对所有8个分支的VPN隧道接口统一配置MSS:

1
2
3
4
5
6
7
8
9
# 批量配置脚本(总部侧)
for tunnel in vpn-tunnel-HN vpn-tunnel-HD vpn-tunnel-HB vpn-tunnel-HX vpn-tunnel-HW vpn-tunnel-HN2 vpn-tunnel-HS vpn-tunnel-HNE; do
config system interface
edit "$tunnel"
set tcp-mss-sender 1396
set tcp-mss-receiver 1396
next
end
done

并在每台分支FortiGate上同样配置对应的隧道接口。

根因分析

问题的根本原因是 IPSec VPN隧道的MTU开销未被TCP层感知和适配,叠加 IPS策略误杀PMTU Discovery的ICMP消息,形成了”黑盒”效应:

核心根因一:TCP MSS未适配隧道MTU

IPSec ESP封装会在原始IP包外增加约42字节的头部开销,导致物理链路上可承载的有效载荷从1500字节降到约1458字节(考虑NAT-T UDP封装则更低)。但TCP层在握手时协商的MSS是基于直连网段MTU 1500计算得出的1460,完全不知道中间有一个MTU更低的隧道。FortiGate的隧道接口默认未开启MSS协商修改功能(tcp-mss-sender/receiver=0),导致这个信息鸿沟无法自动弥合。

核心根因二:IPS拦截ICMP Fragmentation Needed阻断PMTU发现

RFC 1191定义的Path MTU Discovery机制本应作为MSS协商的后备方案——当TCP包因DF位无法分片而被中间路由器丢弃时,路由器应返回ICMP Type 3 Code 4消息通知发送方降低包大小。但FortiGate的IPS默认规则将此类ICMP消息标记为潜在DOS攻击并丢弃,使得PMTU Discovery机制完全失效。

两个根因叠加:TCP不知道隧道MTU限制(MSS过大),又无法通过ICMP得知自己发的包太大(PMTU Discovery被阻断),导致发送方陷入”盲重传”的死循环。

预防措施

1. 标准化VPN隧道TCP MSS配置

制定VPN隧道配置标准,所有新建隧道接口必须设置MSS值:

1
2
3
4
5
6
# FortiGate VPN隧道MSS配置标准
tcp-mss-sender = 隧道MTU - 40
tcp-mss-receiver = 隧道MTU - 40
# 常见值:
# IPSec ESP (无NAT-T):MTU=1458 → MSS=1418
# IPSec ESP (NAT-T/UDP封装):MTU=1436 → MSS=1396

将此标准纳入VPN隧道配置Checklist,新建或修改隧道时强制执行。

2. IPS策略优化——放行PMTU Discovery ICMP

在所有FortiGate的IPS Sensor中,将以下ICMP规则从默认的Drop改为Pass或至少Disable:

  • ICMP.DOS.FRGNeeded (Type 3 Code 4 - Fragmentation Needed)
  • ICMP.Info.SourceQuench (Type 4 - Source Quench,虽已废弃但部分设备仍使用)

这些ICMP消息是IP网络正常运行的必要组成部分,误杀它们会导致PMTU Discovery失效,远比所谓的DOS风险危害更大。

3. 定期路径MTU探测巡检

编写自动化巡检脚本,每周对所有VPN隧道执行路径MTU探测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# pmtu_check.sh - VPN隧道路径MTU巡检
BRANCHES=("10.20.1.0/24 华南" "10.30.1.0/24 华东" ...)
HQ_ERP="10.10.1.100"
ALERT_THRESHOLD=1400

for branch in "${BRANCHES[@]}"; do
network=$(echo $branch | awk '{print $1}')
name=$(echo $branch | awk '{print $2}')
gw=$(echo $network | sed 's/0\/24/1/')

# 从分支网关探测路径MTU
pmtu=$(tracepath -n $HQ_ERP 2>&1 | grep "pmtu" | tail -1 | awk '{print $NF}')

if [ "$pmtu" -lt "$ALERT_THRESHOLD" ]; then
echo "[ALERT] $name 隧道路径MTU异常: $pmtu (期望 >= $ALERT_THRESHOLD)"
else
echo "[OK] $name 隧道路径MTU正常: $pmtu"
fi
done

当路径MTU低于阈值时自动告警,防止MSS配置被意外修改或隧道MTU因封装方式变化而降低。

4. 网络变更流程纳入MTU审查

在FortiGate配置变更审批流程中加入MTU/MSS审查环节:

  • 新增VPN隧道 → 必须检查MSS配置
  • 更改IPSec封装方式(如从ESP改为ESP+UDP封装)→ 重新计算MSS值
  • 修改IPS策略 → 检查是否影响PMTU Discovery ICMP放行
  • 更换ISP或链路类型 → 测试路径MTU是否变化

5. 文档化MTU计算方法

编写内部网络运维手册中的MTU计算章节,记录所有VPN隧道的:

隧道名称 封装方式 预留开销 隧道MTU MSS值 物理接口MTU
vpn-HN ESP+NAT-T 64字节 1436 1396 1500
vpn-HD ESP 42字节 1458 1418 1500

让每位运维工程师都能快速查阅和计算正确的MSS值。

总结

这次故障的排查给我留下了深刻的教训:

教训一:Ping通不代表网络通。 Ping只是ICMP小包测试,它能证明链路可达,但完全不能证明大包传输能力。在涉及VPN隧道的故障排查中,必须用不同大小的Ping包(ping -l 1500)或tracepath进行路径MTU探测,才能发现隐藏的MTU问题。

教训二:MTU/MSS不匹配是VPN环境的”经典杀手”。 IPSec封装带来的MTU缩减是必然的、可计算的,但TCP层对此毫不知情。如果不主动在隧道接口配置MSS协商修改,就等于让TCP盲目发送超大数据包,轻则分片降速,重则完全卡死。这是VPN运维的基本功课,不能等出事了才补。

教训三:IPS策略不能一刀切地拦截ICMP。 安全策略的”拦截一切ICMP”看似防御了ICMP-based DOS攻击,实则破坏了PMTU Discovery这一网络基础机制。安全与可用必须平衡,对于RFC定义的必要ICMP消息(Fragmentation Needed、Time Exceeded等),必须放行。

教训四:故障排查要善于利用”小包通、大包不通”这个特征。 当你发现小请求能成功但大数据传输失败,或者Ping通但应用不通,第一反应就应该是检查MTU/MSS。这个特征太过典型,不必浪费时间去查服务器性能、应用日志、路由表——直接从MTU入手往往能最快定位。

最终,一个1436的隧道MTU和未设置的MSS参数,加上一条过于激进的IPS规则,三者合力制造了一个”Ping正常但业务瘫痪”的经典假象。修复只需两行配置,但理解它需要整个IP/TCP栈的知识。运维不只是改配置,更是理解协议。