In September 1996 an ISP in New York called Panix stopped delivering mail. Not because a server crashed or a disk filled up — because someone was sending it a flood of connection requests that never turned into connections. The attack was cheap, it was hard to trace, and there was no good defense for it. Within days, Daniel Bernstein and Eric Schenk had worked out one anyway. Their fix is still running, on by default, in every Linux machine you’ve ever touched. It’s called SYN cookies, and the interesting part isn’t that it works — it’s the strange thing it gives up to work.
I’ve spent a few posts now on attacks that win by making a server hold onto something it can’t afford to hold. Slowloris parks a worker on a request that never ends. Rapid Reset makes the server do work for a stream the client already cancelled. The SYN flood is the oldest member of that family, and SYN cookies are the cleanest answer anyone ever found to it — because instead of trying to hold the scarce thing more efficiently, they refuse to hold it at all.
The handshake has a hole in the middle
Every TCP connection starts with three packets. The client sends a SYN. The server replies SYN-ACK. The client finishes with an ACK. After that the connection is “established” and data can flow. You know this one.
Here’s the part that matters. When the server sends its SYN-ACK, it can’t just forget about the connection until the final ACK shows up — it has to remember the details: whose connection this is, what sequence number it chose, what options were negotiated. So it allocates a small block of memory, a half-open connection record, and files it in a fixed-size queue called the SYN backlog. The connection sits there, half-built, waiting for the client to complete it.
Two facts collide badly. The record is created when the SYN arrives — before the server has heard anything back from the client. And the backlog is small. So an attacker doesn’t need to send data, doesn’t need bandwidth, doesn’t even need a real IP address. They send a stream of SYN packets with forged source addresses, the server dutifully allocates a half-open record and fires off a SYN-ACK to each forged address, and then it waits for an ACK that will never come, because the addresses were never real. Fill the backlog with these ghosts and there’s no room left for a legitimate SYN. The server isn’t overloaded. It’s just full of appointments with people who don’t exist.
It’s the same con every time. Get the server to commit something scarce — a backlog slot, here — to a promise the client has no intention of keeping.
The trick: don’t remember, reconstruct
The obvious fixes are all bad. Make the backlog bigger, and the attacker sends more SYNs — they cost nothing. Time out half-open connections faster, and you start dropping legitimate clients on slow links. Every one of these is trying to hold the scarce resource a little more cleverly, and you can’t out-clever an attacker whose per-unit cost is a single spoofed packet.
Bernstein’s move was to notice that the server is storing state for one reason: it needs to recognize the client’s final ACK and match it back to the right half-open connection. And there’s a field in that ACK the server already controls — its own initial sequence number. TCP requires the client to echo it back, plus one, in the acknowledgment. Whatever number the server puts in its SYN-ACK, it gets handed right back.
So: what if the sequence number was the state?
That’s the whole idea. Instead of choosing a random initial sequence number and remembering it in a table, the server computes the sequence number from the connection itself, sends it, and stores nothing. When an ACK comes back, the server recomputes what the number should have been and checks it matches. A real client returns a valid number because the server just gave it one. A spoofed flood can’t, because it never received the SYN-ACK in the first place — those went to forged addresses. The backlog stops being a resource worth attacking, because there’s nothing in it.
What fits in 32 bits
The catch is that a TCP sequence number is only 32 bits, and now those 32 bits have to carry everything the server would otherwise have kept in memory. The layout Bernstein designed, and Linux still uses, splits them three ways:
- 5 bits for a coarse timestamp — a counter that ticks roughly once a minute — so the server can reject cookies that are too old and won’t accept a replayed
ACKforever. - 3 bits for the maximum segment size. Not the actual MSS, which needs sixteen bits — just an index into a table of eight common values, rounded to the nearest one.
- 24 bits for a keyed hash of the connection’s four-tuple (source and destination IP and port) plus the timestamp, mixed with a secret only the server knows.
That last field is the security of the whole scheme. An attacker can’t produce a valid ACK out of thin air, because they’d have to guess a 24-bit hash keyed by a secret they don’t have. And they can’t replay an old captured one for long, because the timestamp ages it out. When the final ACK arrives, the server pulls the sequence number back apart: recompute the hash, confirm it matches and isn’t stale, read the MSS index back out, and build the real connection — all from a number the client handed back, with no table lookup because there was never a table.
The elegant part is what it throws away
Here’s the tradeoff nobody mentions when they call SYN cookies “elegant.” They’re lossy. On purpose.
A normal SYN carries options in its header — window scaling, selective acknowledgment (SACK), timestamps — and the server is supposed to remember them for the life of the connection. But there’s no room. Three bits bought you a rounded-off MSS and nothing else. Window scaling, which lets a connection use a receive window bigger than 64 KB and is the difference between a fast transfer and a slow one on any modern link — gone. SACK, which lets TCP recover from packet loss without retransmitting everything — gone. A connection built from a cookie is a connection with its performance options quietly stripped off.
There’s a second cost, subtler. Because the server kept no state, it can’t retransmit its own SYN-ACK if that packet gets lost. Normally the server would notice the missing ACK and resend. Under cookies it has nothing to resend from, so a dropped SYN-ACK means the client has to time out and start over.
This is why SYN cookies were never meant to be the normal path, and Linux treats them exactly right. net.ipv4.tcp_syncookies has been on by default for years, but it does nothing until the backlog actually overflows. Under normal load you get ordinary handshakes with all their options intact. Only when the queue is full — when you’re plausibly under attack — does the kernel fall back to the stateless, lossy, cookie mode. It’s a pressure-release valve, not the front door. You accept degraded connections precisely when the alternative is accepting none.
The lossiness bothered people enough that in 2008, kernel 2.6.26 shipped a fix that’s almost as clever as the original. The TCP timestamp option is also echoed back by the client. So if the client supports timestamps, Linux stuffs the window-scale and SACK settings into the low bits of the timestamp it sends, gets them handed back in the ACK, and reconstructs the options it couldn’t fit in the sequence number. Bernstein’s original couldn’t do this; the modern implementation quietly recovers most of what the 32-bit budget forced it to drop. The same move as the cookie itself — if you have no memory, write the state onto the wire and let the client carry it back for you.
It’s worth being honest that the scheme isn’t free security, either. Twenty-four bits of hash and a minute-wide timestamp window is strong enough to stop a spoofed flood cold, but researchers have shown it lowers the bar for blind connection spoofing — an attacker who can guess a valid cookie can, in narrow circumstances, forge a connection they never received the SYN-ACK for. You’re trading a sliver of robustness for staying up under attack. Given the alternative is being Panix in September 1996, that’s a trade almost everyone takes.
The constraint was the design
What I keep coming back to is that SYN cookies exist because of a limit, not despite one. There was no spare memory to hold half-open connections during a flood, so the state had to go somewhere else — and the only place available was a number the client was already required to echo. The 32-bit budget forced the rounding, the lossiness, the whole awkward, brilliant shape of the thing. A more generous field would have produced a more boring, and worse, design.
That’s the same lesson the slow attacks teach from the other direction. The whole family — SYN floods, Slowloris, the HTTP/2 floods — works by getting a server to commit something scarce to an unverified promise, and every real defense is a variation on the same instinct: don’t hold anything for a client until it’s proven it’s really there. SYN cookies are the purest version of that instinct ever written. Hold nothing. Prove it on the return trip. It fit in thirty-two bits in 1996, and it’s still holding the door in 2026.