tokio-aws-lc
Async TLS for Tokio on top of aws-lc-sys,
with built-in Linux kTLS install for AEAD sessions.
Why this crate
tokio-rustls over rustls's aws-lc-rs provider is the right default
for most workloads. tokio-aws-lc exists for two cases it doesn't
cover:
- kTLS without manual plumbing. The kernel's
tlsULP turns per-record AEAD into aread(2)/write(2), dropping a userspace copy and the AEAD CPU on bulk transfers. Wiring it against rustls means reaching past the public API to extract the negotiated traffic keys and callingsetsockopt(SOL_TLS, ...)by hand. This crate installs both directions onaccept/connectcompletion whenever the negotiated cipher is one the kernel understands, and the data path flips toread(2)/write(2)automatically. - A single AWS-LC. A codebase that already links
aws-lc-sys(FIPS deployments, hand-rolled EC code, shared X.509 handling) otherwise pulls two near-identical libcryptos once it also depends on rustls'saws-lc-rsprovider. This crate keeps it to one.
If neither matters, prefer tokio-rustls.
How this crate uses AWS-LC vs how rustls does
The two stacks pull very different surface area from the same upstream
project, and it's easy to assume "rustls + aws-lc-rs" means rustls is
using AWS-LC's TLS engine. It is not.
- rustls +
aws-lc-rsprovider uses AWS-LC only as a crypto library: AEAD primitives (AES-GCM, ChaCha20-Poly1305), HKDF, signature verification (RSA, ECDSA,EdDSA), key exchange (X25519, the NIST P-curves, the hybrid PQ groups), and the TLS 1.2 PRF. Everything else — the TLS state machine, the record layer, the handshake, X.509 chain validation (viawebpki), SNI, ALPN, session tickets — is rustls's own pure-Rust code.aws-lc-rsexposes a ring-compatible API on top ofaws-lc-sysso rustls can swap providers; nothing inlibsslis reachable from there. tokio-aws-lcuses AWS-LC'slibssl. The TLS state machine, record layer, handshake, X.509 chain validation, SNI matching, and ALPN selection all live in AWS-LC; this crate is a Tokio adapter layered onSSL_CTX/SSL/BIO. TheServerConfigandClientConfigbuilders are thin wrappers that translate typed configuration intoSSL_CTX_set_*calls.
That difference is what makes the kTLS path tractable here:
extracting the post-handshake traffic keys and sequence numbers is a
sequence of SSL_* getters in libssl, and there's nowhere in
rustls's public API to reach them without forking. It is also why
behavior around things like client-cert verification, IP-SAN
matching, and protocol-version selection follows AWS-LC's
conventions rather than rustls's — the builder API hides the FFI but
not the semantics.
Status
Pre-release (0.2.0).
| Surface | State |
|---|---|
Server (TlsAcceptor, ServerConfig) |
Round-trip tested against openssl s_client and against this crate's client. |
Client (TlsConnector, ClientConfig) |
PEM / system trust roots, SNI, IP-SAN, mTLS, ALPN. |
Linux kTLS (TLS_TX + TLS_RX) |
TLS 1.3 (AES-128/256-GCM, ChaCha20-Poly1305) and TLS 1.2 AEAD; integration-tested via /proc/net/tls_stat. |
Optional hyper integration |
Server acceptor + tower::Service<Uri> client connector for hyper 1.x. |
Not in 0.1: session resumption, UnixStream, BIO-pair fallback for
arbitrary IO, post-attach TLS 1.3 KeyUpdate, tracing instrumentation,
Windows.
Install
[]
= "0.2"
The default build pulls only aws-lc-sys and tokio. The hyper
adapters are opt-in:
= { = "0.2", = ["hyper"] }
Build prerequisites are whatever aws-lc-sys itself needs: cmake,
clang, libclang-dev, perl, plus a working C toolchain.
Quick start
Server
use Arc;
use AsyncWriteExt;
use TcpListener;
use ;
# async
Client
use Arc;
use ;
use TcpStream;
use ;
# async
TlsConnector::connect accepts both DNS names and IP literals. DNS
names go out as the SNI extension and are matched against the
certificate's DNS SANs / CN; IP literals skip SNI (RFC 6066 §3) and
are matched against the certificate's iPAddress SANs.
Linux kTLS
When the negotiated session is kTLS-eligible (TLS 1.2 or 1.3 with
AES-GCM or ChaCha20-Poly1305), the kernel tls ULP is attached to
the TCP socket and the negotiated AEAD keys are uploaded for both
directions. After that, AsyncRead / AsyncWrite move plaintext
through read(2) / write(2) and the kernel runs the AEAD record
layer.
Install happens automatically when TlsAcceptor::accept /
TlsConnector::connect resolves. Hosts that can't take the install
— non-Linux, kernels without CONFIG_TLS, the module not loaded,
sessions with an ineligible cipher — stay on the userspace AEAD path
without surfacing an error. Inspect TlsStream::ktls_active() after
the handshake to confirm.
The probe is cheap (SSL_pending + one or three setsockopt calls,
on the order of a microsecond on loopback) so it's safe to leave on.
To skip it entirely, call disable_ktls() on the builder:
use ServerConfig;
#
That's worth doing when you statically know the environment can't use
kTLS (locked-down containers without the module) or when you need
long-lived sessions where TLS 1.3 KeyUpdate may eventually fire.
tokio_aws_lc::host_ktls_available() is a startup probe that checks
for /proc/net/tls_stat, so consumers can branch on host support
without touching a real session.
Host prerequisites:
- Kernel ≥ 5.1 for TLS 1.3 (AES-GCM TX is 4.13+, AES-GCM RX is 4.17+, ChaCha20-Poly1305 is 5.11+).
tlsmodule loaded (sudo modprobe tls, or addtlsto/etc/modules-load.d/). Inside containers, the host kernel must provide it.
Re-key behavior
No userspace TLS stack handles a TLS 1.3 KeyUpdate after a kTLS
attach transparently: once the kernel owns the receive path, the
userspace engine never sees the post-handshake record, so it can't
rotate the kernel's keys. This crate currently lets the kernel
return EIO on any non-application_data record reaching the
socket, which surfaces as an IO error and tears the session down.
That is the strictest of the available options — HAProxy peeks at
TLS_GET_RECORD_TYPE via recvmsg cmsg to swallow
NewSessionTicket records and translate close_notify alerts into
EOF, but Tokio's AsyncRead has no cmsg surface to plumb that
through.
Callers that need long-lived sessions across rekeys should
disable_ktls() on those connections. Server-emitted post-handshake
records do not happen on this crate's server path: tickets are
disabled by construction (SSL_OP_NO_TICKET +
SSL_CTX_set_num_tickets(0)), and the crate never initiates a
KeyUpdate itself.
Examples
# TLS echo server (uses the bundled test fixtures)
# Minimal HTTPS client
# Hyper 1.x server with HTTP/1.1 + HTTP/2 ALPN
Cargo features
| Feature | Default | Description |
|---|---|---|
hyper |
off | Server acceptor adapter and client tower::Service<Uri> connector for hyper 1.x via hyper-util. |
There is no parallel adapter for legacy hyper 0.14.
Testing
The server_roundtrip integration test shells out to openssl s_client and self-skips if openssl is not on PATH. The kTLS
tests skip themselves when /proc/net/tls_stat is unavailable or the
kernel rejects the per-direction crypto-info upload.
MSRV
Rust 1.82 for the default build. The hyper feature transitively
pulls dependencies that track a newer rustc; consumers enabling it
must follow hyper's MSRV.