II. Implementing ICMP in Rust
Tags: icmp, rust, networkprogramming
Published: 2021-07-18 15:00:44

---
title: II. Implementing ICMP in Rust
published: true
description: Coding ICMP in Rust with TUN/TAP Device
tags: icmp, rust, icmp, networkprogramming
//cover_image: https://direct_url_to_image.jpg
---

*Make sure to checkout previous part where we talk about IP and ICMP layouts if you haven't already: [I. Implementing ICMP in Rust](https://dev.to/xphoniex/i-implementing-icmp-in-rust-296o)*

## 3. (Cont'd) Implementation in Rust

First thing we need to do is to create a TUN/TAP device that allows us to receive/send raw frames directly. It's a kernel-feature which enables us to have software-defined interfaces.

```
fn main() {
    let mut nic = tun_tap::Iface::without_packet_info("tun0", tun_tap::Mode::Tun).unwrap();
    let mut buf = [0u8; 1500];
    loop {
        let nbytes = nic.recv(&mut buf[..]).unwrap();

        match etherparse::Ipv4HeaderSlice::from_slice(&buf[..nbytes]) {
            Ok(iph) => {
                let src = iph.source_addr();
                let dst = iph.destination_addr();
                let proto = iph.protocol();

                if proto != 1 {
                    continue;
                }

                let data_buf = &buf[iph.slice().len()..nbytes];

                if let Some(mut c) = Connection::start(
                    iph,
                    data_buf,
                ).unwrap() {
                    println!("connection c started!");
                    c.respond(&mut nic).unwrap();
                    println!("responded to {} packet from {} ", proto, src);
                }
            }
            Err(e) => {
                eprintln!("ignoring weird packet {:?}", e);
            }
        }
    }
}
```

In our `main`, we create our device and a `buf` and keep reading from it in a loop. Whenever we find an `IPv4` header, `etherparse` already takes care of the parsing for us and gives us the `iph` object.

For example, if you've read the previous installment you should know the `proto == 1` for ICMP echo and echo reply packets, thus we skip everything else:

```
                if proto != 1 {
                    continue;
                }
```

Finally we read the actual `data`, which is everything except header bytes and create a connection:

```
                let data_buf = &buf[iph.slice().len()..nbytes];

                if let Some(mut c) = Connection::start(
                    iph,
                    data_buf,
                ).unwrap() {
                    println!("connection c started!");
                    c.respond(&mut nic).unwrap();
                    println!("responded to {} packet from {} ", proto, src);
                }
```

Since we're not dealing with a stateful protocol like TCP, we can get away with not using `Connection` but for sake of conforming to John's TCP implementation, I use one as well.

Still you need `Connection` to distinguish multiple streams and detect out of order sequence numbers.

```
pub struct Connection {
    ip: etherparse::Ipv4Header,
    icmp_id: u16,
    seq_no: u16,
}

impl Connection {
    pub fn start(iph: etherparse::Ipv4HeaderSlice, data: &[u8]) -> std::io::Result<Option<Self>> {
        let mut c = Connection {
            ip: etherparse::Ipv4Header::new(
                0,
                64,
                etherparse::IpTrafficClass::Icmp,
                [
                    iph.destination()[0],
                    iph.destination()[1],
                    iph.destination()[2],
                    iph.destination()[3],
                ],
                [
                    iph.source()[0],
                    iph.source()[1],
                    iph.source()[2],
                    iph.source()[3],
                ],
            ),
            icmp_id: u16::from_be_bytes(data[4..6].try_into().unwrap()),
            seq_no: u16::from_be_bytes(data[6..8].try_into().unwrap()),
        };

        Ok(Some(c))
    }

    pub fn respond(&mut self, nic: &mut tun_tap::Iface,) -> std::io::Result<usize> {
        let mut buf = [0u8; 1500];

        self.ip.set_payload_len(84-20 as usize);

        use std::io::Write;
        let mut unwritten = &mut buf[..];
        self.ip.write(&mut unwritten);

        let mut icmp_reply = [0u8; 64];
        // type
        icmp_reply[0] = ICMP_ECHO_REPLY;
        // code, always 0
        icmp_reply[1] = 0; 

        // checksum = 2 & 3, empty for now
        icmp_reply[2] = 0x00; 
        icmp_reply[3] = 0x00;

        // id = 4 & 5
        icmp_reply[4] = ((self.icmp_id >> 8) & 0xff) as u8; 
        icmp_reply[5] = (self.icmp_id & 0xff) as u8;

        // seq_no = 6 & 7
        icmp_reply[6] = ((self.seq_no >> 8) & 0xff) as u8; 
        icmp_reply[7] = (self.seq_no & 0xff) as u8;

        unwritten.write(&icmp_reply);

        let unwritten = unwritten.len();
        nic.send(&buf[..buf.len() - unwritten])?;

        Ok(0)
    }
}
```

