From 0f8130ff23804356d61c649c21058bc6530d3176 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 20 Feb 2026 20:02:57 -0800 Subject: [PATCH 01/20] Add QUIC support This commit exposes various QUIC methods so that you can build QUIC clients and servers --- ext/openssl/extconf.rb | 23 ++ ext/openssl/ossl.h | 4 + ext/openssl/ossl_ssl.c | 624 ++++++++++++++++++++++++++++++++++++++ lib/openssl/buffering.rb | 10 +- lib/openssl/ssl.rb | 35 +++ test/openssl/test_quic.rb | 162 ++++++++++ 6 files changed, 855 insertions(+), 3 deletions(-) create mode 100644 test/openssl/test_quic.rb diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index a897c86b6..f75d9e752 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -169,6 +169,29 @@ def find_openssl_library # added in 3.5.0 have_func("SSL_get0_peer_signature_name(NULL, NULL)", ssl_h) +# QUIC support - added in OpenSSL 3.2.0 +have_func("OSSL_QUIC_client_method()", ssl_h) +have_func("OSSL_QUIC_client_thread_method()", ssl_h) +have_func("SSL_new_stream(NULL, 0)", ssl_h) +have_func("SSL_accept_stream(NULL, 0)", ssl_h) +have_func("SSL_stream_conclude(NULL)", ssl_h) +have_func("SSL_get_stream_id(NULL)", ssl_h) +have_func("SSL_set_default_stream_mode(NULL, 0)", ssl_h) +have_func("SSL_set_blocking_mode(NULL, 0)", ssl_h) +have_func("SSL_get_blocking_mode(NULL)", ssl_h) +have_func("SSL_handle_events(NULL)", ssl_h) +have_func("SSL_get_event_timeout(NULL, NULL, NULL)", ssl_h) +have_func("SSL_get0_connection(NULL)", ssl_h) +have_func("SSL_is_connection(NULL)", ssl_h) +have_func("SSL_set1_initial_peer_addr(NULL, NULL)", ssl_h) +have_func("OSSL_QUIC_server_method()", ssl_h) +have_func("SSL_new_listener(NULL, 0)", ssl_h) +have_func("SSL_accept_connection(NULL, 0)", ssl_h) +have_func("SSL_get_accept_connection_queue_len(NULL)", ssl_h) +have_func("SSL_listen(NULL)", ssl_h) +have_func("SSL_poll(NULL, 0, 0, NULL, 0, NULL)", ssl_h) +have_func("SSL_set_incoming_stream_policy(NULL, 0, 0)", ssl_h) + Logging::message "=== Checking done. ===\n" # Append flags from environment variables. diff --git a/ext/openssl/ossl.h b/ext/openssl/ossl.h index 0b479a720..02563b2d5 100644 --- a/ext/openssl/ossl.h +++ b/ext/openssl/ossl.h @@ -78,6 +78,10 @@ # define OSSL_HAVE_IMMUTABLE_PKEY #endif +#if !OSSL_IS_LIBRESSL && defined(HAVE_OSSL_QUIC_CLIENT_METHOD) +# define OSSL_USE_QUIC +#endif + /* * Common Module */ diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index c6dec32a9..196562021 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1552,6 +1552,63 @@ ossl_sslctx_flush_sessions(int argc, VALUE *argv, VALUE self) return self; } +/* + * QUIC support + */ +#ifdef OSSL_USE_QUIC +/* + * call-seq: + * SSLContext.quic(:client) -> ctx + * SSLContext.quic(:client_thread) -> ctx + * SSLContext.quic(:server) -> ctx + * + * Creates a new SSLContext for QUIC. The argument specifies the QUIC mode. + * Requires OpenSSL 3.2+. + */ +static VALUE +ossl_sslctx_s_quic(VALUE klass, VALUE quic_sym) +{ + SSL_CTX *ctx; + const SSL_METHOD *method; + long mode; + VALUE obj; + ID quic_id; + + Check_Type(quic_sym, T_SYMBOL); + quic_id = SYM2ID(quic_sym); + + if (quic_id == rb_intern("client")) + method = OSSL_QUIC_client_method(); +#ifdef HAVE_OSSL_QUIC_CLIENT_THREAD_METHOD + else if (quic_id == rb_intern("client_thread")) + method = OSSL_QUIC_client_thread_method(); +#endif +#ifdef HAVE_OSSL_QUIC_SERVER_METHOD + else if (quic_id == rb_intern("server")) + method = OSSL_QUIC_server_method(); +#endif + else + ossl_raise(rb_eArgError, "unknown QUIC mode: %"PRIsVALUE, quic_sym); + + obj = TypedData_Wrap_Struct(klass, &ossl_sslctx_type, 0); + ctx = SSL_CTX_new(method); + if (!ctx) + ossl_raise(eSSLError, "SSL_CTX_new"); + + mode = SSL_MODE_ENABLE_PARTIAL_WRITE | + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | + SSL_MODE_RELEASE_BUFFERS; + SSL_CTX_set_mode(ctx, mode); + RTYPEDDATA_DATA(obj) = ctx; + SSL_CTX_set_ex_data(ctx, ossl_sslctx_ex_ptr_idx, (void *)obj); + + rb_obj_call_init(obj, 0, NULL); + rb_ivar_set(obj, rb_intern("@quic"), quic_sym); + + return obj; +} +#endif + /* * SSLSocket class */ @@ -1674,6 +1731,11 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) SSL_set_ex_data(ssl, ossl_ssl_ex_ptr_idx, (void *)self); SSL_set_info_callback(ssl, ssl_info_cb); +#ifdef HAVE_SSL_SET_BLOCKING_MODE + // Always set non-blocking mode for QUIC connections + // This is a no-op on non-QUIC connections + SSL_set_blocking_mode(ssl, 0); +#endif rb_call_super(0, NULL); @@ -2723,6 +2785,496 @@ ossl_ssl_get_group(VALUE self) } #endif +/* + * QUIC stream and event methods + */ +#ifdef OSSL_USE_QUIC +static ID id_i_connection; + +/* + * call-seq: + * ssl.new_stream(flags = 0) => SSLSocket or nil + * + * Creates a new QUIC stream on this connection. Returns a new SSLSocket + * representing the stream. The +flags+ parameter can include + * OpenSSL::SSL::STREAM_FLAG_UNI to create a unidirectional stream. + * + * When STREAM_FLAG_NO_BLOCK is set, returns +nil+ if the stream cannot be + * created immediately (e.g. the handshake is not yet complete). Without + * NO_BLOCK, raises SSLError on failure. + */ +static VALUE +ossl_ssl_new_stream(int argc, VALUE *argv, VALUE self) +{ + SSL *ssl, *stream_ssl; + VALUE flags_v, stream_obj; + uint64_t flags = 0; + + rb_scan_args(argc, argv, "01", &flags_v); + if (!NIL_P(flags_v)) + flags = NUM2UINT64T(flags_v); + + GetSSL(self, ssl); + stream_ssl = SSL_new_stream(ssl, flags); + if (!stream_ssl) { + if (flags & SSL_STREAM_FLAG_NO_BLOCK) + return Qnil; + ossl_raise(eSSLError, "SSL_new_stream"); + } + + stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, stream_ssl); + SSL_set_ex_data(stream_ssl, ossl_ssl_ex_ptr_idx, (void *)stream_obj); + + /* Set @io and @context from the parent, and @connection to prevent GC */ + rb_ivar_set(stream_obj, id_i_io, rb_attr_get(self, id_i_io)); + rb_ivar_set(stream_obj, id_i_context, rb_attr_get(self, id_i_context)); + rb_ivar_set(stream_obj, id_i_connection, self); + rb_funcall(stream_obj, rb_intern("initialize_buffer"), 0); + + return stream_obj; +} + +/* + * call-seq: + * ssl.accept_stream(flags = 0) => SSLSocket or nil + * + * Accepts an incoming QUIC stream from the peer. Returns a new SSLSocket + * representing the stream, or +nil+ if no stream is available (when + * using non-blocking mode or STREAM_FLAG_NO_BLOCK). + */ +static VALUE +ossl_ssl_accept_stream(int argc, VALUE *argv, VALUE self) +{ + SSL *ssl, *stream_ssl; + VALUE flags_v, stream_obj; + uint64_t flags = 0; + + rb_scan_args(argc, argv, "01", &flags_v); + if (!NIL_P(flags_v)) + flags = NUM2UINT64T(flags_v); + + GetSSL(self, ssl); + stream_ssl = SSL_accept_stream(ssl, flags); + if (!stream_ssl) + return Qnil; + + stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, stream_ssl); + SSL_set_ex_data(stream_ssl, ossl_ssl_ex_ptr_idx, (void *)stream_obj); + + rb_ivar_set(stream_obj, id_i_io, rb_attr_get(self, id_i_io)); + rb_ivar_set(stream_obj, id_i_context, rb_attr_get(self, id_i_context)); + rb_ivar_set(stream_obj, id_i_connection, self); + rb_funcall(stream_obj, rb_intern("initialize_buffer"), 0); + + return stream_obj; +} + +/* + * call-seq: + * ssl.stream_conclude => self + * + * Signals FIN on the QUIC stream, indicating that no more data will be sent. + */ +static VALUE +ossl_ssl_stream_conclude(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + if (!SSL_stream_conclude(ssl, 0)) + ossl_raise(eSSLError, "SSL_stream_conclude"); + + return self; +} + +/* + * call-seq: + * ssl.stream_id => Integer + * + * Returns the QUIC stream ID for this SSL object. + */ +static VALUE +ossl_ssl_stream_id(VALUE self) +{ + SSL *ssl; + uint64_t id; + + GetSSL(self, ssl); + id = SSL_get_stream_id(ssl); + return ULL2NUM(id); +} + +/* + * call-seq: + * ssl.default_stream_mode = mode + * + * Sets the default stream mode for a QUIC connection. +mode+ should be + * one of the symbols :none, :auto_bidi, or :auto_uni. + */ +static VALUE +ossl_ssl_set_default_stream_mode(VALUE self, VALUE mode) +{ + SSL *ssl; + uint32_t m; + ID mode_id; + + GetSSL(self, ssl); + + mode_id = SYM2ID(mode); + if (mode_id == rb_intern("none")) + m = SSL_DEFAULT_STREAM_MODE_NONE; + else if (mode_id == rb_intern("auto_bidi")) + m = SSL_DEFAULT_STREAM_MODE_AUTO_BIDI; + else if (mode_id == rb_intern("auto_uni")) + m = SSL_DEFAULT_STREAM_MODE_AUTO_UNI; + else + ossl_raise(rb_eArgError, "unknown default stream mode"); + + if (!SSL_set_default_stream_mode(ssl, m)) + ossl_raise(eSSLError, "SSL_set_default_stream_mode"); + + return mode; +} + +/* + * call-seq: + * ssl.handle_events => nil + * + * Processes any pending QUIC events. This should be called periodically + * when using non-blocking mode. + */ +static VALUE +ossl_ssl_handle_events(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + SSL_handle_events(ssl); + + return Qnil; +} + +/* + * call-seq: + * ssl.net_read_desired? => true or false + * + * Returns +true+ if the QUIC engine wants to read from the network. + * Use this to determine whether to include the underlying socket in the + * read set when calling IO.select. + */ +static VALUE +ossl_ssl_net_read_desired(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return SSL_net_read_desired(ssl) ? Qtrue : Qfalse; +} + +/* + * call-seq: + * ssl.net_write_desired? => true or false + * + * Returns +true+ if the QUIC engine wants to write to the network. + * Use this to determine whether to include the underlying socket in the + * write set when calling IO.select. + */ +static VALUE +ossl_ssl_net_write_desired(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return SSL_net_write_desired(ssl) ? Qtrue : Qfalse; +} + +/* + * call-seq: + * ssl.event_timeout => Float or nil + * + * Returns the amount of time in seconds until the next QUIC timeout event, + * or +nil+ if no timeout is currently active (infinite). + */ +static VALUE +ossl_ssl_event_timeout(VALUE self) +{ + SSL *ssl; + struct timeval tv; + int is_infinite; + + GetSSL(self, ssl); + if (!SSL_get_event_timeout(ssl, &tv, &is_infinite)) + ossl_raise(eSSLError, "SSL_get_event_timeout"); + + if (is_infinite) + return Qnil; + + return DBL2NUM((double)tv.tv_sec + (double)tv.tv_usec / 1000000.0); +} + +/* + * call-seq: + * ssl.connection? => true or false + * + * Returns +true+ if this SSL object represents a QUIC connection (as opposed + * to a QUIC stream). + */ +static VALUE +ossl_ssl_is_connection(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return SSL_is_connection(ssl) ? Qtrue : Qfalse; +} + +/* + * call-seq: + * ssl.init_finished? => true or false + * + * Returns +true+ if the TLS/QUIC handshake has completed for this connection. + */ +static VALUE +ossl_ssl_is_init_finished(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return SSL_is_init_finished(ssl) ? Qtrue : Qfalse; +} + +#ifdef HAVE_SSL_NEW_LISTENER +/* + * call-seq: + * SSLSocket.new_listener(io, context:) => SSLSocket + * + * Creates a new QUIC listener bound to the given UDP socket _io_. + * The _context_ must be an SSLContext created with quic: :server. + */ +static VALUE +ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) +{ + VALUE v_io, opts, v_ctx, listener_obj; + SSL_CTX *ctx; + SSL *listener; + rb_io_t *fptr; + + static ID kw_ids[1]; + VALUE kw_args[1]; + + rb_scan_args(argc, argv, "1:", &v_io, &opts); + + if (!kw_ids[0]) + kw_ids[0] = rb_intern_const("context"); + rb_get_kwargs(opts, kw_ids, 1, 0, kw_args); + v_ctx = kw_args[0]; + + GetSSLCTX(v_ctx, ctx); + ossl_sslctx_setup(v_ctx); + + listener = SSL_new_listener(ctx, 0); + if (!listener) + ossl_raise(eSSLError, "SSL_new_listener"); + + Check_Type(v_io, T_FILE); + GetOpenFile(v_io, fptr); + if (!SSL_set_fd(listener, TO_SOCKET(rb_io_descriptor(v_io)))) { + SSL_free(listener); + ossl_raise(eSSLError, "SSL_set_fd"); + } + + listener_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, listener); + SSL_set_ex_data(listener, ossl_ssl_ex_ptr_idx, (void *)listener_obj); + + rb_ivar_set(listener_obj, id_i_io, v_io); + rb_ivar_set(listener_obj, id_i_context, v_ctx); + rb_funcall(listener_obj, rb_intern("initialize_buffer"), 0); + + return listener_obj; +} +#endif + +#ifdef HAVE_SSL_ACCEPT_CONNECTION +/* + * call-seq: + * ssl.accept_connection(flags = 0) => SSLSocket or nil + * + * Accepts an incoming QUIC connection from the listener. Returns a new + * SSLSocket representing the connection, or +nil+ if no connection is + * available (when using non-blocking mode or ACCEPT_CONNECTION_NO_BLOCK). + */ +static VALUE +ossl_ssl_accept_connection(int argc, VALUE *argv, VALUE self) +{ + SSL *ssl, *conn_ssl; + VALUE flags_v, conn_obj; + uint64_t flags = 0; + + rb_scan_args(argc, argv, "01", &flags_v); + if (!NIL_P(flags_v)) + flags = NUM2UINT64T(flags_v); + + GetSSL(self, ssl); + conn_ssl = SSL_accept_connection(ssl, flags); + if (!conn_ssl) + return Qnil; + + conn_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, conn_ssl); + SSL_set_ex_data(conn_ssl, ossl_ssl_ex_ptr_idx, (void *)conn_obj); + + rb_ivar_set(conn_obj, id_i_io, rb_attr_get(self, id_i_io)); + rb_ivar_set(conn_obj, id_i_context, rb_attr_get(self, id_i_context)); + rb_ivar_set(conn_obj, id_i_connection, self); + rb_funcall(conn_obj, rb_intern("initialize_buffer"), 0); + + return conn_obj; +} +#endif + +#ifdef HAVE_SSL_LISTEN +/* + * call-seq: + * ssl.listen => self + * + * Starts listening for incoming QUIC connections on this listener. + */ +static VALUE +ossl_ssl_listen(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + if (!SSL_listen(ssl)) + ossl_raise(eSSLError, "SSL_listen"); + + return self; +} +#endif + +#ifdef HAVE_SSL_GET_ACCEPT_CONNECTION_QUEUE_LEN +/* + * call-seq: + * ssl.accept_connection_queue_len => Integer + * + * Returns the number of pending incoming QUIC connections waiting + * to be accepted on this listener. + */ +static VALUE +ossl_ssl_accept_connection_queue_len(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return SIZET2NUM(SSL_get_accept_connection_queue_len(ssl)); +} +#endif + +#ifdef HAVE_SSL_SET_INCOMING_STREAM_POLICY +/* + * call-seq: + * ssl.incoming_stream_policy = policy + * + * Sets the incoming stream policy for a QUIC connection. + * _policy_ should be one of +INCOMING_STREAM_POLICY_AUTO+, + * +INCOMING_STREAM_POLICY_ACCEPT+, or +INCOMING_STREAM_POLICY_REJECT+. + */ +static VALUE +ossl_ssl_set_incoming_stream_policy(VALUE self, VALUE policy) +{ + SSL *ssl; + + GetSSL(self, ssl); + if (!SSL_set_incoming_stream_policy(ssl, NUM2INT(policy), 0)) + ossl_raise(eSSLError, "SSL_set_incoming_stream_policy"); + + return policy; +} +#endif + +#ifdef HAVE_SSL_POLL +/* + * call-seq: + * SSLSocket.poll(items, timeout = nil, flags = 0) => Array + * + * Polls multiple QUIC SSL objects for events. _items_ is an Array of + * [ssl, events] pairs where _events_ is a bitmask of + * +POLL_EVENT_*+ constants. + * + * _timeout_ is the maximum time to wait in seconds (Float), or +nil+ + * to block indefinitely, or +0+ to return immediately. + * + * Returns an Array of [ssl, revents] pairs for items that + * have events ready. + */ +static VALUE +ossl_ssl_poll(int argc, VALUE *argv, VALUE klass) +{ + VALUE items_ary, timeout_v, flags_v, result; + uint64_t flags = 0; + long i, n; + SSL_POLL_ITEM *items; + struct timeval tv; + int has_timeout, ret; + size_t result_count = 0; + + rb_scan_args(argc, argv, "12", &items_ary, &timeout_v, &flags_v); + Check_Type(items_ary, T_ARRAY); + + if (!NIL_P(flags_v)) + flags = NUM2ULL(flags_v); + + n = RARRAY_LEN(items_ary); + items = ALLOCA_N(SSL_POLL_ITEM, n); + + for (i = 0; i < n; i++) { + VALUE pair = RARRAY_AREF(items_ary, i); + VALUE ssl_obj, events_v; + SSL *ssl; + + Check_Type(pair, T_ARRAY); + if (RARRAY_LEN(pair) != 2) + rb_raise(rb_eArgError, "each item must be [ssl, events]"); + + ssl_obj = RARRAY_AREF(pair, 0); + events_v = RARRAY_AREF(pair, 1); + + GetSSL(ssl_obj, ssl); + items[i].desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL; + items[i].desc.value.ssl = ssl; + items[i].events = NUM2ULL(events_v); + items[i].revents = 0; + } + + if (NIL_P(timeout_v)) { + has_timeout = 0; + } else { + double t = NUM2DBL(timeout_v); + tv.tv_sec = (time_t)t; + tv.tv_usec = (suseconds_t)((t - (double)tv.tv_sec) * 1000000.0); + has_timeout = 1; + } + + ret = SSL_poll(items, (size_t)n, sizeof(SSL_POLL_ITEM), + has_timeout ? &tv : NULL, flags, &result_count); + + if (!ret) + ossl_raise(eSSLError, "SSL_poll"); + + result = rb_ary_new(); + for (i = 0; i < n; i++) { + if (items[i].revents) { + rb_ary_push(result, rb_ary_new_from_args(2, + RARRAY_AREF(RARRAY_AREF(items_ary, i), 0), + ULL2NUM(items[i].revents))); + } + } + + return result; +} +#endif + +#endif /* OSSL_USE_QUIC */ + #endif /* !defined(OPENSSL_NO_SOCK) */ void @@ -3064,6 +3616,9 @@ Init_ossl_ssl(void) rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0); rb_define_alias(cSSLContext, "freeze", "setup"); +#ifdef OSSL_USE_QUIC + rb_define_singleton_method(cSSLContext, "quic", ossl_sslctx_s_quic, 1); +#endif /* * No session caching for client or server @@ -3169,6 +3724,72 @@ Init_ossl_ssl(void) rb_define_method(cSSLSocket, "group", ossl_ssl_get_group, 0); #endif +#ifdef OSSL_USE_QUIC + rb_define_method(cSSLSocket, "new_stream", ossl_ssl_new_stream, -1); + rb_define_method(cSSLSocket, "accept_stream", ossl_ssl_accept_stream, -1); + rb_define_method(cSSLSocket, "stream_conclude", ossl_ssl_stream_conclude, 0); + rb_define_method(cSSLSocket, "stream_id", ossl_ssl_stream_id, 0); + rb_define_method(cSSLSocket, "default_stream_mode=", ossl_ssl_set_default_stream_mode, 1); + rb_define_method(cSSLSocket, "handle_events", ossl_ssl_handle_events, 0); + rb_define_method(cSSLSocket, "net_read_desired?", ossl_ssl_net_read_desired, 0); + rb_define_method(cSSLSocket, "net_write_desired?", ossl_ssl_net_write_desired, 0); + rb_define_method(cSSLSocket, "event_timeout", ossl_ssl_event_timeout, 0); + rb_define_method(cSSLSocket, "connection?", ossl_ssl_is_connection, 0); + rb_define_method(cSSLSocket, "init_finished?", ossl_ssl_is_init_finished, 0); + + /* Create a unidirectional stream */ + rb_define_const(mSSL, "STREAM_FLAG_UNI", UINT2NUM(SSL_STREAM_FLAG_UNI)); + /* Do not block when creating or accepting a stream */ + rb_define_const(mSSL, "STREAM_FLAG_NO_BLOCK", UINT2NUM(SSL_STREAM_FLAG_NO_BLOCK)); +#ifdef HAVE_SSL_NEW_LISTENER + rb_define_singleton_method(cSSLSocket, "new_listener", ossl_ssl_new_listener, -1); +#endif +#ifdef HAVE_SSL_ACCEPT_CONNECTION + rb_define_method(cSSLSocket, "accept_connection", ossl_ssl_accept_connection, -1); + rb_define_const(mSSL, "ACCEPT_CONNECTION_NO_BLOCK", ULL2NUM(SSL_ACCEPT_CONNECTION_NO_BLOCK)); +#endif +#ifdef HAVE_SSL_LISTEN + rb_define_method(cSSLSocket, "listen", ossl_ssl_listen, 0); +#endif +#ifdef HAVE_SSL_GET_ACCEPT_CONNECTION_QUEUE_LEN + rb_define_method(cSSLSocket, "accept_connection_queue_len", ossl_ssl_accept_connection_queue_len, 0); +#endif +#ifdef HAVE_SSL_POLL + rb_define_singleton_method(cSSLSocket, "poll", ossl_ssl_poll, -1); + + rb_define_const(mSSL, "POLL_EVENT_F", ULL2NUM(SSL_POLL_EVENT_F)); + rb_define_const(mSSL, "POLL_EVENT_EL", ULL2NUM(SSL_POLL_EVENT_EL)); + rb_define_const(mSSL, "POLL_EVENT_EC", ULL2NUM(SSL_POLL_EVENT_EC)); + rb_define_const(mSSL, "POLL_EVENT_ECD", ULL2NUM(SSL_POLL_EVENT_ECD)); + rb_define_const(mSSL, "POLL_EVENT_ER", ULL2NUM(SSL_POLL_EVENT_ER)); + rb_define_const(mSSL, "POLL_EVENT_EW", ULL2NUM(SSL_POLL_EVENT_EW)); + rb_define_const(mSSL, "POLL_EVENT_R", ULL2NUM(SSL_POLL_EVENT_R)); + rb_define_const(mSSL, "POLL_EVENT_W", ULL2NUM(SSL_POLL_EVENT_W)); + rb_define_const(mSSL, "POLL_EVENT_IC", ULL2NUM(SSL_POLL_EVENT_IC)); + rb_define_const(mSSL, "POLL_EVENT_ISB", ULL2NUM(SSL_POLL_EVENT_ISB)); + rb_define_const(mSSL, "POLL_EVENT_ISU", ULL2NUM(SSL_POLL_EVENT_ISU)); + rb_define_const(mSSL, "POLL_EVENT_OSB", ULL2NUM(SSL_POLL_EVENT_OSB)); + rb_define_const(mSSL, "POLL_EVENT_OSU", ULL2NUM(SSL_POLL_EVENT_OSU)); + rb_define_const(mSSL, "POLL_EVENT_RW", ULL2NUM(SSL_POLL_EVENT_RW)); + rb_define_const(mSSL, "POLL_EVENT_RE", ULL2NUM(SSL_POLL_EVENT_RE)); + rb_define_const(mSSL, "POLL_EVENT_WE", ULL2NUM(SSL_POLL_EVENT_WE)); + rb_define_const(mSSL, "POLL_EVENT_RWE", ULL2NUM(SSL_POLL_EVENT_RWE)); + rb_define_const(mSSL, "POLL_EVENT_E", ULL2NUM(SSL_POLL_EVENT_E)); + rb_define_const(mSSL, "POLL_EVENT_IS", ULL2NUM(SSL_POLL_EVENT_IS)); + rb_define_const(mSSL, "POLL_EVENT_ISE", ULL2NUM(SSL_POLL_EVENT_ISE)); + rb_define_const(mSSL, "POLL_EVENT_I", ULL2NUM(SSL_POLL_EVENT_I)); + rb_define_const(mSSL, "POLL_EVENT_OS", ULL2NUM(SSL_POLL_EVENT_OS)); + rb_define_const(mSSL, "POLL_EVENT_OSE", ULL2NUM(SSL_POLL_EVENT_OSE)); + rb_define_const(mSSL, "POLL_FLAG_NO_HANDLE_EVENTS", ULL2NUM(SSL_POLL_FLAG_NO_HANDLE_EVENTS)); +#endif +#ifdef HAVE_SSL_SET_INCOMING_STREAM_POLICY + rb_define_method(cSSLSocket, "incoming_stream_policy=", ossl_ssl_set_incoming_stream_policy, 1); + rb_define_const(mSSL, "INCOMING_STREAM_POLICY_AUTO", INT2NUM(SSL_INCOMING_STREAM_POLICY_AUTO)); + rb_define_const(mSSL, "INCOMING_STREAM_POLICY_ACCEPT", INT2NUM(SSL_INCOMING_STREAM_POLICY_ACCEPT)); + rb_define_const(mSSL, "INCOMING_STREAM_POLICY_REJECT", INT2NUM(SSL_INCOMING_STREAM_POLICY_REJECT)); +#endif +#endif + rb_define_const(mSSL, "VERIFY_NONE", INT2NUM(SSL_VERIFY_NONE)); rb_define_const(mSSL, "VERIFY_PEER", INT2NUM(SSL_VERIFY_PEER)); rb_define_const(mSSL, "VERIFY_FAIL_IF_NO_PEER_CERT", INT2NUM(SSL_VERIFY_FAIL_IF_NO_PEER_CERT)); @@ -3326,5 +3947,8 @@ Init_ossl_ssl(void) DefIVarID(context); DefIVarID(hostname); DefIVarID(sync_close); +#ifdef OSSL_USE_QUIC + DefIVarID(connection); +#endif #endif /* !defined(OPENSSL_NO_SOCK) */ } diff --git a/lib/openssl/buffering.rb b/lib/openssl/buffering.rb index 1464a4292..cf5e1dd7b 100644 --- a/lib/openssl/buffering.rb +++ b/lib/openssl/buffering.rb @@ -58,9 +58,7 @@ def append_as_bytes(string) def initialize(*) super - @eof = false - @rbuffer = Buffer.new - @sync = @io.sync + initialize_buffer end # @@ -68,6 +66,12 @@ def initialize(*) # private + def initialize_buffer + @eof = false + @rbuffer = Buffer.new + @sync = @io.sync + end + ## # Fills the buffer from the underlying SSLSocket diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index 3268c126b..e6108ccf7 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -91,12 +91,24 @@ class SSLContext # If an argument is given, #ssl_version= is called with the value. Note # that this form is deprecated. New applications should use #min_version= # and #max_version= as necessary. + # + # For QUIC contexts, use SSLContext.quic instead. def initialize(version = nil) + @quic = nil self.ssl_version = version if version self.verify_mode = OpenSSL::SSL::VERIFY_NONE self.verify_hostname = false end + # Returns the QUIC mode (e.g. +:client+) if this is a QUIC context, + # or +nil+ for a TLS context. + attr_reader :quic + + # Returns +true+ if this is a QUIC context. + def quic? + !!@quic + end + ## # call-seq: # ctx.set_params(params = {}) -> params @@ -470,6 +482,29 @@ def open(remote_host, remote_port, local_host=nil, local_port=nil, context: nil) return OpenSSL::SSL::SSLSocket.new(sock, context) end end + + # call-seq: + # SSLSocket.open_quic(remote_host, remote_port, context:) => ssl + # + # Creates a QUIC connection to _remote_host_ on _remote_port_ using + # a UDP socket. The _context_ must be an SSLContext created with + # SSLContext.quic (e.g. SSLContext.quic(:client)). + # + # Returns a connected SSLSocket with +sync_close+ set to +true+. + def open_quic(remote_host, remote_port, context:) + udp = UDPSocket.new + begin + udp.connect(remote_host, remote_port) + ssl = new(udp, context) + ssl.hostname = remote_host + ssl.sync_close = true + ssl.connect + ssl + rescue + udp.close rescue nil + raise + end + end end end diff --git a/test/openssl/test_quic.rb b/test/openssl/test_quic.rb new file mode 100644 index 000000000..216e7e6ba --- /dev/null +++ b/test/openssl/test_quic.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true +require_relative "utils" + +if defined?(OpenSSL::SSL) + +class OpenSSL::TestQUIC < Test::Unit::TestCase + QUIC_SUPPORTED = OpenSSL::SSL::SSLContext.respond_to?(:quic) + + def test_quic_context_client + pend "QUIC not supported" unless QUIC_SUPPORTED + + ctx = OpenSSL::SSL::SSLContext.quic(:client) + assert_equal :client, ctx.quic + assert_predicate ctx, :quic? + end + + def test_quic_context_client_thread + pend "QUIC not supported" unless QUIC_SUPPORTED + # :client_thread may not be available on all builds + begin + ctx = OpenSSL::SSL::SSLContext.quic(:client_thread) + assert_equal :client_thread, ctx.quic + assert_predicate ctx, :quic? + rescue OpenSSL::SSL::SSLError + pend "QUIC client_thread method not available" + end + end + + def test_quic_context_unknown_mode_raises + pend "QUIC not supported" unless QUIC_SUPPORTED + + assert_raise(ArgumentError) do + OpenSSL::SSL::SSLContext.quic(:bogus) + end + end + + def test_tls_context_backward_compat + ctx = OpenSSL::SSL::SSLContext.new + assert_nil ctx.quic + refute_predicate ctx, :quic? + end + + def test_quic_context_frozen_after_setup + pend "QUIC not supported" unless QUIC_SUPPORTED + + ctx = OpenSSL::SSL::SSLContext.quic(:client) + assert_equal true, ctx.setup + assert_predicate ctx, :frozen? + assert_nil ctx.setup + end + + def test_quic_context_verify_defaults + pend "QUIC not supported" unless QUIC_SUPPORTED + + ctx = OpenSSL::SSL::SSLContext.quic(:client) + assert_equal OpenSSL::SSL::VERIFY_NONE, ctx.verify_mode + end + + def test_quic_socket_with_udp + pend "QUIC not supported" unless QUIC_SUPPORTED + + ctx = OpenSSL::SSL::SSLContext.quic(:client) + udp = UDPSocket.new + begin + udp.connect("127.0.0.1", 12345) + ssl = OpenSSL::SSL::SSLSocket.new(udp, ctx) + assert_instance_of OpenSSL::SSL::SSLSocket, ssl + ensure + udp.close rescue nil + end + end + + def test_quic_stream_constants + pend "QUIC not supported" unless QUIC_SUPPORTED + + assert_kind_of Integer, OpenSSL::SSL::STREAM_FLAG_UNI + assert_kind_of Integer, OpenSSL::SSL::STREAM_FLAG_NO_BLOCK + end + + # --- Listener / server-side tests (OpenSSL 3.5+) --- + + LISTENER_SUPPORTED = QUIC_SUPPORTED && + OpenSSL::SSL::SSLSocket.respond_to?(:new_listener) + + def test_new_listener_method_defined + pend "QUIC listener not supported" unless LISTENER_SUPPORTED + + assert_respond_to OpenSSL::SSL::SSLSocket, :new_listener + end + + def test_new_listener_creates_socket + pend "QUIC listener not supported" unless LISTENER_SUPPORTED + + ctx = OpenSSL::SSL::SSLContext.quic(:server) + udp = UDPSocket.new + begin + udp.bind("127.0.0.1", 0) + listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) + assert_instance_of OpenSSL::SSL::SSLSocket, listener + ensure + udp.close rescue nil + end + end + + def test_accept_connection_method_defined + pend "QUIC listener not supported" unless LISTENER_SUPPORTED + pend "accept_connection not available" unless + OpenSSL::SSL::SSLSocket.method_defined?(:accept_connection) + + ctx = OpenSSL::SSL::SSLContext.quic(:server) + udp = UDPSocket.new + begin + udp.bind("127.0.0.1", 0) + listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) + assert_respond_to listener, :accept_connection + ensure + udp.close rescue nil + end + end + + def test_listen_method_defined + pend "QUIC listener not supported" unless LISTENER_SUPPORTED + pend "listen not available" unless + OpenSSL::SSL::SSLSocket.method_defined?(:listen) + + ctx = OpenSSL::SSL::SSLContext.quic(:server) + udp = UDPSocket.new + begin + udp.bind("127.0.0.1", 0) + listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) + assert_respond_to listener, :listen + ensure + udp.close rescue nil + end + end + + def test_accept_connection_queue_len_method_defined + pend "QUIC listener not supported" unless LISTENER_SUPPORTED + pend "accept_connection_queue_len not available" unless + OpenSSL::SSL::SSLSocket.method_defined?(:accept_connection_queue_len) + + ctx = OpenSSL::SSL::SSLContext.quic(:server) + udp = UDPSocket.new + begin + udp.bind("127.0.0.1", 0) + listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) + assert_respond_to listener, :accept_connection_queue_len + ensure + udp.close rescue nil + end + end + + def test_accept_connection_no_block_constant + pend "QUIC listener not supported" unless LISTENER_SUPPORTED + pend "ACCEPT_CONNECTION_NO_BLOCK not defined" unless + OpenSSL::SSL.const_defined?(:ACCEPT_CONNECTION_NO_BLOCK) + + assert_kind_of Integer, OpenSSL::SSL::ACCEPT_CONNECTION_NO_BLOCK + end +end + +end From 3a28e676428b7831a1bde9b5fca62ce85f80457a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sun, 22 Feb 2026 16:40:26 -0800 Subject: [PATCH 02/20] fix tv_usec type --- ext/openssl/ossl_ssl.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 196562021..389db66b9 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -24,6 +24,14 @@ # define TO_SOCKET(s) (s) #endif +#ifndef TYPEOF_TIMEVAL_TV_USEC +# if INT_MAX >= 1000000 +# define TYPEOF_TIMEVAL_TV_USEC int +# else +# define TYPEOF_TIMEVAL_TV_USEC long +# endif +#endif + #define GetSSLCTX(obj, ctx) do { \ TypedData_Get_Struct((obj), SSL_CTX, &ossl_sslctx_type, (ctx)); \ } while (0) @@ -3250,7 +3258,7 @@ ossl_ssl_poll(int argc, VALUE *argv, VALUE klass) } else { double t = NUM2DBL(timeout_v); tv.tv_sec = (time_t)t; - tv.tv_usec = (suseconds_t)((t - (double)tv.tv_sec) * 1000000.0); + tv.tv_usec = (TYPEOF_TIMEVAL_TV_USEC)((t - (double)tv.tv_sec) * 1000000.0); has_timeout = 1; } From b62d9a56792f2e0c34eadaf2d70726f4f064bb2a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 26 Feb 2026 16:13:18 -0800 Subject: [PATCH 03/20] Remove default_stream_mode= --- ext/openssl/ossl_ssl.c | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 389db66b9..f245eb0ec 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1743,6 +1743,8 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) // Always set non-blocking mode for QUIC connections // This is a no-op on non-QUIC connections SSL_set_blocking_mode(ssl, 0); + // This is also a no-op on non-QUIC connections + SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE); #endif rb_call_super(0, NULL); @@ -2912,38 +2914,6 @@ ossl_ssl_stream_id(VALUE self) return ULL2NUM(id); } -/* - * call-seq: - * ssl.default_stream_mode = mode - * - * Sets the default stream mode for a QUIC connection. +mode+ should be - * one of the symbols :none, :auto_bidi, or :auto_uni. - */ -static VALUE -ossl_ssl_set_default_stream_mode(VALUE self, VALUE mode) -{ - SSL *ssl; - uint32_t m; - ID mode_id; - - GetSSL(self, ssl); - - mode_id = SYM2ID(mode); - if (mode_id == rb_intern("none")) - m = SSL_DEFAULT_STREAM_MODE_NONE; - else if (mode_id == rb_intern("auto_bidi")) - m = SSL_DEFAULT_STREAM_MODE_AUTO_BIDI; - else if (mode_id == rb_intern("auto_uni")) - m = SSL_DEFAULT_STREAM_MODE_AUTO_UNI; - else - ossl_raise(rb_eArgError, "unknown default stream mode"); - - if (!SSL_set_default_stream_mode(ssl, m)) - ossl_raise(eSSLError, "SSL_set_default_stream_mode"); - - return mode; -} - /* * call-seq: * ssl.handle_events => nil @@ -3737,7 +3707,6 @@ Init_ossl_ssl(void) rb_define_method(cSSLSocket, "accept_stream", ossl_ssl_accept_stream, -1); rb_define_method(cSSLSocket, "stream_conclude", ossl_ssl_stream_conclude, 0); rb_define_method(cSSLSocket, "stream_id", ossl_ssl_stream_id, 0); - rb_define_method(cSSLSocket, "default_stream_mode=", ossl_ssl_set_default_stream_mode, 1); rb_define_method(cSSLSocket, "handle_events", ossl_ssl_handle_events, 0); rb_define_method(cSSLSocket, "net_read_desired?", ossl_ssl_net_read_desired, 0); rb_define_method(cSSLSocket, "net_write_desired?", ossl_ssl_net_write_desired, 0); From c373fdc14e5c055464f3d43fa1ca92a29e321cfa Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 26 Feb 2026 16:50:36 -0800 Subject: [PATCH 04/20] always set the connection to non-blocking mode --- ext/openssl/ossl_ssl.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index f245eb0ec..e20b250f4 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3063,6 +3063,13 @@ ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) listener_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, listener); SSL_set_ex_data(listener, ossl_ssl_ex_ptr_idx, (void *)listener_obj); +#ifdef HAVE_SSL_SET_BLOCKING_MODE + // Always set non-blocking mode for QUIC connections + // This is a no-op on non-QUIC connections + SSL_set_blocking_mode(listener, 0); + // This is also a no-op on non-QUIC connections + SSL_set_default_stream_mode(listener, SSL_DEFAULT_STREAM_MODE_NONE); +#endif rb_ivar_set(listener_obj, id_i_io, v_io); rb_ivar_set(listener_obj, id_i_context, v_ctx); @@ -3099,6 +3106,13 @@ ossl_ssl_accept_connection(int argc, VALUE *argv, VALUE self) conn_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, conn_ssl); SSL_set_ex_data(conn_ssl, ossl_ssl_ex_ptr_idx, (void *)conn_obj); +#ifdef HAVE_SSL_SET_BLOCKING_MODE + // Always set non-blocking mode for QUIC connections + // This is a no-op on non-QUIC connections + SSL_set_blocking_mode(ssl, 0); + // This is also a no-op on non-QUIC connections + SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE); +#endif rb_ivar_set(conn_obj, id_i_io, rb_attr_get(self, id_i_io)); rb_ivar_set(conn_obj, id_i_context, rb_attr_get(self, id_i_context)); From 910bed19171f563c144bcca46a2e531628fca9da Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 26 Feb 2026 17:53:12 -0800 Subject: [PATCH 05/20] remove SSLSocket.poll Clients should use IO.select and non-blocking sockets instead --- ext/openssl/extconf.rb | 1 - ext/openssl/ossl_ssl.c | 83 ------------------------------------------ 2 files changed, 84 deletions(-) diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index f75d9e752..ab9d8bff7 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -189,7 +189,6 @@ def find_openssl_library have_func("SSL_accept_connection(NULL, 0)", ssl_h) have_func("SSL_get_accept_connection_queue_len(NULL)", ssl_h) have_func("SSL_listen(NULL)", ssl_h) -have_func("SSL_poll(NULL, 0, 0, NULL, 0, NULL)", ssl_h) have_func("SSL_set_incoming_stream_policy(NULL, 0, 0)", ssl_h) Logging::message "=== Checking done. ===\n" diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index e20b250f4..9d045cc86 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3182,89 +3182,6 @@ ossl_ssl_set_incoming_stream_policy(VALUE self, VALUE policy) return policy; } #endif - -#ifdef HAVE_SSL_POLL -/* - * call-seq: - * SSLSocket.poll(items, timeout = nil, flags = 0) => Array - * - * Polls multiple QUIC SSL objects for events. _items_ is an Array of - * [ssl, events] pairs where _events_ is a bitmask of - * +POLL_EVENT_*+ constants. - * - * _timeout_ is the maximum time to wait in seconds (Float), or +nil+ - * to block indefinitely, or +0+ to return immediately. - * - * Returns an Array of [ssl, revents] pairs for items that - * have events ready. - */ -static VALUE -ossl_ssl_poll(int argc, VALUE *argv, VALUE klass) -{ - VALUE items_ary, timeout_v, flags_v, result; - uint64_t flags = 0; - long i, n; - SSL_POLL_ITEM *items; - struct timeval tv; - int has_timeout, ret; - size_t result_count = 0; - - rb_scan_args(argc, argv, "12", &items_ary, &timeout_v, &flags_v); - Check_Type(items_ary, T_ARRAY); - - if (!NIL_P(flags_v)) - flags = NUM2ULL(flags_v); - - n = RARRAY_LEN(items_ary); - items = ALLOCA_N(SSL_POLL_ITEM, n); - - for (i = 0; i < n; i++) { - VALUE pair = RARRAY_AREF(items_ary, i); - VALUE ssl_obj, events_v; - SSL *ssl; - - Check_Type(pair, T_ARRAY); - if (RARRAY_LEN(pair) != 2) - rb_raise(rb_eArgError, "each item must be [ssl, events]"); - - ssl_obj = RARRAY_AREF(pair, 0); - events_v = RARRAY_AREF(pair, 1); - - GetSSL(ssl_obj, ssl); - items[i].desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL; - items[i].desc.value.ssl = ssl; - items[i].events = NUM2ULL(events_v); - items[i].revents = 0; - } - - if (NIL_P(timeout_v)) { - has_timeout = 0; - } else { - double t = NUM2DBL(timeout_v); - tv.tv_sec = (time_t)t; - tv.tv_usec = (TYPEOF_TIMEVAL_TV_USEC)((t - (double)tv.tv_sec) * 1000000.0); - has_timeout = 1; - } - - ret = SSL_poll(items, (size_t)n, sizeof(SSL_POLL_ITEM), - has_timeout ? &tv : NULL, flags, &result_count); - - if (!ret) - ossl_raise(eSSLError, "SSL_poll"); - - result = rb_ary_new(); - for (i = 0; i < n; i++) { - if (items[i].revents) { - rb_ary_push(result, rb_ary_new_from_args(2, - RARRAY_AREF(RARRAY_AREF(items_ary, i), 0), - ULL2NUM(items[i].revents))); - } - } - - return result; -} -#endif - #endif /* OSSL_USE_QUIC */ #endif /* !defined(OPENSSL_NO_SOCK) */ From 19f70ee9dc18935773557c3c743b0f75a24cd9e4 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 27 Feb 2026 15:10:17 -0800 Subject: [PATCH 06/20] Add blocking and non-blocking variants of accept_stream, and accept_connection --- ext/openssl/ossl_ssl.c | 178 +++++++++++++++++++++++++------------- test/openssl/test_quic.rb | 47 ++-------- 2 files changed, 127 insertions(+), 98 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 9d045cc86..3a740e3f9 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2813,11 +2813,28 @@ static ID id_i_connection; * created immediately (e.g. the handshake is not yet complete). Without * NO_BLOCK, raises SSLError on failure. */ +static VALUE +ossl_ssl_wrap_stream(VALUE self, SSL *stream_ssl) +{ + VALUE stream_obj; + + stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, stream_ssl); + SSL_set_ex_data(stream_ssl, ossl_ssl_ex_ptr_idx, (void *)stream_obj); + + /* Set @io and @context from the parent, and @connection to prevent GC */ + rb_ivar_set(stream_obj, id_i_io, rb_attr_get(self, id_i_io)); + rb_ivar_set(stream_obj, id_i_context, rb_attr_get(self, id_i_context)); + rb_ivar_set(stream_obj, id_i_connection, self); + rb_funcall(stream_obj, rb_intern("initialize_buffer"), 0); + + return stream_obj; +} + static VALUE ossl_ssl_new_stream(int argc, VALUE *argv, VALUE self) { SSL *ssl, *stream_ssl; - VALUE flags_v, stream_obj; + VALUE flags_v; uint64_t flags = 0; rb_scan_args(argc, argv, "01", &flags_v); @@ -2832,51 +2849,60 @@ ossl_ssl_new_stream(int argc, VALUE *argv, VALUE self) ossl_raise(eSSLError, "SSL_new_stream"); } - stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, stream_ssl); - SSL_set_ex_data(stream_ssl, ossl_ssl_ex_ptr_idx, (void *)stream_obj); - - /* Set @io and @context from the parent, and @connection to prevent GC */ - rb_ivar_set(stream_obj, id_i_io, rb_attr_get(self, id_i_io)); - rb_ivar_set(stream_obj, id_i_context, rb_attr_get(self, id_i_context)); - rb_ivar_set(stream_obj, id_i_connection, self); - rb_funcall(stream_obj, rb_intern("initialize_buffer"), 0); - - return stream_obj; + return ossl_ssl_wrap_stream(self, stream_ssl); } /* * call-seq: - * ssl.accept_stream(flags = 0) => SSLSocket or nil + * ssl.accept_stream => SSLSocket + * + * Accepts an incoming QUIC stream from the peer. Blocks until a stream is + * available. Returns a new SSLSocket representing the stream. * - * Accepts an incoming QUIC stream from the peer. Returns a new SSLSocket - * representing the stream, or +nil+ if no stream is available (when - * using non-blocking mode or STREAM_FLAG_NO_BLOCK). + * Raises OpenSSL::SSL::SSLError on failure. */ static VALUE -ossl_ssl_accept_stream(int argc, VALUE *argv, VALUE self) +ossl_ssl_accept_stream(VALUE self) { SSL *ssl, *stream_ssl; - VALUE flags_v, stream_obj; - uint64_t flags = 0; - - rb_scan_args(argc, argv, "01", &flags_v); - if (!NIL_P(flags_v)) - flags = NUM2UINT64T(flags_v); GetSSL(self, ssl); - stream_ssl = SSL_accept_stream(ssl, flags); + stream_ssl = SSL_accept_stream(ssl, 0); if (!stream_ssl) - return Qnil; + ossl_raise(eSSLError, "SSL_accept_stream"); - stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, stream_ssl); - SSL_set_ex_data(stream_ssl, ossl_ssl_ex_ptr_idx, (void *)stream_obj); + return ossl_ssl_wrap_stream(self, stream_ssl); +} - rb_ivar_set(stream_obj, id_i_io, rb_attr_get(self, id_i_io)); - rb_ivar_set(stream_obj, id_i_context, rb_attr_get(self, id_i_context)); - rb_ivar_set(stream_obj, id_i_connection, self); - rb_funcall(stream_obj, rb_intern("initialize_buffer"), 0); +/* + * call-seq: + * ssl.accept_stream_nonblock([opts]) => SSLSocket or :wait_readable + * + * Accepts an incoming QUIC stream from the peer without blocking. Returns a + * new SSLSocket if a stream is available, or raises IO::WaitReadable if none + * is ready. + * + * By specifying a keyword argument _exception_ to +false+, you can indicate + * that accept_stream_nonblock should not raise an IO::WaitReadable exception, + * but return the symbol +:wait_readable+ instead. + */ +static VALUE +ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) +{ + SSL *ssl, *stream_ssl; + VALUE opts; - return stream_obj; + rb_scan_args(argc, argv, "0:", &opts); + + GetSSL(self, ssl); + stream_ssl = SSL_accept_stream(ssl, SSL_STREAM_FLAG_NO_BLOCK); + if (!stream_ssl) { + if (no_exception_p(opts)) + return sym_wait_readable; + ossl_raise(eSSLErrorWaitReadable, "accept_stream would block"); + } + + return ossl_ssl_wrap_stream(self, stream_ssl); } /* @@ -3080,38 +3106,16 @@ ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) #endif #ifdef HAVE_SSL_ACCEPT_CONNECTION -/* - * call-seq: - * ssl.accept_connection(flags = 0) => SSLSocket or nil - * - * Accepts an incoming QUIC connection from the listener. Returns a new - * SSLSocket representing the connection, or +nil+ if no connection is - * available (when using non-blocking mode or ACCEPT_CONNECTION_NO_BLOCK). - */ static VALUE -ossl_ssl_accept_connection(int argc, VALUE *argv, VALUE self) +ossl_ssl_wrap_connection(VALUE self, SSL *conn_ssl) { - SSL *ssl, *conn_ssl; - VALUE flags_v, conn_obj; - uint64_t flags = 0; - - rb_scan_args(argc, argv, "01", &flags_v); - if (!NIL_P(flags_v)) - flags = NUM2UINT64T(flags_v); - - GetSSL(self, ssl); - conn_ssl = SSL_accept_connection(ssl, flags); - if (!conn_ssl) - return Qnil; + VALUE conn_obj; conn_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, conn_ssl); SSL_set_ex_data(conn_ssl, ossl_ssl_ex_ptr_idx, (void *)conn_obj); #ifdef HAVE_SSL_SET_BLOCKING_MODE - // Always set non-blocking mode for QUIC connections - // This is a no-op on non-QUIC connections - SSL_set_blocking_mode(ssl, 0); - // This is also a no-op on non-QUIC connections - SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE); + SSL_set_blocking_mode(conn_ssl, 0); + SSL_set_default_stream_mode(conn_ssl, SSL_DEFAULT_STREAM_MODE_NONE); #endif rb_ivar_set(conn_obj, id_i_io, rb_attr_get(self, id_i_io)); @@ -3121,6 +3125,59 @@ ossl_ssl_accept_connection(int argc, VALUE *argv, VALUE self) return conn_obj; } + +/* + * call-seq: + * ssl.accept_connection => SSLSocket + * + * Accepts an incoming QUIC connection from the listener. Blocks until a + * connection is available. Returns a new SSLSocket representing the connection. + * + * Raises OpenSSL::SSL::SSLError on failure. + */ +static VALUE +ossl_ssl_accept_connection(VALUE self) +{ + SSL *ssl, *conn_ssl; + + GetSSL(self, ssl); + conn_ssl = SSL_accept_connection(ssl, 0); + if (!conn_ssl) + ossl_raise(eSSLError, "SSL_accept_connection"); + + return ossl_ssl_wrap_connection(self, conn_ssl); +} + +/* + * call-seq: + * ssl.accept_connection_nonblock([opts]) => SSLSocket or :wait_readable + * + * Accepts an incoming QUIC connection from the listener without blocking. + * Returns a new SSLSocket if a connection is available, or raises + * IO::WaitReadable if none is ready. + * + * By specifying a keyword argument _exception_ to +false+, you can indicate + * that accept_connection_nonblock should not raise an IO::WaitReadable + * exception, but return the symbol +:wait_readable+ instead. + */ +static VALUE +ossl_ssl_accept_connection_nonblock(int argc, VALUE *argv, VALUE self) +{ + SSL *ssl, *conn_ssl; + VALUE opts; + + rb_scan_args(argc, argv, "0:", &opts); + + GetSSL(self, ssl); + conn_ssl = SSL_accept_connection(ssl, SSL_ACCEPT_CONNECTION_NO_BLOCK); + if (!conn_ssl) { + if (no_exception_p(opts)) + return sym_wait_readable; + ossl_raise(eSSLErrorWaitReadable, "accept_connection would block"); + } + + return ossl_ssl_wrap_connection(self, conn_ssl); +} #endif #ifdef HAVE_SSL_LISTEN @@ -3635,7 +3692,8 @@ Init_ossl_ssl(void) #ifdef OSSL_USE_QUIC rb_define_method(cSSLSocket, "new_stream", ossl_ssl_new_stream, -1); - rb_define_method(cSSLSocket, "accept_stream", ossl_ssl_accept_stream, -1); + rb_define_method(cSSLSocket, "accept_stream", ossl_ssl_accept_stream, 0); + rb_define_method(cSSLSocket, "accept_stream_nonblock", ossl_ssl_accept_stream_nonblock, -1); rb_define_method(cSSLSocket, "stream_conclude", ossl_ssl_stream_conclude, 0); rb_define_method(cSSLSocket, "stream_id", ossl_ssl_stream_id, 0); rb_define_method(cSSLSocket, "handle_events", ossl_ssl_handle_events, 0); @@ -3653,8 +3711,8 @@ Init_ossl_ssl(void) rb_define_singleton_method(cSSLSocket, "new_listener", ossl_ssl_new_listener, -1); #endif #ifdef HAVE_SSL_ACCEPT_CONNECTION - rb_define_method(cSSLSocket, "accept_connection", ossl_ssl_accept_connection, -1); - rb_define_const(mSSL, "ACCEPT_CONNECTION_NO_BLOCK", ULL2NUM(SSL_ACCEPT_CONNECTION_NO_BLOCK)); + rb_define_method(cSSLSocket, "accept_connection", ossl_ssl_accept_connection, 0); + rb_define_method(cSSLSocket, "accept_connection_nonblock", ossl_ssl_accept_connection_nonblock, -1); #endif #ifdef HAVE_SSL_LISTEN rb_define_method(cSSLSocket, "listen", ossl_ssl_listen, 0); diff --git a/test/openssl/test_quic.rb b/test/openssl/test_quic.rb index 216e7e6ba..300c4da1c 100644 --- a/test/openssl/test_quic.rb +++ b/test/openssl/test_quic.rb @@ -82,12 +82,6 @@ def test_quic_stream_constants LISTENER_SUPPORTED = QUIC_SUPPORTED && OpenSSL::SSL::SSLSocket.respond_to?(:new_listener) - def test_new_listener_method_defined - pend "QUIC listener not supported" unless LISTENER_SUPPORTED - - assert_respond_to OpenSSL::SSL::SSLSocket, :new_listener - end - def test_new_listener_creates_socket pend "QUIC listener not supported" unless LISTENER_SUPPORTED @@ -102,61 +96,38 @@ def test_new_listener_creates_socket end end - def test_accept_connection_method_defined - pend "QUIC listener not supported" unless LISTENER_SUPPORTED - pend "accept_connection not available" unless - OpenSSL::SSL::SSLSocket.method_defined?(:accept_connection) - - ctx = OpenSSL::SSL::SSLContext.quic(:server) - udp = UDPSocket.new - begin - udp.bind("127.0.0.1", 0) - listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) - assert_respond_to listener, :accept_connection - ensure - udp.close rescue nil - end - end - - def test_listen_method_defined + def test_accept_connection_nonblock_no_exception pend "QUIC listener not supported" unless LISTENER_SUPPORTED - pend "listen not available" unless - OpenSSL::SSL::SSLSocket.method_defined?(:listen) ctx = OpenSSL::SSL::SSLContext.quic(:server) udp = UDPSocket.new begin udp.bind("127.0.0.1", 0) listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) - assert_respond_to listener, :listen + listener.listen + result = listener.accept_connection_nonblock(exception: false) + assert_equal :wait_readable, result ensure udp.close rescue nil end end - def test_accept_connection_queue_len_method_defined + def test_accept_connection_nonblock_raises pend "QUIC listener not supported" unless LISTENER_SUPPORTED - pend "accept_connection_queue_len not available" unless - OpenSSL::SSL::SSLSocket.method_defined?(:accept_connection_queue_len) ctx = OpenSSL::SSL::SSLContext.quic(:server) udp = UDPSocket.new begin udp.bind("127.0.0.1", 0) listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) - assert_respond_to listener, :accept_connection_queue_len + listener.listen + assert_raise(OpenSSL::SSL::SSLErrorWaitReadable) do + listener.accept_connection_nonblock + end ensure udp.close rescue nil end end - - def test_accept_connection_no_block_constant - pend "QUIC listener not supported" unless LISTENER_SUPPORTED - pend "ACCEPT_CONNECTION_NO_BLOCK not defined" unless - OpenSSL::SSL.const_defined?(:ACCEPT_CONNECTION_NO_BLOCK) - - assert_kind_of Integer, OpenSSL::SSL::ACCEPT_CONNECTION_NO_BLOCK - end end end From d779fbaf869a456e6b1bc1da218d59c2cd6f0abe Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 27 Feb 2026 16:07:13 -0800 Subject: [PATCH 07/20] make sure stream ssl is also non-blocking --- ext/openssl/ossl_ssl.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 3a740e3f9..8216d6631 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2902,6 +2902,14 @@ ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) ossl_raise(eSSLErrorWaitReadable, "accept_stream would block"); } +#ifdef HAVE_SSL_SET_BLOCKING_MODE + // Always set non-blocking mode for QUIC connections + // This is a no-op on non-QUIC connections + SSL_set_blocking_mode(stream_ssl, 0); + // This is also a no-op on non-QUIC connections + SSL_set_default_stream_mode(stream_ssl, SSL_DEFAULT_STREAM_MODE_NONE); +#endif + return ossl_ssl_wrap_stream(self, stream_ssl); } From c88b896107a2ee733b77fc9c8ad6f71ad7dbffb1 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 27 Feb 2026 16:36:25 -0800 Subject: [PATCH 08/20] use the right non-blocking flag for accepting streams --- ext/openssl/ossl_ssl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 8216d6631..a55a29b12 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2895,7 +2895,7 @@ ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) rb_scan_args(argc, argv, "0:", &opts); GetSSL(self, ssl); - stream_ssl = SSL_accept_stream(ssl, SSL_STREAM_FLAG_NO_BLOCK); + stream_ssl = SSL_accept_stream(ssl, SSL_ACCEPT_STREAM_NO_BLOCK); if (!stream_ssl) { if (no_exception_p(opts)) return sym_wait_readable; @@ -3713,7 +3713,7 @@ Init_ossl_ssl(void) /* Create a unidirectional stream */ rb_define_const(mSSL, "STREAM_FLAG_UNI", UINT2NUM(SSL_STREAM_FLAG_UNI)); - /* Do not block when creating or accepting a stream */ + /* Do not block when creating a stream */ rb_define_const(mSSL, "STREAM_FLAG_NO_BLOCK", UINT2NUM(SSL_STREAM_FLAG_NO_BLOCK)); #ifdef HAVE_SSL_NEW_LISTENER rb_define_singleton_method(cSSLSocket, "new_listener", ossl_ssl_new_listener, -1); From c04fb7f8fe7b9ac4461c032140f7cf6b30310e5e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 12:56:51 -0800 Subject: [PATCH 09/20] assert that things exist, not what they are --- test/openssl/test_quic.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/openssl/test_quic.rb b/test/openssl/test_quic.rb index 300c4da1c..975d6207b 100644 --- a/test/openssl/test_quic.rb +++ b/test/openssl/test_quic.rb @@ -64,7 +64,7 @@ def test_quic_socket_with_udp begin udp.connect("127.0.0.1", 12345) ssl = OpenSSL::SSL::SSLSocket.new(udp, ctx) - assert_instance_of OpenSSL::SSL::SSLSocket, ssl + assert ssl, "SSLSocket should be available" ensure udp.close rescue nil end @@ -73,8 +73,8 @@ def test_quic_socket_with_udp def test_quic_stream_constants pend "QUIC not supported" unless QUIC_SUPPORTED - assert_kind_of Integer, OpenSSL::SSL::STREAM_FLAG_UNI - assert_kind_of Integer, OpenSSL::SSL::STREAM_FLAG_NO_BLOCK + assert OpenSSL::SSL::STREAM_FLAG_UNI, "STREAM_FLAG_UNI should be available" + assert OpenSSL::SSL::STREAM_FLAG_NO_BLOCK, "STREAM_FLAG_NO_BLOCK should be available" end # --- Listener / server-side tests (OpenSSL 3.5+) --- @@ -90,7 +90,7 @@ def test_new_listener_creates_socket begin udp.bind("127.0.0.1", 0) listener = OpenSSL::SSL::SSLSocket.new_listener(udp, context: ctx) - assert_instance_of OpenSSL::SSL::SSLSocket, listener + assert listener, "SSLSocket listener should be available" ensure udp.close rescue nil end From 6e19b3500f33149eaa2d3f8f164e37048deb35d9 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:07:07 -0800 Subject: [PATCH 10/20] Check for OpenSSL 3.5.0+ instead of each function --- ext/openssl/extconf.rb | 26 +++++--------------------- ext/openssl/ossl_ssl.c | 35 +++++++++++------------------------ 2 files changed, 16 insertions(+), 45 deletions(-) diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index ab9d8bff7..07a3d043e 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -169,27 +169,11 @@ def find_openssl_library # added in 3.5.0 have_func("SSL_get0_peer_signature_name(NULL, NULL)", ssl_h) -# QUIC support - added in OpenSSL 3.2.0 -have_func("OSSL_QUIC_client_method()", ssl_h) -have_func("OSSL_QUIC_client_thread_method()", ssl_h) -have_func("SSL_new_stream(NULL, 0)", ssl_h) -have_func("SSL_accept_stream(NULL, 0)", ssl_h) -have_func("SSL_stream_conclude(NULL)", ssl_h) -have_func("SSL_get_stream_id(NULL)", ssl_h) -have_func("SSL_set_default_stream_mode(NULL, 0)", ssl_h) -have_func("SSL_set_blocking_mode(NULL, 0)", ssl_h) -have_func("SSL_get_blocking_mode(NULL)", ssl_h) -have_func("SSL_handle_events(NULL)", ssl_h) -have_func("SSL_get_event_timeout(NULL, NULL, NULL)", ssl_h) -have_func("SSL_get0_connection(NULL)", ssl_h) -have_func("SSL_is_connection(NULL)", ssl_h) -have_func("SSL_set1_initial_peer_addr(NULL, NULL)", ssl_h) -have_func("OSSL_QUIC_server_method()", ssl_h) -have_func("SSL_new_listener(NULL, 0)", ssl_h) -have_func("SSL_accept_connection(NULL, 0)", ssl_h) -have_func("SSL_get_accept_connection_queue_len(NULL)", ssl_h) -have_func("SSL_listen(NULL)", ssl_h) -have_func("SSL_set_incoming_stream_policy(NULL, 0, 0)", ssl_h) +# QUIC support - requires OpenSSL 3.5.0+, not available in LibreSSL +if is_openssl && checking_for("OpenSSL version >= 3.5.0") { + try_static_assert("OPENSSL_VERSION_NUMBER >= 0x30500000L", "openssl/opensslv.h") } + $defs.push("-DHAVE_OSSL_QUIC_CLIENT_METHOD") +end Logging::message "=== Checking done. ===\n" diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index a55a29b12..0c142a8aa 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1587,11 +1587,11 @@ ossl_sslctx_s_quic(VALUE klass, VALUE quic_sym) if (quic_id == rb_intern("client")) method = OSSL_QUIC_client_method(); -#ifdef HAVE_OSSL_QUIC_CLIENT_THREAD_METHOD +#ifdef OSSL_USE_QUIC else if (quic_id == rb_intern("client_thread")) method = OSSL_QUIC_client_thread_method(); #endif -#ifdef HAVE_OSSL_QUIC_SERVER_METHOD +#ifdef OSSL_USE_QUIC else if (quic_id == rb_intern("server")) method = OSSL_QUIC_server_method(); #endif @@ -1739,7 +1739,7 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) SSL_set_ex_data(ssl, ossl_ssl_ex_ptr_idx, (void *)self); SSL_set_info_callback(ssl, ssl_info_cb); -#ifdef HAVE_SSL_SET_BLOCKING_MODE +#ifdef OSSL_USE_QUIC // Always set non-blocking mode for QUIC connections // This is a no-op on non-QUIC connections SSL_set_blocking_mode(ssl, 0); @@ -2902,7 +2902,7 @@ ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) ossl_raise(eSSLErrorWaitReadable, "accept_stream would block"); } -#ifdef HAVE_SSL_SET_BLOCKING_MODE +#ifdef OSSL_USE_QUIC // Always set non-blocking mode for QUIC connections // This is a no-op on non-QUIC connections SSL_set_blocking_mode(stream_ssl, 0); @@ -3055,7 +3055,7 @@ ossl_ssl_is_init_finished(VALUE self) return SSL_is_init_finished(ssl) ? Qtrue : Qfalse; } -#ifdef HAVE_SSL_NEW_LISTENER +#ifdef OSSL_USE_QUIC /* * call-seq: * SSLSocket.new_listener(io, context:) => SSLSocket @@ -3097,13 +3097,8 @@ ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) listener_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, listener); SSL_set_ex_data(listener, ossl_ssl_ex_ptr_idx, (void *)listener_obj); -#ifdef HAVE_SSL_SET_BLOCKING_MODE - // Always set non-blocking mode for QUIC connections - // This is a no-op on non-QUIC connections SSL_set_blocking_mode(listener, 0); - // This is also a no-op on non-QUIC connections SSL_set_default_stream_mode(listener, SSL_DEFAULT_STREAM_MODE_NONE); -#endif rb_ivar_set(listener_obj, id_i_io, v_io); rb_ivar_set(listener_obj, id_i_context, v_ctx); @@ -3113,7 +3108,7 @@ ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) } #endif -#ifdef HAVE_SSL_ACCEPT_CONNECTION +#ifdef OSSL_USE_QUIC static VALUE ossl_ssl_wrap_connection(VALUE self, SSL *conn_ssl) { @@ -3121,10 +3116,8 @@ ossl_ssl_wrap_connection(VALUE self, SSL *conn_ssl) conn_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, conn_ssl); SSL_set_ex_data(conn_ssl, ossl_ssl_ex_ptr_idx, (void *)conn_obj); -#ifdef HAVE_SSL_SET_BLOCKING_MODE SSL_set_blocking_mode(conn_ssl, 0); SSL_set_default_stream_mode(conn_ssl, SSL_DEFAULT_STREAM_MODE_NONE); -#endif rb_ivar_set(conn_obj, id_i_io, rb_attr_get(self, id_i_io)); rb_ivar_set(conn_obj, id_i_context, rb_attr_get(self, id_i_context)); @@ -3188,7 +3181,7 @@ ossl_ssl_accept_connection_nonblock(int argc, VALUE *argv, VALUE self) } #endif -#ifdef HAVE_SSL_LISTEN +#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.listen => self @@ -3208,7 +3201,7 @@ ossl_ssl_listen(VALUE self) } #endif -#ifdef HAVE_SSL_GET_ACCEPT_CONNECTION_QUEUE_LEN +#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.accept_connection_queue_len => Integer @@ -3226,7 +3219,7 @@ ossl_ssl_accept_connection_queue_len(VALUE self) } #endif -#ifdef HAVE_SSL_SET_INCOMING_STREAM_POLICY +#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.incoming_stream_policy = policy @@ -3715,17 +3708,11 @@ Init_ossl_ssl(void) rb_define_const(mSSL, "STREAM_FLAG_UNI", UINT2NUM(SSL_STREAM_FLAG_UNI)); /* Do not block when creating a stream */ rb_define_const(mSSL, "STREAM_FLAG_NO_BLOCK", UINT2NUM(SSL_STREAM_FLAG_NO_BLOCK)); -#ifdef HAVE_SSL_NEW_LISTENER +#ifdef OSSL_USE_QUIC rb_define_singleton_method(cSSLSocket, "new_listener", ossl_ssl_new_listener, -1); -#endif -#ifdef HAVE_SSL_ACCEPT_CONNECTION rb_define_method(cSSLSocket, "accept_connection", ossl_ssl_accept_connection, 0); rb_define_method(cSSLSocket, "accept_connection_nonblock", ossl_ssl_accept_connection_nonblock, -1); -#endif -#ifdef HAVE_SSL_LISTEN rb_define_method(cSSLSocket, "listen", ossl_ssl_listen, 0); -#endif -#ifdef HAVE_SSL_GET_ACCEPT_CONNECTION_QUEUE_LEN rb_define_method(cSSLSocket, "accept_connection_queue_len", ossl_ssl_accept_connection_queue_len, 0); #endif #ifdef HAVE_SSL_POLL @@ -3756,7 +3743,7 @@ Init_ossl_ssl(void) rb_define_const(mSSL, "POLL_EVENT_OSE", ULL2NUM(SSL_POLL_EVENT_OSE)); rb_define_const(mSSL, "POLL_FLAG_NO_HANDLE_EVENTS", ULL2NUM(SSL_POLL_FLAG_NO_HANDLE_EVENTS)); #endif -#ifdef HAVE_SSL_SET_INCOMING_STREAM_POLICY +#ifdef OSSL_USE_QUIC rb_define_method(cSSLSocket, "incoming_stream_policy=", ossl_ssl_set_incoming_stream_policy, 1); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_AUTO", INT2NUM(SSL_INCOMING_STREAM_POLICY_AUTO)); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_ACCEPT", INT2NUM(SSL_INCOMING_STREAM_POLICY_ACCEPT)); From f5fe4178ff7a7d91ba1d7d91c12958c702c007d8 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:09:22 -0800 Subject: [PATCH 11/20] Remove client thread mode --- ext/openssl/ossl_ssl.c | 19 ++----------------- test/openssl/test_quic.rb | 12 ------------ 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 0c142a8aa..c12e91422 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -24,14 +24,6 @@ # define TO_SOCKET(s) (s) #endif -#ifndef TYPEOF_TIMEVAL_TV_USEC -# if INT_MAX >= 1000000 -# define TYPEOF_TIMEVAL_TV_USEC int -# else -# define TYPEOF_TIMEVAL_TV_USEC long -# endif -#endif - #define GetSSLCTX(obj, ctx) do { \ TypedData_Get_Struct((obj), SSL_CTX, &ossl_sslctx_type, (ctx)); \ } while (0) @@ -1566,9 +1558,8 @@ ossl_sslctx_flush_sessions(int argc, VALUE *argv, VALUE self) #ifdef OSSL_USE_QUIC /* * call-seq: - * SSLContext.quic(:client) -> ctx - * SSLContext.quic(:client_thread) -> ctx - * SSLContext.quic(:server) -> ctx + * SSLContext.quic(:client) -> ctx + * SSLContext.quic(:server) -> ctx * * Creates a new SSLContext for QUIC. The argument specifies the QUIC mode. * Requires OpenSSL 3.2+. @@ -1587,14 +1578,8 @@ ossl_sslctx_s_quic(VALUE klass, VALUE quic_sym) if (quic_id == rb_intern("client")) method = OSSL_QUIC_client_method(); -#ifdef OSSL_USE_QUIC - else if (quic_id == rb_intern("client_thread")) - method = OSSL_QUIC_client_thread_method(); -#endif -#ifdef OSSL_USE_QUIC else if (quic_id == rb_intern("server")) method = OSSL_QUIC_server_method(); -#endif else ossl_raise(rb_eArgError, "unknown QUIC mode: %"PRIsVALUE, quic_sym); diff --git a/test/openssl/test_quic.rb b/test/openssl/test_quic.rb index 975d6207b..e612c4038 100644 --- a/test/openssl/test_quic.rb +++ b/test/openssl/test_quic.rb @@ -14,18 +14,6 @@ def test_quic_context_client assert_predicate ctx, :quic? end - def test_quic_context_client_thread - pend "QUIC not supported" unless QUIC_SUPPORTED - # :client_thread may not be available on all builds - begin - ctx = OpenSSL::SSL::SSLContext.quic(:client_thread) - assert_equal :client_thread, ctx.quic - assert_predicate ctx, :quic? - rescue OpenSSL::SSL::SSLError - pend "QUIC client_thread method not available" - end - end - def test_quic_context_unknown_mode_raises pend "QUIC not supported" unless QUIC_SUPPORTED From 20e0ac977ec0e97e85be21f36f0c74e840489a95 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:15:43 -0800 Subject: [PATCH 12/20] remove polling constants --- ext/openssl/ossl_ssl.c | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index c12e91422..1bf11ca1d 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3693,47 +3693,16 @@ Init_ossl_ssl(void) rb_define_const(mSSL, "STREAM_FLAG_UNI", UINT2NUM(SSL_STREAM_FLAG_UNI)); /* Do not block when creating a stream */ rb_define_const(mSSL, "STREAM_FLAG_NO_BLOCK", UINT2NUM(SSL_STREAM_FLAG_NO_BLOCK)); -#ifdef OSSL_USE_QUIC rb_define_singleton_method(cSSLSocket, "new_listener", ossl_ssl_new_listener, -1); rb_define_method(cSSLSocket, "accept_connection", ossl_ssl_accept_connection, 0); rb_define_method(cSSLSocket, "accept_connection_nonblock", ossl_ssl_accept_connection_nonblock, -1); rb_define_method(cSSLSocket, "listen", ossl_ssl_listen, 0); rb_define_method(cSSLSocket, "accept_connection_queue_len", ossl_ssl_accept_connection_queue_len, 0); -#endif -#ifdef HAVE_SSL_POLL - rb_define_singleton_method(cSSLSocket, "poll", ossl_ssl_poll, -1); - - rb_define_const(mSSL, "POLL_EVENT_F", ULL2NUM(SSL_POLL_EVENT_F)); - rb_define_const(mSSL, "POLL_EVENT_EL", ULL2NUM(SSL_POLL_EVENT_EL)); - rb_define_const(mSSL, "POLL_EVENT_EC", ULL2NUM(SSL_POLL_EVENT_EC)); - rb_define_const(mSSL, "POLL_EVENT_ECD", ULL2NUM(SSL_POLL_EVENT_ECD)); - rb_define_const(mSSL, "POLL_EVENT_ER", ULL2NUM(SSL_POLL_EVENT_ER)); - rb_define_const(mSSL, "POLL_EVENT_EW", ULL2NUM(SSL_POLL_EVENT_EW)); - rb_define_const(mSSL, "POLL_EVENT_R", ULL2NUM(SSL_POLL_EVENT_R)); - rb_define_const(mSSL, "POLL_EVENT_W", ULL2NUM(SSL_POLL_EVENT_W)); - rb_define_const(mSSL, "POLL_EVENT_IC", ULL2NUM(SSL_POLL_EVENT_IC)); - rb_define_const(mSSL, "POLL_EVENT_ISB", ULL2NUM(SSL_POLL_EVENT_ISB)); - rb_define_const(mSSL, "POLL_EVENT_ISU", ULL2NUM(SSL_POLL_EVENT_ISU)); - rb_define_const(mSSL, "POLL_EVENT_OSB", ULL2NUM(SSL_POLL_EVENT_OSB)); - rb_define_const(mSSL, "POLL_EVENT_OSU", ULL2NUM(SSL_POLL_EVENT_OSU)); - rb_define_const(mSSL, "POLL_EVENT_RW", ULL2NUM(SSL_POLL_EVENT_RW)); - rb_define_const(mSSL, "POLL_EVENT_RE", ULL2NUM(SSL_POLL_EVENT_RE)); - rb_define_const(mSSL, "POLL_EVENT_WE", ULL2NUM(SSL_POLL_EVENT_WE)); - rb_define_const(mSSL, "POLL_EVENT_RWE", ULL2NUM(SSL_POLL_EVENT_RWE)); - rb_define_const(mSSL, "POLL_EVENT_E", ULL2NUM(SSL_POLL_EVENT_E)); - rb_define_const(mSSL, "POLL_EVENT_IS", ULL2NUM(SSL_POLL_EVENT_IS)); - rb_define_const(mSSL, "POLL_EVENT_ISE", ULL2NUM(SSL_POLL_EVENT_ISE)); - rb_define_const(mSSL, "POLL_EVENT_I", ULL2NUM(SSL_POLL_EVENT_I)); - rb_define_const(mSSL, "POLL_EVENT_OS", ULL2NUM(SSL_POLL_EVENT_OS)); - rb_define_const(mSSL, "POLL_EVENT_OSE", ULL2NUM(SSL_POLL_EVENT_OSE)); - rb_define_const(mSSL, "POLL_FLAG_NO_HANDLE_EVENTS", ULL2NUM(SSL_POLL_FLAG_NO_HANDLE_EVENTS)); -#endif -#ifdef OSSL_USE_QUIC + rb_define_method(cSSLSocket, "incoming_stream_policy=", ossl_ssl_set_incoming_stream_policy, 1); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_AUTO", INT2NUM(SSL_INCOMING_STREAM_POLICY_AUTO)); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_ACCEPT", INT2NUM(SSL_INCOMING_STREAM_POLICY_ACCEPT)); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_REJECT", INT2NUM(SSL_INCOMING_STREAM_POLICY_REJECT)); -#endif #endif rb_define_const(mSSL, "VERIFY_NONE", INT2NUM(SSL_VERIFY_NONE)); From 833156fe4a409797976794839d47d63e500cf869 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:21:19 -0800 Subject: [PATCH 13/20] clean up redundant if-defs --- ext/openssl/ossl_ssl.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 1bf11ca1d..c8ab0f530 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2887,13 +2887,8 @@ ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) ossl_raise(eSSLErrorWaitReadable, "accept_stream would block"); } -#ifdef OSSL_USE_QUIC - // Always set non-blocking mode for QUIC connections - // This is a no-op on non-QUIC connections SSL_set_blocking_mode(stream_ssl, 0); - // This is also a no-op on non-QUIC connections SSL_set_default_stream_mode(stream_ssl, SSL_DEFAULT_STREAM_MODE_NONE); -#endif return ossl_ssl_wrap_stream(self, stream_ssl); } @@ -3040,7 +3035,6 @@ ossl_ssl_is_init_finished(VALUE self) return SSL_is_init_finished(ssl) ? Qtrue : Qfalse; } -#ifdef OSSL_USE_QUIC /* * call-seq: * SSLSocket.new_listener(io, context:) => SSLSocket @@ -3091,9 +3085,7 @@ ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) return listener_obj; } -#endif -#ifdef OSSL_USE_QUIC static VALUE ossl_ssl_wrap_connection(VALUE self, SSL *conn_ssl) { @@ -3164,9 +3156,7 @@ ossl_ssl_accept_connection_nonblock(int argc, VALUE *argv, VALUE self) return ossl_ssl_wrap_connection(self, conn_ssl); } -#endif -#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.listen => self @@ -3184,9 +3174,7 @@ ossl_ssl_listen(VALUE self) return self; } -#endif -#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.accept_connection_queue_len => Integer @@ -3202,9 +3190,7 @@ ossl_ssl_accept_connection_queue_len(VALUE self) GetSSL(self, ssl); return SIZET2NUM(SSL_get_accept_connection_queue_len(ssl)); } -#endif -#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.incoming_stream_policy = policy @@ -3224,7 +3210,6 @@ ossl_ssl_set_incoming_stream_policy(VALUE self, VALUE policy) return policy; } -#endif #endif /* OSSL_USE_QUIC */ #endif /* !defined(OPENSSL_NO_SOCK) */ From 9143a58a1ec6f6ad8b15085025072b32526da749 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:23:58 -0800 Subject: [PATCH 14/20] fix up doc --- ext/openssl/ossl_ssl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index c8ab0f530..ac1b603e7 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1562,7 +1562,7 @@ ossl_sslctx_flush_sessions(int argc, VALUE *argv, VALUE self) * SSLContext.quic(:server) -> ctx * * Creates a new SSLContext for QUIC. The argument specifies the QUIC mode. - * Requires OpenSSL 3.2+. + * Requires OpenSSL 3.5+. */ static VALUE ossl_sslctx_s_quic(VALUE klass, VALUE quic_sym) From 629ffb4abfc5082fd2d8c2b55e223fd3d6553304 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 15:46:46 -0800 Subject: [PATCH 15/20] Add stream_read_state method We need this method to determine if a stream is closed or not. Calling read_nonblock on a closed stream will raise an exception (even with `exception: false`) so we need this API to know not to call `read_nonblock` --- ext/openssl/ossl_ssl.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index ac1b603e7..6abf0db6a 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3210,6 +3210,33 @@ ossl_ssl_set_incoming_stream_policy(VALUE self, VALUE policy) return policy; } + +/* + * call-seq: + * ssl.stream_read_state => Integer + * + * Returns the read state of a QUIC stream as an integer. The possible values + * are: + * + * - +SSL_STREAM_STATE_NONE+ (0): not a QUIC stream object + * - +SSL_STREAM_STATE_OK+ (1): stream is readable + * - +SSL_STREAM_STATE_WRONG_DIR+ (2): stream is unidirectional in the wrong direction + * - +SSL_STREAM_STATE_FINISHED+ (3): FIN received, no more data + * - +SSL_STREAM_STATE_RESET_LOCAL+ (4): stream was reset locally + * - +SSL_STREAM_STATE_RESET_REMOTE+ (5): stream was reset by the peer (RESET_STREAM) + * - +SSL_STREAM_STATE_CONN_CLOSED+ (6): connection is closed + * + * A state of +SSL_STREAM_STATE_RESET_REMOTE+ or +SSL_STREAM_STATE_CONN_CLOSED+ + * means that calling +read_nonblock+ will raise an +SSLError+. + */ +static VALUE +ossl_ssl_stream_read_state(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return INT2NUM(SSL_get_stream_read_state(ssl)); +} #endif /* OSSL_USE_QUIC */ #endif /* !defined(OPENSSL_NO_SOCK) */ @@ -3688,6 +3715,14 @@ Init_ossl_ssl(void) rb_define_const(mSSL, "INCOMING_STREAM_POLICY_AUTO", INT2NUM(SSL_INCOMING_STREAM_POLICY_AUTO)); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_ACCEPT", INT2NUM(SSL_INCOMING_STREAM_POLICY_ACCEPT)); rb_define_const(mSSL, "INCOMING_STREAM_POLICY_REJECT", INT2NUM(SSL_INCOMING_STREAM_POLICY_REJECT)); + rb_define_method(cSSLSocket, "stream_read_state", ossl_ssl_stream_read_state, 0); + rb_define_const(mSSL, "SSL_STREAM_STATE_NONE", INT2NUM(SSL_STREAM_STATE_NONE)); + rb_define_const(mSSL, "SSL_STREAM_STATE_OK", INT2NUM(SSL_STREAM_STATE_OK)); + rb_define_const(mSSL, "SSL_STREAM_STATE_WRONG_DIR", INT2NUM(SSL_STREAM_STATE_WRONG_DIR)); + rb_define_const(mSSL, "SSL_STREAM_STATE_FINISHED", INT2NUM(SSL_STREAM_STATE_FINISHED)); + rb_define_const(mSSL, "SSL_STREAM_STATE_RESET_LOCAL", INT2NUM(SSL_STREAM_STATE_RESET_LOCAL)); + rb_define_const(mSSL, "SSL_STREAM_STATE_RESET_REMOTE", INT2NUM(SSL_STREAM_STATE_RESET_REMOTE)); + rb_define_const(mSSL, "SSL_STREAM_STATE_CONN_CLOSED", INT2NUM(SSL_STREAM_STATE_CONN_CLOSED)); #endif rb_define_const(mSSL, "VERIFY_NONE", INT2NUM(SSL_VERIFY_NONE)); From 89cd1570494a98c4b6a1f41d7f11c2aecc649b7a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 5 Mar 2026 16:20:05 -0800 Subject: [PATCH 16/20] add error checking for SSL_new_stream and SSL_accept_stream --- ext/openssl/ossl_ssl.c | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 6abf0db6a..aa1d07680 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1802,6 +1802,23 @@ no_exception_p(VALUE opts) return 0; } +static VALUE +ossl_ssl_quic_null_error(SSL *ssl, const char *funcname, VALUE opts) +{ + int err = SSL_get_error(ssl, 0); + + switch (err) { + case SSL_ERROR_NONE: + case SSL_ERROR_WANT_READ: + if (no_exception_p(opts)) + return sym_wait_readable; + ossl_raise(eSSLErrorWaitReadable, "%s would block", funcname); + default: + ossl_raise(eSSLError, "%s", funcname); + } +} + + // Provided by Ruby 3.2.0 and later in order to support the default IO#timeout. #ifndef RUBY_IO_TIMEOUT_DEFAULT #define RUBY_IO_TIMEOUT_DEFAULT Qnil @@ -2829,8 +2846,15 @@ ossl_ssl_new_stream(int argc, VALUE *argv, VALUE self) GetSSL(self, ssl); stream_ssl = SSL_new_stream(ssl, flags); if (!stream_ssl) { - if (flags & SSL_STREAM_FLAG_NO_BLOCK) - return Qnil; + if (flags & SSL_STREAM_FLAG_NO_BLOCK) { + switch (SSL_get_error(ssl, 0)) { + case SSL_ERROR_NONE: + case SSL_ERROR_WANT_READ: + return Qnil; + default: + ossl_raise(eSSLError, "SSL_new_stream"); + } + } ossl_raise(eSSLError, "SSL_new_stream"); } @@ -2881,11 +2905,8 @@ ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) GetSSL(self, ssl); stream_ssl = SSL_accept_stream(ssl, SSL_ACCEPT_STREAM_NO_BLOCK); - if (!stream_ssl) { - if (no_exception_p(opts)) - return sym_wait_readable; - ossl_raise(eSSLErrorWaitReadable, "accept_stream would block"); - } + if (!stream_ssl) + return ossl_ssl_quic_null_error(ssl, "SSL_accept_stream", opts); SSL_set_blocking_mode(stream_ssl, 0); SSL_set_default_stream_mode(stream_ssl, SSL_DEFAULT_STREAM_MODE_NONE); From 74d2861faa8fe452ab436208cd5cb8010c30cc05 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 5 Mar 2026 16:21:59 -0800 Subject: [PATCH 17/20] check errors if handle_events fails --- ext/openssl/ossl_ssl.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index aa1d07680..939d50c9c 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2962,7 +2962,8 @@ ossl_ssl_handle_events(VALUE self) SSL *ssl; GetSSL(self, ssl); - SSL_handle_events(ssl); + if (!SSL_handle_events(ssl)) + ossl_raise(eSSLError, "SSL_handle_events"); return Qnil; } From afe56fc2d17719d702db7ba358a9c569322f232a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 5 Mar 2026 17:53:39 -0800 Subject: [PATCH 18/20] do not check error after non-blocking accept --- ext/openssl/ossl_ssl.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 939d50c9c..123fabcf1 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2905,8 +2905,10 @@ ossl_ssl_accept_stream_nonblock(int argc, VALUE *argv, VALUE self) GetSSL(self, ssl); stream_ssl = SSL_accept_stream(ssl, SSL_ACCEPT_STREAM_NO_BLOCK); - if (!stream_ssl) - return ossl_ssl_quic_null_error(ssl, "SSL_accept_stream", opts); + if (!stream_ssl) { + if (no_exception_p(opts)) return sym_wait_readable; + ossl_raise(eSSLErrorWaitReadable, "accept_stream would block"); + } SSL_set_blocking_mode(stream_ssl, 0); SSL_set_default_stream_mode(stream_ssl, SSL_DEFAULT_STREAM_MODE_NONE); From 596b4cff757101865ba9bf25a4c053a4ea64535c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 6 Mar 2026 17:58:47 -0800 Subject: [PATCH 19/20] assign objects after allocation --- ext/openssl/ossl_ssl.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 123fabcf1..7f0428515 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2820,7 +2820,8 @@ ossl_ssl_wrap_stream(VALUE self, SSL *stream_ssl) { VALUE stream_obj; - stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, stream_ssl); + stream_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, NULL); + RTYPEDDATA_DATA(stream_obj) = stream_ssl; SSL_set_ex_data(stream_ssl, ossl_ssl_ex_ptr_idx, (void *)stream_obj); /* Set @io and @context from the parent, and @connection to prevent GC */ @@ -3091,15 +3092,14 @@ ossl_ssl_new_listener(int argc, VALUE *argv, VALUE klass) if (!listener) ossl_raise(eSSLError, "SSL_new_listener"); + listener_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, NULL); + RTYPEDDATA_DATA(listener_obj) = listener; + SSL_set_ex_data(listener, ossl_ssl_ex_ptr_idx, (void *)listener_obj); + Check_Type(v_io, T_FILE); GetOpenFile(v_io, fptr); - if (!SSL_set_fd(listener, TO_SOCKET(rb_io_descriptor(v_io)))) { - SSL_free(listener); + if (!SSL_set_fd(listener, TO_SOCKET(rb_io_descriptor(v_io)))) ossl_raise(eSSLError, "SSL_set_fd"); - } - - listener_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, listener); - SSL_set_ex_data(listener, ossl_ssl_ex_ptr_idx, (void *)listener_obj); SSL_set_blocking_mode(listener, 0); SSL_set_default_stream_mode(listener, SSL_DEFAULT_STREAM_MODE_NONE); @@ -3115,7 +3115,8 @@ ossl_ssl_wrap_connection(VALUE self, SSL *conn_ssl) { VALUE conn_obj; - conn_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, conn_ssl); + conn_obj = TypedData_Wrap_Struct(cSSLSocket, &ossl_ssl_type, NULL); + RTYPEDDATA_DATA(conn_obj) = conn_ssl; SSL_set_ex_data(conn_ssl, ossl_ssl_ex_ptr_idx, (void *)conn_obj); SSL_set_blocking_mode(conn_ssl, 0); SSL_set_default_stream_mode(conn_ssl, SSL_DEFAULT_STREAM_MODE_NONE); From b6107170fb2f6c2571200550cf4cca7c9cef2209 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 6 Mar 2026 18:20:34 -0800 Subject: [PATCH 20/20] Use non-blocking connections, but retry as if blocking We want to have blocking operations, but we can't hold the GVL, so we'll emulate blocking operations by calling the non-blocking API, then call io_waitreadable --- ext/openssl/ossl_ssl.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 7f0428515..93abd88fe 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2875,11 +2875,20 @@ static VALUE ossl_ssl_accept_stream(VALUE self) { SSL *ssl, *stream_ssl; + VALUE io = rb_attr_get(self, id_i_io); GetSSL(self, ssl); - stream_ssl = SSL_accept_stream(ssl, 0); - if (!stream_ssl) - ossl_raise(eSSLError, "SSL_accept_stream"); + + /* + * Use NO_BLOCK flag and retry in a loop. We treat any NULL return as + * "not ready" and wait for the socket to become readable, rather than + * checking SSL_get_error(), because SSL_get_error() returns incorrect + * error codes for SSL_accept_stream (it stalls instead of returning a + * retryable error). + */ + while ((stream_ssl = SSL_accept_stream(ssl, SSL_ACCEPT_STREAM_NO_BLOCK)) == NULL) { + io_wait_readable(io); + } return ossl_ssl_wrap_stream(self, stream_ssl); } @@ -3142,11 +3151,20 @@ static VALUE ossl_ssl_accept_connection(VALUE self) { SSL *ssl, *conn_ssl; + VALUE io = rb_attr_get(self, id_i_io); GetSSL(self, ssl); - conn_ssl = SSL_accept_connection(ssl, 0); - if (!conn_ssl) - ossl_raise(eSSLError, "SSL_accept_connection"); + + /* + * Use NO_BLOCK flag and retry in a loop. We treat any NULL return as + * "not ready" and wait for the socket to become readable, rather than + * checking SSL_get_error(), because SSL_get_error() returns incorrect + * error codes for SSL_accept_connection (it returns "conn use only" + * instead of a retryable error). + */ + while ((conn_ssl = SSL_accept_connection(ssl, SSL_ACCEPT_CONNECTION_NO_BLOCK)) == NULL) { + io_wait_readable(io); + } return ossl_ssl_wrap_connection(self, conn_ssl); }