simple_comms 2.0.1

Rust implementation of a communication protocol for AH
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
//! A background task that drives a `simple_comms` connection full-duplex:
//! independent, concurrent send and receive on one already-`Noise_NK`-secured
//! connection, automatic `MsgType::Heartbeat` liveness, and both
//! self-initiated and peer-initiated `MsgType::Rekey` handled inline. See
//! `docs/HANDSHAKE.md` for the wider connection lifecycle this sits on top
//! of, and [`crate::network::send_receive`] for the simpler
//! request/response API this complements (a connection that doesn't need a
//! live duplex session -- a one-shot exchange, or the pre-driver handshake
//! itself -- can keep using that instead).
//!
//! Unlike [`crate::network::send_receive`]'s helpers, [`ConnectionDriver`]
//! owns the connection exclusively for its lifetime: it splits the
//! underlying stream into independent read/write halves and runs a single
//! background task that `select!`s across them, an outbound queue, a rekey
//! control channel, and a heartbeat timer. Because that one task is the
//! *only* thing that ever touches the connection's [`ConnectionCtx`] or
//! stream halves, no lock is needed around them -- every operation
//! (including an in-progress rekey) is naturally serialized, so nothing can
//! race a send against a rekey, and no signal can be missed.

use std::time::{Duration, Instant};

use dusa_collection_utils::core::errors::{ErrorArrayItem, Errors};
use dusa_collection_utils::core::logger::LogLevel;
use dusa_collection_utils::log;
use tokio::io::{self, AsyncRead, AsyncWrite, ReadHalf, WriteHalf};
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;
use tokio::time::MissedTickBehavior;

use crate::protocol::{
    flags::MsgType,
    handshake::{NoiseIdentity, rekey_initiator_rw, rekey_responder_rw},
    heartbeat::{Heartbeat, HeartbeatState},
    message::{ConnectionCtx, ProtocolMessage, SessionAck, SessionRequest, read_message_raw_buffered},
    proto::Proto,
};

/// One of the message kinds a [`ConnectionDriver`] surfaces to the
/// application via [`ConnectionHandle::recv`]. `Heartbeat`/`Rekey`/`Close`/
/// `Hello`/`HelloAck` never reach here -- they're handled internally by the
/// driver loop.
#[derive(Debug)]
pub enum DriverMessage<APP> {
    /// An application `Data` message.
    Data(ProtocolMessage<APP>),
    /// A logical-session `Open` request; see `docs/HANDSHAKE.md`. Routing
    /// this to the right application handler (by
    /// `ProtocolMessage::header::meta().session_id` or otherwise) is left
    /// to the caller, same as the rest of `Open`/`OpenAck` bookkeeping.
    Open(ProtocolMessage<SessionRequest>),
    /// An `OpenAck` confirmation.
    OpenAck(ProtocolMessage<SessionAck>),
}

/// Tuning knobs for [`ConnectionDriver::spawn`].
#[derive(Debug, Clone, Copy)]
pub struct DriverConfig {
    /// How often to send `MsgType::Heartbeat`, and the unit
    /// [`Heartbeat::state`] judges peer liveness against (`Suspect` at 3
    /// missed intervals, `Timeout` -- connection torn down -- at 5).
    pub heartbeat_interval: Duration,
    /// Bound on the outbound [`ConnectionHandle::send`] queue.
    pub outbound_buffer: usize,
    /// Bound on the inbound [`ConnectionHandle::recv`] queue.
    pub inbound_buffer: usize,
}

impl Default for DriverConfig {
    fn default() -> Self {
        Self {
            heartbeat_interval: Duration::from_secs(15),
            outbound_buffer: 64,
            inbound_buffer: 64,
        }
    }
}

/// Which side of the original `Noise_NK` handshake this connection played
/// (see `docs/HANDSHAKE.md`). A rekey re-runs `Hello`/`HelloAck` with the
/// same asymmetry as the original handshake -- only the side that knows the
/// peer's static key can *ask* for one (`rekey_initiator_rw`); only the
/// side with a static identity can *answer* one (`rekey_responder_rw`) --
/// so the driver needs to know which role it's playing to handle both
/// self-initiated ([`ConnectionHandle::rekey`]) and peer-initiated (an
/// incoming `MsgType::Rekey` frame) rekeys correctly.
pub enum ConnectionRole {
    /// This side ran [`crate::network::send_receive::establish_connection_initiator`].
    /// Only this role can call [`ConnectionHandle::rekey`].
    Initiator { remote_static_pubkey: [u8; 32] },
    /// This side ran [`crate::network::send_receive::establish_connection_responder`].
    /// Only this role reacts to a peer-initiated `MsgType::Rekey`.
    Responder { identity: NoiseIdentity },
}

