openentropy_core/sources/system/
sysctl.rs1use std::collections::HashMap;
6use std::thread;
7use std::time::Duration;
8
9use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
10
11use crate::sources::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: 3.0,
38 composite: false,
39 is_fast: false,
40};
41
42impl SysctlSource {
43 pub fn new() -> Self {
44 Self
45 }
46}
47
48impl Default for SysctlSource {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54fn snapshot_sysctl() -> Option<HashMap<String, i64>> {
58 let stdout = run_command(SYSCTL_PATH, &["-a"])?;
59 let mut map = HashMap::new();
60
61 for line in stdout.lines() {
62 let (key, val_str) = if let Some(idx) = line.find(": ") {
64 (&line[..idx], line[idx + 2..].trim())
65 } else if let Some(idx) = line.find(" = ") {
66 (&line[..idx], line[idx + 3..].trim())
67 } else {
68 continue;
69 };
70
71 if let Ok(v) = val_str.parse::<i64>() {
73 map.insert(key.to_string(), v);
74 }
75 }
76
77 Some(map)
78}
79
80impl EntropySource for SysctlSource {
81 fn info(&self) -> &SourceInfo {
82 &SYSCTL_INFO
83 }
84
85 fn is_available(&self) -> bool {
86 std::path::Path::new(SYSCTL_PATH).exists()
87 }
88
89 fn collect(&self, n_samples: usize) -> Vec<u8> {
90 let snap1 = match snapshot_sysctl() {
92 Some(s) => s,
93 None => return Vec::new(),
94 };
95
96 thread::sleep(SNAPSHOT_DELAY);
97
98 let snap2 = match snapshot_sysctl() {
99 Some(s) => s,
100 None => return Vec::new(),
101 };
102
103 let mut deltas: Vec<i64> = Vec::new();
105 for (key, v2) in &snap2 {
106 if let Some(v1) = snap1.get(key) {
107 let delta = v2.wrapping_sub(*v1);
108 if delta != 0 {
109 deltas.push(delta);
110 }
111 }
112 }
113
114 extract_delta_bytes_i64(&deltas, n_samples)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn sysctl_info() {
124 let src = SysctlSource::new();
125 assert_eq!(src.name(), "sysctl_deltas");
126 assert_eq!(src.info().category, SourceCategory::System);
127 assert!(!src.info().composite);
128 }
129
130 #[test]
131 #[cfg(target_os = "macos")]
132 #[ignore] fn sysctl_collects_bytes() {
134 let src = SysctlSource::new();
135 if src.is_available() {
136 let data = src.collect(64);
137 assert!(!data.is_empty());
138 assert!(data.len() <= 64);
139 }
140 }
141}