Skip to main content

rtime_net/
udp.rs

1use std::net::SocketAddr;
2
3use rtime_core::timestamp::NtpTimestamp;
4use tokio::net::UdpSocket;
5
6/// Timestamping mode currently active on the socket.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum TimestampMode {
9    /// Timestamps are taken in userspace via `NtpTimestamp::now()`.
10    Userspace,
11    /// Kernel software timestamps via `SO_TIMESTAMPNS` or `SO_TIMESTAMPING`.
12    Software,
13    /// Hardware timestamps via `SO_TIMESTAMPING` with `RAW_HARDWARE`.
14    Hardware,
15}
16
17/// A UDP socket wrapper that captures timestamps on send/receive.
18///
19/// Supports three timestamping modes (best to worst accuracy):
20/// 1. Hardware (SO_TIMESTAMPING with RAW_HARDWARE) -- requires NIC support
21/// 2. Kernel software (SO_TIMESTAMPING or SO_TIMESTAMPNS)
22/// 3. Userspace (NtpTimestamp::now() immediately after syscall)
23pub struct TimestampedSocket {
24    inner: UdpSocket,
25    mode: TimestampMode,
26}
27
28/// A received UDP packet with its source address and receive timestamp.
29pub struct ReceivedPacket {
30    pub data: Vec<u8>,
31    pub addr: SocketAddr,
32    pub recv_time: NtpTimestamp,
33}
34
35impl TimestampedSocket {
36    /// Bind to the given address (e.g. "0.0.0.0:123" or "127.0.0.1:0").
37    pub async fn bind(addr: &str) -> Result<Self, std::io::Error> {
38        let socket = UdpSocket::bind(addr).await?;
39        Ok(Self {
40            inner: socket,
41            mode: TimestampMode::Userspace,
42        })
43    }
44
45    /// Wrap an already-bound tokio UdpSocket.
46    pub fn from_socket(socket: UdpSocket) -> Self {
47        Self {
48            inner: socket,
49            mode: TimestampMode::Userspace,
50        }
51    }
52
53    /// Send data to the given address and return the transmit timestamp.
54    ///
55    /// The timestamp is taken as close to the actual send as possible.
56    /// With software timestamping this is immediately after the syscall returns.
57    pub async fn send_to(
58        &self,
59        buf: &[u8],
60        addr: SocketAddr,
61    ) -> Result<NtpTimestamp, std::io::Error> {
62        self.inner.send_to(buf, addr).await?;
63        let ts = NtpTimestamp::now();
64        Ok(ts)
65    }
66
67    /// Receive data into the provided buffer.
68    ///
69    /// Returns a `ReceivedPacket` containing the data (truncated to actual
70    /// received length), the sender's address, and the receive timestamp.
71    /// The timestamp is taken as close to the actual receive as possible.
72    pub async fn recv_from(&self, buf: &mut [u8]) -> Result<ReceivedPacket, std::io::Error> {
73        let (len, addr) = self.inner.recv_from(buf).await?;
74        let recv_time = NtpTimestamp::now();
75        Ok(ReceivedPacket {
76            data: buf[..len].to_vec(),
77            addr,
78            recv_time,
79        })
80    }
81
82    /// Get the local address this socket is bound to.
83    pub fn local_addr(&self) -> Result<SocketAddr, std::io::Error> {
84        self.inner.local_addr()
85    }
86
87    /// Get a reference to the inner tokio UdpSocket.
88    pub fn inner(&self) -> &UdpSocket {
89        &self.inner
90    }
91
92    /// Current timestamping mode.
93    pub fn timestamp_mode(&self) -> TimestampMode {
94        self.mode
95    }
96
97    /// Enable kernel software timestamps.
98    ///
99    /// On Linux, uses `SO_TIMESTAMPNS` for nanosecond-resolution software timestamps.
100    /// On FreeBSD, uses `SO_TIMESTAMP` for microsecond-resolution timestamps.
101    /// The kernel timestamps packets in the network stack, which is more accurate
102    /// than userspace timestamps taken after the syscall returns.
103    #[cfg(target_os = "linux")]
104    pub fn enable_software_timestamps(&mut self) -> std::io::Result<()> {
105        nix::sys::socket::setsockopt(
106            &self.inner,
107            nix::sys::socket::sockopt::ReceiveTimestampns,
108            &true,
109        )
110        .map_err(std::io::Error::from)?;
111
112        self.mode = TimestampMode::Software;
113        Ok(())
114    }
115
116    /// Enable kernel software timestamps.
117    ///
118    /// On FreeBSD, uses `SO_TIMESTAMP` for microsecond-resolution timestamps from
119    /// the kernel network stack.
120    #[cfg(target_os = "freebsd")]
121    pub fn enable_software_timestamps(&mut self) -> std::io::Result<()> {
122        nix::sys::socket::setsockopt(
123            &self.inner,
124            nix::sys::socket::sockopt::ReceiveTimestamp,
125            &true,
126        )
127        .map_err(std::io::Error::from)?;
128
129        self.mode = TimestampMode::Software;
130        Ok(())
131    }
132
133    /// Enable kernel software timestamps (unsupported platform fallback).
134    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
135    pub fn enable_software_timestamps(&mut self) -> std::io::Result<()> {
136        // No kernel timestamping available; remain in userspace mode.
137        Ok(())
138    }
139
140    /// Enable hardware timestamps via `SO_TIMESTAMPING` (Linux).
141    ///
142    /// Attempts to enable hardware timestamping using `SOF_TIMESTAMPING_TX_HARDWARE`,
143    /// `SOF_TIMESTAMPING_RX_HARDWARE`, and `SOF_TIMESTAMPING_RAW_HARDWARE`. If hardware
144    /// timestamping is not supported by the NIC, falls back to software timestamping
145    /// via `SO_TIMESTAMPING` with the software flags.
146    ///
147    /// Returns `Ok(true)` if hardware timestamping is active, `Ok(false)` if it
148    /// fell back to software timestamping.
149    #[cfg(target_os = "linux")]
150    pub fn enable_hardware_timestamps(&mut self) -> std::io::Result<bool> {
151        use nix::sys::socket::{TimestampingFlag, setsockopt, sockopt};
152
153        // Try hardware first.
154        let hw_flags = TimestampingFlag::SOF_TIMESTAMPING_TX_HARDWARE
155            | TimestampingFlag::SOF_TIMESTAMPING_RX_HARDWARE
156            | TimestampingFlag::SOF_TIMESTAMPING_RAW_HARDWARE;
157
158        if setsockopt(&self.inner, sockopt::Timestamping, &hw_flags).is_ok() {
159            self.mode = TimestampMode::Hardware;
160            return Ok(true);
161        }
162
163        // Hardware not available -- fall back to software via SO_TIMESTAMPING.
164        let sw_flags = TimestampingFlag::SOF_TIMESTAMPING_TX_SOFTWARE
165            | TimestampingFlag::SOF_TIMESTAMPING_RX_SOFTWARE
166            | TimestampingFlag::SOF_TIMESTAMPING_SOFTWARE;
167
168        setsockopt(&self.inner, sockopt::Timestamping, &sw_flags).map_err(std::io::Error::from)?;
169
170        self.mode = TimestampMode::Software;
171        Ok(false)
172    }
173
174    /// Enable hardware timestamps (FreeBSD).
175    ///
176    /// Hardware timestamping is not available through the FreeBSD socket API.
177    /// Returns `Ok(false)` to indicate software-only timestamps are active.
178    #[cfg(target_os = "freebsd")]
179    pub fn enable_hardware_timestamps(&mut self) -> std::io::Result<bool> {
180        // FreeBSD does not expose hardware timestamping through setsockopt.
181        // Fall back to software timestamps.
182        self.enable_software_timestamps()?;
183        Ok(false)
184    }
185
186    /// Enable hardware timestamps (unsupported platform fallback).
187    ///
188    /// Returns `Ok(false)` since hardware timestamping is not available.
189    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
190    pub fn enable_hardware_timestamps(&mut self) -> std::io::Result<bool> {
191        Ok(false)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[tokio::test]
200    async fn bind_ephemeral_port() {
201        let sock = TimestampedSocket::bind("127.0.0.1:0")
202            .await
203            .expect("bind should succeed");
204        let addr = sock.local_addr().expect("local_addr should succeed");
205        assert!(addr.port() > 0, "should have been assigned a port");
206        assert_eq!(addr.ip(), std::net::Ipv4Addr::LOCALHOST);
207    }
208
209    #[tokio::test]
210    async fn send_recv_loopback() {
211        // Bind two sockets on loopback with ephemeral ports
212        let sender = TimestampedSocket::bind("127.0.0.1:0")
213            .await
214            .expect("bind sender");
215        let receiver = TimestampedSocket::bind("127.0.0.1:0")
216            .await
217            .expect("bind receiver");
218
219        let receiver_addr = receiver.local_addr().expect("receiver local_addr");
220        let payload = b"NTP test packet data";
221
222        // Send from sender to receiver
223        let send_ts = sender
224            .send_to(payload, receiver_addr)
225            .await
226            .expect("send_to");
227
228        // Receive on the receiver side
229        let mut buf = [0u8; 1024];
230        let received = receiver.recv_from(&mut buf).await.expect("recv_from");
231
232        assert_eq!(received.data, payload);
233        assert_eq!(received.addr, sender.local_addr().expect("sender addr"));
234
235        // Timestamps should be non-zero and in reasonable order
236        assert_ne!(send_ts, NtpTimestamp::ZERO);
237        assert_ne!(received.recv_time, NtpTimestamp::ZERO);
238        // recv_time should be >= send_ts (on same machine, software timestamps)
239        assert!(
240            received.recv_time.raw() >= send_ts.raw(),
241            "recv_time ({:?}) should be >= send_ts ({:?})",
242            received.recv_time,
243            send_ts
244        );
245    }
246
247    #[tokio::test]
248    async fn from_socket_works() {
249        let raw = UdpSocket::bind("127.0.0.1:0")
250            .await
251            .expect("bind raw socket");
252        let addr = raw.local_addr().expect("raw local_addr");
253
254        let wrapped = TimestampedSocket::from_socket(raw);
255        assert_eq!(wrapped.local_addr().expect("wrapped local_addr"), addr);
256    }
257
258    #[tokio::test]
259    async fn multiple_packets_loopback() {
260        let sender = TimestampedSocket::bind("127.0.0.1:0")
261            .await
262            .expect("bind sender");
263        let receiver = TimestampedSocket::bind("127.0.0.1:0")
264            .await
265            .expect("bind receiver");
266
267        let receiver_addr = receiver.local_addr().expect("receiver addr");
268
269        // Send multiple packets and verify all arrive
270        for i in 0u8..5 {
271            let payload = [i; 48]; // NTP-sized packet
272            sender.send_to(&payload, receiver_addr).await.expect("send");
273
274            let mut buf = [0u8; 1024];
275            let pkt = receiver.recv_from(&mut buf).await.expect("recv");
276            assert_eq!(pkt.data.len(), 48);
277            assert_eq!(pkt.data[0], i);
278        }
279    }
280}