Build a TCP/IP Stack from Scratch · Module 01

Hands-on Test: Observe Packets

Hands-on test: Observe packets

Let's see the traffic you just generated. We'll capture packets inside the containers and identify the layers by eye.

5.1 Watch ARP and ICMP on the wire

Start a capture on the stack container:

 lab git:(main) docker compose exec stack bash -lc 'tcpdump -ni eth0 -vvv -l arp or icmp'
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

In another terminal, send a ping from the client:

docker compose exec client ping -c 3 10.10.0.4

Back in the capture window, you should see something like:

tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
16:28:58.137481 IP (tos 0x0, ttl 64, id 3157, offset 0, flags [DF], proto ICMP (1), length 84)
 10.10.0.3 > 10.10.0.4: ICMP echo request, id 3, seq 1, length 64
16:28:58.137511 IP (tos 0x0, ttl 64, id 45375, offset 0, flags [none], proto ICMP (1), length 84)
 10.10.0.4 > 10.10.0.3: ICMP echo reply, id 3, seq 1, length 64
16:29:03.193121 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 10.10.0.4 tell 10.10.0.3, length 28
16:29:03.193153 ARP, Ethernet (len 6), IPv4 (len 4), Reply 10.10.0.4 is-at 02:42:0a:0a:00:04, length 28

Understanding the Output

That output is exactly what we want to see — it's the first glimpse of your "network" coming alive. Let's unpack it a bit.

ICMP Messages

The first two lines are ICMP (Internet Control Message Protocol) packets — the famous ping.

  • The 10.10.0.3 > 10.10.0.4: ICMP echo request means the client sent a "Hello, are you there?" message.
  • The immediate reply 10.10.0.4 > 10.10.0.3: ICMP echo reply is the stack container saying "Yep, I'm here."

These are standard Layer 3 (network layer) messages carried inside IP packets.

ARP Messages

Then you see two ARP (Address Resolution Protocol) frames. ARP lives one layer lower, at Layer 2.

Before the client can send that ICMP request, it needs the MAC address of 10.10.0.4 to build the Ethernet frame.

So it broadcasts a request:

"Who has 10.10.0.4? Tell 10.10.0.3."

The stack container answers:

"I do! My MAC is 02:42:0a:0a:00:04."

Once that mapping is cached, the client can keep sending packets directly to that MAC address without asking again.

Layer Summary

So, in those four lines, you just watched all the layers in motion:

  1. ARP — translating IP to MAC.
  2. Ethernet — carrying the frames.
  3. IP — wrapping the ICMP message.
  4. ICMP — giving you the visible "ping" response.

Congratulations — that's a full network exchange, end to end, happening entirely in your virtual lab.

5.2 Peek at the Ethernet frame

Capture one packet in hex on the stack side:

docker compose exec stack bash -lc \
'tcpdump -XX -s 0 -c 1 -ni eth0 icmp'

You'll see a hex dump like:

16:33:56.909265 IP 10.10.0.3 > 10.10.0.4: ICMP echo request, id 4, seq 1, length 64
 0x0000: 0242 0a0a 0004 0242 0a0a 0003 0800 4500 .B.....B......E.
 0x0010: 0054 ef70 4000 4001 371e 0a0a 0003 0a0a .T.p@.@.7.......
 0x0020: 0004 0800 c1fb 0004 0001 f4e3 e768 0000 .............h..
 0x0030: 0000 8ddf 0d00 0000 0000 1011 1213 1415 ................
 0x0040: 1617 1819 1a1b 1c1d 1e1f 2021 2223 2425 ...........!"#$%
 0x0050: 2627 2829 2a2b 2c2d 2e2f 3031 3233 3435 &'()*+,-./012345
 0x0060: 3637 67
1 packet captured

Beautiful — that's a single ICMP Echo Request captured in raw bytes.

Try to spot:

  • EtherType 0x0800 for IPv4 (in the Ethernet header).
  • IPv4 first byte 0x45 (version 4, header length 5×4=20 bytes).
  • ICMP type (8 request or 0 reply).

Layer-by-Layer Analysis

Let's walk through it layer by layer, so you can start to see the structure behind all those hex digits.

Layer 2 — Ethernet

The first 14 bytes form the Ethernet header:

0x0000: 0242 0a0a 0004 0242 0a0a 0003 0800

Break it down:

  • Destination MAC: 02:42:0a:0a:00:04 → the stack container
  • Source MAC: 02:42:0a:0a:00:03 → the client container
  • EtherType: 0800 → IPv4 payload follows

That 0800 is a little flag saying, "the next protocol inside me is IP."

Layer 3 — IPv4

Next comes the IP header:

4500 0054 ef70 4000 4001 371e 0a0a 0003 0a0a 0004

Let's read the first few fields:

  • 45 → Version 4 (4), Header Length 5×4 = 20 bytes (5)
  • 00 → Type of Service (not used here)
  • 0054 → Total length = 84 bytes
  • ef70 → Identification (used for fragmentation)
  • 4000 → Flags + Fragment offset (no fragmentation)
  • 40 → TTL = 64 hops
  • 01 → Protocol = ICMP
  • 371e → Header checksum
  • 0a0a0003 → Source IP = 10.10.0.3
  • 0a0a0004 → Destination IP = 10.10.0.4

Already, this tells us it's an ICMP packet from the client to the stack, total size 84 bytes.

Layer 4 — ICMP

The ICMP message starts right after the 20-byte IP header:

0800 c1fb 0004 0001 ...
  • 08 → Type 8 (Echo Request)
  • 00 → Code 0 (standard ping)
  • c1fb → ICMP checksum
  • 0004 → Identifier
  • 0001 → Sequence number

That's the "hello" message your ping sent.

The stack's kernel will reply with the same ID and sequence number, but with Type 0 (Echo Reply) instead of 8.

Payload

Everything after that (f4e3 e768 … 3637) is just filler data — 56 bytes by default.

It doesn't carry meaning for ICMP; it's there so you can measure round-trip time with real packet sizes.

The Big Picture

So in this one line of tcpdump, you're literally seeing three layers stacked together:

Ethernet header → IP header → ICMP payload

That's the same nesting you'll recreate in code later — constructing and parsing these exact bytes by hand.

Exercise

Draw the packet as a raw byte array and then separate each section and try to understand the concept of encapsulation.

If you take a closer look you'll see that the IP header contains a total length value set to 84 bytes. This includes the IP header (20 bytes) and the ICMP header (8 bytes) plus the payload of 56 bytes, resulting in an IP packet of 84 bytes that is encapsulated in an Ethernet frame of 98 bytes total (84 plus 14 Ethernet Header).

Encapsulation is a big concept in the layer stack representation of networking.

5.3 Quick sanity checks

  • If ARP never appears: are both containers on 10.10.0.0/24?
  • If ping times out: check docker compose ps and try ip -brief addr in both containers.
  • If tcpdump shows nothing: make sure you're capturing on the right interface (eth0 inside containers).

You've just confirmed three things: containers can talk, ARP resolves neighbors, and ICMP packets are flowing. This is the exact viewpoint you'll keep as you start replacing the kernel's behavior with your own stack.