A Go library that offloads TLS encryption to the Linux kernel. It wraps net.Listener so you can use it with net/http (or anything that accepts a net.Listener) without changing your application code.
The TLS handshake still happens in userspace via crypto/tls. After the handshake completes, the library extracts the negotiated keys and hands them to the kernel via setsockopt. From that point on, the kernel handles record encryption and decryption directly, bypassing the userspace TLS stack entirely.
If kTLS setup fails for any reason (unsupported kernel, wrong cipher, missing module), the connection silently falls back to regular userspace TLS. Your server keeps working either way.
Only TLS 1.3 connections get offloaded. TLS 1.2 connections work fine but stay in userspace (working on adding support for kTLS 1.2).
See the kernel TLS offload docs for background on how kTLS works at the kernel level.
- Linux with the
tlskernel module loaded (modprobe tls) - Go 1.24+
- TLS 1.3
You can also check at runtime:
if ktls.Available() {
// kernel supports kTLS
}package main
import (
"crypto/tls"
"fmt"
"net"
"net/http"
ktls "github.com/northernside/ktls"
)
func main() {
cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
if err != nil {
panic(err)
}
tcpLn, err := net.Listen("tcp", ":443")
if err != nil {
panic(err)
}
ln := &ktls.Listener{
TCPListener: tcpLn,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
RX: true, // also offload decryption (not just encryption), opt in for now because of possible instability
OnError: func(err error) {
fmt.Println("kTLS setup failed, using userspace TLS:", err)
},
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello from kTLS")
})
http.Serve(ln, mux)
}Request.TLS is populated correctly even when kTLS is active, so middleware that checks for TLS (HSTS, cert info, etc.) works as expected.
Listener.Accept()accepts a TCP connection and does a normal TLS 1.3 handshake viacrypto/tls- During the handshake, a
KeyLogWritercaptures the traffic secrets thatcrypto/tlsoutputs in NSS key log format - A record counter sits between the raw TCP connection and
tls.Conn, counting application data records to determine the correct RX sequence number - Any data already decrypted by
tls.Connduring the handshake gets drained so it's not lost - The library parses the key log to extract
SERVER_TRAFFIC_SECRET_0andCLIENT_TRAFFIC_SECRET_0, derives the encryption key and IV via HKDF-Expand-Label (RFC 8446 section 7.1), and packs them into the kernel'scrypto_infostruct setsockoptwithSOL_TLS/TLS_TX/TLS_RXhands the keys to the kernel- The returned
net.Connreads and writes directly through the kernel TLS layer
If any step fails, Accept() returns the original tls.Conn and calls OnError. The connection works fine either way.
When a TLS 1.3 client sends a KeyUpdate message, the kernel pauses decryption and returns EKEYEXPIRED on the next read. The library handles this transparently: it derives the next traffic secret using HKDF-Expand-Label with the "traffic upd" label (RFC 8446 section 7.2), re-arms the kernel with the new key via setsockopt, and retries the read. Your application code doesn't need to do anything.
All three TLS 1.3 cipher suites defined by RFC 8446:
TLS_AES_128_GCM_SHA256(0x1301)TLS_AES_256_GCM_SHA384(0x1302)TLS_CHACHA20_POLY1305_SHA256(0x1303)
These are the only cipher suites in TLS 1.3, so all TLS 1.3 connections are eligible for offloading.
type Listener struct {
TCPListener net.Listener
TLSConfig *tls.Config
RX bool
OnError func(error)
}Implements net.Listener. Pass it to http.Serve, http.Server.Serve, or anything else that takes a listener.
TCPListener-- the underlying TCP listenerTLSConfig-- standardcrypto/tlsconfig. Must have at least one certificateRX-- enable kernel-side decryption. TX (encryption) is always enabled when kTLS is activeOnError-- called when kTLS setup fails on a connection. The connection still works through userspace TLS. nil means silently ignore errors
Returns true if the kernel supports kTLS. Tries to set TCP_ULP on a throwaway socket.
HTTP/3 is not supported. kTLS hooks into the TCP stack via TCP_ULP, but HTTP/3 runs over QUIC (UDP), so kernel TLS offloading cannot apply.
Everything compiles on all platforms. Available() returns false, and connections always use userspace TLS.