Networking for beginners. Part 8: ICMP
All parts of the series “Networking for beginners”In the sixth part the router several times dropped a packet and told the sender about it: when TTL reached zero, when no route was found, when a packet with the DF flag did not fit in the MTU. IP by itself cannot report anything: its header has no room for errors, and the router keeps no state. For this, a second protocol has lived alongside IP from the very beginning — ICMP, the Internet Control Message Protocol (RFC 792). It is also under the hood of the two best-known network commands, ping and traceroute, and it is also the cause of half of the mysterious problems of the "ping works but the site won't open" kind.
What ICMP is
ICMP is a network-layer protocol, but it travels inside an IP packet with Protocol = 1, as if it were a transport protocol. This is not a contradiction but a convenience: that way any stack that speaks IP can handle it. The header is 8 bytes:
bytes: 1 1 2 4
┌──────┬──────┬──────────┬──────────────────────┐
│ Type │ Code │ Checksum │ depends on type │ then data
└──────┴──────┴──────────┴──────────────────────┘
Type is the kind of message, Code a refinement within the kind. Messages fall into two groups, and this division matters more than the numbers.
Queries and replies — ICMP as a protocol in its own right. The main pair:
| Type | What |
|---|---|
| 8 | Echo Request — "answer me" |
| 0 | Echo Reply — the answer |
The four bytes after the header here are the Identifier and Sequence Number, followed by arbitrary data that the reply copies unchanged.
Error messages — the reason ICMP exists. They are sent in response to someone else's packet that could not be delivered:
| Type | Code | What happened | Who sends it |
|---|---|---|---|
| 3 | 0 | Network Unreachable — no route to the network | a router |
| 3 | 1 | Host Unreachable — the network exists, the host does not respond (ARP) | the last router |
| 3 | 2 | Protocol Unreachable — the recipient has no such protocol | the host |
| 3 | 3 | Port Unreachable — nobody listens on the port (UDP) | the host |
| 3 | 4 | Fragmentation Needed — a packet with DF does not fit in the MTU | a router |
| 3 | 13 | Communication Administratively Prohibited — forbidden by a firewall | a router or host |
| 5 | 0, 1 | Redirect — there is a better gateway for this network/host | a router |
| 11 | 0 | Time Exceeded in Transit — TTL reached zero | a router |
| 11 | 1 | Fragment Reassembly Time Exceeded — not all fragments arrived | the host |
| 12 | 0 | Parameter Problem — an error in the IP header | anyone |
The key detail: the data of every error message holds the IP header of the original packet and the first 8 bytes of its data. Eight bytes are enough to capture the TCP or UDP ports, which means a stack receiving the error can figure out which socket it concerns and hand it to the right application. That is why connect() returns "No route to host" or "Connection refused" immediately rather than after a timeout. Modern stacks put more into an error, up to 576 bytes of the packet, but the 8-byte rule is the minimum.
Type 3 code 4 has one more field: in the second two bytes of "depends on type" the router writes the MTU of the next hop. Path MTU Discovery rests on it; see the section below. For Redirect, that space holds the address of the gateway to use.
Rules that keep ICMP from causing an avalanche:
- errors are not sent in response to ICMP errors (but they are sent in response to echo: otherwise ping would not work);
- errors are not sent in response to packets to broadcast and multicast addresses, or to fragments other than the first;
- sending errors is rate-limited: by default Linux sends no more than one per second per destination address for most types.
The last rule explains gaps in traceroute output: the router is alive but does not have to answer everyone.
ping
ping sends an Echo Request and waits for an Echo Reply, measuring the time between them:
$ ping -c 3 203.0.113.10
PING 203.0.113.10 (203.0.113.10) 56(84) bytes of data.
64 bytes from 203.0.113.10: icmp_seq=1 ttl=55 time=12.3 ms
64 bytes from 203.0.113.10: icmp_seq=2 ttl=55 time=12.1 ms
64 bytes from 203.0.113.10: icmp_seq=3 ttl=55 time=12.4 ms
--- 203.0.113.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 12.112/12.267/12.401/0.120 ms
What is what here:
- 56(84) bytes — 56 bytes of data; with 8 bytes of ICMP and 20 bytes of IP, 84. In the data ping puts the send timestamp, from which it computes
timeusing the returned copy, plus filler. - icmp_seq — the Sequence Number, incremented by one. Gaps in the numbers are losses, repeats are duplicates (this happens in loops and with two paths).
- ttl=55 — the TTL in the reply, that is, what remains of the server's initial value after the return path. A server with TTL 64 is nine hops away from us. This value is about the return path, not the forward one.
- time — the RTT, there and back. Half of it is not the one-way delay: paths can be asymmetric.
Different kinds of "no response" that ping shows differently and that you need to tell apart:
| Output | Means |
|---|---|
From 192.168.1.10 icmp_seq=1 Destination Host Unreachable |
your own stack: the neighbour or gateway did not answer ARP (part 7). The problem is in your own segment |
From 198.51.100.1 icmp_seq=1 Destination Net Unreachable |
router 198.51.100.1 found no route. The packet reached it and hit a wall |
From 198.51.100.1 ... Packet filtered |
type 3 code 13: a firewall on that router forbade it |
From 203.0.113.9 ... Time to live exceeded |
a routing loop; the packet circled until its TTL ran out |
silence, then 100% packet loss |
packets or replies are being lost silently: the host is off, a firewall drops without replying, ICMP is blocked |
Flags that will come in handy later: -s 1472 — the data size, -M do — set DF and forbid fragmentation, -t 5 — the initial TTL, -I eth1 — the interface. On Windows ping -l 1472 -f does the same for size and DF, and there it sends four requests by default instead of going on forever.
All stacks are required to answer echo, but administrators often block it "against scanning". So silence to a ping proves nothing: the host may be alive and closed. But a reply proves everything: the host is alive and there is a route in both directions.
traceroute
How do you learn all the routers along a path if IP does not record them? Through TTL. Send a packet with TTL = 1: the first router decrements it to zero, drops it and sends back Time Exceeded from its own address. Now we know the first hop. TTL = 2 — the second router. And so on, until the packet reaches the destination:
$ traceroute 203.0.113.10
traceroute to 203.0.113.10 (203.0.113.10), 30 hops max, 60 byte packets
1 192.168.1.1 (192.168.1.1) 0.412 ms 0.388 ms 0.371 ms
2 198.51.100.1 (198.51.100.1) 1.924 ms 1.901 ms 1.887 ms
3 198.51.100.65 (198.51.100.65) 4.102 ms 4.088 ms 4.071 ms
4 * * *
5 203.0.113.1 (203.0.113.1) 11.512 ms 11.498 ms 11.480 ms
6 203.0.113.10 (203.0.113.10) 12.301 ms 12.288 ms 12.270 ms
Three probes go out for each TTL, hence three times. How does traceroute know it has arrived? For that, the probes are crafted so that the final host also answers them with an error:
- the classic Unix traceroute sends UDP to ports that surely do not exist, starting at 33434. Routers reply with Time Exceeded, while the final host replies with Port Unreachable (type 3 code 3), and that is the signal to stop;
tracerton Windows andtraceroute -Isend ICMP Echo, and the end is marked by an Echo Reply;traceroute -Tsends a TCP SYN to port 80 or 443: it gets through firewalls that block both UDP to odd ports and ICMP.
A * * * line is a hop from which no reply arrived within the timeout. Reasons: the router does not send Time Exceeded (it is configured that way), it does but no more than once a second and we missed the slot, or its replies are filtered along the way. If the path continues after the asterisks, the hop is just a quiet one, and that is not a problem. If the asterisks go on to the end, packets are not getting any further, and the problem is at the last router that answered or right behind it.
What traceroute does not show:
- The return path. Each reply came back its own way, which may differ from the forward one. A time at hop 4 larger than at hop 5 is perfectly normal: router 4 just generates ICMP slowly, or its reply took a longer route.
- A single path. When traffic is balanced across equal-cost routes, a router spreads packets by a hash of the addresses and ports. The classic traceroute changes the destination port with every probe, and three probes for one hop may take three different paths. The output then "jumps around". The
paris-traceroutetool and the-Uflag keep the ports constant. - The address the router faces forward with. Time Exceeded is sent from the address of the interface the packet arrived on, that is, from each router's "ingress" address.
The mtr tool combines ping and traceroute: it sends probes continuously and shows cumulative loss and latency for each hop. For diagnosing "where are packets being lost" it is more convenient than either.
Path MTU Discovery
Back to fragmentation from the sixth part. A modern stack sets DF on all packets and relies on the network to tell it if a packet does not fit. That is Path MTU Discovery (RFC 1191):
host R1 (MTU 1500) R2 (MTU 1492, PPPoE) server
│ │ │ │
├── 1500 bytes, DF ──────────►├── 1500 bytes, DF ──────────►│ too big, DF: drop │
│ │ │ │
│◄── ICMP 3/4, MTU = 1492 ────┼─────────────────────────────┤ │
│ │ │ │
│ remember: 1492 to server │ │ │
├── 1492 bytes, DF ──────────►├─────────────────────────────┼─────────────────────────────►│
- The host sends a packet the size of its MTU, 1500, with the DF flag.
- A router whose outgoing MTU is smaller drops the packet and replies with Fragmentation Needed, with the next-hop MTU in the header.
- The host's stack receives the error, uses the enclosed header to find which destination address it relates to, and remembers: the MTU to this address is 1492. TCP reduces its segment size, retransmits what was lost, and everything carries on.
- The entry lives for 10 minutes, after which the stack tries increasing the size again: the path may have changed.
You can see what the stack has remembered like this:
$ ip route get 203.0.113.10
203.0.113.10 via 192.168.1.1 dev eth0 src 192.168.1.10 uid 1000
cache expires 598sec mtu 1492
The black hole
The scheme works exactly as long as the ICMP error reaches the sender. But between router R2 and the host there may be a firewall configured with "ICMP is ping, we don't need ping, block ICMP entirely". Then:
- small packets (requests, login pages, ping) get through;
- a large packet (a response with an image, a file, command output) is dropped at R2, and the message about it never reaches the server;
- the server retransmits the same packet of the same size, again and again, until TCP gives up.
From the outside it looks like "the site starts loading and hangs", "SSH lets me in, but ls in a big directory hangs", "the VPN comes up, but nothing works". This is a PMTU black hole, and it is probably the most common network problem that people cannot find for weeks, because ping works.
Fixes, from most to least correct:
- Allow ICMP type 3 code 4 (and type 11, and type 3 with codes 1 and 3). A firewall that filters ICMP must filter selectively: you can block echo and timestamp if you really want to, but not the errors.
- MSS clamping on the router with the narrow link — the trick from the fourth part. When establishing a connection, TCP announces its maximum segment size; the router rewrites it so that a segment plus headers fits into its MTU, and large packets never appear at all. It only helps TCP.
- Probing without ICMP (RFC 4821):
sysctl net.ipv4.tcp_mtu_probing=1. TCP, noticing that retransmissions of a large segment do not get through while small ones do, tries intermediate sizes on its own. Slower, but it does not depend on the goodwill of firewalls. - Lower the interface MTU on the host. Crude, but it works.
You can find a black hole with the same ping using DF and adjusting the size. 1472 bytes of data plus 28 bytes of headers is exactly 1500:
$ ping -M do -s 1472 203.0.113.10
ping: local error: message too long, mtu=1492
Here the local stack already knows about 1492 from the earlier error. If it did not, and there was a hole, replies would simply not come back, while -s 1464 would go through. That is how, by binary search, you find the real path MTU.
Redirect
There are two routers in a segment: R1 is the default gateway, R2 leads to the network 10.0.0.0/8. The host only knows R1. A packet to 10.1.2.3 goes to R1, which looks at its table and sees that the next hop is R2, in the same segment the packet came from. R1 forwards the packet to R2 and sends the host an ICMP Redirect: "for 10.1.2.3, use R2". The host adds a temporary route and from then on goes to 10.0.0.0/8 directly.
Convenient: there is no need to configure routes on every host. Dangerous: anyone in the segment can send a redirect and divert a host's traffic to themselves, exactly as with ARP in the previous part. That is why stacks accept a redirect only from their current gateway and only for an address on their own network, many systems turn it off entirely (net.ipv4.conf.all.accept_redirects=0), and routers are forbidden from sending them. If a route marked redirected shows up in ip route get, you know where it came from.
What can be filtered
Since ICMP is a favourite victim of firewalls, here is a short cheat sheet.
Must always be allowed, otherwise the network breaks:
- type 3 code 4 — Fragmentation Needed (otherwise black holes);
- type 11 — Time Exceeded (otherwise traceroute does not work and you lose diagnostics for loops);
- type 3 codes 0, 1, 3, 13 — without them applications wait for a timeout instead of getting an immediate error.
Can be blocked if there is a reason: echo request from outside (though you then lose ping as a diagnostic tool for yourself), timestamp (type 13/14, reveals the host's time), redirect at the network edge.
An old attack that makes people block more than they should: smurf — an echo request to the broadcast address of someone else's network with a spoofed source address, so all the hosts on that network reply to the victim. The defence is not answering echo sent to broadcast addresses — in Linux that is net.ipv4.icmp_echo_ignore_broadcasts=1 by default — and forbidding directed broadcast on routers. There is no need to block ICMP entirely for this.
In IPv6 all of this is multiplied: there ICMPv6 is also responsible for what ARP does in IPv4, and filtering "all ICMP" breaks the network immediately rather than half an hour later. More on this in part 14.
Seeing it for yourself
All ICMP on an interface, with types and codes:
$ sudo tcpdump -i eth0 -n -v icmp
IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto ICMP (1), length 84)
192.168.1.10 > 203.0.113.10: ICMP echo request, id 1234, seq 1, length 64
IP (tos 0x0, ttl 55, id 0, offset 0, flags [none], proto ICMP (1), length 84)
203.0.113.10 > 192.168.1.10: ICMP echo reply, id 1234, seq 1, length 64
IP (tos 0xc0, ttl 63, id 0, offset 0, flags [none], proto ICMP (1), length 576)
198.51.100.1 > 192.168.1.10: ICMP 203.0.113.10 unreachable - need to frag (mtu 1492), length 556
IP (tos 0x0, ttl 63, id 2, offset 0, flags [DF], proto TCP (6), length 1500)
192.168.1.10.51234 > 203.0.113.10.443: ...
The third message is Fragmentation Needed with the MTU and the enclosed header of the original packet, which tcpdump parsed as well: you can see it was a 1500-byte TCP segment to port 443. This is how black holes are caught: if such messages are visible on the router but not on the host, someone along the way is dropping them.
To compare probing with ICMP, UDP and TCP when something is being filtered: traceroute -I, traceroute (UDP), traceroute -T -p 443. If only the last one gets through, there is a firewall on the path that only allows the required ports.
And run mtr -n 203.0.113.10 for a minute to see at which hop losses begin. Loss at an intermediate hop that is absent at the last one is not loss but a router rate-limiting ICMP; loss that continues from some hop all the way to the end is real.
Key takeaways
- ICMP is error reporting and diagnostics alongside IP. Every error carries the header of the original packet and 8 bytes of data, which the stack uses to find the socket at fault.
- ping is echo request/reply;
Destination Host Unreachablefrom your own address means ARP, from someone else's means routing. The TTL in the reply tells you about the return path. - traceroute raises TTL from one and collects Time Exceeded from every router; the end is Port Unreachable or Echo Reply. It shows the forward path, one of the possible ones.
- Path MTU Discovery: DF plus Fragmentation Needed with the MTU in the reply. A firewall that drops ICMP creates a black hole: small things get through, big ones hang. The cure is allowing type 3 code 4, MSS clamping,
tcp_mtu_probing. - Redirect is a hint that "there is a closer gateway", useful and dangerous; usually turned off.
This concludes the IPv4 block: addresses, routing, translation into MAC addresses and error messages. Next comes the transport layer, starting with the simple one: Part 9: UDP. Eight bytes of header, ports and sockets, what "connectionless" means and who it suits, and how UDP behaves behind NAT.
Only registered users can leave a comment.
Sign in or register to leave a comment.