Skip to main content

xds_server/
connections.rs

1//! Connection tracking and limits.
2//!
3//! This module provides connection tracking and limiting functionality
4//! to prevent resource exhaustion from too many concurrent connections.
5//!
6//! # Example
7//!
8//! ```rust
9//! use xds_server::connections::{ConnectionTracker, ConnectionLimits};
10//!
11//! let limits = ConnectionLimits::new(1000, 100);
12//! let tracker = ConnectionTracker::new(limits);
13//!
14//! if let Some(_guard) = tracker.try_acquire(None) {
15//!     // Connection accepted
16//! } else {
17//!     // Connection rejected - at limit
18//! }
19//! ```
20
21use std::collections::HashMap;
22use std::net::SocketAddr;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::Arc;
25use std::time::Instant;
26
27use parking_lot::RwLock;
28use tracing::{debug, warn};
29
30/// Limits for connection tracking.
31#[derive(Debug, Clone)]
32pub struct ConnectionLimits {
33    /// Maximum total connections.
34    pub max_connections: u64,
35    /// Maximum connections per IP address.
36    pub max_per_ip: u64,
37    /// Maximum concurrent streams per connection.
38    pub max_streams_per_connection: u32,
39}
40
41impl Default for ConnectionLimits {
42    fn default() -> Self {
43        Self {
44            max_connections: 10_000,
45            max_per_ip: 100,
46            max_streams_per_connection: 100,
47        }
48    }
49}
50
51impl ConnectionLimits {
52    /// Create new connection limits.
53    pub fn new(max_connections: u64, max_per_ip: u64) -> Self {
54        Self {
55            max_connections,
56            max_per_ip,
57            ..Default::default()
58        }
59    }
60
61    /// Set maximum streams per connection.
62    pub fn with_max_streams(mut self, max: u32) -> Self {
63        self.max_streams_per_connection = max;
64        self
65    }
66}
67
68/// Information about a tracked connection.
69#[derive(Debug, Clone)]
70pub struct ConnectionInfo {
71    /// Unique connection ID.
72    pub id: u64,
73    /// Remote address (if available).
74    pub remote_addr: Option<SocketAddr>,
75    /// When the connection was established.
76    pub connected_at: Instant,
77    /// Number of active streams.
78    pub active_streams: u32,
79}
80
81/// Tracker for managing active connections.
82///
83/// Provides connection counting, per-IP limits, and connection metadata.
84#[derive(Debug)]
85pub struct ConnectionTracker {
86    inner: Arc<ConnectionTrackerInner>,
87}
88
89#[derive(Debug)]
90struct ConnectionTrackerInner {
91    limits: ConnectionLimits,
92    /// Counter for generating connection IDs.
93    next_id: AtomicU64,
94    /// Total active connections.
95    active: AtomicU64,
96    /// Connections per IP address.
97    per_ip: RwLock<HashMap<std::net::IpAddr, u64>>,
98    /// Active connection info.
99    connections: RwLock<HashMap<u64, ConnectionInfo>>,
100}
101
102impl Clone for ConnectionTracker {
103    fn clone(&self) -> Self {
104        Self {
105            inner: Arc::clone(&self.inner),
106        }
107    }
108}
109
110impl Default for ConnectionTracker {
111    fn default() -> Self {
112        Self::new(ConnectionLimits::default())
113    }
114}
115
116impl ConnectionTracker {
117    /// Create a new connection tracker with the given limits.
118    pub fn new(limits: ConnectionLimits) -> Self {
119        Self {
120            inner: Arc::new(ConnectionTrackerInner {
121                limits,
122                next_id: AtomicU64::new(1),
123                active: AtomicU64::new(0),
124                per_ip: RwLock::new(HashMap::new()),
125                connections: RwLock::new(HashMap::new()),
126            }),
127        }
128    }
129
130    /// Get the connection limits.
131    pub fn limits(&self) -> &ConnectionLimits {
132        &self.inner.limits
133    }
134
135    /// Get the current number of active connections.
136    pub fn active_connections(&self) -> u64 {
137        self.inner.active.load(Ordering::Relaxed)
138    }
139
140    /// Get connections for a specific IP.
141    pub fn connections_for_ip(&self, ip: std::net::IpAddr) -> u64 {
142        self.inner
143            .per_ip
144            .read()
145            .get(&ip)
146            .copied()
147            .unwrap_or(0)
148    }
149
150    /// Try to acquire a connection slot.
151    ///
152    /// Returns `None` if connection limits are exceeded.
153    pub fn try_acquire(&self, remote_addr: Option<SocketAddr>) -> Option<ConnectionGuard> {
154        // Check total limit
155        let current = self.inner.active.load(Ordering::Relaxed);
156        if current >= self.inner.limits.max_connections {
157            warn!(
158                current = current,
159                limit = self.inner.limits.max_connections,
160                "connection rejected: at max connections"
161            );
162            return None;
163        }
164
165        // Check per-IP limit if address is known
166        if let Some(addr) = remote_addr {
167            let ip = addr.ip();
168            let mut per_ip = self.inner.per_ip.write();
169            let ip_count = per_ip.get(&ip).copied().unwrap_or(0);
170
171            if ip_count >= self.inner.limits.max_per_ip {
172                warn!(
173                    ip = %ip,
174                    current = ip_count,
175                    limit = self.inner.limits.max_per_ip,
176                    "connection rejected: at max per-IP limit"
177                );
178                return None;
179            }
180
181            // Increment per-IP counter
182            *per_ip.entry(ip).or_insert(0) += 1;
183        }
184
185        // Increment total counter
186        self.inner.active.fetch_add(1, Ordering::Relaxed);
187
188        // Generate connection ID and store info
189        let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
190        let info = ConnectionInfo {
191            id,
192            remote_addr,
193            connected_at: Instant::now(),
194            active_streams: 0,
195        };
196
197        self.inner.connections.write().insert(id, info);
198
199        debug!(
200            id = id,
201            remote_addr = ?remote_addr,
202            active = self.active_connections(),
203            "connection accepted"
204        );
205
206        Some(ConnectionGuard {
207            tracker: self.clone(),
208            id,
209            remote_addr,
210        })
211    }
212
213    /// Get information about all active connections.
214    pub fn list_connections(&self) -> Vec<ConnectionInfo> {
215        self.inner
216            .connections
217            .read()
218            .values()
219            .cloned()
220            .collect()
221    }
222
223    /// Get information about a specific connection.
224    pub fn get_connection(&self, id: u64) -> Option<ConnectionInfo> {
225        self.inner.connections.read().get(&id).cloned()
226    }
227
228    fn release(&self, id: u64, remote_addr: Option<SocketAddr>) {
229        // Decrement total counter
230        self.inner.active.fetch_sub(1, Ordering::Relaxed);
231
232        // Decrement per-IP counter
233        if let Some(addr) = remote_addr {
234            let ip = addr.ip();
235            let mut per_ip = self.inner.per_ip.write();
236            if let Some(count) = per_ip.get_mut(&ip) {
237                *count = count.saturating_sub(1);
238                if *count == 0 {
239                    per_ip.remove(&ip);
240                }
241            }
242        }
243
244        // Remove connection info
245        self.inner.connections.write().remove(&id);
246
247        debug!(
248            id = id,
249            remote_addr = ?remote_addr,
250            active = self.active_connections(),
251            "connection released"
252        );
253    }
254}
255
256/// Guard for a tracked connection.
257///
258/// Automatically releases the connection slot when dropped.
259#[derive(Debug)]
260pub struct ConnectionGuard {
261    tracker: ConnectionTracker,
262    id: u64,
263    remote_addr: Option<SocketAddr>,
264}
265
266impl ConnectionGuard {
267    /// Get the connection ID.
268    pub fn id(&self) -> u64 {
269        self.id
270    }
271
272    /// Get the remote address.
273    pub fn remote_addr(&self) -> Option<SocketAddr> {
274        self.remote_addr
275    }
276
277    /// Increment the stream count for this connection.
278    pub fn add_stream(&self) -> bool {
279        let mut connections = self.tracker.inner.connections.write();
280        if let Some(info) = connections.get_mut(&self.id) {
281            if info.active_streams >= self.tracker.inner.limits.max_streams_per_connection {
282                return false;
283            }
284            info.active_streams += 1;
285            true
286        } else {
287            false
288        }
289    }
290
291    /// Decrement the stream count for this connection.
292    pub fn remove_stream(&self) {
293        let mut connections = self.tracker.inner.connections.write();
294        if let Some(info) = connections.get_mut(&self.id) {
295            info.active_streams = info.active_streams.saturating_sub(1);
296        }
297    }
298}
299
300impl Drop for ConnectionGuard {
301    fn drop(&mut self) {
302        self.tracker.release(self.id, self.remote_addr);
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use std::net::{IpAddr, Ipv4Addr};
310
311    #[test]
312    fn connection_limits_default() {
313        let limits = ConnectionLimits::default();
314        assert_eq!(limits.max_connections, 10_000);
315        assert_eq!(limits.max_per_ip, 100);
316    }
317
318    #[test]
319    fn tracker_basic() {
320        let tracker = ConnectionTracker::new(ConnectionLimits::new(10, 5));
321        assert_eq!(tracker.active_connections(), 0);
322
323        let guard = tracker.try_acquire(None).expect("should acquire connection");
324        assert_eq!(tracker.active_connections(), 1);
325        assert!(guard.id() > 0);
326
327        drop(guard);
328        assert_eq!(tracker.active_connections(), 0);
329    }
330
331    #[test]
332    fn tracker_max_connections() {
333        let tracker = ConnectionTracker::new(ConnectionLimits::new(2, 10));
334
335        let _g1 = tracker.try_acquire(None).expect("should acquire first connection");
336        let _g2 = tracker.try_acquire(None).expect("should acquire second connection");
337
338        // Should be rejected
339        assert!(tracker.try_acquire(None).is_none());
340    }
341
342    #[test]
343    fn tracker_per_ip_limit() {
344        let tracker = ConnectionTracker::new(ConnectionLimits::new(100, 2));
345
346        let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
347        let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8080);
348
349        let _g1 = tracker.try_acquire(Some(addr1)).expect("should acquire first connection");
350        let _g2 = tracker.try_acquire(Some(addr1)).expect("should acquire second connection");
351
352        // Third connection from same IP should be rejected
353        assert!(tracker.try_acquire(Some(addr1)).is_none());
354
355        // But different IP should work
356        let _g3 = tracker.try_acquire(Some(addr2)).expect("should acquire from different IP");
357    }
358
359    #[test]
360    fn tracker_per_ip_release() {
361        let tracker = ConnectionTracker::new(ConnectionLimits::new(100, 2));
362
363        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
364
365        {
366            let _g1 = tracker.try_acquire(Some(addr)).expect("should acquire first");
367            let _g2 = tracker.try_acquire(Some(addr)).expect("should acquire second");
368            assert!(tracker.try_acquire(Some(addr)).is_none());
369        }
370
371        // After guards dropped, should be able to acquire again
372        let _g = tracker.try_acquire(Some(addr)).expect("should acquire after release");
373        assert_eq!(tracker.connections_for_ip(addr.ip()), 1);
374    }
375
376    #[test]
377    fn tracker_stream_counting() {
378        let tracker = ConnectionTracker::new(ConnectionLimits::new(10, 10).with_max_streams(2));
379
380        let guard = tracker.try_acquire(None).expect("should acquire connection");
381
382        assert!(guard.add_stream());
383        assert!(guard.add_stream());
384        assert!(!guard.add_stream()); // Should fail - at limit
385
386        guard.remove_stream();
387        assert!(guard.add_stream()); // Should work now
388    }
389
390    #[test]
391    fn tracker_list_connections() {
392        let tracker = ConnectionTracker::new(ConnectionLimits::default());
393
394        let _g1 = tracker.try_acquire(None).expect("should acquire first");
395        let _g2 = tracker.try_acquire(None).expect("should acquire second");
396
397        let connections = tracker.list_connections();
398        assert_eq!(connections.len(), 2);
399    }
400}