Skip to content

Switch OpenSSL::SSL::Socket from BIO to SSL_set_fd#16641

Closed
carlhoerberg wants to merge 1 commit intocrystal-lang:masterfrom
carlhoerberg:feature/ssl-socket-use-ssl-set-fd
Closed

Switch OpenSSL::SSL::Socket from BIO to SSL_set_fd#16641
carlhoerberg wants to merge 1 commit intocrystal-lang:masterfrom
carlhoerberg:feature/ssl-socket-use-ssl-set-fd

Conversation

@carlhoerberg
Copy link
Copy Markdown
Contributor

Summary

  • Replace the custom Crystal BIO wrapper with SSL_set_fd() so OpenSSL operates directly on the socket file descriptor, enabling kTLS (kernel TLS) offload when available
  • Handle WANT_READ/WANT_WRITE during handshake, read, write, and shutdown by waiting via Crystal::EventLoop, making the SSL socket properly non-blocking
  • Add ktls_send?/ktls_recv? methods to query kTLS status

Details

The previous implementation used a custom OpenSSL::BIO that proxied all I/O through Crystal's IO interface. This prevented the kernel from intercepting socket operations for kTLS. By switching to SSL_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 io parameter of OpenSSL::SSL::Socket is now type-restricted to ::Socket (was untyped IO). In practice all callers (HTTP::Client, HTTP::WebSocket, OpenSSL::SSL::Server, all specs) already pass TCPSocket, so nothing should break.

Test plan

  • crystal spec spec/std/openssl/ssl/socket_spec.cr — 11 examples, 0 failures
  • crystal spec spec/std/openssl/ssl/server_spec.cr — 8 examples, 0 failures
  • crystal spec spec/std/http/server/server_spec.cr — 31 examples, 0 failures
  • crystal spec spec/std/http/client/client_spec.cr — 35 examples, 0 failures

🤖 Generated with Claude Code

@carlhoerberg carlhoerberg force-pushed the feature/ssl-socket-use-ssl-set-fd branch from 72f8a1e to ff40bd0 Compare February 5, 2026 20:56
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>
@carlhoerberg carlhoerberg force-pushed the feature/ssl-socket-use-ssl-set-fd branch from ff40bd0 to 08f08bf Compare February 5, 2026 21:06
Comment on lines +122 to +124
if @io.is_a?(IO::Buffered)
@io.sync = true
@io.read_buffering = false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Does it make sense to handle want_write? for a read operation? And similarly want_read? for write operations?

Copy link
Copy Markdown
Collaborator

@ysbaddaden ysbaddaden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@straight-shoota
Copy link
Copy Markdown
Member

I suppose this can be closed since we already merged kTLS support in #16646?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants