clock_bound_d/
lib.rs

1//! ClockBound Daemon
2//!
3//! This crate implements the ClockBound daemon
4
5mod chrony_client;
6mod clock_bound_runner;
7mod clock_snapshot_poller;
8mod clock_state_fsm;
9mod clock_state_fsm_no_disruption;
10mod phc_utils;
11pub mod signal;
12
13use std::path::Path;
14use std::str::FromStr;
15use std::sync::atomic;
16
17#[cfg(any(test, feature = "test"))]
18use crate::phc_utils::MockPhcWithSysfsErrorBound as PhcWithSysfsErrorBound;
19#[cfg(not(any(test, feature = "test")))]
20use crate::phc_utils::PhcWithSysfsErrorBound;
21use clock_bound_shm::ShmWriter;
22use clock_bound_vmclock::{shm::VMCLOCK_SHM_DEFAULT_PATH, shm_reader::VMClockShmReader};
23use chrony_client::UnixDomainSocket;
24use clock_bound_runner::ClockBoundRunner;
25use clock_snapshot_poller::chronyd_snapshot_poller::ChronyDaemonSnapshotPoller;
26use tracing::{debug, error};
27
28pub use phc_utils::get_error_bound_sysfs_path;
29
30// TODO: make this a parameter on the CLI?
31pub const CLOCKBOUND_SHM_DEFAULT_PATH: &str = "/var/run/clockbound/shm0";
32
33/// PhcInfo holds the refid of the PHC in chronyd (i.e. PHC0), and the
34/// interface on which the PHC is enabled.
35#[derive(Clone, PartialEq, Eq, Hash, Debug)]
36pub struct PhcInfo {
37    pub refid: u32,
38    pub sysfs_error_bound_path: std::path::PathBuf,
39}
40
41/// Boolean value that tracks whether a manually triggered disruption is pending and need to be
42/// actioned.
43pub static FORCE_DISRUPTION_PENDING: atomic::AtomicBool = atomic::AtomicBool::new(false);
44
45/// Boolean value that can be toggled to signal periods of forced disruption vs. "normal" periods.
46pub static FORCE_DISRUPTION_STATE: atomic::AtomicBool = atomic::AtomicBool::new(false);
47
48/// The status of the system clock reported by chronyd
49#[derive(Debug, Copy, Clone, PartialEq)]
50pub enum ChronyClockStatus {
51    /// The status of the clock is unknown.
52    Unknown = 0,
53
54    /// The clock is kept accurate by the synchronization daemon.
55    Synchronized = 1,
56
57    /// The clock is free running and not updated by the synchronization daemon.
58    FreeRunning = 2,
59}
60
61impl From<u16> for ChronyClockStatus {
62    // Chrony is signalling it is not synchronized by setting both bits in the Leap Indicator.
63    fn from(value: u16) -> Self {
64        match value {
65            0..=2 => Self::Synchronized,
66            3 => Self::FreeRunning,
67            _ => Self::Unknown,
68        }
69    }
70}
71
72/// Enum of possible Clock Disruption States exposed by the daemon.
73#[derive(Debug, Copy, Clone, PartialEq)]
74pub enum ClockDisruptionState {
75    Unknown,
76    Reliable,
77    Disrupted,
78}
79
80/// Custom struct used for indicating a parsing error when parsing a
81/// ClockErrorBoundSource or ClockDisruptionNotificationSource
82/// from str.
83#[derive(Clone, PartialEq, Eq, Hash, Debug)]
84pub struct ParseError;
85
86/// Enum of possible input sources for obtaining the ClockErrorBound.
87#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
88pub enum ClockErrorBoundSource {
89    /// Chrony.
90    Chrony,
91
92    /// VMClock.
93    VMClock,
94}
95
96/// Performs a case-insensitive conversion from str to enum ClockErrorBoundSource.
97impl FromStr for ClockErrorBoundSource {
98    type Err = ParseError;
99    fn from_str(input: &str) -> Result<ClockErrorBoundSource, Self::Err> {
100        match input.to_lowercase().as_str() {
101            "chrony" => Ok(ClockErrorBoundSource::Chrony),
102            "vmclock" => Ok(ClockErrorBoundSource::VMClock),
103            _ => {
104                error!("ClockErrorBoundSource '{:?}' is not supported", input);
105                Err(ParseError)
106            }
107        }
108    }
109}
110
111/// Helper for converting a string ref_id into a u32 for the chrony command protocol.
112///
113/// # Arguments
114///
115/// * `ref_id` - The ref_id as a string to be translated to a u32.
116pub fn refid_to_u32(ref_id: &str) -> Result<u32, String> {
117    let bytes = ref_id.bytes();
118    if bytes.len() <= 4 && bytes.clone().all(|b| b.is_ascii()) {
119        let bytes_as_u32: Vec<u32> = bytes.map(|val| val as u32).collect();
120        Ok(bytes_as_u32
121            .iter()
122            .rev()
123            .enumerate()
124            .fold(0, |acc, (i, val)| acc | (val << (i * 8))))
125    } else {
126        Err(String::from(
127            "The PHC reference ID supplied was not a 4 character ASCII string.",
128        ))
129    }
130}
131
132pub fn run(
133    max_drift_ppb: u32,
134    maybe_phc_info: Option<PhcInfo>,
135    clock_error_bound_source: ClockErrorBoundSource,
136    clock_disruption_support_enabled: bool,
137) {
138    // Create a writer to update the clock error bound shared memory segment
139    let mut writer = match ShmWriter::new(Path::new(CLOCKBOUND_SHM_DEFAULT_PATH)) {
140        Ok(writer) => {
141            debug!("Created a new ShmWriter");
142            writer
143        }
144        Err(e) => {
145            error!(
146                "Failed to create the SHM writer at {:?} {}",
147                CLOCKBOUND_SHM_DEFAULT_PATH, e
148            );
149            panic!("Failed to create SHM writer");
150        }
151    };
152    let clock_status_snapshot_poller = match clock_error_bound_source {
153        ClockErrorBoundSource::Chrony => ChronyDaemonSnapshotPoller::new(
154            Box::new(UnixDomainSocket::default()),
155            maybe_phc_info.map(|phc_info| {
156                PhcWithSysfsErrorBound::new(phc_info.sysfs_error_bound_path, phc_info.refid)
157            }),
158        ),
159        ClockErrorBoundSource::VMClock => {
160            unimplemented!("VMClock ClockErrorBoundSource is not yet implemented");
161        }
162    };
163    let mut vmclock_shm_reader = if !clock_disruption_support_enabled {
164        None
165    } else {
166        match VMClockShmReader::new(VMCLOCK_SHM_DEFAULT_PATH) {
167            Ok(reader) => Some(reader),
168            Err(e) => {
169                panic!(
170                    "VMClockPoller: Failed to create VMClockShmReader. Please check if path {:?} exists and is readable. {:?}",
171                    VMCLOCK_SHM_DEFAULT_PATH, e
172                );
173            }
174        }
175    };
176
177    let mut clock_bound_runner =
178        ClockBoundRunner::new(clock_disruption_support_enabled, max_drift_ppb);
179    clock_bound_runner.run(
180        &mut vmclock_shm_reader,
181        &mut writer,
182        clock_status_snapshot_poller,
183        UnixDomainSocket::default(),
184    );
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_str_to_clockerrorboundsource_conversion() {
193        assert_eq!(
194            ClockErrorBoundSource::from_str("chrony"),
195            Ok(ClockErrorBoundSource::Chrony)
196        );
197        assert_eq!(
198            ClockErrorBoundSource::from_str("Chrony"),
199            Ok(ClockErrorBoundSource::Chrony)
200        );
201        assert_eq!(
202            ClockErrorBoundSource::from_str("CHRONY"),
203            Ok(ClockErrorBoundSource::Chrony)
204        );
205        assert_eq!(
206            ClockErrorBoundSource::from_str("cHrOnY"),
207            Ok(ClockErrorBoundSource::Chrony)
208        );
209        assert_eq!(
210            ClockErrorBoundSource::from_str("vmclock"),
211            Ok(ClockErrorBoundSource::VMClock)
212        );
213        assert_eq!(
214            ClockErrorBoundSource::from_str("VMClock"),
215            Ok(ClockErrorBoundSource::VMClock)
216        );
217        assert_eq!(
218            ClockErrorBoundSource::from_str("VMCLOCK"),
219            Ok(ClockErrorBoundSource::VMClock)
220        );
221        assert_eq!(
222            ClockErrorBoundSource::from_str("vmClock"),
223            Ok(ClockErrorBoundSource::VMClock)
224        );
225        assert!(ClockErrorBoundSource::from_str("other").is_err());
226        assert!(ClockErrorBoundSource::from_str("None").is_err());
227        assert!(ClockErrorBoundSource::from_str("null").is_err());
228        assert!(ClockErrorBoundSource::from_str("").is_err());
229    }
230
231    #[test]
232    fn test_refid_to_u32() {
233        // Test error cases
234        assert!(refid_to_u32("morethan4characters").is_err());
235        let non_valid_ascii_str = "©";
236        assert!(non_valid_ascii_str.len() <= 4);
237        assert!(refid_to_u32(non_valid_ascii_str).is_err());
238
239        // Test actual parsing is as expected
240        // ASCII values: P = 80, H = 72, C = 67, 0 = 48
241        assert_eq!(
242            refid_to_u32("PHC0").unwrap(),
243            80 << 24 | 72 << 16 | 67 << 8 | 48
244        );
245        assert_eq!(refid_to_u32("PHC").unwrap(), 80 << 16 | 72 << 8 | 67);
246        assert_eq!(refid_to_u32("PH").unwrap(), 80 << 8 | 72);
247        assert_eq!(refid_to_u32("P").unwrap(), 80);
248    }
249}