openentropy_core/sources/
sysctl.rs1use std::collections::HashMap;
6use std::thread;
7use std::time::Duration;
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10
11use super::helpers::{extract_delta_bytes_i64, run_command};
12
13const SYSCTL_PATH: &str = "/usr/sbin/sysctl";
15
16const SNAPSHOT_DELAY: Duration = Duration::from_millis(100);
18
19pub struct SysctlSource;
25
26static SYSCTL_INFO: SourceInfo = SourceInfo {
27 name: "sysctl_deltas",
28 description: "Batch-reads ~1600 kernel counters via sysctl -a and extracts deltas from the ~40-60 that change within 200ms",
29 physics: "Batch-reads ~1600 kernel counters via sysctl and extracts deltas from \
30 the ~40-60 that change within 200ms. These counters track page faults, context \
31 switches, TCP segments, interrupts \u{2014} each driven by independent processes. \
32 The LSBs of their deltas reflect the unpredictable micro-timing of the entire \
33 operating system\u{2019}s activity.",
34 category: SourceCategory::System,
35 platform: Platform::MacOS,
36 requirements: &[],
37 entropy_rate_estimate: 5000.0,
38 composite: false,
39};
40
41impl SysctlSource {
42 pub fn new() -> Self {
43 Self
44 }
45}
46
47impl Default for SysctlSource {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53fn snapshot_sysctl() -> Option<HashMap<String, i64>> {
57 let stdout = run_command(SYSCTL_PATH, &["-a"])?;
58 let mut map = HashMap::new();
59
60 for line in stdout.lines() {
61 let (key, val_str) = if let Some(idx) = line.find(": ") {
63 (&line[..idx], line[idx + 2..].trim())
64 } else if let Some(idx) = line.find(" = ") {
65 (&line[..idx], line[idx + 3..].trim())
66 } else {
67 continue;
68 };
69
70 if let Ok(v) = val_str.parse::<i64>() {
72 map.insert(key.to_string(), v);
73 }
74 }
75
76 Some(map)
77}
78
79impl EntropySource for SysctlSource {
80 fn info(&self) -> &SourceInfo {
81 &SYSCTL_INFO
82 }
83
84 fn is_available(&self) -> bool {
85 std::path::Path::new(SYSCTL_PATH).exists()
86 }
87
88 fn collect(&self, n_samples: usize) -> Vec<u8> {
89 let snap1 = match snapshot_sysctl() {
91 Some(s) => s,
92 None => return Vec::new(),
93 };
94
95 thread::sleep(SNAPSHOT_DELAY);
96
97 let snap2 = match snapshot_sysctl() {
98 Some(s) => s,
99 None => return Vec::new(),
100 };
101
102 let mut deltas: Vec<i64> = Vec::new();
104 for (key, v2) in &snap2 {
105 if let Some(v1) = snap1.get(key) {
106 let delta = v2.wrapping_sub(*v1);
107 if delta != 0 {
108 deltas.push(delta);
109 }
110 }
111 }
112
113 extract_delta_bytes_i64(&deltas, n_samples)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn sysctl_info() {
123 let src = SysctlSource::new();
124 assert_eq!(src.name(), "sysctl_deltas");
125 assert_eq!(src.info().category, SourceCategory::System);
126 assert!(!src.info().composite);
127 }
128
129 #[test]
130 #[cfg(target_os = "macos")]
131 #[ignore] fn sysctl_collects_bytes() {
133 let src = SysctlSource::new();
134 if src.is_available() {
135 let data = src.collect(64);
136 assert!(!data.is_empty());
137 assert!(data.len() <= 64);
138 }
139 }
140}