Skip to main content

reth_network/session/
mod.rs

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