Skip to main content

cf_mach/nq_core/connection/
mod.rs

1// Copyright (c) 2023-2024 Cloudflare, Inc.
2// Licensed under the BSD-3-Clause license found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
3
4mod http;
5mod map;
6
7use std::time::Duration;
8
9use crate::nq_core::Timestamp;
10
11pub use self::http::{EstablishedConnection, set_insecure_tls};
12pub use self::map::ConnectionManager;
13
14/// The L7 type of a connection.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ConnectionType {
17    /// Create an HTTP/1.1 connection. To disable tls, set `use_tls: false`.
18    H1 {
19        /// enable tls for this HTTP/1.1 connection.
20        use_tls: bool,
21    },
22    /// Create an HTTP/2 connection.
23    H2,
24    /// Create an HTTP/3 connection.
25    H3,
26}
27
28impl ConnectionType {
29    /// Creates an HTTP/1.1 connection type.
30    pub fn h1() -> ConnectionType {
31        ConnectionType::H1 { use_tls: true }
32    }
33}
34
35/// Timing stats for the establishment of a connection. All durations
36/// are calculated from the start of the connection.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub struct ConnectionTiming {
39    /// When the connection was started.
40    start: Timestamp,
41    /// How long it took to resolve the host to an IP.
42    time_lookup: Duration,
43    /// How long it took for the transport to handshake.
44    ///
45    /// If this was a TCP connection, this is the time
46    /// until the first SYN+ACK.
47    ///
48    /// If this is a QUIC connection, this is the time until
49    /// the QUIC handshake completes.
50    time_connect: Duration,
51    /// How long it took to secure the stream after the transport
52    /// connected.
53    ///
54    /// For TCP streams, this is the time to perform the TLS handshake.
55    ///
56    /// For QUIC streams, this is 0, since the QUIC connection implies
57    /// a secured connection.
58    time_secure: Duration,
59    /// How long it took to setup the L7 protocol, H1/2/3.
60    time_application: Duration,
61
62    // Duration of the DNS lookup
63    dns_time: Duration,
64
65    /// Number of round-trips the TLS handshake took until the connection was
66    /// ready to transmit data (TLS 1.3 -> 1, TLS 1.2 -> 2). Defaults to 1.
67    ///
68    /// Used to normalize the TLS handshake time per draft-ietf-ippm-
69    /// responsiveness-09 §5.3 ("the TLS establishment time needs to be
70    /// normalized to the number of round-trips").
71    tls_round_trips: u32,
72}
73
74impl ConnectionTiming {
75    /// Creates a new [`ConnectionTiming`].
76    pub fn new(start: Timestamp) -> Self {
77        Self {
78            start,
79            time_lookup: Duration::ZERO,
80            time_connect: Duration::ZERO,
81            time_secure: Duration::ZERO,
82            time_application: Duration::ZERO,
83            dns_time: Duration::ZERO,
84            tls_round_trips: 1,
85        }
86    }
87
88    /// Set the time it took to perform DNS resolution of the peer's host.
89    pub fn set_lookup(&mut self, at: Timestamp) {
90        self.time_lookup = at.duration_since(self.start);
91    }
92
93    /// Set the time it took to create the connection with the remote peer.
94    pub fn set_connect(&mut self, at: Timestamp) {
95        self.time_connect = at.duration_since(self.start);
96    }
97
98    /// Set the time it took to secure a connection.
99    pub fn set_secure(&mut self, at: Timestamp) {
100        self.time_secure = at.duration_since(self.start);
101    }
102
103    /// Set the time it took to setup the L7 protocol, H1/2/3.
104    pub fn set_application(&mut self, at: Timestamp) {
105        self.time_application = at.duration_since(self.start);
106    }
107
108    /// Returns when the connection started.
109    pub fn start(&self) -> Timestamp {
110        self.start
111    }
112
113    /// Returns how long it took for DNS to resolve.
114    pub fn time_lookup(&self) -> Duration {
115        self.time_lookup
116    }
117
118    /// Returns how long it took for the transport to connect.
119    pub fn time_connect(&self) -> Duration {
120        self.time_connect
121    }
122
123    /// Set the duration of the DNS lookup
124    pub fn set_dns_lookup(&mut self, duration: Duration) {
125        self.dns_time = duration;
126    }
127
128    /// Returns the DNS lookup duration.
129    pub fn dns_time(&self) -> Duration {
130        self.dns_time
131    }
132
133    /// Returns how long it took for the security handshake to complete.
134    pub fn time_secure(&self) -> Duration {
135        self.time_secure
136    }
137
138    /// Returns how long it took for the H/{1,2,3} handshake to complete.
139    pub fn time_application(&self) -> Duration {
140        self.time_application
141    }
142
143    /// Sets the number of round-trips the TLS handshake took.
144    pub fn set_tls_round_trips(&mut self, round_trips: u32) {
145        self.tls_round_trips = round_trips.max(1);
146    }
147
148    /// Returns the number of round-trips the TLS handshake took (>= 1).
149    pub fn tls_round_trips(&self) -> u32 {
150        self.tls_round_trips.max(1)
151    }
152
153    /// The duration of the TCP handshake alone (excluding DNS resolution),
154    /// i.e. `tcp_f` in draft-ietf-ippm-responsiveness-09 §5.3.
155    ///
156    /// This is the interval between the transport starting to connect and the
157    /// connection being established. When the connection timing starts after
158    /// DNS resolution (as it does for the responsiveness probes), `time_lookup`
159    /// is zero and this is simply `time_connect`.
160    pub fn tcp_handshake(&self) -> Duration {
161        self.time_connect.saturating_sub(self.time_lookup)
162    }
163
164    /// The duration of the TLS handshake alone (excluding the preceding TCP
165    /// handshake), i.e. the un-normalized `tls_f` in
166    /// draft-ietf-ippm-responsiveness-09 §5.3.
167    ///
168    /// For QUIC/H3 connections `time_secure` is zero (TLS is folded into the
169    /// transport handshake), so this saturates to zero rather than underflowing.
170    /// Divide by [`Self::tls_round_trips`] to obtain the normalized value.
171    pub fn tls_handshake(&self) -> Duration {
172        self.time_secure.saturating_sub(self.time_connect)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    /// Build a timing whose phases complete at the given millisecond offsets
181    /// from `start`.
182    fn timing_at(
183        lookup_ms: u64,
184        connect_ms: u64,
185        secure_ms: u64,
186        application_ms: u64,
187    ) -> ConnectionTiming {
188        let start = Timestamp::now();
189        let mut t = ConnectionTiming::new(start);
190        t.set_lookup(start + Duration::from_millis(lookup_ms));
191        t.set_connect(start + Duration::from_millis(connect_ms));
192        t.set_secure(start + Duration::from_millis(secure_ms));
193        t.set_application(start + Duration::from_millis(application_ms));
194        t
195    }
196
197    #[test]
198    fn independent_phase_deltas() {
199        // Post-DNS baseline (lookup = 0): connect at 30ms, secure at 60ms,
200        // application at 62ms. Each network phase is ~30ms (1 RTT).
201        let t = timing_at(0, 30, 60, 62);
202        assert_eq!(t.tcp_handshake(), Duration::from_millis(30));
203        assert_eq!(t.tls_handshake(), Duration::from_millis(30));
204    }
205
206    #[test]
207    fn tcp_handshake_excludes_dns_lookup() {
208        // If the timing baseline included DNS (lookup at 10ms, connect at 40ms),
209        // the TCP handshake is connect - lookup = 30ms, not 40ms.
210        let t = timing_at(10, 40, 70, 72);
211        assert_eq!(t.tcp_handshake(), Duration::from_millis(30));
212        assert_eq!(t.tls_handshake(), Duration::from_millis(30));
213    }
214
215    #[test]
216    fn tls_handshake_saturates_for_quic_like_zero_secure() {
217        // QUIC/H3: time_secure stays 0 while time_connect is set. Must not
218        // underflow.
219        let start = Timestamp::now();
220        let mut t = ConnectionTiming::new(start);
221        t.set_connect(start + Duration::from_millis(30));
222        // secure left at zero
223        assert_eq!(t.tls_handshake(), Duration::ZERO);
224    }
225
226    #[test]
227    fn tls_round_trips_defaults_to_one_and_clamps() {
228        let t = ConnectionTiming::new(Timestamp::now());
229        assert_eq!(t.tls_round_trips(), 1);
230
231        let mut t = t;
232        t.set_tls_round_trips(2);
233        assert_eq!(t.tls_round_trips(), 2);
234
235        // A zero round-trip count would produce a divide-by-zero downstream;
236        // it is clamped to 1.
237        t.set_tls_round_trips(0);
238        assert_eq!(t.tls_round_trips(), 1);
239    }
240}