enum Control {
    Rekey(oneshot::Sender<Result<(), ErrorArrayItem>>),
    Shutdown,
}

/// Handle to a connection being driven full-duplex by a background task
/// spawned via [`ConnectionDriver::spawn`]. Dropping this without calling
/// [`Self::shutdown`] still stops the driver (its outbound/control channels
/// close), just without a way to observe how it stopped.
pub struct ConnectionHandle<APP> {
    outbound_tx: mpsc::Sender<ProtocolMessage<APP>>,
    inbound_rx: mpsc::Receiver<DriverMessage<APP>>,
    control_tx: mpsc::Sender<Control>,
    task: JoinHandle<io::Result<()>>,
}

impl<APP> ConnectionHandle<APP>
where
    APP: serde::de::DeserializeOwned
        + serde::Serialize
        + std::fmt::Debug
        + Clone
        + Unpin
        + Send
        + 'static,
{
    /// Enqueue `msg` for sending. Doesn't wait for a reply -- this is the
    /// full-duplex push path, not [`crate::network::send_receive::send_message`]'s
    /// request/response one, so a peer can receive any number of these
    /// without needing to reply to each. `msg`'s `ConnectionParams` (set
    /// via [`ProtocolMessage::new`], typically `conn.params`) travel with
    /// it as normal.
    pub async fn send(&self, msg: ProtocolMessage<APP>) -> Result<(), ErrorArrayItem> {
        self.outbound_tx
            .send(msg)
            .await
            .map_err(|_| ErrorArrayItem::new(Errors::ConnectionError, "driver task has stopped"))
    }

    /// Wait for the next dispatched `Data`/`Open`/`OpenAck` message.
    /// Returns `None` once the driver has stopped (peer sent `Close`, a
    /// heartbeat timeout, or a fatal I/O error) -- see [`Self::shutdown`]
    /// to retrieve the reason.
    pub async fn recv(&mut self) -> Option<DriverMessage<APP>> {
        self.inbound_rx.recv().await
    }

    /// Rotate this connection's transport key (`docs/HANDSHAKE.md`'s
    /// `Rekey`), as the connection's original Noise initiator. Waits for
    /// the driver's in-loop rekey to actually complete before returning.
    /// Only valid when this connection was established via
    /// [`crate::network::send_receive::establish_connection_initiator`]
    /// (see [`ConnectionRole`]) -- called on the responder side, this
    /// returns `Err`.
    pub async fn rekey(&self) -> Result<(), ErrorArrayItem> {
        let (ack, done) = oneshot::channel();
        self.control_tx
            .send(Control::Rekey(ack))
            .await
            .map_err(|_| ErrorArrayItem::new(Errors::ConnectionError, "driver task has stopped"))?;
        done.await
            .map_err(|_| ErrorArrayItem::new(Errors::ConnectionError, "driver task has stopped"))?
    }

    /// Whether the driver task has already stopped.
    pub fn is_finished(&self) -> bool {
        self.task.is_finished()
    }

    /// Signal the driver to stop and wait for it to actually do so,
    /// returning whatever error it stopped with -- a heartbeat timeout or a
    /// fatal I/O error -- if it stopped abnormally.
    pub async fn shutdown(mut self) -> io::Result<()> {
        let _ = self.control_tx.send(Control::Shutdown).await;
        self.inbound_rx.close();
        match self.task.await {
            Ok(result) => result,
            Err(join_err) => Err(io::Error::other(join_err.to_string())),
        }
    }
}

/// Spawns and drives a connection full-duplex. See the module docs.
pub struct ConnectionDriver;

