ping-fox 0.1.0

A ping (ICMP) library - simple to use and no root or setuid required.
Documentation
use super::TSocket;
use crate::details::icmp::v4::SequenceNumber;
use crate::details::records::PingReceiveRecordData;
use crate::details::PingError;
use pnet_packet::icmp::{
    echo_reply::EchoReplyPacket,
    echo_request::{EchoRequestPacket as EchoRequestPacketV4, MutableEchoRequestPacket as MutableEchoRequestPacketV4},
    IcmpPacket, IcmpTypes,
};
use pnet_packet::Packet;
use rand::Rng;
use std::io;
use std::net::{IpAddr, Ipv4Addr};
use std::result::Result;
use std::time::Instant;

const PAYLOAD_SIZE: usize = 56;

pub(crate) struct IcmpV4<S> {
    payload: [u8; PAYLOAD_SIZE],
    socket: S,
}

impl<S> IcmpV4<S>
where
    S: TSocket + 'static,
{
    pub(crate) fn new(socket: S) -> IcmpV4<S> {
        let mut payload = [0u8; PAYLOAD_SIZE];
        rand::thread_rng().fill(&mut payload[..]);
        IcmpV4 { payload, socket }
    }

    pub(crate) fn send_to(
        &self,
        ipv4: Ipv4Addr,
        sequence_number: SequenceNumber,
    ) -> Result<(usize, IpAddr, SequenceNumber, Instant), PingError> {
        let ip_addr = IpAddr::V4(ipv4);
        let addr = std::net::SocketAddr::new(ip_addr, 0);

        let package = new_icmpv4_package(sequence_number, &self.payload)
            .ok_or(PingError { message: "could not create ICMP package".to_owned() })?;

        let packet = pnet_packet::Packet::packet(&package);
        let addr2: socket2::SockAddr = addr.into();
        let start_time: Instant = Instant::now();
        self.socket.send_to(packet, &addr2)?;

        Ok((PAYLOAD_SIZE, ip_addr, sequence_number, start_time))
    }

    pub(crate) fn try_receive(&self) -> std::result::Result<Option<PingReceiveRecordData>, io::Error> {
        let mut buf1 = [0u8; 128];
        match self.socket.recv_from(&mut buf1) {
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
            Err(e) => Err(e),
            Ok((package_size, ip_addr, ttl)) => {
                let receive_time: Instant = Instant::now();
                let echo_reply_package = EchoReplyPacket::new(&buf1).expect("could not initialize echo reply package");
                let sequence_number: SequenceNumber = echo_reply_package.get_sequence_number().into();
                Ok(Some(PingReceiveRecordData {
                    package_size,
                    ip_addr,
                    ttl,
                    sequence_number,
                    receive_time,
                }))
            }
        }
    }
}

pub(crate) fn new_icmpv4_package(
    sequence_number: SequenceNumber,
    payload: &[u8],
) -> Option<MutableEchoRequestPacketV4<'static>> {
    let buf = vec![0u8; EchoRequestPacketV4::minimum_packet_size() + payload.len()];
    let mut package = MutableEchoRequestPacketV4::owned(buf)?;
    package.set_sequence_number(sequence_number.into());
    package.set_identifier(0);
    package.set_icmp_type(IcmpTypes::EchoRequest);
    package.set_payload(payload);

    package.set_checksum(0_u16);
    let checksum = pnet_packet::icmp::checksum(&IcmpPacket::new(package.packet())?);
    package.set_checksum(checksum);
    Some(package)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::details::icmp::v4::tests::OnReceive;
    use crate::details::icmp::v4::tests::OnSend;
    use crate::details::icmp::v4::tests::SocketMock;

    #[test]
    fn test_send_one_ping() {
        let socket_mock = SocketMock::new(OnSend::ReturnDefault, OnReceive::ReturnWouldBlock);
        let socket_mock_clone = socket_mock.clone();
        let icmpv4 = IcmpV4::new(socket_mock_clone);

        let addr = Ipv4Addr::new(127, 0, 0, 1);
        let sequence_number = SequenceNumber::start_value();
        let result = icmpv4.send_to(addr, sequence_number);

        assert!(result.is_ok());
        socket_mock
            .should_send_number_of_messages(1)
            .should_send_to_address(&IpAddr::V4(addr));
    }

    #[test]
    fn test_try_receive() {
        let socket_mock: SocketMock = SocketMock::new(OnSend::ReturnDefault, OnReceive::ReturnDefault(1));
        let socket_mock_clone = socket_mock.clone();
        let icmpv4 = IcmpV4::new(socket_mock_clone);

        let result = icmpv4.try_receive();

        assert!(result.is_ok());
        assert!(result.as_ref().unwrap().is_some());
        let PingReceiveRecordData { package_size, ip_addr, ttl: _, sequence_number: _, receive_time: _ } =
            result.unwrap().unwrap();
        assert!(package_size >= EchoReplyPacket::minimum_packet_size());
        assert!(ip_addr == Ipv4Addr::new(127, 0, 0, 1));
        socket_mock.should_receive_number_of_messages(1);
    }
}