Skip to content
Menu

Networking11 min read

Encapsulation

How data gets wrapped at each layer for transmission

What Is Encapsulation?

Encapsulation is the process by which each layer of the networking stack wraps the data from the layer above with its own header (and sometimes a trailer). Each layer treats the entire output of the layer above as an opaque payload — it doesn't inspect or modify it, just wraps it.

The Fundamental Principle

  • Sending side: Data flows down the stack. Each layer adds its header → data grows.
  • Receiving side: Data flows up the stack. Each layer strips its header → data shrinks.
  • Each layer only reads its own header — it treats everything inside as an opaque payload.
  • This is what enables layered independence: TCP doesn't care whether it's running over IPv4 or IPv6. IP doesn't care whether it's running over Ethernet or Wi-Fi.

Note

Analogy: Think of it like nested envelopes. You write a letter (Application data), put it in an envelope with a tracking number (TCP header), put that in a mailer with a ZIP code (IP header), and put that in a courier package with a local delivery address (Ethernet header). Each handler only reads the outermost envelope they're responsible for.

Encapsulation — Layer by Layer

Here's what happens when an HTTP GET request is encapsulated for transmission:

Application Data    [HTTP GET /index.html HTTP/1.1 ...]
TCP Header (20B)   |   Application Data
IP Header (20B)   |   TCP Header   |   Application Data
Eth Header (14B)   |   IP Header   |   TCP Header   |   Application Data   |   FCS (4B)
Preamble (8B)   |   Ethernet Frame   |   Inter-frame Gap (12B) → Bits on the wire

Tip

Key observation: Notice how each layer wraps the entire output of the layer above. The IP layer doesn't know or care that its payload contains a TCP header followed by HTTP data — to IP, it's all just "payload bytes" that need to be delivered to the destination IP address.

Full Packet Walkthrough — HTTP Request from App to Wire

Let's trace exactly what happens when your browser sends GET /api/health HTTP/1.1 to 10.0.1.50:8080.

  1. Application Layer — HTTP
    The browser constructs an HTTP request:
    bash
    GET /api/health HTTP/1.1
    Host: 10.0.1.50:8080
    Accept: application/json
    Connection: keep-alive
    This is just a text payload — approximately 120 bytes of ASCII. The application hands it to the OS via the socket API: send(sockfd, data, len, 0).
  2. Transport Layer — TCP
    The kernel's TCP stack prepends a 20-byte TCP header containing:
    • Source port: 49152 (ephemeral port assigned by the OS)
    • Destination port: 8080
    • Sequence number: tracks byte position in the stream
    • Acknowledgment number: confirms received data
    • Flags: PSH, ACK (push data to application, acknowledge previous segment)
    • Window size: how much more data the sender can accept
    • Checksum: covers the header + data + a pseudo-header from IP
    The result is a TCP segment: [TCP Header (20B) | HTTP Data (~120B)] = ~140 bytes.
  3. Internet Layer — IPv4
    The kernel's IP stack prepends a 20-byte IPv4 header containing:
    • Source IP: 10.0.1.10 (our host)
    • Destination IP: 10.0.1.50 (the server)
    • TTL: 64 (decremented by each router; packet dies at 0)
    • Protocol: 6 (indicates the payload is TCP)
    • Total Length: 160 bytes (20B IP + 20B TCP + ~120B data)
    • Header checksum: covers only the IP header itself
    The result is an IP packet: [IP Header (20B) | TCP Segment (140B)] = ~160 bytes.
  4. Link Layer — Ethernet
    The NIC driver prepends a 14-byte Ethernet header and appends a 4-byte FCS trailer:
    • Destination MAC: aa:bb:cc:dd:ee:ff (next hop — gateway or destination NIC)
    • Source MAC: 11:22:33:44:55:66 (our NIC)
    • EtherType: 0x0800 (indicates the payload is IPv4)
    • FCS: CRC-32 checksum over the entire frame for error detection
    The result is an Ethernet frame: [Eth Header (14B) | IP Packet (160B) | FCS (4B)] = 178 bytes.
  5. Physical Layer
    The NIC prepends an 8-byte preamble (7 bytes of 10101010 + 1 byte SFD 10101011) for clock synchronization, transmits the frame as electrical signals, and appends a 12-byte inter-frame gap (silence between frames).

    Total on the wire: 8 + 178 + 12 = 198 bytes to send ~120 bytes of HTTP text.

Warning

Overhead reality check: For our ~120-byte HTTP payload, we added 78 bytes of overhead (TCP: 20, IP: 20, Ethernet: 14+4, Preamble+IFG: 20). That's roughly 39% overhead. For small requests, protocol overhead is significant. For large transfers (1460-byte TCP payload in a 1518-byte frame), overhead drops to about 4%.

Ethernet Frame Anatomy

Here is the complete structure of an Ethernet II frame carrying an IPv4/TCP payload:

Ethernet Header (14 bytes)

Destination MAC 6 bytes
Source MAC 6 bytes
EtherType 2 bytes

IPv4 Header (20 bytes, no options)

