LearnThe Network StackHow Packets Find Their Way

ICMP and Ping

ping - a hands-on Linux lab on a real virtual machine.

ICMP is how IP reports its own failures. Send echo requests, catch them on the wire, dissect every field, break a ping on purpose, and map a path with traceroute.

Three answers from one command

Run ping at a healthy machine and you get a tidy reply, once a second, for as long as you care to watch.

Run it at an address nobody owns and something stranger happens: an error arrives, and the address in front of it is your own machine. Something wrote you a failure notice, and it was not the target.

Run it at a machine behind a strict firewall and you get the strangest answer of all: nothing. No reply, no error, not one line of output.

One command, three completely different answers. By the end of this lesson you will know exactly who writes each one, what protocol carries it, and how the same machinery maps every router between you and anywhere on earth. You will catch all of it on the wire, byte by byte, and you will break a ping on purpose to watch the failure path work.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The screens show what this lab's own two-machine network prints. The lab user is student on the host icmp-lab. In the real lab at the end, your progress is tracked automatically, so just type commands naturally.

The protocol that carries the bad news

ICMP is the Internet Control Message Protocol. It is the channel IP uses to talk about itself: error reports, reachability checks, routing feedback. When a packet cannot be delivered, ICMP is how anyone finds out.

ICMP is not TCP and it is not UDP. It rides directly on IP as a protocol of its own. The IP header carries a one-byte Protocol field naming its cargo: 6 means TCP, 17 means UDP, 1 means ICMP. Dissect any ICMP packet and the IP header above it reads:

Internet Protocol Version 4
    Protocol: ICMP (1)

That placement has a consequence most people first meet as a confused question: you cannot ping a port. Ports belong to TCP and UDP, one layer up. ICMP has no ports at all, which is why ping can prove a machine is reachable while saying nothing about whether anything on it is listening.

Every ICMP message is labeled by a type (the category) and a code (the detail). A handful of them do almost all of the work:

TypeCodeNameMeaning
80Echo RequestThe question ping asks
00Echo ReplyThe answer it hopes for
30Destination Unreachable: NetworkNo route to that network
31Destination Unreachable: HostThe network exists, the machine does not answer
33Destination Unreachable: PortThe machine answered: nothing listens there
34Fragmentation NeededToo big for a link, and marked do not fragment
110Time ExceededThe packet ran out of TTL mid-journey

Think of IP as the postal service. TCP and UDP are kinds of mail. ICMP is the postal service writing to itself: this address does not exist, this parcel is too heavy, this route is closed.

Written in an evening, named after sonar

The ICMP specification is RFC 792, published by Jon Postel in 1981, four years before the domain name system existed. The ping program came two years later: Mike Muuss wrote it in a single evening in December 1983, while chasing a flaky network at the Army's Ballistic Research Laboratory, and named it after the sound a sonar pulse makes.

traceroute arrived in 1987, when Van Jacobson wanted to see where packets were dying inside a misbehaving network and realized an old field in the IP header could be abused into drawing a map. You will pull the same trick at the end of this lesson.

Muuss called ping "a little thousand-line hack". Forty years later it is still the first command a network engineer types when anything breaks.

Send the first ping

Your lab VM contains the two-machine network you have used since the Watching Traffic lesson: two network namespaces, isolated network stacks that behave like separate machines, joined by a virtual cable.

  [client]  10.0.1.10  <-------->  10.0.1.20  [server]
  MAC: 02:00:0a:00:01:0a           MAC: 02:00:0a:00:01:14

The helper send-ping runs ping from inside the client namespace, aimed at the server, three requests by default (send-ping 5 would send five). Under the hood it runs sudo ip netns exec client ping -c 3 10.0.1.20, the same netns pattern as every command in this track.

Tab completion knows the helpers. Type send- and press Tab: the shell completes any command on the PATH, helpers included. Build the habit now, on the practice terminal and on the VM. It kills most typos before they exist.

Send the first burst:

send-ping

