Skip to main content

snarkos_node_tcp/helpers/
connections.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Objects associated with connection handling.
17
18use std::{
19    collections::HashMap,
20    net::{IpAddr, SocketAddr},
21    ops::Not,
22    sync::{Arc, atomic::AtomicBool},
23    time::Instant,
24};
25
26#[cfg(feature = "locktick")]
27use locktick::parking_lot::RwLock;
28#[cfg(not(feature = "locktick"))]
29use parking_lot::RwLock;
30use tokio::{
31    io::{AsyncRead, AsyncWrite},
32    net::TcpStream,
33    sync::oneshot,
34    task::JoinHandle,
35};
36use tracing::*;
37
38use crate::Stats;
39#[cfg(doc)]
40use crate::{
41    Tcp,
42    protocols::{Disconnect, Handshake, OnConnect, Reading, Writing},
43};
44
45/// A map of all currently connected addresses to their associated connection.
46#[derive(Default)]
47pub(crate) struct Connections(pub(crate) RwLock<HashMap<SocketAddr, Connection>>);
48
49impl Connections {
50    /// Adds the given connection to the list of active connections.
51    pub(crate) fn add(&self, conn: Connection) {
52        self.0.write().insert(conn.addr, conn);
53    }
54
55    /// Returns `true` if the given address is connected.
56    pub(crate) fn is_connected(&self, addr: SocketAddr) -> bool {
57        self.0.read().contains_key(&addr)
58    }
59
60    /// Removes the connection associated with the given address.
61    pub(crate) fn remove(&self, addr: SocketAddr) -> Option<Connection> {
62        self.0.write().remove(&addr)
63    }
64
65    /// Returns the number of connected addresses.
66    pub(crate) fn num_connected(&self) -> usize {
67        self.0.read().len()
68    }
69
70    /// Returns the list of connected addresses.
71    pub(crate) fn addrs(&self) -> Vec<SocketAddr> {
72        self.0.read().keys().copied().collect()
73    }
74
75    /// Returns the stats of the connection with the given address, if it is still active.
76    pub(crate) fn stats(&self, addr: SocketAddr) -> Option<Arc<Stats>> {
77        self.0.read().get(&addr).map(|conn| Arc::clone(conn.stats()))
78    }
79
80    /// Returns the stats of every active connection.
81    pub(crate) fn stats_snapshot(&self) -> HashMap<SocketAddr, Arc<Stats>> {
82        self.0.read().iter().map(|(addr, conn)| (*addr, Arc::clone(conn.stats()))).collect()
83    }
84
85    /// Returns the number of active connections charged to the given address' IP.
86    ///
87    /// note: This is a scan rather than a lookup, as connections are keyed by their full address;
88    /// it is only performed once per connection setup, and is bounded by `Config::max_connections`.
89    pub(crate) fn num_with_ip(&self, addr: SocketAddr) -> usize {
90        let ip = canonical_ip(addr);
91        self.0.read().keys().filter(|addr| canonical_ip(**addr) == ip).count()
92    }
93}
94
95/// The IP address a connection is charged to, for the purposes of per-IP limits.
96///
97/// Canonicalizing is what keeps the two spellings of an IPv4 host - the native form and the
98/// IPv4-mapped IPv6 form a dual-stack listener reports - in a single bucket. Without it, a peer
99/// reaching a node bound to `::` could claim the per-IP allowance twice over by alternating
100/// between them. Deriving the key here, rather than accepting one from the caller, is what
101/// guarantees every comparison uses the same bucket.
102pub(crate) const fn canonical_ip(addr: SocketAddr) -> IpAddr {
103    addr.ip().to_canonical()
104}
105
106/// A helper trait to facilitate trait-objectification of connection readers.
107pub(crate) trait AR: AsyncRead + Unpin + Send + Sync {}
108impl<T: AsyncRead + Unpin + Send + Sync> AR for T {}
109
110/// A helper trait to facilitate trait-objectification of connection writers.
111pub(crate) trait AW: AsyncWrite + Unpin + Send + Sync {}
112impl<T: AsyncWrite + Unpin + Send + Sync> AW for T {}
113
114/// Created for each active connection; used by the protocols to obtain a handle for
115/// reading and writing, and keeps track of tasks that have been spawned for the connection.
116pub struct Connection {
117    /// The address of the connection.
118    addr: SocketAddr,
119    /// The connection's side in relation to Tcp.
120    side: ConnectionSide,
121    /// Statistics covering this connection alone, for as long as it is active.
122    stats: Arc<Stats>,
123    /// Available and used only in the [`Handshake`] protocol.
124    pub(crate) stream: Option<TcpStream>,
125    /// Available and used only in the [`Reading`] protocol.
126    pub(crate) reader: Option<Box<dyn AR>>,
127    /// Available and used only in the [`Writing`] protocol.
128    pub(crate) writer: Option<Box<dyn AW>>,
129    /// Used to notify the [`Reading`] protocol that the connection is fully ready.
130    pub(crate) readiness_notifier: Option<oneshot::Sender<()>>,
131    /// Prevents the OnDisconnect hook from being triggered multiple times.
132    pub(crate) disconnecting: AtomicBool,
133    /// Handles to tasks spawned for the connection.
134    pub(crate) tasks: Vec<JoinHandle<()>>,
135    /// The tracing span.
136    pub(crate) span: Span,
137}
138
139impl Connection {
140    /// Creates a [`Connection`] with placeholders for protocol-related objects.
141    pub(crate) fn new(addr: SocketAddr, stream: TcpStream, side: ConnectionSide, span: Span) -> Self {
142        Self {
143            addr,
144            stream: Some(stream),
145            reader: None,
146            writer: None,
147            readiness_notifier: None,
148            disconnecting: Default::default(),
149            side,
150            stats: Arc::new(Stats::new(Instant::now())),
151            tasks: Default::default(),
152            span,
153        }
154    }
155
156    /// Returns the address associated with the connection.
157    pub fn addr(&self) -> SocketAddr {
158        self.addr
159    }
160
161    /// Returns the statistics of this connection.
162    #[inline]
163    pub fn stats(&self) -> &Arc<Stats> {
164        &self.stats
165    }
166
167    /// Returns `ConnectionSide::Initiator` if the associated peer initiated the connection
168    /// and `ConnectionSide::Responder` if the connection request was initiated by Tcp.
169    pub fn side(&self) -> ConnectionSide {
170        self.side
171    }
172
173    /// Returns the tracing [`Span`] associated with the connection.
174    #[inline]
175    pub const fn span(&self) -> &Span {
176        &self.span
177    }
178}
179
180/// Indicates who was the initiator and who was the responder when the connection was established.
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182pub enum ConnectionSide {
183    /// The side that initiated the connection.
184    Initiator,
185    /// The side that accepted the connection.
186    Responder,
187}
188
189impl Not for ConnectionSide {
190    type Output = Self;
191
192    fn not(self) -> Self::Output {
193        match self {
194            Self::Initiator => Self::Responder,
195            Self::Responder => Self::Initiator,
196        }
197    }
198}
199
200impl Drop for Connection {
201    fn drop(&mut self) {
202        for task in self.tasks.iter().rev() {
203            task.abort();
204        }
205    }
206}
207
208pub(crate) fn create_connection_span(addr: SocketAddr, parent: &Span) -> Span {
209    macro_rules! try_span {
210        ($lvl:expr) => {
211            let s = span!(parent: parent, $lvl, "conn", addr = %addr);
212            if !s.is_disabled() {
213                return s;
214            }
215        };
216    }
217    try_span!(Level::TRACE);
218    try_span!(Level::DEBUG);
219    try_span!(Level::INFO);
220    try_span!(Level::WARN);
221    error_span!(parent: parent, "conn", addr = %addr)
222}
223
224/// Describes what triggered a disconnect, as delivered to [`Disconnect::handle_disconnect`].
225///
226/// note: Handshake failures do not appear here. A failed handshake prevents the connection
227/// from ever being registered, so there is no connection to disconnect.
228///
229/// note: When several events would race to trigger a disconnect on the same connection,
230/// only the first to claim it is delivered to [`Disconnect::handle_disconnect`]; subsequent
231/// claims are silently dropped.
232#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
233pub enum DisconnectOrigin {
234    /// The [`OnConnect`] task terminated abnormally before defusing its connection cleanup.
235    /// In practice this almost always means the user's [`OnConnect::on_connect`] implementation
236    /// panicked, and the disconnect is a side effect of that panic unwinding past the cleanup
237    /// guard.
238    OnConnectAbort,
239    /// The reader task for this connection terminated. Typical causes are the peer closing
240    /// its end of the socket, a decode error from the user-supplied [`Reading::Codec`], or
241    /// no message arriving within [`Reading::IDLE_TIMEOUT_MS`]. Often (but not always)
242    /// indicates a peer-side issue.
243    Reading,
244    /// The disconnect was initiated by [`Tcp::shut_down`], which tears down every active
245    /// connection as part of stopping the node. Unlike [`DisconnectOrigin::User`], this
246    /// signals that the entire node is going away - reconnection is not meaningful.
247    Shutdown,
248    /// The disconnect was explicitly requested via [`Tcp::disconnect`]. This is the only
249    /// origin produced directly by user code; the others all reflect events the library
250    /// detected internally.
251    User,
252    /// The writer task for this connection terminated. Typical causes are a [`Writing::TIMEOUT_MS`]
253    /// timeout while flushing, an underlying socket write error, or the message channel being
254    /// closed. Often correlates with the peer disappearing, but can also reflect local-side
255    /// pipeline problems (slow consumer, broken pipe).
256    Writing,
257}