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