prompt: student@icmp-lab:~$ answer: send-ping ||| send-ping 3 ||| sudo ip netns exec client ping -c 3 10.0.1.20 output: PING 10.0.1.20 (10.0.1.20) 56(84) bytes of data. 64 bytes from 10.0.1.20: icmp_seq=1 ttl=64 time=0.045 ms 64 bytes from 10.0.1.20: icmp_seq=2 ttl=64 time=0.062 ms 64 bytes from 10.0.1.20: icmp_seq=3 ttl=64 time=0.058 ms

--- 10.0.1.20 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2003ms rtt min/avg/max/mdev = 0.045/0.055/0.062/0.007 ms hint: The helper does it all: send-ping

Read the first line closely, because its numbers are an anatomy lesson. 56(84) bytes of data means 56 bytes of payload, which becomes 84 on the wire once the 8-byte ICMP header and the 20-byte IP header are added. The replies then say 64 bytes: the ICMP message alone, 56 payload plus 8 header, measured before the IP wrapper.

icmp_seq counts up from 1, one number per request. A gap in that column (1, 2, 4) is a lost packet, spotted without any math.

ttl=64 is the reply's remaining Time To Live, a hop counter every router decrements by one. Linux starts packets at 64, so a reply still holding 64 crossed zero routers. It is a fingerprint too: Windows starts at 128 and many network appliances at 255, so a stranger's starting TTL leaks what it runs. Subtract the TTL you received from the sender's starting value and you have counted the routers in between.

time=0.045 ms is the round-trip time, request out to reply back. Where does ping store the departure time to compute it? Inside the packet: the payload opens with a timestamp, which the far end echoes back untouched. Ping keeps no table of what it sent. The wire carries the state.

Commit: how much traffic was that?

You watched three replies come back. Before you put the same conversation under a packet capture, commit to what the capture will contain.

Catch the pairs on the wire

Time to see those packets. The cap helper runs tshark inside the client namespace, and both flags you need came up in the Watching Traffic lesson: -c N stops after N packets, and -Y icmp is a display filter that shows only ICMP and hides everything else (ARP, TCP, all of it).

Run the ping in the background with &, and capture four packets:

send-ping & cap -c 4 -Y icmp

prompt: student@icmp-lab:~$ answer: send-ping & cap -c 4 -Y icmp ||| send-ping 5 & cap -c 4 -Y icmp ||| send-ping & cap -Y icmp -c 4 output: 1 0.000000 10.0.1.10 -> 10.0.1.20 ICMP 98 Echo (ping) request id=0x0001, seq=1/256, ttl=64 2 0.000038 10.0.1.20 -> 10.0.1.10 ICMP 98 Echo (ping) reply id=0x0001, seq=1/256, ttl=64 3 1.001234 10.0.1.10 -> 10.0.1.20 ICMP 98 Echo (ping) request id=0x0001, seq=2/512, ttl=64 4 1.001267 10.0.1.20 -> 10.0.1.10 ICMP 98 Echo (ping) reply id=0x0001, seq=2/512, ttl=64 hint: Background the ping, then filter the capture: send-ping & cap -c 4 -Y icmp

Perfect pairs. Request out, reply back tens of microseconds later, then a one-second silence before the next pair, because ping paces itself at one request per second. Two things on this screen belong to this run alone: the timings, and the id. Your VM will print different numbers there and identical structure everywhere else.

98 is the frame length, and you can now build it from parts: 56 payload + 8 ICMP + 20 IP + 14 Ethernet = 98 bytes.

id=0x0001 is the session number. Every packet of this run carries the same id, and a second ping running beside it would carry a different one. That is how each ping process finds its own replies in a shared stream. Where that id comes from depends on the socket ping was given. With an ICMP datagram socket, the kernel assigns the id itself, exactly the way it assigns UDP port numbers, and steers incoming replies by id. That socket type is gated by net.ipv4.ping_group_range, which is per network namespace and starts closed inside a fresh one, so the pings in this lab run as root on a raw socket and ping picks its own id.

seq=1/256 is one field printed twice. The two sequence bytes read in network byte order are 0x0001, the 1. The same two bytes read backwards are 0x0100, which is 256. tshark prints both because some old IP stacks wrote the field byte-swapped, and the next pair confirms the pattern: 2 backwards is 512.

Open one packet all the way up

