Skip to main content

reth_network/
error.rs

1//! Possible errors when interacting with the network.
2
3use crate::session::PendingSessionHandshakeError;
4use reth_dns_discovery::resolver::NetError;
5use reth_ecies::ECIESErrorImpl;
6use reth_eth_wire::{
7    errors::{EthHandshakeError, EthStreamError, P2PHandshakeError, P2PStreamError},
8    DisconnectReason,
9};
10use reth_network_types::BackoffKind;
11use std::{fmt, io, io::ErrorKind, net::SocketAddr};
12
13/// Service kind.
14#[derive(Debug, PartialEq, Eq, Copy, Clone)]
15pub enum ServiceKind {
16    /// Listener service.
17    Listener(SocketAddr),
18    /// Discovery service.
19    Discovery(SocketAddr),
20}
21
22impl ServiceKind {
23    /// Returns the appropriate flags for each variant.
24    pub const fn flags(&self) -> &'static str {
25        match self {
26            Self::Listener(_) => "--port",
27            Self::Discovery(_) => "--discovery.port",
28        }
29    }
30}
31
32impl fmt::Display for ServiceKind {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Listener(addr) => write!(f, "{addr} (listener service)"),
36            Self::Discovery(addr) => write!(f, "{addr} (discovery service)"),
37        }
38    }
39}
40
41/// All error variants for the network
42#[derive(Debug, thiserror::Error)]
43pub enum NetworkError {
44    /// General IO error.
45    #[error(transparent)]
46    Io(#[from] io::Error),
47    /// Error when an address is already in use.
48    #[error("address {kind} is already in use (os error 98). Choose a different port using {}", kind.flags())]
49    AddressAlreadyInUse {
50        /// Service kind.
51        kind: ServiceKind,
52        /// IO error.
53        error: io::Error,
54    },
55    /// IO error when creating the discovery service
56    #[error("failed to launch discovery service on {0}: {1}")]
57    Discovery(SocketAddr, io::Error),
58    /// An error occurred with discovery v5 node.
59    #[error("discv5 error, {0}")]
60    Discv5Error(#[from] reth_discv5::Error),
61    /// Error when setting up the DNS resolver failed
62    ///
63    /// See also [`DnsResolver`](reth_dns_discovery::DnsResolver::from_system_conf)
64    #[error("failed to configure DNS resolver: {0}")]
65    DnsResolver(#[from] NetError),
66}
67
68impl NetworkError {
69    /// Converts a `std::io::Error` to a more descriptive `NetworkError`.
70    pub fn from_io_error(err: io::Error, kind: ServiceKind) -> Self {
71        match err.kind() {
72            ErrorKind::AddrInUse => Self::AddressAlreadyInUse { kind, error: err },
73            _ => {
74                if let ServiceKind::Discovery(address) = kind {
75                    return Self::Discovery(address, err)
76                }
77                Self::Io(err)
78            }
79        }
80    }
81}
82
83/// Abstraction over errors that can lead to a failed session
84#[auto_impl::auto_impl(&)]
85pub(crate) trait SessionError: fmt::Debug + fmt::Display {
86    /// Returns true if the error indicates that the corresponding peer should be removed from peer
87    /// discovery, for example if it's using a different genesis hash.
88    fn merits_discovery_ban(&self) -> bool;
89
90    /// Returns true if the error indicates that we'll never be able to establish a connection to
91    /// that peer. For example, not matching capabilities or a mismatch in protocols.
92    ///
93    /// Note: This does not necessarily mean that either of the peers are in violation of the
94    /// protocol but rather that they'll never be able to connect with each other. This check is
95    /// a superset of [`Self::merits_discovery_ban`] which checks if the peer should not be part
96    /// of the gossip network.
97    fn is_fatal_protocol_error(&self) -> bool;
98
99    /// Whether we should backoff.
100    ///
101    /// Returns the severity of the backoff that should be applied, or `None`, if no backoff should
102    /// be applied.
103    ///
104    /// In case of `Some(BackoffKind)` will temporarily prevent additional
105    /// connection attempts.
106    fn should_backoff(&self) -> Option<BackoffKind>;
107}
108
109impl SessionError for EthStreamError {
110    fn merits_discovery_ban(&self) -> bool {
111        match self {
112            Self::P2PStreamError(P2PStreamError::HandshakeError(
113                P2PHandshakeError::HelloNotInHandshake |
114                P2PHandshakeError::NonHelloMessageInHandshake,
115            )) => true,
116            Self::EthHandshakeError(err) => {
117                #[expect(clippy::match_same_arms)]
118                match err {
119                    EthHandshakeError::NoResponse => {
120                        // this happens when the conn simply stalled
121                        false
122                    }
123                    EthHandshakeError::InvalidFork(_) => {
124                        // this can occur when the remote or our node is running an outdated client,
125                        // we shouldn't treat this as fatal, because the node can come back online
126                        // with an updated version any time
127                        false
128                    }
129                    _ => true,
130                }
131            }
132            _ => false,
133        }
134    }
135
136    fn is_fatal_protocol_error(&self) -> bool {
137        match self {
138            Self::P2PStreamError(err) => {
139                matches!(
140                    err,
141                    P2PStreamError::HandshakeError(
142                        P2PHandshakeError::NoSharedCapabilities |
143                            P2PHandshakeError::HelloNotInHandshake |
144                            P2PHandshakeError::NonHelloMessageInHandshake |
145                            P2PHandshakeError::Disconnected(
146                                DisconnectReason::UselessPeer |
147                                    DisconnectReason::IncompatibleP2PProtocolVersion |
148                                    DisconnectReason::ProtocolBreach
149                            )
150                    ) | P2PStreamError::UnknownReservedMessageId(_) |
151                        P2PStreamError::UnknownSubprotocolMessageId(_) |
152                        P2PStreamError::EmptyProtocolMessage |
153                        P2PStreamError::ParseSharedCapability(_) |
154                        P2PStreamError::CapabilityNotShared |
155                        P2PStreamError::Disconnected(
156                            DisconnectReason::UselessPeer |
157                                DisconnectReason::IncompatibleP2PProtocolVersion |
158                                DisconnectReason::ProtocolBreach
159                        ) |
160                        P2PStreamError::MismatchedProtocolVersion { .. }
161                )
162            }
163            Self::EthHandshakeError(err) => {
164                #[expect(clippy::match_same_arms)]
165                match err {
166                    EthHandshakeError::NoResponse => {
167                        // this happens when the conn simply stalled
168                        false
169                    }
170                    EthHandshakeError::InvalidFork(_) => {
171                        // this can occur when the remote or our node is running an outdated client,
172                        // we shouldn't treat this as fatal, because the node can come back online
173                        // with an updated version any time
174                        false
175                    }
176                    _ => true,
177                }
178            }
179            _ => false,
180        }
181    }
182
183    fn should_backoff(&self) -> Option<BackoffKind> {
184        if let Some(err) = self.as_io() {
185            return err.should_backoff()
186        }
187
188        if let Some(err) = self.as_disconnected() {
189            return match err {
190                DisconnectReason::TooManyPeers |
191                DisconnectReason::AlreadyConnected |
192                DisconnectReason::PingTimeout |
193                DisconnectReason::DisconnectRequested |
194                DisconnectReason::TcpSubsystemError => Some(BackoffKind::Low),
195
196                DisconnectReason::ProtocolBreach |
197                DisconnectReason::UselessPeer |
198                DisconnectReason::IncompatibleP2PProtocolVersion |
199                DisconnectReason::NullNodeIdentity |
200                DisconnectReason::ClientQuitting |
201                DisconnectReason::UnexpectedHandshakeIdentity |
202                DisconnectReason::ConnectedToSelf |
203                DisconnectReason::SubprotocolSpecific => {
204                    // These are considered fatal, and are handled by the
205                    // [`SessionError::is_fatal_protocol_error`]
206                    Some(BackoffKind::High)
207                }
208            }
209        }
210
211        // This only checks for a subset of error variants, the counterpart of
212        // [`SessionError::is_fatal_protocol_error`]
213        match self {
214            // timeouts
215            Self::EthHandshakeError(EthHandshakeError::NoResponse) |
216            Self::P2PStreamError(
217                P2PStreamError::HandshakeError(P2PHandshakeError::NoResponse) |
218                P2PStreamError::PingTimeout,
219            ) => Some(BackoffKind::Low),
220            // malformed or abusive messages
221            Self::P2PStreamError(
222                P2PStreamError::Rlp(_) |
223                P2PStreamError::UnknownReservedMessageId(_) |
224                P2PStreamError::UnknownSubprotocolMessageId(_) |
225                P2PStreamError::UnknownDisconnectReason(_) |
226                P2PStreamError::MessageTooBig { .. } |
227                P2PStreamError::SubprotocolMessageTooBig { .. } |
228                P2PStreamError::EmptyProtocolMessage |
229                P2PStreamError::InvalidPingPongPayload(_) |
230                P2PStreamError::TooManyPings |
231                P2PStreamError::PingerError(_) |
232                P2PStreamError::Snap(_),
233            ) => Some(BackoffKind::Medium),
234            Self::P2PStreamError(P2PStreamError::SubprotocolInboundBufferFull { .. }) => {
235                Some(BackoffKind::Low)
236            }
237            Self::EthHandshakeError(EthHandshakeError::InvalidFork(_)) => {
238                // the remote can come back online after updating client version, so we can back off
239                // for a bit
240                Some(BackoffKind::Medium)
241            }
242            _ => None,
243        }
244    }
245}
246
247impl SessionError for PendingSessionHandshakeError {
248    fn merits_discovery_ban(&self) -> bool {
249        match self {
250            Self::Eth(eth) => eth.merits_discovery_ban(),
251            Self::Ecies(err) => matches!(
252                err.inner(),
253                ECIESErrorImpl::TagCheckDecryptFailed |
254                    ECIESErrorImpl::TagCheckHeaderFailed |
255                    ECIESErrorImpl::TagCheckBodyFailed |
256                    ECIESErrorImpl::InvalidAuthData |
257                    ECIESErrorImpl::InvalidAckData |
258                    ECIESErrorImpl::InvalidHeader |
259                    ECIESErrorImpl::Secp256k1(_) |
260                    ECIESErrorImpl::InvalidHandshake { .. }
261            ),
262            // A peer that announces someone else's node id is broken or hostile, and its discovery
263            // record cannot be trusted to point at a usable peer.
264            Self::UnexpectedHandshakeIdentity(_) => true,
265            Self::Timeout | Self::UnsupportedExtraCapability => false,
266        }
267    }
268
269    fn is_fatal_protocol_error(&self) -> bool {
270        match self {
271            Self::Eth(eth) => eth.is_fatal_protocol_error(),
272            Self::Ecies(err) => matches!(
273                err.inner(),
274                ECIESErrorImpl::TagCheckDecryptFailed |
275                    ECIESErrorImpl::TagCheckHeaderFailed |
276                    ECIESErrorImpl::TagCheckBodyFailed |
277                    ECIESErrorImpl::InvalidAuthData |
278                    ECIESErrorImpl::InvalidAckData |
279                    ECIESErrorImpl::InvalidHeader |
280                    ECIESErrorImpl::Secp256k1(_) |
281                    ECIESErrorImpl::InvalidHandshake { .. }
282            ),
283            Self::Timeout => false,
284            Self::UnsupportedExtraCapability | Self::UnexpectedHandshakeIdentity(_) => true,
285        }
286    }
287
288    fn should_backoff(&self) -> Option<BackoffKind> {
289        match self {
290            Self::Eth(eth) => eth.should_backoff(),
291            Self::Ecies(_) => Some(BackoffKind::Low),
292            Self::Timeout => Some(BackoffKind::Medium),
293            Self::UnsupportedExtraCapability | Self::UnexpectedHandshakeIdentity(_) => {
294                Some(BackoffKind::High)
295            }
296        }
297    }
298}
299
300impl SessionError for io::Error {
301    fn merits_discovery_ban(&self) -> bool {
302        false
303    }
304
305    fn is_fatal_protocol_error(&self) -> bool {
306        false
307    }
308
309    fn should_backoff(&self) -> Option<BackoffKind> {
310        match self.kind() {
311            // these usually happen when the remote instantly drops the connection, for example
312            // if the previous connection isn't properly cleaned up yet and the peer is temp.
313            // banned.
314            ErrorKind::ConnectionReset | ErrorKind::BrokenPipe => Some(BackoffKind::Low),
315            ErrorKind::ConnectionRefused => {
316                // peer is unreachable, e.g. port not open or down
317                Some(BackoffKind::High)
318            }
319            _ => Some(BackoffKind::Medium),
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use std::net::{Ipv4Addr, SocketAddrV4};
328
329    #[test]
330    fn test_is_fatal_disconnect() {
331        let err = PendingSessionHandshakeError::Eth(EthStreamError::P2PStreamError(
332            P2PStreamError::HandshakeError(P2PHandshakeError::Disconnected(
333                DisconnectReason::UselessPeer,
334            )),
335        ));
336
337        assert!(err.is_fatal_protocol_error());
338    }
339
340    #[test]
341    fn test_should_backoff() {
342        let err = EthStreamError::P2PStreamError(P2PStreamError::HandshakeError(
343            P2PHandshakeError::Disconnected(DisconnectReason::TooManyPeers),
344        ));
345
346        assert_eq!(err.as_disconnected(), Some(DisconnectReason::TooManyPeers));
347        assert_eq!(err.should_backoff(), Some(BackoffKind::Low));
348
349        let err = EthStreamError::P2PStreamError(P2PStreamError::HandshakeError(
350            P2PHandshakeError::NoResponse,
351        ));
352        assert_eq!(err.should_backoff(), Some(BackoffKind::Low));
353
354        let err = EthStreamError::P2PStreamError(P2PStreamError::InvalidPingPongPayload(0x02));
355        assert_eq!(err.should_backoff(), Some(BackoffKind::Medium));
356
357        let err = EthStreamError::P2PStreamError(P2PStreamError::TooManyPings);
358        assert!(err.is_protocol_breach());
359        assert_eq!(err.should_backoff(), Some(BackoffKind::Medium));
360    }
361
362    #[test]
363    fn subprotocol_ingress_errors_have_distinct_reputation_outcomes() {
364        let capability = reth_eth_wire::Capability::new_static("test", 1);
365        let unknown =
366            EthStreamError::P2PStreamError(P2PStreamError::UnknownSubprotocolMessageId(0xff));
367        assert!(unknown.is_protocol_breach());
368        assert!(unknown.is_fatal_protocol_error());
369
370        let oversized = EthStreamError::P2PStreamError(P2PStreamError::SubprotocolMessageTooBig {
371            capability: capability.clone(),
372            message_size: 5,
373            max_size: 4,
374        });
375        assert!(!oversized.is_protocol_breach());
376        assert!(!oversized.is_fatal_protocol_error());
377        assert_eq!(oversized.should_backoff(), Some(BackoffKind::Medium));
378
379        let full = EthStreamError::P2PStreamError(P2PStreamError::SubprotocolInboundBufferFull {
380            capability,
381        });
382        assert!(!full.is_protocol_breach());
383        assert!(!full.is_fatal_protocol_error());
384        assert_eq!(full.should_backoff(), Some(BackoffKind::Low));
385    }
386
387    #[test]
388    fn test_address_in_use_message() {
389        let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1234));
390        let kinds = [ServiceKind::Discovery(addr), ServiceKind::Listener(addr)];
391
392        for kind in &kinds {
393            let err = NetworkError::AddressAlreadyInUse {
394                kind: *kind,
395                error: io::Error::from(ErrorKind::AddrInUse),
396            };
397
398            assert!(err.to_string().contains(kind.flags()));
399        }
400    }
401}