refactor(client): replace unsafe libc calls with nix for Linux ancillary data

Eliminate unsafe code blocks and manual pointer arithmetic in Linux-specific receive paths by utilizing nix for socket options and control message parsing. This improves safety and maintainability while preserving the existing kernel RX timestamp and traffic class functionality.
This commit is contained in:
Nils Andreas Svee committed 2026-08-11 13:38:56 +07:00
1 parent 3c6c0e396f
commit 373bf32d0f
4 files changed
+56 -149

No files matched your search

Generated
+2 -1
View File
@@ -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]]
+4 -2
View File
@@ -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]
+1 -5
View File
@@ -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;
+49 -141
View File
@@ -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<ReceivedDatagram, io::Error> {
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<ReceivedDatagram, io::Error> {
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<ReceivedDatagram, io::Error> {
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<S>(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);
}
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);
ControlMessageOwned::Ipv4Tos(tos) => {
meta.traffic_class = Some(tos);
}
ControlMessageOwned::Ipv6TClass(traffic_class) => {
meta.traffic_class = u8::try_from(traffic_class).ok();
}
_ => {}
}
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<u8> {
if data_len >= mem::size_of::<u8>() {
Some(*libc::CMSG_DATA(cmsg).cast::<u8>())
} else {
None
}
}
unsafe fn read_int_cmsg_low_byte(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option<u8> {
if data_len >= mem::size_of::<libc::c_int>() {
let value = ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast::<libc::c_int>());
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<SystemTime> {
if data_len < mem::size_of::<libc::timespec>() {
fn system_time_from_timespec(timespec: TimeSpec) -> Option<SystemTime> {
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::<libc::timespec>());
system_time_from_timespec(timespec)
}
fn system_time_from_timespec(timespec: libc::timespec) -> Option<SystemTime> {
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
);
}