第一百一十五章:握手之道
问道期涉及内核源码:
一
林小源再次登上了 TCP 商船。
这一次,船长没有带他看甲板上的货箱,而是把他领进了船舱最深处的一间密室。密室的墙壁上画满了状态图——圆圈和箭头交织成一张复杂的网。圆圈里写着各种名字:、、、FIN_WAIT_1……
"这是 TCP 的状态机。" 船长站在图前,神情严肃,"每一条 TCP 连接,从出生到死亡,都在这张图里。"
他指着左下角的 圆圈:"一切从这里开始。客户端调用 connect(),内核调用 ,进入 状态。"
他拿起一支笔,在图上画了一条线。
"第一步,客户端发送 SYN 报文。这个报文里带着客户端的初始序列号——,一个随机生成的 32 位数字。不是 0,不是 1,是随机的。"
"为什么随机?"
船长没有直接回答,而是反问:"你去一个陌生的客栈,掌柜给你一把钥匙,钥匙上写着房间号。如果房间号是从 1 开始顺序排的,别人随便猜一个就能进你的房间。但如果房间号是随机的——比如 0x7a3f2b1c——别人就猜不到了。序列号也是一样。随机的 ISN 防止了序列号预测攻击。"
他在图上继续画线。"第二步,服务器收到 SYN,回复 SYN+ACK。这个报文里带着服务器的 ISN,以及对客户端 ISN 的确认——ack = 客户端 ISN + 1。服务器进入 状态。"
"第三步,客户端收到 SYN+ACK,回复 ACK。确认服务器的 ISN——ack = 服务器 ISN + 1。双方进入 状态。连接建立。"
林小源盯着那三条线,它们在状态图上画出了一个完美的三角形。
三条线在状态图上画出一个完美的三角形——每一步都在确认身份,三步缺一不可。
二
船长的表情忽然变得凝重。
"你得知道一件事。" 他压低声音,"握手不是没有风险的。"
他从抽屉里拿出一封急报,上面画着一个骷髅标志。"SYN Flood 攻击。攻击者伪造源 IP 地址,向服务器发送大量的 SYN 报文。服务器收到每一个 SYN,都要分配资源——创建 ,分配内存,启动定时器——然后等待 ACK。"
"但 ACK 永远不会来。"
"对。因为源 IP 是假的。" 船长把急报拍在桌上,"服务器的半连接队列——syn_table——被塞满了。资源耗尽,正常的连接请求也进不来了。这就是 SYN Flood。"
"怎么防?"
"SYN Cookies。" 船长的语气缓和了一些,"内核不再为 SYN 分配资源。它把连接信息——MSS、窗口大小、时间戳——编码进 ISN 里。服务器发出去的 SYN+ACK 的序列号就是 cookie。如果客户端回了 ACK,内核从 ACK 里反推出 cookie,验证合法性,然后才分配资源。"
"这样就不用存储半连接状态了?"
"对。syn_table 可以是空的。所有状态都编码在序列号里,不需要额外内存。" 船长收起急报,"但有代价——时间戳编码的精度有限,MSS 只能用几个比特表示,所以选项被压缩了。不过总比被打死强。"
林小源看着状态图上的 圆圈,忽然觉得它不再只是一个技术术语——它是一个脆弱的状态,等待着一个可能永远不会来的回应。
林小源盯着那个 圆圈看了很久——它不再只是一个技术术语,而是一个脆弱的状态,等着一个可能永远不会来的回应。
三
船长带林小源走出了密室,来到甲板上。
夜已经深了。海面上一片漆黑,只有远处几盏航标灯在闪烁。
"你知道为什么 ISN 要随机吗?" 船长问。
"你之前说了,防止序列号预测。"
"不止如此。" 船长摇摇头,"如果 ISN 是可预测的,攻击者可以伪造 TCP 连接。他知道你的 ISN,就能构造正确的 ACK,冒充合法客户端。更可怕的是——他可以往已建立的连接里注入伪造的数据包。"
"IP 欺骗?"
"对。攻击者嗅探到一个正在进行的 TCP 连接,知道了双方的 IP 和端口,再猜出当前的序列号,就能往连接里塞入自己的数据。接收方根本分不清哪些是合法的、哪些是伪造的。"
船长望着远处的航标灯:"所以 Linux 的 ISN 生成算法用了两样东西——时间戳和密钥。时间戳保证 ISN 随时间递增,密钥是随机的,每天更换。就算攻击者知道时间,不知道密钥也猜不出 ISN。"
他转过身来,拍了拍林小源的肩膀:"小子,安全不是事后补的,是设计时就埋进去的。TCP 的三次握手不只是为了建立连接——它的每一步都在确认身份、防止欺骗。SYN 说'我来了',SYN+ACK 说'我确认你来了,你也确认我',ACK 说'我们都确认了'。三步,缺一不可。"
林小源望着漆黑的海面,忽然明白了为什么这个协议要设计得如此复杂——不是为了炫技,而是为了在不可信的网络上建立可信的连接。
他忽然明白了——安全不是事后补的,是设计时就埋进去的。随机的 ISN、加密的密钥,每一步都在为不可信的网络建立可信的连接。
/*
* TCP 三次握手:
*
* 客户端 服务器
* │ │
* │ ──── SYN (seq=x) ───→ │
* │ │
* │ ← SYN+ACK (seq=y, │
* │ ack=x+1) ──── │
* │ │
* │ ──── ACK (ack=y+1) ─→ │
* │ │
* │ 连接已建立 │
*
* 为什么是三次?
* 两次: 服务器不知道客户端是否收到 SYN+ACK
* 三次: 双方都确认对方收到了自己的序列号
*
* SYN Flood 攻击:
* 攻击者发送大量 SYN
* 服务器分配资源等待 ACK
* 资源耗尽
*/
printf("=== TCP 三次握手 — 建立连接 ===\n\n");
printf("三次握手过程:\n\n");
printf(" 客户端 服务器\n");
printf(" │ │\n");
printf(" │ ──── SYN (seq=x) ───→ │\n");
printf(" │ │\n");
printf(" │ ← SYN+ACK (seq=y, │\n");
printf(" │ ack=x+1) ──── │\n");
printf(" │ │\n");
printf(" │ ──── ACK (ack=y+1) ─→ │\n");
printf(" │ │\n");
printf(" │ 连接已建立 │\n\n");
printf("--- 每一步的作用 ---\n");
printf("SYN:\n");
printf(" 客户端发送初始序列号 x\n");
printf(" 请求建立连接\n\n");
printf("SYN+ACK:\n");
printf(" 服务器确认 x\n");
printf(" 发送自己的序列号 y\n\n");
printf("ACK:\n");
printf(" 客户端确认 y\n");
printf(" 连接建立\n\n");
printf("--- 为什么是三次 ---\n");
printf("两次不够:\n");
printf(" 服务器不知道客户端是否收到 SYN+ACK\n\n");
printf("三次足够:\n");
printf(" 双方都确认对方收到了自己的序列号\n\n");
printf("--- SYN Flood 攻击 ---\n");
printf("攻击方式:\n");
printf(" 发送大量 SYN\n");
printf(" 不完成握手\n\n");
printf("影响:\n");
printf(" 服务器资源耗尽\n");
printf(" 无法接受新连接\n\n");
printf("防御:\n");
printf(" SYN cookies\n");
printf(" 不分配资源,编码在序列号中\n");#include <stdio.h>
/*
* TCP 三次握手:
*
* 客户端 服务器
* │ │
* │ ──── SYN (seq=x) ───→ │
* │ │
* │ ← SYN+ACK (seq=y, │
* │ ack=x+1) ──── │
* │ │
* │ ──── ACK (ack=y+1) ─→ │
* │ │
* │ 连接已建立 │
*
* 为什么是三次?
* 两次: 服务器不知道客户端是否收到 SYN+ACK
* 三次: 双方都确认对方收到了自己的序列号
*
* SYN Flood 攻击:
* 攻击者发送大量 SYN
* 服务器分配资源等待 ACK
* 资源耗尽
*/
int main() {
printf("=== TCP 三次握手 — 建立连接 ===\n\n");
printf("三次握手过程:\n\n");
printf(" 客户端 服务器\n");
printf(" │ │\n");
printf(" │ ──── SYN (seq=x) ───→ │\n");
printf(" │ │\n");
printf(" │ ← SYN+ACK (seq=y, │\n");
printf(" │ ack=x+1) ──── │\n");
printf(" │ │\n");
printf(" │ ──── ACK (ack=y+1) ─→ │\n");
printf(" │ │\n");
printf(" │ 连接已建立 │\n\n");
printf("--- 每一步的作用 ---\n");
printf("SYN:\n");
printf(" 客户端发送初始序列号 x\n");
printf(" 请求建立连接\n\n");
printf("SYN+ACK:\n");
printf(" 服务器确认 x\n");
printf(" 发送自己的序列号 y\n\n");
printf("ACK:\n");
printf(" 客户端确认 y\n");
printf(" 连接建立\n\n");
printf("--- 为什么是三次 ---\n");
printf("两次不够:\n");
printf(" 服务器不知道客户端是否收到 SYN+ACK\n\n");
printf("三次足够:\n");
printf(" 双方都确认对方收到了自己的序列号\n\n");
printf("--- SYN Flood 攻击 ---\n");
printf("攻击方式:\n");
printf(" 发送大量 SYN\n");
printf(" 不完成握手\n\n");
printf("影响:\n");
printf(" 服务器资源耗尽\n");
printf(" 无法接受新连接\n\n");
printf("防御:\n");
printf(" SYN cookies\n");
printf(" 不分配资源,编码在序列号中\n");
return 0;
}道藏笔记
内核启示
TCP 三次握手建立连接。
三次握手:
- SYN — 客户端发送初始序列号
- SYN+ACK — 服务器确认并发送自己的序列号
- ACK — 客户端确认,连接建立
为什么是三次:
- 两次不够:服务器不知道客户端是否收到
- 三次足够:双方都确认对方
SYN Flood 攻击:
- 发送大量 SYN 不完成握手
- 服务器资源耗尽
- 防御:SYN cookies
握手是"确认"的仪式——确保双方都准备好。
握手之试
三次握手建立连接的主角是哪一种可靠传输协议?