LearnThe Network StackConnections

The TCP Handshake

tshark - a hands-on Linux lab on a real virtual machine.

SYN, SYN-ACK, ACK. Three packets before a single byte of data. Watch the handshake happen in real time.

Three packets before anybody says anything

Before a single byte of data can travel over a TCP connection, three packets must be exchanged. Not two. Not one. Exactly three. This is the three-way handshake, and it happens every time you visit a website, SSH into a server, or send an email.

You have already seen those three packets go past. In the previous lesson you captured ten packets of a web request and read the first three as SYN, SYN-ACK, ACK. This lesson is the zoom lens: the same three packets, opened up field by field, until you can say what each one promises and what would break without it.

By the end you will have watched the handshake live, read the flag bits as a string of characters, dissected a SYN packet down to its options, followed the sequence-number arithmetic through a whole connection, and caught the goodbye on its own.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The real VM comes at the end, under Ready to practice. Your progress in the real lab is tracked automatically, so type commands naturally and watch the checkmarks appear.

Why a handshake?

TCP is reliable. That means both sides need to agree on a few things before data flows:

The handshake accomplishes all three in exactly three packets.

The three packets

Here is what happens when a client connects to a server:

Packet 1: Client -> Server   [SYN]       "I want to connect. My sequence number starts at X."
Packet 2: Server -> Client   [SYN, ACK]  "OK, I accept. My sequence number starts at Y. I acknowledge your X."
Packet 3: Client -> Server   [ACK]       "Got it. Connection established."

SYN means "synchronize." It is how each side announces its starting sequence number. ACK means "acknowledge." It confirms receipt of the other side's sequence number.

After these three packets, the connection is open and data can flow in both directions.

Why the third packet exists

The handshake was settled in 1981, in RFC 793, the specification that still defines TCP. The problem it had to solve sounds obscure and turns out to be everywhere: a network can deliver an old packet late. A connection request that wandered around a slow path for a minute can arrive long after the machine that sent it gave up, and a server that accepted it blindly would open a connection nobody wanted, then sit there holding memory for a conversation that never comes.

Three packets fix that. The server does not treat a connection as real until the client answers the server's own random starting number. A stale request cannot produce that answer, because it never saw the number.

That gap between packet two and packet three has a name, half-open, and in 1996 it became famous. Attackers flooded servers with SYN packets that were never answered, filling the queue of half-open connections until real users could not get in. The defence that came out of it, SYN cookies, lets a server forget half-open connections entirely and reconstruct the state from the client's reply when it arrives. Your Linux box still has that defence switched on today.

Your lab network

Same topology as the previous lesson:

  [client]  10.0.1.10  <-------->  10.0.1.20  [server]
  MAC: 02:00:0a:00:01:0a           MAC: 02:00:0a:00:01:14

  Server: HTTP on TCP port 80

Every time you run send-curl, the client opens a TCP connection to the server, sends an HTTP request, receives the response, and closes the connection. That means every send-curl triggers a full handshake that you can capture.

cap is the same wrapper as before: it runs tshark on the client side of the link, so anything you pass to cap goes straight to tshark.

Tab completion works on command names, not just on paths. Type send- and press Tab twice to see the traffic generators, or ca and Tab to finish cap. Every command in this lesson is long enough that a typo is likely and Tab is faster than retyping. It also proves the command exists before you press Enter.

Commit: why does the client answer at all?

Look at the three packets again. The client asks, the server accepts. Then the client sends a third packet that carries no data whatsoever. Take a position on what it is for before you capture one.

Capturing the handshake

One detail decides whether this works: the capture has to be running before the traffic happens. tshark needs a second or two to load its dissectors and open the interface. If you fire the request first, the handshake is over before tshark is listening, and you sit staring at an empty capture.

So schedule the traffic to fire a few seconds later, in the background, and start the capture straight away:

(sleep 5; send-curl) &

That line means "wait 5 seconds, then run send-curl, and do all of that in the background so I get my prompt back". Now start the capture and stop after 10 packets, showing only TCP. -Y tcp is a display filter: show only TCP.

The packet numbers, the timestamps and the client port will be different on your VM. Ten lines will scroll past; the screen below is trimmed to the first three, because those three are the handshake and the rest is the web request you already met:

cap -c 10 -Y tcp

