Skip to main content

ipfrs_network/
connection_manager.rs

1//! Connection management with limits and pruning
2//!
3//! This module implements intelligent connection management:
4//! - Connection limits (total, inbound, outbound)
5//! - Priority-based connection scoring
6//! - Automatic pruning of low-value connections
7//! - Reserved slots for important peers
8
9use libp2p::PeerId;
10use parking_lot::RwLock;
11use serde::Serialize;
12use std::collections::{HashMap, HashSet};
13use std::time::{Duration, Instant};
14use tracing::{debug, info, warn};
15
16/// Connection manager configuration
17#[derive(Debug, Clone)]
18pub struct ConnectionLimitsConfig {
19    /// Maximum total connections
20    pub max_connections: usize,
21    /// Maximum inbound connections
22    pub max_inbound: usize,
23    /// Maximum outbound connections
24    pub max_outbound: usize,
25    /// Reserved slots for important peers
26    pub reserved_slots: usize,
27    /// Connection idle timeout
28    pub idle_timeout: Duration,
29    /// Minimum score to avoid pruning (0-100)
30    pub min_score_threshold: u8,
31}
32
33impl Default for ConnectionLimitsConfig {
34    fn default() -> Self {
35        Self {
36            max_connections: 256,
37            max_inbound: 128,
38            max_outbound: 128,
39            reserved_slots: 8,
40            idle_timeout: Duration::from_secs(300),
41            min_score_threshold: 30,
42        }
43    }
44}
45
46/// Connection direction
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ConnectionDirection {
49    Inbound,
50    Outbound,
51}
52
53/// Information about a connection
54#[derive(Debug, Clone)]
55struct ConnectionInfo {
56    /// Peer ID
57    peer_id: PeerId,
58    /// Connection direction
59    direction: ConnectionDirection,
60    /// Time when connection was established
61    established_at: Instant,
62    /// Last activity time
63    last_activity: Instant,
64    /// Connection score (0-100)
65    score: u8,
66    /// Whether this peer has reserved slot
67    reserved: bool,
68    /// Number of messages sent
69    messages_sent: u64,
70    /// Number of messages received
71    messages_received: u64,
72    /// Average latency if known
73    avg_latency_ms: Option<u64>,
74}
75
76impl ConnectionInfo {
77    fn new(peer_id: PeerId, direction: ConnectionDirection) -> Self {
78        let now = Instant::now();
79        Self {
80            peer_id,
81            direction,
82            established_at: now,
83            last_activity: now,
84            score: 50, // Start neutral
85            reserved: false,
86            messages_sent: 0,
87            messages_received: 0,
88            avg_latency_ms: None,
89        }
90    }
91
92    fn is_idle(&self, timeout: Duration) -> bool {
93        self.last_activity.elapsed() > timeout
94    }
95
96    fn touch(&mut self) {
97        self.last_activity = Instant::now();
98    }
99
100    /// Calculate connection value for pruning decisions
101    fn calculate_value(&self) -> u64 {
102        let age_secs = self.established_at.elapsed().as_secs();
103        let activity = self.messages_sent + self.messages_received;
104
105        // Value = score * 10 + activity_rate + latency_bonus
106        let base_value = self.score as u64 * 10;
107        let activity_rate = (activity * 60)
108            .checked_div(age_secs)
109            .unwrap_or(activity * 60); // messages per minute
110        let latency_bonus = match self.avg_latency_ms {
111            Some(lat) if lat < 50 => 20,
112            Some(lat) if lat < 100 => 10,
113            Some(lat) if lat < 200 => 5,
114            _ => 0,
115        };
116
117        base_value + activity_rate + latency_bonus
118    }
119}
120
121/// Connection manager
122pub struct ConnectionManager {
123    /// Configuration
124    config: ConnectionLimitsConfig,
125    /// Active connections
126    connections: RwLock<HashMap<PeerId, ConnectionInfo>>,
127    /// Reserved peers (always allowed to connect)
128    reserved_peers: RwLock<HashSet<PeerId>>,
129    /// Banned peers (never allowed to connect)
130    banned_peers: RwLock<HashSet<PeerId>>,
131}
132
133impl ConnectionManager {
134    /// Create a new connection manager
135    pub fn new(config: ConnectionLimitsConfig) -> Self {
136        Self {
137            config,
138            connections: RwLock::new(HashMap::new()),
139            reserved_peers: RwLock::new(HashSet::new()),
140            banned_peers: RwLock::new(HashSet::new()),
141        }
142    }
143
144    /// Check if a new connection should be accepted
145    pub fn should_accept(&self, peer_id: &PeerId, direction: ConnectionDirection) -> bool {
146        // Always reject banned peers
147        if self.banned_peers.read().contains(peer_id) {
148            debug!("Rejecting banned peer: {}", peer_id);
149            return false;
150        }
151
152        // Always accept reserved peers (up to reserved slots limit)
153        if self.reserved_peers.read().contains(peer_id) {
154            let reserved_count = self
155                .connections
156                .read()
157                .values()
158                .filter(|c| c.reserved)
159                .count();
160            if reserved_count < self.config.reserved_slots {
161                return true;
162            }
163        }
164
165        let connections = self.connections.read();
166
167        // Check total limit
168        if connections.len() >= self.config.max_connections {
169            debug!(
170                "At max connections ({}), rejecting {}",
171                self.config.max_connections, peer_id
172            );
173            return false;
174        }
175
176        // Check direction-specific limits
177        let (inbound, outbound) =
178            connections
179                .values()
180                .fold((0, 0), |(i, o), c| match c.direction {
181                    ConnectionDirection::Inbound => (i + 1, o),
182                    ConnectionDirection::Outbound => (i, o + 1),
183                });
184
185        match direction {
186            ConnectionDirection::Inbound => {
187                if inbound >= self.config.max_inbound {
188                    debug!(
189                        "At max inbound ({}), rejecting {}",
190                        self.config.max_inbound, peer_id
191                    );
192                    return false;
193                }
194            }
195            ConnectionDirection::Outbound => {
196                if outbound >= self.config.max_outbound {
197                    debug!(
198                        "At max outbound ({}), rejecting {}",
199                        self.config.max_outbound, peer_id
200                    );
201                    return false;
202                }
203            }
204        }
205
206        true
207    }
208
209    /// Register a new connection
210    pub fn connection_established(&self, peer_id: PeerId, direction: ConnectionDirection) {
211        let is_reserved = self.reserved_peers.read().contains(&peer_id);
212
213        let mut connections = self.connections.write();
214        let mut info = ConnectionInfo::new(peer_id, direction);
215        info.reserved = is_reserved;
216
217        connections.insert(peer_id, info);
218        info!("Connection established: {} ({:?})", peer_id, direction);
219    }
220
221    /// Unregister a connection
222    pub fn connection_closed(&self, peer_id: &PeerId) {
223        let mut connections = self.connections.write();
224        if connections.remove(peer_id).is_some() {
225            debug!("Connection closed: {}", peer_id);
226        }
227    }
228
229    /// Record activity on a connection
230    pub fn record_activity(&self, peer_id: &PeerId, sent: bool) {
231        let mut connections = self.connections.write();
232        if let Some(info) = connections.get_mut(peer_id) {
233            info.touch();
234            if sent {
235                info.messages_sent += 1;
236            } else {
237                info.messages_received += 1;
238            }
239        }
240    }
241
242    /// Update connection score
243    pub fn update_score(&self, peer_id: &PeerId, delta: i16) {
244        let mut connections = self.connections.write();
245        if let Some(info) = connections.get_mut(peer_id) {
246            let new_score = (info.score as i16 + delta).clamp(0, 100) as u8;
247            info.score = new_score;
248        }
249    }
250
251    /// Update connection latency
252    pub fn update_latency(&self, peer_id: &PeerId, latency_ms: u64) {
253        let mut connections = self.connections.write();
254        if let Some(info) = connections.get_mut(peer_id) {
255            info.avg_latency_ms = Some(latency_ms);
256            info.touch();
257        }
258    }
259
260    /// Add a peer to reserved list
261    pub fn add_reserved(&self, peer_id: PeerId) {
262        self.reserved_peers.write().insert(peer_id);
263
264        // Update connection if exists
265        if let Some(info) = self.connections.write().get_mut(&peer_id) {
266            info.reserved = true;
267        }
268
269        info!("Added reserved peer: {}", peer_id);
270    }
271
272    /// Remove a peer from reserved list
273    pub fn remove_reserved(&self, peer_id: &PeerId) {
274        self.reserved_peers.write().remove(peer_id);
275
276        // Update connection if exists
277        if let Some(info) = self.connections.write().get_mut(peer_id) {
278            info.reserved = false;
279        }
280
281        debug!("Removed reserved peer: {}", peer_id);
282    }
283
284    /// Ban a peer
285    pub fn ban_peer(&self, peer_id: PeerId) {
286        self.banned_peers.write().insert(peer_id);
287        self.reserved_peers.write().remove(&peer_id);
288        warn!("Banned peer: {}", peer_id);
289    }
290
291    /// Unban a peer
292    pub fn unban_peer(&self, peer_id: &PeerId) {
293        self.banned_peers.write().remove(peer_id);
294        info!("Unbanned peer: {}", peer_id);
295    }
296
297    /// Check if a peer is banned
298    pub fn is_banned(&self, peer_id: &PeerId) -> bool {
299        self.banned_peers.read().contains(peer_id)
300    }
301
302    /// Get peers that should be disconnected (pruning candidates)
303    pub fn get_prune_candidates(&self, count: usize) -> Vec<PeerId> {
304        let connections = self.connections.read();
305
306        // Filter out reserved peers and those above threshold
307        let mut candidates: Vec<_> = connections
308            .values()
309            .filter(|c| !c.reserved && c.score < self.config.min_score_threshold)
310            .map(|c| (c.peer_id, c.calculate_value()))
311            .collect();
312
313        // Sort by value (lowest first)
314        candidates.sort_by_key(|(_, value)| *value);
315
316        candidates
317            .into_iter()
318            .take(count)
319            .map(|(peer_id, _)| peer_id)
320            .collect()
321    }
322
323    /// Get idle connections that should be closed
324    pub fn get_idle_connections(&self) -> Vec<PeerId> {
325        let connections = self.connections.read();
326        let timeout = self.config.idle_timeout;
327
328        connections
329            .values()
330            .filter(|c| !c.reserved && c.is_idle(timeout))
331            .map(|c| c.peer_id)
332            .collect()
333    }
334
335    /// Prune connections to make room for new ones
336    ///
337    /// Returns peer IDs that should be disconnected
338    pub fn prune_to_limit(&self) -> Vec<PeerId> {
339        let connections = self.connections.read();
340        let current = connections.len();
341
342        if current <= self.config.max_connections {
343            return vec![];
344        }
345
346        let to_prune = current - self.config.max_connections;
347        drop(connections);
348
349        let candidates = self.get_prune_candidates(to_prune);
350        info!(
351            "Pruning {} connections to stay within limit",
352            candidates.len()
353        );
354        candidates
355    }
356
357    /// Get all connected peer IDs
358    pub fn connected_peers(&self) -> Vec<PeerId> {
359        self.connections.read().keys().cloned().collect()
360    }
361
362    /// Get connection count
363    pub fn connection_count(&self) -> usize {
364        self.connections.read().len()
365    }
366
367    /// Check if connected to a peer
368    pub fn is_connected(&self, peer_id: &PeerId) -> bool {
369        self.connections.read().contains_key(peer_id)
370    }
371
372    /// Get connection statistics
373    pub fn stats(&self) -> ConnectionManagerStats {
374        let connections = self.connections.read();
375
376        let (inbound, outbound) =
377            connections
378                .values()
379                .fold((0, 0), |(i, o), c| match c.direction {
380                    ConnectionDirection::Inbound => (i + 1, o),
381                    ConnectionDirection::Outbound => (i, o + 1),
382                });
383
384        let reserved = connections.values().filter(|c| c.reserved).count();
385
386        let avg_score = if connections.is_empty() {
387            0
388        } else {
389            connections.values().map(|c| c.score as u64).sum::<u64>() / connections.len() as u64
390        };
391
392        ConnectionManagerStats {
393            total_connections: connections.len(),
394            max_connections: self.config.max_connections,
395            inbound_connections: inbound,
396            outbound_connections: outbound,
397            reserved_connections: reserved,
398            banned_peers: self.banned_peers.read().len(),
399            average_score: avg_score as u8,
400        }
401    }
402}
403
404impl Default for ConnectionManager {
405    fn default() -> Self {
406        Self::new(ConnectionLimitsConfig::default())
407    }
408}
409
410/// Connection manager statistics
411#[derive(Debug, Clone, Serialize)]
412pub struct ConnectionManagerStats {
413    /// Total active connections
414    pub total_connections: usize,
415    /// Maximum connections allowed
416    pub max_connections: usize,
417    /// Inbound connection count
418    pub inbound_connections: usize,
419    /// Outbound connection count
420    pub outbound_connections: usize,
421    /// Reserved connection count
422    pub reserved_connections: usize,
423    /// Number of banned peers
424    pub banned_peers: usize,
425    /// Average connection score
426    pub average_score: u8,
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    fn random_peer() -> PeerId {
434        PeerId::random()
435    }
436
437    #[test]
438    fn test_connection_manager_basic() {
439        let manager = ConnectionManager::default();
440        let peer1 = random_peer();
441        let peer2 = random_peer();
442
443        assert!(manager.should_accept(&peer1, ConnectionDirection::Inbound));
444
445        manager.connection_established(peer1, ConnectionDirection::Inbound);
446        assert!(manager.is_connected(&peer1));
447        assert_eq!(manager.connection_count(), 1);
448
449        manager.connection_established(peer2, ConnectionDirection::Outbound);
450        assert_eq!(manager.connection_count(), 2);
451
452        manager.connection_closed(&peer1);
453        assert!(!manager.is_connected(&peer1));
454        assert_eq!(manager.connection_count(), 1);
455    }
456
457    #[test]
458    fn test_connection_limits() {
459        let config = ConnectionLimitsConfig {
460            max_connections: 3,
461            max_inbound: 2,
462            max_outbound: 2,
463            ..Default::default()
464        };
465        let manager = ConnectionManager::new(config);
466
467        // Fill up inbound
468        let peer1 = random_peer();
469        let peer2 = random_peer();
470        manager.connection_established(peer1, ConnectionDirection::Inbound);
471        manager.connection_established(peer2, ConnectionDirection::Inbound);
472
473        // Should reject new inbound
474        let peer3 = random_peer();
475        assert!(!manager.should_accept(&peer3, ConnectionDirection::Inbound));
476
477        // But outbound should be ok
478        assert!(manager.should_accept(&peer3, ConnectionDirection::Outbound));
479        manager.connection_established(peer3, ConnectionDirection::Outbound);
480
481        // Now at max total, should reject all
482        let peer4 = random_peer();
483        assert!(!manager.should_accept(&peer4, ConnectionDirection::Inbound));
484        assert!(!manager.should_accept(&peer4, ConnectionDirection::Outbound));
485    }
486
487    #[test]
488    fn test_reserved_peers() {
489        let config = ConnectionLimitsConfig {
490            max_connections: 2,
491            reserved_slots: 1,
492            ..Default::default()
493        };
494        let manager = ConnectionManager::new(config);
495
496        let reserved_peer = random_peer();
497        manager.add_reserved(reserved_peer);
498
499        let peer1 = random_peer();
500        let peer2 = random_peer();
501        manager.connection_established(peer1, ConnectionDirection::Inbound);
502        manager.connection_established(peer2, ConnectionDirection::Outbound);
503
504        // At max, but reserved peer should be accepted
505        assert!(manager.should_accept(&reserved_peer, ConnectionDirection::Inbound));
506    }
507
508    #[test]
509    fn test_banned_peers() {
510        let manager = ConnectionManager::default();
511        let peer = random_peer();
512
513        assert!(manager.should_accept(&peer, ConnectionDirection::Inbound));
514
515        manager.ban_peer(peer);
516        assert!(manager.is_banned(&peer));
517        assert!(!manager.should_accept(&peer, ConnectionDirection::Inbound));
518
519        manager.unban_peer(&peer);
520        assert!(!manager.is_banned(&peer));
521        assert!(manager.should_accept(&peer, ConnectionDirection::Inbound));
522    }
523
524    #[test]
525    fn test_activity_tracking() {
526        let manager = ConnectionManager::default();
527        let peer = random_peer();
528
529        manager.connection_established(peer, ConnectionDirection::Outbound);
530
531        // Record some activity
532        manager.record_activity(&peer, true); // sent
533        manager.record_activity(&peer, false); // received
534        manager.record_activity(&peer, true); // sent
535
536        let stats = manager.stats();
537        assert_eq!(stats.total_connections, 1);
538    }
539
540    #[test]
541    fn test_score_update() {
542        let manager = ConnectionManager::default();
543        let peer = random_peer();
544
545        manager.connection_established(peer, ConnectionDirection::Inbound);
546        manager.update_score(&peer, 20); // 50 + 20 = 70
547        manager.update_score(&peer, -40); // 70 - 40 = 30
548
549        // Score should be clamped
550        manager.update_score(&peer, -100); // Should clamp to 0
551    }
552
553    #[test]
554    fn test_prune_candidates() {
555        let config = ConnectionLimitsConfig {
556            min_score_threshold: 50,
557            ..Default::default()
558        };
559        let manager = ConnectionManager::new(config);
560
561        // Add some peers
562        let high_score = random_peer();
563        let low_score1 = random_peer();
564        let low_score2 = random_peer();
565        let reserved = random_peer();
566
567        manager.connection_established(high_score, ConnectionDirection::Inbound);
568        manager.connection_established(low_score1, ConnectionDirection::Inbound);
569        manager.connection_established(low_score2, ConnectionDirection::Outbound);
570        manager.add_reserved(reserved);
571        manager.connection_established(reserved, ConnectionDirection::Inbound);
572
573        // Adjust scores
574        manager.update_score(&high_score, 30); // 80
575        manager.update_score(&low_score1, -30); // 20
576        manager.update_score(&low_score2, -25); // 25
577
578        // Get prune candidates
579        let candidates = manager.get_prune_candidates(2);
580
581        // Should include low score peers but not reserved
582        assert!(!candidates.contains(&reserved));
583        assert!(!candidates.contains(&high_score));
584        assert!(candidates.len() <= 2);
585    }
586
587    #[test]
588    fn test_idle_connections() {
589        let config = ConnectionLimitsConfig {
590            idle_timeout: Duration::from_millis(50),
591            ..Default::default()
592        };
593        let manager = ConnectionManager::new(config);
594
595        let peer = random_peer();
596        manager.connection_established(peer, ConnectionDirection::Inbound);
597
598        // Not idle yet
599        assert!(manager.get_idle_connections().is_empty());
600
601        // Wait for timeout
602        std::thread::sleep(Duration::from_millis(100));
603
604        // Should be idle now
605        let idle = manager.get_idle_connections();
606        assert_eq!(idle.len(), 1);
607        assert_eq!(idle[0], peer);
608    }
609
610    #[test]
611    fn test_stats() {
612        let manager = ConnectionManager::default();
613
614        let peer1 = random_peer();
615        let peer2 = random_peer();
616        let reserved = random_peer();
617
618        manager.connection_established(peer1, ConnectionDirection::Inbound);
619        manager.connection_established(peer2, ConnectionDirection::Outbound);
620        manager.add_reserved(reserved);
621        manager.connection_established(reserved, ConnectionDirection::Inbound);
622
623        let banned = random_peer();
624        manager.ban_peer(banned);
625
626        let stats = manager.stats();
627        assert_eq!(stats.total_connections, 3);
628        assert_eq!(stats.inbound_connections, 2);
629        assert_eq!(stats.outbound_connections, 1);
630        assert_eq!(stats.reserved_connections, 1);
631        assert_eq!(stats.banned_peers, 1);
632    }
633}