openentropy_core/sources/
network.rs1use std::net::{SocketAddr, TcpStream, UdpSocket};
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::time::{Duration, Instant};
10
11use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
12
13const DNS_SERVERS: &[&str] = &["8.8.8.8", "1.1.1.1", "9.9.9.9"];
18const DNS_HOSTNAMES: &[&str] = &["example.com", "google.com", "github.com"];
19const DNS_PORT: u16 = 53;
20const DNS_TIMEOUT: Duration = Duration::from_secs(2);
21
22pub struct DNSTimingSource {
29 index: AtomicUsize,
31}
32
33static DNS_TIMING_INFO: SourceInfo = SourceInfo {
34 name: "dns_timing",
35 description: "Round-trip timing of DNS A-record queries to public resolvers",
36 physics: "Measures round-trip time of DNS queries to public resolvers. \
37 Jitter comes from: network switch queuing, router buffer state, \
38 ISP congestion, DNS server load, TCP/IP stack scheduling, NIC \
39 interrupt coalescing, and electromagnetic propagation variations.",
40 category: SourceCategory::Network,
41 platform: Platform::Any,
42 requirements: &[],
43 entropy_rate_estimate: 100.0,
44 composite: false,
45};
46
47impl DNSTimingSource {
48 pub fn new() -> Self {
49 Self {
50 index: AtomicUsize::new(0),
51 }
52 }
53}
54
55impl Default for DNSTimingSource {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61fn encode_dns_name(hostname: &str) -> Vec<u8> {
65 let mut out = Vec::with_capacity(hostname.len() + 2);
66 for label in hostname.split('.') {
67 out.push(label.len() as u8);
68 out.extend_from_slice(label.as_bytes());
69 }
70 out.push(0); out
72}
73
74fn build_dns_query(tx_id: u16, hostname: &str) -> Vec<u8> {
76 let mut pkt = Vec::with_capacity(32);
77 pkt.extend_from_slice(&tx_id.to_be_bytes()); pkt.extend_from_slice(&[0x01, 0x00]); pkt.extend_from_slice(&[0x00, 0x01]); pkt.extend_from_slice(&[0x00, 0x00]); pkt.extend_from_slice(&[0x00, 0x00]); pkt.extend_from_slice(&[0x00, 0x00]); pkt.extend_from_slice(&encode_dns_name(hostname));
86 pkt.extend_from_slice(&[0x00, 0x01]); pkt.extend_from_slice(&[0x00, 0x01]); pkt
89}
90
91fn dns_query_rtt(server: &str, hostname: &str, timeout: Duration) -> Option<u128> {
94 let addr: SocketAddr = format!("{}:{}", server, DNS_PORT).parse().ok()?;
95 let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
96 socket.set_read_timeout(Some(timeout)).ok()?;
97 socket.set_write_timeout(Some(timeout)).ok()?;
98
99 let tx_id = (Instant::now().elapsed().as_nanos() & 0xFFFF) as u16;
101 let query = build_dns_query(tx_id, hostname);
102
103 let start = Instant::now();
104 socket.send_to(&query, addr).ok()?;
105
106 let mut buf = [0u8; 512];
107 let _n = socket.recv_from(&mut buf).ok()?;
108 Some(start.elapsed().as_nanos())
109}
110
111impl EntropySource for DNSTimingSource {
112 fn info(&self) -> &SourceInfo {
113 &DNS_TIMING_INFO
114 }
115
116 fn is_available(&self) -> bool {
117 dns_query_rtt(DNS_SERVERS[0], DNS_HOSTNAMES[0], DNS_TIMEOUT).is_some()
120 }
121
122 fn collect(&self, n_samples: usize) -> Vec<u8> {
123 let mut entropy = Vec::with_capacity(n_samples);
124 let server_count = DNS_SERVERS.len();
125 let hostname_count = DNS_HOSTNAMES.len();
126
127 let mut prev_nanos: Option<u128> = None;
128
129 while entropy.len() < n_samples {
130 let idx = self.index.fetch_add(1, Ordering::Relaxed);
131 let server = DNS_SERVERS[idx % server_count];
132 let hostname = DNS_HOSTNAMES[idx % hostname_count];
133
134 if let Some(nanos) = dns_query_rtt(server, hostname, DNS_TIMEOUT) {
135 let nanos_bytes = nanos.to_le_bytes(); entropy.push(nanos_bytes[0]);
140 if entropy.len() >= n_samples {
141 break;
142 }
143
144 entropy.push(nanos_bytes[1]);
146 if entropy.len() >= n_samples {
147 break;
148 }
149
150 entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
152 if entropy.len() >= n_samples {
153 break;
154 }
155
156 if let Some(prev) = prev_nanos {
158 let delta = nanos.abs_diff(prev);
159 let delta_bytes = delta.to_le_bytes();
160 entropy.push(delta_bytes[0]);
161 if entropy.len() < n_samples {
162 entropy.push(delta_bytes[1]);
163 }
164 }
165 prev_nanos = Some(nanos);
166 }
167 }
169
170 entropy.truncate(n_samples);
171 entropy
172 }
173}
174
175const TCP_TARGETS: &[&str] = &["8.8.8.8:53", "1.1.1.1:53", "9.9.9.9:53"];
180const TCP_TIMEOUT: Duration = Duration::from_secs(2);
181
182pub struct TCPConnectSource {
189 index: AtomicUsize,
191}
192
193static TCP_CONNECT_INFO: SourceInfo = SourceInfo {
194 name: "tcp_connect_timing",
195 description: "Nanosecond timing of TCP three-way handshakes to remote hosts",
196 physics: "Times the TCP three-way handshake (SYN -> SYN-ACK -> ACK). \
197 The timing captures: NIC DMA transfer jitter, kernel socket \
198 buffer allocation, remote server load, network path congestion, \
199 and router queuing delays.",
200 category: SourceCategory::Network,
201 platform: Platform::Any,
202 requirements: &[],
203 entropy_rate_estimate: 50.0,
204 composite: false,
205};
206
207impl TCPConnectSource {
208 pub fn new() -> Self {
209 Self {
210 index: AtomicUsize::new(0),
211 }
212 }
213}
214
215impl Default for TCPConnectSource {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221fn tcp_connect_rtt(target: &str, timeout: Duration) -> Option<u128> {
223 let addr: SocketAddr = target.parse().ok()?;
224 let start = Instant::now();
225 let _stream = TcpStream::connect_timeout(&addr, timeout).ok()?;
226 Some(start.elapsed().as_nanos())
227}
228
229impl EntropySource for TCPConnectSource {
230 fn info(&self) -> &SourceInfo {
231 &TCP_CONNECT_INFO
232 }
233
234 fn is_available(&self) -> bool {
235 tcp_connect_rtt(TCP_TARGETS[0], TCP_TIMEOUT).is_some()
236 }
237
238 fn collect(&self, n_samples: usize) -> Vec<u8> {
239 let mut entropy = Vec::with_capacity(n_samples);
240 let target_count = TCP_TARGETS.len();
241
242 let mut prev_nanos: Option<u128> = None;
243
244 while entropy.len() < n_samples {
245 let idx = self.index.fetch_add(1, Ordering::Relaxed);
246 let target = TCP_TARGETS[idx % target_count];
247
248 if let Some(nanos) = tcp_connect_rtt(target, TCP_TIMEOUT) {
249 let nanos_bytes = nanos.to_le_bytes();
250
251 entropy.push(nanos_bytes[0]);
253 if entropy.len() >= n_samples {
254 break;
255 }
256
257 entropy.push(nanos_bytes[1]);
258 if entropy.len() >= n_samples {
259 break;
260 }
261
262 entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
264 if entropy.len() >= n_samples {
265 break;
266 }
267
268 if let Some(prev) = prev_nanos {
270 let delta = nanos.abs_diff(prev);
271 let delta_bytes = delta.to_le_bytes();
272 entropy.push(delta_bytes[0]);
273 if entropy.len() < n_samples {
274 entropy.push(delta_bytes[1]);
275 }
276 }
277 prev_nanos = Some(nanos);
278 }
279 }
280
281 entropy.truncate(n_samples);
282 entropy
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn dns_name_encoding() {
292 let encoded = encode_dns_name("example.com");
293 assert_eq!(encoded[0], 7); assert_eq!(&encoded[1..8], b"example");
295 assert_eq!(encoded[8], 3); assert_eq!(&encoded[9..12], b"com");
297 assert_eq!(encoded[12], 0); }
299
300 #[test]
301 fn dns_query_packet_structure() {
302 let pkt = build_dns_query(0x1234, "example.com");
303 assert_eq!(pkt[0], 0x12);
305 assert_eq!(pkt[1], 0x34);
306 assert_eq!(pkt[2], 0x01);
308 assert_eq!(pkt[3], 0x00);
309 assert_eq!(pkt[4], 0x00);
311 assert_eq!(pkt[5], 0x01);
312 let len = pkt.len();
314 assert_eq!(&pkt[len - 4..], &[0x00, 0x01, 0x00, 0x01]);
315 }
316
317 #[test]
318 fn dns_source_info() {
319 let src = DNSTimingSource::new();
320 assert_eq!(src.info().name, "dns_timing");
321 assert_eq!(src.info().category, SourceCategory::Network);
322 assert!((src.info().entropy_rate_estimate - 100.0).abs() < f64::EPSILON);
323 }
324
325 #[test]
326 fn tcp_source_info() {
327 let src = TCPConnectSource::new();
328 assert_eq!(src.info().name, "tcp_connect_timing");
329 assert_eq!(src.info().category, SourceCategory::Network);
330 assert!((src.info().entropy_rate_estimate - 50.0).abs() < f64::EPSILON);
331 }
332
333 #[test]
334 #[ignore] fn dns_timing_collects_bytes() {
336 let src = DNSTimingSource::new();
337 if src.is_available() {
338 let data = src.collect(32);
339 assert!(!data.is_empty());
340 assert!(data.len() <= 32);
341 }
342 }
343
344 #[test]
345 #[ignore] fn tcp_connect_collects_bytes() {
347 let src = TCPConnectSource::new();
348 if src.is_available() {
349 let data = src.collect(32);
350 assert!(!data.is_empty());
351 assert!(data.len() <= 32);
352 }
353 }
354}