Skip to main content

ant_quic/masque/
context.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Context ID management for MASQUE CONNECT-UDP Bind
9//!
10//! Per draft-ietf-masque-connect-udp-listen-10:
11//! - Clients allocate even Context IDs
12//! - Servers allocate odd Context IDs
13//! - Context ID 0 is reserved for unextended UDP proxying
14//! - Only one uncompressed context allowed at a time
15//!
16//! This module provides the [`ContextManager`] for managing context lifecycles
17//! and enforcing the allocation rules required by the specification.
18
19use std::collections::HashMap;
20use std::fmt;
21use std::net::SocketAddr;
22use std::time::Instant;
23
24use crate::VarInt;
25
26/// Context allocation and state management
27///
28/// Manages both locally allocated contexts (sent via COMPRESSION_ASSIGN)
29/// and remotely allocated contexts (received via COMPRESSION_ASSIGN).
30#[derive(Debug)]
31pub struct ContextManager {
32    /// Locally allocated contexts
33    local_contexts: HashMap<VarInt, ContextInfo>,
34    /// Remotely allocated contexts
35    remote_contexts: HashMap<VarInt, ContextInfo>,
36    /// Current uncompressed context (only one allowed)
37    uncompressed_context: Option<VarInt>,
38    /// Next local context ID to allocate
39    next_local_id: u64,
40    /// Whether we allocate even (client) or odd (server) IDs
41    is_client: bool,
42}
43
44/// Information about a registered context
45#[derive(Debug, Clone)]
46pub struct ContextInfo {
47    /// Target address (None for uncompressed)
48    pub target: Option<SocketAddr>,
49    /// Current state
50    pub state: ContextState,
51    /// Creation timestamp
52    pub created_at: Instant,
53    /// Last activity timestamp
54    pub last_activity: Instant,
55}
56
57/// Context lifecycle states
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ContextState {
60    /// COMPRESSION_ASSIGN sent, awaiting ACK
61    Pending,
62    /// COMPRESSION_ACK received, context active
63    Active,
64    /// COMPRESSION_CLOSE sent or received
65    Closing,
66    /// Fully closed
67    Closed,
68}
69
70impl fmt::Display for ContextState {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            ContextState::Pending => write!(f, "pending"),
74            ContextState::Active => write!(f, "active"),
75            ContextState::Closing => write!(f, "closing"),
76            ContextState::Closed => write!(f, "closed"),
77        }
78    }
79}
80
81impl ContextManager {
82    /// Create a new context manager
83    ///
84    /// # Arguments
85    ///
86    /// * `is_client` - true if we're the initiating endpoint (allocates even IDs)
87    ///
88    /// # Example
89    ///
90    /// ```
91    /// use ant_quic::masque::ContextManager;
92    ///
93    /// // Client creates a manager that allocates even IDs
94    /// let client_mgr = ContextManager::new(true);
95    ///
96    /// // Server creates a manager that allocates odd IDs
97    /// let server_mgr = ContextManager::new(false);
98    /// ```
99    pub fn new(is_client: bool) -> Self {
100        Self {
101            local_contexts: HashMap::new(),
102            remote_contexts: HashMap::new(),
103            uncompressed_context: None,
104            // Start at 2 for client (0 reserved), 1 for server
105            next_local_id: if is_client { 2 } else { 1 },
106            is_client,
107        }
108    }
109
110    /// Returns whether this manager is for a client endpoint
111    pub fn is_client(&self) -> bool {
112        self.is_client
113    }
114
115    /// Allocate a new local context ID
116    ///
117    /// Clients allocate even IDs starting from 2.
118    /// Servers allocate odd IDs starting from 1.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`ContextError::IdSpaceExhausted`] if no more IDs are available.
123    pub fn allocate_local(&mut self) -> Result<VarInt, ContextError> {
124        let id = self.next_local_id;
125
126        // Ensure we stay within VarInt bounds
127        if id > VarInt::MAX.into_inner() {
128            return Err(ContextError::IdSpaceExhausted);
129        }
130
131        // Increment by 2 to stay in our allocation space (even/odd)
132        self.next_local_id = self
133            .next_local_id
134            .checked_add(2)
135            .ok_or(ContextError::IdSpaceExhausted)?;
136
137        VarInt::from_u64(id).map_err(|_| ContextError::IdSpaceExhausted)
138    }
139
140    /// Register a new uncompressed context
141    ///
142    /// An uncompressed context allows sending datagrams with inline target
143    /// information. Per the specification, only one uncompressed context
144    /// is allowed at a time.
145    ///
146    /// # Errors
147    ///
148    /// - [`ContextError::DuplicateUncompressed`] if an uncompressed context already exists
149    /// - [`ContextError::ReservedId`] if context_id is 0
150    pub fn register_uncompressed(&mut self, context_id: VarInt) -> Result<(), ContextError> {
151        if self.uncompressed_context.is_some() {
152            return Err(ContextError::DuplicateUncompressed);
153        }
154
155        if context_id.into_inner() == 0 {
156            return Err(ContextError::ReservedId);
157        }
158
159        let info = ContextInfo {
160            target: None,
161            state: ContextState::Pending,
162            created_at: Instant::now(),
163            last_activity: Instant::now(),
164        };
165
166        self.local_contexts.insert(context_id, info);
167        self.uncompressed_context = Some(context_id);
168
169        Ok(())
170    }
171
172    /// Register a new compressed context for a specific target
173    ///
174    /// A compressed context eliminates the need to include target address
175    /// information in each datagram, reducing overhead.
176    ///
177    /// # Errors
178    ///
179    /// - [`ContextError::DuplicateTarget`] if a context for this target already exists
180    pub fn register_compressed(
181        &mut self,
182        context_id: VarInt,
183        target: SocketAddr,
184    ) -> Result<(), ContextError> {
185        // Check for duplicate target
186        for info in self
187            .local_contexts
188            .values()
189            .chain(self.remote_contexts.values())
190        {
191            if info.target == Some(target) && info.state != ContextState::Closed {
192                return Err(ContextError::DuplicateTarget(target));
193            }
194        }
195
196        let info = ContextInfo {
197            target: Some(target),
198            state: ContextState::Pending,
199            created_at: Instant::now(),
200            last_activity: Instant::now(),
201        };
202
203        self.local_contexts.insert(context_id, info);
204
205        Ok(())
206    }
207
208    /// Register a remote context (received via COMPRESSION_ASSIGN)
209    ///
210    /// This is called when we receive a COMPRESSION_ASSIGN from the peer.
211    /// The context starts in Active state since we'll send COMPRESSION_ACK.
212    ///
213    /// # Errors
214    ///
215    /// - [`ContextError::DuplicateTarget`] if a context for this target already exists
216    /// - [`ContextError::DuplicateUncompressed`] if registering uncompressed and one exists
217    pub fn register_remote(
218        &mut self,
219        context_id: VarInt,
220        target: Option<SocketAddr>,
221    ) -> Result<(), ContextError> {
222        // Check for duplicate uncompressed
223        if target.is_none() && self.uncompressed_context.is_some() {
224            return Err(ContextError::DuplicateUncompressed);
225        }
226
227        // Check for duplicate target
228        if let Some(t) = target {
229            for info in self
230                .local_contexts
231                .values()
232                .chain(self.remote_contexts.values())
233            {
234                if info.target == Some(t) && info.state != ContextState::Closed {
235                    return Err(ContextError::DuplicateTarget(t));
236                }
237            }
238        }
239
240        let info = ContextInfo {
241            target,
242            state: ContextState::Active, // Remote contexts are active once we ACK
243            created_at: Instant::now(),
244            last_activity: Instant::now(),
245        };
246
247        self.remote_contexts.insert(context_id, info);
248
249        if target.is_none() {
250            self.uncompressed_context = Some(context_id);
251        }
252
253        Ok(())
254    }
255
256    /// Handle received COMPRESSION_ACK
257    ///
258    /// Transitions a pending local context to active state.
259    ///
260    /// # Errors
261    ///
262    /// - [`ContextError::UnknownContext`] if the context ID is not found
263    /// - [`ContextError::InvalidState`] if the context is not in Pending state
264    pub fn handle_ack(&mut self, context_id: VarInt) -> Result<(), ContextError> {
265        let info = self
266            .local_contexts
267            .get_mut(&context_id)
268            .ok_or(ContextError::UnknownContext)?;
269
270        if info.state != ContextState::Pending {
271            return Err(ContextError::InvalidState);
272        }
273
274        info.state = ContextState::Active;
275        info.last_activity = Instant::now();
276
277        Ok(())
278    }
279
280    /// Close a context (local or remote)
281    ///
282    /// Transitions the context to Closed state and clears the uncompressed
283    /// context tracking if applicable.
284    ///
285    /// # Errors
286    ///
287    /// - [`ContextError::UnknownContext`] if the context ID is not found
288    pub fn close(&mut self, context_id: VarInt) -> Result<(), ContextError> {
289        if let Some(info) = self.local_contexts.get_mut(&context_id) {
290            info.state = ContextState::Closed;
291            info.last_activity = Instant::now();
292        } else if let Some(info) = self.remote_contexts.get_mut(&context_id) {
293            info.state = ContextState::Closed;
294            info.last_activity = Instant::now();
295        } else {
296            return Err(ContextError::UnknownContext);
297        }
298
299        if self.uncompressed_context == Some(context_id) {
300            self.uncompressed_context = None;
301        }
302
303        Ok(())
304    }
305
306    /// Look up context by target address
307    ///
308    /// Returns the Context ID for an active compressed context targeting
309    /// the specified address, if one exists.
310    pub fn get_by_target(&self, target: SocketAddr) -> Option<VarInt> {
311        for (id, info) in self
312            .local_contexts
313            .iter()
314            .chain(self.remote_contexts.iter())
315        {
316            if info.target == Some(target) && info.state == ContextState::Active {
317                return Some(*id);
318            }
319        }
320        None
321    }
322
323    /// Get the active uncompressed context ID if available
324    pub fn uncompressed(&self) -> Option<VarInt> {
325        self.uncompressed_context.filter(|id| {
326            self.local_contexts
327                .get(id)
328                .or_else(|| self.remote_contexts.get(id))
329                .map(|i| i.state == ContextState::Active)
330                .unwrap_or(false)
331        })
332    }
333
334    /// Get information about a context
335    pub fn get_context(&self, context_id: VarInt) -> Option<&ContextInfo> {
336        self.local_contexts
337            .get(&context_id)
338            .or_else(|| self.remote_contexts.get(&context_id))
339    }
340
341    /// Get target address for a context
342    pub fn get_target(&self, context_id: VarInt) -> Option<SocketAddr> {
343        self.get_context(context_id).and_then(|info| info.target)
344    }
345
346    /// Update last activity time for a context
347    pub fn touch(&mut self, context_id: VarInt) -> Result<(), ContextError> {
348        if let Some(info) = self.local_contexts.get_mut(&context_id) {
349            info.last_activity = Instant::now();
350            Ok(())
351        } else if let Some(info) = self.remote_contexts.get_mut(&context_id) {
352            info.last_activity = Instant::now();
353            Ok(())
354        } else {
355            Err(ContextError::UnknownContext)
356        }
357    }
358
359    /// Get count of active contexts
360    pub fn active_count(&self) -> usize {
361        self.local_contexts
362            .values()
363            .chain(self.remote_contexts.values())
364            .filter(|info| info.state == ContextState::Active)
365            .count()
366    }
367
368    /// Clean up closed contexts older than the specified age
369    pub fn cleanup_closed(&mut self, max_age: std::time::Duration) {
370        let now = Instant::now();
371        self.local_contexts.retain(|_, info| {
372            info.state != ContextState::Closed || now.duration_since(info.last_activity) < max_age
373        });
374        self.remote_contexts.retain(|_, info| {
375            info.state != ContextState::Closed || now.duration_since(info.last_activity) < max_age
376        });
377    }
378
379    /// Get iterator over all local context IDs
380    pub fn local_context_ids(&self) -> impl Iterator<Item = VarInt> + '_ {
381        self.local_contexts.keys().copied()
382    }
383
384    /// Get iterator over all remote context IDs
385    pub fn remote_context_ids(&self) -> impl Iterator<Item = VarInt> + '_ {
386        self.remote_contexts.keys().copied()
387    }
388}
389
390/// Context management errors
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub enum ContextError {
393    /// Context ID space exhausted (no more IDs available)
394    IdSpaceExhausted,
395    /// Only one uncompressed context allowed
396    DuplicateUncompressed,
397    /// Context ID 0 is reserved
398    ReservedId,
399    /// Duplicate target address
400    DuplicateTarget(SocketAddr),
401    /// Unknown context ID
402    UnknownContext,
403    /// Invalid context state for operation
404    InvalidState,
405}
406
407impl fmt::Display for ContextError {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        match self {
410            ContextError::IdSpaceExhausted => write!(f, "context ID space exhausted"),
411            ContextError::DuplicateUncompressed => {
412                write!(f, "only one uncompressed context allowed")
413            }
414            ContextError::ReservedId => write!(f, "context ID 0 is reserved"),
415            ContextError::DuplicateTarget(addr) => {
416                write!(f, "duplicate target address: {}", addr)
417            }
418            ContextError::UnknownContext => write!(f, "unknown context ID"),
419            ContextError::InvalidState => write!(f, "invalid context state for operation"),
420        }
421    }
422}
423
424impl std::error::Error for ContextError {}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use std::net::{IpAddr, Ipv4Addr};
430
431    #[test]
432    fn test_context_allocation_client() {
433        let mut mgr = ContextManager::new(true);
434        assert!(mgr.is_client());
435
436        let id1 = mgr.allocate_local().unwrap();
437        assert_eq!(id1.into_inner(), 2); // Client starts at 2 (even)
438
439        let id2 = mgr.allocate_local().unwrap();
440        assert_eq!(id2.into_inner(), 4);
441
442        let id3 = mgr.allocate_local().unwrap();
443        assert_eq!(id3.into_inner(), 6);
444    }
445
446    #[test]
447    fn test_context_allocation_server() {
448        let mut mgr = ContextManager::new(false);
449        assert!(!mgr.is_client());
450
451        let id1 = mgr.allocate_local().unwrap();
452        assert_eq!(id1.into_inner(), 1); // Server starts at 1 (odd)
453
454        let id2 = mgr.allocate_local().unwrap();
455        assert_eq!(id2.into_inner(), 3);
456    }
457
458    #[test]
459    fn test_uncompressed_context_limit() {
460        let mut mgr = ContextManager::new(true);
461        let id = mgr.allocate_local().unwrap();
462        mgr.register_uncompressed(id).unwrap();
463
464        let id2 = mgr.allocate_local().unwrap();
465        let result = mgr.register_uncompressed(id2);
466        assert_eq!(result, Err(ContextError::DuplicateUncompressed));
467    }
468
469    #[test]
470    fn test_reserved_id_zero() {
471        let mut mgr = ContextManager::new(true);
472        let result = mgr.register_uncompressed(VarInt::from_u32(0));
473        assert_eq!(result, Err(ContextError::ReservedId));
474    }
475
476    #[test]
477    fn test_compressed_context_lifecycle() {
478        let mut mgr = ContextManager::new(true);
479        let id = mgr.allocate_local().unwrap();
480        let target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
481
482        mgr.register_compressed(id, target).unwrap();
483        assert_eq!(mgr.get_context(id).unwrap().state, ContextState::Pending);
484
485        mgr.handle_ack(id).unwrap();
486        assert_eq!(mgr.get_context(id).unwrap().state, ContextState::Active);
487
488        assert_eq!(mgr.get_by_target(target), Some(id));
489        assert_eq!(mgr.get_target(id), Some(target));
490
491        mgr.close(id).unwrap();
492        assert_eq!(mgr.get_context(id).unwrap().state, ContextState::Closed);
493        assert_eq!(mgr.get_by_target(target), None);
494    }
495
496    #[test]
497    fn test_duplicate_target() {
498        let mut mgr = ContextManager::new(true);
499        let target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 9000);
500
501        let id1 = mgr.allocate_local().unwrap();
502        mgr.register_compressed(id1, target).unwrap();
503        mgr.handle_ack(id1).unwrap();
504
505        let id2 = mgr.allocate_local().unwrap();
506        let result = mgr.register_compressed(id2, target);
507        assert_eq!(result, Err(ContextError::DuplicateTarget(target)));
508    }
509
510    #[test]
511    fn test_remote_context_registration() {
512        let mut mgr = ContextManager::new(true);
513        let target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
514
515        // Remote context from server (odd ID)
516        mgr.register_remote(VarInt::from_u32(1), Some(target))
517            .unwrap();
518
519        // Remote contexts start as Active
520        assert_eq!(
521            mgr.get_context(VarInt::from_u32(1)).unwrap().state,
522            ContextState::Active
523        );
524
525        // Should be findable by target
526        assert_eq!(mgr.get_by_target(target), Some(VarInt::from_u32(1)));
527    }
528
529    #[test]
530    fn test_active_count() {
531        let mut mgr = ContextManager::new(true);
532
533        assert_eq!(mgr.active_count(), 0);
534
535        let id1 = mgr.allocate_local().unwrap();
536        let target1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 1000);
537        mgr.register_compressed(id1, target1).unwrap();
538        mgr.handle_ack(id1).unwrap();
539
540        assert_eq!(mgr.active_count(), 1);
541
542        let id2 = mgr.allocate_local().unwrap();
543        let target2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 2000);
544        mgr.register_compressed(id2, target2).unwrap();
545        mgr.handle_ack(id2).unwrap();
546
547        assert_eq!(mgr.active_count(), 2);
548
549        mgr.close(id1).unwrap();
550        assert_eq!(mgr.active_count(), 1);
551    }
552
553    #[test]
554    fn test_unknown_context_errors() {
555        let mut mgr = ContextManager::new(true);
556        let unknown_id = VarInt::from_u32(999);
557
558        assert_eq!(
559            mgr.handle_ack(unknown_id),
560            Err(ContextError::UnknownContext)
561        );
562        assert_eq!(mgr.close(unknown_id), Err(ContextError::UnknownContext));
563        assert_eq!(mgr.touch(unknown_id), Err(ContextError::UnknownContext));
564    }
565
566    #[test]
567    fn test_invalid_state_ack() {
568        let mut mgr = ContextManager::new(true);
569        let id = mgr.allocate_local().unwrap();
570        let target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
571
572        mgr.register_compressed(id, target).unwrap();
573        mgr.handle_ack(id).unwrap();
574
575        // Double ack should fail
576        assert_eq!(mgr.handle_ack(id), Err(ContextError::InvalidState));
577    }
578
579    #[test]
580    fn test_context_iterators() {
581        let mut mgr = ContextManager::new(true);
582
583        let id1 = mgr.allocate_local().unwrap();
584        let id2 = mgr.allocate_local().unwrap();
585        let target1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 1000);
586        let target2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 2000);
587
588        mgr.register_compressed(id1, target1).unwrap();
589        mgr.register_compressed(id2, target2).unwrap();
590
591        let local_ids: Vec<_> = mgr.local_context_ids().collect();
592        assert_eq!(local_ids.len(), 2);
593        assert!(local_ids.contains(&id1));
594        assert!(local_ids.contains(&id2));
595
596        // Register a remote context
597        let remote_id = VarInt::from_u32(1);
598        let remote_target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
599        mgr.register_remote(remote_id, Some(remote_target)).unwrap();
600
601        let remote_ids: Vec<_> = mgr.remote_context_ids().collect();
602        assert_eq!(remote_ids.len(), 1);
603        assert!(remote_ids.contains(&remote_id));
604    }
605}