1use 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#[derive(Debug, PartialEq, Eq, Copy, Clone)]
15pub enum ServiceKind {
16 Listener(SocketAddr),
18 Discovery(SocketAddr),
20}
21
22impl ServiceKind {
23 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#[derive(Debug, thiserror::Error)]
43pub enum NetworkError {
44 #[error(transparent)]
46 Io(#[from] io::Error),
47 #[error("address {kind} is already in use (os error 98). Choose a different port using {}", kind.flags())]
49 AddressAlreadyInUse {
50 kind: ServiceKind,
52 error: io::Error,
54 },
55 #[error("failed to launch discovery service on {0}: {1}")]
57 Discovery(SocketAddr, io::Error),
58 #[error("discv5 error, {0}")]
60 Discv5Error(#[from] reth_discv5::Error),
61 #[error("failed to configure DNS resolver: {0}")]
65 DnsResolver(#[from] NetError),
66}
67
68impl NetworkError {
69 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#[auto_impl::auto_impl(&)]
85pub(crate) trait SessionError: fmt::Debug + fmt::Display {
86 fn merits_discovery_ban(&self) -> bool;
89
90 fn is_fatal_protocol_error(&self) -> bool;
98
99 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 false
122 }
123 EthHandshakeError::InvalidFork(_) => {
124 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 false
169 }
170 EthHandshakeError::InvalidFork(_) => {
171 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 Some(BackoffKind::High)
207 }
208 }
209 }
210
211 match self {
214 Self::EthHandshakeError(EthHandshakeError::NoResponse) |
216 Self::P2PStreamError(
217 P2PStreamError::HandshakeError(P2PHandshakeError::NoResponse) |
218 P2PStreamError::PingTimeout,
219 ) => Some(BackoffKind::Low),
220 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 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 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 ErrorKind::ConnectionReset | ErrorKind::BrokenPipe => Some(BackoffKind::Low),
315 ErrorKind::ConnectionRefused => {
316 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}