We create our `Connection` type and implement two methods: `start` and `respond`. All `start` does is create an IP header with the source and destination swapped.

Then in `respond`, we start writing the actual ICMP packet. We start by setting IP's size to 84, which if you remember from last part was the *Total Length* of our IP packet. 20 bytes is the IP header so we deduce that and prepare a 64 bytes buffer to hold our ICMP.

All we do here is change the ICMP type to `0` which is *echo reply*, leave checksum as `0`, and copy in the `identifier` and `sequence number`.

Remember ICMP layout from previous part?
```
08          Type  		 	 0th byte      	= Echo message
00          Code  		 	 1st byte                    
492b        Checksum  	 	 2-3rd byte
5514        Identifier  	 4-5th byte    	= id 21780 (in raw capture)
0001        Sequence Number  6-7th byte     = seq 1
```

## 4. Testing the implementation

We're ready to run our program.

Once it creates the device, we can `ping` it with any IP address associated with it and get:

```
# ping 192.168.0.3

PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=1626615676072 ms
wrong data byte #16 should be 0x10 but was 0x0
#16	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
#48	0 0 0 0 0 0 0 0
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=1626615677081 ms
wrong data byte #16 should be 0x10 but was 0x0
#16	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
#48	0 0 0 0 0 0 0 0
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=1626615678105 ms
wrong data byte #16 should be 0x10 but was 0x0
#16	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
#48	0 0 0 0 0 0 0 0
```
 
Turns out it's expecting our byte #16 to be `0x10`, our #17 to be `0x11` and so on. So we add:

```
        for i in 16..64-8 {
            icmp_reply[i+8] = (0x10 + (i - 16)) as u8;
        }
```

to our `respond` method and try `ping` again:

```
# ping 192.168.0.3

PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=1626615769165 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=1626615770169 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=1626615771193 ms
64 bytes from 192.168.0.3: icmp_seq=4 ttl=64 time=1626615772217 ms
64 bytes from 192.168.0.3: icmp_seq=5 ttl=64 time=1626615773241 ms
```

We get a working `ping` back and forth, only all the `times` are **all wrong**.

After a lot of head scratches, and double and triple checking the wiki page of IP, ICMP, etc, I finally found the solution on RFC-792 page:

> The data received in the echo message must be returned in the echo reply message.

So instead of blindly adding `0x10`, `0x11` to my packet, I copy the original values:

```
icmp_reply[8..64].clone_from_slice(&self.data[8..64]);
```

and try once again:

```
# ping 192.168.0.3

PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.162 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.214 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.174 ms
64 bytes from 192.168.0.3: icmp_seq=4 ttl=64 time=0.226 ms
```

**Everything seems to be working, except I don't have the correct checksum. Or any checksum for that mattter.**

It's not showing up in `ping` but if you use Wireshark:

![checksum-invalid](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g4di8r7tjhki15zkmsuq.png)

Following RFCs and Wikis only made me more confused when trying to write the checksum function. I had better luck writing one in Python and translating it to Rust after getting it right:

```
fn calculate_checksum(data: &mut [u8]) {
    let mut f = 0;
    let mut chk: u32 = 0;
    while f + 2 <= data.len() {
        chk += u16::from_le_bytes(data[f..f+2].try_into().unwrap()) as u32;

        f += 2;
    }

    while chk > 0xffff {
        chk = (chk & 0xffff) + (chk >> 2*8);
    }

    let mut chk = chk as u16;

    chk = !chk & 0xffff;

    // endianness
    //chk = chk >> 8 | ((chk & 0xff) << 8);

    data[3] = (chk >> 8) as u8;
    data[2] = (chk & 0xff) as u8;

}
```

![checksum-correct](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xszkel9ss8l91tt970t1.png)

And everything seems to be working, finally.

## 5. Github

The code is available on my Github [xphoniex/icmp-rust](https://github.com/xphoniex/icmp-rust).