Back to Blog

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

The Problem

You're receiving thousands of packets. Some are missing. Some are out of order. Some are duplicates. How do you:

  1. Know which packets you've received?
  2. Know which packets are missing?
  3. Reassemble them in the correct order?
  4. 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:

TCP Header Format 20 bytes of connection and flow control
TCP Flags (click to toggle):
Flags byte: 0x12 (SYN+ACK - connection accepted)
Python Implementation
import struct
from dataclasses import dataclass
from enum import IntFlag

class TcpFlags(IntFlag):
    FIN = 0x01
    SYN = 0x02
    RST = 0x04
    PSH = 0x08
    ACK = 0x10
    URG = 0x20
    ECE = 0x40
    CWR = 0x80

@dataclass
class TcpHeader:
    src_port: int
    dst_port: int
    seq_num: int
    ack_num: int
    data_offset: int = 5  # 20 bytes
    flags: int = 0
    window: int = 65535
    checksum: int = 0
    urgent_ptr: int = 0

    def pack(self) -> bytes:
        offset_flags = (self.data_offset << 12) | self.flags
        return struct.pack(
            '!HHIIHHHH',
            self.src_port,
            self.dst_port,
            self.seq_num,
            self.ack_num,
            offset_flags,
            self.window,
            self.checksum,
            self.urgent_ptr
        )

# Example: Create a SYN packet
syn_packet = TcpHeader(
    src_port=12345,
    dst_port=80,
    seq_num=1000,
    ack_num=0,
    flags=TcpFlags.SYN
)
Try It Out

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:

Key Insight

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.

The Problem

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.

TCP 3-Way Handshake How TCP establishes a reliable connection
💻
Client
CLOSED
🖥️
Server
LISTEN
Click "Start Handshake" to begin
Client ISN: 1000

Initial Sequence Number chosen by the client. The server acknowledges by sending ack=1001.

Server ISN: 5000

The server's own sequence number. The client acknowledges by sending ack=5001.

Try It Out

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
Key Insight

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
The Problem

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.

Continue to Part 4: TCP Data Flow →