Switch OpenSSL::SSL::Socket from BIO to SSL_set_fd#16641
Switch OpenSSL::SSL::Socket from BIO to SSL_set_fd#16641carlhoerberg wants to merge 1 commit intocrystal-lang:masterfrom
Conversation
72f8a1e to
ff40bd0
Compare
Replace the Crystal BIO wrapper with SSL_set_fd() so OpenSSL operates directly on the socket file descriptor. This enables the kernel to intercept socket operations for kTLS (kernel TLS) when available. - Add SSL_set_fd, SSL_get_rbio, SSL_get_wbio bindings to LibSSL - Add BIO_ctrl binding to LibCrypto - Type-restrict `io` parameter to `::Socket` (all callers already pass TCPSocket) - Handle WANT_READ/WANT_WRITE during handshake, read, write, and shutdown by waiting via Crystal::EventLoop - Enable ENABLE_PARTIAL_WRITE mode for proper non-blocking write behavior with SSL_set_fd - Add ktls_send?/ktls_recv? status methods - Simplify timeout/address property delegation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ff40bd0 to
08f08bf
Compare
| if @io.is_a?(IO::Buffered) | ||
| @io.sync = true | ||
| @io.read_buffering = false |
There was a problem hiding this comment.
issue: If we restrict @io to ::Socket, the restriction to IO::Buffered is unnecessary.
| error = LibSSL.ssl_get_error(@ssl, ret) | ||
| case error | ||
| when .want_read? then wait_readable | ||
| when .want_write? then wait_writable |
There was a problem hiding this comment.
question: Does it make sense to handle want_write? for a read operation? And similarly want_read? for write operations?
There was a problem hiding this comment.
I think there's a major issue with this approach: the main loop (try accept/read/write -> wait readable/writable) assumes that the socket has been configured for nonblocking IO, but IOCP and io_uring set the socket as blocking, so every SSL_read and SSL_write will always block the thread...
I believe SSL_set_fd is incompatible with the IOCP evloop on Windows, as demonstrated by the CI hanging. I assume io_uring will have the exact same issue (read/write must happen through io_uring).
Reading https://www.kernel.org/doc/html/latest/networking/tls.html we only need a TLS library for the handshake, then we can recv/send (or read/write) normally (i.e. use the evloop directly) at the exception of TLS control messages. But that's a very drastic and complex change.
Maybe we can make the BIO aware of/compatible with KTLS somehow? There's zero documentation (not even for implementing custom BIOs) and it probably relies on a few internal constants, but it should be possible: https://github.com/openssl/openssl/blob/baf4156f7052cf5fa08aaf5187dc1f5d25e49664/crypto/bio/bss_sock.c
NOTE: the loop also has a minor issue (fixable): it's subject to timeout drift: each wait will restart from the IO's configured timeout, pushing the deadline further on each iteration, instead of being a fixed deadline.
|
I suppose this can be closed since we already merged kTLS support in #16646? |
Summary
SSL_set_fd()so OpenSSL operates directly on the socket file descriptor, enabling kTLS (kernel TLS) offload when availableWANT_READ/WANT_WRITEduring handshake, read, write, and shutdown by waiting viaCrystal::EventLoop, making the SSL socket properly non-blockingktls_send?/ktls_recv?methods to query kTLS statusDetails
The previous implementation used a custom
OpenSSL::BIOthat proxied all I/O through Crystal'sIOinterface. This prevented the kernel from intercepting socket operations for kTLS. By switching toSSL_set_fd, OpenSSL reads/writes directly on the socket fd, which allows the kernel's kTLS module to take over encryption when supported.New bindings:
SSL_set_fd,SSL_get_rbio,SSL_get_wbio(LibSSL),BIO_ctrl(LibCrypto)Breaking change: The
ioparameter ofOpenSSL::SSL::Socketis now type-restricted to::Socket(was untypedIO). In practice all callers (HTTP::Client,HTTP::WebSocket,OpenSSL::SSL::Server, all specs) already passTCPSocket, so nothing should break.Test plan
crystal spec spec/std/openssl/ssl/socket_spec.cr— 11 examples, 0 failurescrystal spec spec/std/openssl/ssl/server_spec.cr— 8 examples, 0 failurescrystal spec spec/std/http/server/server_spec.cr— 31 examples, 0 failurescrystal spec spec/std/http/client/client_spec.cr— 35 examples, 0 failures🤖 Generated with Claude Code