Build Your Own TCP/IP Stack: TCP Basics & Handshake
You're downloading a file from the internet. It's 10 megabytes. Millions of bytes of data.
Your network splits it into thousands of tiny IP packets. Each one takes its own path through the internet, bouncing between routers.
Now here's the problem.
Packet #472 gets dropped by a congested router. Packet #103 arrives before packet #102. Packet #88 arrives twice because of a routing loop.
How do you put the file back together?
IP doesn't help. It's "best effort." Packets are fire-and-forget. We need something smarter.
The Reliability Challenge
Let's think through what can go wrong with IP:
Problem 1: Lost Packets Router queues overflow → packet dropped → you never receive it
Problem 2: Out-of-Order Delivery Packets take different paths → arrive in wrong sequence
Problem 3: Duplicates Network glitches → same packet arrives twice
Problem 4: No Guarantees IP makes zero promises about delivery
You're receiving thousands of packets. Some are missing. Some are out of order. Some are duplicates. How do you:
- Know which packets you've received?
- Know which packets are missing?
- Reassemble them in the correct order?
- Ask for retransmission of missing packets?
Take a moment. How would you solve this?
The Obvious Solutions
Let's work through this step by step.
For ordering: Number the packets.
If we label each packet with a sequence number (1, 2, 3, ...), the receiver can reassemble them correctly no matter what order they arrive in.
For detecting loss: Use acknowledgments.
When the receiver gets a packet, it sends back a message: "I received packet #472." If the sender never gets an acknowledgment, it knows the packet was lost.
For handling duplicates: Check the sequence number.
If packet #88 arrives twice, the receiver sees "I already have #88" and discards the duplicate.
For retransmission: Timeouts + re-send.
The sender waits for acknowledgment. If it doesn't arrive within a timeout period, resend the packet.
These four mechanisms (sequence numbers, acknowledgments, duplicate detection, and retransmission) are the foundation of TCP.
Let's build them.
Designing the TCP Header
We need a new layer that sits inside IP packets. This layer needs fields for all our mechanisms.
Let's design it:
Source and Destination Ports (16 bits each) Wait, ports? We haven't mentioned these yet. Here's the problem: your computer might have 20 connections open at once. How does it know which data belongs to which application?
Ports solve this. HTTP uses port 80. HTTPS uses port 443. Your browser might use port 54321. The combination (IP address + port) uniquely identifies a connection endpoint.
Sequence Number (32 bits) Which byte position does this segment represent? This is the key to ordering.
Acknowledgment Number (32 bits) "I've successfully received all bytes up to (but not including) this number." This is how the receiver confirms receipt.
Flags (various control bits) Special signals: SYN (start connection), ACK (acknowledgment valid), FIN (close connection), RST (reset/abort)
Window Size (16 bits) "I can accept this many more bytes." Flow control prevents overwhelming the receiver.
Checksum (16 bits) Same concept as IP checksum, but covering the TCP header + data.
Let's see it all together:
Click on each field. Pay special attention to sequence number (which bytes are these?) and acknowledgment number (which bytes have I received?). Try clicking the different flags at the bottom to see what they do.
Notice something important:
TCP doesn't think in packets. It thinks in BYTES. Sequence numbers aren't "packet 1, packet 2, packet 3." They're "byte 0, byte 1460, byte 2920." If you send 1000 bytes starting at sequence 5000, the next segment starts at sequence 6000. The stream is continuous.
This is crucial. TCP provides the illusion of a continuous stream of bytes, even though the network delivers discrete packets.
The Connection Setup Problem
Before we can send data, we need to solve a new problem.
Imagine you're the sender. You start sending data at sequence number 0. But wait. The network is chaotic. Old packets from previous connections might still be floating around.
What if an old packet from a previous connection (using sequence number 0) arrives after you start your new connection (also using sequence number 0)? The receiver can't tell which packet belongs to which connection!
We need each new connection to have unique sequence numbers that don't collide with old packets.
Solution: Start each connection at a random sequence number.
The client picks a random starting sequence (say, 5000). The server picks its own random starting sequence (say, 8000). Now even if old packets are floating around, they won't have the right sequence numbers for this connection.
But there's a catch: both sides need to know each other's starting sequence numbers.
How do they exchange this information?
Inventing the Three-Way Handshake
Let's think through how to establish a connection:
Attempt 1: Two messages?
- Client → Server: "My sequence starts at 5000"
- Server → Client: "My sequence starts at 8000"
Problem: The server doesn't know if the client received its message. If that second message gets lost, the client is waiting forever and the server thinks the connection is established. Disaster.
Attempt 2: Add acknowledgments
Let's use our ACK mechanism:
Message 1: Client → Server (SYN)
SYN flag set
Sequence: 5000
"I want to connect. My sequence starts at 5000."
Message 2: Server → Client (SYN-ACK)
SYN flag set, ACK flag set
Sequence: 8000
Acknowledgment: 5001
"OK! My sequence starts at 8000. I got your 5000."
Message 3: Client → Server (ACK)
ACK flag set
Sequence: 5001
Acknowledgment: 8001
"I got your 8000. We're connected!"
Why three messages instead of two?
- After message 1: Server knows client can send
- After message 2: Client knows server can send AND receive
- After message 3: Server knows client received its SYN-ACK
Only after all three messages do both sides have complete confidence that the connection is working in both directions.
This is the famous three-way handshake.
Click Start Handshake and watch all three messages. Notice how each side picks a random starting sequence number. See how the acknowledgment is always "your sequence + 1."
After the handshake, both sides know:
- The other side's starting sequence number
- The other side can send and receive
- The connection is ready for data
The state changes to ESTABLISHED. Data can now flow.
Connection States: Tracking the Lifecycle
TCP connections go through different states. Understanding them helps debug problems (like "why can't I reuse this port?").
The main states for establishing a connection:
- CLOSED: No connection exists
- LISTEN: Server is waiting for incoming connections
- SYN-SENT: Client sent SYN, waiting for SYN-ACK
- SYN-RECEIVED: Server got SYN, waiting for final ACK
- ESTABLISHED: Handshake complete, data can flow
After the three-way handshake, both sides are in the ESTABLISHED state. This is where data transfer happens.
There are actually 11 states total, but most handle edge cases during connection shutdown. If you've ever wondered why closing a TCP connection seems to take forever, it's because of multiple state transitions with mandatory waiting periods (we'll cover this in the next part).
For now, the key point: after the handshake completes, both sides are ESTABLISHED and ready to send/receive data.
When Things Go Wrong: The RST Flag
What if you try to connect to a server that isn't listening on that port? Or send data to a connection that was closed?
TCP handles this with the RST (reset) flag. It means "I have no idea what you're talking about. Stop immediately."
Common RST scenarios:
- Connect to a port where nothing is listening → server sends RST
- Send data to a server that rebooted → server sends RST (connection doesn't exist anymore)
- Severe error detected → one side sends RST to abort immediately
RST is harsh. It terminates the connection immediately. No graceful shutdown, no negotiation. Just "abort now."
Building It: Python Implementation
Let's implement TCP headers and the three-way handshake:
TCP Segment Builder
import struct
import random
class TCPSegment:
"""Build and parse TCP segments."""
# TCP Flags
FIN = 0x01
SYN = 0x02
RST = 0x04
PSH = 0x08
ACK = 0x10
URG = 0x20
def __init__(self, src_port, dest_port, seq, ack, flags, window, data=b''):
self.src_port = src_port
self.dest_port = dest_port
self.seq = seq
self.ack = ack
self.data_offset = 5 # Header length in 32-bit words (5 = 20 bytes)
self.flags = flags
self.window = window
self.checksum = 0
self.urgent_pointer = 0
self.data = data
def to_bytes(self, src_ip, dest_ip):
"""Convert TCP segment to bytes."""
# Build TCP header
data_offset_flags = (self.data_offset << 12) | self.flags
header = struct.pack('!HHIIHHH',
self.src_port,
self.dest_port,
self.seq,
self.ack,
data_offset_flags,
self.window,
0, # Checksum (placeholder)
)
header += struct.pack('!H', self.urgent_pointer)
# Calculate checksum (requires pseudo-header)
checksum = self.calculate_checksum(src_ip, dest_ip, header + self.data)
# Rebuild with correct checksum
header = struct.pack('!HHIIHHH',
self.src_port,
self.dest_port,
self.seq,
self.ack,
data_offset_flags,
self.window,
checksum,
)
header += struct.pack('!H', self.urgent_pointer)
return header + self.data
def calculate_checksum(self, src_ip, dest_ip, segment):
"""Calculate TCP checksum (includes pseudo-header)."""
import socket
# Build pseudo-header
src_ip_int = struct.unpack('!I', socket.inet_aton(src_ip))[0]
dest_ip_int = struct.unpack('!I', socket.inet_aton(dest_ip))[0]
pseudo_header = struct.pack('!IIBBH',
src_ip_int,
dest_ip_int,
0, # Reserved
6, # Protocol (TCP = 6)
len(segment)
)
# Combine pseudo-header and segment
data = pseudo_header + segment
# Calculate checksum
if len(data) % 2 != 0:
data += b'\x00'
checksum = 0
for i in range(0, len(data), 2):
word = (data[i] << 8) + data[i + 1]
checksum += word
while checksum >> 16:
checksum = (checksum & 0xFFFF) + (checksum >> 16)
return ~checksum & 0xFFFF
# Example: Build a TCP SYN segment
syn = TCPSegment(
src_port=54321, # Random client port
dest_port=80, # HTTP
seq=random.randint(0, 2**32 - 1), # Random initial sequence
ack=0, # No ACK yet
flags=TCPSegment.SYN,
window=65535, # Max window size
data=b'' # No data in SYN
)
segment_bytes = syn.to_bytes('10.0.0.1', '8.8.8.8')
print(f"TCP SYN segment: {len(segment_bytes)} bytes")Three-Way Handshake State Machine
import random
from enum import Enum
class TCPState(Enum):
"""TCP connection states."""
CLOSED = 0
LISTEN = 1
SYN_SENT = 2
SYN_RECEIVED = 3
ESTABLISHED = 4
FIN_WAIT_1 = 5
FIN_WAIT_2 = 6
CLOSE_WAIT = 7
CLOSING = 8
LAST_ACK = 9
TIME_WAIT = 10
class TCPConnection:
"""Simple TCP connection state machine."""
def __init__(self, is_client=True):
self.state = TCPState.CLOSED
self.is_client = is_client
self.seq = random.randint(0, 2**32 - 1) # Our sequence number
self.ack = 0 # Their sequence number
self.window = 65535
def send_syn(self):
"""Client: Send SYN to initiate connection."""
if self.state != TCPState.CLOSED:
raise Exception(f"Cannot send SYN from state {self.state}")
print(f"CLIENT: Sending SYN (seq={self.seq})")
self.state = TCPState.SYN_SENT
return TCPSegment(
src_port=54321,
dest_port=80,
seq=self.seq,
ack=0,
flags=TCPSegment.SYN,
window=self.window
)
def receive_syn(self, segment):
"""Server: Receive SYN, send SYN-ACK."""
if self.state != TCPState.LISTEN:
raise Exception(f"Cannot receive SYN in state {self.state}")
print(f"SERVER: Received SYN (seq={segment.seq})")
self.ack = segment.seq + 1 # ACK their sequence + 1
print(f"SERVER: Sending SYN-ACK (seq={self.seq}, ack={self.ack})")
self.state = TCPState.SYN_RECEIVED
return TCPSegment(
src_port=80,
dest_port=54321,
seq=self.seq,
ack=self.ack,
flags=TCPSegment.SYN | TCPSegment.ACK,
window=self.window
)
def receive_syn_ack(self, segment):
"""Client: Receive SYN-ACK, send ACK."""
if self.state != TCPState.SYN_SENT:
raise Exception(f"Cannot receive SYN-ACK in state {self.state}")
print(f"CLIENT: Received SYN-ACK (seq={segment.seq}, ack={segment.ack})")
self.ack = segment.seq + 1 # ACK their sequence + 1
self.seq = segment.ack # They ACKed our SYN
print(f"CLIENT: Sending ACK (seq={self.seq}, ack={self.ack})")
self.state = TCPState.ESTABLISHED
return TCPSegment(
src_port=54321,
dest_port=80,
seq=self.seq,
ack=self.ack,
flags=TCPSegment.ACK,
window=self.window
)
def receive_ack(self, segment):
"""Server: Receive final ACK, connection established."""
if self.state != TCPState.SYN_RECEIVED:
raise Exception(f"Cannot receive ACK in state {self.state}")
print(f"SERVER: Received ACK (seq={segment.seq}, ack={segment.ack})")
self.seq = segment.ack # They ACKed our SYN
self.state = TCPState.ESTABLISHED
print("SERVER: Connection ESTABLISHED!")
# Example: Simulate three-way handshake
print("=== Three-Way Handshake Simulation ===\n")
client = TCPConnection(is_client=True)
server = TCPConnection(is_client=False)
server.state = TCPState.LISTEN # Server is listening
# Step 1: Client sends SYN
syn_segment = client.send_syn()
# Step 2: Server receives SYN, sends SYN-ACK
syn_ack_segment = server.receive_syn(syn_segment)
# Step 3: Client receives SYN-ACK, sends ACK
ack_segment = client.receive_syn_ack(syn_ack_segment)
print(f"CLIENT: Connection ESTABLISHED!")
# Step 4: Server receives ACK
server.receive_ack(ack_segment)
print(f"\nFinal state:")
print(f" Client: {client.state}, seq={client.seq}, ack={client.ack}")
print(f" Server: {server.state}, seq={server.seq}, ack={server.ack}")Output:
=== Three-Way Handshake Simulation ===
CLIENT: Sending SYN (seq=1523147829)
SERVER: Received SYN (seq=1523147829)
SERVER: Sending SYN-ACK (seq=3841562901, ack=1523147830)
CLIENT: Received SYN-ACK (seq=3841562901, ack=1523147830)
CLIENT: Sending ACK (seq=1523147830, ack=3841562902)
CLIENT: Connection ESTABLISHED!
SERVER: Received ACK (seq=1523147830, ack=3841562902)
SERVER: Connection ESTABLISHED!
Final state:
Client: TCPState.ESTABLISHED, seq=1523147830, ack=3841562902
Server: TCPState.ESTABLISHED, seq=3841562902, ack=1523147830
This code actually implements the TCP state machine! Both sides start with random sequence numbers, exchange them during the handshake, and end up in ESTABLISHED state with synchronized sequence numbers. This is the foundation that makes reliable communication possible.
What We've Built
Let's review the entire stack so far:
Layer 1: Ethernet
- Local delivery using MAC addresses
- Frames, ARP, broadcast
Layer 2: IP
- Global routing using IP addresses
- Packets, TTL, checksums, ICMP
Layer 3: TCP
- Reliable, ordered delivery
- Sequence numbers, acknowledgments, retransmission
- Three-way handshake for connection setup
- Ports for multiplexing multiple connections
We can now establish connections and ensure both sides are ready to communicate. The foundation is solid.
But we haven't actually transferred any data yet. We've only connected.
The Next Challenge
We've solved:
- ✓ How to establish a connection
- ✓ How to number bytes (sequence numbers)
- ✓ How to confirm receipt (acknowledgments)
But we haven't solved:
- ✗ How to actually send data efficiently
- ✗ How to handle lost packets during data transfer
- ✗ How to avoid overwhelming the receiver
- ✗ How to close the connection cleanly
You have a connection. Both sides know each other's starting sequence numbers. Now you need to send a large file. How do you send it as fast as possible without losing data or overwhelming the receiver?
That's what we'll build next: the data flow mechanisms that make TCP fast and efficient.