ktls now under the rustls org
Thanks to my sponsors: Manuel Hutter, Matt Heise, Olly Swanson, Luis, Bob Ippolito, Lawrence Bethlenfalvy, Luuk, Lena Schönburg, Dragoon, Hadrien G., Yann Schwartz, Mario Fleischhacker, Michal Hošna, Raphaël Thériault, Alex Krantz, Pete Bevin, Marc-Andre Giroux, Matt Jackson, Luke Konopka, Wyatt Herkamp and 262 more
What’s a ktls
I started work on ktls and ktls-sys, a pair of crates exposing Kernel TLS offload to Rust, about two years ago.
kTLS lets the kernel (and, in turn, any network interface that supports it) take care of encryption, framing, etc., for the entire duration of a TLS connection… as soon as you have a TLS connection.
For the handshake itself (hellos, change cipher, encrypted extensions, certificate verification, etc.), you still have to use a userland TLS implementation.
The Illustrated TLS 1.3 Connection shows the handshake in great detail.
In Rust, the natural choice is rustls, which got a very favorable audit in 2020, and so that’s why the higher-level of my two crates, ktls, has a dependency on rustls.
This posed a challenge early on: for the kernel to take over encryption, continuing where rustls left off, we
have to, well, not only deal with any data rustls may have already decrypted (TLS frames, TCP segments,
and whatever read/recvmsg writes into your buffer don’t necessarily align), but also to exfiltrate the
session keys (and sequence numbers), something that rustls keeps well-hidden by design.
The exact data being exported varies depending on which cipher is used, and it was a bit of work to find an API design both the rustls maintainers and myself were happy with — that work took place two years ago and landed in rustls in a little under a month.
The ktls API today
Today, ktls’s API takes a rustls ClientConnection or ServerConnection (kTLS works in either direction)
and gives you back a TcpStream and a Vec<u8> of already-decrypted data.
Here’s an excerpt of a code sample from loona, my HTTP/1+2 implementation:
// use a self-signed certificate for testing
let certified_key = rcgen:: generate_simple_self_signed ( vec! [ "localhost" . to_string ()]). unwrap ();
let crt = certified_key. cert . der ();
let key = certified_key. key_pair . serialize_der ();
let mut server_config = ServerConfig :: builder ()
. with_no_client_auth ()
. with_single_cert (
vec! [ crt. clone ()],
PrivatePkcs8KeyDer :: from ( key. clone ()). into (),
)
. unwrap ();
// not strictly needed for kTLS, but useful for debugging:
// this reads the SSLKEYLOGFILE environment variable, and writes
// the secrets to it
server_config. key_log = Arc :: new ( rustls:: KeyLogFile :: new ());
// we'll need to extract secrets to give them to the kernel — the
// rustls API has us explicitly opt into this
server_config. enable_secret_extraction = true ;
server_config. alpn_protocols = vec! [ b"h2" . to_vec (), b"http/1.1" . to_vec ()];
let acceptor = tokio_rustls:: TlsAcceptor :: from ( Arc :: new ( server_config));
let stream: TcpStream = todo! ( "TCP listen/accept logic goes here" );
// this wrapper is able to stop reading at the boundary between
// two TLS messages — which is done in `config_ktls_{client,server}`
// further down, once the handshake is done.
let stream = CorkStream :: new ( stream);
// this is the usual way for a server to establish a TLS connection
// asynchronously with `tokio-rustls`
let stream = acceptor. accept ( stream). await ?;
// as usual, we get access to the rustls session, including
// the negotiated ALPN protocol
let sc = stream. get_ref (). 1 ;
let alpn_proto = sc. alpn_protocol (). and_then ( |p| std:: str:: from_utf8 ( p). ok (). map ( |s| s. to_string ()));
debug! ( ?alpn_proto, "Performed TLS handshake" );
// this extracts the secrets, configures kTLS as the "ULP"
// (upper-layer protocol), and returns a `TcpStream` that
// transparently does TLS.
let stream = ktls:: config_ktls_server ( stream). await ?;
debug! ( "kTLS successfully set up" );
let ( drained, stream) = stream. into_raw ();
let drained = drained. unwrap_or_default ();
debug! ( "{} bytes already decoded by rustls" , drained. len ());
Coordinating & collaborating
But rustls occasionally evolves, and undergoes APIs changes (whenever better — safer, more correct, more flexible — interfaces
are found).
And whenever a new version comes out, crates like tokio-rustls
and ktls must be updated to be compatible with it.
In the past, I’ve been irritated at the several months lag between a rustls update and the corresponding
tokio-rustls release — I recently complained about it online and the maintainers let me know that this
is unlikely to happen again in the future because both packages are now under the same GitHub organization,
https://github.com/rustls — they also offered to adopt ktls, so that it would also stay in-sync, and
so we could easily orchestrate simultaneous releases of rustls, tokio-rustls, and ktls.
I’d like to thank Dirkjan for offering to adopt ktls — even though I’ll be
around too, to deal with whatever comes up :)
Did you know I also make videos? Check them out on PeerTube and also YouTube!
Here's another article just for you:
Rust modules vs files
A while back, I asked on Twitter what people found confusing in Rust, and one of the top topics was “how the module system maps to files”.
I remember struggling with that a lot when I first started Rust, so I’ll try to explain it in a way that makes sense to me.
Important note
All that follows is written for Rust 2021 edition. I have no interest in learning (or teaching) the ins and outs of the previous version, especially because it was a lot more confusing to me.