美国服务器FRP内网穿透频繁断流?TCP MSS分片丢失深度排障笔记

故障初现:Client 连上了,但业务就像“羊癫疯”

客户托管的美国服务器(洛杉矶机房)通过 FRP 0.52.3 暴露内网一个 Web 服务。现象很诡异:

  • 从公网访问,TCP 三次握手正常(SYN, SYN-ACK, ACK 时序完美)
  • 但发送第一个 HTTP GET 请求后,卡住 10-30 秒,然后超时或收到乱序包
  • 并非每次必现,使用 curl -v --connect-timeout 5 --max-time 30 复现概率约 60%

第一反应是 FRP 的 buffer 或连接池问题,但这台服务器上跑着轻云互联的大带宽实例,之前压测过数千并发连接,网络层面不应该有瓶颈。排除法开始。

Step 1:排除 FRP 自身逻辑

直接在内网终端服务器上抓包:

# 在 FRP 客户端所在内网机器上
tcpdump -i eth0 -nn 'port 8080' -w /tmp/frp_client.pcap

# 在 FRP 服务端(美国服务器)上
tcpdump -i eth0 -nn 'port 7000 or port 8080' -w /tmp/frp_server.pcap

对比两个抓包文件,发现一个致命细节:

  • 内网 Client 收到 HTTP GET 后,正确响应了 1460字节的 TCP segment(DF 标志置位)
  • 美国服务器上的 FRP 服务端,将这部分数据通过 TLS 隧道转发出来时,IP 包总长度为 1500(刚好 MTU 上限)
  • 公网客户端回 ACK 后,下一个包就延迟了,且出现 TCP Retransmission

根因定位:MSS Clamping 失效 + 隧道封装放大

FRP 的 TLS 加密隧道有个隐藏特性:它会对原始 TCP 载荷进行 chunked encryption,每块加密后加上 8-32 字节的 TLS 头。当原始 HTTP Response 刚好填满 1460 MSS 时,经过 FRP 封装后的 IP 包会达到 1500+(通常 1508~1536)。

美国服务器网卡 MTU 是 1500,路径上某个中间节点(很可能是某段跨国海底光缆的运营商设备)设置了 ICMP Fragmentation Needed 黑洞——它不发送 ICMP 包,直接丢弃 oversized 包。

验证手法:

# 在美国服务器上测试路径 MTU
ping -M do -s 1472 -c 10 8.8.8.8    # 这个通常OK
ping -M do -s 1500 -c 10 8.8.8.8    # 丢包100%
ping -M do -s 1473 -c 10 8.8.8.8    # 丢包100%,确认MTU黑洞点在1473以上

但 FRP 的 TCP 流中,MSS 协商是在 FRP 客户端与 FRP 服务端之间(即内网机器 -> 美国服务器)完成的,而最终的 HTTP 流是 公网用户 -> FRP 服务端 -> FRP 客户端 -> 内网服务 的多段拼接。前一段(公网用户 -> 美国服务器)的 MSS 被正常限制了,但 FRP 服务端 -- FRP 客户端 段的 MSS 并没有被钳制,导致内网吐出的大包被封装后超过路径 MTU。

解决方案:三层围堵,根治分片丢失

方案一:强制 FRP 隧道 MSS Clamping(立即生效)

在轻云互联美国服务器的 iptables 中,对 FRP 的 control/data 端口强制钳制 MSS:

iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  --dport 7000 -j TCPMSS --clamp-mss-to-pmtu

# 同样作用于 data port(默认与 control 同端口)
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  --dport 8080 -j TCPMSS --clamp-mss-to-pmtu

这条规则让 FRP 服务端在 SYN 握手时,把 MSS 空间强制限制到 PMTU(路径 MTU - 隧道开销)。但注意:上面的 --dport 8080 是你映射的公网端口,不是 FRP 的 agent 端口。

方案二:调整 FRP 服务端 MTU 上限(更彻底)

frps.toml 里强制隧道层 MTU:

# frps.toml
bind_addr = "0.0.0.0"
bind_port = 7000

# 关键参数:限制隧道层的最大传输单元
tls_mtu = 1400    # 默认是 0(不限制),设 1400 让所有封装后包 < 1480
tls_min_version = "tls12"

这个 tls_mtu 参数在 FRP 0.52+ 版本有效。设为 1400 后,FRP 服务端会将分发给客户端的加密数据块大小限制在 1400 字节以内,确保经过 TLS 封装后不会超过 1500。

方案三:根治性路径修补(不依赖 FRP 配置)

在美国服务器上修改默认网卡 MTU,并配合 fwmark 让 FRP 流量走小 MTU 路由:

# 创建虚拟接口 frp_mtu
ip link add frp_mtu type dummy
ip link set frp_mtu mtu 1400

# 用 nftables 对 FRP 端口打标记
nft add table ip mangle
nft add chain ip mangle frp_mangle { type route hook output priority mangle; }
nft add rule ip mangle frp_mangle tcp dport { 7000, 8080 } meta mark set 0x2
nft add rule ip mangle frp_mangle tcp sport { 7000, 8080 } meta mark set 0x2

# 策略路由让标记流量走 frp_mtu
echo "200 frp" >> /etc/iproute2/rt_tables
ip rule add fwmark 0x2 table frp
ip route add default via [你的网关] dev eth0 table frp mtu 1400

这属于比较重的方案,但能彻底隔离所有 FRP 相关流量的 MTU 问题,不依赖应用层配置。

验证与复盘

实施方案一和方案二后,curl 测试连续 1000 次无超时:

for i in {1..1000}; do
  curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 15 \
    http://[你的FRP域名]:8080/test -o /dev/null || echo "FAIL at $i"
  sleep 0.1
done

丢包率从 60% 降至 0。抓包确认所有输出包大小均未超过 1480 字节。

为什么其他文章没提这个坑?

大多数 FRP 教程只教怎么“穿透”,没讲透 TCP 隧道在跨国链路上的 MTU 放大效应。尤其是美国服务器到国内或欧洲的路径,中间常经过 NTT、GTT、Cogent 等运营商对等互联节点,这些节点对 ICMP 的处理策略各异,MTU 黑洞远比想象的普遍。轻云互联的美西节点由于接入了多级 BGP 优化路径,标准场景下 PMTU 正常,但 FRP 的加密隧道额外放大了 30-80 字节,刚好踩线。

关键总结(对照坑点)

  • 别迷信“大带宽 = 没有问题”:路径 MTU 黑洞和带宽是两个维度,100Gbps 链路也可能被 1501 字节的包堵死。
  • FRP 的 tls_mtu 参数是 0.52 以后才稳定可用,老版本需要用 iptables TCPMSS 来救急。
  • 抓包要看双向:这案例中,服务端出口包没问题(因为做了分片),但路径中间丢了。仅看一端永远找不到根因。
  • 测试路径 MTU 要用 -M do 并配合 TTL 分析,光用 ping -s 不够,要确认丢包点在哪跳。

最后说一句:如果你也在美国服务器上跑 FRP 做穿透,先检查自己版本有没有 tls_mtu 参数,顺手设成 1400 能省掉你后续 80% 的排障时间。