Skip to main content

openentropy_core/sources/network/
dns_timing.rs

1//! DNS timing entropy source.
2//!
3//! Exploits the inherent unpredictability in DNS query round-trip times,
4//! which arise from queuing delays, congestion, server load, NIC
5//! interrupt coalescing, and electromagnetic propagation variations.
6
7use 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
15// ---------------------------------------------------------------------------
16// DNS timing source
17// ---------------------------------------------------------------------------
18
19const 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
24/// Entropy source that measures the round-trip time of DNS A-record queries
25/// sent to public resolvers. Timing jitter in the nanosecond range is
26/// harvested as raw entropy.
27///
28/// No tunable parameters — cycles through a fixed set of public DNS servers
29/// and hostnames automatically.
30pub struct DNSTimingSource {
31    /// Monotonically increasing index used to cycle through servers/hostnames.
32    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
64/// Encode a hostname into DNS wire format (sequence of length-prefixed labels).
65///
66/// Example: "example.com" -> b"\x07example\x03com\x00"
67fn 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); // root label
74    out
75}
76
77/// Build a minimal DNS query packet for an A record.
78fn build_dns_query(tx_id: u16, hostname: &str) -> Vec<u8> {
79    let mut pkt = Vec::with_capacity(32);
80    // Header
81    pkt.extend_from_slice(&tx_id.to_be_bytes()); // Transaction ID
82    pkt.extend_from_slice(&[0x01, 0x00]); // Flags: standard query, recursion desired
83    pkt.extend_from_slice(&[0x00, 0x01]); // Questions: 1
84    pkt.extend_from_slice(&[0x00, 0x00]); // Answer RRs: 0
85    pkt.extend_from_slice(&[0x00, 0x00]); // Authority RRs: 0
86    pkt.extend_from_slice(&[0x00, 0x00]); // Additional RRs: 0
87    // Question section
88    pkt.extend_from_slice(&encode_dns_name(hostname));
89    pkt.extend_from_slice(&[0x00, 0x01]); // Type: A
90    pkt.extend_from_slice(&[0x00, 0x01]); // Class: IN
91    pkt
92}
93
94/// Send a single DNS query and return the RTT in nanoseconds, or `None` on
95/// failure.
96fn 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    // Use low 16 bits of wall clock nanoseconds as transaction ID.
103    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    // Read responses until we get one matching our transaction ID or timeout.
116    // Cap attempts to avoid spinning on a flood of stale responses.
117    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        // Wrong txid — stale response from a prior query. Try again.
123    }
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); // length of "example"
174        assert_eq!(&encoded[1..8], b"example");
175        assert_eq!(encoded[8], 3); // length of "com"
176        assert_eq!(&encoded[9..12], b"com");
177        assert_eq!(encoded[12], 0); // root label
178    }
179
180    #[test]
181    fn dns_query_packet_structure() {
182        let pkt = build_dns_query(0x1234, "example.com");
183        // Transaction ID
184        assert_eq!(pkt[0], 0x12);
185        assert_eq!(pkt[1], 0x34);
186        // Flags: standard query, recursion desired
187        assert_eq!(pkt[2], 0x01);
188        assert_eq!(pkt[3], 0x00);
189        // Questions count
190        assert_eq!(pkt[4], 0x00);
191        assert_eq!(pkt[5], 0x01);
192        // The packet should end with type A (0x0001) and class IN (0x0001)
193        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] // Requires network connectivity
207    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}