Skip to main content

reth_network/session/
mod.rs

1//! Support for handling peer sessions.
2
3mod active;
4mod conn;
5mod counter;
6mod handle;
7mod types;
8pub use types::BlockRangeInfo;
9
10use crate::{
11    message::PeerMessage,
12    metrics::SessionManagerMetrics,
13    protocol::{IntoRlpxSubProtocol, OnNotSupported, RlpxSubProtocolHandlers, RlpxSubProtocols},
14    session::active::ActiveSession,
15};
16use active::QueuedOutgoingMessages;
17use alloy_primitives::map::{FbBuildHasher, HashMap};
18use counter::SessionCounter;
19use futures::{future::Either, io, FutureExt, StreamExt};
20use reth_ecies::{stream::ECIESStream, ECIESError};
21use reth_eth_wire::{
22    errors::EthStreamError, handshake::EthRlpxHandshake, multiplex::RlpxProtocolMultiplexer,
23    BlockRangeUpdate, Capabilities, DisconnectReason, EthSnapStream, EthStream, EthVersion,
24    HelloMessageWithProtocols, NetworkPrimitives, UnauthedP2PStream, UnifiedStatus,
25    HANDSHAKE_TIMEOUT,
26};
27use reth_ethereum_forks::{ForkFilter, ForkId, ForkTransition, Head};
28use reth_metrics::common::mpsc::MeteredPollSender;
29use reth_network_api::{PeerRequest, PeerRequestSender};
30use reth_network_peers::PeerId;
31use reth_network_types::SessionsConfig;
32use reth_tasks::Runtime;
33use rustc_hash::FxHashMap;
34use secp256k1::SecretKey;
35use std::{
36    future::Future,
37    net::SocketAddr,
38    sync::{atomic::AtomicU64, Arc},
39    task::{Context, Poll},
40    time::{Duration, Instant},
41};
42use tokio::{
43    io::{AsyncRead, AsyncWrite},
44    net::TcpStream,
45    sync::{mpsc, oneshot},
46};
47use tokio_stream::wrappers::ReceiverStream;
48use tokio_util::sync::PollSender;
49use tracing::{instrument, trace};
50
51use crate::session::active::{
52    request_timeout_interval, BroadcastItemCounter, RANGE_UPDATE_INTERVAL,
53};
54pub use conn::EthRlpxConnection;
55use handle::SessionCommandSender;
56pub use handle::{
57    ActiveSessionHandle, ActiveSessionMessage, PendingSessionEvent, PendingSessionHandle,
58    SessionCommand,
59};
60pub use reth_network_api::{Direction, PeerInfo};
61
62/// Internal identifier for active sessions.
63#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Hash)]
64pub struct SessionId(usize);
65
66/// Manages a set of sessions.
67#[must_use = "Session Manager must be polled to process session events."]
68#[derive(Debug)]
69pub struct SessionManager<N: NetworkPrimitives> {
70    /// Tracks the identifier for the next session.
71    next_id: usize,
72    /// Keeps track of all sessions
73    counter: SessionCounter,
74    ///  The maximum initial time an [`ActiveSession`] waits for a response from the peer before it
75    /// responds to an _internal_ request with a `TimeoutError`
76    initial_internal_request_timeout: Duration,
77    /// If an [`ActiveSession`] does not receive a response at all within this duration then it is
78    /// considered a protocol violation and the session will initiate a drop.
79    protocol_breach_request_timeout: Duration,
80    /// The timeout after which a pending session attempt is considered failed.
81    pending_session_timeout: Duration,
82    /// The secret key used for authenticating sessions.
83    secret_key: SecretKey,
84    /// The `Status` message to send to peers.
85    status: UnifiedStatus,
86    /// The `HelloMessage` message to send to peers.
87    hello_message: HelloMessageWithProtocols,
88    /// The [`ForkFilter`] used to validate the peer's `Status` message.
89    fork_filter: ForkFilter,
90    /// Size of the command buffer per session.
91    session_command_buffer: usize,
92    /// The executor for spawned tasks.
93    executor: Runtime,
94    /// All pending session that are currently handshaking, exchanging `Hello`s.
95    ///
96    /// Events produced during the authentication phase are reported to this manager. Once the
97    /// session is authenticated, it can be moved to the `active_session` set.
98    pending_sessions: FxHashMap<SessionId, PendingSessionHandle>,
99    /// All active sessions that are ready to exchange messages.
100    active_sessions: HashMap<PeerId, ActiveSessionHandle<N>, FbBuildHasher<64>>,
101    /// The original Sender half of the [`PendingSessionEvent`] channel.
102    ///
103    /// When a new (pending) session is created, the corresponding [`PendingSessionHandle`] will
104    /// get a clone of this sender half.
105    pending_sessions_tx: mpsc::Sender<PendingSessionEvent<N>>,
106    /// Receiver half that listens for [`PendingSessionEvent`] produced by pending sessions.
107    pending_session_rx: ReceiverStream<PendingSessionEvent<N>>,
108    /// The original Sender half of the [`ActiveSessionMessage`] channel.
109    ///
110    /// When active session state is reached, the corresponding [`ActiveSessionHandle`] will get a
111    /// clone of this sender half.
112    active_session_tx: MeteredPollSender<ActiveSessionMessage<N>>,
113    /// Receiver half that listens for [`ActiveSessionMessage`] produced by pending sessions.
114    active_session_rx: ReceiverStream<ActiveSessionMessage<N>>,
115    /// Additional `RLPx` sub-protocols to be used by the session manager.
116    extra_protocols: RlpxSubProtocols,
117    /// Tracks the ongoing graceful disconnections attempts for incoming connections.
118    disconnections_counter: DisconnectionsCounter,
119    /// Metrics for the session manager.
120    metrics: SessionManagerMetrics,
121    /// The [`EthRlpxHandshake`] is used to perform the initial handshake with the peer.
122    handshake: Arc<dyn EthRlpxHandshake>,
123    /// Maximum allowed ETH message size for post-handshake ETH/Snap streams.
124    eth_max_message_size: usize,
125    /// Shared local range information that gets propagated to active sessions.
126    /// This represents the range of blocks that this node can serve to other peers.
127    local_range_info: BlockRangeInfo,
128    /// When true, block announcement messages (`NewBlock`, `NewBlockHashes`) are rejected before
129    /// RLP decoding on new sessions to avoid memory amplification.
130    reject_block_announcements: bool,
131}
132
133// === impl SessionManager ===
134
135impl<N: NetworkPrimitives> SessionManager<N> {
136    /// Creates a new empty [`SessionManager`].
137    #[expect(clippy::too_many_arguments)]
138    pub fn new(
139        secret_key: SecretKey,
140        config: SessionsConfig,
141        executor: Runtime,
142        status: UnifiedStatus,
143        hello_message: HelloMessageWithProtocols,
144        fork_filter: ForkFilter,
145        extra_protocols: RlpxSubProtocols,
146        handshake: Arc<dyn EthRlpxHandshake>,
147        eth_max_message_size: usize,
148        reject_block_announcements: bool,
149    ) -> Self {
150        let (pending_sessions_tx, pending_sessions_rx) = mpsc::channel(config.session_event_buffer);
151        let (active_session_tx, active_session_rx) = mpsc::channel(config.session_event_buffer);
152        let active_session_tx = PollSender::new(active_session_tx);
153
154        // Initialize local range info from the status
155        let local_range_info = BlockRangeInfo::new(
156            status.earliest_block.unwrap_or_default(),
157            status.latest_block.unwrap_or_default(),
158            status.blockhash,
159        );
160
161        Self {
162            next_id: 0,
163            counter: SessionCounter::new(config.limits),
164            initial_internal_request_timeout: config.initial_internal_request_timeout,
165            protocol_breach_request_timeout: config.protocol_breach_request_timeout,
166            pending_session_timeout: config.pending_session_timeout,
167            secret_key,
168            status,
169            hello_message,
170            fork_filter,
171            session_command_buffer: config.session_command_buffer,
172            executor,
173            pending_sessions: Default::default(),
174            active_sessions: Default::default(),
175            pending_sessions_tx,
176            pending_session_rx: ReceiverStream::new(pending_sessions_rx),
177            active_session_tx: MeteredPollSender::new(active_session_tx, "network_active_session"),
178            active_session_rx: ReceiverStream::new(active_session_rx),
179            extra_protocols,
180            disconnections_counter: Default::default(),
181            metrics: Default::default(),
182            handshake,
183            eth_max_message_size,
184            local_range_info,
185            reject_block_announcements,
186        }
187    }
188
189    /// Returns the currently tracked [`ForkId`].
190    pub(crate) const fn fork_id(&self) -> ForkId {
191        self.fork_filter.current()
192    }
193
194    /// Check whether the provided [`ForkId`] is compatible based on the validation rules in
195    /// `EIP-2124`.
196    pub fn is_valid_fork_id(&self, fork_id: ForkId) -> bool {
197        self.fork_filter.validate(fork_id).is_ok()
198    }
199
200    /// Returns the next unique [`SessionId`].
201    const fn next_id(&mut self) -> SessionId {
202        let id = self.next_id;
203        self.next_id += 1;
204        SessionId(id)
205    }
206
207    /// Returns the current status of the session.
208    pub const fn status(&self) -> UnifiedStatus {
209        self.status
210    }
211
212    /// Returns the secret key used for authenticating sessions.
213    pub const fn secret_key(&self) -> SecretKey {
214        self.secret_key
215    }
216
217    /// Returns a borrowed reference to the active sessions.
218    pub const fn active_sessions(
219        &self,
220    ) -> &HashMap<PeerId, ActiveSessionHandle<N>, FbBuildHasher<64>> {
221        &self.active_sessions
222    }
223
224    /// Returns the session hello message.
225    pub fn hello_message(&self) -> HelloMessageWithProtocols {
226        self.hello_message.clone()
227    }
228
229    /// Adds an additional protocol handler to the `RLPx` sub-protocol list.
230    pub(crate) fn add_rlpx_sub_protocol(&mut self, protocol: impl IntoRlpxSubProtocol) {
231        self.extra_protocols.push(protocol)
232    }
233
234    /// Returns the number of currently pending connections.
235    #[inline]
236    pub(crate) fn num_pending_connections(&self) -> usize {
237        self.pending_sessions.len()
238    }
239
240    /// Spawns the given future onto a new task that is tracked in the `spawned_tasks`
241    /// [`JoinSet`](tokio::task::JoinSet).
242    fn spawn<F>(&self, f: F)
243    where
244        F: Future<Output = ()> + Send + 'static,
245    {
246        self.executor.spawn_task(f);
247    }
248
249    /// Invoked on a received status update.
250    ///
251    /// If the updated activated another fork, this will return a [`ForkTransition`] and updates the
252    /// active [`ForkId`]. See also [`ForkFilter::set_head`].
253    pub(crate) fn on_status_update(&mut self, head: Head) -> Option<ForkTransition> {
254        self.status.blockhash = head.hash;
255        self.status.total_difficulty = Some(head.total_difficulty);
256        let transition = self.fork_filter.set_head(head);
257        self.status.forkid = self.fork_filter.current();
258        self.status.latest_block = Some(head.number);
259
260        transition
261    }
262
263    /// An incoming TCP connection was received. This starts the authentication process to turn this
264    /// stream into an active peer session.
265    ///
266    /// Returns an error if the configured limit has been reached.
267    pub(crate) fn on_incoming(
268        &mut self,
269        stream: TcpStream,
270        remote_addr: SocketAddr,
271    ) -> Result<SessionId, ExceedsSessionLimit> {
272        self.counter.ensure_pending_inbound()?;
273
274        let session_id = self.next_id();
275
276        trace!(
277            target: "net::session",
278            ?remote_addr,
279            ?session_id,
280            "new pending incoming session"
281        );
282
283        let (disconnect_tx, disconnect_rx) = oneshot::channel();
284        let pending_events = self.pending_sessions_tx.clone();
285        let secret_key = self.secret_key;
286        let hello_message = self.hello_message.clone();
287        let status = self.status;
288        let fork_filter = self.fork_filter.clone();
289        let extra_handlers = self.extra_protocols.on_incoming(remote_addr);
290        self.spawn(pending_session_with_timeout(
291            self.pending_session_timeout,
292            session_id,
293            remote_addr,
294            Direction::Incoming,
295            pending_events.clone(),
296            start_pending_incoming_session(
297                self.handshake.clone(),
298                self.eth_max_message_size,
299                disconnect_rx,
300                session_id,
301                stream,
302                pending_events,
303                remote_addr,
304                secret_key,
305                hello_message,
306                status,
307                fork_filter,
308                extra_handlers,
309            ),
310        ));
311
312        let handle = PendingSessionHandle {
313            disconnect_tx: Some(disconnect_tx),
314            direction: Direction::Incoming,
315        };
316        self.pending_sessions.insert(session_id, handle);
317        self.counter.inc_pending_inbound();
318        Ok(session_id)
319    }
320
321    /// Starts a new pending session from the local node to the given remote node.
322    pub fn dial_outbound(&mut self, remote_addr: SocketAddr, remote_peer_id: PeerId) {
323        // The error can be dropped because no dial will be made if it would exceed the limit
324        if self.counter.ensure_pending_outbound().is_ok() {
325            let session_id = self.next_id();
326            let (disconnect_tx, disconnect_rx) = oneshot::channel();
327            let pending_events = self.pending_sessions_tx.clone();
328            let secret_key = self.secret_key;
329            let hello_message = self.hello_message.clone();
330            let fork_filter = self.fork_filter.clone();
331            let status = self.status;
332            let extra_handlers = self.extra_protocols.on_outgoing(remote_addr, remote_peer_id);
333            self.spawn(pending_session_with_timeout(
334                self.pending_session_timeout,
335                session_id,
336                remote_addr,
337                Direction::Outgoing(remote_peer_id),
338                pending_events.clone(),
339                start_pending_outbound_session(
340                    self.handshake.clone(),
341                    self.eth_max_message_size,
342                    disconnect_rx,
343                    pending_events,
344                    session_id,
345                    remote_addr,
346                    remote_peer_id,
347                    secret_key,
348                    hello_message,
349                    status,
350                    fork_filter,
351                    extra_handlers,
352                ),
353            ));
354
355            let handle = PendingSessionHandle {
356                disconnect_tx: Some(disconnect_tx),
357                direction: Direction::Outgoing(remote_peer_id),
358            };
359            self.pending_sessions.insert(session_id, handle);
360            self.counter.inc_pending_outbound();
361        }
362    }
363
364    /// Initiates a shutdown of the channel.
365    ///
366    /// This will trigger the disconnect on the session task to gracefully terminate. The result
367    /// will be picked up by the receiver.
368    pub fn disconnect(&self, node: PeerId, reason: Option<DisconnectReason>) {
369        if let Some(session) = self.active_sessions.get(&node) {
370            session.disconnect(reason);
371        }
372    }
373
374    /// Initiates a shutdown of all sessions.
375    ///
376    /// It will trigger the disconnect on all the session tasks to gracefully terminate. The result
377    /// will be picked by the receiver.
378    pub fn disconnect_all(&self, reason: Option<DisconnectReason>) {
379        for session in self.active_sessions.values() {
380            session.disconnect(reason);
381        }
382    }
383
384    /// Disconnects all pending sessions.
385    pub fn disconnect_all_pending(&mut self) {
386        for session in self.pending_sessions.values_mut() {
387            session.disconnect();
388        }
389    }
390
391    /// Sends a message to the peer's session.
392    ///
393    /// Broadcast messages use size-based backpressure: the total number of in-flight broadcast
394    /// items (across the command channel, overflow channel, and session outgoing queue) is tracked
395    /// by a shared atomic counter. If the bounded command channel is full but the broadcast limit
396    /// hasn't been reached, the message overflows to a dedicated unbounded channel.
397    pub fn send_message(&self, peer_id: &PeerId, msg: PeerMessage<N>) {
398        if let Some(session) = self.active_sessions.get(peer_id) &&
399            !session.commands.send_message(msg)
400        {
401            self.metrics.total_outgoing_peer_messages_dropped.increment(1);
402        }
403    }
404
405    /// Removes the [`PendingSessionHandle`] if it exists.
406    fn remove_pending_session(&mut self, id: &SessionId) -> Option<PendingSessionHandle> {
407        let session = self.pending_sessions.remove(id)?;
408        self.counter.dec_pending(&session.direction);
409        Some(session)
410    }
411
412    /// Removes the [`PendingSessionHandle`] if it exists.
413    fn remove_active_session(&mut self, id: &PeerId) -> Option<ActiveSessionHandle<N>> {
414        let session = self.active_sessions.remove(id)?;
415        self.counter.dec_active(&session.direction);
416        Some(session)
417    }
418
419    /// Try to gracefully disconnect an incoming connection by initiating a ECIES connection and
420    /// sending a disconnect. If [`SessionManager`] is at capacity for ongoing disconnections, will
421    /// simply drop the incoming connection.
422    pub(crate) fn try_disconnect_incoming_connection(
423        &self,
424        stream: TcpStream,
425        reason: DisconnectReason,
426    ) {
427        if !self.disconnections_counter.has_capacity() {
428            // drop the connection if we don't have capacity for gracefully disconnecting
429            return
430        }
431
432        let guard = self.disconnections_counter.clone();
433        let secret_key = self.secret_key;
434
435        self.spawn(async move {
436            trace!(
437                target: "net::session",
438                "gracefully disconnecting incoming connection"
439            );
440            if let Ok(stream) = get_ecies_stream(stream, secret_key, Direction::Incoming).await {
441                let mut unauth = UnauthedP2PStream::new(stream);
442                let _ = unauth.send_disconnect(reason).await;
443                drop(guard);
444            }
445        });
446    }
447
448    /// This polls all the session handles and returns [`SessionEvent`].
449    ///
450    /// Active sessions are prioritized.
451    pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SessionEvent<N>> {
452        // Poll events from active sessions
453        match self.active_session_rx.poll_next_unpin(cx) {
454            Poll::Pending => {}
455            Poll::Ready(None) => {
456                unreachable!("Manager holds both channel halves.")
457            }
458            Poll::Ready(Some(event)) => {
459                return match event {
460                    ActiveSessionMessage::Disconnected { peer_id, remote_addr } => {
461                        trace!(
462                            target: "net::session",
463                            ?peer_id,
464                            "gracefully disconnected active session."
465                        );
466                        self.remove_active_session(&peer_id);
467                        Poll::Ready(SessionEvent::Disconnected { peer_id, remote_addr })
468                    }
469                    ActiveSessionMessage::ClosedOnConnectionError {
470                        peer_id,
471                        remote_addr,
472                        error,
473                    } => {
474                        trace!(target: "net::session", ?peer_id, %error,"closed session.");
475                        self.remove_active_session(&peer_id);
476                        Poll::Ready(SessionEvent::SessionClosedOnConnectionError {
477                            remote_addr,
478                            peer_id,
479                            error,
480                        })
481                    }
482                    ActiveSessionMessage::ValidMessage { peer_id, message } => {
483                        Poll::Ready(SessionEvent::ValidMessage { peer_id, message })
484                    }
485                    ActiveSessionMessage::BadMessage { peer_id } => {
486                        Poll::Ready(SessionEvent::BadMessage { peer_id })
487                    }
488                    ActiveSessionMessage::ProtocolBreach { peer_id } => {
489                        Poll::Ready(SessionEvent::ProtocolBreach { peer_id })
490                    }
491                }
492            }
493        }
494
495        // Poll the pending session event stream
496        let event = match self.pending_session_rx.poll_next_unpin(cx) {
497            Poll::Pending => return Poll::Pending,
498            Poll::Ready(None) => unreachable!("Manager holds both channel halves."),
499            Poll::Ready(Some(event)) => event,
500        };
501        match event {
502            PendingSessionEvent::Established {
503                session_id,
504                remote_addr,
505                local_addr,
506                peer_id,
507                capabilities,
508                mut conn,
509                status,
510                direction,
511                client_id,
512                peer_listen_port,
513            } => {
514                // move from pending to established.
515                self.remove_pending_session(&session_id);
516
517                // If there's already a session to the peer then we disconnect right away
518                if self.active_sessions.contains_key(&peer_id) {
519                    trace!(
520                        target: "net::session",
521                        ?session_id,
522                        ?remote_addr,
523                        ?peer_id,
524                        ?direction,
525                        "already connected"
526                    );
527
528                    self.spawn(async move {
529                        // send a disconnect message
530                        let _ =
531                            conn.into_inner().disconnect(DisconnectReason::AlreadyConnected).await;
532                    });
533
534                    return Poll::Ready(SessionEvent::AlreadyConnected {
535                        peer_id,
536                        remote_addr,
537                        direction,
538                    })
539                }
540
541                let (commands_tx, commands_rx) = mpsc::channel(self.session_command_buffer);
542                let (unbounded_tx, unbounded_rx) = mpsc::unbounded_channel();
543
544                let (to_session_tx, messages_rx) = mpsc::channel(self.session_command_buffer);
545
546                let messages = PeerRequestSender::new(peer_id, to_session_tx);
547
548                let timeout = Arc::new(AtomicU64::new(
549                    self.initial_internal_request_timeout.as_millis() as u64,
550                ));
551
552                // negotiated version
553                let version = conn.version();
554
555                // Configure the interval at which the range information is updated, starting with
556                // ETH69. We use interval_at to delay the first tick, avoiding sending
557                // BlockRangeUpdate immediately after connection (which can cause issues with
558                // peers that don't properly handle the message).
559                let range_update_interval = (conn.version() >= EthVersion::Eth69).then(|| {
560                    let start = tokio::time::Instant::now() + RANGE_UPDATE_INTERVAL;
561                    let mut interval = tokio::time::interval_at(start, RANGE_UPDATE_INTERVAL);
562                    interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
563                    interval
564                });
565
566                // Shared counter of in-flight broadcast items. The session task must decrement
567                // this when it pops messages from the outgoing queue, and the
568                // `SessionCommandSender` increments it before enqueuing. This invariant ensures
569                // the `SessionManager` always has an accurate view of total buffered broadcast
570                // pressure for a peer.
571                let broadcast_items = BroadcastItemCounter::new();
572                let remote_range_info = status.block_range_update().map(|update| {
573                    BlockRangeInfo::new(update.earliest, update.latest, update.latest_hash)
574                });
575
576                if self.reject_block_announcements {
577                    conn.set_reject_block_announcements(true);
578                }
579
580                let session = ActiveSession {
581                    next_id: 0,
582                    remote_peer_id: peer_id,
583                    remote_addr,
584                    remote_capabilities: Arc::clone(&capabilities),
585                    session_id,
586                    commands_rx: ReceiverStream::new(commands_rx),
587                    unbounded_rx,
588                    unbounded_broadcast_msgs: self.metrics.total_unbounded_broadcast_msgs.clone(),
589                    to_session_manager: self.active_session_tx.clone(),
590                    pending_message_to_session: None,
591                    internal_request_rx: ReceiverStream::new(messages_rx).fuse(),
592                    inflight_requests: Default::default(),
593                    conn,
594                    queued_outgoing: QueuedOutgoingMessages::new(
595                        self.metrics.queued_outgoing_messages.clone(),
596                        broadcast_items.clone(),
597                    ),
598                    received_requests_from_remote: Default::default(),
599                    internal_request_timeout_interval: request_timeout_interval(
600                        self.initial_internal_request_timeout,
601                    ),
602                    internal_request_timeout: Arc::clone(&timeout),
603                    protocol_breach_request_timeout: self.protocol_breach_request_timeout,
604                    terminate_message: None,
605                    range_info: remote_range_info.clone(),
606                    local_range_info: self.local_range_info.clone(),
607                    range_update_interval,
608                    last_sent_latest_block: None,
609                };
610
611                let supports_snap = session.conn.supports_snap();
612                self.spawn(session);
613
614                let client_version = client_id.into();
615                let handle = ActiveSessionHandle {
616                    status: status.clone(),
617                    direction,
618                    session_id,
619                    remote_id: peer_id,
620                    version,
621                    established: Instant::now(),
622                    capabilities: Arc::clone(&capabilities),
623                    commands: SessionCommandSender::new(commands_tx, unbounded_tx, broadcast_items),
624                    client_version: Arc::clone(&client_version),
625                    remote_addr,
626                    local_addr,
627                    peer_listen_port,
628                };
629
630                self.active_sessions.insert(peer_id, handle);
631                self.counter.inc_active(&direction);
632
633                if direction.is_outgoing() {
634                    self.metrics.total_dial_successes.increment(1);
635                }
636
637                Poll::Ready(SessionEvent::SessionEstablished {
638                    peer_id,
639                    remote_addr,
640                    client_version,
641                    version,
642                    capabilities,
643                    status,
644                    messages,
645                    direction,
646                    timeout,
647                    range_info: remote_range_info,
648                    supports_snap,
649                })
650            }
651            PendingSessionEvent::Disconnected { remote_addr, session_id, direction, error } => {
652                trace!(
653                    target: "net::session",
654                    ?session_id,
655                    ?remote_addr,
656                    ?error,
657                    "disconnected pending session"
658                );
659                self.remove_pending_session(&session_id);
660                match direction {
661                    Direction::Incoming => {
662                        Poll::Ready(SessionEvent::IncomingPendingSessionClosed {
663                            remote_addr,
664                            error,
665                        })
666                    }
667                    Direction::Outgoing(peer_id) => {
668                        Poll::Ready(SessionEvent::OutgoingPendingSessionClosed {
669                            remote_addr,
670                            peer_id,
671                            error,
672                        })
673                    }
674                }
675            }
676            PendingSessionEvent::OutgoingConnectionError {
677                remote_addr,
678                session_id,
679                peer_id,
680                error,
681            } => {
682                trace!(
683                    target: "net::session",
684                    %error,
685                    ?session_id,
686                    ?remote_addr,
687                    ?peer_id,
688                    "connection refused"
689                );
690                self.remove_pending_session(&session_id);
691                Poll::Ready(SessionEvent::OutgoingConnectionError { remote_addr, peer_id, error })
692            }
693            PendingSessionEvent::EciesAuthError { remote_addr, session_id, error, direction } => {
694                trace!(
695                    target: "net::session",
696                    %error,
697                    ?session_id,
698                    ?remote_addr,
699                    "ecies auth failed"
700                );
701                self.remove_pending_session(&session_id);
702                match direction {
703                    Direction::Incoming => {
704                        Poll::Ready(SessionEvent::IncomingPendingSessionClosed {
705                            remote_addr,
706                            error: Some(PendingSessionHandshakeError::Ecies(error)),
707                        })
708                    }
709                    Direction::Outgoing(peer_id) => {
710                        Poll::Ready(SessionEvent::OutgoingPendingSessionClosed {
711                            remote_addr,
712                            peer_id,
713                            error: Some(PendingSessionHandshakeError::Ecies(error)),
714                        })
715                    }
716                }
717            }
718        }
719    }
720
721    /// Updates the advertised block range that this node can serve to other peers starting with
722    /// Eth69.
723    ///
724    /// This method updates both the local status message that gets sent to peers during handshake
725    /// and the shared local range information that gets propagated to active sessions (Eth69).
726    /// The range information is used in ETH69 protocol where peers announce the range of blocks
727    /// they can serve to optimize data synchronization.
728    pub(crate) fn update_advertised_block_range(&mut self, block_range_update: BlockRangeUpdate) {
729        self.status.earliest_block = Some(block_range_update.earliest);
730        self.status.latest_block = Some(block_range_update.latest);
731        self.status.blockhash = block_range_update.latest_hash;
732
733        // Update the shared local range info that gets propagated to active sessions
734        self.local_range_info.update(
735            block_range_update.earliest,
736            block_range_update.latest,
737            block_range_update.latest_hash,
738        );
739    }
740}
741
742/// A counter for ongoing graceful disconnections attempts.
743#[derive(Default, Debug, Clone)]
744struct DisconnectionsCounter(Arc<()>);
745
746impl DisconnectionsCounter {
747    const MAX_CONCURRENT_GRACEFUL_DISCONNECTIONS: usize = 15;
748
749    /// Returns true if the [`DisconnectionsCounter`] still has capacity
750    /// for an additional graceful disconnection.
751    fn has_capacity(&self) -> bool {
752        Arc::strong_count(&self.0) <= Self::MAX_CONCURRENT_GRACEFUL_DISCONNECTIONS
753    }
754}
755
756/// Events produced by the [`SessionManager`]
757#[derive(Debug)]
758pub enum SessionEvent<N: NetworkPrimitives> {
759    /// A new session was successfully authenticated.
760    ///
761    /// This session is now able to exchange data.
762    SessionEstablished {
763        /// The remote node's public key
764        peer_id: PeerId,
765        /// The remote node's socket address
766        remote_addr: SocketAddr,
767        /// The user agent of the remote node, usually containing the client name and version
768        client_version: Arc<str>,
769        /// The capabilities the remote node has announced
770        capabilities: Arc<Capabilities>,
771        /// negotiated eth version
772        version: EthVersion,
773        /// The Status message the peer sent during the `eth` handshake
774        status: Arc<UnifiedStatus>,
775        /// The channel for sending messages to the peer with the session
776        messages: PeerRequestSender<PeerRequest<N>>,
777        /// The direction of the session, either `Inbound` or `Outgoing`
778        direction: Direction,
779        /// The maximum time that the session waits for a response from the peer before timing out
780        /// the connection
781        timeout: Arc<AtomicU64>,
782        /// The range info for the peer.
783        range_info: Option<BlockRangeInfo>,
784        /// Whether the connection negotiated `snap/2` and can serve [`PeerRequest::GetSnap`].
785        supports_snap: bool,
786    },
787    /// The peer was already connected with another session.
788    AlreadyConnected {
789        /// The remote node's public key
790        peer_id: PeerId,
791        /// The remote node's socket address
792        remote_addr: SocketAddr,
793        /// The direction of the session, either `Inbound` or `Outgoing`
794        direction: Direction,
795    },
796    /// A session received a valid message via `RLPx`.
797    ValidMessage {
798        /// The remote node's public key
799        peer_id: PeerId,
800        /// Message received from the peer.
801        message: PeerMessage<N>,
802    },
803    /// Received a bad message from the peer.
804    BadMessage {
805        /// Identifier of the remote peer.
806        peer_id: PeerId,
807    },
808    /// Remote peer is considered in protocol violation
809    ProtocolBreach {
810        /// Identifier of the remote peer.
811        peer_id: PeerId,
812    },
813    /// Closed an incoming pending session during handshaking.
814    IncomingPendingSessionClosed {
815        /// The remote node's socket address
816        remote_addr: SocketAddr,
817        /// The pending handshake session error that caused the session to close
818        error: Option<PendingSessionHandshakeError>,
819    },
820    /// Closed an outgoing pending session during handshaking.
821    OutgoingPendingSessionClosed {
822        /// The remote node's socket address
823        remote_addr: SocketAddr,
824        /// The remote node's public key
825        peer_id: PeerId,
826        /// The pending handshake session error that caused the session to close
827        error: Option<PendingSessionHandshakeError>,
828    },
829    /// Failed to establish a tcp stream
830    OutgoingConnectionError {
831        /// The remote node's socket address
832        remote_addr: SocketAddr,
833        /// The remote node's public key
834        peer_id: PeerId,
835        /// The error that caused the outgoing connection to fail
836        error: io::Error,
837    },
838    /// Session was closed due to an error
839    SessionClosedOnConnectionError {
840        /// The id of the remote peer.
841        peer_id: PeerId,
842        /// The socket we were connected to.
843        remote_addr: SocketAddr,
844        /// The error that caused the session to close
845        error: EthStreamError,
846    },
847    /// Active session was gracefully disconnected.
848    Disconnected {
849        /// The remote node's public key
850        peer_id: PeerId,
851        /// The remote node's socket address that we were connected to
852        remote_addr: SocketAddr,
853    },
854}
855
856/// Errors that can occur during handshaking/authenticating the underlying streams.
857#[derive(Debug, thiserror::Error)]
858pub enum PendingSessionHandshakeError {
859    /// The pending session failed due to an error while establishing the `eth` stream
860    #[error(transparent)]
861    Eth(EthStreamError),
862    /// The pending session failed due to an error while establishing the ECIES stream
863    #[error(transparent)]
864    Ecies(ECIESError),
865    /// Thrown when the authentication timed out
866    #[error("authentication timed out")]
867    Timeout,
868    /// Thrown when the remote lacks the required capability
869    #[error("Mandatory extra capability unsupported")]
870    UnsupportedExtraCapability,
871}
872
873impl PendingSessionHandshakeError {
874    /// Returns the [`DisconnectReason`] if the error is a disconnect message
875    pub const fn as_disconnected(&self) -> Option<DisconnectReason> {
876        match self {
877            Self::Eth(eth_err) => eth_err.as_disconnected(),
878            _ => None,
879        }
880    }
881}
882
883/// The error thrown when the max configured limit has been reached and no more connections are
884/// accepted.
885#[derive(Debug, Clone, thiserror::Error)]
886#[error("session limit reached {0}")]
887pub struct ExceedsSessionLimit(pub(crate) u32);
888
889/// Starts a pending session authentication with a timeout.
890pub(crate) async fn pending_session_with_timeout<F, N: NetworkPrimitives>(
891    timeout: Duration,
892    session_id: SessionId,
893    remote_addr: SocketAddr,
894    direction: Direction,
895    events: mpsc::Sender<PendingSessionEvent<N>>,
896    f: F,
897) where
898    F: Future<Output = ()>,
899{
900    if tokio::time::timeout(timeout, f).await.is_err() {
901        trace!(target: "net::session", ?remote_addr, ?direction, "pending session timed out");
902        let event = PendingSessionEvent::Disconnected {
903            remote_addr,
904            session_id,
905            direction,
906            error: Some(PendingSessionHandshakeError::Timeout),
907        };
908        let _ = events.send(event).await;
909    }
910}
911
912/// Starts the authentication process for a connection initiated by a remote peer.
913///
914/// This will wait for the _incoming_ handshake request and answer it.
915#[expect(clippy::too_many_arguments)]
916pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
917    handshake: Arc<dyn EthRlpxHandshake>,
918    eth_max_message_size: usize,
919    disconnect_rx: oneshot::Receiver<()>,
920    session_id: SessionId,
921    stream: TcpStream,
922    events: mpsc::Sender<PendingSessionEvent<N>>,
923    remote_addr: SocketAddr,
924    secret_key: SecretKey,
925    hello: HelloMessageWithProtocols,
926    status: UnifiedStatus,
927    fork_filter: ForkFilter,
928    extra_handlers: RlpxSubProtocolHandlers,
929) {
930    authenticate(
931        handshake,
932        eth_max_message_size,
933        disconnect_rx,
934        events,
935        stream,
936        session_id,
937        remote_addr,
938        secret_key,
939        Direction::Incoming,
940        hello,
941        status,
942        fork_filter,
943        extra_handlers,
944    )
945    .await
946}
947
948/// Starts the authentication process for a connection initiated by a remote peer.
949#[instrument(level = "trace", target = "net::network", skip_all, fields(%remote_addr, peer_id = ?remote_peer_id))]
950#[expect(clippy::too_many_arguments)]
951async fn start_pending_outbound_session<N: NetworkPrimitives>(
952    handshake: Arc<dyn EthRlpxHandshake>,
953    eth_max_message_size: usize,
954    disconnect_rx: oneshot::Receiver<()>,
955    events: mpsc::Sender<PendingSessionEvent<N>>,
956    session_id: SessionId,
957    remote_addr: SocketAddr,
958    remote_peer_id: PeerId,
959    secret_key: SecretKey,
960    hello: HelloMessageWithProtocols,
961    status: UnifiedStatus,
962    fork_filter: ForkFilter,
963    extra_handlers: RlpxSubProtocolHandlers,
964) {
965    let stream = match TcpStream::connect(remote_addr).await {
966        Ok(stream) => {
967            if let Err(err) = stream.set_nodelay(true) {
968                tracing::warn!(target: "net::session", "set nodelay failed: {:?}", err);
969            }
970            stream
971        }
972        Err(error) => {
973            let _ = events
974                .send(PendingSessionEvent::OutgoingConnectionError {
975                    remote_addr,
976                    session_id,
977                    peer_id: remote_peer_id,
978                    error,
979                })
980                .await;
981            return
982        }
983    };
984    authenticate(
985        handshake,
986        eth_max_message_size,
987        disconnect_rx,
988        events,
989        stream,
990        session_id,
991        remote_addr,
992        secret_key,
993        Direction::Outgoing(remote_peer_id),
994        hello,
995        status,
996        fork_filter,
997        extra_handlers,
998    )
999    .await
1000}
1001
1002/// Authenticates a session
1003#[expect(clippy::too_many_arguments)]
1004async fn authenticate<N: NetworkPrimitives>(
1005    handshake: Arc<dyn EthRlpxHandshake>,
1006    eth_max_message_size: usize,
1007    disconnect_rx: oneshot::Receiver<()>,
1008    events: mpsc::Sender<PendingSessionEvent<N>>,
1009    stream: TcpStream,
1010    session_id: SessionId,
1011    remote_addr: SocketAddr,
1012    secret_key: SecretKey,
1013    direction: Direction,
1014    hello: HelloMessageWithProtocols,
1015    status: UnifiedStatus,
1016    fork_filter: ForkFilter,
1017    extra_handlers: RlpxSubProtocolHandlers,
1018) {
1019    let local_addr = stream.local_addr().ok();
1020    let stream = match get_ecies_stream(stream, secret_key, direction).await {
1021        Ok(stream) => stream,
1022        Err(error) => {
1023            let _ = events
1024                .send(PendingSessionEvent::EciesAuthError {
1025                    remote_addr,
1026                    session_id,
1027                    error,
1028                    direction,
1029                })
1030                .await;
1031            return
1032        }
1033    };
1034
1035    let unauthed = UnauthedP2PStream::new(stream);
1036
1037    let auth = authenticate_stream(
1038        handshake,
1039        eth_max_message_size,
1040        unauthed,
1041        session_id,
1042        remote_addr,
1043        local_addr,
1044        direction,
1045        hello,
1046        status,
1047        fork_filter,
1048        extra_handlers,
1049    )
1050    .boxed();
1051
1052    match futures::future::select(disconnect_rx, auth).await {
1053        Either::Left((_, _)) => {
1054            let _ = events
1055                .send(PendingSessionEvent::Disconnected {
1056                    remote_addr,
1057                    session_id,
1058                    direction,
1059                    error: None,
1060                })
1061                .await;
1062        }
1063        Either::Right((res, _)) => {
1064            let _ = events.send(res).await;
1065        }
1066    }
1067}
1068
1069/// Returns an [`ECIESStream`] if it can be built. If not, send a
1070/// [`PendingSessionEvent::EciesAuthError`] and returns `None`
1071async fn get_ecies_stream<Io: AsyncRead + AsyncWrite + Unpin>(
1072    stream: Io,
1073    secret_key: SecretKey,
1074    direction: Direction,
1075) -> Result<ECIESStream<Io>, ECIESError> {
1076    match direction {
1077        Direction::Incoming => ECIESStream::incoming(stream, secret_key).await,
1078        Direction::Outgoing(remote_peer_id) => {
1079            ECIESStream::connect(stream, secret_key, remote_peer_id).await
1080        }
1081    }
1082}
1083
1084/// Authenticate the stream via handshake
1085///
1086/// On Success return the authenticated stream as [`PendingSessionEvent`].
1087///
1088/// If additional [`RlpxSubProtocolHandlers`] are provided, the hello message will be updated to
1089/// also negotiate the additional protocols.
1090#[expect(clippy::too_many_arguments)]
1091async fn authenticate_stream<N: NetworkPrimitives>(
1092    handshake: Arc<dyn EthRlpxHandshake>,
1093    eth_max_message_size: usize,
1094    stream: UnauthedP2PStream<ECIESStream<TcpStream>>,
1095    session_id: SessionId,
1096    remote_addr: SocketAddr,
1097    local_addr: Option<SocketAddr>,
1098    direction: Direction,
1099    mut hello: HelloMessageWithProtocols,
1100    mut status: UnifiedStatus,
1101    fork_filter: ForkFilter,
1102    mut extra_handlers: RlpxSubProtocolHandlers,
1103) -> PendingSessionEvent<N> {
1104    // Add extra protocols to the hello message
1105    extra_handlers.retain(|handler| hello.try_add_protocol(handler.protocol()).is_ok());
1106
1107    // conduct the p2p rlpx handshake and return the rlpx authenticated stream
1108    let (mut p2p_stream, their_hello) = match stream.handshake(hello).await {
1109        Ok(stream_res) => stream_res,
1110        Err(err) => {
1111            return PendingSessionEvent::Disconnected {
1112                remote_addr,
1113                session_id,
1114                direction,
1115                error: Some(PendingSessionHandshakeError::Eth(err.into())),
1116            }
1117        }
1118    };
1119
1120    // if we have extra handlers, check if it must be supported by the remote
1121    if !extra_handlers.is_empty() {
1122        // ensure that no extra handlers that aren't supported are not mandatory
1123        while let Some(pos) = extra_handlers.iter().position(|handler| {
1124            p2p_stream
1125                .shared_capabilities()
1126                .ensure_matching_capability(&handler.protocol().cap)
1127                .is_err()
1128        }) {
1129            let handler = extra_handlers.remove(pos);
1130            if handler.on_unsupported_by_peer(
1131                p2p_stream.shared_capabilities(),
1132                direction,
1133                their_hello.id,
1134            ) == OnNotSupported::Disconnect
1135            {
1136                return PendingSessionEvent::Disconnected {
1137                    remote_addr,
1138                    session_id,
1139                    direction,
1140                    error: Some(PendingSessionHandshakeError::UnsupportedExtraCapability),
1141                };
1142            }
1143        }
1144    }
1145
1146    // Ensure we negotiated mandatory eth protocol
1147    let eth_version = match p2p_stream.shared_capabilities().eth_version() {
1148        Ok(version) => version,
1149        Err(err) => {
1150            return PendingSessionEvent::Disconnected {
1151                remote_addr,
1152                session_id,
1153                direction,
1154                error: Some(PendingSessionHandshakeError::Eth(err.into())),
1155            }
1156        }
1157    };
1158
1159    // Before trying status handshake, set up the version to negotiated shared version
1160    status.set_eth_version(eth_version);
1161
1162    let (conn, their_status) = if p2p_stream.shared_capabilities().len() == 1 {
1163        // if the shared caps are 1, we know both support the eth version
1164        // if the hello handshake was successful we can try status handshake
1165
1166        // perform the eth protocol handshake
1167        match handshake
1168            .handshake(&mut p2p_stream, status, fork_filter.clone(), HANDSHAKE_TIMEOUT)
1169            .await
1170        {
1171            Ok(their_status) => {
1172                let eth_stream =
1173                    EthStream::with_max_message_size(eth_version, p2p_stream, eth_max_message_size);
1174                (eth_stream.into(), their_status)
1175            }
1176            Err(err) => {
1177                return PendingSessionEvent::Disconnected {
1178                    remote_addr,
1179                    session_id,
1180                    direction,
1181                    error: Some(PendingSessionHandshakeError::Eth(err)),
1182                }
1183            }
1184        }
1185    } else if p2p_stream.shared_capabilities().is_exact_eth_snap_v2() {
1186        // Exactly `eth` + `snap/2` (no other extras): use the dedicated stream instead of the
1187        // general-purpose satellite multiplexer. If `snap/2` is negotiated alongside other extra
1188        // capabilities, fall through to the satellite path — the dedicated stream only composes
1189        // `eth` and `snap/2`.
1190        match EthSnapStream::handshake(
1191            p2p_stream,
1192            status,
1193            fork_filter,
1194            handshake,
1195            eth_max_message_size,
1196        )
1197        .await
1198        {
1199            Ok((stream, their_status)) => (stream.into(), their_status),
1200            Err(err) => {
1201                return PendingSessionEvent::Disconnected {
1202                    remote_addr,
1203                    session_id,
1204                    direction,
1205                    error: Some(PendingSessionHandshakeError::Eth(err)),
1206                }
1207            }
1208        }
1209    } else {
1210        // Multiplex the stream with the extra protocols
1211        let mut multiplex_stream = RlpxProtocolMultiplexer::new(p2p_stream);
1212
1213        // install additional handlers
1214        for handler in extra_handlers.into_iter() {
1215            let cap = handler.protocol().cap;
1216            let remote_peer_id = their_hello.id;
1217
1218            multiplex_stream
1219                .install_protocol(&cap, move |conn| {
1220                    handler.into_connection(direction, remote_peer_id, conn)
1221                })
1222                .ok();
1223        }
1224
1225        let (multiplex_stream, their_status) = match multiplex_stream
1226            .into_eth_satellite_stream(status, fork_filter, handshake, eth_max_message_size)
1227            .await
1228        {
1229            Ok((multiplex_stream, their_status)) => (multiplex_stream, their_status),
1230            Err(err) => {
1231                return PendingSessionEvent::Disconnected {
1232                    remote_addr,
1233                    session_id,
1234                    direction,
1235                    error: Some(PendingSessionHandshakeError::Eth(err)),
1236                }
1237            }
1238        };
1239
1240        (multiplex_stream.into(), their_status)
1241    };
1242
1243    // `port` field is effectively deprecated, so we treat 0 value as a missing port.
1244    let peer_listen_port = (their_hello.port != 0).then_some(their_hello.port);
1245
1246    PendingSessionEvent::Established {
1247        session_id,
1248        remote_addr,
1249        local_addr,
1250        peer_id: their_hello.id,
1251        capabilities: Arc::new(Capabilities::from(their_hello.capabilities)),
1252        status: Arc::new(their_status),
1253        conn,
1254        direction,
1255        client_id: their_hello.client_version,
1256        peer_listen_port,
1257    }
1258}