refactor(client): replace unsafe libc calls with nix for Linux ancillary data #65
No files matched your search
Generated
+2
-1
@@ -710,7 +710,7 @@ name = "irtt-client"
|
|||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
"libc",
|
"nix 0.31.3",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -927,6 +927,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -13,14 +13,16 @@ categories = ["network-programming"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
irtt-proto.workspace = true
|
irtt-proto.workspace = true
|
||||||
libc = { version = "0.2.189", optional = true }
|
|
||||||
socket2 = { version = "0.6.5", features = ["all"] }
|
socket2 = { version = "0.6.5", features = ["all"] }
|
||||||
thiserror = "2.0.20"
|
thiserror = "2.0.20"
|
||||||
tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"], optional = true}
|
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]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
ancillary = ["dep:libc"]
|
ancillary = ["dep:nix"]
|
||||||
tokio = ["dep:tokio"]
|
tokio = ["dep:tokio"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|||||||
@@ -18,11 +18,7 @@
|
|||||||
//! [`managed`] contains the unified managed task/handle implementation,
|
//! [`managed`] contains the unified managed task/handle implementation,
|
||||||
//! including the dedicated-runtime blocking frontend for synchronous callers.
|
//! including the dedicated-runtime blocking frontend for synchronous callers.
|
||||||
//!
|
//!
|
||||||
#![cfg_attr(
|
#![forbid(unsafe_code)]
|
||||||
not(all(target_os = "linux", feature = "ancillary")),
|
|
||||||
forbid(unsafe_code)
|
|
||||||
)]
|
|
||||||
#![cfg_attr(all(target_os = "linux", feature = "ancillary"), deny(unsafe_code))]
|
|
||||||
|
|
||||||
#[cfg(feature = "tokio")]
|
#[cfg(feature = "tokio")]
|
||||||
mod async_client;
|
mod async_client;
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
#![allow(unsafe_code)]
|
|
||||||
use std::{
|
use std::{
|
||||||
io, mem,
|
io::{self, IoSliceMut},
|
||||||
net::{SocketAddr, UdpSocket},
|
net::{SocketAddr, UdpSocket},
|
||||||
os::fd::{AsRawFd, RawFd},
|
os::fd::{AsRawFd, RawFd},
|
||||||
ptr,
|
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
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};
|
use crate::{metadata::ReceiveMeta, receive::ReceivedDatagram, timing::ClientTimestamp};
|
||||||
|
|
||||||
const CONTROL_LEN: usize = 128;
|
const CONTROL_LEN: usize = 128;
|
||||||
|
|
||||||
pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr) -> io::Result<()> {
|
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);
|
let socket = socket2::SockRef::from(socket);
|
||||||
if remote.is_ipv4() {
|
if remote.is_ipv4() {
|
||||||
socket.set_recv_tos_v4(true)
|
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(
|
pub(crate) fn recv_datagram(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
) -> Result<ReceivedDatagram, io::Error> {
|
) -> 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")]
|
#[cfg(feature = "tokio")]
|
||||||
@@ -55,120 +37,67 @@ pub(crate) fn try_recv_tokio_datagram(
|
|||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
) -> Result<ReceivedDatagram, io::Error> {
|
) -> Result<ReceivedDatagram, io::Error> {
|
||||||
socket.try_io(tokio::io::Interest::READABLE, || {
|
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(
|
fn recv_datagram_fd(
|
||||||
socket_fd: RawFd,
|
socket_fd: RawFd,
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
flags: libc::c_int,
|
flags: MsgFlags,
|
||||||
) -> Result<ReceivedDatagram, io::Error> {
|
) -> Result<ReceivedDatagram, io::Error> {
|
||||||
let mut control = ControlBuffer::new();
|
let mut control = ControlBuffer::new();
|
||||||
let mut iov = libc::iovec {
|
let mut iov = [IoSliceMut::new(buf)];
|
||||||
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 len = unsafe {
|
// The socket is connected, so the source address is not needed; `()` skips
|
||||||
// SAFETY: `msg` points to one writable iovec backed by `buf`, and the
|
// copying it out.
|
||||||
// control buffer is writable and lives until `recvmsg` returns. The
|
let msg = recvmsg::<()>(socket_fd, &mut iov, Some(control.as_mut_slice()), flags)?;
|
||||||
// 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",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let received_at = ClientTimestamp::now();
|
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 {
|
Ok(ReceivedDatagram {
|
||||||
len,
|
len: msg.bytes,
|
||||||
received_at,
|
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 meta = ReceiveMeta::default();
|
||||||
let mut cmsg = libc::CMSG_FIRSTHDR(msg);
|
// A control buffer the kernel had to truncate (`MSG_CTRUNC`) cannot be
|
||||||
while !cmsg.is_null() {
|
// walked reliably, so `cmsgs` refuses to parse it. The datagram itself is
|
||||||
let cmsg_ref = &*cmsg;
|
// still valid; report it without ancillary metadata.
|
||||||
let header_len = libc::CMSG_LEN(0) as usize;
|
let Ok(cmsgs) = msg.cmsgs() else {
|
||||||
if cmsg_ref.cmsg_len < header_len {
|
return meta;
|
||||||
cmsg = libc::CMSG_NXTHDR(msg, cmsg);
|
};
|
||||||
continue;
|
|
||||||
|
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
|
meta
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_traffic_class_cmsg(cmsg: &libc::cmsghdr) -> bool {
|
fn system_time_from_timespec(timespec: TimeSpec) -> Option<SystemTime> {
|
||||||
(cmsg.cmsg_level == libc::IPPROTO_IP && cmsg.cmsg_type == libc::IP_TOS)
|
let seconds = u64::try_from(timespec.tv_sec()).ok()?;
|
||||||
|| (cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_TCLASS)
|
let nanos = u32::try_from(timespec.tv_nsec()).ok()?;
|
||||||
}
|
if nanos >= 1_000_000_000 {
|
||||||
|
|
||||||
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>() {
|
|
||||||
return None;
|
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))
|
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))]
|
#[repr(align(8))]
|
||||||
struct ControlBuffer([u8; CONTROL_LEN]);
|
struct ControlBuffer([u8; CONTROL_LEN]);
|
||||||
|
|
||||||
@@ -177,12 +106,8 @@ impl ControlBuffer {
|
|||||||
Self([0; CONTROL_LEN])
|
Self([0; CONTROL_LEN])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_mut_ptr(&mut self) -> *mut u8 {
|
fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||||
self.0.as_mut_ptr()
|
&mut self.0
|
||||||
}
|
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +119,8 @@ mod tests {
|
|||||||
time::{Duration, UNIX_EPOCH},
|
time::{Duration, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use nix::sys::time::TimeSpec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
event::PacketMeta,
|
event::PacketMeta,
|
||||||
receive::{configure_receive_metadata, recv_datagram},
|
receive::{configure_receive_metadata, recv_datagram},
|
||||||
@@ -412,11 +339,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn timespec_conversion_accepts_valid_unix_timestamp() {
|
fn timespec_conversion_accepts_valid_unix_timestamp() {
|
||||||
let timestamp = super::system_time_from_timespec(libc::timespec {
|
let timestamp = super::system_time_from_timespec(TimeSpec::new(1, 2)).unwrap();
|
||||||
tv_sec: 1,
|
|
||||||
tv_nsec: 2,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
timestamp.duration_since(UNIX_EPOCH).unwrap(),
|
timestamp.duration_since(UNIX_EPOCH).unwrap(),
|
||||||
@@ -426,25 +349,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn timespec_conversion_rejects_negative_or_invalid_values() {
|
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!(
|
assert_eq!(
|
||||||
super::system_time_from_timespec(libc::timespec {
|
super::system_time_from_timespec(TimeSpec::new(0, 1_000_000_000)),
|
||||||
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,
|
|
||||||
}),
|
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user