#channel #actor #network

ductile

A channel implementation that allows both local in-memory channels and remote TCP-based channels with the same interface

2 unstable releases

0.2.0 Sep 28, 2021
0.1.0 Aug 2, 2020

#120 in Concurrency

Download history 1/week @ 2021-08-14 2/week @ 2021-09-18 31/week @ 2021-09-25 6/week @ 2021-10-02 52/week @ 2021-10-09 3/week @ 2021-10-16 2/week @ 2021-10-23 43/week @ 2021-10-30 58/week @ 2021-11-06 42/week @ 2021-11-13 11/week @ 2021-11-20 63/week @ 2021-11-27

174 downloads per month

MIT license

45KB
424 lines

ductile

Rust Audit crates.io Docs

A channel implementation that allows both local in-memory channels and remote TCP-based channels with the same interface.

Components

This crate exposes an interface similar to std::sync::mpsc channels. It provides a multiple producers, single consumer channel that can use under the hood local in-memory channels (provided by crossbeam_channel) but also network channels via TCP sockets. The remote connection can also be encrypted using ChaCha20.

Like std::sync::mpsc, there could be more ChannelSender but there can be only one ChannelReceiver. The two ends of the channel are generic over the message type sent but the type must match (this is checked at compile time only for local channels). If the types do not match errors will be returned and possibly a panic can occur since the channel breaks.

The channels also offer a raw mode where the data is not serialized and send as-is, improving drastically the performances for unstructured data. It should be noted that you can mix the two modes in the same channel but you must be careful to always receive with the correct mode (you cannot receive raw data with the normal recv method). Extra care should be taken when cloning the sender and using it from more threads.

With remote channels the messages are serialized using bincode.

Usage

Here there are some simple examples of how you can use this crate:

Simple local channel

let (tx, rx) = new_local_channel();
tx.send(42u64).unwrap();
tx.send_raw(&vec![1, 2, 3, 4]).unwrap();
let answer = rx.recv().unwrap();
assert_eq!(answer, 42u64);
let data = rx.recv_raw().unwrap();
assert_eq!(data, vec![1, 2, 3, 4]);

Local channel with custom data types

Note that your types must be Serialize and Deserialize.

#[derive(Serialize, Deserialize)]
struct Thing {
    pub x: u32,
    pub y: String,
}
let (tx, rx) = new_local_channel();
tx.send(Thing {
    x: 42,
    y: "foobar".into(),
})
.unwrap();
let thing: Thing = rx.recv().unwrap();
assert_eq!(thing.x, 42);
assert_eq!(thing.y, "foobar");

Remote channels

let port = 18452; // let's hope we can bind this port!
let mut server = ChannelServer::bind(("127.0.0.1", port)).unwrap();

// this examples need a second thread since the handshake cannot be done using a single thread
// only
let client_thread = std::thread::spawn(move || {
    let (sender, receiver) = connect_channel(("127.0.0.1", port)).unwrap();

    sender.send(vec![1, 2, 3, 4]).unwrap();

    let data: Vec<i32> = receiver.recv().unwrap();
    assert_eq!(data, vec![5, 6, 7, 8]);

    sender.send(vec![9, 10, 11, 12]).unwrap();
    sender.send_raw(&vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap();
});

let (sender, receiver, _addr) = server.next().unwrap();
let data: Vec<i32> = receiver.recv().unwrap();
assert_eq!(data, vec![1, 2, 3, 4]);

sender.send(vec![5, 6, 7, 8]).unwrap();

let data = receiver.recv().unwrap();
assert_eq!(data, vec![9, 10, 11, 12]);
let data = receiver.recv_raw().unwrap();
assert_eq!(data, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);

Remote channel with encryption

let port = 18453;
let enc_key = [69u8; 32];
let mut server = ChannelServer::bind_with_enc(("127.0.0.1", port), enc_key).unwrap();

let client_thread = std::thread::spawn(move || {
    let (sender, receiver) = connect_channel_with_enc(("127.0.0.1", port), &enc_key).unwrap();

    sender.send(vec![1u8, 2, 3, 4]).unwrap();

    let data: Vec<u8> = receiver.recv().unwrap();
    assert_eq!(data, vec![5u8, 6, 7, 8]);

    sender.send(vec![69u8; 12345]).unwrap();
    sender.send_raw(&vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap();
});

let (sender, receiver, _addr) = server.next().unwrap();

let data: Vec<u8> = receiver.recv().unwrap();
assert_eq!(data, vec![1u8, 2, 3, 4]);

sender.send(vec![5u8, 6, 7, 8]).unwrap();

let data = receiver.recv().unwrap();
assert_eq!(data, vec![69u8; 12345]);
let file = receiver.recv_raw().unwrap();
assert_eq!(file, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);

Protocol

Messages

All the normal (non-raw) messages are encapsulated inside a ChannelMessage::Message, serialized and sent normally.

The messages in raw mode are sent differently depending if the channel is local or remote. If the channel is local there is no serialization penality so the data is simply sent into the channel. If the channel is removed to avoid serialization a small message with the data length is sent first, followed by the actual payload (that can be eventually encrypted).

Handshakes

Local channels do not need an handshake, therefore this section refers only to remote channels.

There are 2 kinds of handshake: one for encrypted channels and one for non-encrypted ones. The porpuse of the handshake is to share encryption information (like the nonce) and check if the encryption key is correct.

For encrypted channels 2 rounds of handshakes take place:

  • both parts generate 12 bytes of cryptographically secure random data (the channel nonce) and send them to the other party unencrypted.
  • after receiving the channel nonce each party encrypts a known contant (a magic number of 4 bytes) and sends it back to the other.
  • when those 4 bytes are received they get decrypted and if they match the initial magic number the key is probably valid and the handshake completes.

For unencrytpted channels the same handshake is done but with a static key and nonce and only the magic is encrypted. All the following messages will be sent unencrypted.

License: MIT

Dependencies

~1.7–2.4MB
~50K SLoC

sSa