impl ConnectionDriver {
    /// Split `stream` and spawn the background task that drives it. `ctx`
    /// must already be an established connection (from
    /// [`crate::network::send_receive::establish_connection_initiator`] or
    /// [`crate::network::send_receive::establish_connection_responder`]) --
    /// the driver doesn't perform the initial handshake itself, only
    /// `Rekey`; `role` must match how `ctx` was established (see
    /// [`ConnectionRole`]).
    pub fn spawn<S, APP>(
        stream: S,
        ctx: ConnectionCtx,
        role: ConnectionRole,
        proto: Proto,
        config: DriverConfig,
    ) -> ConnectionHandle<APP>
    where
        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
        APP: serde::de::DeserializeOwned
            + serde::Serialize
            + std::fmt::Debug
            + Clone
            + Unpin
            + Send
            + 'static,
    {
        let (read_half, write_half) = tokio::io::split(stream);
        let (outbound_tx, outbound_rx) = mpsc::channel(config.outbound_buffer);
        let (inbound_tx, inbound_rx) = mpsc::channel(config.inbound_buffer);
        let (control_tx, control_rx) = mpsc::channel(4);

        let task = tokio::spawn(driver_loop::<S, APP>(
            read_half,
            write_half,
            ctx,
            role,
            proto,
            config,
            DriverChannels { outbound_rx, inbound_tx, control_rx },
        ));

        ConnectionHandle {
            outbound_tx,
            inbound_rx,
            control_tx,
            task,
        }
    }
}

/// The channel endpoints [`driver_loop`] owns, bundled into one parameter
/// to stay under a sane argument count.
struct DriverChannels<APP> {
    outbound_rx: mpsc::Receiver<ProtocolMessage<APP>>,
    inbound_tx: mpsc::Sender<DriverMessage<APP>>,
    control_rx: mpsc::Receiver<Control>,
}

