Hi,
I tested a simple configuration with connecting to a tls backend server
using kTLS.
haproxy.cfg
```
global
expose-experimental-directives
defaults
mode tcp
timeout connect 5s
timeout client 30s
timeout server 30s
frontend my_frontend
bind *:8080
use_backend my_backend
backend my_backend
server my_server 10.1.0.3:4043 ssl verify none ktls on force-tlsv12
```
I've noticed that when connecting to a backend server on loopback
(server my_server 127.0.0.1:4043 ...) HAProxy always enabled kTLS,
but when connecting to an external address it often failed to do so.
I did some digging and it turns out that after calling connect() on a
socket,
HAProxy immediately calls setsockopt(..., TCP_ULP, "tls", ...)
Linux tls module requires the socket to be in TCP_ESTABLISHED state:
https://elixir.bootlin.com/linux/v7.0.12/source/net/tls/tls_main.c#L1059-L1066
If a SYN+ACK packet from the server don't arrive in time when we call
setsockopt(), it will fail with ENOTCONN error.
I've tested on HAProxy 3.4.0 with OpenSSL 3.5.6 and AWS-LC 5.0.0
strace
```
3947 socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 156
3947 fcntl(156, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
3947 setsockopt(156, SOL_TCP, TCP_NODELAY, [1], 4) = 0
3947 connect(156, {sa_family=AF_INET, sin_port=htons(4043),
sin_addr=inet_addr("10.1.0.3")}, 16) = -1 EINPROGRESS (Operation now in
progress)
3947 setsockopt(156, SOL_TCP, TCP_ULP, [7564404], 4) = -1 ENOTCONN
(Transport endpoint is not connected)
3947 recvmsg(155, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="
make teardown-iface \342\200\224 prz"..., iov_len=1024}],
msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 1024
3947 epoll_ctl(148, EPOLL_CTL_ADD, 156,
{events=EPOLLIN|EPOLLOUT|EPOLLRDHUP, data={u32=156, u64=156}}) = 0
3947 clock_gettime(CLOCK_THREAD_CPUTIME_ID, {tv_sec=0,
tv_nsec=4370772}) = 0
3947 epoll_wait(148, [{events=EPOLLOUT, data={u32=156, u64=156}}], 200,
0) = 1
```
haproxy -vv (OpenSSL build):
```
HAProxy version 3.4.0-64a335366 2026/06/03 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2
2031.
Known bugs: http://www.haproxy.org/bugs/bugs-3.4.0.html
Running on: Linux 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri
Mar 13 19:51:50 UTC 2026 x86_64
Build options :
TARGET = linux-glibc
CC = cc
CFLAGS = -O2 -g -fwrapv -fvect-cost-model=very-cheap
OPTIONS = USE_OPENSSL=1 USE_KTLS=1
DEBUG =
Feature list : -51DEGREES +ACCEPT4 +ACME +BACKTRACE -CLOSEFROM
+CPU_AFFINITY +CRYPT_H -DEVICEATLAS +DL -ECH -ENGINE +EPOLL -EVPORTS
+GETADDRINFO +HAVE_TCP_MD5SIG -KQUEUE +KTLS -LIBATOMIC +LIBCRYPT
+LINUX_CAP +LINUX_SPLICE +LINUX_TPROXY -LUA -MATH -MEMORY_PROFILING
+NETFILTER +NS -OBSOLETE_LINKER +OPENSSL -OPENSSL_AWSLC -OPENSSL_WOLFSSL
-OT -PCRE -PCRE2 -PCRE2_JIT -PCRE_JIT +POLL +PRCTL -PROCCTL -PROMEX
-PTHREAD_EMULATION -QUIC -QUIC_OPENSSL_COMPAT +RT +SHM_OPEN +SLZ +SSL
-STATIC_PCRE -STATIC_PCRE2 +TFO +THREAD +THREAD_DUMP +TPROXY +TRACE
-WURFL -ZLIB
Detected feature list : +HAVE_WORKING_TCP_MD5SIG
Default settings :
bufsize = 16384, maxrewrite = 1024, maxpollevents = 200
Built with multi-threading support (MAX_TGROUPS=32, MAX_THREADS=1024,
default=48).
Built with SSL library version : OpenSSL 3.5.6 7 Apr 2026
Running on SSL library version : OpenSSL 3.5.6 7 Apr 2026
SSL library supports TLS extensions : yes
SSL library supports SNI : yes
SSL library default verify directory : /usr/lib/ssl/certs
SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
Built with network namespace support.
Built with libslz for stateless compression.
Compression algorithms supported : identity("identity"),
deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using: IP_TRANSPARENT
IPV6_TRANSPARENT IP_FREEBIND
Built without PCRE or PCRE2 support (using libc's regex instead)
Encrypted password support via crypt(3): yes
Built with gcc compiler version 13.3.0
Available polling systems :
epoll : pref=300, test result OK
poll : pref=200, test result OK
select : pref=150, test result OK
Total: 3 (3 usable), will use epoll.
Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto'
keyword)
h2 : mode=HTTP side=FE|BE mux=H2 flags=HTX|HOL_RISK|NO_UPG
<default> : mode=HTTP side=FE|BE mux=H1 flags=HTX
h1 : mode=HTTP side=FE|BE mux=H1 flags=HTX|NO_UPG
fcgi : mode=HTTP side=BE mux=FCGI flags=HTX|HOL_RISK|NO_UPG
<default> : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
spop : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
<default> : mode=TCP side=FE|BE mux=PASS flags=
none : mode=TCP side=FE|BE mux=PASS flags=NO_UPG
Available services : none
Available filters :
[BWLIM] bwlim-in
[BWLIM] bwlim-out
[CACHE] cache
[COMP] comp-req
[COMP] comp-res
[COMP] compression
[FCGI] fcgi-app
[SPOE] spoe
[TRACE] trace
```
haproxy -vv output (AWS-LC build):
```
HAProxy version 3.4.0-64a335366 2026/06/03 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2
2031.
Known bugs: http://www.haproxy.org/bugs/bugs-3.4.0.html
Running on: Linux 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri
Mar 13 19:51:50 UTC 2026 x86_64
Build options :
TARGET = linux-glibc
CC = cc
CFLAGS = -O2 -g -fwrapv -fvect-cost-model=very-cheap
OPTIONS = USE_OPENSSL_AWSLC=1 USE_KTLS=1
DEBUG =
Feature list : -51DEGREES +ACCEPT4 +ACME +BACKTRACE -CLOSEFROM
+CPU_AFFINITY +CRYPT_H -DEVICEATLAS +DL -ECH -ENGINE +EPOLL -EVPORTS
+GETADDRINFO +HAVE_TCP_MD5SIG -KQUEUE +KTLS -LIBATOMIC +LIBCRYPT
+LINUX_CAP +LINUX_SPLICE +LINUX_TPROXY -LUA -MATH -MEMORY_PROFILING
+NETFILTER +NS -OBSOLETE_LINKER +OPENSSL +OPENSSL_AWSLC -OPENSSL_WOLFSSL
-OT -PCRE -PCRE2 -PCRE2_JIT -PCRE_JIT +POLL +PRCTL -PROCCTL -PROMEX
-PTHREAD_EMULATION -QUIC -QUIC_OPENSSL_COMPAT +RT +SHM_OPEN +SLZ +SSL
-STATIC_PCRE -STATIC_PCRE2 +TFO +THREAD +THREAD_DUMP +TPROXY +TRACE
-WURFL -ZLIB
Detected feature list : +HAVE_WORKING_TCP_MD5SIG
Default settings :
bufsize = 16384, maxrewrite = 1024, maxpollevents = 200
Built with multi-threading support (MAX_TGROUPS=32, MAX_THREADS=1024,
default=48).
Built with SSL library version : OpenSSL 1.1.1 (compatible; AWS-LC
5.0.0)
Running on SSL library version : AWS-LC 5.0.0
SSL library supports TLS extensions : yes
SSL library supports SNI : yes
SSL library FIPS mode : no
SSL library default verify directory : /etc/ssl/certs
SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
Built with network namespace support.
Built with libslz for stateless compression.
Compression algorithms supported : identity("identity"),
deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using: IP_TRANSPARENT
IPV6_TRANSPARENT IP_FREEBIND
Built without PCRE or PCRE2 support (using libc's regex instead)
Encrypted password support via crypt(3): yes
Built with gcc compiler version 13.3.0
Available polling systems :
epoll : pref=300, test result OK
poll : pref=200, test result OK
select : pref=150, test result OK
Total: 3 (3 usable), will use epoll.
Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto'
keyword)
h2 : mode=HTTP side=FE|BE mux=H2 flags=HTX|HOL_RISK|NO_UPG
<default> : mode=HTTP side=FE|BE mux=H1 flags=HTX
h1 : mode=HTTP side=FE|BE mux=H1 flags=HTX|NO_UPG
fcgi : mode=HTTP side=BE mux=FCGI flags=HTX|HOL_RISK|NO_UPG
<default> : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
spop : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
<default> : mode=TCP side=FE|BE mux=PASS flags=
none : mode=TCP side=FE|BE mux=PASS flags=NO_UPG
Available services : none
Available filters :
[BWLIM] bwlim-in
[BWLIM] bwlim-out
[CACHE] cache
[COMP] comp-req
[COMP] comp-res
[COMP] compression
[FCGI] fcgi-app
[SPOE] spoe
[TRACE] trace
```
I've moved the setsockopt(..., TCP_ULP, "tls", ...) call just before
configuring the kTLS keys and it resolved the issue for me.
I've also tested the change on another system with kTLS disbled and
HAProxy
properly falls back to software tls.
From 5f1c0bf40ed9a8549ef7ae7127e39ee77ad0e2b8 Mon Sep 17 00:00:00 2001
From: Karol Kucharski <[email protected]>
Date: Wed, 10 Jun 2026 16:48:18 +0200
Subject: [PATCH] BUG: ktls: defer enabling TLS ULP on a socket until
just
before configuring kTLS keys
The Linux tls module requires a socket to be in TCP_ESTABLISHED state
before we can enable the TLS ULP on the socket, if the socket is in any
other state then setsockopt(..., TCP_ULP, "tls", ...) call will fail
with
ENOTCONN error.
We can enable kTLS on a socket only after the TCP handshake has
completed.
Signed-off-by: Karol Kucharski <[email protected]>
---
include/haproxy/ssl_sock-t.h | 1 +
src/ssl_sock.c | 34 ++++++++++++++++++++++++++--------
2 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/include/haproxy/ssl_sock-t.h b/include/haproxy/ssl_sock-t.h
index 6af6dea9f..c40c55b33 100644
--- a/include/haproxy/ssl_sock-t.h
+++ b/include/haproxy/ssl_sock-t.h
@@ -255,6 +255,7 @@ struct ssl_keylog {
#define SSL_SOCK_F_KTLS_RECV (1 << 3) /* kTLS receive is
configure on that socket */
#define SSL_SOCK_F_CTRL_SEND (1 << 4) /* We want to send a
kTLS control message for that socket */
#define SSL_SOCK_F_HAS_ALPN (1 << 5) /* An ALPN has been
negotiated */
+#define SSL_SOCK_F_KTLS_ULP (1 << 6) /* TLS ULP is enabled
on that socket */
struct ssl_sock_ctx {
struct connection *conn;
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 4c703fe5c..84066c20e 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -439,6 +439,20 @@ static int ha_ssl_read(BIO *h, char *buf, int size)
}
#ifdef HA_USE_KTLS
+/* Returns 0 on success, -1 on failure */
+static int ktls_enable_ulp(struct ssl_sock_ctx *ctx)
+{
+ int ret = 0;
+
+ if (!(ctx->flags & SSL_SOCK_F_KTLS_ULP)) {
+ ret = setsockopt(ctx->conn->handle.fd, SOL_TCP, TCP_ULP, "tls",
sizeof("tls"));
+ if (ret == 0)
+ ctx->flags |= SSL_SOCK_F_KTLS_ULP;
+ }
+
+ return ret;
+}
+
/* Returns 0 on success, -1 on failure */
static int ktls_set_key(struct ssl_sock_ctx *ctx, void *info, size_t
info_len, int is_tx)
{
@@ -485,6 +499,12 @@ static long ha_ssl_ctrl(BIO *h, int cmd, long arg1,
void *arg2)
if (!(ctx->flags & SSL_SOCK_F_KTLS_ENABLED))
return 0;
+
+ if (ktls_enable_ulp(ctx) != 0) {
+ ctx->flags &= ~SSL_SOCK_F_KTLS_ENABLED;
+ return 0;
+ }
+
/*
* As OpenSSL doesn't export struct tls_crypto_info_all,
* and it puts the size at the end of the struct,
@@ -5670,14 +5690,7 @@ static int ssl_sock_start(struct connection
*conn, void *xprt_ctx)
if (ret < 0)
return ret;
}
-#ifdef HA_USE_KTLS
- /*
- * Make the socket usable for kTLS. That does not mean that we will
- * use kTLS, though, just that the socket will be able to do it.
- */
- if ((ctx->flags & SSL_SOCK_F_KTLS_ENABLED) &&
setsockopt(conn->handle.fd, SOL_TCP, TCP_ULP, "tls", sizeof("tls")) !=
0)
- ctx->flags &= ~SSL_SOCK_F_KTLS_ENABLED;
-#endif
+
tasklet_wakeup(ctx->wait_event.tasklet);
return 0;
@@ -6617,6 +6630,11 @@ static void ssl_sock_setup_ktls(struct
ssl_sock_ctx *ctx)
if (!(ctx->flags & SSL_SOCK_F_KTLS_ENABLED))
return;
+ if (ktls_enable_ulp(ctx) != 0) {
+ ctx->flags &= ~SSL_SOCK_F_KTLS_ENABLED;
+ return;
+ }
+
switch (SSL_version(ctx->ssl)) {
case TLS_1_2_VERSION:
is_tls_12 = 1;
--
2.47.3