prompt: student@handshake-lab:~$ answer: cap -c 10 -Y tcp ||| cap -Y tcp -c 10 ||| sudo ip netns exec client tshark -i veth-c -c 10 -Y tcp output: 1 0.000000 10.0.1.10 -> 10.0.1.20 TCP 74 43218 > 80 [SYN] Seq=0 Win=64240 Len=0 2 0.000034 10.0.1.20 -> 10.0.1.10 TCP 74 80 > 43218 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 3 0.000052 10.0.1.10 -> 10.0.1.20 TCP 66 43218 > 80 [ACK] Seq=1 Ack=1 Win=64240 Len=0 hint: The capture shortcut, a count of ten, and a display filter of tcp on its own: cap -c 10 -Y tcp

Those first three packets are the handshake. Read them carefully:

1. Packet 1 [SYN]: The client sends a SYN to port 80 on the server. Seq=0 is the client's starting sequence number. Win is the client's receive window (how many bytes it is willing to buffer). 2. Packet 2 [SYN, ACK]: The server responds with SYN and ACK. It has its own Seq=0 and acknowledges the client's sequence by setting Ack=1 (meaning "I received your byte 0, send me byte 1 next"). 3. Packet 3 [ACK]: The client acknowledges the server's sequence number. Seq=1, Ack=1. Connection is now established.

Notice Len=0 on all three. Not one byte of anybody's data has moved yet, and the connection is already open. Capturing this is the first of the five things your lab grades.

tshark shows "relative" sequence numbers by default. The real sequence numbers are large random values chosen by each side when the connection starts. tshark subtracts that starting value so the numbers read 0, 1, 2 instead. You can see the real values with -o tcp.relative_sequence_numbers:FALSE.

Commit: how many packets match a SYN filter?

A display filter can name a single flag instead of a whole protocol. tcp.flags.syn==1 means "packets with the SYN bit set", and you would write the capture like this:

(sleep 5; send-curl) &
cap -c 5 -Y "tcp.flags.syn==1"

Quote the filter whenever it contains == or spaces, otherwise the shell hands the pieces to tshark as separate arguments. Now commit to a number before you read on.

Reading TCP flags

Every TCP packet carries a set of flags, single-bit indicators that control the connection. The important ones:

FlagMeaning
SYNSynchronize sequence numbers (connection setup)
ACKAcknowledgment field is valid
FINSender is finished sending data (connection teardown)
RSTReset the connection (abort)
PSHPush data to the application immediately

Instead of printing whole packet summaries, you can ask tshark for named fields only. -T fields switches to that mode and each -e names one field to print. Ask for the packet number, both addresses, and the flags:

(sleep 5; send-curl) &

Each row is one packet, with the columns separated by tabs. The last column is the flags string. Ten rows will print; the screen below keeps the first five, which is where the story is:

cap -c 10 -T fields -e frame.number -e ip.src -e ip.dst -e tcp.flags.str

prompt: student@handshake-lab:~$ answer: cap -c 10 -T fields -e frame.number -e ip.src -e ip.dst -e tcp.flags.str ||| sudo ip netns exec client tshark -i veth-c -c 10 -T fields -e frame.number -e ip.src -e ip.dst -e tcp.flags.str output: 1 10.0.1.10 10.0.1.20 ..........S. 2 10.0.1.20 10.0.1.10 .......A..S. 3 10.0.1.10 10.0.1.20 .......A.... 4 10.0.1.10 10.0.1.20 .......AP... 5 10.0.1.20 10.0.1.10 .......A.... hint: Fields mode with four -e flags, the last one being the flags string: cap -c 10 -T fields -e frame.number -e ip.src -e ip.dst -e tcp.flags.str

The flags string is always 12 characters, one position per flag. A letter means that flag is set and a placeholder character means it is not. Counting from the left, position 8 is ACK, position 9 is PSH, position 10 is RST, position 11 is SYN, position 12 is FIN. S is SYN, A is ACK, P is PSH, F is FIN.

So packet 1 has only SYN. Packet 2 has ACK and SYN, the SYN-ACK. Packet 4 has ACK and PSH, which means it carries actual data.

You are now reading the handshake without any words at all, straight off the bits, and that is the second of the five things your lab grades. This is also the form that scales: a thousand-packet capture reduced to one column tells you where connections opened, where data moved and where things reset.

Full dissection of a SYN packet

To see everything inside a SYN packet, use -V (verbose) with a SYN filter. One extra condition, and tcp.flags.ack==0, excludes the SYN-ACK, so exactly one packet matches: the very first one of the connection.

(sleep 5; send-curl) &

tshark prints every layer of that packet, which runs to dozens of lines. The screen below is trimmed to the TCP part, the lines this step reads, and the port number and window will differ on your VM:

cap -c 1 -V -Y "tcp.flags.syn==1 and tcp.flags.ack==0"

