<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>握手 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E6%8F%A1%E6%89%8B/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E6%8F%A1%E6%89%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>SYN Cookie 在队列溢出时作答</title><link>https://www.xbcatm.com/zh/posts/tcp-syn-cookies-when-syns-overflow/</link><pubDate>Sat, 30 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/tcp-syn-cookies-when-syns-overflow/</guid><description>当握手队列被占满时，SYN Cookie 让服务器在不必保存连接的情况下作答，用存储换取对洪泛的抵抗能力。</description><content:encoded>&lt;p&gt;监听套接字维护的是两个队列，不是一个。半开连接先待在 SYN 队列里，直到客户端的最后一个 ACK 到达；握手完成的连接随后进入 accept 队列，等着应用调用 &lt;code&gt;accept()&lt;/code&gt;。两者各有各的上限，而究竟是哪一个溢出，决定了客户端体验到什么。&lt;/p&gt;
&lt;p&gt;SYN Cookie 是为第一个队列准备的。它满了以后，服务器不再分配状态，转而把稍后需要的东西编进序号里，于是握手在零内存占用下完成。这确实是一次救援，同时也是一次降级：被救下来的那条连接，比正常情况下本该建立的那条更差。&lt;/p&gt;
&lt;h2 id="两个队列两种症状"&gt;两个队列，两种症状&lt;/h2&gt;
&lt;p&gt;一个 SYN 会分配一个 request socket，把连接推进 &lt;code&gt;SYN_RECV&lt;/code&gt; 并回一个 SYN-ACK。客户端的 ACK 再把它移进 accept 队列、状态变成 &lt;code&gt;ESTABLISHED&lt;/code&gt;。SYN 队列由 &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; 按监听套接字限流——最小值 128，随内存放大，每个表项大约占 304 字节。accept 队列的上限来自 &lt;code&gt;somaxconn&lt;/code&gt;（当前内核默认 4096，Linux 5.4 之前是 128），再与应用程序传给 &lt;code&gt;listen()&lt;/code&gt; 的 backlog 共同起作用。&lt;/p&gt;
&lt;p&gt;两种溢出长得完全不一样。SYN 队列溢出正是 syncookie 为之而生的场景：连接照样能完成。accept 队列溢出更糟也更安静——在 &lt;code&gt;tcp_abort_on_overflow&lt;/code&gt; 取默认值 0 的情况下，服务器直接忽略那个最后的 ACK，于是 &lt;code&gt;connect()&lt;/code&gt; 返回成功，客户端把请求发出去，然后再也等不到任何回应。实测：backlog 设为 1、客户端 400 个，400 个全都以为自己连上了，服务器实际只能接受 3 个，另外 397 个在两端都没有任何报错的情况下丢掉了。&lt;/p&gt;
&lt;h2 id="cookie-就是你拒绝保存的那份状态"&gt;Cookie 就是你拒绝保存的那份状态&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tcp_syncookies&lt;/code&gt; 取默认值 1 时，SYN backlog 一溢出，内核就发 cookie。它不再分配 request socket，而是把初始序号算成「高 8 位放计数器、低 24 位放截断哈希」，哈希覆盖两端地址、两个端口、计数器和一个服务端密钥。计数器每 64 秒前进一格，所以同一个四元组在每一格上算出的 cookie 都不同。&lt;/p&gt;
&lt;p&gt;校验过程就是把它倒过来。ACK 的确认号高 8 位带着计数器，服务器拿它和自己的当前格、上一格比对，再按四元组重算哈希并比较低 24 位。复现这套构造的结果是：刚发出的 ACK 校验通过；70 秒后重放的 ACK——也就是往前一格——依然通过；200 秒和 600 秒之后则被判为过期。把源端口或源地址改动一个，立刻被拒，所以 cookie 被绑死在这条连接上，无法搬到别处使用。盲猜的成功概率是 1677 万分之一，整个安全论证就靠这个数字。&lt;/p&gt;
&lt;h2 id="cookie-装不下的东西"&gt;Cookie 装不下的东西&lt;/h2&gt;
&lt;p&gt;带 cookie 的 SYN-ACK 不携带任何选项。内核文档说得毫不客气：syncookie「严重违反 TCP 协议，不允许使用 TCP 扩展，可能导致某些服务严重劣化」。代价最贵的是窗口缩放——没有它，接收窗口上限是 65535 字节，而不是缩放因子 7 下的 8388480 字节。在 100 毫秒的路径上，这就是 5.2 Mbit/s 对 671.1 Mbit/s，128 倍的差距，只因为少了一个选项。SACK 同样没了，于是一个窗口里丢了好几个段的发送方，只能靠累积确认一次恢复一个。&lt;/p&gt;
&lt;p&gt;MSS 算是部分救回来了：cookie 路径偷走三个哈希位去索引一张小 MSS 表，而这是唯一活下来的选项。时间戳、SACK、窗口缩放则干脆不存在。所以处于 cookie 模式的服务器不只是少收了几条连接，它收下的那些连接本身是慢的，而这种慢看起来像网络问题，不像握手问题。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;第一种失败，是 cookie 把真实的洪泛从监控视野里藏了起来。&lt;code&gt;SyncookiesSent&lt;/code&gt; 上涨就是信号，但按设计，洪泛在应用层是隐形的：没有半开队列可告警、没有失败连接、没有错误日志，只有一个计数器。建立在错误率或 &lt;code&gt;SYN_RECV&lt;/code&gt; 深度上的看板，会在服务器正被持续攻击时显示一切健康。要盯的是 cookie 计数器及其与新建连接数的比值，不是应用本身。&lt;/p&gt;
&lt;p&gt;第二种失败，是 accept 队列照样溢出，而 cookie 对此毫无办法。&lt;code&gt;ListenOverflows&lt;/code&gt; 上涨，客户端在第一个请求上超时，瓶颈在 &lt;code&gt;accept()&lt;/code&gt; 而不在 SYN 队列——实测 400 条里丢掉 397 条，而每个客户端都以为自己连上了。解法是让应用接受得更快、加 worker、抬高 &lt;code&gt;somaxconn&lt;/code&gt; 和 listen backlog，而不是去调 syncookie。要记住 cookie 只在 SYN 队列溢出时才启用：瓶颈若在 accept 队列，把它打开或关掉都不会改变任何数字。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;同时看两个队列和计数器：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sysctl net.ipv4.tcp_syncookies net.ipv4.tcp_max_syn_backlog net.core.somaxconn
nstat -az | grep -E &amp;#39;Syncookies|ListenOverflow|ListenDrops&amp;#39;
ss -lnt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;健康的服务器上三个计数器都读 0，&lt;code&gt;ss -lnt&lt;/code&gt; 里的 &lt;code&gt;Send-Q&lt;/code&gt; 与配置的 backlog 一致，&lt;code&gt;Recv-Q&lt;/code&gt; 为 0。&lt;code&gt;SyncookiesSent&lt;/code&gt; 上涨说明 SYN 队列溢出过；&lt;code&gt;ListenOverflows&lt;/code&gt; 上涨说明 accept 队列溢出过，而那一个不是 cookie 能修的。如果监听套接字的 &lt;code&gt;Recv-Q&lt;/code&gt; 长期大于零，说明已建立的连接正在等一个调用 &lt;code&gt;accept()&lt;/code&gt; 不够快的应用。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc4987.html"&gt;RFC 4987 TCP SYN 洪泛缓解&lt;/a&gt;，cookie 的构造方式及其取舍，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9293.html"&gt;RFC 9293 TCP&lt;/a&gt;，三次握手与 backlog 的概念，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/networking/ip-sysctl.html"&gt;Linux 内核 ip-sysctl&lt;/a&gt;，tcp_syncookies、tcp_max_syn_backlog、somaxconn 与 tcp_abort_on_overflow，已核验 2026-09-16。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>