Learn › The Network Stack › Connections
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.
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.
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.
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.
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.
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.
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.
>>> To acknowledge the server's starting sequence number. Read the exchange as two synchronizations and two acknowledgments: the client synchronizes, the server acknowledges that and synchronizes in the same breath, and the client acknowledges the server. Four jobs, three packets, because the middle one does double duty. If you picked the speed answer, TCP does measure round-trip time, but it does that continuously once the connection is up, and no packet in the handshake exists for it. If you picked the first byte of the request, watch the captures ahead closely: the third packet carries Len=0, meaning zero bytes of data. The request comes afterwards, in a packet of its own.
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.
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.
>>> Two: the initial SYN and the SYN-ACK, because SYN-ACK is not a third kind of packet, it is one packet with two bits set at once. The capture asks for five and gets two, so it sits waiting for three more that will never come, and you end it with Ctrl+C. If you picked one, that is the mental model this step exists to correct: flags are independent bits, and a packet can raise several of them together. If you picked five, notice what SYN means. It synchronizes a starting sequence number, and that only happens at the beginning. Setting it on a packet mid-connection would be a bug, or an attack.
Every TCP packet carries a set of flags, single-bit indicators that control the connection. The important ones:
| Flag | Meaning |
|---|---|
| SYN | Synchronize sequence numbers (connection setup) |
| ACK | Acknowledgment field is valid |
| FIN | Sender is finished sending data (connection teardown) |
| RST | Reset the connection (abort) |
| PSH | Push 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.
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.
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.
>>> A SYN consumes one sequence number of its own. That is what makes the handshake reliable rather than hopeful: the SYN occupies a position in the byte stream, so it can be acknowledged, and an acknowledged SYN cannot be quietly lost. FIN behaves the same way at the other end of the connection. If you picked guessing, nothing in TCP guesses about sequence numbers; every number in the acknowledgment field refers to something the sender has already committed to. If you picked rounding, relative numbering only subtracts the random starting value, it never invents a byte.
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.
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.
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.
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.