Skip to main content

darkbio_wire/transport/
sender.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3
4//! Handles bound to individual transport sessions. Neither an idle sender nor
5//! any of its clones keeps the session or the byte stream alive.
6
7use super::Write;
8use super::outbound::Outbound;
9use super::{Error, sealing};
10use crate::LogId;
11use darkbio_crypto::xhpke;
12use std::fmt;
13use std::sync::{Mutex, Weak};
14use tracing::{debug, warn};
15
16/// Cloneable handle for sending messages into a session from any thread.
17/// Returned by [`Client::connect`](super::Client::connect) or delivered in a
18/// server's [`Connected`](super::Event::Connected) event.
19///
20/// Sends share one encryption sequence and are written in that order. Each send
21/// waits for its own frame and receives its own result. A handle belongs
22/// permanently to the session that issued it. Session failure, disconnect,
23/// reconnect or dropping the client/server invalidates all that session's handles.
24/// A new session supplies a new sender. Old handles cannot send into it.
25///
26/// The client/server retains the session and stream. Idle senders hold only weak
27/// references and do not extend either lifetime. An active send temporarily
28/// retains both, but can write only while its session remains current.
29/// Dropping a sender does not end the session.
30pub struct Sender<W: Write> {
31    outbound: Weak<Outbound<W>>, // Writer retained by the client/server and active sends
32    sealer: Weak<Mutex<xhpke::Sender>>, // Encryption context whose allocation identifies the session
33    log_id: LogId,                      // Label of the session in log lines
34}
35
36impl<W: Write> Sender<W> {
37    /// Stores weak references to the writer and encryption allocation. Each
38    /// active send temporarily retains both. An idle handle owns neither.
39    pub(super) fn new(
40        outbound: Weak<Outbound<W>>,
41        sealer: Weak<Mutex<xhpke::Sender>>,
42        log_id: LogId,
43    ) -> Self {
44        Self {
45            outbound,
46            sealer,
47            log_id,
48        }
49    }
50
51    /// Label of the session this sender belongs to, for log lines.
52    pub(crate) fn log_id(&self) -> LogId {
53        self.log_id
54    }
55
56    /// Encrypts a message, writes its complete frame and flushes the output.
57    /// Concurrent sends preserve encryption order on the wire. The next message
58    /// can be encrypted while the previous one is being written. An oversized
59    /// message is refused without advancing encryption or ending the session.
60    ///
61    /// The write timeout starts after acquiring the writer. Frame encoding,
62    /// recovery delimiters, partial writes and flush all share that budget.
63    /// Encryption and waiting for the writer are outside the budget.
64    ///
65    /// An output failure ends the session before returning. Subsequent sends and
66    /// receive completions are refused. A timeout returns [`Error::SendFailed`]
67    /// containing an I/O `TimedOut` error and leaves the byte stream reusable.
68    /// Server failure notification uses the frame's remaining budget and is
69    /// skipped after timeout. Ending this way does not wake a blocked receive.
70    ///
71    /// Ending from another thread waits for a send holding the writer lock.
72    /// Queued sends can still encrypt, but must belong to the current session
73    /// when they acquire the writer.
74    ///
75    /// Returns [`Error::Terminated`] if the outgoing transport was released.
76    /// Returns [`Error::EncryptionFailed`] if the context was released or its
77    /// session ended. Permanent stream closure is observed through I/O, so an
78    /// overlapping send may succeed. Unexpected encryption failures and poisoned
79    /// locks panic. Transport reuse after a panic is unsupported.
80    pub fn send(&self, message: &[u8]) -> Result<(), Error> {
81        let Some(outbound) = self.outbound.upgrade() else {
82            debug!("wire send refused, transport released");
83            return Err(Error::Terminated);
84        };
85        let Some(context) = self.sealer.upgrade() else {
86            debug!("wire send refused, session {} ended", self.log_id);
87            return Err(Error::EncryptionFailed("session ended".into()));
88        };
89        let mut sealer = context.lock().expect("encryption lock not poisoned");
90        let packet = match sealing::seal(&mut sealer, message) {
91            Ok(packet) => packet,
92            Err(Error::PacketTooLarge(size)) => {
93                warn!("wire message of {} bytes exceeds the sending limit", size);
94                return outbound.refuse_oversized(&context, size);
95            }
96            Err(err) => panic!("message encryption failed: {err}"),
97        };
98        // Acquire the writer before releasing encryption, keeping wire order
99        // equal to sealing order while the next message seals during this I/O.
100        let mut writer = outbound.lock();
101        drop(sealer);
102        let result = writer.send(&context, &packet, self.log_id);
103        if let Err(Error::EncryptionFailed(_)) = &result {
104            debug!("wire send refused, session {} ended", self.log_id);
105        }
106        result
107    }
108
109    /// Ends this sender's session. On the server, also sends Dropped under the
110    /// writer lock. Does nothing if the session has ended or been replaced.
111    /// May wait for an active write, so the protocol closes its local queues and
112    /// promises first, then calls this from its writer thread.
113    pub(crate) fn disconnect(&self) -> Result<(), Error> {
114        if let (Some(outbound), Some(context)) = (self.outbound.upgrade(), self.sealer.upgrade()) {
115            outbound.disconnect(&context)?;
116        }
117        Ok(())
118    }
119}
120
121impl<W: Write> Clone for Sender<W> {
122    fn clone(&self) -> Self {
123        Self::new(self.outbound.clone(), self.sealer.clone(), self.log_id)
124    }
125}
126
127impl<W: Write> fmt::Debug for Sender<W> {
128    /// Shows the session label and whether the session still accepts sends.
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.debug_struct("Sender")
131            .field("session", &self.log_id)
132            .field("valid", &(self.sealer.strong_count() > 0))
133            .finish()
134    }
135}
136
137#[cfg(test)]
138#[cfg_attr(coverage_nightly, coverage(off))]
139mod tests {
140    use super::*;
141    use crate::testing;
142    use crate::transport::Closer;
143    use crate::transport::DEFAULT_WRITE_TIMEOUT;
144    use crate::transport::framing::FrameReader;
145    use crate::transport::mock::payload;
146    use crate::transport::outbound::Side;
147    use crate::transport::testing::Memory;
148    use std::io;
149    use std::panic::{self, AssertUnwindSafe};
150    use std::sync::{Arc, TryLockError, mpsc};
151    use std::thread;
152    use std::time::{Duration, Instant};
153
154    /// Retains a sending context as a client/server would and binds a sender to it.
155    fn connect<W: Write>(
156        outbound: &Arc<Outbound<W>>,
157        sender: xhpke::Sender,
158    ) -> (Arc<Mutex<xhpke::Sender>>, Sender<W>) {
159        let sealer = Arc::new(Mutex::new(sender));
160        let sender = outbound.bind(&sealer);
161        (sealer, sender)
162    }
163
164    /// Waits until a sender acquires encryption while the writer is held by
165    /// the test or an earlier send. No sleep determines the ordering.
166    fn wait_sealing(sealer: &Mutex<xhpke::Sender>) {
167        let deadline = Instant::now() + Duration::from_secs(5);
168        while !matches!(sealer.try_lock(), Err(TryLockError::WouldBlock)) {
169            assert!(
170                Instant::now() < deadline,
171                "sender did not acquire encryption context"
172            );
173            thread::yield_now();
174        }
175    }
176
177    /// A pair of contexts standing in for an established session.
178    fn contexts() -> (xhpke::Sender, xhpke::Receiver) {
179        let secret = xhpke::SecretKey::generate();
180        let (sender, encap) = secret.public_key().new_sender(b"test").unwrap();
181        let receiver = secret.new_receiver(&encap, b"test").unwrap();
182        (sender, receiver)
183    }
184
185    /// Writer collecting everything written into a shared buffer.
186    #[derive(Clone, Default)]
187    struct Collector(Arc<Mutex<Vec<u8>>>);
188
189    impl Write for Collector {
190        fn set_write_deadline(&mut self, deadline: Instant) -> io::Result<()> {
191            testing::remaining(deadline)?;
192            Ok(())
193        }
194    }
195
196    impl io::Write for Collector {
197        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
198            self.0.lock().unwrap().extend_from_slice(buf);
199            Ok(buf.len())
200        }
201
202        fn flush(&mut self) -> io::Result<()> {
203            Ok(())
204        }
205    }
206
207    /// Holds its first write until released, then fails or panics as configured.
208    /// Later writes succeed. Dropping the writer notifies the test driver.
209    struct Gate {
210        entered: mpsc::Sender<()>,
211        release: Option<mpsc::Receiver<()>>,
212        dropped: mpsc::Sender<()>,
213        panics: bool,
214        deadline: Option<Instant>,
215    }
216
217    impl Gate {
218        /// Creates the gate and channels to observe a blocked write, release it
219        /// and observe the writer's drop.
220        fn new() -> (
221            Self,
222            mpsc::Receiver<()>,
223            mpsc::Sender<()>,
224            mpsc::Receiver<()>,
225        ) {
226            let (entered_tx, entered) = mpsc::channel();
227            let (release, release_rx) = mpsc::channel();
228            let (dropped_tx, dropped) = mpsc::channel();
229            let gate = Self {
230                entered: entered_tx,
231                release: Some(release_rx),
232                dropped: dropped_tx,
233                panics: false,
234                deadline: None,
235            };
236            (gate, entered, release, dropped)
237        }
238    }
239
240    impl Write for Gate {
241        fn set_write_deadline(&mut self, deadline: Instant) -> io::Result<()> {
242            self.deadline = Some(deadline);
243            Ok(())
244        }
245    }
246
247    impl io::Write for Gate {
248        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
249            let deadline = self.deadline.expect("write deadline installed");
250            testing::remaining(deadline)?;
251            match self.release.take() {
252                Some(release) => {
253                    let _ = self.entered.send(());
254                    release
255                        .recv_timeout(testing::remaining(deadline)?)
256                        .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))?;
257                    if self.panics {
258                        panic!("injected panic");
259                    }
260                    Err(io::Error::other("gate closed"))
261                }
262                None => Ok(buf.len()),
263            }
264        }
265
266        fn flush(&mut self) -> io::Result<()> {
267            testing::remaining(self.deadline.expect("write deadline installed"))?;
268            Ok(())
269        }
270    }
271
272    impl Drop for Gate {
273        fn drop(&mut self) {
274            let _ = self.dropped.send(());
275        }
276    }
277
278    // Tests that concurrent messages go out in encryption order. The receiver
279    // must decrypt every frame in sequence and recover every submitted message.
280    #[test]
281    fn test_send_order() {
282        testing::init_tracing();
283
284        let (sender, mut receiver) = contexts();
285        let collector = Collector::default();
286        let outbound = Arc::new(Outbound::new(
287            collector.clone(),
288            Side::Client,
289            Closer::new(|| {}),
290            DEFAULT_WRITE_TIMEOUT,
291        ));
292        let (_sealer, sender) = connect(&outbound, sender);
293
294        let threads: Vec<_> = (0..8)
295            .map(|thread| {
296                let sender = sender.clone();
297                thread::spawn(move || {
298                    for i in 0..20 {
299                        sender.send(&payload(thread * 100 + i)).unwrap();
300                    }
301                })
302            })
303            .collect();
304        for thread in threads {
305            thread.join().unwrap();
306        }
307
308        // Every frame must open in the order written, or the sequence is off
309        let written = collector.0.lock().unwrap().clone();
310        let mut reader = FrameReader::new(Memory::new(&written[..]), Closer::new(|| {}));
311        let mut messages = Vec::new();
312        loop {
313            let packet = match reader.next_packet(None) {
314                Err(Error::Terminated) => break,
315                result => result.unwrap().unwrap(),
316            };
317            messages.push(sealing::open(&mut receiver, packet).unwrap());
318        }
319        messages.sort_unstable();
320        let mut expected: Vec<Vec<u8>> = (0..8)
321            .flat_map(|thread| (0..20).map(move |i| payload(thread * 100 + i)))
322            .collect();
323        expected.sort_unstable();
324        assert_eq!(messages, expected);
325    }
326
327    // Tests that a send holding encryption observes termination when it acquires
328    // the writer lock. The old send races a replacement for that lock; only the
329    // replacement's frame may reach the byte stream.
330    #[test]
331    fn test_end_with_queued_send() {
332        testing::init_tracing();
333
334        let collector = Collector::default();
335        let outbound = Arc::new(Outbound::new(
336            collector.clone(),
337            Side::Client,
338            Closer::new(|| {}),
339            DEFAULT_WRITE_TIMEOUT,
340        ));
341        let (crypto, _) = contexts();
342        let (sealer, sender) = connect(&outbound, crypto);
343        let mut writer = outbound.lock();
344        let sending = thread::spawn(move || sender.send(&payload(1)));
345
346        wait_sealing(&sealer);
347        assert!(writer.end(&sealer));
348        drop(sealer);
349        drop(writer);
350
351        let (crypto, mut peer) = contexts();
352        let (_replacement, fresh) = connect(&outbound, crypto);
353        fresh.send(&payload(2)).unwrap();
354        assert!(matches!(
355            sending.join().unwrap(),
356            Err(Error::EncryptionFailed(_))
357        ));
358
359        let bytes = collector.0.lock().unwrap().clone();
360        let mut reader = FrameReader::new(Memory::new(&bytes[..]), Closer::new(|| {}));
361        let packet = reader.next_packet(None).unwrap().unwrap();
362        assert_eq!(sealing::open(&mut peer, packet).unwrap(), payload(2));
363        assert!(matches!(reader.next_packet(None), Err(Error::Terminated)));
364    }
365
366    // Tests that a send receives its own write failure. The message encrypted
367    // behind it must be refused before writing. Later sends must also fail,
368    // even if they perform extra encryption before finding the ended session.
369    #[test]
370    fn test_send_failure_attribution() {
371        testing::init_tracing();
372
373        let (gate, entered, release, _) = Gate::new();
374        let (sender, _) = contexts();
375        let outbound = Arc::new(Outbound::new(
376            gate,
377            Side::Client,
378            Closer::new(|| {}),
379            DEFAULT_WRITE_TIMEOUT,
380        ));
381        let (sealer, sender) = connect(&outbound, sender);
382
383        // The first sender blocks inside its write, the second seals behind it
384        // and waits for the write lock while retaining the encryption lock.
385        let first = {
386            let sender = sender.clone();
387            thread::spawn(move || sender.send(&payload(1)))
388        };
389        entered.recv_timeout(Duration::from_secs(5)).unwrap();
390        // The first write must leave encryption available for the next message.
391        drop(sealer.try_lock().expect("encryption held during writing"));
392        let second = {
393            let sender = sender.clone();
394            thread::spawn(move || sender.send(&payload(2)))
395        };
396        wait_sealing(&sealer);
397
398        // The write fails, taking the session with it
399        release.send(()).unwrap();
400        let result = first.join().unwrap();
401        assert!(matches!(result, Err(Error::SendFailed(_))), "{result:?}");
402        let result = second.join().unwrap();
403        assert!(
404            matches!(result, Err(Error::EncryptionFailed(_))),
405            "{result:?}"
406        );
407        let result = sender.send(&payload(3));
408        assert!(
409            matches!(result, Err(Error::EncryptionFailed(_))),
410            "{result:?}"
411        );
412        assert!(outbound.finish_receive(&sealer, Ok(Vec::new())).is_err());
413    }
414
415    // Tests that senders stay bound to their original session after replacement
416    // or ending. Closing the stream is observed through I/O. Dropping its owner
417    // makes later sends return Terminated.
418    #[test]
419    fn test_send_refusals() {
420        testing::init_tracing();
421
422        let outbound = Arc::new(Outbound::new(
423            Memory::new(Vec::new()),
424            Side::Client,
425            Closer::new(|| {}),
426            DEFAULT_WRITE_TIMEOUT,
427        ));
428        let (crypto, _) = contexts();
429        let (first_sealer, first) = connect(&outbound, crypto);
430        first.send(&payload(1)).unwrap();
431
432        outbound.end(&first_sealer);
433        assert!(matches!(
434            first.send(&payload(2)),
435            Err(Error::EncryptionFailed(_))
436        ));
437
438        let (crypto, _) = contexts();
439        let (second_sealer, second) = connect(&outbound, crypto);
440        assert!(matches!(
441            first.send(&payload(3)),
442            Err(Error::EncryptionFailed(_))
443        ));
444        drop(first_sealer);
445        assert!(matches!(
446            first.send(&payload(4)),
447            Err(Error::EncryptionFailed(_))
448        ));
449        second.send(&payload(5)).unwrap();
450
451        // Closure leaves logical termination to the operation's I/O result.
452        outbound.close();
453        outbound
454            .finish_receive(&second_sealer, Ok(Vec::new()))
455            .unwrap();
456        let result = second.send(&payload(6));
457        assert!(
458            matches!(&result, Err(Error::SendFailed(err)) if err.kind() == io::ErrorKind::NotConnected),
459            "{result:?}"
460        );
461        assert!(
462            outbound
463                .finish_receive(&second_sealer, Ok(Vec::new()))
464                .is_err()
465        );
466        drop(outbound);
467        assert!(matches!(second.send(&payload(7)), Err(Error::Terminated)));
468    }
469
470    // Tests close while a send blocks in I/O, another waits with encryption
471    // locked, and session ending also waits for the writer. Close must release
472    // the blocked I/O without taking those locks. Both sends and ending then
473    // finish, and a surviving sender refuses new messages.
474    #[test]
475    fn test_close_with_stuck_sends() {
476        testing::init_tracing();
477
478        let (gate, entered, release, dropped) = Gate::new();
479        let (sender, _) = contexts();
480        let closer = Closer::new(move || {
481            let _ = release.send(());
482        });
483        let outbound = Arc::new(Outbound::new(
484            gate,
485            Side::Client,
486            closer,
487            DEFAULT_WRITE_TIMEOUT,
488        ));
489        let (sealer, sender) = connect(&outbound, sender);
490
491        let first = {
492            let sender = sender.clone();
493            thread::spawn(move || sender.send(&payload(1)))
494        };
495        entered.recv_timeout(Duration::from_secs(5)).unwrap();
496        let second = {
497            let sender = sender.clone();
498            thread::spawn(move || sender.send(&payload(2)))
499        };
500        wait_sealing(&sealer);
501
502        let (ending_tx, started) = mpsc::channel();
503        let ending = {
504            let outbound = outbound.clone();
505            let sealer = sealer.clone();
506            thread::spawn(move || {
507                ending_tx.send(()).unwrap();
508                outbound.end(&sealer);
509            })
510        };
511        started.recv_timeout(Duration::from_secs(5)).unwrap();
512
513        let (closed_tx, closed) = mpsc::channel();
514        let owner = {
515            let outbound = outbound.clone();
516            thread::spawn(move || {
517                outbound.close();
518                closed_tx.send(()).unwrap();
519            })
520        };
521        closed.recv_timeout(Duration::from_secs(5)).unwrap();
522        owner.join().unwrap();
523        ending.join().unwrap();
524        assert!(matches!(first.join().unwrap(), Err(Error::SendFailed(_))));
525        assert!(matches!(
526            second.join().unwrap(),
527            Err(Error::EncryptionFailed(_))
528        ));
529        assert!(outbound.finish_receive(&sealer, Ok(Vec::new())).is_err());
530        assert!(matches!(
531            sender.send(&payload(3)),
532            Err(Error::EncryptionFailed(_))
533        ));
534        drop(outbound);
535        dropped.recv_timeout(Duration::from_secs(5)).unwrap();
536        assert!(matches!(sender.send(&payload(4)), Err(Error::Terminated)));
537    }
538
539    // Tests that an I/O panic releases its active-operation count. Shutdown can
540    // then complete, and the last active send releases the transport writer.
541    #[test]
542    fn test_close_with_panicking_send() {
543        testing::init_tracing();
544
545        let (mut gate, entered, release, dropped) = Gate::new();
546        gate.panics = true;
547        let (sender, _) = contexts();
548        let closer = Closer::new(move || {
549            let _ = release.send(());
550        });
551        let outbound = Arc::new(Outbound::new(
552            gate,
553            Side::Client,
554            closer,
555            DEFAULT_WRITE_TIMEOUT,
556        ));
557        let (_sealer, sender) = connect(&outbound, sender);
558
559        let sending = thread::spawn(move || {
560            panic::catch_unwind(AssertUnwindSafe(|| sender.send(&payload(1))))
561        });
562        entered.recv_timeout(Duration::from_secs(5)).unwrap();
563        outbound.close();
564        assert!(sending.join().unwrap().is_err());
565        drop(outbound);
566        dropped.recv_timeout(Duration::from_secs(5)).unwrap();
567    }
568}