3proxy/tests/cases/ktls.py
Vladimir Dubrovin cfe52878d0 splice() over kernel TLS
A service with ssl_ktls keeps its splice() mode. A connection whose TLS
sockets all got kernel TLS both ways may splice, as a plain one does when
no data filters apply; one where the kernel did not take TLS (nosplice)
copies as before.

TLS may hold data for a socket that a splice() from it would not see:
OpenSSL reads more from a kernel TLS socket than it is asked for. sockmap
asks the TLS code (splicetls) and reads that through TLS into the buffer
first, once the pipe has drained, so the order is kept, then splices on.
A read from a TLS socket counts as large at 4K, TLS handing out a record
at a time, and the short read collecting what TLS left from the last one
does not end a run of large ones. A splice() refused because a record is
not data (an alert, a key update, a ticket) falls back to reading it
through TLS instead of ending the connection.

The kernel TLS test compares the bodies with the origin's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:50:02 +03:00

81 lines
2.8 KiB
Python

"""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; where the kernel took
# TLS a large body is spliced, so what arrives is compared with the origin
for size in (100000, 3000000):
url = f"http://127.0.0.1:{origin}/data?size={size}"
got = t.http(url, proxy=through)
t.eq(size, got.length, f"a {size} byte body survives the kernel TLS chain")
t.ok(f"the {size} byte body arrives unchanged") if got.body == t.http(url).body \
else t.fail(f"the {size} byte body arrives unchanged", "the origin's bytes",
"different bytes")
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")