In the code example below, I can see that kTLS is not used when context.wrap_socket is called with an unconnected socket and it is used when a preconnected socket is passed.

Wrapping the unconnected socket is similar to an example from the ssl module docs.

https://github.com/python/cpython/pull/99907 compiled against both the system OpenSSL 3.2.4 and OpenSSL 3.6.0-dev from its master branch produce the same result.

import socket
import ssl

import certifi

def check_ktls(sslobj):
    print(f"kTLS send={sslobj.uses_ktls_for_send()} read={sslobj.uses_ktls_for_read()}")

hostname = "www.python.org"
port = 443

context = ssl.create_default_context()
context.load_verify_locations(certifi.where(), None, None)
context.options |= ssl.OP_ENABLE_KTLS

print("Wrapping not connected socket")
with context.wrap_socket(socket.socket(), server_hostname=hostname) as ssock:
    ssock.connect((hostname, port))
    ssock.send(b"GET / HTTP/1.1\\r\\n\\r\\n")
    check_ktls(ssock._sslobj)
    print(ssock.cipher())

print()
print("Wrapping connected socket")
with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        ssock.send(b"GET / HTTP/1.1\\r\\n\\r\\n")
        check_ktls(ssock._sslobj)
        print(ssock.cipher())
$ sudo modprobe tls  # Fedora 41
$ ./python check.py
Wrapping not connected socket
kTLS send=False read=False
('TLS_AES_128_GCM_SHA256', 'TLSv1.3', 128)

Wrapping connected socket
kTLS send=True read=True
('TLS_AES_128_GCM_SHA256', 'TLSv1.3', 128)

Output of strace -o no_ktls_trace.log -f -e trace=%network,read,write,setsockopt ./python [check.py](<http://check.py>) contains setsockopt(*5*, SOL_TCP, TCP_ULP, [*7564404*], *4*) = -*1* ENOTCONN (Transport endpoint is not connected) for unconnected socket.

The same call for the preconnected socket has status 0.

After some investigation, I made a small rearrangement of existing code lines which resulted in both cases using kTLS

diff --git a/Lib/ssl.py b/Lib/ssl.py
index b2b666ec4fd..e47c1b4e35d 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -1395,16 +1395,16 @@ def _real_connect(self, addr, connect_ex):
         # connected at the time of the call.  We connect it, then wrap it.
         if self._connected or self._sslobj is not None:
             raise ValueError("attempt to connect already-connected SSLSocket!")
-        self._sslobj = self.context._wrap_socket(
-            self, False, self.server_hostname,
-            owner=self, session=self._session
-        )
         try:
             if connect_ex:
                 rc = super().connect_ex(addr)
             else:
                 rc = None
                 super().connect(addr)
+            self._sslobj = self.context._wrap_socket(
+                self, False, self.server_hostname,
+                owner=self, session=self._session
+            )
             if not rc:
                 self._connected = True
                 if self.do_handshake_on_connect:

_ssl__SSLContext__wrap_socket_impl calls newPySSLSocket which calls SSL_set_fd which calls ktls_enable which calls setsockopt(fd, SOL_TCP, TCP_ULP, "tls", sizeof("tls")) that appears in the strace output.

An OpenSSL issue mentioning the same error with my reproducer ‣

C reproducer displaying that rearranging “Bind the socket to SSL” and “Connect to the server” affects kTLS enablement

I had a feeling that this may be a security issue for CPython. For example, that a user can set a different important option and it may be ignored implicitly when an unconnected socket is wrapped. So I checked whether this issue has any effect on any other SSL options. Other options configured handshake behavior mostly, and I didn’t find any ignored. verify_flags and set_ciphers with a security level seem not to be affected too.