Part VII — The Transport Layer: TCP mechanics
TCP gives an application a reliable, ordered, bidirectional byte stream over
unreliable IP. "Byte stream" means TCP does not preserve your message
boundaries — 3 writes of 100 bytes may arrive as one read() of 300 or five
reads of 60. Delimiting messages is the application's job (length prefixes,
newlines, HTTP Content-Length, etc.).
23. The TCP header, field by field
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data |Rsvd |C|E|U|A|P|R|S|F| |
| Offset| |W|C|R|C|S|S|Y|I| Window Size |
| | |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (0-40 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Field | Size | Meaning / real-world relevance |
|---|---|---|
| Source / Dest Port | 16 b each | With the IPs, forms the 4-tuple. |
| Sequence Number | 32 b | Byte offset of the first data byte in this segment within the stream. On the SYN it carries the ISN (Initial Sequence Number, randomized — see 24.4). |
| Acknowledgment Number | 32 b | "I have received every byte up to (ack−1); send me byte ack next." Cumulative. Valid only when ACK flag set (which is always, after the SYN). |
| Data Offset | 4 b | Header length in 32-bit words; 5 = 20 bytes, 15 = 60 max. Tells where data starts. |
| Reserved | 3 b | Must be 0. |
| CWR / ECE | 1 b each | ECN signaling: ECE = "I saw a CE-marked packet, slow down"; CWR = "I reduced my window." Works with the IP-header ECN bits (Ch. 10, 31). |
| URG + Urgent Pointer | 1 b + 16 b | "Urgent data" up to the pointer. Effectively dead (Telnet Ctrl-C heritage); often stripped by middleboxes; a minor security footgun. |
| ACK | 1 b | Ack number is meaningful. Set on everything except the initial SYN. |
| PSH | 1 b | "Push buffered data to the app now." Mostly advisory today. |
| RST | 1 b | Reset: abort the connection immediately, no graceful close. Sent for "connection refused" (SYN to a closed port), for data on a nonexistent connection, or by an app calling close() with SO_LINGER 0, or by a firewall forging it. |
| SYN | 1 b | Synchronize sequence numbers — connection setup. Consumes one sequence number. |
| FIN | 1 b | "I'm done sending." Graceful half-close. Consumes one sequence number. |
| Window Size | 16 b | Receive window (rwnd): how many more bytes the sender of this segment is willing to receive right now (flow control, Ch. 30). Max 65535 unless window scaling multiplies it. |
| Checksum | 16 b | Covers pseudo-header (src/dst IP, protocol, TCP length) + TCP header + data. Mandatory. NICs usually offload it (Ch. 49) — so a local capture may show "checksum incorrect (offloaded)". |
| Urgent Pointer | 16 b | see URG. |
23.1 The options that actually matter (negotiated on the SYN)
| Option | Kind | Purpose |
|---|---|---|
| MSS | 2 | Max Segment Size this host will accept (Ch. 8). Each side states its own; effective = min. Typically 1460 (Ethernet) or 1448 (with timestamps). |
| Window Scale (WScale) | 3 | Left-shift the 16-bit window by 0–14 bits → windows up to ~1 GB. Must be on both SYNs or scaling is off for the whole connection. Essential for high bandwidth-delay paths (Ch. 35). |
| SACK-Permitted | 4 | "I support Selective ACK." Enables SACK blocks in later segments (Ch. 29). |
| SACK | 5 | In data segments: "I have bytes A–B and C–D but I'm missing the gap" — lets the sender retransmit only the holes. |
| Timestamps (TSopt) | 8 | Two 32-bit values (TSval/TSecr). Used for accurate RTT measurement on every segment (RTTM) and PAWS (Protection Against Wrapped Sequences) on fast links. Costs 12 bytes/segment (10 + 2 padding). |
| MP_CAPABLE / MP_JOIN | 30 | Multipath TCP — one logical connection across multiple paths (Wi-Fi + cellular). |
| TCP Fast Open (TFO) | 34 | Send data in the SYN on repeat visits (a cookie authorizes it), saving one RTT. Patchy middlebox support. |
23.2 Reading flags in a capture
tcpdump shows flags as letters in [ ]:
| Symbol | Flag |
|---|---|
S |
SYN |
S. |
SYN+ACK (. = ACK) |
. |
ACK only |
P. |
PSH+ACK (data) |
F. |
FIN+ACK (graceful close) |
R / R. |
RST / RST+ACK (abort) |
E,W |
ECE, CWR |
Example line:
10:00:00.100 IP 10.0.0.5.51000 > 93.184.216.34.443: Flags [S], seq 1000,
win 64240, options [mss 1460,sackOK,TS val 111 ecr 0,nop,wscale 7], length 0
23.3 Sequence and acknowledgment arithmetic
- SYN and FIN each consume one sequence number even though they carry no
data. So after
SYN seq=1000, the first data byte isseq=1001. - A pure ACK carries no data and does not advance the sender's sequence number (which is why a lost pure ACK is simply superseded by the next one — ACKs are not retransmitted).
tcpdump -Sshows absolute sequence numbers; by default it shows relative ones (starting at 0/1) which is far easier to read.
23.4 War story
A load balancer health check "passed" but real traffic failed. Capture showed the
LB's SYN had wscale 7 and sackOK, the backend's SYN-ACK had no options at
all — an old middlebox was stripping TCP options on that path. Result: window
capped at 64 KB, no SACK; on the ~30 ms link, throughput ceilinged at
65535 / 0.03 ≈ 17 Mbit/s and any loss caused a full timeout. Routing the flow
around the option-stripping box restored gigabit. Lesson: always compare the
options on both SYNs; a middlebox that mangles them silently kneecaps TCP.
Try it yourself
sudo tcpdump -i any -n -c10 -S 'tcp[tcpflags] & (tcp-syn) != 0' # all SYNs, abs seq
sudo tcpdump -i any -n 'tcp port 443' -vv | grep -m4 options
# Verify negotiated features for a live socket:
ss -tie dst 93.184.216.34 # shows wscale, sack, ts, cubic, rtt, cwnd, mss...