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.
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).
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.
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.
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.
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:
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.
Decremented by each router. Packet discarded at 0. Prevents infinite loops. Used by traceroute.
IPv4
DF flag
"Don't Fragment" — tells routers to drop the packet rather than fragment it. Enables Path MTU Discovery.
TCP
Sequence Number
Byte offset of the first byte in this segment within the stream. Enables ordering and retransmission.
TCP
Window Size
How many bytes the receiver can currently buffer. Flow control mechanism.
TCP
SYN/ACK/FIN/RST
Connection 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
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
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)."
Every host on the segment receives the broadcast. Only Host B (which owns 10.0.1.50) responds. All others silently discard it.
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."
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 Type2B (0x0001 = Ethernet)
Proto Type2B (0x0800 = IPv4)
HW Size1B (6)
Proto Size1B (4)
Opcode2B (1=Req, 2=Reply)
Sender MAC Address6 bytes
Sender IP4 bytes
Target MAC Address6 bytes (00:00:00:00:00:00 in request)
Target IP4 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
Check dst IP, decrement TTL, verify checksum, strip IP header. Protocol 6 → pass to TCP.
TCP processing
Check dst port, manage sequence/ACK, strip TCP header. Port 8080 → pass to application.
Application receives
HTTP server reads "GET /api/health"
Demultiplexing — How Each Layer Finds the Right Handler
Layer
Field Used
Example
Determines
Ethernet
EtherType
0x0800
Pass to IPv4 stack
IP
Protocol
6
Pass to TCP handler
TCP
Destination Port
8080
Deliver 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:
Router receives frame on ingress interface. Ethernet header says dst MAC = router's MAC. It strips the Ethernet header and reads the IP packet.
Router performs a routing table lookup on the destination IP to determine the next hop and egress interface.
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.
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.
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
Hop
Src MAC
Dst MAC
Src IP
Dst IP
Host A → Router
AA:AA
Router eth0 MAC
10.0.1.10
10.0.2.50
Router → Host B
Router eth1 MAC
BB:BB
10.0.1.10
10.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
# 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