diff --git a/Cargo.lock b/Cargo.lock index 6c1fcdf..07026f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,7 +710,7 @@ name = "irtt-client" version = "0.4.0" dependencies = [ "irtt-proto", - "libc", + "nix 0.31.3", "socket2", "thiserror 2.0.20", "tokio", @@ -927,6 +927,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] diff --git a/crates/irtt-client/Cargo.toml b/crates/irtt-client/Cargo.toml index 1ee984f..2b5f177 100644 --- a/crates/irtt-client/Cargo.toml +++ b/crates/irtt-client/Cargo.toml @@ -13,14 +13,16 @@ categories = ["network-programming"] [dependencies] irtt-proto.workspace = true -libc = { version = "0.2.189", optional = true } socket2 = { version = "0.6.5", features = ["all"] } thiserror = "2.0.20" tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"], optional = true} +[target.'cfg(target_os = "linux")'.dependencies] +nix = { version = "0.31", default-features = false, features = ["net", "uio"], optional = true } + [features] default = [] -ancillary = ["dep:libc"] +ancillary = ["dep:nix"] tokio = ["dep:tokio"] [package.metadata.docs.rs] diff --git a/crates/irtt-client/src/lib.rs b/crates/irtt-client/src/lib.rs index 13e1c17..58471f8 100644 --- a/crates/irtt-client/src/lib.rs +++ b/crates/irtt-client/src/lib.rs @@ -18,11 +18,7 @@ //! [`managed`] contains the unified managed task/handle implementation, //! including the dedicated-runtime blocking frontend for synchronous callers. //! -#![cfg_attr( - not(all(target_os = "linux", feature = "ancillary")), - forbid(unsafe_code) -)] -#![cfg_attr(all(target_os = "linux", feature = "ancillary"), deny(unsafe_code))] +#![forbid(unsafe_code)] #[cfg(feature = "tokio")] mod async_client; diff --git a/crates/irtt-client/src/receive/linux.rs b/crates/irtt-client/src/receive/linux.rs index dd3384b..98294b0 100644 --- a/crates/irtt-client/src/receive/linux.rs +++ b/crates/irtt-client/src/receive/linux.rs @@ -1,18 +1,21 @@ -#![allow(unsafe_code)] use std::{ - io, mem, + io::{self, IoSliceMut}, net::{SocketAddr, UdpSocket}, os::fd::{AsRawFd, RawFd}, - ptr, time::{Duration, SystemTime, UNIX_EPOCH}, }; +use nix::sys::{ + socket::{recvmsg, setsockopt, sockopt, ControlMessageOwned, MsgFlags, RecvMsg}, + time::TimeSpec, +}; + use crate::{metadata::ReceiveMeta, receive::ReceivedDatagram, timing::ClientTimestamp}; const CONTROL_LEN: usize = 128; pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr) -> io::Result<()> { - enable_kernel_rx_timestamps(socket)?; + setsockopt(socket, sockopt::ReceiveTimestampns, &true)?; let socket = socket2::SockRef::from(socket); if remote.is_ipv4() { socket.set_recv_tos_v4(true) @@ -21,32 +24,11 @@ pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr) } } -fn enable_kernel_rx_timestamps(socket: &UdpSocket) -> io::Result<()> { - let enabled: libc::c_int = 1; - let result = unsafe { - // SAFETY: The file descriptor is borrowed from a valid `UdpSocket`. - // `enabled` is a properly aligned `c_int`, and the length matches the - // pointed-to value for the `SO_TIMESTAMPNS` boolean socket option. - libc::setsockopt( - socket.as_raw_fd(), - libc::SOL_SOCKET, - libc::SO_TIMESTAMPNS, - (&enabled as *const libc::c_int).cast(), - mem::size_of_val(&enabled) as libc::socklen_t, - ) - }; - if result == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } -} - pub(crate) fn recv_datagram( socket: &UdpSocket, buf: &mut [u8], ) -> Result { - recv_datagram_fd(socket.as_raw_fd(), buf, 0) + recv_datagram_fd(socket.as_raw_fd(), buf, MsgFlags::empty()) } #[cfg(feature = "tokio")] @@ -55,120 +37,67 @@ pub(crate) fn try_recv_tokio_datagram( buf: &mut [u8], ) -> Result { socket.try_io(tokio::io::Interest::READABLE, || { - recv_datagram_fd(socket.as_raw_fd(), buf, libc::MSG_DONTWAIT) + recv_datagram_fd(socket.as_raw_fd(), buf, MsgFlags::MSG_DONTWAIT) }) } fn recv_datagram_fd( socket_fd: RawFd, buf: &mut [u8], - flags: libc::c_int, + flags: MsgFlags, ) -> Result { let mut control = ControlBuffer::new(); - let mut iov = libc::iovec { - iov_base: buf.as_mut_ptr().cast(), - iov_len: buf.len(), - }; - let mut msg: libc::msghdr = unsafe { mem::zeroed() }; - msg.msg_iov = &mut iov; - msg.msg_iovlen = 1; - msg.msg_control = control.as_mut_ptr().cast(); - msg.msg_controllen = control.len(); + let mut iov = [IoSliceMut::new(buf)]; - let len = unsafe { - // SAFETY: `msg` points to one writable iovec backed by `buf`, and the - // control buffer is writable and lives until `recvmsg` returns. The - // socket file descriptor is borrowed from a valid UDP socket. - libc::recvmsg(socket_fd, &mut msg, flags) - }; - if len < 0 { - return Err(io::Error::last_os_error()); - } - let len = usize::try_from(len).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "recvmsg returned an unrepresentable datagram length", - ) - })?; + // The socket is connected, so the source address is not needed; `()` skips + // copying it out. + let msg = recvmsg::<()>(socket_fd, &mut iov, Some(control.as_mut_slice()), flags)?; let received_at = ClientTimestamp::now(); - let meta = unsafe { - // SAFETY: `msg` was initialized by a successful `recvmsg` call. The - // parser only reads cmsghdr entries within `msg_controllen` and ignores - // short or unrelated control messages. - parse_receive_meta(&msg) - }; Ok(ReceivedDatagram { - len, + len: msg.bytes, received_at, - meta, + meta: receive_meta(&msg), }) } -unsafe fn parse_receive_meta(msg: &libc::msghdr) -> ReceiveMeta { +fn receive_meta(msg: &RecvMsg<'_, '_, S>) -> ReceiveMeta { let mut meta = ReceiveMeta::default(); - let mut cmsg = libc::CMSG_FIRSTHDR(msg); - while !cmsg.is_null() { - let cmsg_ref = &*cmsg; - let header_len = libc::CMSG_LEN(0) as usize; - if cmsg_ref.cmsg_len < header_len { - cmsg = libc::CMSG_NXTHDR(msg, cmsg); - continue; + // A control buffer the kernel had to truncate (`MSG_CTRUNC`) cannot be + // walked reliably, so `cmsgs` refuses to parse it. The datagram itself is + // still valid; report it without ancillary metadata. + let Ok(cmsgs) = msg.cmsgs() else { + return meta; + }; + + for cmsg in cmsgs { + match cmsg { + ControlMessageOwned::ScmTimestampns(timestamp) => { + meta.kernel_rx_timestamp = system_time_from_timespec(timestamp); + } + ControlMessageOwned::Ipv4Tos(tos) => { + meta.traffic_class = Some(tos); + } + ControlMessageOwned::Ipv6TClass(traffic_class) => { + meta.traffic_class = u8::try_from(traffic_class).ok(); + } + _ => {} } - let data_len = cmsg_ref.cmsg_len - header_len; - if is_traffic_class_cmsg(cmsg_ref) { - meta.traffic_class = read_int_cmsg_low_byte(cmsg, data_len); - } else if is_kernel_rx_timestamp_cmsg(cmsg_ref) { - meta.kernel_rx_timestamp = read_timespec_cmsg(cmsg, data_len); - } - cmsg = libc::CMSG_NXTHDR(msg, cmsg); } meta } -fn is_traffic_class_cmsg(cmsg: &libc::cmsghdr) -> bool { - (cmsg.cmsg_level == libc::IPPROTO_IP && cmsg.cmsg_type == libc::IP_TOS) - || (cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_TCLASS) -} - -fn is_kernel_rx_timestamp_cmsg(cmsg: &libc::cmsghdr) -> bool { - cmsg.cmsg_level == libc::SOL_SOCKET && cmsg.cmsg_type == libc::SCM_TIMESTAMPNS -} - -unsafe fn read_u8_cmsg(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option { - if data_len >= mem::size_of::() { - Some(*libc::CMSG_DATA(cmsg).cast::()) - } else { - None - } -} - -unsafe fn read_int_cmsg_low_byte(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option { - if data_len >= mem::size_of::() { - let value = ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast::()); - Some((value & 0xff) as u8) - } else { - read_u8_cmsg(cmsg, data_len) - } -} - -unsafe fn read_timespec_cmsg(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option { - if data_len < mem::size_of::() { +fn system_time_from_timespec(timespec: TimeSpec) -> Option { + let seconds = u64::try_from(timespec.tv_sec()).ok()?; + let nanos = u32::try_from(timespec.tv_nsec()).ok()?; + if nanos >= 1_000_000_000 { return None; } - let timespec = ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast::()); - system_time_from_timespec(timespec) -} - -fn system_time_from_timespec(timespec: libc::timespec) -> Option { - if timespec.tv_sec < 0 || timespec.tv_nsec < 0 || timespec.tv_nsec >= 1_000_000_000 { - return None; - } - let seconds = u64::try_from(timespec.tv_sec).ok()?; - let nanos = u32::try_from(timespec.tv_nsec).ok()?; UNIX_EPOCH.checked_add(Duration::new(seconds, nanos)) } +/// Reusable per-receive control buffer. `recvmsg` control data must satisfy +/// `cmsghdr` alignment, which 8-byte alignment covers on supported targets. #[repr(align(8))] struct ControlBuffer([u8; CONTROL_LEN]); @@ -177,12 +106,8 @@ impl ControlBuffer { Self([0; CONTROL_LEN]) } - fn as_mut_ptr(&mut self) -> *mut u8 { - self.0.as_mut_ptr() - } - - fn len(&self) -> usize { - self.0.len() + fn as_mut_slice(&mut self) -> &mut [u8] { + &mut self.0 } } @@ -194,6 +119,8 @@ mod tests { time::{Duration, UNIX_EPOCH}, }; + use nix::sys::time::TimeSpec; + use crate::{ event::PacketMeta, receive::{configure_receive_metadata, recv_datagram}, @@ -412,11 +339,7 @@ mod tests { #[test] fn timespec_conversion_accepts_valid_unix_timestamp() { - let timestamp = super::system_time_from_timespec(libc::timespec { - tv_sec: 1, - tv_nsec: 2, - }) - .unwrap(); + let timestamp = super::system_time_from_timespec(TimeSpec::new(1, 2)).unwrap(); assert_eq!( timestamp.duration_since(UNIX_EPOCH).unwrap(), @@ -426,25 +349,10 @@ mod tests { #[test] fn timespec_conversion_rejects_negative_or_invalid_values() { + assert_eq!(super::system_time_from_timespec(TimeSpec::new(-1, 0)), None); + assert_eq!(super::system_time_from_timespec(TimeSpec::new(0, -1)), None); assert_eq!( - super::system_time_from_timespec(libc::timespec { - tv_sec: -1, - tv_nsec: 0, - }), - None - ); - assert_eq!( - super::system_time_from_timespec(libc::timespec { - tv_sec: 0, - tv_nsec: -1, - }), - None - ); - assert_eq!( - super::system_time_from_timespec(libc::timespec { - tv_sec: 0, - tv_nsec: 1_000_000_000, - }), + super::system_time_from_timespec(TimeSpec::new(0, 1_000_000_000)), None ); }