Skip to main content

ntp_server/
error.rs

1// Copyright 2026 U.S. Federal Government (in countries where recognized)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Custom error types for the NTP server.
5//!
6//! All public APIs continue to return `io::Result<T>` for backward compatibility.
7//! Internally, errors are constructed as `NtpServerError` variants and converted
8//! to `io::Error` automatically via `From<NtpServerError> for io::Error`.
9//!
10//! Users who want programmatic error matching can downcast via
11//! `io::Error::get_ref()`:
12//!
13//! ```no_run
14//! use ntp_server::error::NtpServerError;
15//!
16//! # fn example(result: std::io::Result<()>) {
17//! match result {
18//!     Ok(()) => println!("server running"),
19//!     Err(e) => {
20//!         if let Some(srv_err) = e.get_ref()
21//!             .and_then(|inner| inner.downcast_ref::<NtpServerError>())
22//!         {
23//!             match srv_err {
24//!                 NtpServerError::Protocol(p) => eprintln!("protocol error: {p}"),
25//!                 NtpServerError::Nts(n) => eprintln!("NTS error: {n}"),
26//!                 _ => eprintln!("server error: {srv_err}"),
27//!             }
28//!         }
29//!     }
30//! }
31//! # }
32//! ```
33
34// Re-export proto error types for backward compatibility.
35pub use ntp_proto::error::ParseError;
36
37use std::fmt;
38use std::io;
39
40/// Errors that can occur during NTP server operations.
41#[derive(Debug)]
42pub enum NtpServerError {
43    /// NTP protocol validation failure (malformed requests, unexpected fields).
44    Protocol(ProtocolError),
45    /// NTS key establishment or authentication failure.
46    Nts(NtsError),
47    /// Invalid configuration (bad addresses, invalid TLS credentials).
48    Config(ConfigError),
49    /// Underlying I/O error (socket bind, send/recv, etc.).
50    Io(io::Error),
51}
52
53/// NTP protocol validation errors for incoming client requests.
54///
55/// These correspond to the checks performed in
56/// `server_common::validation` per RFC 5905 Section 8.
57#[derive(Clone, Debug)]
58pub enum ProtocolError {
59    /// Request packet too short (< 48 bytes).
60    RequestTooShort {
61        /// Number of bytes received.
62        received: usize,
63    },
64    /// Request has unexpected mode (expected Client or SymmetricActive).
65    UnexpectedMode {
66        /// The mode value received.
67        mode: u8,
68    },
69    /// Unsupported NTP version in request.
70    UnsupportedVersion {
71        /// The version value received.
72        version: u8,
73    },
74    /// Client transmit timestamp is zero.
75    ZeroTransmitTimestamp,
76    /// Request has no extension fields (expected for NTS).
77    NoExtensionFields,
78    /// Generic protocol error.
79    Other(String),
80}
81
82/// NTS (Network Time Security) server-side errors.
83///
84/// These cover NTS-KE negotiation failures, cookie decryption errors, and
85/// AEAD authentication failures encountered while processing NTS-authenticated
86/// NTP requests.
87#[derive(Clone, Debug)]
88pub enum NtsError {
89    /// Missing required NTS extension field in client request.
90    MissingExtension {
91        /// Name of the missing extension field.
92        field: &'static str,
93    },
94    /// Failed to decrypt NTS cookie (expired or invalid master key).
95    CookieDecryptionFailed,
96    /// Failed to parse NTS Authenticator extension field.
97    AuthenticatorParseFailed,
98    /// NTS AEAD authentication failed (C2S key verification).
99    AuthenticationFailed,
100    /// TLS key export failed during NTS-KE.
101    KeyExportFailed {
102        /// Detail about the failure.
103        detail: String,
104    },
105    /// Master key store lock poisoned.
106    KeyStorePoisoned,
107    /// Unrecognized critical NTS-KE record from client.
108    UnrecognizedCriticalRecord {
109        /// The unrecognized record type.
110        record_type: u16,
111    },
112    /// Client did not send a supported Next Protocol (NTPv4).
113    MissingNextProtocol,
114    /// Generic NTS error.
115    Other(String),
116}
117
118/// Server configuration errors.
119#[derive(Clone, Debug)]
120pub enum ConfigError {
121    /// Invalid listen address.
122    InvalidListenAddress {
123        /// The address that was invalid.
124        address: String,
125        /// Detail about why it is invalid.
126        detail: String,
127    },
128    /// Invalid TLS certificate or private key.
129    InvalidTlsCredentials {
130        /// Detail about the failure.
131        detail: String,
132    },
133    /// Generic configuration error.
134    Other(String),
135}
136
137// ── Display implementations ─────────────────────────────────────────
138
139impl fmt::Display for NtpServerError {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            NtpServerError::Protocol(e) => write!(f, "NTP server protocol error: {e}"),
143            NtpServerError::Nts(e) => write!(f, "NTS server error: {e}"),
144            NtpServerError::Config(e) => write!(f, "NTP server config error: {e}"),
145            NtpServerError::Io(e) => write!(f, "{e}"),
146        }
147    }
148}
149
150impl fmt::Display for ProtocolError {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        match self {
153            ProtocolError::RequestTooShort { received } => {
154                write!(f, "NTP request too short ({received} bytes)")
155            }
156            ProtocolError::UnexpectedMode { mode } => {
157                write!(f, "unexpected request mode: {mode}")
158            }
159            ProtocolError::UnsupportedVersion { version } => {
160                write!(f, "unsupported NTP version: {version}")
161            }
162            ProtocolError::ZeroTransmitTimestamp => {
163                write!(f, "client transmit timestamp is zero")
164            }
165            ProtocolError::NoExtensionFields => {
166                write!(f, "request has no extension fields")
167            }
168            ProtocolError::Other(msg) => write!(f, "{msg}"),
169        }
170    }
171}
172
173impl fmt::Display for NtsError {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match self {
176            NtsError::MissingExtension { field } => {
177                write!(f, "missing NTS extension field: {field}")
178            }
179            NtsError::CookieDecryptionFailed => {
180                write!(f, "failed to decrypt NTS cookie (expired or invalid)")
181            }
182            NtsError::AuthenticatorParseFailed => {
183                write!(f, "failed to parse NTS Authenticator")
184            }
185            NtsError::AuthenticationFailed => {
186                write!(f, "NTS AEAD authentication failed")
187            }
188            NtsError::KeyExportFailed { detail } => {
189                write!(f, "TLS key export failed: {detail}")
190            }
191            NtsError::KeyStorePoisoned => {
192                write!(f, "master key store lock poisoned")
193            }
194            NtsError::UnrecognizedCriticalRecord { record_type } => {
195                write!(f, "unrecognized critical NTS-KE record type: {record_type}")
196            }
197            NtsError::MissingNextProtocol => {
198                write!(f, "client did not send a supported Next Protocol")
199            }
200            NtsError::Other(msg) => write!(f, "{msg}"),
201        }
202    }
203}
204
205impl fmt::Display for ConfigError {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        match self {
208            ConfigError::InvalidListenAddress { address, detail } => {
209                write!(f, "invalid listen address '{address}': {detail}")
210            }
211            ConfigError::InvalidTlsCredentials { detail } => {
212                write!(f, "invalid TLS credentials: {detail}")
213            }
214            ConfigError::Other(msg) => write!(f, "{msg}"),
215        }
216    }
217}
218
219// ── Error trait implementations ─────────────────────────────────────
220
221impl std::error::Error for NtpServerError {
222    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
223        match self {
224            NtpServerError::Io(e) => Some(e),
225            _ => None,
226        }
227    }
228}
229
230impl std::error::Error for ProtocolError {}
231impl std::error::Error for NtsError {}
232impl std::error::Error for ConfigError {}
233
234// ── From conversions ────────────────────────────────────────────────
235
236impl From<NtpServerError> for io::Error {
237    fn from(err: NtpServerError) -> io::Error {
238        let kind = match &err {
239            NtpServerError::Protocol(_) => io::ErrorKind::InvalidData,
240            NtpServerError::Nts(NtsError::KeyExportFailed { .. }) => io::ErrorKind::Other,
241            NtpServerError::Nts(NtsError::KeyStorePoisoned) => io::ErrorKind::Other,
242            NtpServerError::Nts(_) => io::ErrorKind::InvalidData,
243            NtpServerError::Config(_) => io::ErrorKind::InvalidInput,
244            NtpServerError::Io(e) => e.kind(),
245        };
246        // Preserve the original io::Error directly for the Io variant.
247        if let NtpServerError::Io(e) = err {
248            return e;
249        }
250        io::Error::new(kind, err)
251    }
252}
253
254impl From<io::Error> for NtpServerError {
255    fn from(err: io::Error) -> NtpServerError {
256        NtpServerError::Io(err)
257    }
258}
259
260impl From<ProtocolError> for NtpServerError {
261    fn from(err: ProtocolError) -> NtpServerError {
262        NtpServerError::Protocol(err)
263    }
264}
265
266impl From<NtsError> for NtpServerError {
267    fn from(err: NtsError) -> NtpServerError {
268        NtpServerError::Nts(err)
269    }
270}
271
272impl From<ConfigError> for NtpServerError {
273    fn from(err: ConfigError) -> NtpServerError {
274        NtpServerError::Config(err)
275    }
276}
277
278// ── Tests ───────────────────────────────────────────────────────────
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_protocol_error_display() {
286        let e = ProtocolError::RequestTooShort { received: 10 };
287        assert_eq!(e.to_string(), "NTP request too short (10 bytes)");
288    }
289
290    #[test]
291    fn test_protocol_error_unexpected_mode() {
292        let e = ProtocolError::UnexpectedMode { mode: 5 };
293        assert_eq!(e.to_string(), "unexpected request mode: 5");
294    }
295
296    #[test]
297    fn test_protocol_error_unsupported_version() {
298        let e = ProtocolError::UnsupportedVersion { version: 2 };
299        assert_eq!(e.to_string(), "unsupported NTP version: 2");
300    }
301
302    #[test]
303    fn test_protocol_error_zero_transmit() {
304        let e = ProtocolError::ZeroTransmitTimestamp;
305        assert_eq!(e.to_string(), "client transmit timestamp is zero");
306    }
307
308    #[test]
309    fn test_protocol_error_no_extensions() {
310        let e = ProtocolError::NoExtensionFields;
311        assert_eq!(e.to_string(), "request has no extension fields");
312    }
313
314    #[test]
315    fn test_nts_error_display() {
316        let e = NtsError::MissingExtension {
317            field: "Unique Identifier",
318        };
319        assert_eq!(
320            e.to_string(),
321            "missing NTS extension field: Unique Identifier"
322        );
323    }
324
325    #[test]
326    fn test_nts_error_cookie_decryption() {
327        let e = NtsError::CookieDecryptionFailed;
328        assert_eq!(
329            e.to_string(),
330            "failed to decrypt NTS cookie (expired or invalid)"
331        );
332    }
333
334    #[test]
335    fn test_nts_error_authentication() {
336        let e = NtsError::AuthenticationFailed;
337        assert_eq!(e.to_string(), "NTS AEAD authentication failed");
338    }
339
340    #[test]
341    fn test_nts_error_key_export() {
342        let e = NtsError::KeyExportFailed {
343            detail: "no session".to_string(),
344        };
345        assert_eq!(e.to_string(), "TLS key export failed: no session");
346    }
347
348    #[test]
349    fn test_nts_error_key_store_poisoned() {
350        let e = NtsError::KeyStorePoisoned;
351        assert_eq!(e.to_string(), "master key store lock poisoned");
352    }
353
354    #[test]
355    fn test_config_error_display() {
356        let e = ConfigError::InvalidListenAddress {
357            address: "bad:addr".to_string(),
358            detail: "not a valid socket address".to_string(),
359        };
360        assert_eq!(
361            e.to_string(),
362            "invalid listen address 'bad:addr': not a valid socket address"
363        );
364    }
365
366    #[test]
367    fn test_config_error_tls() {
368        let e = ConfigError::InvalidTlsCredentials {
369            detail: "bad PEM".to_string(),
370        };
371        assert_eq!(e.to_string(), "invalid TLS credentials: bad PEM");
372    }
373
374    #[test]
375    fn test_server_error_to_io_error_kind() {
376        let cases: Vec<(NtpServerError, io::ErrorKind)> = vec![
377            (
378                NtpServerError::Protocol(ProtocolError::ZeroTransmitTimestamp),
379                io::ErrorKind::InvalidData,
380            ),
381            (
382                NtpServerError::Nts(NtsError::AuthenticationFailed),
383                io::ErrorKind::InvalidData,
384            ),
385            (
386                NtpServerError::Nts(NtsError::KeyStorePoisoned),
387                io::ErrorKind::Other,
388            ),
389            (
390                NtpServerError::Config(ConfigError::Other("test".to_string())),
391                io::ErrorKind::InvalidInput,
392            ),
393        ];
394        for (srv_err, expected_kind) in cases {
395            let io_err: io::Error = srv_err.into();
396            assert_eq!(io_err.kind(), expected_kind);
397        }
398    }
399
400    #[test]
401    fn test_server_error_downcast_roundtrip() {
402        let err = NtpServerError::Protocol(ProtocolError::RequestTooShort { received: 10 });
403        let io_err: io::Error = err.into();
404        assert_eq!(io_err.kind(), io::ErrorKind::InvalidData);
405
406        let inner = io_err
407            .get_ref()
408            .unwrap()
409            .downcast_ref::<NtpServerError>()
410            .unwrap();
411        assert!(matches!(
412            inner,
413            NtpServerError::Protocol(ProtocolError::RequestTooShort { received: 10 })
414        ));
415    }
416
417    #[test]
418    fn test_io_error_passthrough() {
419        let orig = io::Error::new(io::ErrorKind::ConnectionReset, "reset");
420        let kind = orig.kind();
421        let srv_err = NtpServerError::Io(orig);
422        let io_err: io::Error = srv_err.into();
423        assert_eq!(io_err.kind(), kind);
424        assert_eq!(io_err.to_string(), "reset");
425    }
426
427    #[test]
428    fn test_from_io_error() {
429        let orig = io::Error::new(io::ErrorKind::BrokenPipe, "broken");
430        let srv_err: NtpServerError = orig.into();
431        assert!(matches!(srv_err, NtpServerError::Io(_)));
432    }
433
434    #[test]
435    fn test_from_protocol_error() {
436        let proto_err = ProtocolError::ZeroTransmitTimestamp;
437        let srv_err: NtpServerError = proto_err.into();
438        assert!(matches!(srv_err, NtpServerError::Protocol(_)));
439    }
440
441    #[test]
442    fn test_from_nts_error() {
443        let nts_err = NtsError::AuthenticationFailed;
444        let srv_err: NtpServerError = nts_err.into();
445        assert!(matches!(srv_err, NtpServerError::Nts(_)));
446    }
447
448    #[test]
449    fn test_from_config_error() {
450        let cfg_err = ConfigError::Other("test".to_string());
451        let srv_err: NtpServerError = cfg_err.into();
452        assert!(matches!(srv_err, NtpServerError::Config(_)));
453    }
454
455    #[test]
456    fn test_ntp_server_error_source() {
457        let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "broken");
458        let srv_err = NtpServerError::Io(io_err);
459        assert!(std::error::Error::source(&srv_err).is_some());
460
461        let proto_err = NtpServerError::Protocol(ProtocolError::ZeroTransmitTimestamp);
462        assert!(std::error::Error::source(&proto_err).is_none());
463    }
464}