async fn driver_loop<S, APP>(
    mut read_half: ReadHalf<S>,
    mut write_half: WriteHalf<S>,
    mut ctx: ConnectionCtx,
    role: ConnectionRole,
    proto: Proto,
    config: DriverConfig,
    channels: DriverChannels<APP>,
) -> io::Result<()>
where
    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
    APP: serde::de::DeserializeOwned
        + serde::Serialize
        + std::fmt::Debug
        + Clone
        + Unpin
        + Send
        + 'static,
{
    let DriverChannels { mut outbound_rx, inbound_tx, mut control_rx } = channels;

    let mut heartbeat = Heartbeat::new(config.heartbeat_interval, Instant::now());
    let mut ticker = tokio::time::interval(config.heartbeat_interval);
    ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);

    // Owned here (not inside the read future) so a cancelled read -- this
    // branch losing the select! race to another one -- doesn't lose bytes
    // already consumed from `read_half`; see `read_message_raw_buffered`.
    let mut frame_buf: Vec<u8> = Vec::new();

    loop {
        tokio::select! {
            read_result = read_message_raw_buffered(&mut read_half, Some(&mut ctx), &mut frame_buf) => {
                let (header, payload) = match read_result {
                    Ok(v) => v,
                    Err(err) => {
                        log!(LogLevel::Error, "driver read error: {err}");
                        return Err(err);
                    }
                };

                match header.msg_type() {
                    MsgType::Heartbeat => {
                        heartbeat.mark_recv(Instant::now());
                    }
                    MsgType::Data => {
                        let msg: ProtocolMessage<APP> = ProtocolMessage::finish(header, &payload)?;
                        if inbound_tx.send(DriverMessage::Data(msg)).await.is_err() {
                            return Ok(());
                        }
                    }
                    MsgType::Open => {
                        let msg: ProtocolMessage<SessionRequest> =
                            ProtocolMessage::finish(header, &payload)?;
                        if inbound_tx.send(DriverMessage::Open(msg)).await.is_err() {
                            return Ok(());
                        }
                    }
                    MsgType::OpenAck => {
                        let msg: ProtocolMessage<SessionAck> =
                            ProtocolMessage::finish(header, &payload)?;
                        if inbound_tx.send(DriverMessage::OpenAck(msg)).await.is_err() {
                            return Ok(());
                        }
                    }
                    MsgType::Rekey => match &role {
                        ConnectionRole::Responder { identity } => {
                            match rekey_responder_rw(&mut read_half, &mut write_half, identity).await {
                                Ok(new_ctx) => {
                                    ctx = new_ctx;
                                    heartbeat.mark_recv(Instant::now());
                                    log!(LogLevel::Info, "rekeyed (peer-initiated)");
                                }
                                Err(err) => {
                                    log!(LogLevel::Error, "peer-initiated rekey failed: {err}");
                                    return Err(err);
                                }
                            }
                        }
                        ConnectionRole::Initiator { .. } => {
                            log!(
                                LogLevel::Warn,
                                "ignoring unexpected Rekey signal from peer -- this side is the Noise initiator"
                            );
                        }
                    },
                    MsgType::Close => {
                        log!(LogLevel::Info, "peer closed the connection");
                        return Ok(());
                    }
                    other => {
                        log!(LogLevel::Warn, "driver ignoring unexpected message type: {other:?}");
                    }
                }
            }

            outbound = outbound_rx.recv() => {
                let Some(msg) = outbound else {
                    // The handle (and every clone of `outbound_tx`) was
                    // dropped without an explicit `shutdown()` -- stop.
                    return Ok(());
                };
                if let Err(err) = msg.write_to(&mut write_half, proto, Some(&mut ctx)).await {
                    log!(LogLevel::Error, "driver write error: {err}");
                    return Err(err);
                }
            }

            ctrl = control_rx.recv() => {
                match ctrl {
                    Some(Control::Shutdown) | None => return Ok(()),
                    Some(Control::Rekey(ack)) => {
                        let result = match &role {
                            ConnectionRole::Initiator { remote_static_pubkey } => {
                                rekey_initiator_rw(&mut read_half, &mut write_half, &mut ctx, remote_static_pubkey)
                                    .await
                                    .map(|new_ctx| {
                                        ctx = new_ctx;
                                        heartbeat.mark_sent(Instant::now());
                                        log!(LogLevel::Info, "rekeyed (self-initiated)");
                                    })
                                    .map_err(|err| ErrorArrayItem::new(Errors::Network, err.to_string()))
                            }
                            ConnectionRole::Responder { .. } => Err(ErrorArrayItem::new(
                                Errors::Unauthorized,
                                "only the connection's original Noise initiator can self-initiate a rekey",
                            )),
                        };
                        let _ = ack.send(result);
                    }
                }
            }

            _ = ticker.tick() => {
                if heartbeat.should_send(Instant::now()) {
                    let hb: ProtocolMessage<()> = ProtocolMessage::new(ctx.params, MsgType::Heartbeat, ())?;
                    if let Err(err) = hb.write_to(&mut write_half, proto, Some(&mut ctx)).await {
                        log!(LogLevel::Error, "driver heartbeat write error: {err}");
                        return Err(err);
                    }
                    heartbeat.mark_sent(Instant::now());
                }

                if heartbeat.state(Instant::now()) == HeartbeatState::Timeout {
                    log!(LogLevel::Error, "peer heartbeat timeout -- tearing down connection");
                    return Err(io::Error::new(io::ErrorKind::TimedOut, "peer heartbeat timeout"));
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::network::send_receive::{establish_connection_initiator, establish_connection_responder};
    use crate::protocol::flags::ConnectionParams;

    /// Two drivers, each spawned as a real background task (`tokio::spawn`,
    /// not `tokio::join!`, since this is specifically testing task
    /// concurrency): the client pushes several `Data` messages back-to-back
    /// with no reply in between, and the server independently pushes one
    /// back, unprompted -- proving sends aren't coupled to a request/reply
    /// pairing in either direction on the same connection.
    #[tokio::test]
    async fn full_duplex_push_without_reply() {
        let identity = NoiseIdentity::generate().unwrap();
        let remote_pub = identity.public_key();
        let (mut client_stream, mut server_stream) = tokio::io::duplex(8192);

        let (client_ctx, server_ctx) = tokio::join!(
            establish_connection_initiator(&mut client_stream, &remote_pub, ConnectionParams::ENCRYPTED),
            establish_connection_responder(&mut server_stream, &identity),
        );
        let client_ctx = client_ctx.unwrap();
        let server_ctx = server_ctx.unwrap();

        let config = DriverConfig::default();
        let mut client: ConnectionHandle<Vec<u8>> = ConnectionDriver::spawn(
            client_stream,
            client_ctx,
            ConnectionRole::Initiator { remote_static_pubkey: remote_pub },
            Proto::TCP,
            config,
        );
        let mut server: ConnectionHandle<Vec<u8>> = ConnectionDriver::spawn(
            server_stream,
            server_ctx,
            ConnectionRole::Responder { identity },
            Proto::TCP,
            config,
        );

        for seq in 0..3u8 {
            client
                .send(ProtocolMessage::new(ConnectionParams::ENCRYPTED, MsgType::Data, vec![seq]).unwrap())
                .await
                .unwrap();
        }

        for seq in 0..3u8 {
            match server.recv().await.unwrap() {
                DriverMessage::Data(msg) => assert_eq!(msg.payload, vec![seq]),
                other => panic!("unexpected message: {other:?}"),
            }
        }

        // The other half of "full duplex": the server can push back,
        // unprompted, on the same connection.
        server
            .send(ProtocolMessage::new(ConnectionParams::ENCRYPTED, MsgType::Data, b"pong".to_vec()).unwrap())
            .await
            .unwrap();
        match client.recv().await.unwrap() {
            DriverMessage::Data(msg) => assert_eq!(msg.payload, b"pong".to_vec()),
            other => panic!("unexpected message: {other:?}"),
        }

        let _ = client.shutdown().await;
        let _ = server.shutdown().await;
    }

    /// If the peer goes silent (connection stays open, but nothing arrives
    /// -- as opposed to an EOF/closed socket) for 5 heartbeat intervals,
    /// [`Heartbeat::state`] reports `Timeout` and the driver tears itself
    /// down, ending `recv()` and surfacing the reason via `shutdown()`.
    #[tokio::test(start_paused = true)]
    async fn heartbeat_timeout_tears_down_driver() {
        let identity = NoiseIdentity::generate().unwrap();
        let remote_pub = identity.public_key();
        let (mut client_stream, mut server_stream) = tokio::io::duplex(8192);

        let (client_ctx, server_ctx) = tokio::join!(
            establish_connection_initiator(&mut client_stream, &remote_pub, ConnectionParams::ENCRYPTED),
            establish_connection_responder(&mut server_stream, &identity),
        );
        let client_ctx = client_ctx.unwrap();
        let _server_ctx = server_ctx.unwrap();
        // Keep the server's half alive (an open, merely silent connection)
        // without ever reading or writing on it -- if it were dropped
        // instead, the client would see EOF and error out on that, rather
        // than actually exercising the heartbeat timeout path.
        let _server_stream = server_stream;

        let config = DriverConfig {
            heartbeat_interval: Duration::from_millis(50),
            ..Default::default()
        };
        let mut client: ConnectionHandle<Vec<u8>> = ConnectionDriver::spawn(
            client_stream,
            client_ctx,
            ConnectionRole::Initiator { remote_static_pubkey: remote_pub },
            Proto::TCP,
            config,
        );

        // 5+ missed intervals -> Timeout (see Heartbeat::state).
        tokio::time::advance(config.heartbeat_interval * 6).await;

        assert!(client.recv().await.is_none());
        assert!(client.shutdown().await.is_err());
    }

    /// A self-initiated rekey mid-stream doesn't lose or misdecrypt
    /// messages sent shortly before or after it -- the driver loop
    /// serializes the rekey against the outbound queue, so every send
    /// (whichever side of the rekey it lands on) is written under whatever
    /// cipher epoch is current at that point, and the peer -- reading the
    /// same ordered stream -- decrypts each one the same way.
    #[tokio::test]
    async fn rekey_while_sending_preserves_message_order() {
        let identity = NoiseIdentity::generate().unwrap();
        let remote_pub = identity.public_key();
        let (mut client_stream, mut server_stream) = tokio::io::duplex(8192);

        let (client_ctx, server_ctx) = tokio::join!(
            establish_connection_initiator(&mut client_stream, &remote_pub, ConnectionParams::ENCRYPTED),
            establish_connection_responder(&mut server_stream, &identity),
        );
        let client_ctx = client_ctx.unwrap();
        let server_ctx = server_ctx.unwrap();

        let config = DriverConfig::default();
        let client: ConnectionHandle<Vec<u8>> = ConnectionDriver::spawn(
            client_stream,
            client_ctx,
            ConnectionRole::Initiator { remote_static_pubkey: remote_pub },
            Proto::TCP,
            config,
        );
        let mut server: ConnectionHandle<Vec<u8>> = ConnectionDriver::spawn(
            server_stream,
            server_ctx,
            ConnectionRole::Responder { identity },
            Proto::TCP,
            config,
        );

        for seq in 0..5u8 {
            client
                .send(ProtocolMessage::new(ConnectionParams::ENCRYPTED, MsgType::Data, vec![seq]).unwrap())
                .await
                .unwrap();
        }

        client.rekey().await.unwrap();

        for seq in 5..10u8 {
            client
                .send(ProtocolMessage::new(ConnectionParams::ENCRYPTED, MsgType::Data, vec![seq]).unwrap())
                .await
                .unwrap();
        }

        for seq in 0..10u8 {
            match server.recv().await.unwrap() {
                DriverMessage::Data(msg) => assert_eq!(msg.payload, vec![seq]),
                other => panic!("unexpected message: {other:?}"),
            }
        }

        // A responder can't self-initiate a rekey (see ConnectionRole).
        assert!(server.rekey().await.is_err());

        let _ = client.shutdown().await;
        let _ = server.shutdown().await;
    }
}