openentropy_core/sources/network/
dns_timing.rs1use std::net::{SocketAddr, UdpSocket};
8use std::sync::OnceLock;
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::time::{Duration, Instant};
11
12use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
13use crate::sources::helpers::extract_timing_entropy;
14
15const DNS_SERVERS: &[&str] = &["8.8.8.8", "1.1.1.1", "9.9.9.9"];
20const DNS_HOSTNAMES: &[&str] = &["example.com", "google.com", "github.com"];
21const DNS_PORT: u16 = 53;
22const DNS_TIMEOUT: Duration = Duration::from_secs(2);
23
24pub struct DNSTimingSource {
31 index: AtomicUsize,
33}
34
35static DNS_TIMING_INFO: SourceInfo = SourceInfo {
36 name: "dns_timing",
37 description: "Round-trip timing of DNS A-record queries to public resolvers",
38 physics: "Measures round-trip time of DNS queries to public resolvers. \
39 Jitter comes from: network switch queuing, router buffer state, \
40 ISP congestion, DNS server load, TCP/IP stack scheduling, NIC \
41 interrupt coalescing, and electromagnetic propagation variations.",
42 category: SourceCategory::Network,
43 platform: Platform::Any,
44 requirements: &[],
45 entropy_rate_estimate: 3.0,
46 composite: false,
47 is_fast: false,
48};
49
50impl DNSTimingSource {
51 pub fn new() -> Self {
52 Self {
53 index: AtomicUsize::new(0),
54 }
55 }
56}
57
58impl Default for DNSTimingSource {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64fn encode_dns_name(hostname: &str) -> Vec<u8> {
68 let mut out = Vec::with_capacity(hostname.len() + 2);
69 for label in hostname.split('.') {
70 out.push(label.len() as u8);
71 out.extend_from_slice(label.as_bytes());
72 }
73 out.push(0); out
75}
76
77fn build_dns_query(tx_id: u16, hostname: &str) -> Vec<u8> {
79 let mut pkt = Vec::with_capacity(32);
80 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));
89 pkt.extend_from_slice(&[0x00, 0x01]); pkt.extend_from_slice(&[0x00, 0x01]); pkt
92}
93
94fn dns_query_rtt(server: &str, hostname: &str, timeout: Duration) -> Option<u128> {
97 let addr: SocketAddr = format!("{}:{}", server, DNS_PORT).parse().ok()?;
98 let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
99 socket.set_read_timeout(Some(timeout)).ok()?;
100 socket.set_write_timeout(Some(timeout)).ok()?;
101
102 let tx_id = (std::time::SystemTime::now()
104 .duration_since(std::time::SystemTime::UNIX_EPOCH)
105 .unwrap_or_default()
106 .as_nanos()
107 & 0xFFFF) as u16;
108 let query = build_dns_query(tx_id, hostname);
109
110 let tx_id_bytes = tx_id.to_be_bytes();
111 let start = Instant::now();
112 socket.send_to(&query, addr).ok()?;
113
114 let mut buf = [0u8; 512];
115 for _ in 0..8 {
118 let (n, _src) = socket.recv_from(&mut buf).ok()?;
119 if n >= 2 && buf[0] == tx_id_bytes[0] && buf[1] == tx_id_bytes[1] {
120 return Some(start.elapsed().as_nanos());
121 }
122 }
124 None
125}
126
127impl EntropySource for DNSTimingSource {
128 fn info(&self) -> &SourceInfo {
129 &DNS_TIMING_INFO
130 }
131
132 fn is_available(&self) -> bool {
133 static DNS_AVAILABLE: OnceLock<bool> = OnceLock::new();
134 *DNS_AVAILABLE
135 .get_or_init(|| dns_query_rtt(DNS_SERVERS[0], DNS_HOSTNAMES[0], DNS_TIMEOUT).is_some())
136 }
137
138 fn collect(&self, n_samples: usize) -> Vec<u8> {
139 let server_count = DNS_SERVERS.len();
140 let hostname_count = DNS_HOSTNAMES.len();
141
142 let raw_count = n_samples + 64;
143 let mut timings = Vec::with_capacity(raw_count);
144 let max_iterations = raw_count * 4;
145 let mut iter_count = 0;
146 let deadline = Instant::now() + Duration::from_secs(4);
147
148 while timings.len() < raw_count && iter_count < max_iterations {
149 if Instant::now() >= deadline {
150 break;
151 }
152 iter_count += 1;
153 let idx = self.index.fetch_add(1, Ordering::Relaxed);
154 let server = DNS_SERVERS[idx % server_count];
155 let hostname = DNS_HOSTNAMES[idx % hostname_count];
156
157 if let Some(nanos) = dns_query_rtt(server, hostname, DNS_TIMEOUT) {
158 timings.push(nanos as u64);
159 }
160 }
161
162 extract_timing_entropy(&timings, n_samples)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn dns_name_encoding() {
172 let encoded = encode_dns_name("example.com");
173 assert_eq!(encoded[0], 7); assert_eq!(&encoded[1..8], b"example");
175 assert_eq!(encoded[8], 3); assert_eq!(&encoded[9..12], b"com");
177 assert_eq!(encoded[12], 0); }
179
180 #[test]
181 fn dns_query_packet_structure() {
182 let pkt = build_dns_query(0x1234, "example.com");
183 assert_eq!(pkt[0], 0x12);
185 assert_eq!(pkt[1], 0x34);
186 assert_eq!(pkt[2], 0x01);
188 assert_eq!(pkt[3], 0x00);
189 assert_eq!(pkt[4], 0x00);
191 assert_eq!(pkt[5], 0x01);
192 let len = pkt.len();
194 assert_eq!(&pkt[len - 4..], &[0x00, 0x01, 0x00, 0x01]);
195 }
196
197 #[test]
198 fn dns_source_info() {
199 let src = DNSTimingSource::new();
200 assert_eq!(src.info().name, "dns_timing");
201 assert_eq!(src.info().category, SourceCategory::Network);
202 assert!((src.info().entropy_rate_estimate - 3.0).abs() < f64::EPSILON);
203 }
204
205 #[test]
206 #[ignore] fn dns_timing_collects_bytes() {
208 let src = DNSTimingSource::new();
209 if src.is_available() {
210 let data = src.collect(32);
211 assert!(!data.is_empty());
212 assert!(data.len() <= 32);
213 }
214 }
215}