1use std::fmt;
2
3use crate::{ConnectionError, VarInt};
4
5pub const ANT_QUIC_CLOSE_CODE_BASE: u32 = 0x4E5B00;
9const CLOSE_CODE_SUPERSEDED: u32 = ANT_QUIC_CLOSE_CODE_BASE;
10const CLOSE_CODE_READER_EXIT: u32 = ANT_QUIC_CLOSE_CODE_BASE + 0x01;
11const CLOSE_CODE_PEER_SHUTDOWN: u32 = ANT_QUIC_CLOSE_CODE_BASE + 0x02;
12const CLOSE_CODE_BANNED: u32 = ANT_QUIC_CLOSE_CODE_BASE + 0x03;
13const CLOSE_CODE_LIFECYCLE_CLEANUP: u32 = ANT_QUIC_CLOSE_CODE_BASE + 0x04;
14const CLOSE_CODE_LIVENESS_TIMEOUT: u32 = ANT_QUIC_CLOSE_CODE_BASE + 0x05;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum ConnectionCloseReason {
19 Superseded,
21 ReaderExit,
23 PeerShutdown,
25 Banned,
27 LifecycleCleanup,
29 LivenessTimeout,
34 ApplicationClosed,
36 ConnectionClosed,
38 TimedOut,
40 Reset,
42 TransportError,
44 LocallyClosed,
46 VersionMismatch,
48 CidsExhausted,
50 Unknown,
52}
53
54impl ConnectionCloseReason {
55 pub fn app_error_code(self) -> Option<VarInt> {
57 let code = match self {
58 Self::Superseded => CLOSE_CODE_SUPERSEDED,
59 Self::ReaderExit => CLOSE_CODE_READER_EXIT,
60 Self::PeerShutdown => CLOSE_CODE_PEER_SHUTDOWN,
61 Self::Banned => CLOSE_CODE_BANNED,
62 Self::LifecycleCleanup => CLOSE_CODE_LIFECYCLE_CLEANUP,
63 Self::LivenessTimeout => CLOSE_CODE_LIVENESS_TIMEOUT,
64 Self::ApplicationClosed
65 | Self::ConnectionClosed
66 | Self::TimedOut
67 | Self::Reset
68 | Self::TransportError
69 | Self::LocallyClosed
70 | Self::VersionMismatch
71 | Self::CidsExhausted
72 | Self::Unknown => return None,
73 };
74 Some(VarInt::from_u32(code))
75 }
76
77 pub fn as_str(self) -> &'static str {
79 match self {
80 Self::Superseded => "Superseded",
81 Self::ReaderExit => "ReaderExit",
82 Self::PeerShutdown => "PeerShutdown",
83 Self::Banned => "Banned",
84 Self::LifecycleCleanup => "LifecycleCleanup",
85 Self::LivenessTimeout => "LivenessTimeout",
86 Self::ApplicationClosed => "ApplicationClosed",
87 Self::ConnectionClosed => "ConnectionClosed",
88 Self::TimedOut => "TimedOut",
89 Self::Reset => "Reset",
90 Self::TransportError => "TransportError",
91 Self::LocallyClosed => "LocallyClosed",
92 Self::VersionMismatch => "VersionMismatch",
93 Self::CidsExhausted => "CidsExhausted",
94 Self::Unknown => "Unknown",
95 }
96 }
97
98 pub fn reason_bytes(self) -> &'static [u8] {
100 self.as_str().as_bytes()
101 }
102
103 pub fn from_app_error_code(code: VarInt) -> Option<Self> {
105 match code.into_inner() as u32 {
106 CLOSE_CODE_SUPERSEDED => Some(Self::Superseded),
107 CLOSE_CODE_READER_EXIT => Some(Self::ReaderExit),
108 CLOSE_CODE_PEER_SHUTDOWN => Some(Self::PeerShutdown),
109 CLOSE_CODE_BANNED => Some(Self::Banned),
110 CLOSE_CODE_LIFECYCLE_CLEANUP => Some(Self::LifecycleCleanup),
111 CLOSE_CODE_LIVENESS_TIMEOUT => Some(Self::LivenessTimeout),
112 _ => None,
113 }
114 }
115
116 pub fn from_connection_error(error: &ConnectionError) -> Self {
118 match error {
119 ConnectionError::ApplicationClosed(frame) => {
120 Self::from_app_error_code(frame.error_code).unwrap_or(Self::ApplicationClosed)
121 }
122 ConnectionError::ConnectionClosed(_) => Self::ConnectionClosed,
123 ConnectionError::TransportError(_) => Self::TransportError,
124 ConnectionError::VersionMismatch => Self::VersionMismatch,
125 ConnectionError::Reset => Self::Reset,
126 ConnectionError::TimedOut => Self::TimedOut,
127 ConnectionError::LocallyClosed => Self::LocallyClosed,
128 ConnectionError::CidsExhausted => Self::CidsExhausted,
129 }
130 }
131}
132
133impl fmt::Display for ConnectionCloseReason {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.write_str(self.as_str())
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub(crate) enum ConnectionLifecycleState {
141 Live,
142 Superseded {
143 replaced_by_generation: u64,
144 },
145 Closing {
146 reason: ConnectionCloseReason,
147 },
148 Closed {
149 reason: ConnectionCloseReason,
150 closed_at_unix_ms: u64,
151 },
152}
153
154impl ConnectionLifecycleState {
155 pub(crate) fn name(self) -> &'static str {
156 match self {
157 Self::Live => "Live",
158 Self::Superseded { .. } => "Superseded",
159 Self::Closing { .. } => "Closing",
160 Self::Closed { .. } => "Closed",
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use bytes::Bytes;
169
170 #[test]
173 fn reason_as_str_all_variants() {
174 let cases = [
175 (ConnectionCloseReason::Superseded, "Superseded"),
176 (ConnectionCloseReason::ReaderExit, "ReaderExit"),
177 (ConnectionCloseReason::PeerShutdown, "PeerShutdown"),
178 (ConnectionCloseReason::Banned, "Banned"),
179 (ConnectionCloseReason::LifecycleCleanup, "LifecycleCleanup"),
180 (ConnectionCloseReason::LivenessTimeout, "LivenessTimeout"),
181 (
182 ConnectionCloseReason::ApplicationClosed,
183 "ApplicationClosed",
184 ),
185 (ConnectionCloseReason::ConnectionClosed, "ConnectionClosed"),
186 (ConnectionCloseReason::TimedOut, "TimedOut"),
187 (ConnectionCloseReason::Reset, "Reset"),
188 (ConnectionCloseReason::TransportError, "TransportError"),
189 (ConnectionCloseReason::LocallyClosed, "LocallyClosed"),
190 (ConnectionCloseReason::VersionMismatch, "VersionMismatch"),
191 (ConnectionCloseReason::CidsExhausted, "CidsExhausted"),
192 (ConnectionCloseReason::Unknown, "Unknown"),
193 ];
194
195 for (reason, expected) in &cases {
196 assert_eq!(reason.as_str(), *expected);
197 }
198 }
199
200 #[test]
201 fn reason_display_matches_as_str() {
202 let reasons = [
203 ConnectionCloseReason::Superseded,
204 ConnectionCloseReason::ReaderExit,
205 ConnectionCloseReason::PeerShutdown,
206 ConnectionCloseReason::Banned,
207 ConnectionCloseReason::LifecycleCleanup,
208 ConnectionCloseReason::LivenessTimeout,
209 ConnectionCloseReason::ApplicationClosed,
210 ConnectionCloseReason::ConnectionClosed,
211 ConnectionCloseReason::TimedOut,
212 ConnectionCloseReason::Reset,
213 ConnectionCloseReason::TransportError,
214 ConnectionCloseReason::LocallyClosed,
215 ConnectionCloseReason::VersionMismatch,
216 ConnectionCloseReason::CidsExhausted,
217 ConnectionCloseReason::Unknown,
218 ];
219
220 for reason in &reasons {
221 assert_eq!(format!("{reason}"), reason.as_str());
222 }
223 }
224
225 #[test]
226 fn reason_reason_bytes_equals_as_str_bytes() {
227 let reasons = [
228 ConnectionCloseReason::Superseded,
229 ConnectionCloseReason::ReaderExit,
230 ConnectionCloseReason::LivenessTimeout,
231 ConnectionCloseReason::Unknown,
232 ];
233
234 for reason in &reasons {
235 assert_eq!(reason.reason_bytes(), reason.as_str().as_bytes());
236 }
237 }
238
239 #[test]
240 fn reason_equality() {
241 assert_eq!(
242 ConnectionCloseReason::Superseded,
243 ConnectionCloseReason::Superseded
244 );
245 assert_ne!(
246 ConnectionCloseReason::Superseded,
247 ConnectionCloseReason::ReaderExit
248 );
249 }
250
251 #[test]
252 fn reason_clone() {
253 let r = ConnectionCloseReason::Superseded;
254 assert_eq!(r.clone(), r);
255 }
256
257 #[test]
260 fn lifecycle_reasons_have_app_error_codes() {
261 let has_code = [
262 ConnectionCloseReason::Superseded,
263 ConnectionCloseReason::ReaderExit,
264 ConnectionCloseReason::PeerShutdown,
265 ConnectionCloseReason::Banned,
266 ConnectionCloseReason::LifecycleCleanup,
267 ConnectionCloseReason::LivenessTimeout,
268 ];
269
270 for reason in &has_code {
271 assert!(
272 reason.app_error_code().is_some(),
273 "{reason:?} should have an app_error_code"
274 );
275 }
276 }
277
278 #[test]
279 fn non_lifecycle_reasons_have_no_app_error_code() {
280 let no_code = [
281 ConnectionCloseReason::ApplicationClosed,
282 ConnectionCloseReason::ConnectionClosed,
283 ConnectionCloseReason::TimedOut,
284 ConnectionCloseReason::Reset,
285 ConnectionCloseReason::TransportError,
286 ConnectionCloseReason::LocallyClosed,
287 ConnectionCloseReason::VersionMismatch,
288 ConnectionCloseReason::CidsExhausted,
289 ConnectionCloseReason::Unknown,
290 ];
291
292 for reason in &no_code {
293 assert!(
294 reason.app_error_code().is_none(),
295 "{reason:?} should NOT have an app_error_code"
296 );
297 }
298 }
299
300 #[test]
301 fn lifecycle_error_codes_start_at_base() {
302 let superseded_code = ConnectionCloseReason::Superseded
303 .app_error_code()
304 .unwrap()
305 .into_inner() as u32;
306 assert_eq!(superseded_code, ANT_QUIC_CLOSE_CODE_BASE);
307
308 let liveness_code = ConnectionCloseReason::LivenessTimeout
309 .app_error_code()
310 .unwrap()
311 .into_inner() as u32;
312 assert_eq!(liveness_code, ANT_QUIC_CLOSE_CODE_BASE + 5);
313 }
314
315 #[test]
318 fn from_app_error_code_roundtrip() {
319 let lifecycle_reasons = [
320 ConnectionCloseReason::Superseded,
321 ConnectionCloseReason::ReaderExit,
322 ConnectionCloseReason::PeerShutdown,
323 ConnectionCloseReason::Banned,
324 ConnectionCloseReason::LifecycleCleanup,
325 ConnectionCloseReason::LivenessTimeout,
326 ];
327
328 for reason in &lifecycle_reasons {
329 let code = reason.app_error_code().unwrap();
330 let mapped = ConnectionCloseReason::from_app_error_code(code);
331 assert_eq!(mapped, Some(*reason));
332 }
333 }
334
335 #[test]
336 fn from_app_error_code_unknown_code() {
337 let code = VarInt::from_u32(0x1234);
338 let result = ConnectionCloseReason::from_app_error_code(code);
339 assert_eq!(result, None);
340 }
341
342 #[test]
343 fn from_app_error_code_zero() {
344 let code = VarInt::from_u32(0);
346 let result = ConnectionCloseReason::from_app_error_code(code);
347 assert_eq!(result, None);
348 }
349
350 #[test]
353 fn from_connection_error_application_closed_maps_to_lifecycle() {
354 let code = VarInt::from_u32(CLOSE_CODE_SUPERSEDED);
355 let app_close = crate::frame::ApplicationClose {
356 error_code: code,
357 reason: Bytes::new(),
358 };
359 let frame = crate::ConnectionError::ApplicationClosed(app_close);
360 let reason = ConnectionCloseReason::from_connection_error(&frame);
361 assert_eq!(reason, ConnectionCloseReason::Superseded);
362 }
363
364 #[test]
365 fn from_connection_error_application_closed_falls_back() {
366 let code = VarInt::from_u32(0x1234);
367 let app_close = crate::frame::ApplicationClose {
368 error_code: code,
369 reason: Bytes::new(),
370 };
371 let frame = crate::ConnectionError::ApplicationClosed(app_close);
372 let reason = ConnectionCloseReason::from_connection_error(&frame);
373 assert_eq!(reason, ConnectionCloseReason::ApplicationClosed);
374 }
375
376 #[test]
379 fn lifecycle_state_name_live() {
380 assert_eq!(ConnectionLifecycleState::Live.name(), "Live");
381 }
382
383 #[test]
384 fn lifecycle_state_name_superseded() {
385 let state = ConnectionLifecycleState::Superseded {
386 replaced_by_generation: 42,
387 };
388 assert_eq!(state.name(), "Superseded");
389 }
390
391 #[test]
392 fn lifecycle_state_name_closing() {
393 let state = ConnectionLifecycleState::Closing {
394 reason: ConnectionCloseReason::PeerShutdown,
395 };
396 assert_eq!(state.name(), "Closing");
397 }
398
399 #[test]
400 fn lifecycle_state_name_closed() {
401 let state = ConnectionLifecycleState::Closed {
402 reason: ConnectionCloseReason::LivenessTimeout,
403 closed_at_unix_ms: 1000,
404 };
405 assert_eq!(state.name(), "Closed");
406 }
407
408 #[test]
409 fn lifecycle_state_equality() {
410 assert_eq!(
411 ConnectionLifecycleState::Live,
412 ConnectionLifecycleState::Live
413 );
414 assert_ne!(
415 ConnectionLifecycleState::Live,
416 ConnectionLifecycleState::Superseded {
417 replaced_by_generation: 1,
418 }
419 );
420 }
421
422 #[test]
423 fn lifecycle_state_debug() {
424 let state = ConnectionLifecycleState::Live;
425 let debug = format!("{state:?}");
426 assert!(debug.contains("Live"));
427 }
428}