Skip to content

Instantly share code, notes, and snippets.

@recatek
Created March 23, 2023 21:18
Show Gist options
  • Save recatek/b346a7aa9047b12741370dc5b9afd289 to your computer and use it in GitHub Desktop.
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
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