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, 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.
25pub struct DNSTimingSource {
26    info: SourceInfo,
27    /// Monotonically increasing index used to cycle through servers/hostnames.
28    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
57/// Encode a hostname into DNS wire format (sequence of length-prefixed labels).
58///
59/// Example: "example.com" -> b"\x07example\x03com\x00"
60fn 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); // root label
67    out
68}
69
70/// Build a minimal DNS query packet for an A record.
71fn build_dns_query(tx_id: u16, hostname: &str) -> Vec<u8> {
72    let mut pkt = Vec::with_capacity(32);
73    // Header
74    pkt.extend_from_slice(&tx_id.to_be_bytes()); // Transaction ID
75    pkt.extend_from_slice(&[0x01, 0x00]); // Flags: standard query, recursion desired
76    pkt.extend_from_slice(&[0x00, 0x01]); // Questions: 1
77    pkt.extend_from_slice(&[0x00, 0x00]); // Answer RRs: 0
78    pkt.extend_from_slice(&[0x00, 0x00]); // Authority RRs: 0
79    pkt.extend_from_slice(&[0x00, 0x00]); // Additional RRs: 0
80    // Question section
81    pkt.extend_from_slice(&encode_dns_name(hostname));
82    pkt.extend_from_slice(&[0x00, 0x01]); // Type: A
83    pkt.extend_from_slice(&[0x00, 0x01]); // Class: IN
84    pkt
85}
86
87/// Send a single DNS query and return the RTT in nanoseconds, or `None` on
88/// failure.
89fn 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    // Use low 16 bits of current nanosecond timestamp as transaction ID.
96    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        // Try one query; if we get a response within the timeout the source is
114        // usable.
115        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                // Extract least-significant bytes of the RTT (most jittery bits).
132                let nanos_bytes = nanos.to_le_bytes(); // 16 bytes (u128)
133
134                // LSB of the raw RTT
135                entropy.push(nanos_bytes[0]);
136                if entropy.len() >= n_samples {
137                    break;
138                }
139
140                // Second byte has some entropy too
141                entropy.push(nanos_bytes[1]);
142                if entropy.len() >= n_samples {
143                    break;
144                }
145
146                // XOR of byte 0 and byte 1 for mixing
147                entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
148                if entropy.len() >= n_samples {
149                    break;
150                }
151
152                // Timing delta from previous query (inter-query jitter)
153                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            // On failure, just move on to the next server/hostname pair.
164        }
165
166        entropy.truncate(n_samples);
167        entropy
168    }
169}
170
171// ---------------------------------------------------------------------------
172// TCP connect timing source
173// ---------------------------------------------------------------------------
174
175const 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
178/// Entropy source that times TCP three-way handshakes to remote hosts.
179/// The nanosecond-resolution timing captures NIC DMA jitter, kernel buffer
180/// allocation, remote server load, and network path congestion.
181pub struct TCPConnectSource {
182    info: SourceInfo,
183    /// Monotonically increasing index used to cycle through targets.
184    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
213/// Attempt a TCP connect and return the handshake duration in nanoseconds.
214fn 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                // Least-significant bytes of the handshake RTT
244                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                // XOR mix
255                entropy.push(nanos_bytes[0] ^ nanos_bytes[1]);
256                if entropy.len() >= n_samples {
257                    break;
258                }
259
260                // Timing delta from previous handshake
261                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); // length of "example"
286        assert_eq!(&encoded[1..8], b"example");
287        assert_eq!(encoded[8], 3); // length of "com"
288        assert_eq!(&encoded[9..12], b"com");
289        assert_eq!(encoded[12], 0); // root label
290    }
291
292    #[test]
293    fn dns_query_packet_structure() {
294        let pkt = build_dns_query(0x1234, "example.com");
295        // Transaction ID
296        assert_eq!(pkt[0], 0x12);
297        assert_eq!(pkt[1], 0x34);
298        // Flags: standard query, recursion desired
299        assert_eq!(pkt[2], 0x01);
300        assert_eq!(pkt[3], 0x00);
301        // Questions count
302        assert_eq!(pkt[4], 0x00);
303        assert_eq!(pkt[5], 0x01);
304        // The packet should end with type A (0x0001) and class IN (0x0001)
305        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}