I. Implementing ICMP in Rust

This post is inspired by Jon Gjengset series titled "Implementing TCP in Rust".

This should be an interesting read for anyone who wants to get a better understanding of networking protocols down to bits and doesn't mind diving into RFCs.

So what is ICMP anyway? It's the protocol that makes ping and traceroute possible. If you're a gamer or ever joined an online call, then you know how important those ping/latency numbers are.

Unlike TCP/UDP, which are Layer 4 protocols in OSI Model, ICMP actually belongs to Layer 3 which is the same level as IP, itself the main protocol of the Internet.

1. IP Packet

First, I'll show you what an IPv4 (RFC-791) header looks like, remember all your connections on the internet start with one of these:

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                  Example Internet Datagram Header

To capture a raw IP frame, ping localhost in another terminal and listen with:

$ tcpdump -i lo -p icmp -X
15:23:33.885666 IP localhost > localhost: ICMP echo request, id 21780, seq 1, length 64
    0x0000:  4500 0054 9210 4000 4001 aa96 7f00 0001  E..T..@.@.......
    0x0010:  7f00 0001 0800 492b 5514 0001 2d08 f460  ......I+U...-..`
    0x0020:  0000 0000 6c83 0d00 0000 0000 1011 1213  ....l...........
    0x0030:  1415 1617 1819 1a1b 1c1d 1e1f 2021 2223  .............!"#
    0x0040:  2425 2627 2829 2a2b 2c2d 2e2f 3031 3233  $%&'()*+,-./0123
    0x0050:  3435 3637                                4567
15:23:33.885695 IP localhost > localhost: ICMP echo reply, id 21780, seq 1, length 64
    0x0000:  4500 0054 9211 0000 4001 ea95 7f00 0001  E..T....@.......
    0x0010:  7f00 0001 0000 512b 5514 0001 2d08 f460  ......Q+U...-..`
    0x0020:  0000 0000 6c83 0d00 0000 0000 1011 1213  ....l...........
    0x0030:  1415 1617 1819 1a1b 1c1d 1e1f 2021 2223  .............!"#
    0x0040:  2425 2627 2829 2a2b 2c2d 2e2f 3031 3233  $%&'()*+,-./0123
    0x0050:  3435 3637                                4567

Let's start with the first IP and decode the header — the first 20 bytes of 4500 0054 9210 4000 4001 aa96 7f00 0001 7f00 0001 ...:

4           Version                 = IPv4 version
5           IHL                     = 20 bytes
00          ToS
0054        Total Length            = 84 bytes
9210        Identification
4           Flags
000         Fragment Offset
40          Time to Live            = 64 TTL
01          Protocol                = ICMP
aa96        Header Checksum
7f00 0001   Source IP Address       = 127.0.0.1
7f00 0001   Destination IP Address  = 127.0.0.1

2. ICMP Packet

From RFC-792 we can see the layout of Echo or Echo reply message:

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Type      |     Code      |          Checksum             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Identifier          |        Sequence Number        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Data ...
+-+-+-+-+-

And by replacing data bytes, the remaining bytes after the header, we get our ICMP header:

08          Type                    = Echo message
00          Code                     
492b        Checksum
5514        Identifier              = id 21780 (in raw capture)
0001        Sequence Number         = seq 1

If you compare this ICMP packet with the one from second IP frame, two fields have changed:

  1. Type which has become 00 or Echo reply message
  2. Checksum, naturally because content has changed

3. Implementation in Rust

Now that we know what an ICMP packet looks like, we are ready to implement it in Rust.

Unlike TCP/UDP, we have no concept of ports here and only use IPs to find our path. (Now you understand why ping has no port?).

Furthermore, astute reader may have noticed that the only thing that changes in the ICMP echo reply is the Type which goes from 08 to 00.

We've got all we need. Check out: II. Implementing ICMP in Rust

38