Summary lines show who talked. The -V flag makes tshark print the full dissection instead: every layer, every field, every byte accounted for. The real screen also dissects the Frame, Ethernet and IP layers; the practice terminal shows the two ICMP sections, which is where this lesson lives.

Two packets are enough, one request and one reply:

send-ping & cap -c 2 -V -Y icmp

prompt: student@icmp-lab:~$ answer: send-ping & cap -c 2 -V -Y icmp ||| send-ping & cap -c 2 -Y icmp -V ||| send-ping 5 & cap -c 2 -V -Y icmp output: Internet Control Message Protocol Type: 8 (Echo (ping) request) Code: 0 Checksum: 0x8d4e [correct] [Checksum Status: Good] Identifier (BE): 1 (0x0001) Identifier (LE): 256 (0x0100) Sequence Number (BE): 1 (0x0001) Sequence Number (LE): 256 (0x0100) [Response frame: 2] Timestamp from icmp data: Aug 5, 2026 09:14:03.221130000 UTC [Timestamp from icmp data (relative): 0.000012000 seconds] Data (48 bytes)

Internet Control Message Protocol Type: 0 (Echo (ping) reply) Code: 0 Checksum: 0x954e [correct] [Checksum Status: Good] Identifier (BE): 1 (0x0001) Identifier (LE): 256 (0x0100) Sequence Number (BE): 1 (0x0001) Sequence Number (LE): 256 (0x0100) [Request frame: 1] [Response time: 0.038 ms] Timestamp from icmp data: Aug 5, 2026 09:14:03.221130000 UTC [Timestamp from icmp data (relative): 0.000050000 seconds] Data (48 bytes) hint: Add the verbose flag to the same capture: send-ping & cap -c 2 -V -Y icmp

Put the two sections side by side and hunt for the difference. The Identifier came back untouched. The Sequence Number came back untouched. The timestamp and the 48 data bytes were echoed exactly. One field genuinely changed: Type 8 became Type 0.

The checksum proves it with arithmetic. It moved from 0x8d4e to 0x954e, a shift of exactly 0x0800, which is precisely the type byte's 8 leaving the sum. Your two checksums will be different numbers, because they cover your id and your timestamp, and the gap between them will still be 0x0800. When a checksum moves by exactly the field you changed, you have proof that nothing else in the packet was touched.

Now look at which lines wear square brackets: [Checksum Status: Good], [Response frame: 2], [Response time: 0.038 ms]. Those bytes exist on no wire anywhere. Brackets are Wireshark marking its own conclusions, computed afterwards by matching id and seq across packets. Plain field: read from the packet. Bracketed field: inferred by the tool. That one convention keeps captures honest, and it is why the request already knows its reply lives in frame 2.

One more thing this screen cannot show: nobody on the server ran a program to answer you. Echo Reply is generated inside the kernel's ICMP code, before any process is consulted. A machine can have every service crashed and still answer ping perfectly. Ping proves the network stack is alive, and nothing above it.

Milestone: you can read the feedback channel raw

Stop and take stock. You have sent echo requests, watched the pairs travel, and opened one to the byte level. You can name the protocol number, the type, the code, the id, the seq and the checksum, and you know which dissection lines were never on the wire at all.

Most people use ping for a decade without once seeing what it sends. You just read the entire message. Two skills remain: shaping the probe on purpose, and reading what comes back when things go wrong. Wrong is where ICMP earns its keep.

Shape the probe: count and size

send-ping wraps two flags you should now own directly. -c N sends exactly N requests and stops. -s N sets the payload to N bytes instead of the default 56. Both ride on a raw ping, which in this lab means the full netns form.

Why resize a ping? Links have a Maximum Transmission Unit, an MTU: the largest frame they will carry, 1500 bytes on standard Ethernet. A small ping can pass where a big one dies, and comparing the two is the fastest MTU sanity check there is.

Send five pings carrying 1000 bytes each:

sudo ip netns exec client ping -c 5 -s 1000 10.0.1.20

