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    /// Replaces the active [`ForkFilter`] with a newly derived one and returns the resulting
267    /// [`ForkId`].
268    ///
269    /// This is used to adopt a fork schedule that changed at runtime (e.g. an L1-signalled
270    /// upgrade) without restarting the node. The caller is expected to build `fork_filter` from
271    /// the updated chain spec already advanced to the node's current head, so that
272    /// [`ForkFilter::current`] reflects the correct [`ForkId`] immediately.
273    ///
274    /// Every subsequent handshake clones this filter and advertises the returned [`ForkId`], so
275    /// the node's advertised fork identity stays aligned with the rules it now enforces. Existing
276    /// active sessions are not revalidated here; see [`SessionManager::active_sessions`].
277    pub(crate) fn set_fork_filter(&mut self, fork_filter: ForkFilter) -> ForkId {
278        self.fork_filter = fork_filter;
279        self.status.forkid = self.fork_filter.current();
280        self.status.forkid
281    }
282
283    /// An incoming TCP connection was received. This starts the authentication process to turn this
284    /// stream into an active peer session.
285    ///
286    /// Returns an error if the configured limit has been reached.
287    pub(crate) fn on_incoming(
288        &mut self,
289        stream: TcpStream,
290        remote_addr: SocketAddr,
291    ) -> Result<SessionId, ExceedsSessionLimit> {
292        self.counter.ensure_pending_inbound()?;
293
294        let session_id = self.next_id();
295
296        trace!(
297            target: "net::session",
298            ?remote_addr,
299            ?session_id,
300            "new pending incoming session"
301        );
302
303        let (disconnect_tx, disconnect_rx) = oneshot::channel();
304        let pending_events = self.pending_sessions_tx.clone();
305        let secret_key = self.secret_key;
306        let hello_message = self.hello_message.clone();
307        let status = self.status;
308        let fork_filter = self.fork_filter.clone();
309        let extra_handlers = self.extra_protocols.on_incoming(remote_addr);
310        self.spawn(pending_session_with_timeout(
311            self.pending_session_timeout,
312            session_id,
313            remote_addr,
314            Direction::Incoming,
315            pending_events.clone(),
316            start_pending_incoming_session(
317                self.handshake.clone(),
318                self.eth_max_message_size,
319                disconnect_rx,
320                session_id,
321                stream,
322                pending_events,
323                remote_addr,
324                secret_key,
325                hello_message,
326                status,
327                fork_filter,
328                extra_handlers,
329            ),
330        ));
331
332        let handle = PendingSessionHandle {
333            disconnect_tx: Some(disconnect_tx),
334            direction: Direction::Incoming,
335        };
336        self.pending_sessions.insert(session_id, handle);
337        self.counter.inc_pending_inbound();
338        Ok(session_id)
339    }
340
341    /// Starts a new pending session from the local node to the given remote node.
342    pub fn dial_outbound(&mut self, remote_addr: SocketAddr, remote_peer_id: PeerId) {
343        // The error can be dropped because no dial will be made if it would exceed the limit
344        if self.counter.ensure_pending_outbound().is_ok() {
345            let session_id = self.next_id();
346            let (disconnect_tx, disconnect_rx) = oneshot::channel();
347            let pending_events = self.pending_sessions_tx.clone();
348            let secret_key = self.secret_key;
349            let hello_message = self.hello_message.clone();
350            let fork_filter = self.fork_filter.clone();
351            let status = self.status;
352            let extra_handlers = self.extra_protocols.on_outgoing(remote_addr, remote_peer_id);
353            self.spawn(pending_session_with_timeout(
354                self.pending_session_timeout,
355                session_id,
356                remote_addr,
357                Direction::Outgoing(remote_peer_id),
358                pending_events.clone(),
359                start_pending_outbound_session(
360                    self.handshake.clone(),
361                    self.eth_max_message_size,
362                    disconnect_rx,
363                    pending_events,
364                    session_id,
365                    remote_addr,
366                    remote_peer_id,
367                    secret_key,
368                    hello_message,
369                    status,
370                    fork_filter,
371                    extra_handlers,
372                ),
373            ));
374
375            let handle = PendingSessionHandle {
376                disconnect_tx: Some(disconnect_tx),
377                direction: Direction::Outgoing(remote_peer_id),
378            };
379            self.pending_sessions.insert(session_id, handle);
380            self.counter.inc_pending_outbound();
381        }
382    }
383
384    /// Initiates a shutdown of the channel.
385    ///
386    /// This will trigger the disconnect on the session task to gracefully terminate. The result
387    /// will be picked up by the receiver.
388    pub fn disconnect(&self, node: PeerId, reason: Option<DisconnectReason>) {
389        if let Some(session) = self.active_sessions.get(&node) {
390            session.disconnect(reason);
391        }
392    }
393
394    /// Initiates a shutdown of all sessions.
395    ///
396    /// It will trigger the disconnect on all the session tasks to gracefully terminate. The result
397    /// will be picked by the receiver.
398    pub fn disconnect_all(&self, reason: Option<DisconnectReason>) {
399        for session in self.active_sessions.values() {
400            session.disconnect(reason);
401        }
402    }
403
404    /// Disconnects all pending sessions.
405    pub fn disconnect_all_pending(&mut self) {
406        for session in self.pending_sessions.values_mut() {
407            session.disconnect();
408        }
409    }
410
411    /// Sends a message to the peer's session.
412    ///
413    /// Broadcast messages use size-based backpressure: the total number of in-flight broadcast
414    /// items (across the command channel, overflow channel, and session outgoing queue) is tracked
415    /// by a shared atomic counter. If the bounded command channel is full but the broadcast limit
416    /// hasn't been reached, the message overflows to a dedicated unbounded channel.
417    pub fn send_message(&self, peer_id: &PeerId, msg: PeerMessage<N>) {
418        if let Some(session) = self.active_sessions.get(peer_id) &&
419            !session.commands.send_message(msg)
420        {
421            self.metrics.total_outgoing_peer_messages_dropped.increment(1);
422        }
423    }
424
425    /// Removes the [`PendingSessionHandle`] if it exists.
426    fn remove_pending_session(&mut self, id: &SessionId) -> Option<PendingSessionHandle> {
427        let session = self.pending_sessions.remove(id)?;
428        self.counter.dec_pending(&session.direction);
429        Some(session)
430    }
431
432    /// Removes the [`PendingSessionHandle`] if it exists.
433    fn remove_active_session(&mut self, id: &PeerId) -> Option<ActiveSessionHandle<N>> {
434        let session = self.active_sessions.remove(id)?;
435        self.counter.dec_active(&session.direction);
436        Some(session)
437    }
438
439    /// Try to gracefully disconnect an incoming connection by initiating a ECIES connection and
440    /// sending a disconnect. If [`SessionManager`] is at capacity for ongoing disconnections, will
441    /// simply drop the incoming connection.
442    pub(crate) fn try_disconnect_incoming_connection(
443        &self,
444        stream: TcpStream,
445        reason: DisconnectReason,
446    ) {
447        if !self.disconnections_counter.has_capacity() {
448            // drop the connection if we don't have capacity for gracefully disconnecting
449            return
450        }
451
452        let guard = self.disconnections_counter.clone();
453        let secret_key = self.secret_key;
454
455        self.spawn(async move {
456            trace!(
457                target: "net::session",
458                "gracefully disconnecting incoming connection"
459            );
460            if let Ok(stream) = get_ecies_stream(stream, secret_key, Direction::Incoming).await {
461                let mut unauth = UnauthedP2PStream::new(stream);
462                let _ = unauth.send_disconnect(reason).await;
463                drop(guard);
464            }
465        });
466    }
467
468    /// This polls all the session handles and returns [`SessionEvent`].
469    ///
470    /// Active sessions are prioritized.
471    pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SessionEvent<N>> {
472        // Poll events from active sessions
473        match self.active_session_rx.poll_next_unpin(cx) {
474            Poll::Pending => {}
475            Poll::Ready(None) => {
476                unreachable!("Manager holds both channel halves.")
477            }
478            Poll::Ready(Some(event)) => {
479                return match event {
480                    ActiveSessionMessage::Disconnected { peer_id, remote_addr } => {
481                        trace!(
482                            target: "net::session",
483                            ?peer_id,
484                            "gracefully disconnected active session."
485                        );
486                        self.remove_active_session(&peer_id);
487                        Poll::Ready(SessionEvent::Disconnected { peer_id, remote_addr })
488                    }
489                    ActiveSessionMessage::ClosedOnConnectionError {
490                        peer_id,
491                        remote_addr,
492                        error,
493                    } => {
494                        trace!(target: "net::session", ?peer_id, %error,"closed session.");
495                        self.remove_active_session(&peer_id);
496                        Poll::Ready(SessionEvent::SessionClosedOnConnectionError {
497                            remote_addr,
498                            peer_id,
499                            error,
500                        })
501                    }
502                    ActiveSessionMessage::ValidMessage { peer_id, message } => {
503                        Poll::Ready(SessionEvent::ValidMessage { peer_id, message })
504                    }
505                    ActiveSessionMessage::BadMessage { peer_id } => {
506                        Poll::Ready(SessionEvent::BadMessage { peer_id })
507                    }
508                    ActiveSessionMessage::ProtocolBreach { peer_id } => {
509                        Poll::Ready(SessionEvent::ProtocolBreach { peer_id })
510                    }
511                }
512            }
513        }
514
515        // Poll the pending session event stream
516        let event = match self.pending_session_rx.poll_next_unpin(cx) {
517            Poll::Pending => return Poll::Pending,
518            Poll::Ready(None) => unreachable!("Manager holds both channel halves."),
519            Poll::Ready(Some(event)) => event,
520        };
521        match event {
522            PendingSessionEvent::Established {
523                session_id,
524                remote_addr,
525                local_addr,
526                peer_id,
527                capabilities,
528                mut conn,
529                status,
530                direction,
531                client_id,
532                peer_listen_port,
533            } => {
534                // move from pending to established.
535                self.remove_pending_session(&session_id);
536
537                // If there's already a session to the peer then we disconnect right away
538                if self.active_sessions.contains_key(&peer_id) {
539                    trace!(
540                        target: "net::session",
541                        ?session_id,
542                        ?remote_addr,
543                        ?peer_id,
544                        ?direction,
545                        "already connected"
546                    );
547
548                    self.spawn(async move {
549                        // send a disconnect message
550                        let _ =
551                            conn.into_inner().disconnect(DisconnectReason::AlreadyConnected).await;
552                    });
553
554                    return Poll::Ready(SessionEvent::AlreadyConnected {
555                        peer_id,
556                        remote_addr,
557                        direction,
558                    })
559                }
560
561                let (commands_tx, commands_rx) = mpsc::channel(self.session_command_buffer);
562                let (unbounded_tx, unbounded_rx) = mpsc::unbounded_channel();
563
564                let (to_session_tx, messages_rx) = mpsc::channel(self.session_command_buffer);
565
566                let messages = PeerRequestSender::new(peer_id, to_session_tx);
567
568                let timeout = Arc::new(AtomicU64::new(
569                    self.initial_internal_request_timeout.as_millis() as u64,
570                ));
571
572                // negotiated version
573                let version = conn.version();
574
575                // Configure the interval at which the range information is updated, starting with
576                // ETH69. We use interval_at to delay the first tick, avoiding sending
577                // BlockRangeUpdate immediately after connection (which can cause issues with
578                // peers that don't properly handle the message).
579                let range_update_interval = (conn.version() >= EthVersion::Eth69).then(|| {
580                    let start = tokio::time::Instant::now() + RANGE_UPDATE_INTERVAL;
581                    let mut interval = tokio::time::interval_at(start, RANGE_UPDATE_INTERVAL);
582                    interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
583                    interval
584                });
585
586                // Shared counter of in-flight broadcast items. The session task must decrement
587                // this when it pops messages from the outgoing queue, and the
588                // `SessionCommandSender` increments it before enqueuing. This invariant ensures
589                // the `SessionManager` always has an accurate view of total buffered broadcast
590                // pressure for a peer.
591                let broadcast_items = BroadcastItemCounter::new();
592                let remote_range_info = status.block_range_update().map(|update| {
593                    BlockRangeInfo::new(update.earliest, update.latest, update.latest_hash)
594                });
595
596                if self.reject_block_announcements {
597                    conn.set_reject_block_announcements(true);
598                }
599
600                let session = ActiveSession {
601                    next_id: 0,
602                    remote_peer_id: peer_id,
603                    remote_addr,
604                    remote_capabilities: Arc::clone(&capabilities),
605                    session_id,
606                    commands_rx: ReceiverStream::new(commands_rx),
607                    unbounded_rx,
608                    unbounded_broadcast_msgs: self.metrics.total_unbounded_broadcast_msgs.clone(),
609                    to_session_manager: self.active_session_tx.clone(),
610                    pending_message_to_session: None,
611                    internal_request_rx: ReceiverStream::new(messages_rx).fuse(),
612                    inflight_requests: Default::default(),
613                    conn,
614                    queued_outgoing: QueuedOutgoingMessages::new(
615                        self.metrics.queued_outgoing_messages.clone(),
616                        broadcast_items.clone(),
617                    ),
618                    received_requests_from_remote: Default::default(),
619                    internal_request_timeout_interval: request_timeout_interval(
620                        self.initial_internal_request_timeout,
621                    ),
622                    internal_request_timeout: Arc::clone(&timeout),
623                    protocol_breach_request_timeout: self.protocol_breach_request_timeout,
624                    terminate_message: None,
625                    range_info: remote_range_info.clone(),
626                    local_range_info: self.local_range_info.clone(),
627                    range_update_interval,
628                    last_sent_latest_block: None,
629                };
630
631                let supports_snap = session.conn.supports_snap();
632                self.spawn(session);
633
634                let client_version = client_id.into();
635                let handle = ActiveSessionHandle {
636                    status: status.clone(),
637                    direction,
638                    session_id,
639                    remote_id: peer_id,
640                    version,
641                    established: Instant::now(),
642                    capabilities: Arc::clone(&capabilities),
643                    commands: SessionCommandSender::new(commands_tx, unbounded_tx, broadcast_items),
644                    client_version: Arc::clone(&client_version),
645                    remote_addr,
646                    local_addr,
647                    peer_listen_port,
648                };
649
650                self.active_sessions.insert(peer_id, handle);
651                self.counter.inc_active(&direction);
652
653                if direction.is_outgoing() {
654                    self.metrics.total_dial_successes.increment(1);
655                }
656
657                Poll::Ready(SessionEvent::SessionEstablished {
658                    peer_id,
659                    remote_addr,
660                    client_version,
661                    version,
662                    capabilities,
663                    status,
664                    messages,
665                    direction,
666                    timeout,
667                    range_info: remote_range_info,
668                    supports_snap,
669                })
670            }
671            PendingSessionEvent::Disconnected { remote_addr, session_id, direction, error } => {
672                trace!(
673                    target: "net::session",
674                    ?session_id,
675                    ?remote_addr,
676                    ?error,
677                    "disconnected pending session"
678                );
679                self.remove_pending_session(&session_id);
680                match direction {
681                    Direction::Incoming => {
682                        Poll::Ready(SessionEvent::IncomingPendingSessionClosed {
683                            remote_addr,
684                            error,
685                        })
686                    }
687                    Direction::Outgoing(peer_id) => {
688                        Poll::Ready(SessionEvent::OutgoingPendingSessionClosed {
689                            remote_addr,
690                            peer_id,
691                            error,
692                        })
693                    }
694                }
695            }
696            PendingSessionEvent::OutgoingConnectionError {
697                remote_addr,
698                session_id,
699                peer_id,
700                error,
701            } => {
702                trace!(
703                    target: "net::session",
704                    %error,
705                    ?session_id,
706                    ?remote_addr,
707                    ?peer_id,
708                    "connection refused"
709                );
710                self.remove_pending_session(&session_id);
711                Poll::Ready(SessionEvent::OutgoingConnectionError { remote_addr, peer_id, error })
712            }
713            PendingSessionEvent::EciesAuthError { remote_addr, session_id, error, direction } => {
714                trace!(
715                    target: "net::session",
716                    %error,
717                    ?session_id,
718                    ?remote_addr,
719                    "ecies auth failed"
720                );
721                self.remove_pending_session(&session_id);
722                match direction {
723                    Direction::Incoming => {
724                        Poll::Ready(SessionEvent::IncomingPendingSessionClosed {
725                            remote_addr,
726                            error: Some(PendingSessionHandshakeError::Ecies(error)),
727                        })
728                    }
729                    Direction::Outgoing(peer_id) => {
730                        Poll::Ready(SessionEvent::OutgoingPendingSessionClosed {
731                            remote_addr,
732                            peer_id,
733                            error: Some(PendingSessionHandshakeError::Ecies(error)),
734                        })
735                    }
736                }
737            }
738        }
739    }
740
741    /// Updates the advertised block range that this node can serve to other peers starting with
742    /// Eth69.
743    ///
744    /// This method updates both the local status message that gets sent to peers during handshake
745    /// and the shared local range information that gets propagated to active sessions (Eth69).
746    /// The range information is used in ETH69 protocol where peers announce the range of blocks
747    /// they can serve to optimize data synchronization.
748    pub(crate) fn update_advertised_block_range(&mut self, block_range_update: BlockRangeUpdate) {
749        self.status.earliest_block = Some(block_range_update.earliest);
750        self.status.latest_block = Some(block_range_update.latest);
751        self.status.blockhash = block_range_update.latest_hash;
752
753        // Update the shared local range info that gets propagated to active sessions
754        self.local_range_info.update(
755            block_range_update.earliest,
756            block_range_update.latest,
757            block_range_update.latest_hash,
758        );
759    }
760}
761
762/// A counter for ongoing graceful disconnections attempts.
763#[derive(Default, Debug, Clone)]
764struct DisconnectionsCounter(Arc<()>);
765
766impl DisconnectionsCounter {
767    const MAX_CONCURRENT_GRACEFUL_DISCONNECTIONS: usize = 15;
768
769    /// Returns true if the [`DisconnectionsCounter`] still has capacity
770    /// for an additional graceful disconnection.
771    fn has_capacity(&self) -> bool {
772        Arc::strong_count(&self.0) <= Self::MAX_CONCURRENT_GRACEFUL_DISCONNECTIONS
773    }
774}
775
776/// Events produced by the [`SessionManager`]
777#[derive(Debug)]
778pub enum SessionEvent<N: NetworkPrimitives> {
779    /// A new session was successfully authenticated.
780    ///
781    /// This session is now able to exchange data.
782    SessionEstablished {
783        /// The remote node's public key
784        peer_id: PeerId,
785        /// The remote node's socket address
786        remote_addr: SocketAddr,
787        /// The user agent of the remote node, usually containing the client name and version
788        client_version: Arc<str>,
789        /// The capabilities the remote node has announced
790        capabilities: Arc<Capabilities>,
791        /// negotiated eth version
792        version: EthVersion,
793        /// The Status message the peer sent during the `eth` handshake
794        status: Arc<UnifiedStatus>,
795        /// The channel for sending messages to the peer with the session
796        messages: PeerRequestSender<PeerRequest<N>>,
797        /// The direction of the session, either `Inbound` or `Outgoing`
798        direction: Direction,
799        /// The maximum time that the session waits for a response from the peer before timing out
800        /// the connection
801        timeout: Arc<AtomicU64>,
802        /// The range info for the peer.
803        range_info: Option<BlockRangeInfo>,
804        /// Whether the connection negotiated `snap/2` and can serve [`PeerRequest::GetSnap`].
805        supports_snap: bool,
806    },
807    /// The peer was already connected with another session.
808    AlreadyConnected {
809        /// The remote node's public key
810        peer_id: PeerId,
811        /// The remote node's socket address
812        remote_addr: SocketAddr,
813        /// The direction of the session, either `Inbound` or `Outgoing`
814        direction: Direction,
815    },
816    /// A session received a valid message via `RLPx`.
817    ValidMessage {
818        /// The remote node's public key
819        peer_id: PeerId,
820        /// Message received from the peer.
821        message: PeerMessage<N>,
822    },
823    /// Received a bad message from the peer.
824    BadMessage {
825        /// Identifier of the remote peer.
826        peer_id: PeerId,
827    },
828    /// Remote peer is considered in protocol violation
829    ProtocolBreach {
830        /// Identifier of the remote peer.
831        peer_id: PeerId,
832    },
833    /// Closed an incoming pending session during handshaking.
834    IncomingPendingSessionClosed {
835        /// The remote node's socket address
836        remote_addr: SocketAddr,
837        /// The pending handshake session error that caused the session to close
838        error: Option<PendingSessionHandshakeError>,
839    },
840    /// Closed an outgoing pending session during handshaking.
841    OutgoingPendingSessionClosed {
842        /// The remote node's socket address
843        remote_addr: SocketAddr,
844        /// The remote node's public key
845        peer_id: PeerId,
846        /// The pending handshake session error that caused the session to close
847        error: Option<PendingSessionHandshakeError>,
848    },
849    /// Failed to establish a tcp stream
850    OutgoingConnectionError {
851        /// The remote node's socket address
852        remote_addr: SocketAddr,
853        /// The remote node's public key
854        peer_id: PeerId,
855        /// The error that caused the outgoing connection to fail
856        error: io::Error,
857    },
858    /// Session was closed due to an error
859    SessionClosedOnConnectionError {
860        /// The id of the remote peer.
861        peer_id: PeerId,
862        /// The socket we were connected to.
863        remote_addr: SocketAddr,
864        /// The error that caused the session to close
865        error: EthStreamError,
866    },
867    /// Active session was gracefully disconnected.
868    Disconnected {
869        /// The remote node's public key
870        peer_id: PeerId,
871        /// The remote node's socket address that we were connected to
872        remote_addr: SocketAddr,
873    },
874}
875
876/// Errors that can occur during handshaking/authenticating the underlying streams.
877#[derive(Debug, thiserror::Error)]
878pub enum PendingSessionHandshakeError {
879    /// The pending session failed due to an error while establishing the `eth` stream
880    #[error(transparent)]
881    Eth(EthStreamError),
882    /// The pending session failed due to an error while establishing the ECIES stream
883    #[error(transparent)]
884    Ecies(ECIESError),
885    /// Thrown when the authentication timed out
886    #[error("authentication timed out")]
887    Timeout,
888    /// Thrown when the remote lacks the required capability
889    #[error("Mandatory extra capability unsupported")]
890    UnsupportedExtraCapability,
891    /// Thrown when the node id in the remote's `Hello` message differs from the identity that the
892    /// ECIES handshake authenticated.
893    #[error("unexpected identity in hello message: {0}")]
894    UnexpectedHandshakeIdentity(GotExpectedBoxed<PeerId>),
895}
896
897impl PendingSessionHandshakeError {
898    /// Returns the [`DisconnectReason`] the session was disconnected with, either because the
899    /// remote sent one or because we sent one before failing the handshake.
900    pub const fn as_disconnected(&self) -> Option<DisconnectReason> {
901        match self {
902            Self::Eth(eth_err) => eth_err.as_disconnected(),
903            Self::UnexpectedHandshakeIdentity(_) => {
904                Some(DisconnectReason::UnexpectedHandshakeIdentity)
905            }
906            _ => None,
907        }
908    }
909}
910
911/// The error thrown when the max configured limit has been reached and no more connections are
912/// accepted.
913#[derive(Debug, Clone, thiserror::Error)]
914#[error("session limit reached {0}")]
915pub struct ExceedsSessionLimit(pub(crate) u32);
916
917/// Starts a pending session authentication with a timeout.
918pub(crate) async fn pending_session_with_timeout<F, N: NetworkPrimitives>(
919    timeout: Duration,
920    session_id: SessionId,
921    remote_addr: SocketAddr,
922    direction: Direction,
923    events: mpsc::Sender<PendingSessionEvent<N>>,
924    f: F,
925) where
926    F: Future<Output = ()>,
927{
928    if tokio::time::timeout(timeout, f).await.is_err() {
929        trace!(target: "net::session", ?remote_addr, ?direction, "pending session timed out");
930        let event = PendingSessionEvent::Disconnected {
931            remote_addr,
932            session_id,
933            direction,
934            error: Some(PendingSessionHandshakeError::Timeout),
935        };
936        let _ = events.send(event).await;
937    }
938}
939
940/// Starts the authentication process for a connection initiated by a remote peer.
941///
942/// This will wait for the _incoming_ handshake request and answer it.
943#[expect(clippy::too_many_arguments)]
944pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
945    handshake: Arc<dyn EthRlpxHandshake>,
946    eth_max_message_size: usize,
947    disconnect_rx: oneshot::Receiver<()>,
948    session_id: SessionId,
949    stream: TcpStream,
950    events: mpsc::Sender<PendingSessionEvent<N>>,
951    remote_addr: SocketAddr,
952    secret_key: SecretKey,
953    hello: HelloMessageWithProtocols,
954    status: UnifiedStatus,
955    fork_filter: ForkFilter,
956    extra_handlers: RlpxSubProtocolHandlers,
957) {
958    authenticate(
959        handshake,
960        eth_max_message_size,
961        disconnect_rx,
962        events,
963        stream,
964        session_id,
965        remote_addr,
966        secret_key,
967        Direction::Incoming,
968        hello,
969        status,
970        fork_filter,
971        extra_handlers,
972    )
973    .await
974}
975
976/// Starts the authentication process for a connection initiated by a remote peer.
977#[instrument(level = "trace", target = "net::network", skip_all, fields(%remote_addr, peer_id = ?remote_peer_id))]
978#[expect(clippy::too_many_arguments)]
979async fn start_pending_outbound_session<N: NetworkPrimitives>(
980    handshake: Arc<dyn EthRlpxHandshake>,
981    eth_max_message_size: usize,
982    disconnect_rx: oneshot::Receiver<()>,
983    events: mpsc::Sender<PendingSessionEvent<N>>,
984    session_id: SessionId,
985    remote_addr: SocketAddr,
986    remote_peer_id: PeerId,
987    secret_key: SecretKey,
988    hello: HelloMessageWithProtocols,
989    status: UnifiedStatus,
990    fork_filter: ForkFilter,
991    extra_handlers: RlpxSubProtocolHandlers,
992) {
993    let stream = match TcpStream::connect(remote_addr).await {
994        Ok(stream) => {
995            if let Err(err) = stream.set_nodelay(true) {
996                tracing::warn!(target: "net::session", "set nodelay failed: {:?}", err);
997            }
998            stream
999        }
1000        Err(error) => {
1001            let _ = events
1002                .send(PendingSessionEvent::OutgoingConnectionError {
1003                    remote_addr,
1004                    session_id,
1005                    peer_id: remote_peer_id,
1006                    error,
1007                })
1008                .await;
1009            return
1010        }
1011    };
1012    authenticate(
1013        handshake,
1014        eth_max_message_size,
1015        disconnect_rx,
1016        events,
1017        stream,
1018        session_id,
1019        remote_addr,
1020        secret_key,
1021        Direction::Outgoing(remote_peer_id),
1022        hello,
1023        status,
1024        fork_filter,
1025        extra_handlers,
1026    )
1027    .await
1028}
1029
1030/// Authenticates a session
1031#[expect(clippy::too_many_arguments)]
1032async fn authenticate<N: NetworkPrimitives>(
1033    handshake: Arc<dyn EthRlpxHandshake>,
1034    eth_max_message_size: usize,
1035    disconnect_rx: oneshot::Receiver<()>,
1036    events: mpsc::Sender<PendingSessionEvent<N>>,
1037    stream: TcpStream,
1038    session_id: SessionId,
1039    remote_addr: SocketAddr,
1040    secret_key: SecretKey,
1041    direction: Direction,
1042    hello: HelloMessageWithProtocols,
1043    status: UnifiedStatus,
1044    fork_filter: ForkFilter,
1045    extra_handlers: RlpxSubProtocolHandlers,
1046) {
1047    let local_addr = stream.local_addr().ok();
1048    let stream = match get_ecies_stream(stream, secret_key, direction).await {
1049        Ok(stream) => stream,
1050        Err(error) => {
1051            let _ = events
1052                .send(PendingSessionEvent::EciesAuthError {
1053                    remote_addr,
1054                    session_id,
1055                    error,
1056                    direction,
1057                })
1058                .await;
1059            return
1060        }
1061    };
1062
1063    let unauthed = UnauthedP2PStream::new(stream);
1064
1065    let auth = authenticate_stream(
1066        handshake,
1067        eth_max_message_size,
1068        unauthed,
1069        session_id,
1070        remote_addr,
1071        local_addr,
1072        direction,
1073        hello,
1074        status,
1075        fork_filter,
1076        extra_handlers,
1077    )
1078    .boxed();
1079
1080    match futures::future::select(disconnect_rx, auth).await {
1081        Either::Left((_, _)) => {
1082            let _ = events
1083                .send(PendingSessionEvent::Disconnected {
1084                    remote_addr,
1085                    session_id,
1086                    direction,
1087                    error: None,
1088                })
1089                .await;
1090        }
1091        Either::Right((res, _)) => {
1092            let _ = events.send(res).await;
1093        }
1094    }
1095}
1096
1097/// Returns an [`ECIESStream`] if it can be built. If not, send a
1098/// [`PendingSessionEvent::EciesAuthError`] and returns `None`
1099async fn get_ecies_stream<Io: AsyncRead + AsyncWrite + Unpin>(
1100    stream: Io,
1101    secret_key: SecretKey,
1102    direction: Direction,
1103) -> Result<ECIESStream<Io>, ECIESError> {
1104    match direction {
1105        Direction::Incoming => ECIESStream::incoming(stream, secret_key).await,
1106        Direction::Outgoing(remote_peer_id) => {
1107            ECIESStream::connect(stream, secret_key, remote_peer_id).await
1108        }
1109    }
1110}
1111
1112/// Authenticate the stream via handshake
1113///
1114/// On Success return the authenticated stream as [`PendingSessionEvent`].
1115///
1116/// If additional [`RlpxSubProtocolHandlers`] are provided, the hello message will be updated to
1117/// also negotiate the additional protocols.
1118#[expect(clippy::too_many_arguments)]
1119async fn authenticate_stream<N: NetworkPrimitives>(
1120    handshake: Arc<dyn EthRlpxHandshake>,
1121    eth_max_message_size: usize,
1122    stream: UnauthedP2PStream<ECIESStream<TcpStream>>,
1123    session_id: SessionId,
1124    remote_addr: SocketAddr,
1125    local_addr: Option<SocketAddr>,
1126    direction: Direction,
1127    mut hello: HelloMessageWithProtocols,
1128    mut status: UnifiedStatus,
1129    fork_filter: ForkFilter,
1130    mut extra_handlers: RlpxSubProtocolHandlers,
1131) -> PendingSessionEvent<N> {
1132    // Add extra protocols to the hello message
1133    extra_handlers.retain(|handler| hello.try_add_protocol(handler.protocol()).is_ok());
1134
1135    let authenticated_peer_id = stream.inner().remote_id();
1136
1137    // conduct the p2p rlpx handshake and return the rlpx authenticated stream
1138    let (mut p2p_stream, their_hello) = match stream.handshake(hello).await {
1139        Ok(stream_res) => stream_res,
1140        Err(err) => {
1141            return PendingSessionEvent::Disconnected {
1142                remote_addr,
1143                session_id,
1144                direction,
1145                error: Some(PendingSessionHandshakeError::Eth(err.into())),
1146            }
1147        }
1148    };
1149
1150    // The ECIES handshake proved possession of this key, so it is the only trustworthy identity of
1151    // the connection. Everything the peer states in `Hello` is unauthenticated.
1152    //
1153    // Bind the session to the authenticated identity before anything else observes the peer. A
1154    // peer that announces a different node id could otherwise act on behalf of that node, for
1155    // example by making an extra protocol report reputation changes against it.
1156    if their_hello.id != authenticated_peer_id {
1157        let _ = p2p_stream.disconnect(DisconnectReason::UnexpectedHandshakeIdentity).await;
1158
1159        return PendingSessionEvent::Disconnected {
1160            remote_addr,
1161            session_id,
1162            direction,
1163            error: Some(PendingSessionHandshakeError::UnexpectedHandshakeIdentity(
1164                GotExpected { got: their_hello.id, expected: authenticated_peer_id }.into(),
1165            )),
1166        }
1167    }
1168
1169    // if we have extra handlers, check if it must be supported by the remote
1170    if !extra_handlers.is_empty() {
1171        // ensure that no extra handlers that aren't supported are not mandatory
1172        while let Some(pos) = extra_handlers.iter().position(|handler| {
1173            p2p_stream
1174                .shared_capabilities()
1175                .ensure_matching_capability(&handler.protocol().cap)
1176                .is_err()
1177        }) {
1178            let handler = extra_handlers.remove(pos);
1179            if handler.on_unsupported_by_peer(
1180                p2p_stream.shared_capabilities(),
1181                direction,
1182                authenticated_peer_id,
1183            ) == OnNotSupported::Disconnect
1184            {
1185                return PendingSessionEvent::Disconnected {
1186                    remote_addr,
1187                    session_id,
1188                    direction,
1189                    error: Some(PendingSessionHandshakeError::UnsupportedExtraCapability),
1190                };
1191            }
1192        }
1193    }
1194
1195    // Ensure we negotiated mandatory eth protocol
1196    let eth_version = match p2p_stream.shared_capabilities().eth_version() {
1197        Ok(version) => version,
1198        Err(err) => {
1199            return PendingSessionEvent::Disconnected {
1200                remote_addr,
1201                session_id,
1202                direction,
1203                error: Some(PendingSessionHandshakeError::Eth(err.into())),
1204            }
1205        }
1206    };
1207
1208    // Before trying status handshake, set up the version to negotiated shared version
1209    status.set_eth_version(eth_version);
1210
1211    let (conn, their_status) = if p2p_stream.shared_capabilities().len() == 1 {
1212        // if the shared caps are 1, we know both support the eth version
1213        // if the hello handshake was successful we can try status handshake
1214
1215        // perform the eth protocol handshake
1216        match handshake
1217            .handshake(&mut p2p_stream, status, fork_filter.clone(), HANDSHAKE_TIMEOUT)
1218            .await
1219        {
1220            Ok(their_status) => {
1221                let eth_stream =
1222                    EthStream::with_max_message_size(eth_version, p2p_stream, eth_max_message_size);
1223                (eth_stream.into(), their_status)
1224            }
1225            Err(err) => {
1226                return PendingSessionEvent::Disconnected {
1227                    remote_addr,
1228                    session_id,
1229                    direction,
1230                    error: Some(PendingSessionHandshakeError::Eth(err)),
1231                }
1232            }
1233        }
1234    } else if p2p_stream.shared_capabilities().is_exact_eth_snap_v2() {
1235        // Exactly `eth` + `snap/2` (no other extras): use the dedicated stream instead of the
1236        // general-purpose satellite multiplexer. If `snap/2` is negotiated alongside other extra
1237        // capabilities, fall through to the satellite path — the dedicated stream only composes
1238        // `eth` and `snap/2`.
1239        match EthSnapStream::handshake(
1240            p2p_stream,
1241            status,
1242            fork_filter,
1243            handshake,
1244            eth_max_message_size,
1245        )
1246        .await
1247        {
1248            Ok((stream, their_status)) => (stream.into(), their_status),
1249            Err(err) => {
1250                return PendingSessionEvent::Disconnected {
1251                    remote_addr,
1252                    session_id,
1253                    direction,
1254                    error: Some(PendingSessionHandshakeError::Eth(err)),
1255                }
1256            }
1257        }
1258    } else {
1259        // Multiplex the stream with the extra protocols
1260        let mut multiplex_stream = RlpxProtocolMultiplexer::new(p2p_stream);
1261
1262        // install additional handlers
1263        for handler in extra_handlers.into_iter() {
1264            let protocol = handler.protocol();
1265            let limits = handler.inbound_limits();
1266            let remote_peer_id = authenticated_peer_id;
1267
1268            // The unsupported-handler pass above guarantees that every remaining handler has a
1269            // matching negotiated capability. The multiplexer retains the same immutable set of
1270            // shared capabilities, so installing one of these handlers cannot fail.
1271            multiplex_stream
1272                .install_protocol_with_limits(&protocol.cap, limits, move |conn| {
1273                    handler.into_connection(direction, remote_peer_id, conn)
1274                })
1275                .expect("remaining handler capability was negotiated");
1276        }
1277
1278        let (multiplex_stream, their_status) = match multiplex_stream
1279            .into_eth_satellite_stream(status, fork_filter, handshake, eth_max_message_size)
1280            .await
1281        {
1282            Ok((multiplex_stream, their_status)) => (multiplex_stream, their_status),
1283            Err(err) => {
1284                return PendingSessionEvent::Disconnected {
1285                    remote_addr,
1286                    session_id,
1287                    direction,
1288                    error: Some(PendingSessionHandshakeError::Eth(err)),
1289                }
1290            }
1291        };
1292
1293        (multiplex_stream.into(), their_status)
1294    };
1295
1296    // `port` field is effectively deprecated, so we treat 0 value as a missing port.
1297    let peer_listen_port = (their_hello.port != 0).then_some(their_hello.port);
1298
1299    PendingSessionEvent::Established {
1300        session_id,
1301        remote_addr,
1302        local_addr,
1303        peer_id: authenticated_peer_id,
1304        capabilities: Arc::new(Capabilities::from(their_hello.capabilities)),
1305        status: Arc::new(their_status),
1306        conn,
1307        direction,
1308        client_id: their_hello.client_version,
1309        peer_listen_port,
1310    }
1311}