diff --git a/quinn-udp/src/cmsg/mod.rs b/quinn-udp/src/cmsg/mod.rs index 4a1c90e222..5da1514a60 100644 --- a/quinn-udp/src/cmsg/mod.rs +++ b/quinn-udp/src/cmsg/mod.rs @@ -83,7 +83,11 @@ impl Drop for Encoder<'_, M> { /// `cmsg` must refer to a native cmsg containing a payload of type `T` pub(crate) unsafe fn decode(cmsg: &impl CMsgHdr) -> T { assert!(mem::align_of::() <= mem::align_of::()); - debug_assert_eq!(cmsg.len(), C::cmsg_len(mem::size_of::())); + debug_assert_eq!( + cmsg.len(), + C::cmsg_len(mem::size_of::()), + "cmsg truncated -- you might need to raise the CMSG_LEN constant for this platform" + ); ptr::read(cmsg.cmsg_data() as *const T) } diff --git a/quinn-udp/src/fallback.rs b/quinn-udp/src/fallback.rs index b35ab343be..5e34647bc7 100644 --- a/quinn-udp/src/fallback.rs +++ b/quinn-udp/src/fallback.rs @@ -72,6 +72,8 @@ impl UdpSocketState { addr: addr.as_socket().unwrap(), ecn: None, dst_ip: None, + #[cfg(not(wasm_browser))] + timestamp: None, }; Ok(1) } diff --git a/quinn-udp/src/lib.rs b/quinn-udp/src/lib.rs index 0f69070f6d..d468accc97 100644 --- a/quinn-udp/src/lib.rs +++ b/quinn-udp/src/lib.rs @@ -115,6 +115,13 @@ pub struct RecvMeta { /// Populated on platforms: Windows, Linux, Android (API level > 25), /// FreeBSD, OpenBSD, NetBSD, macOS, and iOS. pub dst_ip: Option, + /// A timestamp for when the given packet was received by the operating system. + /// Controlled by the `set_recv_timestamping` option on the source socket where available. + /// + /// Populated on platforms with varying clock sources, as follows: + /// - Linux: `CLOCK_REALTIME` (see `SO_TIMESTAMPNS` in `man 7 socket` for more information) + #[cfg(not(wasm_browser))] + pub timestamp: Option, } impl Default for RecvMeta { @@ -126,6 +133,8 @@ impl Default for RecvMeta { stride: 0, ecn: None, dst_ip: None, + #[cfg(not(wasm_browser))] + timestamp: None, } } } diff --git a/quinn-udp/src/unix.rs b/quinn-udp/src/unix.rs index 0dce8aad90..ecc4b43d87 100644 --- a/quinn-udp/src/unix.rs +++ b/quinn-udp/src/unix.rs @@ -12,6 +12,9 @@ use std::{ time::Instant, }; +#[cfg(target_os = "linux")] +use std::time::Duration; + use socket2::SockRef; use super::{ @@ -96,6 +99,11 @@ impl UdpSocketState { unsafe { libc::CMSG_SPACE(mem::size_of::() as _) as usize }; } + if cfg!(target_os = "linux") { + cmsg_platform_space += + unsafe { libc::CMSG_SPACE(mem::size_of::() as _) as usize }; + } + assert!( CMSG_LEN >= unsafe { libc::CMSG_SPACE(mem::size_of::() as _) as usize } @@ -257,6 +265,18 @@ impl UdpSocketState { self.may_fragment } + /// Sets the socket to receive packet receipt timestamps from the operating system. + /// These can be accessed via [`RecvMeta::timestamp`] on packets when enabled. + #[cfg(target_os = "linux")] + pub fn set_recv_timestamping(&self, sock: UdpSockRef<'_>, enabled: bool) -> io::Result<()> { + let enabled = match enabled { + true => OPTION_ON, + false => OPTION_OFF, + }; + + set_socket_option(&*sock.0, libc::SOL_SOCKET, libc::SO_TIMESTAMPNS, enabled) + } + /// Returns true if we previously got an EINVAL error from `sendmsg` syscall. fn sendmsg_einval(&self) -> bool { self.sendmsg_einval.load(Ordering::Relaxed) @@ -543,7 +563,7 @@ fn recv(io: SockRef<'_>, bufs: &mut [IoSliceMut<'_>], meta: &mut [RecvMeta]) -> Ok(1) } -const CMSG_LEN: usize = 88; +const CMSG_LEN: usize = 96; fn prepare_msg( transmit: &Transmit<'_>, @@ -681,6 +701,8 @@ fn decode_recv( let mut dst_ip = None; #[allow(unused_mut)] // only mutable on Linux let mut stride = len; + #[allow(unused_mut)] // only mutable on Linux + let mut timestamp = None; let cmsg_iter = unsafe { cmsg::Iter::new(hdr) }; for cmsg in cmsg_iter { @@ -725,6 +747,11 @@ fn decode_recv( (libc::SOL_UDP, gro::UDP_GRO) => unsafe { stride = cmsg::decode::(cmsg) as usize; }, + #[cfg(target_os = "linux")] + (libc::SOL_SOCKET, libc::SCM_TIMESTAMPNS) => { + let tv = unsafe { cmsg::decode::(cmsg) }; + timestamp = Some(Duration::new(tv.tv_sec as u64, tv.tv_nsec as u32)); + } _ => {} } } @@ -759,6 +786,7 @@ fn decode_recv( addr, ecn: EcnCodepoint::from_bits(ecn_bits), dst_ip, + timestamp, } } @@ -907,6 +935,8 @@ fn set_socket_option( } } +#[allow(dead_code)] +const OPTION_OFF: libc::c_int = 0; const OPTION_ON: libc::c_int = 1; #[cfg(not(any(target_os = "linux", target_os = "android")))] diff --git a/quinn-udp/src/windows.rs b/quinn-udp/src/windows.rs index 81b28e1358..946962eed0 100644 --- a/quinn-udp/src/windows.rs +++ b/quinn-udp/src/windows.rs @@ -268,6 +268,7 @@ impl UdpSocketState { addr: addr.unwrap(), ecn: EcnCodepoint::from_bits(ecn_bits as u8), dst_ip, + timestamp: None, }; Ok(1) } diff --git a/quinn-udp/tests/tests.rs b/quinn-udp/tests/tests.rs index 25de5cfc5b..9cfd172067 100644 --- a/quinn-udp/tests/tests.rs +++ b/quinn-udp/tests/tests.rs @@ -5,6 +5,8 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, UdpSocket}, slice, }; +#[cfg(target_os = "linux")] +use std::{mem::MaybeUninit, time::Duration}; use quinn_udp::{EcnCodepoint, RecvMeta, Transmit, UdpSocketState}; use socket2::Socket; @@ -186,6 +188,72 @@ fn gso() { ); } +#[test] +#[cfg(target_os = "linux")] +fn receive_timestamp() { + let send = UdpSocket::bind((Ipv6Addr::LOCALHOST, 0)) + .or_else(|_| UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))) + .unwrap(); + let recv = UdpSocket::bind((Ipv6Addr::LOCALHOST, 0)) + .or_else(|_| UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))) + .unwrap(); + let dst_addr = recv.local_addr().unwrap(); + + let send_state = UdpSocketState::new((&send).into()).unwrap(); + let recv_state = UdpSocketState::new((&recv).into()).unwrap(); + + recv_state + .set_recv_timestamping((&recv).into(), true) + .expect("failed to set sockopt -- unsupported?"); + + // Reverse non-blocking flag set by `UdpSocketState` to make the test non-racy + recv.set_nonblocking(false).unwrap(); + + let mut buf = [0; u16::MAX as usize]; + let mut meta = RecvMeta::default(); + + let mut time_start = MaybeUninit::::uninit(); + let mut time_end = MaybeUninit::::uninit(); + + // Use the actual CLOCK_REALTIME clock source in linux, rather than relying on SystemTime + let errno = unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, time_start.as_mut_ptr()) }; + assert_eq!(errno, 0, "failed to read CLOCK_REALTIME"); + let time_start = unsafe { time_start.assume_init() }; + let time_start = Duration::new(time_start.tv_sec as u64, time_start.tv_nsec as u32); + + send_state + .try_send( + (&send).into(), + &Transmit { + destination: dst_addr, + ecn: None, + contents: b"hello", + segment_size: None, + src_ip: None, + }, + ) + .unwrap(); + + recv_state + .recv( + (&recv).into(), + &mut [IoSliceMut::new(&mut buf)], + slice::from_mut(&mut meta), + ) + .unwrap(); + + let errno = unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, time_end.as_mut_ptr()) }; + assert_eq!(errno, 0, "failed to read CLOCK_REALTIME"); + let time_end = unsafe { time_end.assume_init() }; + let time_end = Duration::new(time_end.tv_sec as u64, time_end.tv_nsec as u32); + + // Note: there's a very slim chance that the clock could jump (via ntp, etc.) and throw off + // these two checks, but it's still better to validate the timestamp result with that risk + let timestamp = meta.timestamp.unwrap(); + assert!(time_start <= timestamp); + assert!(timestamp <= time_end); +} + fn test_send_recv(send: &Socket, recv: &Socket, transmit: Transmit) { let send_state = UdpSocketState::new(send.into()).unwrap(); let recv_state = UdpSocketState::new(recv.into()).unwrap();