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}