Ver 4 bits
IHL 4 bits
DSCP/ECN 8 bits
Total Length 16 bits
Identification 16 bits
Flags 3 bits
Fragment Offset 13 bits
TTL 8 bits
Protocol 8 bits
Header Checksum 16 bits
Source IP Address 32 bits
Destination IP Address 32 bits

TCP Header (20 bytes, no options)

Source Port 16 bits
Destination Port 16 bits
Sequence Number 32 bits
Acknowledgment Number 32 bits
Data Offset 4 bits
Reserved 3 bits
Flags (NS, CWR, ECE, URG, ACK, PSH, RST, SYN, FIN) 9 bits
Window Size 16 bits
Checksum 16 bits
Urgent Pointer 16 bits

Payload + Trailer

Application Data (HTTP Request) 46 - 1460 bytes (must pad to 46 min)
FCS (CRC-32) 4 bytes

Note

MTU and MSS: The standard Ethernet MTU (Maximum Transmission Unit) is 1500 bytes — this is the maximum IP packet size. With 20 bytes for IP and 20 bytes for TCP headers, the MSS (Maximum Segment Size) is 1460 bytes of application data per frame. Jumbo frames (MTU 9000) increase this but require end-to-end support.

Critical Header Fields Explained

LayerFieldPurpose
EthernetEtherTypeTells the receiver what's inside: 0x0800 = IPv4, 0x86DD = IPv6, 0x0806 = ARP
EthernetFCSCRC-32 error detection — corrupted frames are silently dropped (no retransmission at L2)
IPv4ProtocolTells the receiver what's inside: 6 = TCP, 17 = UDP, 1 = ICMP
IPv4TTLDecremented by each router. Packet discarded at 0. Prevents infinite loops. Used by traceroute.
IPv4DF flag"Don't Fragment" — tells routers to drop the packet rather than fragment it. Enables Path MTU Discovery.
TCPSequence NumberByte offset of the first byte in this segment within the stream. Enables ordering and retransmission.
TCPWindow SizeHow many bytes the receiver can currently buffer. Flow control mechanism.
TCPSYN/ACK/FIN/RSTConnection lifecycle flags: establish, acknowledge, close gracefully, reset forcefully.

ARP — The Bridge Between L2 and L3

The Problem ARP Solves

When a host wants to send a packet to 10.0.1.50 on the same subnet, it knows the destination IP address (Layer 3) but not the destination MAC address (Layer 2). It needs the MAC address to construct the Ethernet frame header. ARP (Address Resolution Protocol) resolves this mapping.

Warning

Scope: ARP only works within a single broadcast domain (same L2 segment / same subnet). It uses Ethernet broadcasts, which don't cross routers. For destinations on other subnets, the host sends to its default gateway's MAC, not the destination host's MAC.

ARP Request / Reply Flow

Host A (10.0.1.10, MAC AA:AA:AA:AA:AA:AA) wants to send data to Host B (10.0.1.50):

Host A
10.0.1.10
Network (Broadcast)
Host B
10.0.1.50
ARP Request: Who has 10.0.1.50? Tell 10.0.1.10
Broadcast to FF:FF:FF:FF:FF:FF — all hosts on the segment receive this
ARP Reply: 10.0.1.50 is at BB:BB:BB:BB:BB:BB
Unicast reply directly to AA:AA:AA:AA:AA:AA
  1. Host A checks its ARP cache: Does it already have a MAC for 10.0.1.50?
    console
    ip neigh show 10.0.1.50
    # If empty or STALE, ARP request is needed
  2. Host A sends an ARP Request as an Ethernet broadcast (dst MAC: FF:FF:FF:FF:FF:FF). The ARP payload says: "Who has IP 10.0.1.50? Please tell 10.0.1.10 (MAC AA:AA:AA:AA:AA:AA)."
  3. Every host on the segment receives the broadcast. Only Host B (which owns 10.0.1.50) responds. All others silently discard it.
  4. Host B sends an ARP Reply as a unicast back to Host A's MAC. The reply says: "10.0.1.50 is at MAC BB:BB:BB:BB:BB:BB."
  5. Host A caches the mapping (10.0.1.50 → BB:BB:BB:BB:BB:BB) and can now construct the Ethernet frame.
    bash
    ip neigh show 10.0.1.50
    10.0.1.50 dev eth0 lladdr bb:bb:bb:bb:bb:bb REACHABLE

ARP Packet Structure

HW Type 2B (0x0001 = Ethernet)
Proto Type 2B (0x0800 = IPv4)
HW Size 1B (6)
Proto Size 1B (4)
Opcode 2B (1=Req, 2=Reply)
Sender MAC Address 6 bytes
Sender IP 4 bytes
Target MAC Address 6 bytes (00:00:00:00:00:00 in request)
Target IP 4 bytes

Note

Note: ARP's EtherType is 0x0806. ARP packets are not IP packets — they sit directly inside Ethernet frames. This is why ARP is often described as "between L2 and L3."

ARP Security Concerns

Warning

