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