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