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}