feat(server): capture kernel receive timestamps

This commit is contained in:
Nils Andreas Svee committed 2026-08-15 13:13:07 +07:00
1 parent c80edb460b
commit 1b0fc2cabe
4 files changed
+603 -49

No files matched your search

+222 -13
View File
@@ -16,8 +16,29 @@
//! have one reachable through a safe API. Everything here goes through `nix`'s
//! safe `recvmsg`/`sendmsg` wrappers; the crate forbids unsafe code, so there is
//! no raw syscall, no `CMSG_*` walking and no hand-written ABI layer.
//!
//! # Kernel receive timestamps
//!
//! A Linux listener additionally asks the kernel for a software receive
//! timestamp (`SO_TIMESTAMPNS`) on every datagram, wildcard or not, and reports
//! it on [`ReceivedDatagram::kernel_rx_timestamp`]. It says when the kernel
//! received the datagram, before the socket queue, Tokio readiness and
//! scheduling that separate that moment from the one the server would otherwise
//! observe.
//!
//! It is the exact opposite of destination metadata in how much it matters.
//! Enabling it is best-effort and never keeps a listener from starting; a
//! datagram that arrives without one, or with one describing no representable
//! instant, is served normally with no timestamp. Nothing about which datagrams
//! reach [`ServerCore`](crate::ServerCore) depends on it.
//!
//! **Nothing consumes it yet.** It is captured here so a follow-up change can
//! decide how a kernel-observed arrival time enters the protocol core; today
//! the core still samples its own receive instant and no protocol timing is
//! affected. It is a software wall-clock reading only — not a NIC hardware
//! timestamp, not monotonic, and not a transmit timestamp.
use std::{io, net::SocketAddr};
use std::{io, net::SocketAddr, time::SystemTime};
use tokio::net::UdpSocket;
@@ -59,19 +80,32 @@ pub(crate) enum ReplySource {
},
}
/// One received datagram and what answering it requires.
/// One received datagram, what answering it requires, and what the kernel
/// observed about its arrival.
#[derive(Debug, Clone, Copy)]
pub(crate) struct ReceivedDatagram {
pub(crate) len: usize,
pub(crate) peer: SocketAddr,
pub(crate) reply_source: ReplySource,
/// When the kernel received this datagram, where it said so.
///
/// Optional accuracy metadata, absent on every target but Linux, on a
/// Linux listener whose kernel refused the option, and on any datagram
/// whose timestamp did not arrive or did not describe a representable
/// instant. A wall-clock reading with no monotonic counterpart: none is
/// synthesized, because there is no sound mapping from one to the other.
///
/// Captured for a follow-up measurement change and consumed by nothing
/// today — see the module documentation.
pub(crate) kernel_rx_timestamp: Option<SystemTime>,
}
/// Asks the kernel to report each datagram's local destination address.
///
/// Called once, at construction, and only for a wildcard listener: an
/// explicit-address listener needs no ancillary metadata, and adding it would
/// widen the platform surface of the common path for nothing.
/// explicit-address listener already replies from the one address it is bound
/// to, and asking for a destination it cannot use would widen the platform
/// surface of the common path for nothing.
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
pub(crate) fn configure_destination_metadata(socket: &UdpSocket, is_ipv4: bool) -> io::Result<()> {
ancillary::configure_destination_metadata(socket, is_ipv4)
@@ -85,6 +119,27 @@ pub(crate) fn configure_destination_metadata(
Err(unsupported())
}
/// Asks the kernel to report when it received each datagram, and reports
/// whether it agreed.
///
/// Called once, at construction, for every listener on a target that has this:
/// unlike destination metadata it costs an explicit listener nothing to have,
/// and both kinds of listener are equally worth timing accurately.
///
/// Deliberately not fallible. A listener without kernel timestamps serves every
/// request exactly as it otherwise would, so a refusal here is not a reason to
/// fail construction, and the returned flag exists to be asserted in tests
/// rather than acted on. The datagrams simply carry no timestamp.
#[cfg(target_os = "linux")]
pub(crate) fn try_configure_kernel_rx_timestamp(socket: &UdpSocket) -> bool {
ancillary::try_configure_kernel_rx_timestamp(socket)
}
#[cfg(not(target_os = "linux"))]
pub(crate) fn try_configure_kernel_rx_timestamp(_socket: &UdpSocket) -> bool {
false
}
/// Receives one datagram, reporting where its reply must come from.
///
/// `select_reply_source` is the listener's own wildcard-ness, decided once at
@@ -97,7 +152,7 @@ pub(crate) fn configure_destination_metadata(
/// impossible. `Ok(None)` covers:
///
/// - payload truncation reported by an ancillary receive (`MSG_TRUNC`);
/// - ancillary metadata truncation (`MSG_CTRUNC`);
/// - ancillary metadata truncation (`MSG_CTRUNC`) on a wildcard listener;
/// - a wildcard listener's missing or unusable local destination;
/// - a datagram filling `buffer` exactly, which is conservatively treated as
/// potentially truncated.
@@ -105,8 +160,13 @@ pub(crate) fn configure_destination_metadata(
/// That last rule exists because `recv_from` reports no truncation flag, so an
/// exactly-full result cannot be told apart from a larger datagram cut down to
/// the supplied capacity. It is expressed against `buffer.len()`, so the policy
/// follows whatever receive capacity the caller supplies, and it applies to both
/// receive paths alike.
/// follows whatever receive capacity the caller supplies, and it applies to
/// every receive path alike.
///
/// A missing or unusable *kernel receive timestamp* is on none of those lists.
/// It costs precision, not correctness, so the datagram is accepted without one
/// — including on an explicit listener whose control data was truncated, where
/// the timestamp is all that can have been lost.
///
/// Cancel-safe: nothing is consumed until a datagram is fully received.
pub(crate) async fn receive(
@@ -118,17 +178,42 @@ pub(crate) async fn receive(
let received = if select_reply_source {
receive_with_destination(socket, buffer).await?
} else {
let (len, peer) = socket.recv_from(buffer).await?;
Some(ReceivedDatagram {
len,
peer,
reply_source: ReplySource::Bound,
})
receive_bound(socket, buffer).await?
};
Ok(received.filter(|datagram| datagram.len < capacity))
}
/// The explicit-address receive, which needs no ancillary data to answer
/// correctly.
///
/// Linux takes it through `recvmsg` anyway, purely so an enabled
/// `SCM_TIMESTAMPNS` can be read off the same call — `recv_from` cannot retrieve
/// control messages. Elsewhere there is no timestamp to collect, so the plain
/// path stays plain rather than widening the common path's platform surface for
/// nothing.
#[cfg(target_os = "linux")]
async fn receive_bound(
socket: &UdpSocket,
buffer: &mut [u8],
) -> io::Result<Option<ReceivedDatagram>> {
ancillary::receive_bound(socket, buffer).await
}
#[cfg(not(target_os = "linux"))]
async fn receive_bound(
socket: &UdpSocket,
buffer: &mut [u8],
) -> io::Result<Option<ReceivedDatagram>> {
let (len, peer) = socket.recv_from(buffer).await?;
Ok(Some(ReceivedDatagram {
len,
peer,
reply_source: ReplySource::Bound,
kernel_rx_timestamp: None,
}))
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
async fn receive_with_destination(
socket: &UdpSocket,
@@ -232,4 +317,128 @@ mod tests {
let (received, _) = receive_one(4, b"irtt-rs").await;
assert!(!matches!(received, Ok(Some(_))));
}
/// A listener that never asked for kernel timestamps receives normally and
/// reports none — the field is optional in the plainest sense, and its
/// absence is not a defect to be worked around later.
#[tokio::test(flavor = "current_thread")]
async fn a_datagram_with_no_kernel_timestamp_is_served_all_the_same() {
let (received, _) = receive_one(16, b"irtt").await;
let received = received.unwrap().expect("an untimestamped datagram serves");
assert_eq!(received.reply_source, ReplySource::Bound);
assert_eq!(received.kernel_rx_timestamp, None);
}
/// Linux-only coverage of the two receive paths, which is where the
/// timestamp actually comes from.
#[cfg(target_os = "linux")]
mod linux {
use std::net::SocketAddr;
use super::*;
/// Sends `payload` from loopback to `listener` and receives it once.
async fn exchange(
listener: &UdpSocket,
select_reply_source: bool,
) -> (Option<ReceivedDatagram>, Vec<u8>) {
let payload = b"irtt";
let destination =
SocketAddr::from((Ipv4Addr::LOCALHOST, listener.local_addr().unwrap().port()));
let sender = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
sender.send_to(payload, destination).await.unwrap();
let mut buffer = vec![0; 64];
let received = receive(listener, &mut buffer, select_reply_source)
.await
.unwrap();
(received, buffer)
}
/// Reports the timestamp, or says why it is skipping the assertion
/// rather than inventing one. Loopback under a normal kernel supplies
/// it; a container or emulator that does not must not fail the run.
fn timestamp_or_skip(received: &ReceivedDatagram, what: &str) {
match received.kernel_rx_timestamp {
Some(timestamp) => assert!(
timestamp.duration_since(std::time::UNIX_EPOCH).is_ok(),
"a kernel receive timestamp is a wall-clock instant"
),
None => eprintln!(
"skipping {what} kernel timestamp assertion: \
kernel did not provide SCM_TIMESTAMPNS"
),
}
}
/// The explicit-address listener's new `recvmsg` path: it must keep
/// reporting exactly what `recv_from` did, and additionally see the
/// timestamp `recv_from` had no way to retrieve.
#[tokio::test(flavor = "current_thread")]
async fn an_explicit_listener_receives_a_datagram_and_its_kernel_timestamp() {
let listener = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
assert!(
try_configure_kernel_rx_timestamp(&listener),
"Linux loopback accepts SO_TIMESTAMPNS"
);
let (received, buffer) = exchange(&listener, false).await;
let received = received.expect("an explicit listener serves a short datagram");
assert_eq!(received.len, 4);
assert_eq!(&buffer[..received.len], b"irtt");
assert_eq!(received.peer.ip(), Ipv4Addr::LOCALHOST);
assert_eq!(received.reply_source, ReplySource::Bound);
timestamp_or_skip(&received, "explicit");
}
/// The regression this whole change risks: asking for a second control
/// message must not crowd out the first. A wildcard listener with both
/// enabled has to recover its destination *and* its timestamp from one
/// datagram, with no `MSG_CTRUNC` drop and no dependence on the order
/// the kernel returns them in.
#[tokio::test(flavor = "current_thread")]
async fn a_wildcard_listener_recovers_its_destination_and_timestamp_together() {
let listener = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await.unwrap();
configure_destination_metadata(&listener, true).unwrap();
assert!(try_configure_kernel_rx_timestamp(&listener));
let (received, buffer) = exchange(&listener, true).await;
let received = received.expect(
"a wildcard listener asking for two control messages still serves the datagram",
);
assert_eq!(received.len, 4);
assert_eq!(&buffer[..received.len], b"irtt");
assert_eq!(received.peer.ip(), Ipv4Addr::LOCALHOST);
assert_eq!(
received.reply_source,
ReplySource::V4 {
address: Ipv4Addr::LOCALHOST
},
"the required destination survives the added timestamp request"
);
timestamp_or_skip(&received, "wildcard");
}
/// The timestamp is not a precondition for either listener: a wildcard
/// one that never enabled it still recovers the destination its reply
/// depends on and serves the request.
#[tokio::test(flavor = "current_thread")]
async fn a_wildcard_listener_without_timestamps_still_recovers_its_destination() {
let listener = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await.unwrap();
configure_destination_metadata(&listener, true).unwrap();
let (received, _) = exchange(&listener, true).await;
let received = received.expect("no timestamp requested, and none needed");
assert_eq!(
received.reply_source,
ReplySource::V4 {
address: Ipv4Addr::LOCALHOST
}
);
assert_eq!(received.kernel_rx_timestamp, None);
}
}
}