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;