Optional kernel TLS with OpenSSL (ssl_ktls)

ssl_ktls asks OpenSSL to hand TLS to the kernel (SSL_OP_ENABLE_KTLS)
on the connections of the services which follow, both to clients and
to servers; ssl_noktls, the default, keeps it in 3proxy. Where the
library, the kernel and the cipher allow it, the kernel then encrypts
and decrypts, and SSL_read/SSL_write go on working over it. What a
service got is logged on its first TLS connection.

Reading ahead is left off where the kernel took receiving, which reads
a record at a time. A build whose library has no kernel TLS (wolfSSL,
OpenSSL before 3.0 or without it) warns and ignores the command.
splice() is not used for TLS services yet.

Tested on AlmaLinux 10 (OpenSSL 3.5.8, tls module): both directions
of both legs of a TLS chain in the kernel, the suite passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-14 17:29:21 +03:00
parent 07fd3083fa
commit d0e88a7b88
6 changed files with 153 additions and 6 deletions

View File

@ -1380,6 +1380,16 @@ ssl_client_mode</b> <i>mode</i> - when to establish TLS
connection: 0 - on connect (default), 1 - after
authentication, 2 - before data, 3 - only for secure parent
types (ending with &rsquo;s&rsquo;) <b><br>
ssl_ktls</b> - use kernel TLS (Linux, FreeBSD) for the
connections of the services which follow, where the TLS
library, the kernel and the negotiated cipher allow it: the
kernel then encrypts and decrypts the data; <b>ssl_noktls</b>
(default) keeps it in 3proxy. Needs OpenSSL 3.0 or later built
with kernel TLS (enable-ktls) and, on Linux, the tls kernel
module; receiving with TLS 1.3 needs a later OpenSSL than 3.0.
Whether the kernel took sending and receiving is logged on the
first TLS connection of a service. Not supported with wolfSSL.
<b><br>
ssl_certcache</b> <i>/path/to/cache/</i> - location for the
generated MITM certificates cache</p>

View File

@ -1588,6 +1588,15 @@ SSL/TLS can be used for:
.BR ssl_client_mode
\fImode\fR - when to establish TLS connection: 0 - on connect (default), 1 - after authentication, 2 - before data, 3 - only for secure parent types (ending with 's')
.br
.BR ssl_ktls
- use kernel TLS (Linux, FreeBSD) for the connections of the services which
follow, where the TLS library, the kernel and the negotiated cipher allow it:
the kernel then encrypts and decrypts the data; \fBssl_noktls\fR (default)
keeps it in 3proxy. Needs OpenSSL 3.0 or later built with kernel TLS
(enable-ktls) and, on Linux, the tls kernel module; receiving with TLS 1.3
needs a later OpenSSL than 3.0. Whether the kernel took sending and receiving is
logged on the first TLS connection of a service. Not supported with wolfSSL.
.br
.BR ssl_certcache
\fI/path/to/cache/\fR - location for the generated MITM certificates cache

View File

