Skip to main content

zakura_network/
peer_registry.rs

1//! Active peer connection metadata for local diagnostics.
2
3use std::{
4    collections::HashMap,
5    sync::{Arc, Mutex},
6    time::{Duration, Instant},
7};
8
9use crate::{
10    peer::ConnectionInfo,
11    protocol::external::types::Version,
12    zakura::{ZakuraConnId, ZakuraPeerId},
13    PeerSocketAddr,
14};
15
16/// A snapshot of one active peer connection.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ConnectedPeer {
19    /// The remote socket address for this direct connection.
20    pub addr: PeerSocketAddr,
21    /// The sanitized user agent advertised during the handshake.
22    pub user_agent: Arc<str>,
23    /// The protocol version advertised by the peer.
24    pub version: Version,
25    /// Whether the remote peer initiated the connection.
26    pub is_inbound: bool,
27    /// The latest heartbeat round-trip time.
28    pub rtt: Option<Duration>,
29    /// The latest heartbeat send time.
30    pub ping_sent_at: Option<Instant>,
31}
32
33impl ConnectedPeer {
34    /// Build a registry entry from a completed legacy handshake.
35    pub(crate) fn from_connection_info(connection_info: &ConnectionInfo) -> Option<Self> {
36        Some(Self {
37            addr: connection_info.connected_addr.diagnostic_remote_addr()?,
38            user_agent: sanitize_subversion(&connection_info.remote.user_agent).into(),
39            version: connection_info.remote.version,
40            is_inbound: connection_info.connected_addr.is_inbound(),
41            rtt: None,
42            ping_sent_at: None,
43        })
44    }
45}
46
47/// Shared registry for active legacy and Zakura peer connections.
48#[derive(Clone, Debug, Default)]
49pub(crate) struct PeerRegistry {
50    inner: Arc<Mutex<RegistryState>>,
51}
52
53#[derive(Debug, Default)]
54struct RegistryState {
55    next_legacy_generation: u64,
56    active_connections: HashMap<ConnectionKey, ConnectedPeer>,
57    native_conn_ids: HashMap<ZakuraPeerId, ZakuraConnId>,
58    retained_native_metadata: HashMap<ZakuraPeerId, ConnectedPeer>,
59}
60
61#[derive(Clone, Debug, Eq, Hash, PartialEq)]
62enum ConnectionKey {
63    Legacy(u64),
64    Native(ZakuraPeerId, ZakuraConnId),
65}
66
67impl PeerRegistry {
68    /// Return a point-in-time snapshot of active connections.
69    pub(crate) fn connected_peers(&self) -> Vec<ConnectedPeer> {
70        let mut connected_peers: Vec<_> = self
71            .inner
72            .lock()
73            .expect("peer registry mutex is never poisoned")
74            .active_connections
75            .values()
76            .cloned()
77            .collect();
78        connected_peers.sort_by(|left, right| {
79            left.addr
80                .cmp(&right.addr)
81                .then(left.is_inbound.cmp(&right.is_inbound))
82                .then(left.version.0.cmp(&right.version.0))
83                .then(left.user_agent.cmp(&right.user_agent))
84        });
85        connected_peers
86    }
87
88    /// Register one legacy connection until the returned guard drops.
89    pub(crate) fn register_legacy(
90        &self,
91        connected_peer: ConnectedPeer,
92    ) -> (PeerRegistryGuard, PeerRegistryUpdater) {
93        let mut state = self
94            .inner
95            .lock()
96            .expect("peer registry mutex is never poisoned");
97        let generation = state.next_legacy_generation;
98        state.next_legacy_generation = state
99            .next_legacy_generation
100            .checked_add(1)
101            .expect("legacy connection generation cannot overflow in one process");
102        let key = ConnectionKey::Legacy(generation);
103        state.active_connections.insert(key.clone(), connected_peer);
104        drop(state);
105
106        (
107            PeerRegistryGuard {
108                registry: self.clone(),
109                key: key.clone(),
110            },
111            PeerRegistryUpdater {
112                registry: self.clone(),
113                key,
114            },
115        )
116    }
117
118    /// Attach legacy handshake metadata to one authenticated native connection.
119    ///
120    /// Returns `false` without changing active or retained metadata when another
121    /// `conn_id` currently owns `peer_id`.
122    #[must_use]
123    pub(crate) fn attach_native_metadata(
124        &self,
125        peer_id: ZakuraPeerId,
126        conn_id: ZakuraConnId,
127        connection_metadata: ConnectedPeer,
128        retain_for_redial: bool,
129    ) -> bool {
130        let mut state = self
131            .inner
132            .lock()
133            .expect("peer registry mutex is never poisoned");
134        if state.native_conn_ids.get(&peer_id) != Some(&conn_id) {
135            return false;
136        }
137        if retain_for_redial {
138            state
139                .retained_native_metadata
140                .insert(peer_id.clone(), connection_metadata.clone());
141        }
142        state
143            .active_connections
144            .insert(ConnectionKey::Native(peer_id, conn_id), connection_metadata);
145        true
146    }
147
148    /// Register one authenticated native `conn_id`.
149    ///
150    /// Retained outbound metadata follows a reconnect to its new `conn_id`.
151    pub(crate) fn register_native_connection(&self, peer_id: ZakuraPeerId, conn_id: ZakuraConnId) {
152        let mut state = self
153            .inner
154            .lock()
155            .expect("peer registry mutex is never poisoned");
156        if let Some(previous) = state.native_conn_ids.insert(peer_id.clone(), conn_id) {
157            state
158                .active_connections
159                .remove(&ConnectionKey::Native(peer_id.clone(), previous));
160        }
161        if let Some(connection_metadata) = state.retained_native_metadata.get(&peer_id).cloned() {
162            state
163                .active_connections
164                .insert(ConnectionKey::Native(peer_id, conn_id), connection_metadata);
165        }
166    }
167
168    /// Remove one exact authenticated native `conn_id`.
169    pub(crate) fn deregister_native_connection(
170        &self,
171        peer_id: &ZakuraPeerId,
172        conn_id: ZakuraConnId,
173    ) {
174        let mut state = self
175            .inner
176            .lock()
177            .expect("peer registry mutex is never poisoned");
178        if state.native_conn_ids.get(peer_id) == Some(&conn_id) {
179            state.native_conn_ids.remove(peer_id);
180            state
181                .active_connections
182                .remove(&ConnectionKey::Native(peer_id.clone(), conn_id));
183        }
184    }
185
186    /// Forget retained metadata when the maintained native upgrade dial ends.
187    pub(crate) fn forget_retained_native_metadata(&self, peer_id: &ZakuraPeerId) {
188        self.inner
189            .lock()
190            .expect("peer registry mutex is never poisoned")
191            .retained_native_metadata
192            .remove(peer_id);
193    }
194
195    fn update_connection(&self, key: &ConnectionKey, update: impl FnOnce(&mut ConnectedPeer)) {
196        if let Some(connected_peer) = self
197            .inner
198            .lock()
199            .expect("peer registry mutex is never poisoned")
200            .active_connections
201            .get_mut(key)
202        {
203            update(connected_peer);
204        }
205    }
206
207    fn remove_connection(&self, key: &ConnectionKey) {
208        self.inner
209            .lock()
210            .expect("peer registry mutex is never poisoned")
211            .active_connections
212            .remove(key);
213    }
214}
215
216/// Updates heartbeat metrics for one exact connection generation.
217#[derive(Clone, Debug)]
218pub(crate) struct PeerRegistryUpdater {
219    registry: PeerRegistry,
220    key: ConnectionKey,
221}
222
223impl PeerRegistryUpdater {
224    pub(crate) fn record_ping_sent(&self, now: Instant) {
225        self.registry
226            .update_connection(&self.key, |connected_peer| {
227                connected_peer.ping_sent_at = Some(now);
228            });
229    }
230
231    pub(crate) fn record_response(&self, rtt: Duration) {
232        self.registry
233            .update_connection(&self.key, |connected_peer| {
234                connected_peer.rtt = Some(rtt);
235                connected_peer.ping_sent_at = None;
236            });
237    }
238}
239
240/// Removes one exact legacy connection generation on drop.
241#[derive(Debug)]
242pub(crate) struct PeerRegistryGuard {
243    registry: PeerRegistry,
244    key: ConnectionKey,
245}
246
247impl Drop for PeerRegistryGuard {
248    fn drop(&mut self) {
249        self.registry.remove_connection(&self.key);
250    }
251}
252
253fn sanitize_subversion(user_agent: &str) -> String {
254    user_agent
255        .chars()
256        .filter(|character| {
257            character.is_ascii_alphanumeric() || matches!(character, ' ' | '.' | '-' | '/' | ':')
258        })
259        .collect()
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    fn peer(port: u16) -> ConnectedPeer {
267        ConnectedPeer {
268            addr: format!("127.0.0.1:{port}")
269                .parse()
270                .expect("test address parses"),
271            user_agent: Arc::from("/Zakura:1.0.0/"),
272            version: Version(1),
273            is_inbound: false,
274            rtt: None,
275            ping_sent_at: None,
276        }
277    }
278
279    fn peer_id(byte: u8) -> ZakuraPeerId {
280        ZakuraPeerId::new(vec![byte; 32]).expect("test peer id is valid")
281    }
282
283    #[test]
284    fn legacy_guard_removes_only_its_generation() {
285        let registry = PeerRegistry::default();
286        let (first, _) = registry.register_legacy(peer(1));
287        let (_second, _) = registry.register_legacy(peer(2));
288
289        drop(first);
290
291        assert_eq!(registry.connected_peers(), vec![peer(2)]);
292    }
293
294    #[test]
295    fn native_replacement_ignores_stale_disconnect() {
296        let registry = PeerRegistry::default();
297        let peer_id = peer_id(1);
298        registry.register_native_connection(peer_id.clone(), 1);
299        assert!(registry.attach_native_metadata(peer_id.clone(), 1, peer(1), true));
300        registry.register_native_connection(peer_id.clone(), 2);
301
302        registry.deregister_native_connection(&peer_id, 1);
303        assert_eq!(registry.connected_peers(), vec![peer(1)]);
304
305        registry.deregister_native_connection(&peer_id, 2);
306        assert!(registry.connected_peers().is_empty());
307    }
308
309    #[test]
310    fn native_metadata_can_arrive_after_registration() {
311        let registry = PeerRegistry::default();
312        let peer_id = peer_id(2);
313        registry.register_native_connection(peer_id.clone(), 1);
314        assert!(registry.connected_peers().is_empty());
315
316        assert!(registry.attach_native_metadata(peer_id, 1, peer(1), true));
317        assert_eq!(registry.connected_peers(), vec![peer(1)]);
318    }
319
320    #[test]
321    fn stale_native_metadata_cannot_replace_active_or_retained_metadata() {
322        let registry = PeerRegistry::default();
323        let peer_id = peer_id(3);
324        registry.register_native_connection(peer_id.clone(), 1);
325        assert!(registry.attach_native_metadata(peer_id.clone(), 1, peer(1), true));
326        registry.register_native_connection(peer_id.clone(), 2);
327
328        assert!(!registry.attach_native_metadata(peer_id.clone(), 1, peer(2), true));
329        assert_eq!(registry.connected_peers(), vec![peer(1)]);
330
331        registry.deregister_native_connection(&peer_id, 2);
332        registry.register_native_connection(peer_id, 3);
333        assert_eq!(registry.connected_peers(), vec![peer(1)]);
334    }
335
336    #[test]
337    fn transient_native_metadata_does_not_survive_reconnect() {
338        let registry = PeerRegistry::default();
339        let peer_id = peer_id(4);
340        registry.register_native_connection(peer_id.clone(), 1);
341        assert!(registry.attach_native_metadata(peer_id.clone(), 1, peer(1), false));
342        assert_eq!(registry.connected_peers(), vec![peer(1)]);
343
344        registry.deregister_native_connection(&peer_id, 1);
345        registry.register_native_connection(peer_id, 2);
346        assert!(registry.connected_peers().is_empty());
347    }
348
349    #[test]
350    fn heartbeat_updates_do_not_revive_dropped_generation() {
351        let registry = PeerRegistry::default();
352        let (guard, updater) = registry.register_legacy(peer(1));
353        updater.record_ping_sent(Instant::now());
354        updater.record_response(Duration::from_millis(5));
355        assert_eq!(
356            registry.connected_peers()[0].rtt,
357            Some(Duration::from_millis(5)),
358        );
359
360        drop(guard);
361        updater.record_response(Duration::from_millis(10));
362        assert!(registry.connected_peers().is_empty());
363    }
364
365    #[test]
366    fn subversion_sanitizer_matches_zcashd_allowed_characters() {
367        assert_eq!(
368            sanitize_subversion("/Magic Bean:2.1.1-1/\u{1b}[31m\n"),
369            "/Magic Bean:2.1.1-1/31m"
370        );
371    }
372}