-
-
Save recatek/b346a7aa9047b12741370dc5b9afd289 to your computer and use it in GitHub Desktop.
Proof of Concept CLOCK_MONOTONIC timestamps on packet recv in Rust on FreeBSD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::io; | |
use std::mem::{self, MaybeUninit}; | |
use std::net::{SocketAddrV4, UdpSocket}; | |
use std::os::fd::AsRawFd; | |
use std::ptr; | |
use std::time::Duration; | |
use libc::{c_int, c_uint}; | |
type Socket = c_int; | |
const SOCK_TRUE: c_int = 1; | |
/// Helper macro to execute a system call that returns an `io::Result`. | |
macro_rules! syscall { | |
($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ | |
#[allow(unused_unsafe)] | |
let res = unsafe { libc::$fn($($arg, )*) }; | |
if res == -1 { | |
Err(std::io::Error::last_os_error()) | |
} else { | |
Ok(res) | |
} | |
}}; | |
} | |
fn get_time_monotonic() -> libc::timespec { | |
let mut c_time: MaybeUninit<libc::timespec> = MaybeUninit::uninit(); | |
syscall!(clock_gettime(libc::CLOCK_MONOTONIC, c_time.as_mut_ptr())).unwrap(); | |
unsafe { c_time.assume_init() } | |
} | |
/// # Safety | |
/// Caller must ensure `T` is the correct type for `opt` and `val`. | |
unsafe fn setsockopt<T>(fd: Socket, opt: c_int, val: c_int, payload: T) -> io::Result<()> { | |
syscall!(setsockopt( | |
fd, | |
opt, | |
val, | |
ptr::addr_of!(payload).cast(), | |
mem::size_of::<T>() as libc::socklen_t, | |
)) | |
.map(|_| ()) | |
} | |
#[rustfmt::skip] | |
fn main() { | |
let message = "butts".as_bytes(); | |
// Set up Rust host and client | |
let host = UdpSocket::bind("0.0.0.0:54345").unwrap(); | |
let client = UdpSocket::bind("0.0.0.0:0").unwrap(); | |
client.connect("127.0.0.1:54345").unwrap(); | |
println!("client on {}", client.local_addr().unwrap()); | |
// Set up OS-specific options | |
let host_raw = host.as_raw_fd(); | |
unsafe { | |
// Set the socket to report timestamps and do so in monotonic time (FreeBSD only!) | |
setsockopt(host_raw, libc::SOL_SOCKET, libc::SO_TIMESTAMP, SOCK_TRUE).unwrap(); | |
setsockopt(host_raw, libc::SOL_SOCKET, libc::SO_TS_CLOCK, libc::SO_TS_MONOTONIC).unwrap(); | |
} | |
// Get the time we sent the message | |
let stime = get_time_monotonic(); | |
// Send the message | |
client.send(&message).unwrap(); | |
// Sleep for a bit | |
std::thread::sleep(Duration::from_millis(100)); | |
// Get the time we fetched the message | |
let ftime = get_time_monotonic(); | |
// Receive the message with the control message info | |
unsafe { | |
// Build a buffer as an I/O Vec to contain the received message | |
let mut data_buffer = [0_u8; 2048]; | |
let data_slice = [std::io::IoSliceMut::new(&mut data_buffer)]; | |
// Build a buffer to contain the control messages | |
debug_assert!(libc::CMSG_SPACE(std::mem::size_of::<libc::timespec>() as c_uint) <= 32); | |
let mut cmsg_buffer = [0_u8; 32]; | |
// See pack_mhdr_to_receive in https://docs.rs/nix/latest/src/nix/sys/socket/mod.rs.html | |
// NOTE: The musl msghdr has private fields, so it must be zero-initialized | |
let mut address = mem::MaybeUninit::<libc::sockaddr_in>::uninit(); | |
let mut msg_hdr = mem::MaybeUninit::<libc::msghdr>::zeroed(); | |
let msg_hdr_ptr = msg_hdr.as_mut_ptr(); | |
(*msg_hdr_ptr).msg_name = address.as_mut_ptr() as *mut libc::c_void; | |
(*msg_hdr_ptr).msg_namelen = mem::size_of::<libc::sockaddr_in>() as libc::socklen_t; | |
(*msg_hdr_ptr).msg_iov = data_slice.as_ref().as_ptr() as *mut libc::iovec; | |
(*msg_hdr_ptr).msg_iovlen = data_slice.len() as i32; | |
(*msg_hdr_ptr).msg_control = cmsg_buffer.as_mut_ptr() as *mut libc::c_void; | |
(*msg_hdr_ptr).msg_controllen = cmsg_buffer.len() as u32; | |
(*msg_hdr_ptr).msg_flags = 0_i32; | |
let mut msg_hdr = msg_hdr.assume_init(); | |
// Perform the recv and get the bytes read | |
let bytes = syscall!(recvmsg(host_raw, &mut msg_hdr, 0)).unwrap(); | |
// We assume the recv syscall properly wrote to the address bytes | |
let address = address.assume_init().clone(); | |
let ip = std::net::Ipv4Addr::from(u32::from_be(address.sin_addr.s_addr)); | |
let port = u16::from_be(address.sin_port); | |
let addr = SocketAddrV4::new(ip, port); | |
// Get the first (and only expected) control message header | |
assert!(msg_hdr.msg_controllen > 0); // TODO: Fallback | |
debug_assert!(msg_hdr.msg_control.is_null() == false); | |
debug_assert!(msg_hdr.msg_controllen as usize <= cmsg_buffer.len()); | |
let cmsghdr = libc::CMSG_FIRSTHDR(&msg_hdr); | |
// Read the timestamp from the control message | |
debug_assert!(cmsghdr.is_null() == false); | |
let cmsg_data = libc::CMSG_DATA(cmsghdr); | |
let cmsg_len = (cmsghdr as usize + (*cmsghdr).cmsg_len as usize) - (cmsg_data as usize); | |
debug_assert!((*cmsghdr).cmsg_level == libc::SOL_SOCKET); | |
debug_assert!((*cmsghdr).cmsg_type == 6); // SCM_MONOTONIC isn't in libc :( | |
debug_assert!(cmsg_len == 16); | |
let rtime: libc::timespec = ptr::read_unaligned(cmsg_data as *const _); | |
// Make sure we nave no remaining headers | |
let next = libc::CMSG_NXTHDR(&msg_hdr, cmsghdr); | |
debug_assert!(next.is_null()); | |
// Compute the various time durations | |
let sduration = Duration::new(stime.tv_sec as u64, stime.tv_nsec as u32); | |
let rduration = Duration::new(rtime.tv_sec as u64, rtime.tv_nsec as u32); | |
let fduration = Duration::new(ftime.tv_sec as u64, ftime.tv_nsec as u32); | |
println!( | |
"recv \"{}\" ({} bytes) from {}", | |
std::str::from_utf8(&data_buffer).unwrap(), | |
bytes, | |
addr | |
); | |
println!( | |
"send->recv {} {} {}", | |
sduration.as_millis(), | |
rduration.as_millis(), | |
(rduration - sduration).as_millis() | |
); | |
println!( | |
"send->fetch {} {} {}", | |
sduration.as_millis(), | |
fduration.as_millis(), | |
(fduration - sduration).as_millis() | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment