Skip to main content

openentropy_core/sources/
network.rs

1//! Network-based entropy sources: DNS query timing and TCP handshake timing.
2//!
3//! These sources exploit the inherent unpredictability in network round-trip
4//! times, which arise from queuing delays, congestion, server load, NIC
5//! interrupt coalescing, and electromagnetic propagation variations.
6
7use 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
13// ---------------------------------------------------------------------------
14// DNS timing source
15// ---------------------------------------------------------------------------
16
17const 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
22/// Entropy source that measures the round-trip time of DNS A-record queries
23/// sent to public resolvers. Timing jitter in the nanosecond range is
24/// harvested as raw entropy.
25///
26/// No tunable parameters — cycles through a fixed set of public DNS servers
27/// and hostnames automatically.
28pub struct DNSTimingSource {
29    /// Monotonically increasing index used to cycle through servers/hostnames.
30    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
61/// Encode a hostname into DNS wire format (sequence of length-prefixed labels).
62///
63/// Example: "example.com" -> b"\x07example\x03com\x00"
64fn 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); // root label
71    out
72}
73
74/// Build a minimal DNS query packet for an A record.
75fn build_dns_query(tx_id: u16, hostname: &str) -> Vec<u8> {
76    let mut pkt = Vec::with_capacity(32);
77    // Header
78    pkt.extend_from_slice(&tx_id.to_be_bytes()); // Transaction ID
79    pkt.extend_from_slice(&[0x01, 0x00]); // Flags: standard query, recursion desired
80    pkt.extend_from_slice(&[0x00, 0x01]); // Questions: 1
81    pkt.extend_from_slice(&[0x00, 0x00]); // Answer RRs: 0
82    pkt.extend_from_slice(&[0x00, 0x00]); // Authority RRs: 0
83    pkt.extend_from_slice(&[0x00, 0x00]); // Additional RRs: 0
84    // Question section
85    pkt.extend_from_slice(&encode_dns_name(hostname));
86    pkt.extend_from_slice(&[0x00, 0x01]); // Type: A
87    pkt.extend_from_slice(&[0x00, 0x01]); // Class: IN
88    pkt
89}
90
91/// Send a single DNS query and return the RTT in nanoseconds, or `None` on
92/// failure.
93fn 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    // Use low 16 bits of current nanosecond timestamp as transaction ID.
100    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        // Try one query; if we get a response within the timeout the source is
118        // usable.
119        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                // Extract least-significant bytes of the RTT (most jittery bits).
136                let nanos_bytes = nanos.to_le_bytes(); // 16 bytes (u128)
137
138                // LSB of the raw RTT
139                entropy.push(nanos_bytes[0]);
140                if entropy.len() >= n_samples {
141                    break;
142                }
143
144                // Second byte has some entropy too
145                entropy.push(nanos_bytes[1]);
146                if entropy.len() >= n_samples {
147                    break;
148                }
149
150                // XOR of byte 0 and byte 1 for mixing
151                entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
152                if entropy.len() >= n_samples {
153                    break;
154                }
155
156                // Timing delta from previous query (inter-query jitter)
157                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            // On failure, just move on to the next server/hostname pair.
168        }
169
170        entropy.truncate(n_samples);
171        entropy
172    }
173}
174
175// ---------------------------------------------------------------------------
176// TCP connect timing source
177// ---------------------------------------------------------------------------
178
179const 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
182/// Entropy source that times TCP three-way handshakes to remote hosts.
183/// The nanosecond-resolution timing captures NIC DMA jitter, kernel buffer
184/// allocation, remote server load, and network path congestion.
185///
186/// No tunable parameters — cycles through a fixed set of TCP targets
187/// automatically.
188pub struct TCPConnectSource {
189    /// Monotonically increasing index used to cycle through targets.
190    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
221/// Attempt a TCP connect and return the handshake duration in nanoseconds.
222fn 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                // Least-significant bytes of the handshake RTT
252                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                // XOR mix
263                entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
264                if entropy.len() >= n_samples {
265                    break;
266                }
267
268                // Timing delta from previous handshake
269                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); // length of "example"
294        assert_eq!(&encoded[1..8], b"example");
295        assert_eq!(encoded[8], 3); // length of "com"
296        assert_eq!(&encoded[9..12], b"com");
297        assert_eq!(encoded[12], 0); // root label
298    }
299
300    #[test]
301    fn dns_query_packet_structure() {
302        let pkt = build_dns_query(0x1234, "example.com");
303        // Transaction ID
304        assert_eq!(pkt[0], 0x12);
305        assert_eq!(pkt[1], 0x34);
306        // Flags: standard query, recursion desired
307        assert_eq!(pkt[2], 0x01);
308        assert_eq!(pkt[3], 0x00);
309        // Questions count
310        assert_eq!(pkt[4], 0x00);
311        assert_eq!(pkt[5], 0x01);
312        // The packet should end with type A (0x0001) and class IN (0x0001)
313        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] // Requires network connectivity
335    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] // Requires network connectivity
346    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}