Pure-rust QUIC protocol implementation
Quinn is a pure-rust, future-based implementation of the QUIC transport protocol undergoing standardization by the IETF. This library is at draft 32.
Features
- Simultaneous client/server operation
- Ordered and unordered stream reads for improved performance
- Works on stable Rust, tested on Linux, macOS and Windows
- Pluggable cryptography, with a standard implementation backed by rustls and ring
- Application-layer datagrams for small, unreliable messages
- Future-based async API
- Experimental HTTP over QUIC
- The minimum supported Rust version is 1.45.0
Overview
- quinn: High-level async API based on tokio, see for usage. This will be used by most developers. (Basic benchmarks are included.)
- quinn-proto: Deterministic state machine of the protocol which performs no I/O internally and is suitable for use with custom event loops (and potentially a C or C++ API).
- quinn-h3: Contains an implementation of HTTP-3 and QPACK. It is split internally in a deterministic state machine and a tokio-based high-level async API.
- bench: Benchmarks without any framework.
- interop: Tooling that helps to run interoperability tests.
- fuzz: Fuzz tests.
Getting Started
Examples
$ cargo run --example server ./
$ cargo run --example client https://localhost:4433/Cargo.toml
This launches an HTTP 0.9 server on the loopback address serving the current
working directory, with the client fetching ./Cargo.toml. By default, the
server generates a self-signed certificate and stores it to disk, where the
client will automatically find and trust it.
Links
- Talk at RustFest Paris (May 2018) presentation; slides; YouTube
- Usage examples
- Guide book
Usage Notes
Click to show the notes
Buffers
A Quinn endpoint corresponds to a single UDP socket, no matter how many
connections are in use. Handling high aggregate data rates on a single endpoint
can require a larger UDP buffer than is configured by default in most
environments. If you observe erratic latency and/or throughput over a stable
network link, consider increasing the buffer sizes used. For example, you could
adjust the SO_SNDBUF and SO_RCVBUF options of the UDP socket to be used
before passing it in to Quinn. Note that some platforms (e.g. Linux) require
elevated privileges or modified system configuration for a process to increase
its UDP buffer sizes.
Certificates
By default, Quinn clients validate the cryptographic identity of servers they connect to. This prevents an active, on-path attacker from intercepting messages, but requires trusting some certificate authority. For many purposes, this can be accomplished by using certificates from Let's Encrypt for servers, and relying on the default configuration for clients.
For some cases, including peer-to-peer, trust-on-first-use, deliberately
insecure applications, or any case where servers are not identified by domain
name, this isn't practical. Arbitrary certificate validation logic can be
implemented by enabling the dangerous_configuration feature of rustls and
constructing a Quinn ClientConfig with an overridden certificate verifier by
hand.
When operating your own certificate authority doesn't make sense, rcgen can be used to generate self-signed certificates on demand. To support trust-on-first-use, servers that automatically generate self-signed certificates should write their generated certificate to persistent storage and reuse it on future runs.
Contribution
All feedback welcome. Feel free to file bugs, requests for documentation and any other feedback to the issue tracker.
The quinn-proto test suite uses simulated IO for reproducibility and to avoid
long sleeps in certain timing-sensitive tests. If the SSLKEYLOGFILE
environment variable is set, the tests will emit UDP packets for inspection
using external protocol analyzers like Wireshark, and NSS-compatible key logs
for the client side of each connection will be written to the path specified in
the variable.
The minimum supported Rust version for published releases of our crates will always be at least 6 months old at the time of release.
Authors
- Dirkjan Ochtman - Project owner & founder
- Benjamin Saunders - Project owner & founder
- Jean-Christophe Begue - Project collaborator, author of the HTTP/3 Implementation
Certificates
In this chapter, we discuss the configuration of the certificates that are required for a working Quinn connection.
As QUIC uses TLS 1.3 for authentication of connections, the server needs to provide the client with a certificate confirming its identity, and the client must be configured to trust the certificates it receives from the server.
Insecure Connection
For our example use case, the easiest way to allow the client to trust our server is to disable certificate verification (don't do this in production!).
When the rustls dangerous_configuration feature flag is enabled, a client can be configured to trust any server.
Start by adding a rustls dependency with the dangerous_configuration feature flag to your Cargo.toml file.
quinn = "*"
rustls = { version = "*", features = ["dangerous_configuration", "quic"] }
Then, allow the client to skip the certificate validation by implementing ServerCertVerifier and letting it assert verification for any server.
#![allow(unused)] fn main() { // Implementation of `ServerCertVerifier` that verifies everything as trustworthy. struct SkipCertificationVerification; impl rustls::ServerCertVerifier for SkipCertificationVerification { fn verify_server_cert( &self, _: &rustls::RootCertStore, _: &[rustls::Certificate], _: webpki::DNSNameRef, _: &[u8], ) -> Result<rustls::ServerCertVerified, rustls::TLSError> { Ok(ServerCertVerified::assertion()) } } }
After that, modify the ClientConfig to use this ServerCertVerifier implementation.
#![allow(unused)] fn main() { pub fn insecure() -> ClientConfig { let mut cfg = quinn::ClientConfigBuilder::default().build(); // Get a mutable reference to the 'crypto' config in the 'client config'. let tls_cfg: &mut rustls::ClientConfig = std::sync::Arc::get_mut(&mut cfg.crypto).unwrap(); // Change the certification verifier. // This is only available when compiled with the 'dangerous_configuration' feature. tls_cfg .dangerous() .set_certificate_verifier(Arc::new(SkipCertificationVerification)); cfg } }
Finally, if you plug this ClientConfig into the EndpointBuilder::default_client_config() your client endpoint should verify all connections as trustworthy.
Using Certificates
In this section, we look at certifying an endpoint with a certificate. The certificate can be signed with its key, or with a certificate authority's key.
Self Signed Certificates
Relying on self-signed certificates means that clients allow servers to sign their certificates. This is simpler because no third party is involved in signing the server's certificate. However, self-signed certificates do not protect users from person-in-the-middle attacks, because an interceptor can trivially replace the certificate with one that it has signed. Self-signed certificates, among other options, can be created using the rcgen crate or the openssl binary. This example uses rcgen to generate a certificate.
Let's look at an example:
#![allow(unused)] fn main() { pub fn generate_self_signed_cert(cert_path: &str, key_path: &str) -> anyhow::Result<(quinn::Certificate, quinn::PrivateKey)> { // Generate dummy certificate. let certificate = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); let serialized_key = certificate.serialize_private_key_der(); let serialized_certificate = certificate.serialize_der().unwrap(); // Write to files. fs::write(&cert_path, &serialized_certificate).context("failed to write certificate")?; fs::write(&key_path, &serialized_key).context("failed to write private key")?; let cert = quinn::Certificate::from_der(&cert)?; let key = quinn::PrivateKey::from_der(&serialized_key)?; Ok((cert, key)) } }
Note that generate_simple_self_signed returns a Certificate that can be serialized to both .der and .pem formats.
Non-self-signed Certificates
For this example, we use Let's Encrypt, a well-known Certificate Authority (CA) (certificate issuer) which distributes certificates for free.
Generate Certificate
certbot can be used with Let's Encrypt to generate certificates; its website comes with clear instructions. Because we're generating a certificate for an internal test server, the process used will be slightly different compared to what you would do when generating certificates for an existing (public) website.
On the certbot website, select that you do not have a public web server and follow the given installation instructions. certbot must answer a cryptographic challenge of the Let's Encrypt API to prove that you control the domain. It needs to listen on port 80 (HTTP) or 443 (HTTPS) to achieve this. Open the appropriate port in your firewall and router.
If certbot is installed, run certbot certonly --standalone, this command will start a web server in the background and start the challenge.
certbot asks for the required data and writes the certificate to cert.pem and the private key to privkey.pem.
These files can then be referenced in code.
#![allow(unused)] fn main() { pub fn read_cert_from_file() -> anyhow::Result<(quinn::Certificate, quinn::PrivateKey)> { // Read from certificate and key from directory. let (cert, key) = fs::read(&"./cert.pem").and_then(|x| Ok((x, fs::read(&"./privkey.pem")?)))?; // Parse to certificate chain whereafter taking the first certifcater in this chain. let cert = quinn::CertificateChain::from_pem(&cert)?.iter().next().unwrap().clone(); let key = quinn::PrivateKey::from_pem(&key)?; Ok((quinn::Certificate::from(cert), key)) } }
Configuring Certificates
Now that you have a valid certificate, the client and server need to be configured to use it.
After configuring plug the configuration into the Endpoint.
Configure Server
#![allow(unused)] fn main() { let mut builder = ServerConfigBuilder::default(); builder.certificate(CertificateChain::from_certs(vec![certificate]), key)?; }
This is the only thing you need to do for your server to be secured.
Configure Client
#![allow(unused)] fn main() { let mut builder = ClientConfigBuilder::default(); builder.add_certificate_authority(certificate)?; }
This is the only thing you need to do for your client to trust a server certificate.
Next, let's have a look at how to set up a connection.
Connection Setup
In the previous chapter we looked at how to configure a certificate. This aspect is omitted in this chapter to prevent duplication. But remember that this is required to get your Endpoint up and running. This chapter explains how to set up a connection and prepare it for data transfer.
It all starts with the Endpoint struct, this is the entry point of the library.
Example
Let's start by defining some constants.
#![allow(unused)] fn main() { static SERVER_NAME: &str = "localhost"; fn client_addr() -> SocketAddr { "127.0.0.1:5000".parse::<SocketAddr>().unwrap() } fn server_addr() -> SocketAddr { "127.0.0.1:5001".parse::<SocketAddr>().unwrap() } }
The EndpointBuilder should be used to configure an endpoint. It is also possible to provide Quinn with an initialized socket via with_socket().
Server
First, the server endpoint should be bound to a socket.
The bind() method, which can be used for this, returns a tuple containing the Endpoint and Incoming types.
The Endpoint type can be used to start outgoing connections, and the Incoming type can be used to listen for incoming connections.
#![allow(unused)] fn main() { async fn server() -> anyhow::Result<()> { let mut endpoint_builder = Endpoint::builder(); // Configure this endpoint as a server by passing in `ServerConfig`. endpoint_builder.listen(ServerConfig::default()); // Bind this endpoint to a UDP socket on the given server address. let (endpoint, mut incoming) = endpoint_builder.bind(&server_addr())?; // Start iterating over incoming connections. while let Some(conn) = incoming.next().await { let mut connection: NewConnection = conn.await?; // Save connection somewhere, start transferring, receiving data, see DataTransfer tutorial. } Ok(()) } }
Client
The client needs to connect to the server using the connect(server_name) method.
The SERVER_NAME argument is the DNS name, matching the certificate configured in the server.
#![allow(unused)] fn main() { async fn client() -> anyhow::Result<()> { let mut endpoint_builder = Endpoint::builder(); // Bind this endpoint to a UDP socket on the given client address. let (endpoint, _) = endpoint_builder.bind(&client_addr())?; // Connect to the server passing in the server name which is supposed to be in the server certificate. let connection = endpoint.connect(&server_addr(), SERVER_NAME)?.await?; // Start transferring, receiving data, see data transfer page. Ok(()) } }
Next up, let's have a look at sending data over this connection.
The QUIC protocol
QUIC is a general-purpose network protocol built on top of UDP. The protocol is still under development and standardized by the IETF. Quinn strives to implement the latest draft but may lag a bit behind. Although QUIC is still in a draft phase, the protocol is used for all connections from Chrome web browsers to the Google servers.
QUIC solves a number of transport-layer and application-layer problems experienced by modern web applications. It is very similar to TCP+TLS+HTTP2, but implemented on top of UDP. Having QUIC as a self-contained protocol allows innovations which aren’t possible with existing protocols as they are hampered by legacy clients and middleboxes.
Key advantages of QUIC over TCP+TLS+HTTP2 include:
- Improved connection establishment speed (0-rtt).
- Improved congestion control by moving congestion control algorithms into the user space at both endpoints.
- Improved bandwidth estimation in each direction to avoid congestion.
- Improved multiplexing without head-of-line blocking.
- Contains forward error correction (FEC).
While QUIC's intentions are originally web-oriented, it offers interesting opportunities in other areas like game networking. One thing is for sure, QUIC has many great potentials and will serve us in the future with HTTP/3.
In the upcoming chapter we will be discussing various aspects of QUIC also in relation to Quinn.