MPTCP 深度解析系列(2/20)
一辆测试车停在停车场。工程师打开笔记本,把抓包工具接到 T-Box 的网口上,运行了一条命令:
tcpdump -i eth0 -nn -X 'tcp[tcpflags] & tcp-syn != 0'
屏幕上开始滚动数据包。第一行 SYN 包的十六进制输出里,藏着几个陌生的字节:
0x0040: 1e0c 0001 0000 0000 1a2b 3c4d 5e6f ...
1e = 十进制 30,这是 MPTCP 的 TCP Option 类型号。后面跟着长度、子类型、密钥……
工程师盯着这串字节,脑子里转了个问题:这里面到底发生了什么?
这篇文章,就是那个问题的完整答案。
我们将跟着这辆 T-Box,经历四幕:连接建立(MP_CAPABLE)→ 新路径加入(MP_JOIN)→ 地址通告(ADD_ADDR)→ 中间设备的破坏与应对。每一幕,我们都会深入到字节的级别。
一、地基:TCP Options 空间,MPTCP 的"巴掌大的地盘"
在拆解任何握手细节之前,得先搞清楚 MPTCP 是在多么拥挤的空间里工作的。
40 字节,仅此而已
TCP 头部有一个固定的 20 字节结构(源端口、目的端口、序列号、确认号……),剩下的空间留给"Options"字段。TCP 头部长度字段(Data Offset)用 4 bit 表示,最大值是 15 个 32 bit 单位,也就是 60 字节。减去固定的 20 字节,Options 空间上限是 40 字节。
听起来不算少。但这 40 字节在一个 SYN 包里早就被"预约"得差不多了:
MSS(Maximum Segment Size): 4 字节
Window Scale(RFC 7323): 3 字节
SACK Permitted(RFC 2018): 2 字节
Timestamps(RFC 7323): 10 字节
────────────────────────────────────
以上合计: 19 字节
剩余给 MPTCP: 最多 21 字节
而 MPTCP 的 MP_CAPABLE 选项在 SYN 包里需要 12 字节,在第三个 ACK 包里需要 20 字节。这就意味着,设计 MPTCP 选项格式的工程师们,从第一天起就知道自己在玩一个极限压缩游戏。
MPTCP 选项的通用格式
所有 MPTCP 选项都共用一个框架:
+──────+────────+──────────────+──────────────+
│ Kind │ Length │ SubType(4b) │ Data... │
│ 30 │ 1 byte │ + Flags(4b) │ │
+──────+────────+──────────────+──────────────+
- Kind = 30:这是 IANA 分配给 MPTCP 的选项类型号,固定不变
- SubType(4 bit):区分具体功能
完整的 SubType 列表:
| SubType | 名称 | 作用 |
|---|---|---|
| 0 | MP_CAPABLE | 连接建立时的能力协商 |
| 1 | MP_JOIN | 加入新子流 |
| 2 | DSS | 数据序列信号(每个数据包携带) |
| 3 | ADD_ADDR | 通告新的可用地址 |
| 4 | REMOVE_ADDR | 撤销某个地址 |
| 5 | MP_PRIO | 子流优先级调整 |
| 6 | MP_FAIL | 通知数据不一致 |
| 7 | MP_FASTCLOSE | 快速关闭整个连接 |
| 8 | MP_TCPRST | 携带原因码的重置(v1 新增) |
这张表就是 MPTCP 在 TCP 头部能做到的所有事情。记住这张表,后面每一个机制你都能找到对应的 SubType。
打个比方:这 40 字节的 Options 空间,就像名片的背面——你要在这巴掌大的地方,印上公司名、职位、手机号、微信、邮箱、二维码……每多一样就要挤掉别的内容。MPTCP 的工程师们在这里玩的,是把每一 bit 都压榨干净的极限设计艺术。
实战:用以下命令抓 SYN 包,找 1e(十六进制 30)这个字节,就是 MPTCP 选项的起点:
tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' -X -nn
二、MP_CAPABLE:第一次握手的秘密
三次握手,每一步都有戏
T-Box 向云端服务器发出第一个 SYN 包。在普通 TCP 的三次握手之上,MPTCP 悄悄多做了三件事。
第一步:SYN 包(T-Box 发出)
TCP Options 中的 MP_CAPABLE:
┌──────┬────────┬──────────────────────┐
│ 30 │ 12 │ SubType=0 | Flags │
│ │ │ C=1 H=0 ... │
├──────┴────────┴──────────────────────┤
│ Sender Key(64 bit) │
│ T-Box 随机生成,唯一标识本端 │
└──────────────────────────────────────┘
这里的 Sender Key 是 T-Box 启动时随机生成的 64 bit 密钥,简称 Key_A。它不是用来加密数据的——它是用来派生连接的"身份证号"和安全基础材料的。
Flags 字段里有几个能力协商位:
C(Checksum):表示本端要求计算 MPTCP 数据校验和H:算法标识位,v1 中H=0代表使用 HMAC-SHA256(v0 用 SHA1)
第二步:SYN-ACK(服务端回应)
┌──────┬────────┬──────────────────────┐
│ 30 │ 12 │ SubType=0 | Flags │
├──────┴────────┴──────────────────────┤
│ Sender Key(64 bit) │
│ 服务端随机生成,简称 Key_B │
└──────────────────────────────────────┘
服务端生成自己的 Key_B,放在同样的位置发回来。此时,T-Box 已经同时持有了 Key_A(自己的)和 Key_B(服务端的)。
第三步:第三个 ACK(v1 的重大改进)
v0 的第三个 ACK 只是一个普通的 ACK,没有额外动作。v1 在这里做了一个重要优化:
┌──────┬────────┬──────────────────────┐
│ 30 │ 20 │ SubType=0 | Flags │
├──────┴────────┴──────────────────────┤
│ Key_A(64 bit) │
│ Key_B(64 bit) │
│ Data Length(可选) │
└──────────────────────────────────────┘
v1 允许在第三个 ACK 中直接携带应用数据(Data Length > 0)。这意味着连接建立完成的那一刻,第一批业务数据就已经在路上了,比 v0 节省了一个 RTT。
对于 T-Box 这种云控场景,100ms 的端到端延迟预算里,一个 RTT 可能就是 20–50ms——每一毫秒都不能浪费。
两把密钥生什么用?
握手结束后,双方各自执行同样的计算:
H = HMAC-SHA256(Key_A || Key_B)
Token = H 的前 32 bit ← 这条连接的"身份证号",后续 MP_JOIN 用
IDSN_A = H 的后 64 bit ← T-Box 的 Data Sequence Number 初始值
H2 = HMAC-SHA256(Key_B || Key_A)
IDSN_B = H2 的后 64 bit ← 服务端的 DSN 初始值
这两个派生值非常关键:
Token(32 bit) 是整条 MPTCP 连接的唯一标识符。后续每次 MP_JOIN 都要带上它,让服务端知道"这条新的 TCP 连接是要加入哪个 MPTCP 会话的"。
IDSN(Initial Data Sequence Number) 是数据序列号的随机起点。这是 v1 相对 v0 最重要的安全改进。
v0 的安全漏洞——为什么 DSN 不能从 0 开始
v0(RFC 6824)的数据序列号从固定值 0 开始。听起来无害,但存在一个实际攻击面:
攻击者如果能嗅探到一条 MPTCP 连接的存在(比如通过旁信道),就可以预测 DSN 的值,然后构造一个携带伪造 DSS 选项的 TCP 包,注入到连接中。这就是数据注入攻击(TCP data injection)的 MPTCP 变种。
v1 的修复很优雅:IDSN 基于 HMAC-SHA256 随机生成。攻击者不知道 Key_A 和 Key_B,就无法预测 IDSN,更无法伪造有效的 DSN。
打个比方:v0 的 DSN 就像银行支票的流水号从 0001 开始——知道你今天用了 0100,就能猜到明天用 0101。v1 改成了随机的 8 位密码,猜不出来。
三种"中道崩殂"的场景
不是每次 MP_CAPABLE 握手都能成功。有三种常见的失败路径:
场景 A:服务端不支持 MPTCP
- T-Box 发 SYN+MP_CAPABLE,服务端的 SYN-ACK 里没有 MP_CAPABLE
- T-Box 静默回退到普通 TCP,连接照样建立
- 应用程序无感知,只是失去了多路径能力
场景 B:中间设备剥离了 MP_CAPABLE
- T-Box 发 SYN+MP_CAPABLE,防火墙把 Kind=30 的选项删掉,服务端收到的是普通 SYN
- 服务端回 SYN-ACK,没有 MP_CAPABLE → 结果与场景 A 相同
场景 C:中间设备篡改了 MP_CAPABLE(最危险)
- 某些"智能"代理设备会修改 TCP Options 内容,导致双方的 Key 对不上
- 这种情况 MPTCP 握手会失败,但 TCP 连接不会断——会触发 MP_FASTCLOSE 后回退
三种场景的共同结论:TCP 连接本身从不中断,只是多路径能力的开关被关上了。 这就是 MPTCP 优雅降级设计的价值所在。
v0 vs v1 对比:
| 维度 | RFC 6824(v0) | RFC 8684(v1) |
|---|---|---|
| 安全算法 | HMAC-SHA1 | HMAC-SHA256 |
| 第三个 ACK | 仅 Key,不含数据 | 可携带应用数据(少一个 RTT) |
| IDSN | 从 0 开始,可预测 | 基于 HMAC 随机生成 |
| 版本协商 | 无 | Flags 中携带版本号,支持三级回退 |
| 连接关闭 | 机制不完善 | 新增 MP_FASTCLOSE、MP_TCPRST |
图 1:MP_CAPABLE 握手的字节布局,以及 Key_A/Key_B 派生 Token 和 IDSN 的过程
实战:用 Wireshark 过滤 MPTCP 握手:
# Wireshark 显示过滤器
mptcp.subtype == 0
# tcpdump:找十六进制输出里的 1e 0c(Kind=30, Len=12)
tcpdump -i eth0 -nn -X 'tcp[tcpflags] & tcp-syn != 0'
三、MP_JOIN:长出第二条腿
为什么 MP_JOIN 不是一条普通 TCP 连接?
T-Box 刚刚通过 4G 建立了第一条子流,MPTCP 连接跑起来了。这时车辆换了地方,5G 信号出现了。T-Box 想把 5G 这条路也用上——它需要建立第二条子流,把它"插入"已有的 MPTCP 连接。
问题来了:为什么不直接建一条普通 TCP 连接,然后"声称"它属于这个 MPTCP 会话?
答案很简单:没有认证机制,任何人都可以伪造。
攻击者如果能嗅探到你的 MPTCP Token(这个值在 MP_CAPABLE 完成后就固定了),就可以向你的服务器发一个伪造的 MP_JOIN SYN,把一条恶意连接"插入"你的 MPTCP 会话,然后注入数据或者劫持流量。
所以 MP_JOIN 需要同时解决两个问题:
- 身份绑定:证明"这条新 TCP 连接,是 Token=XXXX 那个 MPTCP 会话的合法成员"
- 双向认证:客户端验证服务端(防止连接到仿冒服务器),服务端验证客户端(防止伪造子流)
这套双向认证机制,就是 MP_JOIN 三次握手的核心。
MP_JOIN 三次握手的字节布局
第一步:SYN(T-Box 发起新子流)
┌──────┬────────┬──────────────────────────┐
│ 30 │ 12 │ SubType=1 | Flags | B │
├──────┴────────┴──────────────────────────┤
│ Address_ID(1 byte) │
│ Token(32 bit)← 连接身份证号 │
│ Nonce_A(32 bit,随机数) │
└──────────────────────────────────────────┘
这里有三个关键字段:
- Token:从 MP_CAPABLE 派生的 32 bit 标识,服务端用它找到对应的 MPTCP 会话
- Address_ID:标识这条子流来自哪个网络接口。4G=1,5G=2——是逻辑编号,不是 IP 地址
- Nonce_A:客户端随机生成的 32 bit 随机数,用于 HMAC 计算,防重放攻击
第二步:SYN-ACK(服务端回应)
┌──────┬────────┬──────────────────────────┐
│ 30 │ 16 │ SubType=1 | Flags │
├──────┴────────┴──────────────────────────┤
│ Address_ID(1 byte,服务端接口标识) │
│ Truncated_HMAC_B(64 bit) │
│ Nonce_B(32 bit,服务端随机数) │
└──────────────────────────────────────────┘
服务端在这一步证明自己的身份:Truncated_HMAC_B 是用 Key_B 计算出的截断 HMAC,让客户端能验证"这确实是我最初握手的那个服务端"。
第三步:第三个 ACK(客户端完成认证)
┌──────┬────────┬──────────────────────────┐
│ 30 │ 24 │ SubType=1 | Flags │
├──────┴────────┴──────────────────────────┤
│ HMAC_A(160 bit = 20 字节) │
└──────────────────────────────────────────┘
客户端在这一步证明自己的身份:HMAC_A 是用 Key_A 计算出的完整 HMAC(20 字节),让服务端能验证"这条新子流确实来自知道 Key_A 的客户端"。
HMAC 计算的完整公式
这是整个 MPTCP 握手里最容易搞混的地方。把它写清楚:
# 服务端计算 Truncated_HMAC_B(放入 SYN-ACK):
HMAC_B_full = HMAC-SHA256(
key = Key_B,
msg = Nonce_A || Nonce_B ← 先 A 后 B(64 bit 拼接)
)
Truncated_HMAC_B = HMAC_B_full 的前 8 字节(64 bit)
# 客户端计算 HMAC_A(放入第三个 ACK):
HMAC_A = HMAC-SHA256(
key = Key_A,
msg = Nonce_B || Nonce_A ← 注意:先 B 后 A,顺序反过来了!
)
HMAC_A_20 = HMAC_A 的前 20 字节(160 bit)
注意 Nonce_A || Nonce_B 和 Nonce_B || Nonce_A 的顺序是反的——这是故意的,确保两个 HMAC 用不同的输入,防止一方推算出另一方的 HMAC。
用 Python 可以直接验证:
import hmac, hashlib, os
# 模拟握手材料(实际来自抓包)
Key_A = bytes.fromhex('1a2b3c4d5e6f7890') # T-Box 的 Key
Key_B = bytes.fromhex('abcdef0123456789') # 服务端的 Key
Nonce_A = bytes.fromhex('11223344')
Nonce_B = bytes.fromhex('aabbccdd')
# 服务端计算 Truncated_HMAC_B
hmac_b = hmac.new(Key_B, Nonce_A + Nonce_B, hashlib.sha256).digest()
truncated_hmac_b = hmac_b[:8] # 前 64 bit
print(f"Truncated_HMAC_B: {truncated_hmac_b.hex()}")
# 客户端计算 HMAC_A(注意 Nonce 顺序反了)
hmac_a = hmac.new(Key_A, Nonce_B + Nonce_A, hashlib.sha256).digest()
hmac_a_20 = hmac_a[:20] # 前 160 bit
print(f"HMAC_A: {hmac_a_20.hex()}")
把这段代码跑一遍,再对着 Wireshark 里的 MP_JOIN 抓包,你就能亲眼验证每一个字节从哪来、到哪去。
类比:酒店换房卡系统
整个 MP_JOIN 认证机制,用一个日常场景来理解:
你入住了一家酒店(MP_CAPABLE 握手),前台给了你一张主卡(Key_A),并登记了你的房间号(Token = 1506)。
第二天,你的朋友来找你,想帮你拿快递进房间(MP_JOIN,新子流)。但你不能直接把主卡给他——酒店不允许。
正确流程是:
- 朋友去前台,报出房间号 1506(SYN 里的 Token)
- 前台查到房间号,用你的入住信息做一个临时验证码给朋友(SYN-ACK 里的 Truncated_HMAC_B)
- 朋友把临时验证码发给你,你用主卡确认这确实是我们酒店的前台给的(第三个 ACK 里的 HMAC_A)
- 验证通过,朋友拿到了进房间的权限
全程没有泄露主卡(Key_A 和 Key_B 始终在双方内存里,不出现在网络包里);前台(服务端)和你(客户端)互相验证了身份;朋友(新的 5G 子流)只是载体,无法单独伪造权限。
Address_ID 的妙用:穿越 NAT 的钥匙
每个网络接口被分配一个 1 字节的 Address_ID(0–255),由 Path Manager 在本地管理。这个 ID 是双方约定的逻辑标识,不是 IP 地址。
为什么不直接用 IP 地址?因为 NAT。
T-Box 的 4G 接口在运营商侧的 IP 是 10.x.x.x(内网),经过 CGNAT 后变成了某个公网 IP,云端服务器实际看到的是这个公网 IP。如果子流的标识绑定在 IP 上,T-Box 和云端根本对不上号。
绑定在 Address_ID 上就不一样了:T-Box 内部知道"Address_ID=1 对应我的 4G 接口",服务端知道"Address_ID=1 对应那个从 IP=x.x.x.x 来的子流"——即使 NAT 改变了 IP,双方的逻辑映射不变。
这一个 byte 的设计,是 MPTCP 能在真实运营商网络上工作的关键之一。
实战:
# 查看当前子流的 Address_ID 分配
ss -Mtnpi
# 示例输出片段:
# mptcp seq:... subflows:2 add_addr_signal:0
# src 10.0.0.1:443 dst 192.168.1.100:54321 uid:1001
四、ADD_ADDR:告知更多地址
场景:云端有备用 IP,想让 T-Box 来连
T-Box 已经建立了 4G 和 5G 两条子流,数据在并行流动。这时,云端服务器发现自己的灾备机房刚刚启动,有一个新的 IP 地址可用,想让 T-Box 过来建第三条子流……
这就是 ADD_ADDR 的场景。
ADD_ADDR 的字节格式(v1)
┌──────┬────────┬──────────────────────────────┐
│ 30 │ N │ SubType=3 | IPver | E │
├──────┴────────┴──────────────────────────────┤
│ Address_ID(1 byte) │
│ IP 地址(4 byte for IPv4,16 byte for IPv6) │
│ Port(2 byte,可选) │
│ Truncated_HMAC(8 byte,v1 新增) │
└──────────────────────────────────────────────┘
几个字段值得特别说明:
E 标志(Echo):E=0 表示"我在通告自己的地址",E=1 表示"我在回显你之前通告给我的地址"(用于地址确认,v1 新增,现在很少用)。
Truncated_HMAC(v1 新增):这是 v1 修复 v0 安全漏洞的核心改动。
v0 的 ADD_ADDR 漏洞
v0 的 ADD_ADDR 没有任何认证——任何人都可以向 T-Box 发送一个伪造的 ADD_ADDR,把里面的 IP 地址换成攻击者控制的服务器。T-Box 收到后,会乖乖地向这个 IP 发起 MP_JOIN……这个 MP_JOIN 里包含了 Token、Nonce 等信息,攻击者可以分析甚至劫持连接。
这是一种 SSRF(Server-Side Request Forgery)的变体。
v1 的修复:ADD_ADDR 里加入了 Truncated_HMAC,用 Key_B 派生计算,验证通告来自合法的对端。T-Box 收到 ADD_ADDR 后,先验证 HMAC,通过才去建子流。
谁发 ADD_ADDR?谁去连?
这是部署时经常搞反的问题。
原则:有公网 IP 的一侧发 ADD_ADDR,另一侧去发起 MP_JOIN。
在 T-Box 场景下:
- T-Box 在 CGNAT 后面,它的"地址"在运营商侧是私有 IP,没有意义
- 云端服务器通常有公网 IP
- 因此:服务端发 ADD_ADDR,T-Box 收到后决定是否发起 MP_JOIN
Linux 内核控制这个行为的参数:
# T-Box 侧:最多接受几个 ADD_ADDR 并主动去建子流
ip mptcp limits set add_addr_accepted 2
# 最多允许几条子流并行
ip mptcp limits set subflows 4
# 查看当前配置
ip mptcp limits show
REMOVE_ADDR:极简的地址撤销
当某个地址不可用时(比如服务端的备用机房下线),发送方会用 REMOVE_ADDR 通知对方:
┌──────┬────────┬──────────────────────────┐
│ 30 │ 4 │ SubType=4 │
├──────┴────────┴──────────────────────────┤
│ Address_ID(1 byte) │
└──────────────────────────────────────────┘
注意:只有 4 字节,只携带 Address_ID,没有 IP 地址。原因同前:NAT 后面的 IP 可能已经变了,Address_ID 才是不变的逻辑标识。
收到 REMOVE_ADDR 后,该 Address_ID 对应的所有子流会被关闭。
Path Manager 的角色(埋钩子)
ADD_ADDR 的收发决策和 MP_JOIN 的发起,都由 Linux 内核的 Path Manager(路径管理器) 负责。内核默认实现是 pm_netlink,通过 netlink 接口接受用户空间的配置。
但默认的 Path Manager 不够聪明:它不知道 5G 信号强度、不知道当前拥塞状态、不知道应该"什么时候激活备用子流才划算"。如果你想要精细控制(比如:4G 延迟超过 80ms 才启动 5G 子流;或者两条链路同时可用时优先用 5G 做主路径),需要写自定义 Path Manager。
这个话题在系列第 6 篇会深入到 net/mptcp/pm_netlink.c 的源码层面。
实战:
# 查看 MPTCP 端点配置
ip mptcp endpoint show
# 监控 MPTCP 事件(ADD_ADDR / REMOVE_ADDR 实时可见)
ip monitor mptcp
# 示例输出:
# [MPTCP_EVENT_ANNOUNCE] msk=0x... addr=10.10.1.1 id=2 flags=...
五、中间设备的破坏与工程应对
理论很美好,现实里有运营商。
T-Box 跑在公网上,数据包会经过运营商的 CGNAT、防火墙、DPI 设备。这些设备大多数是在 MPTCP 诞生之前就设计的,它们不认识 Kind=30 的 Options,处理方式五花八门。
三种破坏模式和诊断方法
模式一:TCP Options 剥离
这是最常见的情况。防火墙或 DPI 设备把它不认识的 TCP Options 直接删掉——"不认识的东西,删了保险"。
-
症状:T-Box 发的 SYN 里有 MP_CAPABLE,但抓到的 SYN-ACK 里没有 → MPTCP 自动回退普通 TCP
-
诊断:
nstat -az | grep MPTCPMPCapableFallbackACK # 这个计数器增长 = 发生了回退
模式二:MP_JOIN SYN 被防火墙拦截
最坑的情况之一。有些防火墙配置了"只允许出站连接"的策略——它认识第一条子流的 SYN(因为是 T-Box 主动发起的),但 MP_JOIN 的 SYN 如果是从服务端向 T-Box 发起的(服务端主动建子流),就会被当作"非法入站连接"丢弃。
-
症状:第一条子流建立成功,MP_JOIN 的 SYN 始终收不到 SYN-ACK
-
诊断:
ss -Mtnpi | grep "subflows=1" # 只有 1 条子流,但预期有 2 条
模式三:CGNAT 的 HMAC 联动破坏
这是技术含量最高的破坏方式。CGNAT 在改写 IP 地址和端口时,会同步更新 TCP Checksum(这是正确的)。但 CGNAT 不懂 MPTCP,它不会更新 MP_JOIN 选项里的 HMAC——因为 HMAC 的计算输入里没有包含源 IP/Port(这是 MPTCP 设计时有意为之的)。
理论上这应该是安全的(HMAC 不依赖 IP),但某些 CGNAT 实现会把 TCP Options 整块修改,导致 HMAC 字节被破坏。
-
症状:MP_JOIN 握手完成了,但数据传输阶段出现大量 RST 或 HMAC 验证失败
-
诊断:
nstat -az | grep MPTCPMPJoinAckHMACFailure # HMAC 校验失败计数增长
中国运营商的现实(给 T-Box 工程师的实战总结)
基于实际测试经验:
| 接入类型 | MPTCP 兼容性 | 建议 |
|---|---|---|
| 移动/联通/电信 公网 4G | 不稳定,部分省份 CGNAT 会剥离 Options | 用探测脚本先验证,做好回退预案 |
| 联通/电信 企业专线 APN(to B) | 较好,通常透传 TCP Options | 推荐用于生产环境 |
| 5G SA 独立组网(试商用) | 理论上支持 ATSSS,MPTCP 最友好 | 稳定性待验证,持续跟进 |
| WiFi + 4G 双链路(室内) | 通常 WiFi 侧无问题,4G 侧看运营商 | 参考上述 4G 建议 |
落地前的标准检查流程:
# 第一步:全量 MPTCP 诊断
nstat -az | grep -i mptcp
# 第二步:查看连接状态和子流数量
ss -Mtnpi
# 第三步:查看实时 MPTCP 事件
ip monitor mptcp
# 第四步:如果怀疑 Options 被剥离,抓 SYN 包对比
tcpdump -i eth0 -nn -X 'tcp[tcpflags] & tcp-syn != 0' | head -100
# 关注以下内核计数器含义:
# MPTCPMPCapableSYNRxMismatch → MP_CAPABLE 被中间设备修改
# MPTCPMPJoinSynRx → 收到 MP_JOIN SYN(服务端侧用)
# MPTCPMPJoinAckHMACFailure → HMAC 校验失败,中间设备篡改
# MPTCPMPCapableFallbackACK → 客户端侧回退到普通 TCP
# MPTCPMPCapableFallbackSYNACK → 服务端侧回退到普通 TCP
关于中间设备的兼容性问题,以及 MPTCP 在中国运营商网络环境下的完整应对方案——包括 NAT 穿越技巧和 DPI 识别规避——我们会在系列第 4 篇专门展开讲。
六、完整流程:一图看懂四幕
把四章的内容串成一张时序图:
T-Box(4G 接口) 云端服务器
│ │
│──SYN [MP_CAPABLE Key_A]──────────────>│ ① T-Box 通告能力,带上自己的密钥
│<──SYN-ACK [MP_CAPABLE Key_B]──────────│ ② 服务端回应,带上自己的密钥
│──ACK [MP_CAPABLE Key_A+B + 初始数据]─>│ ③ v1:连接建立同时发数据(省 1 RTT)
│ │
│<══════ 4G 子流正常传输数据 ══════════>│
│ │
│──SYN [MP_JOIN Token,Nonce_A]─────────>│ ④ 5G 接口发起第二条子流
│<──SYN-ACK [MP_JOIN HMAC_B,Nonce_B]────│ ⑤ 服务端用 Key_B 证明身份
│──ACK [MP_JOIN HMAC_A]────────────────>│ ⑥ T-Box 用 Key_A 证明身份,双向认证完成
│ │
│<══════ 4G + 5G 双路并行 ════════════>│
│ │
│<──ADD_ADDR [AddrID=2, IP=y.y.y.y]─────│ ⑦ 服务端通告备用地址(含 HMAC 认证)
│ │
│──SYN [MP_JOIN Token,Nonce_C]─────────>│ ⑧ T-Box 主动连备用地址(第三条子流)
│<──SYN-ACK [MP_JOIN HMAC_D,Nonce_D]────│
│──ACK [MP_JOIN HMAC_C]────────────────>│ ⑨ 第三条子流建立完成
│ │
│<══════ 4G + 5G + 备用,三路并行 ═════>│
这九步,就是一条 MPTCP 连接从无到有、从一条路变成三条路的完整生命历程。
写在最后
回到停车场里那台 T-Box,屏幕上的 1e 0c 00 01 1a 2b 3c 4d 5e 6f 78 90 现在你能读懂了:
1e= Kind=30,这是 MPTCP0c= Len=12,选项总长度 12 字节00= SubType=0(高 4 bit),Flags(低 4 bit)01= Flags 字节- 后面 8 字节 = Sender Key(Key_A),T-Box 的随机密钥
每一个字节背后,都是 IETF 工作组十年工程经验的结晶:40 字节的 Options 空间压榨到极致、HMAC 双向互认保安全、Address_ID 抽象掉 NAT 地址的变化……
这套机制不完美——中间设备的破坏让 MPTCP 在某些运营商网络上举步维艰。但它在真实互联网上运行了十年,撑住了苹果数亿台设备的流量,也正在撑起云控车联网的生死线。
下一篇预告:「MPTCP vs MPQUIC:下一代多路径传输谁更有前途?」——我们暂别字节级的细节,抬头看看另一个挑战者的设计哲学,以及内核态 vs 用户态的这场根本性取舍,最终会走向何方。
参考资料
- RFC 8684: TCP Extensions for Multipath Operation with Multiple Addresses, IETF, 2020
- RFC 6824: TCP Extensions for Multipath Operation with Multiple Addresses, IETF, 2013
- RFC 8041: Use Cases and Operational Experience with Multipath TCP, IETF, 2017
- Linux kernel source:
net/mptcp/options.c(MP_CAPABLE/MP_JOIN 解析实现) - Linux kernel source:
net/mptcp/crypto.c(HMAC 计算实现) - mptcp.dev: 官方测试工具与诊断指南
- Bagnulo et al., "Multipath TCP Security Analysis", RFC 6182 Appendix, 2011