<?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>Handshake · XB Field Notes</title><link>https://www.xbcatm.com/tags/handshake/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/handshake/index.xml" rel="self" type="application/rss+xml"/><item><title>SYN cookies answer when the queue overflows</title><link>https://www.xbcatm.com/posts/tcp-syn-cookies-when-syns-overflow/</link><pubDate>Sat, 30 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/tcp-syn-cookies-when-syns-overflow/</guid><description>When the handshake queue is full, SYN cookies let the server answer without holding state, exchanging storage for resilience against floods.</description><content:encoded>&lt;p&gt;A listening socket keeps two queues, not one. Half-open connections live in the SYN queue until the final ACK arrives; established ones then wait in the accept queue until the application calls &lt;code&gt;accept()&lt;/code&gt;. Both have a limit, and which one overflows decides what the client experiences.&lt;/p&gt;
&lt;p&gt;SYN cookies exist for the first queue. When it is full the server stops allocating state and encodes what it needs into the sequence number, so the handshake completes with no memory held. That is a real rescue and a degradation: the connection it saves is worse than the one it would have built normally.&lt;/p&gt;
&lt;h2 id="two-queues-two-different-symptoms"&gt;Two queues, two different symptoms&lt;/h2&gt;
&lt;p&gt;A SYN allocates a request socket, moves the connection to &lt;code&gt;SYN_RECV&lt;/code&gt; and sends a SYN-ACK. The client&amp;rsquo;s ACK then moves it to the accept queue as &lt;code&gt;ESTABLISHED&lt;/code&gt;. The SYN queue is capped per listener by &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; — minimum 128, scaling with memory, about 304 bytes per entry. The accept queue is capped by &lt;code&gt;somaxconn&lt;/code&gt; (4096 by default on current kernels, 128 before Linux 5.4) combined with the application&amp;rsquo;s &lt;code&gt;listen()&lt;/code&gt; backlog.&lt;/p&gt;
&lt;p&gt;The two overflows look nothing alike. SYN queue overflow is what syncookies were built for: the connection still completes. Accept queue overflow is worse and quieter — with &lt;code&gt;tcp_abort_on_overflow&lt;/code&gt; at its default of 0 the server ignores the final ACK, so &lt;code&gt;connect()&lt;/code&gt; returns success, the client sends its request, and nothing answers. Measured with backlog 1 against 400 clients: all 400 believed they connected, the server accepted 3, and the other 397 were lost with no error on either side.&lt;/p&gt;
&lt;h2 id="the-cookie-is-the-state-you-refused-to-store"&gt;The cookie is the state you refused to store&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;tcp_syncookies&lt;/code&gt; at its default of 1, the kernel sends a cookie when the SYN backlog overflows. Instead of allocating a request socket it computes the initial sequence number as a counter in the top 8 bits and a truncated hash in the low 24, the hash covering both addresses, both ports, the counter and a server secret. The counter advances every 64 seconds, so the same 4-tuple yields a different cookie each tick.&lt;/p&gt;
&lt;p&gt;Verification reverses it. The ACK&amp;rsquo;s acknowledgement number carries the counter in its top 8 bits; the server checks it against its current and previous tick, recomputes the hash and compares the low 24 bits. Reproducing that construction: a fresh ACK verifies, one replayed 70 seconds later — a single tick — still verifies, and ACKs 200 and 600 seconds later are rejected as stale. Changing the source port or address by one is rejected outright, so the cookie cannot be lifted onto another connection. Blind forgery probability is one in 16.7 million, which is the entire security argument.&lt;/p&gt;
&lt;h2 id="what-a-cookie-cannot-carry"&gt;What a cookie cannot carry&lt;/h2&gt;
&lt;p&gt;A cookie SYN-ACK carries no options. The kernel documentation is blunt: syncookies &amp;ldquo;seriously violate TCP protocol, do not allow to use TCP extensions, can result in serious degradation of some services&amp;rdquo;. The expensive omission is window scaling — without it the receive window caps at 65535 bytes instead of 8388480 at scale factor 7. On a 100 ms path that is 5.2 Mbit/s instead of 671.1 Mbit/s, a 128-fold difference from one missing option. SACK is gone too, so a sender losing several segments in one window recovers one cumulative ACK at a time.&lt;/p&gt;
&lt;p&gt;The MSS is partly recovered: the cookie path steals three hash bits to index a small MSS table, and that is the only option that survives. Timestamps, SACK and window scale are simply absent. A server in cookie mode is not just admitting fewer connections; the ones it admits are slow, and that looks like a network problem rather than a handshake one.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The first failure is cookies hiding a real flood from monitoring. &lt;code&gt;SyncookiesSent&lt;/code&gt; climbing is the signal, but by design the flood is invisible at the application layer: no half-open backlog to alert on, no failed connections, no error log, just a counter. A dashboard built on error rates or &lt;code&gt;SYN_RECV&lt;/code&gt; depth shows a healthy server while it is under sustained attack. Watch the cookie counters and their ratio to new connections, not the application.&lt;/p&gt;
&lt;p&gt;The second failure is the accept queue overflowing anyway, which cookies do nothing about. &lt;code&gt;ListenOverflows&lt;/code&gt; climbs, clients see timeouts on their first request, and the bottleneck is &lt;code&gt;accept()&lt;/code&gt;, not the SYN queue — measured here as 397 of 400 lost with every client believing it connected. The fix is to accept faster, add workers and raise &lt;code&gt;somaxconn&lt;/code&gt; and the listen backlog, not to tune syncookies.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Compare both queues and the counters:&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;On a healthy server all three counters read 0, and &lt;code&gt;ss -lnt&lt;/code&gt; shows &lt;code&gt;Send-Q&lt;/code&gt; matching the configured backlog with &lt;code&gt;Recv-Q&lt;/code&gt; at 0. A rising &lt;code&gt;SyncookiesSent&lt;/code&gt; means the SYN queue overflowed; a rising &lt;code&gt;ListenOverflows&lt;/code&gt; means the accept queue did, and cookies will not fix that one. If &lt;code&gt;Recv-Q&lt;/code&gt; on a listening socket stays above zero, established connections are waiting for an application not calling &lt;code&gt;accept()&lt;/code&gt; fast enough.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 flood mitigation&lt;/a&gt;, the cookie construction and its trade-offs, checked 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;, the three-way handshake and the notion of a backlog, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/networking/ip-sysctl.html"&gt;Linux kernel ip-sysctl&lt;/a&gt;, tcp_syncookies, tcp_max_syn_backlog, somaxconn and tcp_abort_on_overflow, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>