Skip to main content

statime_netptp/
lib.rs

1//! Network handling for PTP/CSPTP in ntpd-rs/statime.
2//!
3//! This crate provides abstractions for dealing with the fact that both PTP
4//! and CSPTP need sockets with local bindings on ports 319 and 320. The
5//! operating system can only hand out 1 of those sockets per interface, plus a
6//! fallback socket. This crate provides multiplexing for multiple uses of this
7//! one socket, making implementing of simultaneous use of PTP and CSPTP, as
8//! well as more than one CSPTP connection at the same time, easier.
9//!
10//! This is mostly intended as an internal crate for the ntpd-rs/statime
11//! ecosystem, though we plan to keep the version numbering consistent with
12//! semver. Be aware that we might be making breaking changes fairly often
13//! though when using this crate, and that we might not be receptive to changes
14//! making it work better for your particular use case if those changes have no
15//! value for ntpd-rs/statime ecosystem.
16#![cfg(target_os = "linux")]
17
18use std::{
19    collections::HashMap,
20    sync::{Arc, RwLock, atomic::AtomicUsize},
21    task::Wake,
22    time::Duration,
23};
24
25use timestamped_socket::{
26    interface::{InterfaceName, lookup_phc},
27    socket::{SendTimestampToken, TimestampData},
28};
29
30use std::io::Result;
31
32use crate::wake::ListWaker;
33
34mod addresses;
35mod interface;
36mod socket;
37mod wake;
38
39use addresses::BoundInterface;
40pub use addresses::PtpAddressFamily;
41pub use interface::Interface;
42pub use socket::{ConnectedSocket, OpenSocket, RecvResult};
43
44/// The maximum size for incoming packets
45pub const MAX_PACKET_SIZE: usize = 4096;
46/// The number of incoming packets that are buffered for a socket.
47pub const PACKET_BUFFER_SIZE: usize = 16;
48/// The number of old send timestamps that are stored per interface.
49pub const CACHED_TIMESTAMPS: usize = 32;
50/// The amount of time we are willing to wait for a send timestamp.
51pub const TIMESTAMP_FETCH_TIMEOUT: Duration = Duration::from_millis(1000);
52
53/// Description of which clock to use for timestamping messages.
54#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
55pub enum TimestampingClock {
56    /// Use the system clock.
57    System,
58    /// Use a hardware clock for the network card.
59    Hardware {
60        /// Which specific hardware clock to use.
61        ///
62        /// By default, we use the primary hardware clock associated with the
63        /// interface. However, when using PTP virtual clocks in linux, this
64        /// can be used to change the clock used to one of those.
65        phc: Option<u32>,
66    },
67}
68
69/// The primary management struct for the PTP/CSPTP network sockets.
70///
71/// Root of the hierarchy for opening sockets. Allows opening of specific
72/// interfaces and configuring the clock sources used for those interface.
73/// These interfaces can then be used to create sockets, which can be used
74/// to handle the actual network traffic.
75#[derive(Clone)]
76pub struct NetworkManager<A: PtpAddressFamily>(Arc<NetworkManagerData<A>>);
77
78struct InterfaceData<BoundInterface> {
79    interface: BoundInterface,
80    hardware_clock: Option<u32>,
81    refcount: usize,
82    send_wakers: Arc<ListWaker>,
83    ts_wakers: Arc<ListWaker>,
84    previous_timestamps: [RwLock<Option<(SendTimestampToken, TimestampData)>>; CACHED_TIMESTAMPS],
85    timestamp_counter: RwLock<usize>,
86}
87
88struct SocketData<A: PtpAddressFamily> {
89    remote_filter: Option<A>,
90    local_filter: Option<A>,
91    interface_filter: Option<InterfaceName>,
92    timestamp_source: TimestampSource,
93    message_channel: tokio::sync::mpsc::Sender<RecvResult<A>>,
94}
95
96struct NetworkManagerData<A: PtpAddressFamily> {
97    interfaces: RwLock<HashMap<Option<InterfaceName>, InterfaceData<A::BoundInterface>>>,
98    read_wakers: Arc<ListWaker>,
99    sockets: RwLock<HashMap<usize, SocketData<A>>>,
100    next_socket_id: AtomicUsize,
101}
102
103impl<A: PtpAddressFamily> NetworkManager<A> {
104    /// Create a new network manager
105    ///
106    /// Immediately opens a general socket on port 319/320 not bound to a
107    /// specific interface, as creating that socket later can be problematic.
108    ///
109    /// # Errors
110    ///
111    /// Returns any IO errors that occurred in setting up the general socket.
112    pub fn new() -> Result<Self> {
113        let mut interfaces = HashMap::new();
114
115        interfaces.insert(
116            None,
117            InterfaceData {
118                interface: A::BoundInterface::open(None, None)?,
119                hardware_clock: None,
120                // Ensure the general interface always stays open.
121                refcount: 1,
122                send_wakers: Arc::default(),
123                ts_wakers: Arc::default(),
124                previous_timestamps: Default::default(),
125                timestamp_counter: RwLock::new(0),
126            },
127        );
128
129        Ok(NetworkManager(Arc::new(NetworkManagerData {
130            interfaces: RwLock::new(interfaces),
131            read_wakers: Arc::default(),
132            next_socket_id: AtomicUsize::new(0),
133            sockets: RwLock::default(),
134        })))
135    }
136}
137
138impl<A: PtpAddressFamily> NetworkManager<A> {
139    /// Get a reference to the general interface.
140    ///
141    /// This interface can be used to create sockets which don't care about
142    /// which network interface their messages actually transit through.
143    /// Timestamping of their messages will always be done with the system
144    /// clock, as that is the only clock guaranteed to be available for
145    /// timestamping on all network interfaces.
146    #[must_use]
147    #[expect(
148        clippy::missing_panics_doc,
149        reason = "Function will only panic if there is an implementation bug in this crate."
150    )]
151    pub fn open_general(&self) -> Interface<A> {
152        // The mutex can only be poisoned from an earlier panic. It is ok for
153        // us to propagate that to all the threads.
154        let mut interfaces = self.0.interfaces.write().unwrap();
155        // The general interface is always present. Should it somehow not be,
156        // panic is the best solution.
157        let entry = interfaces.get_mut(&None).unwrap();
158        // An overflowing reference count is an error condition from which we
159        // cannot reasonably recover. A panic here is the best solution.
160        entry.refcount = entry.refcount.checked_add(1).unwrap();
161        Interface {
162            state: self.0.clone(),
163            name: None,
164            timestamp_source: TimestampSource::System,
165        }
166    }
167
168    /// Open a specific network interface.
169    ///
170    /// This opens the specified network interface, configuring it such that
171    /// the requested clock can be used for timestamping. If the interface was
172    /// already previously opened, it will return a reference to it.
173    ///
174    /// # Errors
175    ///
176    /// Returns any IO errors that occured when trying to create a socket for
177    /// the specified interface.
178    ///
179    /// Will also error when the interface is already open, but was created
180    /// with a different clock for timestamping than currently requested,
181    /// unless the currently requested clock is the system clock.
182    #[expect(
183        clippy::missing_panics_doc,
184        reason = "Function will only panic if there is an implementation bug in this crate."
185    )]
186    pub fn open_interface(
187        &self,
188        interface: InterfaceName,
189        clock: TimestampingClock,
190    ) -> Result<Interface<A>> {
191        let clock_idx = match clock {
192            TimestampingClock::System => None,
193            TimestampingClock::Hardware { phc: None } => {
194                Some(lookup_phc(interface).ok_or(std::io::Error::new(
195                    std::io::ErrorKind::Unsupported,
196                    "Hardware timestamping requested, but not available on interface.",
197                ))?)
198            }
199            TimestampingClock::Hardware { phc: Some(phc) } => Some(phc),
200        };
201
202        // The mutex can only be poisoned from an earlier panic. It is ok for
203        // us to propagate that to all the threads.
204        let mut interfaces = self.0.interfaces.write().unwrap();
205        // Ensure the interface exists and has its refcount increased for the new interface
206        if let Some(entry) = interfaces.get_mut(&Some(interface)) {
207            // Check the hardware clock matches expectations.
208            // FIXME: Implement reopening of the interface when it doesn't yet have a hardware clock associated with it.
209            if clock_idx.is_some() && entry.hardware_clock != clock_idx {
210                return Err(std::io::Error::other(
211                    "Interface already in use with different hardware clock.",
212                ));
213            }
214            // An overflowing reference count is an error condition from which we
215            // cannot reasonably recover. A panic here is the best solution.
216            entry.refcount = entry.refcount.checked_add(1).unwrap();
217        } else {
218            interfaces.insert(
219                Some(interface),
220                InterfaceData {
221                    interface: A::BoundInterface::open(Some(interface), clock_idx)?,
222                    hardware_clock: clock_idx,
223                    refcount: 1,
224                    send_wakers: Arc::default(),
225                    ts_wakers: Arc::default(),
226                    previous_timestamps: Default::default(),
227                    timestamp_counter: RwLock::new(0),
228                },
229            );
230            // Packets may now arrive on the new interface, so everyone
231            // should start listening there.
232            self.0.read_wakers.wake_by_ref();
233        }
234        Ok(Interface {
235            state: self.0.clone(),
236            name: Some(interface),
237            timestamp_source: match clock {
238                TimestampingClock::System => TimestampSource::System,
239                TimestampingClock::Hardware { .. } => TimestampSource::Hardware,
240            },
241        })
242    }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246enum TimestampSource {
247    System,
248    Hardware,
249}
250
251#[cfg(all(test, feature = "privileged_tests"))]
252mod test;