s2n_quic_core/connection/
error.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    application, connection, crypto::packet_protection, endpoint, frame::ConnectionClose, transport,
6};
7use core::{fmt, panic, time::Duration};
8
9/// Errors that a connection can encounter.
10#[derive(Debug, Copy, Clone)]
11#[non_exhaustive]
12pub enum Error {
13    /// The connection was closed without an error
14    #[non_exhaustive]
15    Closed {
16        initiator: endpoint::Location,
17        source: &'static panic::Location<'static>,
18    },
19
20    /// The connection was closed on the transport level
21    ///
22    /// This can occur either locally or by the peer. The argument contains
23    /// the error code which the transport provided in order to close the
24    /// connection.
25    #[non_exhaustive]
26    Transport {
27        code: transport::error::Code,
28        frame_type: u64,
29        reason: &'static str,
30        initiator: endpoint::Location,
31        source: &'static panic::Location<'static>,
32    },
33
34    /// The connection was closed on the application level
35    ///
36    /// This can occur either locally or by the peer. The argument contains
37    /// the error code which the application/ supplied in order to close the
38    /// connection.
39    #[non_exhaustive]
40    Application {
41        error: application::Error,
42        initiator: endpoint::Location,
43        source: &'static panic::Location<'static>,
44    },
45
46    /// The connection was reset by a stateless reset from the peer
47    #[non_exhaustive]
48    StatelessReset {
49        source: &'static panic::Location<'static>,
50    },
51
52    /// The connection was closed because the local connection's idle timer expired
53    #[non_exhaustive]
54    IdleTimerExpired {
55        source: &'static panic::Location<'static>,
56    },
57
58    /// The connection was closed because there are no valid paths
59    #[non_exhaustive]
60    NoValidPath {
61        source: &'static panic::Location<'static>,
62    },
63
64    /// All Stream IDs for Streams on the given connection had been exhausted
65    #[non_exhaustive]
66    StreamIdExhausted {
67        source: &'static panic::Location<'static>,
68    },
69
70    /// The handshake has taken longer to complete than the configured max handshake duration
71    #[non_exhaustive]
72    MaxHandshakeDurationExceeded {
73        max_handshake_duration: Duration,
74        source: &'static panic::Location<'static>,
75    },
76
77    /// The connection should be closed immediately without notifying the peer
78    #[non_exhaustive]
79    ImmediateClose {
80        reason: &'static str,
81        source: &'static panic::Location<'static>,
82    },
83
84    /// The connection attempt was rejected because the endpoint is closing
85    #[non_exhaustive]
86    EndpointClosing {
87        source: &'static panic::Location<'static>,
88    },
89
90    /// The connection was closed due to invalid Application provided configuration
91    #[non_exhaustive]
92    InvalidConfiguration {
93        reason: &'static str,
94        source: &'static panic::Location<'static>,
95    },
96
97    /// The connection was closed due to an unspecified reason
98    #[non_exhaustive]
99    Unspecified {
100        source: &'static panic::Location<'static>,
101    },
102}
103
104#[cfg(feature = "std")]
105impl std::error::Error for Error {}
106
107impl fmt::Display for Error {
108    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
109        match self {
110            Self::Closed { initiator, .. } => write!(
111                f,
112                "The connection was closed without an error by {initiator}"
113            ),
114            Self::Transport { code, frame_type, reason, initiator, .. } => {
115                let error = transport::Error {
116                    code: *code,
117                    frame_type: (*frame_type).try_into().ok().unwrap_or_default(),
118                    reason,
119                };
120                write!(
121                    f,
122                    "The connection was closed on the transport level with error {error} by {initiator}"
123                )
124            },
125            Self::Application { error, initiator, .. } => write!(
126                f,
127                "The connection was closed on the application level with error {error:?} by {initiator}"
128            ),
129            Self::StatelessReset { .. } => write!(
130                f,
131                "The connection was reset by a stateless reset by {}",
132                endpoint::Location::Remote
133            ),
134            Self::IdleTimerExpired {.. } => write!(
135                f,
136                "The connection was closed because the connection's idle timer expired by {}",
137                endpoint::Location::Local
138            ),
139            Self::NoValidPath { .. } => write!(
140                f,
141                "The connection was closed because there are no valid paths"
142            ),
143            Self::StreamIdExhausted { .. } => write!(
144                f,
145                "All Stream IDs for Streams on the given connection had been exhausted"
146            ),
147            Self::MaxHandshakeDurationExceeded { max_handshake_duration, .. } => write!(
148              f,
149                "The connection was closed because the handshake took longer than the max handshake \
150                duration of {max_handshake_duration:?}"
151            ),
152            Self::ImmediateClose { reason, .. } => write!(
153                f,
154                "The connection was closed due to: {reason}"
155            ),
156            Self::EndpointClosing { .. } => {
157                write!(f, "The connection attempt was rejected because the endpoint is closing")
158            }
159            Self::InvalidConfiguration {reason, ..} => write!(
160                f,
161                "The connection was closed due to: {reason}"
162            ),
163            Self::Unspecified { .. } => {
164                write!(f, "The connection was closed due to an unspecified reason")
165            }
166        }
167    }
168}
169
170impl PartialEq for Error {
171    #[inline]
172    fn eq(&self, other: &Self) -> bool {
173        // ignore the `source` attribute when considering if errors are equal
174        match (self, other) {
175            (Error::Closed { initiator: a, .. }, Error::Closed { initiator: b, .. }) => a.eq(b),
176            (
177                Error::Transport {
178                    code: a_code,
179                    frame_type: a_frame_type,
180                    reason: a_reason,
181                    initiator: a_initiator,
182                    ..
183                },
184                Error::Transport {
185                    code: b_code,
186                    frame_type: b_frame_type,
187                    reason: b_reason,
188                    initiator: b_initiator,
189                    ..
190                },
191            ) => {
192                a_code.eq(b_code)
193                    && a_frame_type.eq(b_frame_type)
194                    && a_reason.eq(b_reason)
195                    && a_initiator.eq(b_initiator)
196            }
197            (
198                Error::Application {
199                    error: a_error,
200                    initiator: a_initiator,
201                    ..
202                },
203                Error::Application {
204                    error: b_error,
205                    initiator: b_initiator,
206                    ..
207                },
208            ) => a_error.eq(b_error) && a_initiator.eq(b_initiator),
209            (Error::StatelessReset { .. }, Error::StatelessReset { .. }) => true,
210            (Error::IdleTimerExpired { .. }, Error::IdleTimerExpired { .. }) => true,
211            (Error::NoValidPath { .. }, Error::NoValidPath { .. }) => true,
212            (Error::StreamIdExhausted { .. }, Error::StreamIdExhausted { .. }) => true,
213            (
214                Error::MaxHandshakeDurationExceeded {
215                    max_handshake_duration: a,
216                    ..
217                },
218                Error::MaxHandshakeDurationExceeded {
219                    max_handshake_duration: b,
220                    ..
221                },
222            ) => a.eq(b),
223            (Error::ImmediateClose { reason: a, .. }, Error::ImmediateClose { reason: b, .. }) => {
224                a.eq(b)
225            }
226            (Error::EndpointClosing { .. }, Error::EndpointClosing { .. }) => true,
227            (
228                Error::InvalidConfiguration {
229                    reason: a_reason, ..
230                },
231                Error::InvalidConfiguration {
232                    reason: b_reason, ..
233                },
234            ) => a_reason.eq(b_reason),
235            (Error::Unspecified { .. }, Error::Unspecified { .. }) => true,
236            _ => false,
237        }
238    }
239}
240
241impl Eq for Error {}
242
243impl Error {
244    /// Returns the [`panic::Location`] for the error
245    pub fn source(&self) -> &'static panic::Location<'static> {
246        match self {
247            Error::Closed { source, .. } => source,
248            Error::Transport { source, .. } => source,
249            Error::Application { source, .. } => source,
250            Error::StatelessReset { source } => source,
251            Error::IdleTimerExpired { source } => source,
252            Error::NoValidPath { source } => source,
253            Error::StreamIdExhausted { source } => source,
254            Error::MaxHandshakeDurationExceeded { source, .. } => source,
255            Error::ImmediateClose { source, .. } => source,
256            Error::EndpointClosing { source } => source,
257            Error::InvalidConfiguration { source, .. } => source,
258            Error::Unspecified { source } => source,
259        }
260    }
261
262    #[track_caller]
263    fn from_transport_error(error: transport::Error, initiator: endpoint::Location) -> Self {
264        let source = panic::Location::caller();
265        match error.code {
266            // The connection closed without an error
267            code if code == transport::Error::NO_ERROR.code => Self::Closed { initiator, source },
268            // The connection closed without an error at the application layer
269            code if code == transport::Error::APPLICATION_ERROR.code && initiator.is_remote() => {
270                Self::Closed { initiator, source }
271            }
272            // The connection closed with an actual error
273            _ => Self::Transport {
274                code: error.code,
275                frame_type: error.frame_type.into(),
276                reason: error.reason,
277                initiator,
278                source,
279            },
280        }
281    }
282
283    #[inline]
284    #[track_caller]
285    #[doc(hidden)]
286    pub fn closed(initiator: endpoint::Location) -> Error {
287        let source = panic::Location::caller();
288        Error::Closed { initiator, source }
289    }
290
291    #[inline]
292    #[track_caller]
293    #[doc(hidden)]
294    pub fn immediate_close(reason: &'static str) -> Error {
295        let source = panic::Location::caller();
296        Error::ImmediateClose { reason, source }
297    }
298
299    #[inline]
300    #[track_caller]
301    #[doc(hidden)]
302    pub fn idle_timer_expired() -> Error {
303        let source = panic::Location::caller();
304        Error::IdleTimerExpired { source }
305    }
306
307    #[inline]
308    #[track_caller]
309    #[doc(hidden)]
310    pub fn stream_id_exhausted() -> Error {
311        let source = panic::Location::caller();
312        Error::StreamIdExhausted { source }
313    }
314
315    #[inline]
316    #[track_caller]
317    #[doc(hidden)]
318    pub fn no_valid_path() -> Error {
319        let source = panic::Location::caller();
320        Error::NoValidPath { source }
321    }
322
323    #[inline]
324    #[track_caller]
325    #[doc(hidden)]
326    pub fn stateless_reset() -> Error {
327        let source = panic::Location::caller();
328        Error::StatelessReset { source }
329    }
330
331    #[inline]
332    #[track_caller]
333    #[doc(hidden)]
334    pub fn max_handshake_duration_exceeded(max_handshake_duration: Duration) -> Error {
335        let source = panic::Location::caller();
336        Error::MaxHandshakeDurationExceeded {
337            max_handshake_duration,
338            source,
339        }
340    }
341
342    #[inline]
343    #[track_caller]
344    #[doc(hidden)]
345    pub fn application(error: application::Error) -> Error {
346        let source = panic::Location::caller();
347        Error::Application {
348            error,
349            initiator: endpoint::Location::Local,
350            source,
351        }
352    }
353
354    #[inline]
355    #[track_caller]
356    #[doc(hidden)]
357    pub fn endpoint_closing() -> Error {
358        let source = panic::Location::caller();
359        Error::EndpointClosing { source }
360    }
361
362    #[inline]
363    #[track_caller]
364    #[doc(hidden)]
365    pub fn invalid_configuration(reason: &'static str) -> Error {
366        let source = panic::Location::caller();
367        Error::InvalidConfiguration { source, reason }
368    }
369
370    #[inline]
371    #[track_caller]
372    #[doc(hidden)]
373    pub fn unspecified() -> Error {
374        let source = panic::Location::caller();
375        Error::Unspecified { source }
376    }
377
378    #[inline]
379    #[doc(hidden)]
380    pub fn into_accept_error(error: connection::Error) -> Result<(), connection::Error> {
381        match error {
382            // The connection closed without an error
383            connection::Error::Closed { .. } => Ok(()),
384            // The application closed the connection
385            connection::Error::Transport { code, .. }
386                if code == transport::Error::APPLICATION_ERROR.code =>
387            {
388                Ok(())
389            }
390            // The local connection's idle timer expired
391            connection::Error::IdleTimerExpired { .. } => Ok(()),
392            // Otherwise return the real error to the user
393            _ => Err(error),
394        }
395    }
396}
397
398/// Returns a CONNECTION_CLOSE frame for the given connection Error, if any
399///
400/// The first item will be a close frame for an early (initial, handshake) packet.
401/// The second item will be a close frame for a 1-RTT (application data) packet.
402pub fn as_frame<'a, F: connection::close::Formatter>(
403    error: Error,
404    formatter: &'a F,
405    context: &'a connection::close::Context<'a>,
406) -> Option<(ConnectionClose<'a>, ConnectionClose<'a>)> {
407    match error {
408        Error::Closed { initiator, .. } => {
409            // don't send CONNECTION_CLOSE frames on remote-initiated errors
410            if initiator.is_remote() {
411                return None;
412            }
413
414            let error = transport::Error::NO_ERROR;
415            let early = formatter.format_early_transport_error(context, error);
416            let one_rtt = formatter.format_transport_error(context, error);
417
418            Some((early, one_rtt))
419        }
420        Error::Transport {
421            code,
422            frame_type,
423            reason,
424            initiator,
425            ..
426        } => {
427            // don't send CONNECTION_CLOSE frames on remote-initiated errors
428            if initiator.is_remote() {
429                return None;
430            }
431
432            let error = transport::Error {
433                code,
434                frame_type: frame_type.try_into().unwrap_or_default(),
435                reason,
436            };
437
438            let early = formatter.format_early_transport_error(context, error);
439            let one_rtt = formatter.format_transport_error(context, error);
440            Some((early, one_rtt))
441        }
442        Error::Application {
443            error, initiator, ..
444        } => {
445            // don't send CONNECTION_CLOSE frames on remote-initiated errors
446            if initiator.is_remote() {
447                return None;
448            }
449
450            let early = formatter.format_early_application_error(context, error);
451            let one_rtt = formatter.format_application_error(context, error);
452            Some((early, one_rtt))
453        }
454        // This error comes from the peer so we don't respond with a CONNECTION_CLOSE
455        Error::StatelessReset { .. } => None,
456        // Nothing gets sent on idle timeouts
457        Error::IdleTimerExpired { .. } => None,
458        Error::NoValidPath { .. } => None,
459        Error::StreamIdExhausted { .. } => {
460            let error =
461                transport::Error::PROTOCOL_VIOLATION.with_reason("stream IDs have been exhausted");
462
463            let early = formatter.format_early_transport_error(context, error);
464            let one_rtt = formatter.format_transport_error(context, error);
465
466            Some((early, one_rtt))
467        }
468        Error::MaxHandshakeDurationExceeded { .. } => None,
469        Error::ImmediateClose { .. } => None,
470        Error::EndpointClosing { .. } => None,
471        Error::InvalidConfiguration { .. } => None,
472        Error::Unspecified { .. } => {
473            let error =
474                transport::Error::INTERNAL_ERROR.with_reason("an unspecified error occurred");
475
476            let early = formatter.format_early_transport_error(context, error);
477            let one_rtt = formatter.format_transport_error(context, error);
478
479            Some((early, one_rtt))
480        }
481    }
482}
483
484impl application::error::TryInto for Error {
485    fn application_error(&self) -> Option<application::Error> {
486        if let Self::Application { error, .. } = self {
487            Some(*error)
488        } else {
489            None
490        }
491    }
492}
493
494impl From<transport::Error> for Error {
495    #[track_caller]
496    fn from(error: transport::Error) -> Self {
497        Self::from_transport_error(error, endpoint::Location::Local)
498    }
499}
500
501impl From<ConnectionClose<'_>> for Error {
502    #[track_caller]
503    fn from(error: ConnectionClose) -> Self {
504        if let Some(frame_type) = error.frame_type {
505            let error = transport::Error {
506                code: transport::error::Code::new(error.error_code),
507                // we use an empty `&'static str` so we don't allocate anything
508                // in the event of an error
509                reason: "",
510                frame_type,
511            };
512            Self::from_transport_error(error, endpoint::Location::Remote)
513        } else {
514            let source = panic::Location::caller();
515            Self::Application {
516                error: error.error_code.into(),
517                initiator: endpoint::Location::Remote,
518                source,
519            }
520        }
521    }
522}
523
524#[cfg(feature = "std")]
525impl From<Error> for std::io::Error {
526    fn from(error: Error) -> Self {
527        let kind = error.into();
528        std::io::Error::new(kind, error)
529    }
530}
531
532#[cfg(feature = "std")]
533impl From<Error> for std::io::ErrorKind {
534    fn from(error: Error) -> Self {
535        use std::io::ErrorKind;
536        match error {
537            Error::Closed { .. } => ErrorKind::ConnectionAborted,
538            Error::Transport { code, .. } if code == transport::Error::CONNECTION_REFUSED.code => {
539                ErrorKind::ConnectionRefused
540            }
541            Error::Transport { .. } => ErrorKind::ConnectionReset,
542            Error::Application { .. } => ErrorKind::ConnectionReset,
543            Error::StatelessReset { .. } => ErrorKind::ConnectionReset,
544            Error::IdleTimerExpired { .. } => ErrorKind::TimedOut,
545            Error::NoValidPath { .. } => ErrorKind::Other,
546            Error::StreamIdExhausted { .. } => ErrorKind::Other,
547            Error::MaxHandshakeDurationExceeded { .. } => ErrorKind::TimedOut,
548            Error::ImmediateClose { .. } => ErrorKind::Other,
549            Error::EndpointClosing { .. } => ErrorKind::Other,
550            Error::InvalidConfiguration { .. } => ErrorKind::Other,
551            Error::Unspecified { .. } => ErrorKind::Other,
552        }
553    }
554}
555
556/// Some connection methods may need to indicate both `ConnectionError`s and `DecryptError`s. This
557/// enum is used to allow for either error type to be returned as appropriate.
558#[derive(Clone, Copy, Debug, PartialEq)]
559pub enum ProcessingError {
560    ConnectionError(Error),
561    DecryptError,
562    Other,
563}
564
565impl From<Error> for ProcessingError {
566    fn from(inner_error: Error) -> Self {
567        ProcessingError::ConnectionError(inner_error)
568    }
569}
570
571impl From<crate::transport::Error> for ProcessingError {
572    #[track_caller]
573    fn from(inner_error: crate::transport::Error) -> Self {
574        Self::ConnectionError(inner_error.into())
575    }
576}
577
578impl From<packet_protection::Error> for ProcessingError {
579    fn from(_: packet_protection::Error) -> Self {
580        Self::DecryptError
581    }
582}
583
584mod metrics {
585    use super::{transport, Error};
586    use crate::event::metrics::aggregate::{
587        info::{Str, Variant},
588        AsVariant,
589    };
590
591    macro_rules! impl_variants {
592        ($($name:ident => $name_str:literal),* $(,)?) => {
593            impl AsVariant for Error {
594                const VARIANTS: &'static [Variant] = &{
595                    const fn count(_id: &str) -> usize {
596                        1
597                    }
598
599                    const VARIANTS: usize = 0 $( + count($name_str))*;
600
601                    const TRANSPORT: &'static [Variant] = transport::error::Code::VARIANTS;
602
603                    let mut array = [
604                        Variant { name: Str::new("\0"), id: 0 };
605                        VARIANTS + TRANSPORT.len()
606                    ];
607
608                    let mut id = 0;
609
610                    $(
611                        array[id] = Variant {
612                            name: Str::new(concat!($name_str, "\0")),
613                            id,
614                        };
615                        id += 1;
616                    )*
617
618                    let mut transport_idx = 0;
619                    while transport_idx < TRANSPORT.len() {
620                        let variant = TRANSPORT[transport_idx];
621                        array[id] = Variant {
622                            name: variant.name,
623                            id,
624                        };
625                        id += 1;
626                        transport_idx += 1;
627                    }
628
629                    array
630                };
631
632                #[inline]
633                fn variant_idx(&self) -> usize {
634                    let mut idx = 0;
635
636                    $(
637                        if matches!(self, Error::$name { .. }) {
638                            return idx;
639                        }
640                        idx += 1;
641                    )*
642
643                    if let Error::Transport { code, ..} = self {
644                        code.variant_idx() + idx
645                    } else {
646                        panic!()
647                    }
648                }
649            }
650
651            #[allow(dead_code)]
652            fn exhaustive_test(error: &Error) {
653                match error {
654                    $(
655                        Error::$name { .. } => {},
656                    )*
657                    Error::Transport { .. } => {},
658                }
659            }
660
661            #[test]
662            #[cfg_attr(miri, ignore)]
663            fn variants_test() {
664                insta::assert_debug_snapshot!(Error::VARIANTS);
665
666                let mut seen = std::collections::HashSet::new();
667                for variant in Error::VARIANTS {
668                    assert!(seen.insert(variant.id));
669                }
670            }
671        };
672    }
673
674    impl_variants!(
675        Closed => "CLOSED",
676        Application => "APPLICATION",
677        StatelessReset => "STATELESS_RESET",
678        IdleTimerExpired => "IDLE_TIMER_EXPIRED",
679        NoValidPath => "NO_VALID_PATH",
680        StreamIdExhausted => "STREAM_ID_EXHAUSTED",
681        MaxHandshakeDurationExceeded => "MAX_HANDSHAKE_DURATION_EXCEEDED",
682        ImmediateClose => "IMMEDIATE_CLOSE",
683        EndpointClosing => "ENDPOINT_CLOSING",
684        InvalidConfiguration => "INVALID_CONFIGURATION",
685        Unspecified => "UNSPECIFIED",
686    );
687}