Skip to main content

kipuka_coap/
dtls.rs

1//! DTLS session management for EST-coaps transport security.
2//!
3//! RFC 9148 §5 mandates DTLS to secure all EST-coaps exchanges. This module
4//! provides session tracking, caching abstractions, and a concrete OpenSSL-based
5//! DTLS implementation using memory BIOs for UDP transport.
6//!
7//! # Session Resumption
8//!
9//! Constrained devices benefit significantly from DTLS session resumption
10//! (RFC 6347 §4.2.8, RFC 9147 §5) because the full handshake involves
11//! multiple round trips and is computationally expensive, especially with
12//! post-quantum key exchange (ML-KEM).
13//!
14//! The [`DtlsSessionCache`] provides a bounded, TTL-expiring cache of
15//! established sessions keyed by peer address.
16//!
17//! # OpenSSL DTLS Integration
18//!
19//! The [`DtlsContext`] wraps an `openssl::ssl::SslContext` configured for
20//! DTLS server operation with optional client certificate authentication.
21//! [`DtlsConnection`] handles individual peer sessions using memory BIOs
22//! for non-blocking UDP I/O.
23
24use crate::CoapError;
25use openssl::pkey::PKey;
26use openssl::ssl::{SslContext, SslMethod, SslVerifyMode};
27use openssl::x509::X509;
28use std::collections::HashMap;
29use std::net::SocketAddr;
30use std::time::{Duration, Instant};
31
32/// DTLS protocol version.
33///
34/// This model can represent DTLS 1.2 and 1.3; the OpenSSL transport currently
35/// negotiates DTLS 1.2 only.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum DtlsVersion {
38    /// DTLS 1.2 per RFC 6347.
39    V1_2,
40    /// DTLS 1.3 per RFC 9147.
41    V1_3,
42}
43
44impl DtlsVersion {
45    /// Returns the human-readable version string.
46    pub fn as_str(&self) -> &'static str {
47        match self {
48            Self::V1_2 => "DTLS 1.2",
49            Self::V1_3 => "DTLS 1.3",
50        }
51    }
52}
53
54/// An established DTLS session for a CoAP/EST-coaps connection.
55///
56/// RFC 9148 §5: EST-coaps uses DTLS to secure the CoAP transport.
57/// The OpenSSL transport supports DTLS 1.2 (RFC 6347).
58///
59/// This struct tracks the session state needed for EST operations:
60/// the peer identity (from the client certificate or PSK), the session
61/// identifier for resumption, and protocol version.
62#[derive(Debug, Clone)]
63pub struct DtlsSession {
64    /// Opaque session identifier for resumption.
65    session_id: Vec<u8>,
66    /// Peer network address.
67    peer_addr: SocketAddr,
68    /// Client certificate presented during handshake (DER-encoded), if any.
69    ///
70    /// For certificate-based EST enrollment, the client may present an
71    /// existing certificate for re-enrollment (RFC 9148 §5.3).
72    client_cert: Option<Vec<u8>>,
73    /// Timestamp when the session was established.
74    created_at: Instant,
75    /// Negotiated DTLS protocol version.
76    protocol_version: DtlsVersion,
77}
78
79impl DtlsSession {
80    /// Creates a new DTLS session record.
81    pub fn new(session_id: Vec<u8>, peer_addr: SocketAddr, protocol_version: DtlsVersion) -> Self {
82        Self {
83            session_id,
84            peer_addr,
85            client_cert: None,
86            created_at: Instant::now(),
87            protocol_version,
88        }
89    }
90
91    /// Creates a new DTLS session with a client certificate.
92    pub fn with_client_cert(
93        session_id: Vec<u8>,
94        peer_addr: SocketAddr,
95        protocol_version: DtlsVersion,
96        client_cert_der: Vec<u8>,
97    ) -> Self {
98        Self {
99            session_id,
100            peer_addr,
101            client_cert: Some(client_cert_der),
102            created_at: Instant::now(),
103            protocol_version,
104        }
105    }
106
107    /// Returns the opaque session identifier.
108    pub fn session_id(&self) -> &[u8] {
109        &self.session_id
110    }
111
112    /// Returns the peer network address.
113    pub fn peer_addr(&self) -> SocketAddr {
114        self.peer_addr
115    }
116
117    /// Returns the DER-encoded client certificate, if presented.
118    pub fn client_cert(&self) -> Option<&[u8]> {
119        self.client_cert.as_deref()
120    }
121
122    /// Returns when the session was established.
123    pub fn created_at(&self) -> Instant {
124        self.created_at
125    }
126
127    /// Returns the negotiated DTLS version.
128    pub fn protocol_version(&self) -> DtlsVersion {
129        self.protocol_version
130    }
131
132    /// Checks whether the session has exceeded the given TTL.
133    pub fn is_expired(&self, ttl: Duration) -> bool {
134        self.created_at.elapsed() > ttl
135    }
136
137    /// Extracts client certificate information from this session.
138    ///
139    /// Parses the DER-encoded client certificate using `synta_certificate`
140    /// to extract the subject DN (RFC 4514 string form) and serial number.
141    ///
142    /// Returns `None` if no client certificate was presented or if the
143    /// certificate cannot be parsed.
144    pub fn client_cert_info(&self) -> Option<ClientCertInfo> {
145        let der = self.client_cert.as_ref()?;
146        ClientCertInfo::from_der(der)
147    }
148}
149
150/// Client certificate information extracted from a DTLS handshake.
151///
152/// Used to identify the enrolling client for EST operations that require
153/// mTLS authentication (simpleenroll, simplereenroll, serverkeygen).
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct ClientCertInfo {
156    /// Subject distinguished name (RFC 4514 string form).
157    pub subject_dn: String,
158    /// Certificate serial number (big-endian unsigned integer).
159    pub serial: Vec<u8>,
160    /// Full DER-encoded certificate.
161    pub der_bytes: Vec<u8>,
162}
163
164impl ClientCertInfo {
165    /// Parses a DER-encoded X.509 certificate and extracts identity fields.
166    ///
167    /// Uses `synta_certificate::Certificate::from_der()` for ASN.1 parsing
168    /// and `synta_certificate::format_dn()` for RFC 4514 DN formatting.
169    ///
170    /// Returns `None` if the DER bytes cannot be parsed as a valid certificate.
171    pub fn from_der(der: &[u8]) -> Option<Self> {
172        let cert = synta_certificate::Certificate::from_der(der).ok()?;
173        let subject_dn = synta_certificate::format_dn(cert.tbs_certificate.subject.0);
174        let serial = cert.tbs_certificate.serial_number.as_bytes().to_vec();
175
176        Some(Self {
177            subject_dn,
178            serial,
179            der_bytes: der.to_vec(),
180        })
181    }
182}
183
184/// A bounded, TTL-expiring cache of DTLS sessions keyed by peer address.
185///
186/// Constrained devices perform expensive handshakes (especially with
187/// post-quantum key exchange), so session resumption significantly
188/// reduces latency and power consumption for repeated EST operations.
189///
190/// # Capacity Management
191///
192/// The cache enforces a maximum number of sessions. When full, expired
193/// sessions are purged first. If still full, the oldest session is evicted.
194#[derive(Debug)]
195pub struct DtlsSessionCache {
196    /// Active sessions indexed by peer address.
197    sessions: HashMap<SocketAddr, DtlsSession>,
198    /// Maximum number of cached sessions.
199    max_sessions: usize,
200    /// Time-to-live for cached sessions.
201    ttl: Duration,
202}
203
204impl DtlsSessionCache {
205    /// Creates a new session cache.
206    ///
207    /// # Arguments
208    ///
209    /// * `max_sessions` - Maximum number of sessions to cache.
210    /// * `ttl` - Duration after which sessions expire and become eligible
211    ///   for eviction.
212    pub fn new(max_sessions: usize, ttl: Duration) -> Self {
213        Self {
214            sessions: HashMap::with_capacity(max_sessions),
215            max_sessions,
216            ttl,
217        }
218    }
219
220    /// Inserts a session into the cache.
221    ///
222    /// If the cache is at capacity, expired sessions are purged first.
223    /// If still full, the oldest session is evicted to make room.
224    pub fn insert(&mut self, session: DtlsSession) {
225        if self.sessions.len() >= self.max_sessions
226            && !self.sessions.contains_key(&session.peer_addr)
227        {
228            self.cleanup_expired();
229
230            // If still full after cleanup, evict the oldest session.
231            if self.sessions.len() >= self.max_sessions
232                && let Some(oldest_addr) = self.oldest_session_addr()
233            {
234                self.sessions.remove(&oldest_addr);
235            }
236        }
237
238        self.sessions.insert(session.peer_addr, session);
239    }
240
241    /// Retrieves a cached session for the given peer address.
242    ///
243    /// Returns `None` if no session exists or if the session has expired.
244    /// Expired sessions are removed on access.
245    pub fn get(&mut self, peer_addr: &SocketAddr) -> Option<&DtlsSession> {
246        // Check expiry and remove if stale.
247        if let Some(session) = self.sessions.get(peer_addr)
248            && session.is_expired(self.ttl)
249        {
250            self.sessions.remove(peer_addr);
251            return None;
252        }
253
254        self.sessions.get(peer_addr)
255    }
256
257    /// Removes a session from the cache.
258    ///
259    /// Returns the removed session, or `None` if no session existed for
260    /// the given address.
261    pub fn remove(&mut self, peer_addr: &SocketAddr) -> Option<DtlsSession> {
262        self.sessions.remove(peer_addr)
263    }
264
265    /// Removes all expired sessions from the cache.
266    ///
267    /// Returns the number of sessions removed.
268    pub fn cleanup_expired(&mut self) -> usize {
269        let ttl = self.ttl;
270        let before = self.sessions.len();
271        self.sessions.retain(|_, session| !session.is_expired(ttl));
272        before - self.sessions.len()
273    }
274
275    /// Returns the number of currently cached sessions.
276    pub fn len(&self) -> usize {
277        self.sessions.len()
278    }
279
280    /// Returns whether the cache is empty.
281    pub fn is_empty(&self) -> bool {
282        self.sessions.is_empty()
283    }
284
285    /// Returns the configured maximum number of sessions.
286    pub fn max_sessions(&self) -> usize {
287        self.max_sessions
288    }
289
290    /// Returns the configured TTL.
291    pub fn ttl(&self) -> Duration {
292        self.ttl
293    }
294
295    /// Finds the address of the oldest session in the cache.
296    fn oldest_session_addr(&self) -> Option<SocketAddr> {
297        self.sessions
298            .iter()
299            .min_by_key(|(_, session)| session.created_at)
300            .map(|(addr, _)| *addr)
301    }
302}
303
304/// OpenSSL DTLS server context for EST-coaps.
305///
306/// Wraps an `openssl::ssl::SslContext` configured with the server certificate,
307/// private key, and trusted CA for optional client certificate verification.
308///
309/// RFC 9148 §5 requires DTLS for all EST-coaps exchanges. The server presents
310/// its certificate during the handshake and optionally requests a client
311/// certificate for mTLS-based enrollment authentication.
312///
313/// # Example
314///
315/// ```no_run
316/// # use kipuka_coap::dtls::DtlsContext;
317/// let cert_pem = std::fs::read("server.crt").unwrap();
318/// let key_pem = std::fs::read("server.key").unwrap();
319/// let ca_pem = std::fs::read("ca.crt").unwrap();
320/// let ctx = DtlsContext::new(&cert_pem, &key_pem, &ca_pem).unwrap();
321/// ```
322pub struct DtlsContext {
323    ctx: SslContext,
324}
325
326impl DtlsContext {
327    /// Creates a new DTLS server context.
328    ///
329    /// # Arguments
330    ///
331    /// * `cert_pem` - PEM-encoded server certificate.
332    /// * `key_pem` - PEM-encoded server private key.
333    /// * `ca_pem` - PEM-encoded CA certificate for verifying client certificates.
334    ///
335    /// The context is configured to request (but not require) a client certificate
336    /// during the DTLS handshake. EST operations that need mTLS authentication
337    /// should check `DtlsConnection::client_cert()` after the handshake completes.
338    pub fn new(cert_pem: &[u8], key_pem: &[u8], ca_pem: &[u8]) -> Result<Self, CoapError> {
339        let mut ctx_builder = SslContext::builder(SslMethod::dtls())
340            .map_err(|e| CoapError::DtlsError(format!("Failed to create DTLS context: {e}")))?;
341
342        let cert = X509::from_pem(cert_pem).map_err(|e| {
343            CoapError::DtlsError(format!("Failed to parse server certificate PEM: {e}"))
344        })?;
345        ctx_builder
346            .set_certificate(&cert)
347            .map_err(|e| CoapError::DtlsError(format!("Failed to set server certificate: {e}")))?;
348
349        let key = PKey::private_key_from_pem(key_pem).map_err(|e| {
350            CoapError::DtlsError(format!("Failed to parse server private key PEM: {e}"))
351        })?;
352        ctx_builder
353            .set_private_key(&key)
354            .map_err(|e| CoapError::DtlsError(format!("Failed to set server private key: {e}")))?;
355
356        ctx_builder
357            .check_private_key()
358            .map_err(|e| CoapError::DtlsError(format!("Server certificate/key mismatch: {e}")))?;
359
360        // Request client certificate but do not require it — EST operations
361        // that need mTLS will check the certificate after handshake.
362        ctx_builder.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT);
363        ctx_builder
364            .set_min_proto_version(Some(openssl::ssl::SslVersion::DTLS1_2))
365            .map_err(|e| CoapError::DtlsError(e.to_string()))?;
366
367        // Load the CA certificate for client certificate verification.
368        let ca = X509::from_pem(ca_pem).map_err(|e| {
369            CoapError::DtlsError(format!("Failed to parse CA certificate PEM: {e}"))
370        })?;
371        ctx_builder
372            .cert_store_mut()
373            .add_cert(ca)
374            .map_err(|e| CoapError::DtlsError(format!("Failed to add CA to trust store: {e}")))?;
375
376        Ok(Self {
377            ctx: ctx_builder.build(),
378        })
379    }
380
381    /// Returns a reference to the underlying `SslContext`.
382    ///
383    /// Used by [`DtlsConnection`] to create per-peer SSL instances.
384    pub fn ssl_context(&self) -> &SslContext {
385        &self.ctx
386    }
387}
388
389impl std::fmt::Debug for DtlsContext {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        f.debug_struct("DtlsContext")
392            .field("ctx", &"<SslContext>")
393            .finish()
394    }
395}
396
397/// An established DTLS connection to a single CoAP peer.
398///
399/// Wraps an OpenSSL `Ssl` instance with memory BIOs for non-blocking UDP I/O.
400/// The connection tracks the peer address and the client certificate presented
401/// during the DTLS handshake (if any).
402///
403/// # Memory BIO Architecture
404///
405/// OpenSSL's DTLS API uses BIO (Basic I/O) abstractions. For UDP transport,
406/// we use memory BIOs instead of socket BIOs:
407///
408/// 1. Write received UDP datagram into the read BIO.
409/// 2. Call `SSL_read` to get decrypted application data.
410/// 3. Call `SSL_write` to encrypt response data.
411/// 4. Read from the write BIO to get the encrypted datagram.
412/// 5. Send the encrypted datagram via UDP socket.
413#[derive(Debug, Default)]
414struct DatagramIo {
415    incoming: std::collections::VecDeque<Vec<u8>>,
416    outgoing: Vec<Vec<u8>>,
417}
418impl std::io::Read for DatagramIo {
419    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
420        let packet = self
421            .incoming
422            .pop_front()
423            .ok_or(std::io::ErrorKind::WouldBlock)?;
424        if packet.len() > buf.len() {
425            return Err(std::io::ErrorKind::InvalidData.into());
426        }
427        buf[..packet.len()].copy_from_slice(&packet);
428        Ok(packet.len())
429    }
430}
431impl std::io::Write for DatagramIo {
432    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
433        if self.outgoing.len() >= 64 {
434            return Err(std::io::ErrorKind::OutOfMemory.into());
435        }
436        self.outgoing.push(buf.to_vec());
437        Ok(buf.len())
438    }
439    fn flush(&mut self) -> std::io::Result<()> {
440        Ok(())
441    }
442}
443
444#[derive(Debug)]
445pub struct DtlsConnection {
446    stream: openssl::ssl::SslStream<DatagramIo>,
447    peer_addr: SocketAddr,
448    client_cert: Option<ClientCertInfo>,
449    handshake_complete: bool,
450    pub(crate) created_at: Instant,
451}
452impl DtlsConnection {
453    pub fn new(ctx: &DtlsContext, peer_addr: SocketAddr) -> Result<Self, CoapError> {
454        let mut ssl = openssl::ssl::Ssl::new(ctx.ssl_context()).map_err(dtls_error)?;
455        ssl.set_accept_state();
456        ssl.set_mtu(1200).map_err(dtls_error)?;
457        let stream =
458            openssl::ssl::SslStream::new(ssl, DatagramIo::default()).map_err(dtls_error)?;
459        Ok(Self {
460            stream,
461            peer_addr,
462            client_cert: None,
463            handshake_complete: false,
464            created_at: Instant::now(),
465        })
466    }
467    pub fn peer_addr(&self) -> SocketAddr {
468        self.peer_addr
469    }
470    pub fn client_cert(&self) -> Option<&ClientCertInfo> {
471        self.client_cert.as_ref()
472    }
473    pub fn is_handshake_complete(&self) -> bool {
474        self.handshake_complete
475    }
476    pub fn ssl(&self) -> &openssl::ssl::SslRef {
477        self.stream.ssl()
478    }
479    pub(crate) fn receive(&mut self, packet: &[u8]) -> Result<Vec<Vec<u8>>, CoapError> {
480        self.stream.get_mut().incoming.push_back(packet.to_vec());
481        if !self.handshake_complete {
482            match self.stream.accept() {
483                Ok(()) => {
484                    let cert = self.stream.ssl().peer_certificate().ok_or_else(|| {
485                        CoapError::Unauthorized("client certificate required".into())
486                    })?;
487                    self.client_cert =
488                        ClientCertInfo::from_der(&cert.to_der().map_err(dtls_error)?);
489                    if self.client_cert.is_none() {
490                        return Err(CoapError::Unauthorized("invalid client certificate".into()));
491                    }
492                    self.handshake_complete = true;
493                }
494                Err(e) if retry_ssl(&e) => return Ok(Vec::new()),
495                Err(e) => return Err(dtls_error(e)),
496            }
497        }
498        let mut result = Vec::new();
499        let mut buf = vec![0; 65535];
500        loop {
501            match self.stream.ssl_read(&mut buf) {
502                Ok(0) => break,
503                Ok(n) => result.push(buf[..n].to_vec()),
504                Err(e) if retry_ssl(&e) => break,
505                Err(e) => return Err(dtls_error(e)),
506            }
507        }
508        Ok(result)
509    }
510    pub(crate) fn encrypt(&mut self, data: &[u8]) -> Result<(), CoapError> {
511        if !self.handshake_complete {
512            return Err(CoapError::Unauthorized("DTLS handshake incomplete".into()));
513        }
514        self.stream.ssl_write(data).map_err(dtls_error)?;
515        Ok(())
516    }
517    pub(crate) fn handle_timeout(&mut self) -> Result<(), CoapError> {
518        use foreign_types::ForeignTypeRef;
519        // OpenSSL ssl.h DTLSv1_handle_timeout macro, DTLS_CTRL_HANDLE_TIMEOUT=74.
520        // The SSL object remains exclusively borrowed for the duration of the call.
521        let result = unsafe {
522            openssl_sys::SSL_ctrl(self.stream.ssl().as_ptr(), 74, 0, std::ptr::null_mut())
523        };
524        if result < 0 {
525            return Err(CoapError::DtlsError("DTLS retransmission failed".into()));
526        }
527        Ok(())
528    }
529    pub(crate) fn outgoing(&mut self) -> Vec<Vec<u8>> {
530        std::mem::take(&mut self.stream.get_mut().outgoing)
531    }
532}
533fn retry_ssl(e: &openssl::ssl::Error) -> bool {
534    matches!(
535        e.code(),
536        openssl::ssl::ErrorCode::WANT_READ | openssl::ssl::ErrorCode::WANT_WRITE
537    )
538}
539fn dtls_error(e: impl std::fmt::Display) -> CoapError {
540    CoapError::DtlsError(e.to_string())
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use std::net::{IpAddr, Ipv4Addr};
547
548    fn test_addr(port: u16) -> SocketAddr {
549        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, port as u8)), port)
550    }
551
552    #[test]
553    fn test_dtls_version_as_str() {
554        assert_eq!(DtlsVersion::V1_2.as_str(), "DTLS 1.2");
555        assert_eq!(DtlsVersion::V1_3.as_str(), "DTLS 1.3");
556    }
557
558    #[test]
559    fn test_session_creation() {
560        let addr = test_addr(5683);
561        let session = DtlsSession::new(vec![1, 2, 3], addr, DtlsVersion::V1_3);
562
563        assert_eq!(session.session_id(), &[1, 2, 3]);
564        assert_eq!(session.peer_addr(), addr);
565        assert!(session.client_cert().is_none());
566        assert_eq!(session.protocol_version(), DtlsVersion::V1_3);
567    }
568
569    #[test]
570    fn test_session_with_client_cert_raw_access() {
571        let addr = test_addr(5683);
572        let cert_der = vec![0x30, 0x82, 0x01, 0x00];
573        let session =
574            DtlsSession::with_client_cert(vec![1, 2, 3], addr, DtlsVersion::V1_2, cert_der.clone());
575
576        // Raw DER bytes are always accessible.
577        assert_eq!(session.client_cert(), Some(cert_der.as_slice()));
578        // But parsing dummy bytes as an X.509 certificate correctly returns None.
579        assert!(session.client_cert_info().is_none());
580    }
581
582    #[test]
583    fn test_session_with_real_client_cert() {
584        // Generate a real self-signed certificate for testing.
585        use openssl::asn1::Asn1Time;
586        use openssl::bn::BigNum;
587        use openssl::hash::MessageDigest;
588        use openssl::pkey::PKey;
589        use openssl::rsa::Rsa;
590        use openssl::x509::{X509Builder, X509NameBuilder};
591
592        let rsa = Rsa::generate(2048).unwrap();
593        let key = PKey::from_rsa(rsa).unwrap();
594
595        let mut name_builder = X509NameBuilder::new().unwrap();
596        name_builder
597            .append_entry_by_text("CN", "test-client")
598            .unwrap();
599        let name = name_builder.build();
600
601        let mut builder = X509Builder::new().unwrap();
602        builder.set_version(2).unwrap();
603        builder.set_subject_name(&name).unwrap();
604        builder.set_issuer_name(&name).unwrap();
605        builder.set_pubkey(&key).unwrap();
606
607        let serial = BigNum::from_u32(42).unwrap();
608        builder
609            .set_serial_number(&serial.to_asn1_integer().unwrap())
610            .unwrap();
611
612        let not_before = Asn1Time::days_from_now(0).unwrap();
613        let not_after = Asn1Time::days_from_now(365).unwrap();
614        builder.set_not_before(&not_before).unwrap();
615        builder.set_not_after(&not_after).unwrap();
616
617        builder.sign(&key, MessageDigest::sha256()).unwrap();
618        let cert = builder.build();
619        let cert_der = cert.to_der().unwrap();
620
621        let addr = test_addr(5683);
622        let session =
623            DtlsSession::with_client_cert(vec![1, 2, 3], addr, DtlsVersion::V1_2, cert_der.clone());
624
625        assert_eq!(session.client_cert(), Some(cert_der.as_slice()));
626        let info = session.client_cert_info().unwrap();
627        assert!(
628            info.subject_dn.contains("test-client"),
629            "Expected subject DN to contain 'test-client', got: {}",
630            info.subject_dn
631        );
632        assert!(!info.serial.is_empty());
633        assert_eq!(info.der_bytes, cert_der);
634    }
635
636    #[test]
637    fn test_client_cert_info_from_der_invalid() {
638        // Invalid DER should return None, not panic.
639        assert!(ClientCertInfo::from_der(&[0x00, 0x01]).is_none());
640        assert!(ClientCertInfo::from_der(&[]).is_none());
641    }
642
643    #[test]
644    fn test_session_expiry() {
645        let addr = test_addr(5683);
646        let session = DtlsSession::new(vec![1], addr, DtlsVersion::V1_3);
647
648        // Session just created should not be expired with a long TTL.
649        assert!(!session.is_expired(Duration::from_secs(3600)));
650
651        // Session should be expired with a zero TTL.
652        assert!(session.is_expired(Duration::ZERO));
653    }
654
655    #[test]
656    fn test_cache_insert_and_get() {
657        let mut cache = DtlsSessionCache::new(10, Duration::from_secs(3600));
658        let addr = test_addr(5683);
659        let session = DtlsSession::new(vec![1, 2, 3], addr, DtlsVersion::V1_3);
660
661        cache.insert(session);
662        assert_eq!(cache.len(), 1);
663        assert!(!cache.is_empty());
664
665        let retrieved = cache.get(&addr);
666        assert!(retrieved.is_some());
667        assert_eq!(retrieved.unwrap().session_id(), &[1, 2, 3]);
668    }
669
670    #[test]
671    fn test_cache_remove() {
672        let mut cache = DtlsSessionCache::new(10, Duration::from_secs(3600));
673        let addr = test_addr(5683);
674        let session = DtlsSession::new(vec![1], addr, DtlsVersion::V1_3);
675
676        cache.insert(session);
677        assert_eq!(cache.len(), 1);
678
679        let removed = cache.remove(&addr);
680        assert!(removed.is_some());
681        assert_eq!(cache.len(), 0);
682    }
683
684    #[test]
685    fn test_cache_eviction_on_capacity() {
686        let mut cache = DtlsSessionCache::new(2, Duration::from_secs(3600));
687
688        cache.insert(DtlsSession::new(vec![1], test_addr(1), DtlsVersion::V1_3));
689        cache.insert(DtlsSession::new(vec![2], test_addr(2), DtlsVersion::V1_3));
690        assert_eq!(cache.len(), 2);
691
692        // Third insert should evict the oldest.
693        cache.insert(DtlsSession::new(vec![3], test_addr(3), DtlsVersion::V1_3));
694        assert_eq!(cache.len(), 2);
695
696        // The first session (oldest) should have been evicted.
697        assert!(cache.get(&test_addr(1)).is_none());
698        assert!(cache.get(&test_addr(3)).is_some());
699    }
700
701    #[test]
702    fn test_cache_expired_not_returned() {
703        let mut cache = DtlsSessionCache::new(10, Duration::ZERO);
704        let addr = test_addr(5683);
705        let session = DtlsSession::new(vec![1], addr, DtlsVersion::V1_3);
706
707        cache.insert(session);
708        // With zero TTL, session should be expired immediately.
709        assert!(cache.get(&addr).is_none());
710        assert_eq!(cache.len(), 0);
711    }
712
713    #[test]
714    fn test_cache_cleanup_expired() {
715        let mut cache = DtlsSessionCache::new(10, Duration::ZERO);
716        cache.insert(DtlsSession::new(vec![1], test_addr(1), DtlsVersion::V1_3));
717        cache.insert(DtlsSession::new(vec![2], test_addr(2), DtlsVersion::V1_3));
718
719        // With zero TTL, all sessions are already expired.
720        let removed = cache.cleanup_expired();
721        assert_eq!(removed, 2);
722        assert!(cache.is_empty());
723    }
724
725    #[test]
726    fn test_cache_update_existing() {
727        let mut cache = DtlsSessionCache::new(10, Duration::from_secs(3600));
728        let addr = test_addr(5683);
729
730        cache.insert(DtlsSession::new(vec![1], addr, DtlsVersion::V1_2));
731        cache.insert(DtlsSession::new(vec![2], addr, DtlsVersion::V1_3));
732
733        assert_eq!(cache.len(), 1);
734        let session = cache.get(&addr).unwrap();
735        assert_eq!(session.session_id(), &[2]);
736        assert_eq!(session.protocol_version(), DtlsVersion::V1_3);
737    }
738}