监听套接字维护的是两个队列,不是一个。半开连接先待在 SYN 队列里,直到客户端的最后一个 ACK 到达;握手完成的连接随后进入 accept 队列,等着应用调用 accept()。两者各有各的上限,而究竟是哪一个溢出,决定了客户端体验到什么。

SYN Cookie 是为第一个队列准备的。它满了以后,服务器不再分配状态,转而把稍后需要的东西编进序号里,于是握手在零内存占用下完成。这确实是一次救援,同时也是一次降级:被救下来的那条连接,比正常情况下本该建立的那条更差。

两个队列,两种症状

一个 SYN 会分配一个 request socket,把连接推进 SYN_RECV 并回一个 SYN-ACK。客户端的 ACK 再把它移进 accept 队列、状态变成 ESTABLISHED。SYN 队列由 tcp_max_syn_backlog 按监听套接字限流——最小值 128,随内存放大,每个表项大约占 304 字节。accept 队列的上限来自 somaxconn(当前内核默认 4096,Linux 5.4 之前是 128),再与应用程序传给 listen() 的 backlog 共同起作用。

两种溢出长得完全不一样。SYN 队列溢出正是 syncookie 为之而生的场景:连接照样能完成。accept 队列溢出更糟也更安静——在 tcp_abort_on_overflow 取默认值 0 的情况下,服务器直接忽略那个最后的 ACK,于是 connect() 返回成功,客户端把请求发出去,然后再也等不到任何回应。实测:backlog 设为 1、客户端 400 个,400 个全都以为自己连上了,服务器实际只能接受 3 个,另外 397 个在两端都没有任何报错的情况下丢掉了。

tcp_syncookies 取默认值 1 时,SYN backlog 一溢出,内核就发 cookie。它不再分配 request socket,而是把初始序号算成「高 8 位放计数器、低 24 位放截断哈希」,哈希覆盖两端地址、两个端口、计数器和一个服务端密钥。计数器每 64 秒前进一格,所以同一个四元组在每一格上算出的 cookie 都不同。

校验过程就是把它倒过来。ACK 的确认号高 8 位带着计数器,服务器拿它和自己的当前格、上一格比对,再按四元组重算哈希并比较低 24 位。复现这套构造的结果是:刚发出的 ACK 校验通过;70 秒后重放的 ACK——也就是往前一格——依然通过;200 秒和 600 秒之后则被判为过期。把源端口或源地址改动一个,立刻被拒,所以 cookie 被绑死在这条连接上,无法搬到别处使用。盲猜的成功概率是 1677 万分之一,整个安全论证就靠这个数字。

带 cookie 的 SYN-ACK 不携带任何选项。内核文档说得毫不客气:syncookie「严重违反 TCP 协议,不允许使用 TCP 扩展,可能导致某些服务严重劣化」。代价最贵的是窗口缩放——没有它,接收窗口上限是 65535 字节,而不是缩放因子 7 下的 8388480 字节。在 100 毫秒的路径上,这就是 5.2 Mbit/s 对 671.1 Mbit/s,128 倍的差距,只因为少了一个选项。SACK 同样没了,于是一个窗口里丢了好几个段的发送方,只能靠累积确认一次恢复一个。

MSS 算是部分救回来了:cookie 路径偷走三个哈希位去索引一张小 MSS 表,而这是唯一活下来的选项。时间戳、SACK、窗口缩放则干脆不存在。所以处于 cookie 模式的服务器不只是少收了几条连接,它收下的那些连接本身是慢的,而这种慢看起来像网络问题,不像握手问题。

坏掉的样子

第一种失败,是 cookie 把真实的洪泛从监控视野里藏了起来。SyncookiesSent 上涨就是信号,但按设计,洪泛在应用层是隐形的:没有半开队列可告警、没有失败连接、没有错误日志,只有一个计数器。建立在错误率或 SYN_RECV 深度上的看板,会在服务器正被持续攻击时显示一切健康。要盯的是 cookie 计数器及其与新建连接数的比值,不是应用本身。

第二种失败,是 accept 队列照样溢出,而 cookie 对此毫无办法。ListenOverflows 上涨,客户端在第一个请求上超时,瓶颈在 accept() 而不在 SYN 队列——实测 400 条里丢掉 397 条,而每个客户端都以为自己连上了。解法是让应用接受得更快、加 worker、抬高 somaxconn 和 listen backlog,而不是去调 syncookie。要记住 cookie 只在 SYN 队列溢出时才启用:瓶颈若在 accept 队列,把它打开或关掉都不会改变任何数字。

快速验证

同时看两个队列和计数器:

sysctl net.ipv4.tcp_syncookies net.ipv4.tcp_max_syn_backlog net.core.somaxconn
nstat -az | grep -E 'Syncookies|ListenOverflow|ListenDrops'
ss -lnt

健康的服务器上三个计数器都读 0,ss -lnt 里的 Send-Q 与配置的 backlog 一致,Recv-Q 为 0。SyncookiesSent 上涨说明 SYN 队列溢出过;ListenOverflows 上涨说明 accept 队列溢出过,而那一个不是 cookie 能修的。如果监听套接字的 Recv-Q 长期大于零,说明已建立的连接正在等一个调用 accept() 不够快的应用。

来源