Skip to main content

openentropy_core/sources/network/
tcp_connect.rs

1//! TCP connect timing entropy source.
2//!
3//! Times TCP three-way handshakes to remote hosts. The nanosecond-resolution
4//! timing captures NIC DMA jitter, kernel buffer allocation, remote server
5//! load, and network path congestion.
6
7use std::net::{SocketAddr, TcpStream};
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// TCP connect timing source
17// ---------------------------------------------------------------------------
18
19const TCP_TARGETS: &[&str] = &["8.8.8.8:53", "1.1.1.1:53", "9.9.9.9:53"];
20const TCP_TIMEOUT: Duration = Duration::from_secs(2);
21
22/// Entropy source that times TCP three-way handshakes to remote hosts.
23/// The nanosecond-resolution timing captures NIC DMA jitter, kernel buffer
24/// allocation, remote server load, and network path congestion.
25///
26/// No tunable parameters — cycles through a fixed set of TCP targets
27/// automatically.
28pub struct TCPConnectSource {
29    /// Monotonically increasing index used to cycle through targets.
30    index: AtomicUsize,
31}
32
33static TCP_CONNECT_INFO: SourceInfo = SourceInfo {
34    name: "tcp_connect_timing",
35    description: "Nanosecond timing of TCP three-way handshakes to remote hosts",
36    physics: "Times the TCP three-way handshake (SYN -> SYN-ACK -> ACK). \
37              The timing captures: NIC DMA transfer jitter, kernel socket \
38              buffer allocation, remote server load, network path congestion, \
39              and router queuing delays.",
40    category: SourceCategory::Network,
41    platform: Platform::Any,
42    requirements: &[],
43    entropy_rate_estimate: 2.0,
44    composite: false,
45    is_fast: false,
46};
47
48impl TCPConnectSource {
49    pub fn new() -> Self {
50        Self {
51            index: AtomicUsize::new(0),
52        }
53    }
54}
55
56impl Default for TCPConnectSource {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62/// Attempt a TCP connect and return the handshake duration in nanoseconds.
63fn tcp_connect_rtt(target: &str, timeout: Duration) -> Option<u128> {
64    let addr: SocketAddr = target.parse().ok()?;
65    let start = Instant::now();
66    let _stream = TcpStream::connect_timeout(&addr, timeout).ok()?;
67    Some(start.elapsed().as_nanos())
68}
69
70impl EntropySource for TCPConnectSource {
71    fn info(&self) -> &SourceInfo {
72        &TCP_CONNECT_INFO
73    }
74
75    fn is_available(&self) -> bool {
76        static TCP_AVAILABLE: OnceLock<bool> = OnceLock::new();
77        *TCP_AVAILABLE.get_or_init(|| tcp_connect_rtt(TCP_TARGETS[0], TCP_TIMEOUT).is_some())
78    }
79
80    fn collect(&self, n_samples: usize) -> Vec<u8> {
81        let target_count = TCP_TARGETS.len();
82
83        let raw_count = n_samples + 64;
84        let mut timings = Vec::with_capacity(raw_count);
85        let max_iterations = raw_count * 4;
86        let mut iter_count = 0;
87        let deadline = Instant::now() + Duration::from_secs(4);
88
89        while timings.len() < raw_count && iter_count < max_iterations {
90            if Instant::now() >= deadline {
91                break;
92            }
93            iter_count += 1;
94            let idx = self.index.fetch_add(1, Ordering::Relaxed);
95            let target = TCP_TARGETS[idx % target_count];
96
97            if let Some(nanos) = tcp_connect_rtt(target, TCP_TIMEOUT) {
98                timings.push(nanos as u64);
99            }
100        }
101
102        extract_timing_entropy(&timings, n_samples)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn tcp_source_info() {
112        let src = TCPConnectSource::new();
113        assert_eq!(src.info().name, "tcp_connect_timing");
114        assert_eq!(src.info().category, SourceCategory::Network);
115        assert!((src.info().entropy_rate_estimate - 2.0).abs() < f64::EPSILON);
116    }
117
118    #[test]
119    #[ignore] // Requires network connectivity
120    fn tcp_connect_collects_bytes() {
121        let src = TCPConnectSource::new();
122        if src.is_available() {
123            let data = src.collect(32);
124            assert!(!data.is_empty());
125            assert!(data.len() <= 32);
126        }
127    }
128}