Thuta Learning
IntermediateDevOps & Toolsbeginner

TCP: The Three-Way Handshake

What you'll walk away with

  • Explain the core ideas behind TCP: The Three-Way Handshake
  • Read the diagram and trace how data actually moves through it
  • Run the sample code and verify its output

Build the mental model

IP delivers individual packets and promises nothing: a packet can be lost, duplicated, delayed, or arrive out of order. TCP's job is to build a reliable, ordered byte stream on top of that, and it cannot start until both ends agree on where the stream begins. That agreement is the entire point of the handshake. The client sends a SYN carrying its initial sequence number (ISN); the server replies with a SYN-ACK carrying its own ISN plus an acknowledgement of the client's; the client sends a final ACK. Three segments are needed because a connection is full-duplex: there are two independent streams, and each one needs its starting point announced and confirmed.

The ISN is neither zero nor a simple counter. It is randomized, for two reasons. An off-path attacker who can guess the next sequence number can inject data into someone else's connection without ever seeing the traffic. And a straggling segment from an earlier connection using the same four-tuple could otherwise be accepted as legitimate data in a new one. Randomizing the ISN makes both impractical.

Each side walks its own state machine. The client goes SYN_SENT, then ESTABLISHED; the server waits in LISTEN, moves to SYN_RECEIVED, then ESTABLISHED. Closing, though, is four-way rather than three-way, because each direction is shut down separately with its own FIN and ACK — which is exactly why one side can keep sending after the other has stopped. The side that closes first then sits in TIME_WAIT for twice the maximum segment lifetime, deliberately holding the port so that a late segment from the dead connection cannot be mistaken for data belonging to a new connection reusing the same four-tuple.

text
TCP THREE-WAY HANDSHAKE AND FOUR-WAY CLOSE
------------------------------------------
Client                                          Server
  |                                                |
CLOSED                                          LISTEN
  |------------------ SYN  seq=x ----------------->|
SYN_SENT                                  SYN_RECEIVED
  |<---------- SYN-ACK  seq=y  ack=x+1 ------------|
  |------------ ACK  seq=x+1  ack=y+1 ------------>|
ESTABLISHED                                ESTABLISHED
  |         ... application data flows ...         |
  |----------- FIN  (I am done sending) ---------->|
FIN_WAIT_1                                  CLOSE_WAIT
  |<-------------------- ACK ----------------------|
  |<---------- FIN  (now I am done too) -----------|
                                              LAST_ACK
  |--------------------- ACK --------------------->|
TIME_WAIT                                       CLOSED
  waits 2 x MSL, then CLOSED

Connect it to a real scenario

Suppose you are debugging why your mobile app's login feels slow on a 3G connection. The request itself is 400 bytes and the server responds in 20 ms, yet users wait close to a second. Walk the handshake and the arithmetic explains it. Before a single byte of your POST leaves the phone, TCP has spent a full round trip on SYN and SYN-ACK. TLS then needs one or two more on top of that. On a link with a 200 ms round-trip time, 400-600 ms are gone before the application protocol has even started. This is why connection reuse is so high-leverage: a client with a connection pool, or a server honouring Connection: keep-alive, pays the handshake once and amortises it over hundreds of requests. It is also why HTTP/2 over a single connection beats six parallel HTTP/1.1 connections on a slow link.

The other place the handshake shows up in production is TIME_WAIT. A proxy that opens and closes a connection per request accumulates thousands of sockets stuck in TIME_WAIT on whichever side closed first, each holding a local port for about two minutes. When ephemeral ports run out you start seeing 'cannot assign requested address'. The fix is almost never to shorten TIME_WAIT — it is to stop closing connections you are about to reopen.

Try the working example

python
import struct

# TCP flag bits, least significant bit first (bit 0 = FIN)
FLAG_NAMES = ["FIN", "SYN", "RST", "PSH", "ACK", "URG"]


def build_tcp_header(src_port, dst_port, seq, ack, flags, window):
    """Pack a 20-byte TCP header. data_offset=5 means 5 * 4 = 20 bytes."""
    data_offset = 5
    offset_flags = (data_offset << 12) | flags
    return struct.pack(
        "!HHIIHHHH",
        src_port, dst_port, seq, ack, offset_flags, window, 0, 0
    )


