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