prompt: student@icmp-lab:~$ answer: sudo ip netns exec client ping -c 5 -s 1000 10.0.1.20 ||| sudo ip netns exec client ping -s 1000 -c 5 10.0.1.20 output: PING 10.0.1.20 (10.0.1.20) 1000(1028) bytes of data. 1008 bytes from 10.0.1.20: icmp_seq=1 ttl=64 time=0.089 ms 1008 bytes from 10.0.1.20: icmp_seq=2 ttl=64 time=0.074 ms 1008 bytes from 10.0.1.20: icmp_seq=3 ttl=64 time=0.081 ms 1008 bytes from 10.0.1.20: icmp_seq=4 ttl=64 time=0.069 ms 1008 bytes from 10.0.1.20: icmp_seq=5 ttl=64 time=0.077 ms

--- 10.0.1.20 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4092ms rtt min/avg/max/mdev = 0.069/0.078/0.089/0.007 ms hint: Count flag, size flag, then the target: sudo ip netns exec client ping -c 5 -s 1000 10.0.1.20

Run the arithmetic once more, because it now proves you understand the layering. The header line says 1000(1028): your 1000 bytes, plus 8 of ICMP, plus 20 of IP. The reply lines say 1008: payload plus ICMP header, before the IP wrapper. Three different numbers, one packet, each measured at a different layer.

RTT crept from 0.05 ms to 0.08 ms. Eighteen times the payload, not even double the time: on a local link, per-packet overhead dominates and size barely matters. Across a slow WAN the same comparison splays wide apart, which makes size-stepped pings a poor man's bandwidth probe.

Two size tricks worth keeping. First, the timestamp needs the first 16 bytes of payload, so -s 8 leaves it no room and the time column silently vanishes from ping's output. Now you know why. Second, add -M do to forbid fragmentation and step -s upward: the exact size where pings start dying is your path MTU, and the refusal arrives as ICMP type 3 code 4, the Fragmentation Needed row from the table. Path MTU Discovery is built on that message.

Commit: what does loss look like?

So far every ping succeeded. Before you break one on purpose, commit to what failure prints. Picture a far-off machine whose firewall silently drops every echo request: your packets leave, and nothing ever comes back.

Errors you might see

Four failures cover nearly everything ping will ever tell you. Learn the literal text, what it means, and what to check first.

1. An error with an author

From 10.0.1.10 icmp_seq=1 Destination Host Unreachable

Something actively reported failure, and From names it. When the address after From is a router, that router had no path onward and mailed you a type 3 ICMP error. When it is your own address, the report never crossed the wire at all: your own kernel gave up resolving the target's MAC address and raised the error internally. Check first: ip neigh on the source machine. A FAILED entry means ARP got no answer, which usually means the target is off, unplugged, or the address is wrong.

2. Refused before the first packet

ping: connect: Network is unreachable

Nothing was sent. The kernel searched the routing table, found no route covering that destination, and refused the send outright. Check first: ip route. You are usually missing a default gateway, or a subnet was fat-fingered.

3. Pure silence

--- 203.0.113.9 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 12036ms

The requests left and nothing came back. No replies, no errors, nobody admitting anything: the signature of a firewall silently dropping traffic, or a dead path. Read the clock too: three requests a second apart is two seconds of work, and the command still took twelve, because with nothing received ping waits out its full timeout after the last request. That default wait is ten seconds, and -W 2 shortens it. Check first: rerun with -O to see lateness live (no answer yet for icmp_seq=1), then ping something nearer, like the gateway, to find where the silence starts.

4. Blocked at your own door

ping: sendmsg: Operation not permitted

The kernel built the packet and your own firewall refused to let it out. Check first: the local ruleset, sudo nft list ruleset on modern systems, iptables -L OUTPUT on older ones. Nothing beyond your machine can cause this one.

Challenge: make a ghost answer

Scaffolding off. From here, you build the commands yourself.

The address 10.0.1.99 sits on your subnet and was never assigned to any machine. Send exactly three pings at it from the client namespace, and read every line that comes back against the four failures above.

prompt: student@icmp-lab:~$ answer: sudo ip netns exec client ping -c 3 10.0.1.99 ||| sudo ip netns exec client ping 10.0.1.99 -c 3 output: PING 10.0.1.99 (10.0.1.99) 56(84) b

Practice ICMP and Ping in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.

More lessons in How Packets Find Their Way