prompt: student@handshake-lab:~$ answer: cap -c 1 -V -Y "tcp.flags.syn==1 and tcp.flags.ack==0" ||| cap -c 1 -V -Y 'tcp.flags.syn==1 and tcp.flags.ack==0' ||| sudo ip netns exec client tshark -i veth-c -c 1 -V -Y "tcp.flags.syn==1 and tcp.flags.ack==0" output: Transmission Control Protocol, Src Port: 43218, Dst Port: 80, Seq: 0, Len: 0 Source Port: 43218 Destination Port: 80 Sequence Number: 0 (relative sequence number) Acknowledgment Number: 0 Header Length: 40 bytes (10) Flags: 0x002 (SYN) .... .... ..1. = Syn: Set Window: 64240 Options: (20 bytes) Maximum segment size: 1460 bytes Window scale: 7 (multiply by 128) hint: The dissection flag plus a display filter that keeps SYN and drops SYN-ACK: cap -c 1 -V -Y "tcp.flags.syn==1 and tcp.flags.ack==0"

Key fields to notice:

This is the third of the five things your lab grades, and it is worth sitting with. Those options are negotiated once, in the handshake, and they govern the whole connection afterwards. A connection that performs badly for its entire life often decided why in this one packet.

Commit: acknowledging a byte that was never sent

Go back to the first screen you captured. Packet 1 is a SYN carrying Len=0, zero bytes of data. Packet 2 answers it with Ack=1.

Challenge: follow the arithmetic

Scaffolding off. No command is printed from here on.

Sequence numbers are how TCP tracks every byte. Each side starts with a random number, and it increases by the number of bytes sent. A colleague tells you TCP acknowledgments are approximate. Settle it with numbers instead of opinions.

Fire the traffic with (sleep 5; send-curl) &, then print ten packets in fields mode as four columns: packet number, sequence number, acknowledgment number, and how many bytes of data that packet carried. The field names are frame.number, tcp.seq, tcp.ack and tcp.len, and you used this output mode two steps ago.

Ten rows will print. The first three are the handshake, and they read the same way on every Linux machine:

prompt: student@handshake-lab:~$ answer: cap -c 10 -T fields -e frame.number -e tcp.seq -e tcp.ack -e tcp.len ||| sudo ip netns exec client tshark -i veth-c -c 10 -T fields -e frame.number -e tcp.seq -e tcp.ack -e tcp.len output: 1 0 0 0 2 0 1 0 3 1 1 0 hint: Fields mode again, a count of ten, and four -e flags: the frame number, then the sequence, acknowledgment and length fields.

Now read your own packets 4 onward, because the sizes depend on your curl version and on the headers this server sends back. The rule to check is always the same:

The next Ack equals the previous Seq plus that packet's Len.

Find the packet where the client sends the HTTP request. Note its Seq and its Len. The server's next packet acknowledges with exactly Seq plus Len, meaning "I have everything up to there, send me the next byte". Then the server sends the response and the client acknowledges the same way.

Every byte is accounted for. If a packet is lost, the receiver never acknowledges it, and the sender retransmits. That accounting is what makes TCP reliable, and reading it is the fourth of the five things your lab grades.

Milestone: you can read a connection being born

Stop and count what you can do now. You can make a connection happen on demand, catch its first three packets, and say what each one promises. You can read the flag bits with no words attached. You can open the opening packet and name the options that will govern the rest of its life. And you can follow the arithmetic that ties every acknowledgment to a byte somebody actually sent.

That is the beginning of a connection. There is one more part of the story, and it is the part that leaves sockets lying around on busy servers: how a connection ends.

The full connection lifecycle

A TCP connection has three phases:

1. Setup - the three-way handshake (SYN, SYN-ACK, ACK) 2. Data transfer - request and response with ACKs 3. Teardown - connection close (FIN, FIN-ACK, ACK)

Either side may close first. The little web server in this lab answers the request and then closes immediately, so here the server sends the first FIN:

Server -> Client   [FIN, ACK]  "I am done sending."
Client -> Server   [FIN, ACK]  "I am done too."
Server -> Client   [ACK]       "Acknowledged. Connection closed."

To see the whole story at once, run the task 1 command with a bigger packet count:

(sleep 5; send-curl) &
cap -c 20 -Y tcp

SYN packets at the beginning, FIN packets at the end, the data exchange in between. One HTTP request, start to finish, in your terminal.

Challenge: catch the goodbye on its own

Still no command shown.

Twenty packets is a lot of scrolling when the part you care about is the last three. You want the teardown by itself, with the handshake and the data stripped away.

Fire the traffic with (sleep 5; send-curl) &, then capture with a d

Practice The TCP Handshake in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.

More lessons in Connections