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 ‣
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.