[resolved] Stuck with pnet + icmp


#1

I’m trying to write a small lib to send and receive ICMP pings.

Creating the ICMP packet (echo request) is working just fine, but for whatever reason the response I get
looks like complete gibberish in my program.

However, when I run tcpdump I see that the ICMP echo reply packets are coming in and in the correct format.
I don’t get why this mismatch between tcpdump and my program.

Here’s my code’s output:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/icmp-lib`
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16448, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16458, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16461, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16462, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16466, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16468, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16481, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16487, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16500, sequence_number : 0,  }
EchoReplyPacket { icmp_type : IcmpType(69), icmp_code : IcmpCode(0), checksum : 2048, identifier : 16504, sequence_number : 0,  }

ICMP type 69 makes no sense and is usually sent by Linux when a malformed packet is received.

But when I look at tcpdump, everything looks fine:

$ tcpdump -nni en0 -e icmp[icmptype] == 0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
20:46:06.916818 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 0, length 8
20:46:06.932429 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 1, length 8
20:46:06.948111 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 2, length 8
20:46:06.963769 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 3, length 8
20:46:06.979517 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 4, length 8
20:46:06.996490 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 5, length 8
20:46:07.012577 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 6, length 8
20:46:07.034241 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 7, length 8
20:46:07.049793 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 8, length 8
20:46:07.065258 e8:b6:c2:79:18:4c > <elided>, ethertype IPv4 (0x0800), length 56: 8.8.8.8 > <personal IP elided>: ICMP echo reply, id 123, seq 9, length 8

I’m guessing either the way I read from the socket is incorrect, or I’m using libnet in the wrong way.

Here’s what my code looks like:

use socket2::SockAddr;
use std::time::Instant;
use std::net::IpAddr;
use std::net::SocketAddr;

use ::IcmpSocket;

use pnet::packet::icmp::echo_request::{MutableEchoRequestPacket, EchoRequestPacket};
use pnet::packet::icmp::echo_reply::EchoReplyPacket;
use pnet::packet::icmp::{IcmpPacket, IcmpTypes, IcmpCode, checksum};
use pnet::packet::Packet;

pub struct Pinger {
    sequence_number: u16,
    identifier: u16
}

impl Pinger {
    pub fn new(identifier: u16) -> Pinger {
        Pinger{
            sequence_number: 0,
            identifier,
        }
    }

    pub fn run(&mut self, target: IpAddr, count: u32) {
        let maybe_sock = if target.is_ipv4() {
            IcmpSocket::new_v4()
        } else {
            IcmpSocket::new_v6()
        };

        if maybe_sock.is_err() {
            return;
        }

        let sock = maybe_sock.unwrap();

        let dst_addr = SockAddr::from(SocketAddr::new(target, 0));
        let packet_size = EchoRequestPacket::minimum_packet_size();

        let mut buf: Vec<u8> = vec![0; packet_size];
        for _ in 0..count {
            self.make_packet(&mut buf[..]);
            let start_time = Instant::now();
            sock.send_to(&buf[..], &dst_addr).unwrap();


            buf.clear();
            buf.resize(packet_size, 0);
            sock.recv(&mut buf[..]);

            self.sequence_number += 1;
    
            if let Some(icmp_packet) = EchoReplyPacket::new(&buf[..]) {
                println!("{:?}", icmp_packet);
            }
        }
    }

    fn make_packet(&self, buf: &mut [u8]) {
        let mut echo_packet = MutableEchoRequestPacket::new(buf).unwrap();
        echo_packet.set_sequence_number(self.sequence_number);
        echo_packet.set_identifier(self.identifier);
        echo_packet.set_icmp_type(IcmpTypes::EchoRequest);
        echo_packet.set_icmp_code(IcmpCode::new(0));

        let echo_checksum = checksum(&IcmpPacket::new(echo_packet.packet()).unwrap());
        echo_packet.set_checksum(echo_checksum);
    }
}

Full source can be found here: https://github.com/synthkaf/icmp-lib

Would anyone have a clue as to what might be wrong?


#2

Where is the socket bound (ie bind() call)? You should check for Err return values in those IO operations - may shed some light. As it stands, you ignore the error on recv. Also, I think the resize() call is a nop because it’s specifying the same length as the buffer already is.


#3

To make the program work at all, I had to change the socket type from DGRAM to RAW, using Type::raw() in IcmpSocket::new(), which is also how various ping programs open the sending socket.

With that change, what Socket::recv() delivers is the whole IP packet, which you can see in your debug printout: the bogus ICMP type 69 is byte 0x45, which is the combined IP version + header length at the start of the IP header. To get at the Echo Reply, you need to skip the 20 bytes of the IP header, and for that you have to read at least 28 bytes from the socket. So, in Pinger::run():

let packet_size = 64;    // arbitrary
...
if let Some(icmp_packet) = EchoReplyPacket::new(&buf[20..]) {
...

With that, you’ll get the expected type, sequence number etc.


#4

When doing ICMP pings you actually have two options: either use RAW sockets or use DGRAM with a IPPROTO_ICMP

Using DGRAM is quite limited, as you can only send Echo requests.


#5

Where is the socket bound (ie bind() call)?

AFAIK you don’t need to bind ICMP sockets.
If you look at iputils’ ping.c file (https://github.com/iputils/iputils/blob/master/ping.c#L695-L697) you can see it only calls bind if a specific parameter is given on the command line (i.e., you wanna really set the source).

EDIT2: I tried binding the socket but it didn’t work either :cry:

You should check for Err return values in those IO operations - may shed some light

Good point, I’m gonna print them out and see what gives

EDIT: I changed the program to check the result of calling recv() on the socket and I got nothing :frowning:
No errors.

I think the resize() call is a nop because it’s specifying the same length as the buffer already is.

It isn’t. When you call .clear()on a Vec, all elements are removed. That’s why the resize is needed.
Relevant doc: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.clear


#6

Not by default on all distros. E.g., Ubuntu doesn’t enable unprivileged ping sockets, so you have to frob the ping_group_range sysctl.


#7

Not by default on all distros. E.g., Ubuntu doesn’t enable unprivileged ping sockets, so you have to frob the ping_group_range sysctl.

True, I forgot to double check this :thinking:
Let’s see


#8

With that change, what Socket::recv() delivers is the whole IP packet, which you can see in your debug printout: the bogus ICMP type 69 is byte 0x45 , which is the combined IP version + header length at the start of the IP header. To get at the Echo Reply, you need to skip the 20 bytes of the IP header, and for that you have to read at least 28 bytes from the socket. So, in Pinger::run() :

Sorry I misread your message.
I’m confused.

Why would the DGRAM socket give me the IP packet header? :thinking:
This is really weird.


#9

Thanks @inejge for the great insight!

I didn’t know that when using a DGRAM socket + IPPROTO_ICMP I’d get the full IP packet
from the socket (just like when you use RAW sockets).
I expected to read only the ICMP packet.

Just increasing the size of the buffer and skipping the first 20 bytes let me get the data I needed.
I’ll now cleanup the code to use pnet types to do it properly.