ARP Spoofing / Poisoning: ARP has no authentication. Any host on the segment can send a gratuitous ARP reply claiming any IP maps to its own MAC. This enables man-in-the-middle attacks. Mitigations include:
  • Dynamic ARP Inspection (DAI): Switch validates ARP packets against a DHCP snooping database
  • Static ARP entries: Manually pin critical mappings (doesn't scale)
  • 802.1X: Port-based authentication prevents untrusted hosts from joining the network
  • IPv6 NDP: Uses SEND (SEcure Neighbor Discovery) with cryptographic signatures — but rarely deployed

De-encapsulation on the Receiving Side

When Host B receives the Ethernet frame, the process reverses — headers are stripped as the data moves up the stack:

  1. NIC receives bits

    Physical signals → frame

  2. Ethernet processing

    Check dst MAC, verify FCS, strip Ethernet header. EtherType 0x0800 → pass to IP.

  3. IP processing

    Check dst IP, decrement TTL, verify checksum, strip IP header. Protocol 6 → pass to TCP.

  4. TCP processing

    Check dst port, manage sequence/ACK, strip TCP header. Port 8080 → pass to application.

  5. Application receives

    HTTP server reads "GET /api/health"

Demultiplexing — How Each Layer Finds the Right Handler

LayerField UsedExampleDetermines
EthernetEtherType0x0800Pass to IPv4 stack
IPProtocol6Pass to TCP handler
TCPDestination Port8080Deliver to the application listening on port 8080

This chain of protocol identifiers is how a single NIC can serve traffic for hundreds of applications simultaneously — the stack uses these fields to demultiplex incoming frames to the correct process.

What Happens at a Router (L3 Hop)

When a packet crosses a router to reach a different subnet, the Ethernet header is stripped and replaced while the IP packet remains intact:

  1. Router receives frame on ingress interface. Ethernet header says dst MAC = router's MAC. It strips the Ethernet header and reads the IP packet.
  2. Router performs a routing table lookup on the destination IP to determine the next hop and egress interface.
  3. Router decrements TTL by 1 and recalculates the IP header checksum. If TTL reaches 0, the packet is dropped and an ICMP Time Exceeded message is sent back.
  4. Router constructs a new Ethernet frame on the egress interface with the next hop's MAC address as the destination MAC (found via ARP on that segment) and the router's egress interface MAC as the source.
  5. The IP packet inside is unchanged (except TTL and checksum). Source IP and destination IP remain the original sender and final receiver. Only the L2 addressing changes at each hop.
Host A
10.0.1.10
MAC: AA:AA
Switch
L2 forwarding
Router
eth0: 10.0.1.1
eth1: 10.0.2.1
Switch
L2 forwarding
Host B
10.0.2.50
MAC: BB:BB

Address Changes Across Hops

HopSrc MACDst MACSrc IPDst IP
Host A → RouterAA:AARouter eth0 MAC10.0.1.1010.0.2.50
Router → Host BRouter eth1 MACBB:BB10.0.1.1010.0.2.50

Key insight: MAC addresses change at every L3 hop. IP addresses stay constant end-to-end (unless NAT is involved). This is the fundamental difference between L2 and L3 addressing.

Tip

DevOps context: This hop-by-hop re-encapsulation is exactly what happens in Kubernetes networking. When a pod on Node A sends a packet to a pod on Node B, the CNI plugin (e.g., Calico, Cilium) routes the packet through the node's network stack, potentially through a VXLAN tunnel (which adds another layer of encapsulation), before it reaches the destination pod. Understanding encapsulation is essential for debugging kubectl exec connectivity issues, pod-to-service routing, and MTU problems with overlay networks.

Seeing Encapsulation with tcpdump

You can observe encapsulation directly with packet capture tools:

console
# Capture packets on eth0, show link-layer headers (-e), verbose (-v)
tcpdump -i eth0 -e -v -n host 10.0.1.50 and port 8080

# Output shows all layers:
12:34:56.789 AA:AA:AA:AA:AA:AA > BB:BB:BB:BB:BB:BB, ethertype IPv4 (0x0800), length 178:
    10.0.1.10.49152 > 10.0.1.50.8080: Flags [P.], seq 1:121, ack 1, win 502,
    length 120: HTTP: GET /api/health HTTP/1.1

In one line, tcpdump shows you:

LayerWhat tcpdump Shows
L2 (Ethernet)AA:AA:AA:AA:AA:AA > BB:BB:BB:BB:BB:BB, ethertype IPv4 (0x0800)
L3 (IP)10.0.1.10 > 10.0.1.50, length 178
L4 (TCP).49152 > .8080, Flags [P.], seq 1:121, ack 1, win 502
L7 (HTTP)HTTP: GET /api/health HTTP/1.1
console
# Even more detail with -xx (hex dump of entire frame)
tcpdump -i eth0 -xx -n host 10.0.1.50 and port 8080

# Or use Wireshark for a GUI with per-layer dissection:
tshark -i eth0 -V host 10.0.1.50 and port 8080

Tip

Pro tip: In Kubernetes, capture packets inside a pod's network namespace:
console
# Find the pod's PID
PID=$(crictl inspect --output go-template --template '{{.info.pid}}' $(crictl ps --name mycontainer -q))
# Enter its network namespace and capture
nsenter -t $PID -n tcpdump -i eth0 -e -n -v
Solidnines — solidnines.com