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, 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 {
26 info: SourceInfo,
27 index: AtomicUsize,
29}
30
31impl DNSTimingSource {
32 pub fn new() -> Self {
33 Self {
34 info: SourceInfo {
35 name: "dns_timing",
36 description: "Round-trip timing of DNS A-record queries to public resolvers",
37 physics: "Measures round-trip time of DNS queries to public resolvers. \
38 Jitter comes from: network switch queuing, router buffer state, \
39 ISP congestion, DNS server load, TCP/IP stack scheduling, NIC \
40 interrupt coalescing, and electromagnetic propagation variations.",
41 category: SourceCategory::Network,
42 platform_requirements: &[],
43 entropy_rate_estimate: 100.0,
44 composite: false,
45 },
46 index: AtomicUsize::new(0),
47 }
48 }
49}
50
51impl Default for DNSTimingSource {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57fn encode_dns_name(hostname: &str) -> Vec<u8> {
61 let mut out = Vec::with_capacity(hostname.len() + 2);
62 for label in hostname.split('.') {
63 out.push(label.len() as u8);
64 out.extend_from_slice(label.as_bytes());
65 }
66 out.push(0); out
68}
69
70fn build_dns_query(tx_id: u16, hostname: &str) -> Vec<u8> {
72 let mut pkt = Vec::with_capacity(32);
73 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));
82 pkt.extend_from_slice(&[0x00, 0x01]); pkt.extend_from_slice(&[0x00, 0x01]); pkt
85}
86
87fn dns_query_rtt(server: &str, hostname: &str, timeout: Duration) -> Option<u128> {
90 let addr: SocketAddr = format!("{}:{}", server, DNS_PORT).parse().ok()?;
91 let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
92 socket.set_read_timeout(Some(timeout)).ok()?;
93 socket.set_write_timeout(Some(timeout)).ok()?;
94
95 let tx_id = (Instant::now().elapsed().as_nanos() & 0xFFFF) as u16;
97 let query = build_dns_query(tx_id, hostname);
98
99 let start = Instant::now();
100 socket.send_to(&query, addr).ok()?;
101
102 let mut buf = [0u8; 512];
103 let _n = socket.recv_from(&mut buf).ok()?;
104 Some(start.elapsed().as_nanos())
105}
106
107impl EntropySource for DNSTimingSource {
108 fn info(&self) -> &SourceInfo {
109 &self.info
110 }
111
112 fn is_available(&self) -> bool {
113 dns_query_rtt(DNS_SERVERS[0], DNS_HOSTNAMES[0], DNS_TIMEOUT).is_some()
116 }
117
118 fn collect(&self, n_samples: usize) -> Vec<u8> {
119 let mut entropy = Vec::with_capacity(n_samples);
120 let server_count = DNS_SERVERS.len();
121 let hostname_count = DNS_HOSTNAMES.len();
122
123 let mut prev_nanos: Option<u128> = None;
124
125 while entropy.len() < n_samples {
126 let idx = self.index.fetch_add(1, Ordering::Relaxed);
127 let server = DNS_SERVERS[idx % server_count];
128 let hostname = DNS_HOSTNAMES[idx % hostname_count];
129
130 if let Some(nanos) = dns_query_rtt(server, hostname, DNS_TIMEOUT) {
131 let nanos_bytes = nanos.to_le_bytes(); entropy.push(nanos_bytes[0]);
136 if entropy.len() >= n_samples {
137 break;
138 }
139
140 entropy.push(nanos_bytes[1]);
142 if entropy.len() >= n_samples {
143 break;
144 }
145
146 entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
148 if entropy.len() >= n_samples {
149 break;
150 }
151
152 if let Some(prev) = prev_nanos {
154 let delta = nanos.abs_diff(prev);
155 let delta_bytes = delta.to_le_bytes();
156 entropy.push(delta_bytes[0]);
157 if entropy.len() < n_samples {
158 entropy.push(delta_bytes[1]);
159 }
160 }
161 prev_nanos = Some(nanos);
162 }
163 }
165
166 entropy.truncate(n_samples);
167 entropy
168 }
169}
170
171const TCP_TARGETS: &[&str] = &["8.8.8.8:53", "1.1.1.1:53", "9.9.9.9:53"];
176const TCP_TIMEOUT: Duration = Duration::from_secs(2);
177
178pub struct TCPConnectSource {
182 info: SourceInfo,
183 index: AtomicUsize,
185}
186
187impl TCPConnectSource {
188 pub fn new() -> Self {
189 Self {
190 info: SourceInfo {
191 name: "tcp_connect_timing",
192 description: "Nanosecond timing of TCP three-way handshakes to remote hosts",
193 physics: "Times the TCP three-way handshake (SYN -> SYN-ACK -> ACK). \
194 The timing captures: NIC DMA transfer jitter, kernel socket \
195 buffer allocation, remote server load, network path congestion, \
196 and router queuing delays.",
197 category: SourceCategory::Network,
198 platform_requirements: &[],
199 entropy_rate_estimate: 50.0,
200 composite: false,
201 },
202 index: AtomicUsize::new(0),
203 }
204 }
205}
206
207impl Default for TCPConnectSource {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213fn tcp_connect_rtt(target: &str, timeout: Duration) -> Option<u128> {
215 let addr: SocketAddr = target.parse().ok()?;
216 let start = Instant::now();
217 let _stream = TcpStream::connect_timeout(&addr, timeout).ok()?;
218 Some(start.elapsed().as_nanos())
219}
220
221impl EntropySource for TCPConnectSource {
222 fn info(&self) -> &SourceInfo {
223 &self.info
224 }
225
226 fn is_available(&self) -> bool {
227 tcp_connect_rtt(TCP_TARGETS[0], TCP_TIMEOUT).is_some()
228 }
229
230 fn collect(&self, n_samples: usize) -> Vec<u8> {
231 let mut entropy = Vec::with_capacity(n_samples);
232 let target_count = TCP_TARGETS.len();
233
234 let mut prev_nanos: Option<u128> = None;
235
236 while entropy.len() < n_samples {
237 let idx = self.index.fetch_add(1, Ordering::Relaxed);
238 let target = TCP_TARGETS[idx % target_count];
239
240 if let Some(nanos) = tcp_connect_rtt(target, TCP_TIMEOUT) {
241 let nanos_bytes = nanos.to_le_bytes();
242
243 entropy.push(nanos_bytes[0]);
245 if entropy.len() >= n_samples {
246 break;
247 }
248
249 entropy.push(nanos_bytes[1]);
250 if entropy.len() >= n_samples {
251 break;
252 }
253
254 entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
256 if entropy.len() >= n_samples {
257 break;
258 }
259
260 if let Some(prev) = prev_nanos {
262 let delta = nanos.abs_diff(prev);
263 let delta_bytes = delta.to_le_bytes();
264 entropy.push(delta_bytes[0]);
265 if entropy.len() < n_samples {
266 entropy.push(delta_bytes[1]);
267 }
268 }
269 prev_nanos = Some(nanos);
270 }
271 }
272
273 entropy.truncate(n_samples);
274 entropy
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn dns_name_encoding() {
284 let encoded = encode_dns_name("example.com");
285 assert_eq!(encoded[0], 7); assert_eq!(&encoded[1..8], b"example");
287 assert_eq!(encoded[8], 3); assert_eq!(&encoded[9..12], b"com");
289 assert_eq!(encoded[12], 0); }
291
292 #[test]
293 fn dns_query_packet_structure() {
294 let pkt = build_dns_query(0x1234, "example.com");
295 assert_eq!(pkt[0], 0x12);
297 assert_eq!(pkt[1], 0x34);
298 assert_eq!(pkt[2], 0x01);
300 assert_eq!(pkt[3], 0x00);
301 assert_eq!(pkt[4], 0x00);
303 assert_eq!(pkt[5], 0x01);
304 let len = pkt.len();
306 assert_eq!(&pkt[len - 4..], &[0x00, 0x01, 0x00, 0x01]);
307 }
308
309 #[test]
310 fn dns_source_info() {
311 let src = DNSTimingSource::new();
312 assert_eq!(src.info().name, "dns_timing");
313 assert_eq!(src.info().category, SourceCategory::Network);
314 assert!((src.info().entropy_rate_estimate - 100.0).abs() < f64::EPSILON);
315 }
316
317 #[test]
318 fn tcp_source_info() {
319 let src = TCPConnectSource::new();
320 assert_eq!(src.info().name, "tcp_connect_timing");
321 assert_eq!(src.info().category, SourceCategory::Network);
322 assert!((src.info().entropy_rate_estimate - 50.0).abs() < f64::EPSILON);
323 }
324}