From f5d377615912c291d73a0b251f3a1de7c48abc51 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Fri, 20 Feb 2026 21:38:36 +0700 Subject: [PATCH] refactor: migrate ICMP handling to icmp-socket crate --- Cargo.lock | 32 +++------ Cargo.toml | 11 ++- src/pinger.rs | 71 +++++--------------- src/pinger_icmp.rs | 111 ++++++++++++++---------------- src/pinger_icmp_ts.rs | 153 ++++++++++++++++++++++++------------------ 5 files changed, 168 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8379792..057e2f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "atty" version = "0.2.14" @@ -221,9 +215,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "2163a0e204a148662b6b6816d4b5d5668a5f2f8df498ccbd5cd0e864e78fecba" dependencies = [ "powerfmt", ] @@ -294,15 +288,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "etherparse" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b119b9796ff800751a220394b8b3613f26dd30c48f254f6837e64c464872d1c7" -dependencies = [ - "arrayvec", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -389,6 +374,14 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "icmp-socket" +version = "0.4.0" +source = "git+https://github.com/Lochnair/icmp-socket.git?tag=v0.4.0#ca3695e3e104713fda9872546545dc5266f6ed4c" +dependencies = [ + "socket2", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -794,15 +787,12 @@ version = "0.2.0" dependencies = [ "anyhow", "bincode", - "byteorder", - "etherparse", + "icmp-socket", "log", "neli", "rand", "rust-uci", "rustix", - "serde", - "socket2", "thiserror", "time", ] diff --git a/Cargo.toml b/Cargo.toml index 4a52bb2..1e23a1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,26 +21,23 @@ strip = "symbols" [dependencies] anyhow = "1.0" bincode = "2.0" -byteorder = "1.5" -etherparse = "0.19" +icmp-socket = "0.4.0" neli = "0.7" rand = "0.10" rustix = { version = "1.1", features = ["fs", "thread", "time"] } rust-uci = { version = "0.1", optional = true } -socket2 = { version = "0.6", features = ["all"] } thiserror = "2.0" [dependencies.log] features = ["std"] version = "0.4" -[dependencies.serde] -features = ["derive"] -version = "1.0" - [dependencies.time] features = ["formatting", "macros"] version = "0.3" [features] uci = ["dep:rust-uci"] + +[patch.crates-io] +icmp-socket = { git = 'https://github.com/Lochnair/icmp-socket.git', tag = "v0.4.0"} \ No newline at end of file diff --git a/src/pinger.rs b/src/pinger.rs index 7e21c82..b7ead77 100644 --- a/src/pinger.rs +++ b/src/pinger.rs @@ -1,9 +1,8 @@ use crate::MeasurementType; -use etherparse::err::packet::SliceError; -use log::debug; -use socket2::{Domain, Protocol, SockAddr, Socket, Type}; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; -use std::str::FromStr; +use icmp_socket::socket::IcmpSocket; +use icmp_socket::{IcmpSocket4, Icmpv4Packet}; +use log::{debug}; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::mpsc::Sender; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; @@ -15,13 +14,9 @@ pub enum PingError { #[error("Couldn't parse number")] InvalidNumber(#[from] io::Error), #[error("Error parsing packet")] - InvalidPacket(#[from] SliceError), - #[error("Invalid protocol")] - InvalidProtocol(String), + InvalidPacket(String), #[error("Invalid packet type")] InvalidType(String), - #[error("No transport")] - NoTransport, #[error("Wrong ICMP identifier (expected {expected:?}, found {found:?})")] WrongID { expected: u16, found: u16 }, } @@ -39,34 +34,17 @@ pub struct PingReply { pub last_receive_time_s: Instant, } -fn open_socket(type_: MeasurementType) -> io::Result { +fn open_socket(type_: MeasurementType) -> io::Result { match type_ { MeasurementType::Icmp | MeasurementType::IcmpTimestamps => { - Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4)) - } - MeasurementType::Ntp => Socket::new(Domain::IPV4, Type::DGRAM, None), + IcmpSocket4::new() + }, _ => { unimplemented!() } } } -trait ReadFrom { - fn read_from(&mut self) -> io::Result<(Vec, SockAddr)>; -} - -impl ReadFrom for Socket { - fn read_from(&mut self) -> io::Result<(Vec, SockAddr)> { - let mut buffer = Vec::with_capacity(4096); - let (received, addr) = self.recv_from(buffer.spare_capacity_mut())?; - - unsafe { - buffer.set_len(received); - } - Ok((buffer, addr)) - } -} - pub trait PingListener { fn listen( &mut self, @@ -78,15 +56,11 @@ pub trait PingListener { let socket = &mut open_socket(type_)?; loop { - let (buf, sender) = match socket.read_from() { + let (pkt, sender) = match socket.rcv_from() { Ok(val) => val, Err(_) => continue, }; - // etherparse doesn't like when the size in the header doesn't match the buffer - // so resize the buffer when actual packet size is known - //buf = buf[..size].as_mut(); - let addr: IpAddr = sender.as_socket().unwrap().ip(); let reflectors = reflectors_lock.read().unwrap(); @@ -94,7 +68,7 @@ pub trait PingListener { continue; } - let reply = match self.parse_packet(id, addr, buf.as_slice()) { + let reply = match self.parse_packet(id, addr, pkt) { Ok(val) => val, Err(_) => { // parse_packet will throw an error if it's an unknown protocol etc. @@ -108,7 +82,7 @@ pub trait PingListener { } } - fn parse_packet(&self, id: u16, reflector: IpAddr, buf: &[u8]) -> Result; + fn parse_packet(&self, id: u16, reflector: IpAddr, pkt: Icmpv4Packet) -> Result; } pub trait PingSender { @@ -118,7 +92,7 @@ pub trait PingSender { type_: MeasurementType, reflectors_lock: Arc>>, ) -> anyhow::Result<()> { - let socket = &open_socket(type_)?; + let mut socket = open_socket(type_)?; let mut seq: u16 = 0; let tick_duration_ms: u16 = 500; @@ -131,23 +105,12 @@ pub trait PingSender { Duration::from_millis((tick_duration_ms / reflectors.len() as u16) as u64); for reflector in reflectors.iter() { - let addr: SockAddr = match reflector.is_ipv4() { - true => { - let ip4 = Ipv4Addr::from_str(&*reflector.to_string()).unwrap(); - let sock4 = SocketAddrV4::new(ip4, 0); - sock4.into() - } - false => { - let ip6 = Ipv6Addr::from_str(&*reflector.to_string()).unwrap(); - let sock6 = SocketAddrV6::new(ip6, 0, 0, 0); - sock6.into() - } + let addr: Ipv4Addr = match reflector { + IpAddr::V4(ipv4) => *ipv4, + IpAddr::V6(_) => unimplemented!(), }; - let buf_v = self.craft_packet(id, seq); - let buf = buf_v.as_slice(); - - socket.send_to(buf, &addr)?; + socket.send_to(addr, self.craft_packet(id, seq))?; thread::sleep(sleep_duration); } @@ -159,5 +122,5 @@ pub trait PingSender { } } - fn craft_packet(&self, id: u16, seq: u16) -> Vec; + fn craft_packet(&self, id: u16, seq: u16) -> icmp_socket::packet::Icmpv4Packet; } diff --git a/src/pinger_icmp.rs b/src/pinger_icmp.rs index b73b20e..9e9c201 100644 --- a/src/pinger_icmp.rs +++ b/src/pinger_icmp.rs @@ -3,9 +3,9 @@ use std::time::Instant; use crate::pinger::{PingError, PingListener, PingReply, PingSender}; use crate::time::Time; -use byteorder::*; -use etherparse::TransportSlice::{Icmpv4, Icmpv6}; -use etherparse::{IcmpEchoHeader, Icmpv4Header, Icmpv4Type, SlicedPacket}; +use icmp_socket::Icmpv4Message; +use icmp_socket::Icmpv4Packet; +use icmp_socket::packet::WithEchoRequest; use rustix::thread::ClockId; pub struct PingerICMPEchoListener {} @@ -14,73 +14,62 @@ pub struct PingerICMPEchoSender {} impl PingListener for PingerICMPEchoListener { // Result: RTT, down time, up time - fn parse_packet(&self, id: u16, reflector: IpAddr, buf: &[u8]) -> Result { - match SlicedPacket::from_ip(buf) { - Err(err) => Err(PingError::InvalidPacket(err)), - Ok(value) => match value.transport { - Some(Icmpv4(icmp)) => match icmp.icmp_type() { - Icmpv4Type::EchoReply(echo) => { - if echo.id != id { - return Err(PingError::WrongID { - expected: id, - found: echo.id, - }); - } - - let time_sent = icmp - .payload() - .read_u64::() - .expect("Couldn't parse payload to time") - as i64; - - let clock = Time::new(ClockId::Monotonic); - let time_ms = clock.to_milliseconds() as i64; - - let rtt: i64 = time_ms - time_sent; - Ok(PingReply { - reflector, - seq: echo.seq, - rtt, - current_time: time_ms, - down_time: (rtt / 2) as f64, - up_time: (rtt / 2) as f64, - originate_timestamp: 0, - receive_timestamp: 0, - transmit_timestamp: 0, - last_receive_time_s: Instant::now(), - }) + fn parse_packet(&self, id: u16, reflector: IpAddr, pkt: Icmpv4Packet) -> Result { + match pkt.typ { + 0 => { + if let Icmpv4Message::EchoReply { + identifier, + sequence, + payload, + } = pkt.message + { + if identifier != id { + return Err(PingError::WrongID { + expected: id, + found: identifier, + }); } - type_ => Err(PingError::InvalidType(format!("{:?}", type_))), - }, - Some(Icmpv6(slice)) => Err(PingError::InvalidProtocol(format!("{:?}", slice))), - Some(type_) => Err(PingError::InvalidProtocol(format!("{:?}", type_))), - None => Err(PingError::NoTransport), + + let time_sent = match payload.as_slice().try_into() { + Ok(bytes) => u64::from_be_bytes(bytes) as i64, + Err(_) => { + return Err(PingError::InvalidPacket(format!("Expected 8 bytes payload, but found {}", payload.len()))) + } + }; + + let clock = Time::new(ClockId::Monotonic); + let time_ms = clock.to_milliseconds() as i64; + + let rtt: i64 = time_ms - time_sent; + Ok(PingReply { + reflector, + seq: sequence, + rtt, + current_time: time_ms, + down_time: (rtt / 2) as f64, + up_time: (rtt / 2) as f64, + originate_timestamp: time_sent, + receive_timestamp: 0, + transmit_timestamp: 0, + last_receive_time_s: Instant::now(), + }) + } else { + Err(PingError::InvalidPacket(format!("Packet had type {:?}, but did not match the structure", pkt.typ))) + } + + }, + type_ => Err(PingError::InvalidType(format!("{:?}", type_))), } } } impl PingSender for PingerICMPEchoSender { - fn craft_packet(&self, id: u16, seq: u16) -> Vec { + fn craft_packet(&self, id: u16, seq: u16) -> Icmpv4Packet { let clock = Time::new(ClockId::Monotonic); let time_ms = clock.to_milliseconds(); - let payload = time_ms.to_ne_bytes(); + let payload = time_ms.to_be_bytes().to_vec(); - // Construct a header with checksum based on the payload - let hdr = Icmpv4Header::with_checksum( - Icmpv4Type::EchoRequest(IcmpEchoHeader { id, seq }), - &payload, - ); - - // Create a buffer to hold the result of header + payload - let mut result = Vec::::with_capacity(hdr.header_len() + payload.len()); - - // Write the header to the buffer - hdr.write(&mut result).expect("Error writing packet"); - - // Write the payload to the buffer - result.append(&mut payload.to_vec()); - - result + Icmpv4Packet::with_echo_request(id, seq, payload).unwrap() } } diff --git a/src/pinger_icmp_ts.rs b/src/pinger_icmp_ts.rs index ad778bb..4f6c8d4 100644 --- a/src/pinger_icmp_ts.rs +++ b/src/pinger_icmp_ts.rs @@ -1,89 +1,108 @@ use crate::pinger::{PingError, PingListener, PingReply, PingSender}; use crate::time::Time; -use etherparse::icmpv4::TimestampMessage; -use etherparse::TransportSlice::{Icmpv4, Icmpv6}; -use etherparse::{Icmpv4Header, Icmpv4Type, SlicedPacket}; +use icmp_socket::packet::IcmpPacketBuildError; +use icmp_socket::Icmpv4Message; +use icmp_socket::Icmpv4Packet; use rustix::thread::ClockId; use std::net::IpAddr; use std::time::Instant; +/// Construct a packet for the TimestampRequest messages. +pub trait WithTimestampRequest { + type Packet; + + fn with_timestamp_request( + identifier: u16, + sequence: u16, + originate: u32, + receive: u32, + transmit: u32, + ) -> Result; +} + +impl WithTimestampRequest for Icmpv4Packet { + type Packet = Icmpv4Packet; + + fn with_timestamp_request( + identifier: u16, + sequence: u16, + originate: u32, + receive: u32, + transmit: u32, + ) -> Result { + Ok(Self { + typ: 13, + code: 0, + checksum: 0, + message: Icmpv4Message::Timestamp { + identifier, + sequence, + originate, + receive, + transmit, + }, + }) + } +} + pub struct PingerICMPTimestampListener {} pub struct PingerICMPTimestampSender {} impl PingListener for PingerICMPTimestampListener { // Result: RTT, down time, up time - fn parse_packet(&self, id: u16, reflector: IpAddr, buf: &[u8]) -> Result { - match SlicedPacket::from_ip(buf) { - Err(err) => Err(PingError::InvalidPacket(err)), - Ok(value) => match value.transport { - Some(Icmpv4(icmp)) => match icmp.icmp_type() { - Icmpv4Type::TimestampReply(reply) => { - if reply.id != id { - return Err(PingError::WrongID { - expected: id, - found: reply.id, - }); - } - - let time_now = Time::new(ClockId::Realtime); - let time_since_midnight = time_now.get_time_since_midnight(); - - let originate_timestamp = reply.originate_timestamp; - let receive_timestamp = reply.receive_timestamp; - let transmit_timestamp = reply.transmit_timestamp; - - let rtt: i64 = time_since_midnight - originate_timestamp as i64; - let dl_time: i64 = time_since_midnight - transmit_timestamp as i64; - let ul_time: i64 = receive_timestamp as i64 - originate_timestamp as i64; - - Ok(PingReply { - reflector, - seq: reply.seq, - rtt, - current_time: time_since_midnight, - down_time: dl_time as f64, - up_time: ul_time as f64, - originate_timestamp: originate_timestamp as i64, - receive_timestamp: receive_timestamp as i64, - transmit_timestamp: transmit_timestamp as i64, - last_receive_time_s: Instant::now(), - }) + fn parse_packet(&self, id: u16, reflector: IpAddr, pkt: Icmpv4Packet) -> Result { + match pkt.typ { + // 14 = Timestamp reply + 14 => { + if let Icmpv4Message::TimestampReply { + identifier, + sequence, + originate, + receive, + transmit, + } = pkt.message + { + if identifier != id { + return Err(PingError::WrongID { + expected: id, + found: identifier, + }); } - type_ => Err(PingError::InvalidType(format!("{:?}", type_))), - }, - Some(Icmpv6(slice)) => Err(PingError::InvalidType(format!("{:?}", slice))), - Some(type_) => Err(PingError::InvalidType(format!("{:?}", type_))), - None => Err(PingError::NoTransport), + + let time_now = Time::new(ClockId::Realtime); + let time_since_midnight = time_now.get_time_since_midnight(); + + let rtt: i64 = time_since_midnight - originate as i64; + let dl_time: i64 = time_since_midnight - transmit as i64; + let ul_time: i64 = receive as i64 - originate as i64; + + Ok(PingReply { + reflector, + seq: sequence, + rtt, + current_time: time_since_midnight, + down_time: dl_time as f64, + up_time: ul_time as f64, + originate_timestamp: originate as i64, + receive_timestamp: receive as i64, + transmit_timestamp: transmit as i64, + last_receive_time_s: Instant::now(), + }) + } else { + Err(PingError::InvalidPacket(format!("Packet had type {:?}, but did not match the structure", pkt.typ))) + } + + }, + type_ => Err(PingError::InvalidType(format!("{:?}", type_))), } } } impl PingSender for PingerICMPTimestampSender { - fn craft_packet(&self, id: u16, seq: u16) -> Vec { + fn craft_packet(&self, id: u16, seq: u16) -> Icmpv4Packet { let time_since_midnight = Time::new(ClockId::Realtime).get_time_since_midnight(); - - let payload: [u8; 0] = []; - - // Construct a header with checksum based on the payload - let hdr = Icmpv4Header::with_checksum( - Icmpv4Type::TimestampRequest(TimestampMessage { - id, - seq, - originate_timestamp: time_since_midnight as u32, - receive_timestamp: 0, - transmit_timestamp: 0, - }), - &payload, - ); - - // Create a buffer to hold the result of header + payload - let mut result = Vec::::with_capacity(hdr.header_len()); - - // Write the header to the buffer - hdr.write(&mut result).expect("Error writing packet"); - - result + Icmpv4Packet::with_timestamp_request(id, seq, time_since_midnight as u32, 0, 0).unwrap() } }