Skip to main content

commonware_stream/
encrypted.rs

1//! Encrypted stream implementation using ChaCha20-Poly1305.
2//!
3//! # Design
4//!
5//! ## Handshake
6//!
7//! c.f. [commonware_cryptography::handshake]. One difference here is that the listener does not
8//! know the dialer's public key in advance. Instead, the dialer tells the listener its public key
9//! in the first message. The listener has an opportunity to reject the connection if it does not
10//! wish to connect ([listen] takes in an arbitrary function to implement this).
11//!
12//! ## Encryption
13//!
14//! All traffic is encrypted using ChaCha20-Poly1305. A shared secret is established using an
15//! ephemeral X25519 Diffie-Hellman key exchange. This secret, combined with the handshake
16//! transcript, is used to derive keys for both the handshake's key confirmation messages and
17//! the post-handshake data traffic. Binding the derived keys to the handshake transcript prevents
18//! man-in-the-middle and transcript substitution attacks.
19//!
20//! Each directional cipher uses a 12-byte nonce derived from a counter that is incremented for each
21//! message sent. This counter has sufficient cardinality for over 2.5 trillion years of continuous
22//! communication at a rate of 1 billion messages per second - sufficient for all practical use cases.
23//! This ensures that well-behaving peers can remain connected indefinitely as long as they both
24//! remain online (maximizing p2p network stability). In the unlikely case of counter overflow, the
25//! connection will be terminated and a new connection should be established. This method prevents
26//! nonce reuse (which would compromise message confidentiality) while saving bandwidth (as there is
27//! no need to transmit nonces explicitly).
28//!
29//! # Security
30//!
31//! ## Requirements
32//!
33//! - **Pre-Shared Namespace**: Peers must agree on a unique, application-specific namespace
34//!   out-of-band to prevent cross-application replay attacks.
35//! - **Time Synchronization**: Peer clocks must be synchronized to within the `synchrony_bound`
36//!   to correctly validate timestamps.
37//!
38//! ## Provided
39//!
40//! - **Mutual Authentication**: Both parties prove ownership of their static private keys through
41//!   signatures.
42//! - **Forward Secrecy**: Ephemeral encryption keys ensure that any compromise of long-term static keys
43//!   doesn't expose the contents of previous sessions.
44//! - **Session Uniqueness**: A listener's [commonware_cryptography::handshake::SynAck] is bound to the dialer's [commonware_cryptography::handshake::Syn] message and
45//!   [commonware_cryptography::handshake::Ack]s are bound to the complete handshake transcript, preventing replay attacks and ensuring
46//!   message integrity.
47//! - **Handshake Timeout**: A configurable deadline is enforced for handshake completion to protect
48//!   against malicious peers that create connections but abandon handshakes.
49//!
50//! ## Not Provided
51//!
52//! - **Anonymity**: Peer identities are not hidden during handshakes from network observers (both active
53//!   and passive).
54//! - **Padding**: Messages are encrypted as-is, allowing an attacker to perform traffic analysis.
55//! - **Future Secrecy**: If a peer's static private key is compromised, future sessions will be exposed.
56//! - **0-RTT**: The protocol does not support 0-RTT handshakes (resumed sessions).
57
58use crate::utils::codec::{append_frame, framed_len, recv_frame, send_frame};
59use commonware_codec::{DecodeExt, Encode as _, Error as CodecError, FixedSize};
60use commonware_cryptography::{
61    Signer,
62    handshake::{
63        self, Ack, Context, Error as HandshakeError, RecvCipher, SendCipher, Syn, SynAck, dial_end,
64        dial_start, listen_end, listen_start,
65    },
66};
67use commonware_formatting::hex;
68use commonware_macros::select;
69use commonware_runtime::{
70    BufMut, BufferPool, BufferPooler, Clock, Error as RuntimeError, IoBuf, IoBufMut, IoBufs, Sink,
71    Stream,
72};
73use commonware_utils::SystemTimeExt;
74use rand_core::CryptoRng;
75use std::{future::Future, ops::Range, time::Duration};
76use thiserror::Error;
77
78const TAG_SIZE: u32 = {
79    assert!(handshake::TAG_SIZE <= u32::MAX as usize);
80    handshake::TAG_SIZE as u32
81};
82
83/// Maximum supported plaintext message size.
84pub const MAX_SIZE: u32 = u32::MAX - TAG_SIZE;
85
86/// Errors that can occur when interacting with a stream.
87#[derive(Error, Debug)]
88pub enum Error {
89    #[error("handshake error: {0}")]
90    HandshakeError(HandshakeError),
91    #[error("unable to decode: {0}")]
92    UnableToDecode(CodecError),
93    #[error("peer rejected: {}", hex(_0))]
94    PeerRejected(Vec<u8>),
95    #[error("recv failed")]
96    RecvFailed(RuntimeError),
97    #[error("recv too large: {0} bytes")]
98    RecvTooLarge(usize),
99    #[error("invalid varint length prefix")]
100    InvalidVarint,
101    #[error("send failed")]
102    SendFailed(RuntimeError),
103    #[error("send zero size")]
104    SendZeroSize,
105    #[error("send too large: {0} bytes")]
106    SendTooLarge(usize),
107    #[error("connection closed")]
108    StreamClosed,
109    #[error("handshake timed out")]
110    HandshakeTimeout,
111}
112
113impl From<CodecError> for Error {
114    fn from(value: CodecError) -> Self {
115        Self::UnableToDecode(value)
116    }
117}
118
119impl From<HandshakeError> for Error {
120    fn from(value: HandshakeError) -> Self {
121        Self::HandshakeError(value)
122    }
123}
124
125/// Configuration for a connection.
126///
127/// # Warning
128///
129/// Synchronize this configuration across all peers.
130/// Mismatched configurations may cause dropped connections or parsing errors.
131#[derive(Clone)]
132pub struct Config<S> {
133    /// The private key used for signing messages.
134    ///
135    /// This proves our own identity to other peers.
136    pub signing_key: S,
137
138    /// Unique prefix for all signed messages. Should be application-specific.
139    /// Prevents replay attacks across different applications using the same keys.
140    pub namespace: Vec<u8>,
141
142    /// Maximum message size (in bytes). Prevents memory exhaustion DoS attacks.
143    ///
144    /// The largest supported value is [`MAX_SIZE`].
145    ///
146    /// Fixed-size handshake frames use their protocol-defined sizes instead of
147    /// inheriting this limit.
148    pub max_message_size: u32,
149
150    /// Maximum time drift allowed for future timestamps. Handles clock skew.
151    pub synchrony_bound: Duration,
152
153    /// Maximum age of handshake messages before rejection.
154    pub max_handshake_age: Duration,
155
156    /// The allotted time for the handshake to complete.
157    pub handshake_timeout: Duration,
158}
159
160impl<S> Config<S> {
161    /// Computes current time and acceptable timestamp range.
162    pub fn time_information(&self, ctx: &impl Clock) -> (u64, Range<u64>) {
163        fn duration_to_u64(d: Duration) -> u64 {
164            u64::try_from(d.as_millis()).expect("duration ms should fit in an u64")
165        }
166        let current_time_ms = duration_to_u64(ctx.current().epoch());
167        let ok_timestamps = (current_time_ms
168            .saturating_sub(duration_to_u64(self.max_handshake_age)))
169            ..(current_time_ms.saturating_add(duration_to_u64(self.synchrony_bound)));
170        (current_time_ms, ok_timestamps)
171    }
172}
173
174// Handshake frames are fixed-size protocol messages, so we cap receives to
175// their exact encoded length instead of the application message limit.
176async fn recv_handshake_frame<M, T>(stream: &mut T) -> Result<M, Error>
177where
178    M: DecodeExt<()> + FixedSize,
179    T: Stream,
180{
181    let frame = recv_frame(
182        stream,
183        u32::try_from(M::SIZE).expect("handshake frame should fit in u32"),
184    )
185    .await?;
186    Ok(M::decode(frame)?)
187}
188
189/// Establishes an authenticated connection to a peer as the dialer.
190/// Returns sender and receiver for encrypted communication.
191pub async fn dial<R: BufferPooler + CryptoRng + Clock, S: Signer, I: Stream, O: Sink>(
192    ctx: R,
193    config: Config<S>,
194    peer: S::PublicKey,
195    mut stream: I,
196    mut sink: O,
197) -> Result<(Sender<O>, Receiver<I>), Error> {
198    let pool = ctx.network_buffer_pool().clone();
199    let timeout = ctx.sleep(config.handshake_timeout);
200    let inner_routine = async move {
201        send_frame(
202            &mut sink,
203            config.signing_key.public_key().encode(),
204            config.max_message_size,
205        )
206        .await?;
207
208        let (current_time, ok_timestamps) = config.time_information(&ctx);
209        let (state, syn) = dial_start(
210            ctx,
211            Context::new(
212                &config.namespace,
213                current_time,
214                ok_timestamps,
215                config.signing_key,
216                peer,
217            ),
218        );
219        send_frame(&mut sink, syn.encode(), config.max_message_size).await?;
220
221        let syn_ack = recv_handshake_frame::<SynAck<S::Signature>, _>(&mut stream).await?;
222
223        let (ack, send, recv) = dial_end(state, syn_ack)?;
224        send_frame(&mut sink, ack.encode(), config.max_message_size).await?;
225
226        Ok((
227            Sender {
228                cipher: send,
229                sink,
230                max_message_size: config.max_message_size,
231                pool: pool.clone(),
232            },
233            Receiver {
234                cipher: recv,
235                stream,
236                max_message_size: config.max_message_size,
237                pool,
238            },
239        ))
240    };
241
242    select! {
243        x = inner_routine => x,
244        _ = timeout => Err(Error::HandshakeTimeout),
245    }
246}
247
248/// Accepts an authenticated connection from a peer as the listener.
249/// Returns the peer's identity, sender, and receiver for encrypted communication.
250pub async fn listen<
251    R: BufferPooler + CryptoRng + Clock,
252    S: Signer,
253    I: Stream,
254    O: Sink,
255    Fut: Future<Output = bool>,
256    F: FnOnce(S::PublicKey) -> Fut,
257>(
258    ctx: R,
259    bouncer: F,
260    config: Config<S>,
261    mut stream: I,
262    mut sink: O,
263) -> Result<(S::PublicKey, Sender<O>, Receiver<I>), Error> {
264    let pool = ctx.network_buffer_pool().clone();
265    let timeout = ctx.sleep(config.handshake_timeout);
266    let inner_routine = async move {
267        let peer = recv_handshake_frame::<S::PublicKey, _>(&mut stream).await?;
268        if !bouncer(peer.clone()).await {
269            return Err(Error::PeerRejected(peer.encode().to_vec()));
270        }
271
272        let msg1 = recv_handshake_frame::<Syn<S::Signature>, _>(&mut stream).await?;
273
274        let (current_time, ok_timestamps) = config.time_information(&ctx);
275        let (state, syn_ack) = listen_start(
276            ctx,
277            Context::new(
278                &config.namespace,
279                current_time,
280                ok_timestamps,
281                config.signing_key,
282                peer.clone(),
283            ),
284            msg1,
285        )?;
286        send_frame(&mut sink, syn_ack.encode(), config.max_message_size).await?;
287
288        let ack = recv_handshake_frame::<Ack, _>(&mut stream).await?;
289
290        let (send, recv) = listen_end(state, ack)?;
291
292        Ok((
293            peer,
294            Sender {
295                cipher: send,
296                sink,
297                max_message_size: config.max_message_size,
298                pool: pool.clone(),
299            },
300            Receiver {
301                cipher: recv,
302                stream,
303                max_message_size: config.max_message_size,
304                pool,
305            },
306        ))
307    };
308
309    select! {
310        x = inner_routine => x,
311        _ = timeout => Err(Error::HandshakeTimeout),
312    }
313}
314
315/// Sends encrypted messages to a peer.
316pub struct Sender<O> {
317    cipher: SendCipher,
318    sink: O,
319    max_message_size: u32,
320    pool: BufferPool,
321}
322
323/// Describes one contiguous sink chunk made up of one or more encrypted frames.
324struct ChunkPlan {
325    messages: Vec<IoBufs>,
326    total_len: usize,
327}
328
329impl<O: Sink> Sender<O> {
330    /// Returns the total encoded size of one encrypted frame.
331    ///
332    /// The returned size includes the length prefix, ciphertext, and AEAD tag.
333    fn encrypted_frame_len(&self, plaintext_len: usize) -> Result<usize, Error> {
334        framed_len(
335            plaintext_len + TAG_SIZE as usize,
336            self.max_message_size.saturating_add(TAG_SIZE),
337        )
338    }
339
340    /// Appends one encrypted frame directly into caller-provided storage.
341    ///
342    /// This lets chunk builders append multiple independently framed
343    /// ciphertexts into a single contiguous allocation without staging each
344    /// frame in its own buffer first.
345    fn append_encrypted_frame(
346        &mut self,
347        chunk: &mut IoBufMut,
348        mut bufs: IoBufs,
349    ) -> Result<(), Error> {
350        append_frame(
351            chunk,
352            bufs.len() + TAG_SIZE as usize,
353            self.max_message_size.saturating_add(TAG_SIZE),
354            |chunk, plaintext_offset| {
355                // Copy the plaintext directly into the frame.
356                chunk.put(&mut bufs);
357
358                // Encrypt in-place and append the tag to the frame.
359                let tag = self
360                    .cipher
361                    .send_in_place(&mut chunk.as_mut()[plaintext_offset..])?;
362                chunk.put_slice(&tag);
363                Ok(())
364            },
365        )?;
366        Ok(())
367    }
368
369    /// Builds one contiguous chunk containing one or more encrypted frames.
370    ///
371    /// Callers compute `total_len` up front so this helper can allocate once,
372    /// append each framed ciphertext in order, and freeze the result.
373    fn build_chunk<I>(&mut self, messages: I, total_len: usize) -> Result<IoBuf, Error>
374    where
375        I: IntoIterator<Item = IoBufs>,
376    {
377        let mut chunk = self.pool.alloc(total_len);
378        for msg in messages {
379            self.append_encrypted_frame(&mut chunk, msg)?;
380        }
381        assert_eq!(chunk.len(), total_len);
382        Ok(chunk.freeze())
383    }
384
385    /// Plans `send_many` chunk boundaries without consuming cipher state.
386    ///
387    /// This validation pass ensures any oversize error is reported before
388    /// encryption advances nonces, so the sender remains usable after failure.
389    fn plan_chunks<B, I>(&self, bufs: I) -> Result<Vec<ChunkPlan>, Error>
390    where
391        B: Into<IoBufs>,
392        I: IntoIterator<Item = B>,
393    {
394        let bufs = bufs.into_iter();
395        let (lower, _) = bufs.size_hint();
396        let mut chunks = Vec::with_capacity(lower.max(1));
397        let mut batch = Vec::new();
398        let mut batch_total = 0usize;
399        let max_batch_size = self.pool.config().max_size().get();
400
401        for buf in bufs {
402            let msg = buf.into();
403            let frame_len = self.encrypted_frame_len(msg.len())?;
404
405            // If one framed message is larger than the pooled batch cap, keep
406            // current chunks intact and send that message as its own chunk.
407            if frame_len > max_batch_size {
408                if !batch.is_empty() {
409                    chunks.push(ChunkPlan {
410                        messages: std::mem::take(&mut batch),
411                        total_len: batch_total,
412                    });
413                    batch_total = 0;
414                }
415                chunks.push(ChunkPlan {
416                    messages: vec![msg],
417                    total_len: frame_len,
418                });
419                continue;
420            }
421
422            // Close the current chunk before it would exceed one network
423            // buffer-pool item.
424            if batch_total.saturating_add(frame_len) > max_batch_size {
425                chunks.push(ChunkPlan {
426                    messages: std::mem::take(&mut batch),
427                    total_len: batch_total,
428                });
429                batch_total = 0;
430            }
431
432            batch_total += frame_len;
433            batch.push(msg);
434        }
435
436        if !batch.is_empty() {
437            chunks.push(ChunkPlan {
438                messages: batch,
439                total_len: batch_total,
440            });
441        }
442
443        Ok(chunks)
444    }
445
446    /// Encrypts and sends a message to the peer.
447    ///
448    /// Allocates a buffer from the pool, copies plaintext, encrypts in-place,
449    /// and sends the ciphertext.
450    pub async fn send(&mut self, bufs: impl Into<IoBufs>) -> Result<(), Error> {
451        let bufs = bufs.into();
452        let frame_len = self.encrypted_frame_len(bufs.len())?;
453        let chunk = self.build_chunk(std::iter::once(bufs), frame_len)?;
454        self.sink.send(chunk).await.map_err(Error::SendFailed)
455    }
456
457    /// Encrypts and sends multiple messages in a single sink call.
458    ///
459    /// Each message is framed independently so receivers still observe the
460    /// original message boundaries. Aggregate writes are broken into contiguous
461    /// chunks capped to one network buffer-pool item, then submitted together as
462    /// a chunked `IoBufs`. An individual message larger than that cap is still
463    /// sent as its own chunk.
464    pub async fn send_many<B, I>(&mut self, bufs: I) -> Result<(), Error>
465    where
466        B: Into<IoBufs>,
467        I: IntoIterator<Item = B>,
468    {
469        let plans = self.plan_chunks(bufs)?;
470        if plans.is_empty() {
471            return Ok(());
472        }
473
474        let mut chunks = Vec::with_capacity(plans.len());
475        for plan in plans {
476            chunks.push(self.build_chunk(plan.messages, plan.total_len)?);
477        }
478
479        self.sink
480            .send(IoBufs::from(chunks))
481            .await
482            .map_err(Error::SendFailed)
483    }
484}
485
486/// Receives encrypted messages from a peer.
487pub struct Receiver<I> {
488    cipher: RecvCipher,
489    stream: I,
490    max_message_size: u32,
491    pool: BufferPool,
492}
493
494impl<I: Stream> Receiver<I> {
495    /// Receives and decrypts a message from the peer.
496    ///
497    /// Receives ciphertext and decrypts it in-place when the received frame is
498    /// a single, uniquely-owned buffer. Otherwise, allocates a buffer from the
499    /// pool, copies the ciphertext, and decrypts the copy in-place.
500    pub async fn recv(&mut self) -> Result<IoBufs, Error> {
501        let encrypted = recv_frame(
502            &mut self.stream,
503            self.max_message_size.saturating_add(TAG_SIZE),
504        )
505        .await?;
506
507        // Recover the received frame for in-place decryption when it is a
508        // single, uniquely-owned buffer. Otherwise, copy the ciphertext into
509        // a buffer allocated from the pool.
510        let mut decryption_buf = match encrypted
511            .try_into_single()
512            .and_then(|buf| buf.try_into_mut().map_err(IoBufs::from))
513        {
514            Ok(buf) => buf,
515            Err(mut encrypted) => {
516                let mut buf = self.pool.alloc(encrypted.len());
517                buf.put(&mut encrypted);
518                buf
519            }
520        };
521
522        // Decrypt in-place, get plaintext length back.
523        let plaintext_len = self.cipher.recv_in_place(decryption_buf.as_mut())?;
524
525        // Truncate to remove tag bytes, keeping only plaintext.
526        decryption_buf.truncate(plaintext_len);
527
528        Ok(decryption_buf.freeze().into())
529    }
530}
531
532#[cfg(test)]
533mod test {
534    use super::*;
535    use commonware_codec::varint::UInt;
536    use commonware_cryptography::{Signer, ed25519::PrivateKey};
537    use commonware_runtime::{
538        BufferPoolConfig, Error as RuntimeError, IoBuf, IoBufs, Runner as _, Spawner as _,
539        Supervisor as _, deterministic, mocks,
540    };
541    use commonware_utils::{NZU32, NZUsize, sync::Mutex};
542    use std::{
543        sync::{
544            Arc,
545            atomic::{AtomicUsize, Ordering},
546        },
547        time::Duration,
548    };
549
550    const NAMESPACE: &[u8] = b"fuzz_transport";
551    const MAX_MESSAGE_SIZE: u32 = 64 * 1024; // 64KB buffer
552
553    #[test]
554    fn test_max_message_size_bounds() {
555        assert_eq!(MAX_SIZE + TAG_SIZE, u32::MAX);
556    }
557
558    fn transport_config(signing_key: PrivateKey) -> Config<PrivateKey> {
559        Config {
560            signing_key,
561            namespace: NAMESPACE.to_vec(),
562            max_message_size: MAX_MESSAGE_SIZE,
563            synchrony_bound: Duration::from_secs(1),
564            max_handshake_age: Duration::from_secs(1),
565            handshake_timeout: Duration::from_secs(1),
566        }
567    }
568
569    fn oversized_handshake_prefix(message: &impl commonware_codec::Encode) -> IoBuf {
570        let size = u32::try_from(message.encode().len()).expect("message length should fit in u32");
571        IoBuf::from(UInt(size + 1).encode())
572    }
573
574    struct CountingSink<S> {
575        inner: S,
576        sends: Arc<AtomicUsize>,
577        chunk_counts: Arc<Mutex<Vec<usize>>>,
578    }
579
580    impl<S> CountingSink<S> {
581        fn new(inner: S, sends: Arc<AtomicUsize>, chunk_counts: Arc<Mutex<Vec<usize>>>) -> Self {
582            Self {
583                inner,
584                sends,
585                chunk_counts,
586            }
587        }
588    }
589
590    impl<S: commonware_runtime::Sink> commonware_runtime::Sink for CountingSink<S> {
591        async fn send(&mut self, bufs: impl Into<IoBufs> + Send) -> Result<(), RuntimeError> {
592            let bufs = bufs.into();
593            self.sends.fetch_add(1, Ordering::Relaxed);
594            self.chunk_counts.lock().push(bufs.chunk_count());
595            self.inner.send(bufs).await
596        }
597    }
598
599    /// Wraps a stream to return each read as a fresh, uniquely-owned pooled
600    /// buffer, mirroring the production network backends.
601    ///
602    /// Records the allocation handed out by the most recent read so tests can
603    /// assert that decryption happened in place.
604    struct PoolingStream<S> {
605        inner: S,
606        pool: BufferPool,
607        last_alloc: Arc<Mutex<Range<usize>>>,
608    }
609
610    impl<S: commonware_runtime::Stream> commonware_runtime::Stream for PoolingStream<S> {
611        async fn recv(&mut self, len: usize) -> Result<IoBufs, RuntimeError> {
612            let mut bufs = self.inner.recv(len).await?;
613            let mut buf = self.pool.alloc(len);
614            buf.put(&mut bufs);
615            let buf = buf.freeze();
616            let start = buf.as_ref().as_ptr() as usize;
617            *self.last_alloc.lock() = start..start + buf.len();
618            Ok(buf.into())
619        }
620
621        fn peek(&self, max_len: usize) -> &[u8] {
622            self.inner.peek(max_len)
623        }
624    }
625
626    #[test]
627    fn test_can_setup_and_send_messages() -> Result<(), Error> {
628        let executor = deterministic::Runner::default();
629        executor.start(|context| async move {
630            let dialer_crypto = PrivateKey::from_seed(42);
631            let listener_crypto = PrivateKey::from_seed(24);
632
633            let (dialer_sink, listener_stream) = mocks::Channel::init();
634            let (listener_sink, dialer_stream) = mocks::Channel::init();
635
636            let dialer_config = transport_config(dialer_crypto.clone());
637            let listener_config = transport_config(listener_crypto.clone());
638
639            let listener_handle = context.child("listener").spawn(move |context| async move {
640                listen(
641                    context,
642                    |_| async { true },
643                    listener_config,
644                    listener_stream,
645                    listener_sink,
646                )
647                .await
648            });
649
650            let (mut dialer_sender, mut dialer_receiver) = dial(
651                context,
652                dialer_config,
653                listener_crypto.public_key(),
654                dialer_stream,
655                dialer_sink,
656            )
657            .await?;
658
659            let (listener_peer, mut listener_sender, mut listener_receiver) =
660                listener_handle.await.unwrap()?;
661            assert_eq!(listener_peer, dialer_crypto.public_key());
662            let messages: Vec<&'static [u8]> = vec![b"A", b"B", b"C"];
663            for msg in &messages {
664                dialer_sender.send(&msg[..]).await?;
665                let syn_ack = listener_receiver.recv().await?;
666                assert_eq!(syn_ack.coalesce(), *msg);
667                listener_sender.send(&msg[..]).await?;
668                let ack = dialer_receiver.recv().await?;
669                assert_eq!(ack.coalesce(), *msg);
670            }
671            Ok(())
672        })
673    }
674
675    #[test]
676    fn test_recv_decrypts_unique_frame_in_place() -> Result<(), Error> {
677        let executor = deterministic::Runner::default();
678        executor.start(|context| async move {
679            let dialer_crypto = PrivateKey::from_seed(42);
680            let listener_crypto = PrivateKey::from_seed(24);
681
682            let (dialer_sink, listener_stream) = mocks::Channel::init();
683            let (listener_sink, dialer_stream) = mocks::Channel::init();
684
685            let last_alloc = Arc::new(Mutex::new(0..0));
686            let listener_stream = PoolingStream {
687                inner: listener_stream,
688                pool: context.network_buffer_pool().clone(),
689                last_alloc: last_alloc.clone(),
690            };
691
692            let dialer_config = transport_config(dialer_crypto);
693            let listener_config = transport_config(listener_crypto.clone());
694
695            let listener_handle = context.child("listener").spawn(move |context| async move {
696                listen(
697                    context,
698                    |_| async { true },
699                    listener_config,
700                    listener_stream,
701                    listener_sink,
702                )
703                .await
704            });
705
706            let (mut dialer_sender, _dialer_receiver) = dial(
707                context,
708                dialer_config,
709                listener_crypto.public_key(),
710                dialer_stream,
711                dialer_sink,
712            )
713            .await?;
714
715            let (_, _, mut listener_receiver) = listener_handle.await.unwrap()?;
716
717            // Send both messages before receiving so the second frame's varint
718            // is decoded from the peek buffer, exercising in-place decryption
719            // of a sliced frame in addition to a full one.
720            dialer_sender.send(&b"hello"[..]).await?;
721            dialer_sender.send(&b"world"[..]).await?;
722
723            for expected in [&b"hello"[..], &b"world"[..]] {
724                let received = listener_receiver.recv().await?;
725                let plaintext = received.as_single().expect("single buffer expected");
726                let ptr = plaintext.as_ref().as_ptr() as usize;
727                assert!(
728                    last_alloc.lock().contains(&ptr),
729                    "plaintext should reuse the received frame buffer"
730                );
731                assert_eq!(plaintext.as_ref(), expected);
732            }
733            Ok(())
734        })
735    }
736
737    #[test]
738    fn test_send_many_uses_single_runtime_send() -> Result<(), Error> {
739        let executor = deterministic::Runner::default();
740        executor.start(|context| async move {
741            let dialer_crypto = PrivateKey::from_seed(42);
742            let listener_crypto = PrivateKey::from_seed(24);
743
744            let (dialer_sink, listener_stream) = mocks::Channel::init();
745            let (listener_sink, dialer_stream) = mocks::Channel::init();
746            let sends = Arc::new(AtomicUsize::new(0));
747            let chunk_counts = Arc::new(Mutex::new(Vec::new()));
748
749            let dialer_config = transport_config(dialer_crypto.clone());
750            let listener_config = transport_config(listener_crypto.clone());
751
752            let listener_handle = context.child("listener").spawn(move |context| async move {
753                listen(
754                    context,
755                    |_| async { true },
756                    listener_config,
757                    listener_stream,
758                    listener_sink,
759                )
760                .await
761            });
762
763            let (mut dialer_sender, _dialer_receiver) = dial(
764                context,
765                dialer_config,
766                listener_crypto.public_key(),
767                dialer_stream,
768                CountingSink::new(dialer_sink, sends.clone(), chunk_counts.clone()),
769            )
770            .await?;
771
772            let (_listener_peer, _listener_sender, mut listener_receiver) =
773                listener_handle.await.unwrap()?;
774            sends.store(0, Ordering::Relaxed);
775            chunk_counts.lock().clear();
776
777            // Three small messages should fit in one pooled chunk, so `send_many`
778            // still reaches the runtime as a single single-chunk send call.
779            dialer_sender
780                .send_many(vec![
781                    IoBufs::from(IoBuf::from(b"alpha")),
782                    IoBufs::from(IoBuf::from(b"beta")),
783                    IoBufs::from(IoBuf::from(b"gamma")),
784                ])
785                .await?;
786
787            assert_eq!(sends.load(Ordering::Relaxed), 1);
788            assert_eq!(*chunk_counts.lock(), vec![1]);
789            assert_eq!(
790                listener_receiver.recv().await?.coalesce(),
791                IoBuf::from(b"alpha")
792            );
793            assert_eq!(
794                listener_receiver.recv().await?.coalesce(),
795                IoBuf::from(b"beta")
796            );
797            assert_eq!(
798                listener_receiver.recv().await?.coalesce(),
799                IoBuf::from(b"gamma")
800            );
801            Ok(())
802        })
803    }
804
805    #[test]
806    fn test_send_many_flushes_at_network_pool_item_max() -> Result<(), Error> {
807        let executor = deterministic::Runner::new(
808            deterministic::Config::new().with_network_buffer_pool_config(
809                BufferPoolConfig::for_network()
810                    .with_pool_min_size(256)
811                    .with_size_class_range(NZUsize!(256), NZUsize!(256), NZU32!(4096)),
812            ),
813        );
814        executor.start(|context| async move {
815            let dialer_crypto = PrivateKey::from_seed(42);
816            let listener_crypto = PrivateKey::from_seed(24);
817
818            let (dialer_sink, listener_stream) = mocks::Channel::init();
819            let (listener_sink, dialer_stream) = mocks::Channel::init();
820            let sends = Arc::new(AtomicUsize::new(0));
821            let chunk_counts = Arc::new(Mutex::new(Vec::new()));
822
823            let dialer_config = transport_config(dialer_crypto.clone());
824            let listener_config = transport_config(listener_crypto.clone());
825
826            let listener_handle = context.child("listener").spawn(move |context| async move {
827                listen(
828                    context,
829                    |_| async { true },
830                    listener_config,
831                    listener_stream,
832                    listener_sink,
833                )
834                .await
835            });
836
837            let (mut dialer_sender, _dialer_receiver) = dial(
838                context,
839                dialer_config,
840                listener_crypto.public_key(),
841                dialer_stream,
842                CountingSink::new(dialer_sink, sends.clone(), chunk_counts.clone()),
843            )
844            .await?;
845
846            let (_listener_peer, _listener_sender, mut listener_receiver) =
847                listener_handle.await.unwrap()?;
848            sends.store(0, Ordering::Relaxed);
849            chunk_counts.lock().clear();
850
851            // The first two framed messages fit together under the 256-byte cap,
852            // but the third must spill into a second chunk. We still hand the
853            // runtime one chunked `IoBufs`, so there is only one sink call.
854            let payload = vec![7u8; 100];
855            dialer_sender
856                .send_many(vec![
857                    IoBufs::from(IoBuf::from(payload.clone())),
858                    IoBufs::from(IoBuf::from(payload.clone())),
859                    IoBufs::from(IoBuf::from(payload.clone())),
860                ])
861                .await?;
862
863            assert_eq!(sends.load(Ordering::Relaxed), 1);
864            assert_eq!(*chunk_counts.lock(), vec![2]);
865            for _ in 0..3 {
866                assert_eq!(
867                    listener_receiver.recv().await?.coalesce(),
868                    payload.as_slice()
869                );
870            }
871            Ok(())
872        })
873    }
874
875    #[test]
876    fn test_send_many_sends_oversized_single_message_alone() -> Result<(), Error> {
877        let executor = deterministic::Runner::new(
878            deterministic::Config::new().with_network_buffer_pool_config(
879                BufferPoolConfig::for_network()
880                    .with_pool_min_size(128)
881                    .with_size_class_range(NZUsize!(128), NZUsize!(128), NZU32!(4096)),
882            ),
883        );
884        executor.start(|context| async move {
885            let dialer_crypto = PrivateKey::from_seed(42);
886            let listener_crypto = PrivateKey::from_seed(24);
887
888            let (dialer_sink, listener_stream) = mocks::Channel::init();
889            let (listener_sink, dialer_stream) = mocks::Channel::init();
890            let sends = Arc::new(AtomicUsize::new(0));
891            let chunk_counts = Arc::new(Mutex::new(Vec::new()));
892
893            let dialer_config = transport_config(dialer_crypto.clone());
894            let listener_config = transport_config(listener_crypto.clone());
895
896            let listener_handle = context.child("listener").spawn(move |context| async move {
897                listen(
898                    context,
899                    |_| async { true },
900                    listener_config,
901                    listener_stream,
902                    listener_sink,
903                )
904                .await
905            });
906
907            let (mut dialer_sender, _dialer_receiver) = dial(
908                context,
909                dialer_config,
910                listener_crypto.public_key(),
911                dialer_stream,
912                CountingSink::new(dialer_sink, sends.clone(), chunk_counts.clone()),
913            )
914            .await?;
915
916            let (_listener_peer, _listener_sender, mut listener_receiver) =
917                listener_handle.await.unwrap()?;
918            sends.store(0, Ordering::Relaxed);
919            chunk_counts.lock().clear();
920
921            // A single framed message larger than the cap still goes out, but it
922            // must occupy its own chunk instead of being rejected or merged.
923            let large = vec![3u8; 200];
924            let small = vec![9u8; 16];
925            dialer_sender
926                .send_many(vec![
927                    IoBufs::from(IoBuf::from(large.clone())),
928                    IoBufs::from(IoBuf::from(small.clone())),
929                ])
930                .await?;
931
932            assert_eq!(sends.load(Ordering::Relaxed), 1);
933            assert_eq!(*chunk_counts.lock(), vec![2]);
934            assert_eq!(listener_receiver.recv().await?.coalesce(), large.as_slice());
935            assert_eq!(listener_receiver.recv().await?.coalesce(), small.as_slice());
936            Ok(())
937        })
938    }
939
940    #[test]
941    fn test_send_many_too_large_preserves_sender_state() -> Result<(), Error> {
942        let executor = deterministic::Runner::default();
943        executor.start(|context| async move {
944            let dialer_crypto = PrivateKey::from_seed(42);
945            let listener_crypto = PrivateKey::from_seed(24);
946
947            let (dialer_sink, listener_stream) = mocks::Channel::init();
948            let (listener_sink, dialer_stream) = mocks::Channel::init();
949            let sends = Arc::new(AtomicUsize::new(0));
950            let chunk_counts = Arc::new(Mutex::new(Vec::new()));
951
952            let dialer_config = transport_config(dialer_crypto.clone());
953            let listener_config = transport_config(listener_crypto.clone());
954
955            let listener_handle = context.child("listener").spawn(move |context| async move {
956                listen(
957                    context,
958                    |_| async { true },
959                    listener_config,
960                    listener_stream,
961                    listener_sink,
962                )
963                .await
964            });
965
966            let (mut dialer_sender, _dialer_receiver) = dial(
967                context,
968                dialer_config,
969                listener_crypto.public_key(),
970                dialer_stream,
971                CountingSink::new(dialer_sink, sends.clone(), chunk_counts.clone()),
972            )
973            .await?;
974
975            let (_listener_peer, _listener_sender, mut listener_receiver) =
976                listener_handle.await.unwrap()?;
977            sends.store(0, Ordering::Relaxed);
978            chunk_counts.lock().clear();
979
980            let valid = vec![7u8; 32];
981            let oversized = vec![9u8; MAX_MESSAGE_SIZE as usize + 1];
982            assert!(matches!(
983                dialer_sender
984                    .send_many(vec![
985                        IoBufs::from(IoBuf::from(valid)),
986                        IoBufs::from(IoBuf::from(oversized)),
987                    ])
988                    .await,
989                Err(Error::SendTooLarge(_))
990            ));
991
992            assert_eq!(sends.load(Ordering::Relaxed), 0);
993            assert!(chunk_counts.lock().is_empty());
994
995            let recovered = b"recovered";
996            dialer_sender.send(&recovered[..]).await?;
997            assert_eq!(sends.load(Ordering::Relaxed), 1);
998            assert_eq!(listener_receiver.recv().await?.coalesce(), recovered);
999            Ok(())
1000        })
1001    }
1002
1003    #[test]
1004    fn test_listen_rejects_oversized_fixed_size_peer_key_frame() {
1005        let executor = deterministic::Runner::default();
1006        executor.start(|context| async move {
1007            let dialer_crypto = PrivateKey::from_seed(42);
1008            let listener_crypto = PrivateKey::from_seed(24);
1009            let peer = dialer_crypto.public_key();
1010
1011            let (mut dialer_sink, listener_stream) = mocks::Channel::init();
1012            let (listener_sink, _dialer_stream) = mocks::Channel::init();
1013
1014            // Even with a large application limit, the listener should bound the
1015            // unauthenticated peer-key frame to the fixed public-key size.
1016            let mut listener_config = transport_config(listener_crypto);
1017            listener_config.max_message_size = 1024 * 1024;
1018
1019            // Advertise a frame that is one byte larger than the encoded public
1020            // key and send no payload. The old behavior accepted this because it
1021            // only compared against `max_message_size`.
1022            dialer_sink
1023                .send(oversized_handshake_prefix(&peer))
1024                .await
1025                .unwrap();
1026
1027            let result = listen(
1028                context,
1029                |_| async { true },
1030                listener_config,
1031                listener_stream,
1032                listener_sink,
1033            )
1034            .await;
1035
1036            // The listener should reject immediately on the fixed-size bound
1037            // instead of waiting for more bytes or allocating for the larger
1038            // application limit.
1039            assert!(matches!(result, Err(Error::RecvTooLarge(n)) if n == peer.encode().len() + 1));
1040        });
1041    }
1042
1043    #[test]
1044    fn test_dial_rejects_oversized_fixed_size_syn_ack_frame() {
1045        let executor = deterministic::Runner::default();
1046        executor.start(|context| async move {
1047            let dialer_crypto = PrivateKey::from_seed(42);
1048            let listener_crypto = PrivateKey::from_seed(24);
1049
1050            let (dialer_sink, _listener_stream) = mocks::Channel::init();
1051            let (mut listener_sink, dialer_stream) = mocks::Channel::init();
1052
1053            // Use a large application limit to make sure this path is guarded by
1054            // the fixed SynAck size rather than by post-handshake settings.
1055            let mut dialer_config = transport_config(dialer_crypto);
1056            dialer_config.max_message_size = 1024 * 1024;
1057
1058            // Build a valid SynAck only to derive its true encoded size for the
1059            // oversized prefix we inject below.
1060            let (current_time, ok_timestamps) = dialer_config.time_information(&context);
1061            let listener_public_key = listener_crypto.public_key();
1062            let dialer_public_key = dialer_config.signing_key.public_key();
1063            let (_, syn) = dial_start(
1064                context.child("dialer"),
1065                Context::new(
1066                    &dialer_config.namespace,
1067                    current_time,
1068                    ok_timestamps.clone(),
1069                    dialer_config.signing_key.clone(),
1070                    listener_public_key.clone(),
1071                ),
1072            );
1073            let (_, syn_ack) = listen_start(
1074                context.child("listener"),
1075                Context::new(
1076                    &dialer_config.namespace,
1077                    current_time,
1078                    ok_timestamps,
1079                    listener_crypto,
1080                    dialer_public_key,
1081                ),
1082                syn,
1083            )
1084            .expect("mock handshake should produce a valid syn_ack");
1085
1086            // Send only a length prefix that claims a frame one byte larger than
1087            // the fixed SynAck encoding.
1088            listener_sink
1089                .send(oversized_handshake_prefix(&syn_ack))
1090                .await
1091                .unwrap();
1092
1093            let result = dial(
1094                context,
1095                dialer_config,
1096                listener_public_key,
1097                dialer_stream,
1098                dialer_sink,
1099            )
1100            .await;
1101
1102            // The dialer should reject on the fixed handshake bound before any
1103            // larger application-sized receive path is considered.
1104            assert!(matches!(
1105                result,
1106                Err(Error::RecvTooLarge(n))
1107                    if n == syn_ack.encode().len() + 1
1108            ));
1109        });
1110    }
1111}