dissonance

Crates.iodissonance
lib.rsdissonance
version0.3.5
sourcesrc
created_at2024-11-09 02:53:50.430695
updated_at2024-11-10 14:55:38.543107
descriptionAn async-friendly Rust library for generating Noise-encrypted transport protocols
homepagehttps://github.com/sajmon170/dissonance
repositoryhttps://github.com/sajmon170/dissonance
max_upload_size
id1441778
size29,153
(sajmon170)

documentation

README

dissonance

An async-friendly Rust library for generating Noise-encrypted transport protocols. It provides tools for:

  • creating an AsyncRead + AsyncWrite Noise socket
  • creating a Stream + Sink abstraction layer on top of it (with separate sender and responder message types).

Quickstart - Encrypted raw byte streams with NoiseSocket

In order to create a proper transport you first need to obtain a NoiseSocket. This is done through the NoiseBuilder interface.

NoiseBuilder requires a reliable underlying transport protocol to work!

Suppose you want to upgrade your plain TcpStream to a Noise one as an initiator by performing an IK Noise handshake. For this, you can use the NoiseBuilder::build_as_initiator() method after supplying the necessary handshake data as follows:

let socket = NoiseBuilder::<TcpStream>::new(my_keys, my_tcp_stream)
    .set_my_type(NoiseSelfType::I)
    .set_peer_type(NoisePeerType::K(peer_key))
    .build_as_initiator().await?;

The resulting socket provides a unified AsyncRead + AsyncWrite interface for transporting raw bytes over an encrypted channel.

Abstracting away the bytes with NoiseTransport

An AsyncRead + AsyncWrite interface is often not enough for creating a proper communication protocol. We want to send and receive messages of known types that aren't necessarily the same between the sender and the responder.

As a toy example - suppose the sender is a client and the responder is a server. Each client can request the current date from the server. The server can then respond with a formatted date encoded in a [String]. Let's encode each message type by leveraging Rust's type system:

#[derive(Serialize, Deserialize)]
enum Request {
    GetDateTime
}

#[derive(Serialize, Deserialize)]
enum Response {
    DateTime(String)
}

By encoding each message in an enum we can add more requests and more responses later. By separating request and response types from each other we won't have to match requests when waiting for a response and vice-versa.

Both Request and Response need to be serializable and deserializable in order to send them through a socket. This is done through Serde's Serialize and Deserialize traits.

We can now create a NoiseTransport that abstracts away the bytes and turns the AsyncRead + AsyncWrite Noise socket into a unified Stream + Sink interface:

let transport = NoiseTransport::<TcpStream, Request, Response>::new(socket);

That was easy! We can now use the StreamExt and SinkExt traits to send and receive our messages:

transport.send(Request::GetDateTime).await?;
let response: Response = transport.next().await?;

Using the underlying socket of a NoiseTransport directly

Suppose you want to send a large file (a couple of gigabytes) to the server. Encoding this file as a vector of bytes would not only be extremely inefficient, it would also be impossible since either the sender or receiver could run out of RAM. In this case the best way to send this file would be to copy it with tokio::io::copy() by using the underlying socket directly.

You can get a temporary mutable reference to the underlying socket by calling NoiseTransport::get_mut(). You can then copy the file as you normally would by using the copy() method:

let mut socket = transport.get_mut();
tokio::io::copy(&mut my_file, &mut socket).await?

Technical details

Sending messages over a stream requires serialization. This is done using a combination of Serde with the Postcard serializer.

Individual Noise messages are framed with a big endian u16 LengthDelimitedCodec. Then, each Noise message pack (a set of consecutive Noise frames) gets framed with a big endian u32 LengthDelimitedCodec. This double framing is required for sending large byte streams, as per the previous section.

License: BSD-3-Clause

Commit count: 4

cargo fmt