@ -2046,6 +2046,8 @@ int h_no_client_verify(int argc, unsigned char **argv);
int h_server_verify(int argc, unsigned char **argv);
int h_no_server_verify(int argc, unsigned char **argv);
int h_client_mode(int argc, unsigned char **argv);
int h_ktls(int argc, unsigned char **argv);
int h_noktls(int argc, unsigned char **argv);
#endif
#ifdef WITH_HTTPSRV
int h_http_content_type(int argc, unsigned char **argv);
@ -2183,6 +2185,8 @@ struct commands commandhandlers[]={
{NULL, "ssl_client_sni", h_client_sni, 1, 2},
{NULL, "ssl_client_alpn", h_client_alpn, 1, 0},
{NULL, "ssl_client_mode", h_client_mode, 1, 2},
{NULL, "ssl_ktls", h_ktls, 1, 1},
{NULL, "ssl_noktls", h_noktls, 1, 1},
{NULL, "ssl_certcache", h_certcache, 2, 2},
#endif
#ifdef WITH_TRANSPARENT

View File

@ -69,6 +69,7 @@ static char * client_cipher_list = NULL;
static char * server_cipher_list = NULL;
static char * client_sni = NULL;
static int client_mode = 0;
static int ktls = 0;
struct SSLsock {
@ -393,6 +394,32 @@ SSL_CONN ssl_handshake_to_client(SOCKET s, SSL_CONFIG *config, X509 *server_cert
#define PCONF (((struct SSLstate *)param->sostate)->config)
/* After a handshake: read ahead, unless the kernel took receiving (kernel TLS
reads a record at a time), and say once for the service whether it took
either direction, which depends on the library, the kernel and the cipher. */
static void ssl_after_handshake(struct clientparam* param, SSL_CONN conn){
SSL *ssl = ((ssl_conn *)conn)->ssl;
int ksend = 0, krecv = 0;
SSL_set_mode(ssl, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_AUTO_RETRY);
#if defined(SSL_OP_ENABLE_KTLS) && defined(BIO_get_ktls_send) && defined(BIO_get_ktls_recv)
if(PCONF->ktls){
ksend = BIO_get_ktls_send(SSL_get_wbio(ssl));
krecv = BIO_get_ktls_recv(SSL_get_rbio(ssl));
if(!PCONF->ktls_reported){
char buf[96];
PCONF->ktls_reported = 1;
sprintf(buf, "kernel TLS: sending %s, receiving %s",
ksend? "in the kernel" : "in 3proxy", krecv? "in the kernel" : "in 3proxy");
param->srv->logfunc(param, (unsigned char *)buf);
}
}
#endif
if(!krecv) SSL_set_read_ahead(ssl, SSL_READAHEAD);
(void)ksend;
}
SSL_CONN dosrvcon(struct clientparam* param, SSL_CERT* cert){
SSL_CONN ServerConn;
char *errSSL=NULL;
@ -408,8 +435,7 @@ SSL_CONN dosrvcon(struct clientparam* param, SSL_CERT* cert){
return NULL;
}
SSL_set_mode((SSL *)((ssl_conn *)ServerConn)->ssl, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_AUTO_RETRY);
SSL_set_read_ahead((SSL *)((ssl_conn *)ServerConn)->ssl, SSL_READAHEAD);
ssl_after_handshake(param, ServerConn);
return ServerConn;
}
@ -441,8 +467,7 @@ int domitm(struct clientparam* param){
return 3;
}
SSL_set_mode((SSL *)((ssl_conn *)ClientConn)->ssl, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_AUTO_RETRY);
SSL_set_read_ahead((SSL *)((ssl_conn *)ClientConn)->ssl, SSL_READAHEAD);
ssl_after_handshake(param, ClientConn);
addSSL(param->clisock, ClientConn, param->remsock, ServerConn, param);
@ -549,6 +574,9 @@ SSL_CTX * ssl_cli_ctx(SSL_CONFIG *config, X509 *server_cert, EVP_PKEY *server_ke
}
}
SSL_CTX_set_session_id_context(ctx, (const unsigned char *)"3proxy", 6);
#ifdef SSL_OP_ENABLE_KTLS
if(config->ktls) SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);
#endif
if(config->server_min_proto_version)SSL_CTX_set_min_proto_version(ctx, config->server_min_proto_version);
if(config->server_max_proto_version)SSL_CTX_set_max_proto_version(ctx, config->server_max_proto_version);
if(config->server_cipher_list)SSL_CTX_set_cipher_list(ctx, config->server_cipher_list);
@ -589,6 +617,7 @@ static void* ssl_filter_open(void * idata, struct srvparam * srv){
sc->server_min_proto_version = server_min_proto_version;
sc->server_max_proto_version = server_max_proto_version;
sc->client_mode = client_mode;
sc->ktls = ktls;
sc->timeout = srv->timeouts[CONNECT_TO];
sc->client_verify = client_verify;
sc->server_verify = server_verify;
@ -707,6 +736,9 @@ static void* ssl_filter_open(void * idata, struct srvparam * srv){
fprintf(stderr, "failed to set client context\n");
sc->mitm = sc->cli = 0;
}
#ifdef SSL_OP_ENABLE_KTLS
if(sc->srv_ctx && sc->ktls) SSL_CTX_set_options(sc->srv_ctx, SSL_OP_ENABLE_KTLS);
#endif
if(sc->client_cert){
SSL_CTX_use_certificate(sc->srv_ctx, (X509 *) sc->client_cert);
ssl_ctx_use_pkey(sc->srv_ctx, sc->client_key);
@ -774,8 +806,7 @@ static FILTER_ACTION ssl_filter_client(void *fo, struct clientparam * param, voi
if(err)param->srv->logfunc(param, (unsigned char *)err);
return REJECT;
}
SSL_set_mode((SSL *)((ssl_conn *)ClientConn)->ssl, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_AUTO_RETRY);
SSL_set_read_ahead((SSL *)((ssl_conn *)ClientConn)->ssl, SSL_READAHEAD);
ssl_after_handshake(param, ClientConn);
addSSL(param->clisock, ClientConn, INVALID_SOCKET, NULL, param);
}
return CONTINUE;
@ -1184,6 +1215,20 @@ int h_no_server_verify(int argc, unsigned char **argv){
return 0;
}
int h_ktls(int argc, unsigned char **argv){
#ifdef SSL_OP_ENABLE_KTLS
ktls = 1;
#else
fprintf(stderr, "ssl_ktls: kernel TLS is not supported by this TLS library, ignored\n");
#endif
return 0;
}
int h_noktls(int argc, unsigned char **argv){
ktls = 0;
return 0;
}
static struct symbol ssl_symbols[] = {
{NULL, "ssl_parent", (void *)&ssl_parent},
};
@ -1214,6 +1259,7 @@ void ssl_install(void){
client_verify = 0;
server_verify = 0;
client_mode = 0;
ktls = 0;
free(client_ciphersuites);
client_ciphersuites = NULL;
free(server_ciphersuites);

View File

@ -52,6 +52,8 @@ struct ssl_config {
int client_verify;
int server_verify;
int client_mode;
int ktls; /* ssl_ktls: kernel TLS asked for (SSL_OP_ENABLE_KTLS) */
int ktls_reported; /* whether the kernel took it has been logged */
int timeout; /* seconds a handshake step may wait: the service's CONNECT_TO */
};

76
tests/cases/ktls.py Normal file
View File

@ -0,0 +1,76 @@
"""Kernel TLS (ssl_ktls): a TLS chain works the same whether or not the
kernel takes the encryption, and the proxy says which it did.
Whether it does depends on the TLS library (OpenSSL built with enable-ktls),
the kernel (the tls module on Linux) and the cipher, so the case checks the
data and the report, not that the kernel took it.
"""
def run(t):
certs = t.certs()
if not certs:
t.skip("kernel TLS (openssl is not available to generate certificates)")
return
if not certs.verified:
t.fail("the generated certificate chain verifies", "OK",
certs.verify_output or "openssl verify failed")
return
origin = t.free_port()
parent = t.free_port()
client = t.free_port()
server = t.start("ktls", f"""
log
auth iponly
allow *
http echo * /echo**
http data * /data
httpsrv -p{origin}
flush
ssl_ktls
ssl_server_cert {certs.server}
ssl_server_key {certs.server_key}
ssl_serv
auth iponly
allow *
proxy -p{parent}
flush
ssl_noserv
auth iponly
allow *
parent 1000 connects 127.0.0.1 {parent}
ssl_client_mode 3
ssl_client_ca_file {certs.ca}
ssl_client_verify
ssl_cli
proxy -p{client}
""", ports=[origin, parent, client])
out = server.output()
if "Unknown command" in out:
t.skip("kernel TLS (this build has no SSL support)")
return
through = f"127.0.0.1:{client}"
r = t.http(f"http://127.0.0.1:{origin}/echo", proxy=through)
t.eq(200, r.status, "a request through the TLS chain with ssl_ktls arrives")
t.contains(r, "path=/echo", "the origin sees it")
# many records, both ways through both TLS legs
for size in (100000, 3000000):
t.eq(size, t.http(f"http://127.0.0.1:{origin}/data?size={size}",
proxy=through).length,
f"a {size} byte body survives the kernel TLS chain")
t.eq(200000, t.http(f"http://127.0.0.1:{origin}/data?size=200000&chunked=1",
proxy=through).length,
"a chunked body survives the kernel TLS chain")
if "not supported by this TLS library" in server.output():
t.skip("the kernel TLS report (this TLS library has no kernel TLS)")
return
log = t.wait_output(server, "kernel TLS:")
t.contains(log, "kernel TLS: sending", "the proxy says whether the kernel took TLS")