def describe(label, header):
    src, dst, seq, ack, off_flags, window, _csum, _urg = struct.unpack(
        "!HHIIHHHH", header
    )
    header_len = (off_flags >> 12) * 4
    bits = off_flags & 0x3F
    names = [FLAG_NAMES[i] for i in range(6) if bits & (1 << i)]
    print(label)
    print("  bytes on wire : %d (offset field says %d)" %
          (len(header), header_len))
    print("  ports         :", src, "->", dst)
    print("  seq / ack     :", seq, "/", ack)
    print("  window        :", window)
    print("  flags         :", "+".join(names), "(0x%02x)" % bits)


# Client picks a random initial sequence number. Hard-coded here so the
# example is reproducible; a real stack would generate it unpredictably.
CLIENT_ISN = 1105843201
SERVER_ISN = 3221225472

syn = build_tcp_header(49152, 443, CLIENT_ISN, 0, 0x02, 64240)
syn_ack = build_tcp_header(443, 49152, SERVER_ISN, CLIENT_ISN + 1, 0x12, 65535)
ack = build_tcp_header(49152, 443, CLIENT_ISN + 1, SERVER_ISN + 1, 0x10, 64240)

describe("1. SYN     (client -> server)", syn)
describe("2. SYN-ACK (server -> client)", syn_ack)
describe("3. ACK     (client -> server)", ack)

print()
print("raw SYN hex   :", syn.hex())
print("handshake costs 0 bytes of application data, 3 segments")
You should see
1. SYN     (client -> server)
  bytes on wire : 20 (offset field says 20)
  ports         : 49152 -> 443
  seq / ack     : 1105843201 / 0
  window        : 64240
  flags         : SYN (0x02)
2. SYN-ACK (server -> client)
  bytes on wire : 20 (offset field says 20)
  ports         : 443 -> 49152
  seq / ack     : 3221225472 / 1105843202
  window        : 65535
  flags         : SYN+ACK (0x12)
3. ACK     (client -> server)
  bytes on wire : 20 (offset field says 20)
  ports         : 49152 -> 443
  seq / ack     : 1105843202 / 3221225473
  window        : 64240
  flags         : ACK (0x10)

raw SYN hex   : c00001bb41e9d401000000005002faf000000000
handshake costs 0 bytes of application data, 3 segments

5-minute try-it

Extend build_tcp_header to produce a FIN+ACK (0x11) and an RST (0x04) segment and confirm the decoder names them correctly. Then work out on paper: if a segment carries 100 bytes of data starting at seq=x, what ACK number should come back, and how many sequence-number slots does a bare SYN itself consume?

One important caution

Reading the handshake as 'three packets means three round trips'. It is one and a half round trips, and the cost you actually pay before sending data is one RTT.

Trying to fix ephemeral-port exhaustion by shortening TIME_WAIT (or with tcp_tw_recycle, removed from modern Linux). TIME_WAIT is doing its job; the real bug is a client opening a fresh connection per request.

RFC 9293 - Transmission Control Protocol (TCP)Computer Networking

Easy traps

  • Reading the handshake as 'three packets means three round trips'. It is one and a half round trips, and the cost you actually pay before sending data is one RTT.
  • Trying to fix ephemeral-port exhaustion by shortening TIME_WAIT (or with tcp_tw_recycle, removed from modern Linux). TIME_WAIT is doing its job; the real bug is a client opening a fresh connection per request.
  • Validate sample code in a local or test environment before applying it to a production network.

Exercise

Extend build_tcp_header to produce a FIN+ACK (0x11) and an RST (0x04) segment and confirm the decoder names them correctly. Then work out on paper: if a segment carries 100 bytes of data starting at seq=x, what ACK number should come back, and how many sequence-number slots does a bare SYN itself consume?

You'll know it worked when: 1. SYN (client -> server) bytes on wire : 20 (offset field says 20) ports : 49152 -> 443 seq / ack : 1105843201 / 0 window : 64240 flags : SYN (0x02) 2. SYN-ACK (server -> client) bytes on wire : 20 (offset field says 20) ports : 443 -> 49152 seq / ack : 3221225472 / 1105843202 window : 65535 flags : SYN+ACK (0x12) 3. ACK (client -> server) bytes on wire : 20 (offset field says 20) ports : 49152 -> 443 seq / ack : 1105843202 / 3221225473 window : 64240 flags : ACK (0x10) raw SYN hex : c00001bb41e9d401000000005002faf000000000 handshake costs 0 bytes of application data, 3 segments

TCP: The Three-Way Handshake | Thuta Learning