From f6ee9614269f1419678791d352c4b21fb526f842 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 20 Feb 2026 20:02:57 -0800 Subject: [PATCH 01/21] 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 1f3298094..bbcdded20 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -176,6 +176,29 @@ def find_openssl_library # added in 4.0.0 have_func("ASN1_BIT_STRING_set1(NULL, NULL, 0, 0)", "openssl/asn1.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 3d913a396..eaab6aadb 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1553,6 +1553,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 */ @@ -1676,6 +1733,11 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) if (!SSL_set_ex_data(ssl, ossl_ssl_ex_ptr_idx, (void *)self)) ossl_raise(eSSLError, "SSL_set_ex_data"); 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); @@ -2727,6 +2789,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 @@ -3068,6 +3620,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 @@ -3173,6 +3728,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)); @@ -3330,5 +3951,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 1be844bb98a3292018541524016d1af629d93a66 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sun, 22 Feb 2026 16:40:26 -0800 Subject: [PATCH 02/21] 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 eaab6aadb..ef5342ba0 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) @@ -3254,7 +3262,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 061f72cc33e7692f8be4191c8ccc4ad8c2c5e0d5 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 26 Feb 2026 16:13:18 -0800 Subject: [PATCH 03/21] 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 ef5342ba0..9be1f7646 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1745,6 +1745,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); @@ -2916,38 +2918,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 @@ -3741,7 +3711,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 dfc7ef309def965d6d90e834f97fb30a7953bf7f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 26 Feb 2026 16:50:36 -0800 Subject: [PATCH 04/21] 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 9be1f7646..cd768b4cf 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3067,6 +3067,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); @@ -3103,6 +3110,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 4eac51475b4ef031549d668308436e2d120eec31 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 26 Feb 2026 17:53:12 -0800 Subject: [PATCH 05/21] 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 bbcdded20..5ace0416a 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -196,7 +196,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 cd768b4cf..a594e0814 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3186,89 +3186,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 dbcb89fb2d5443c7866af2cb3c5f4f1bf0af6552 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 27 Feb 2026 15:10:17 -0800 Subject: [PATCH 06/21] 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 a594e0814..25c09d33b 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2817,11 +2817,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); @@ -2836,51 +2853,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); } /* @@ -3084,38 +3110,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)); @@ -3125,6 +3129,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 @@ -3639,7 +3696,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); @@ -3657,8 +3715,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 65738459f86feb8342c894dfe4660e6878b7b0ce Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 27 Feb 2026 16:07:13 -0800 Subject: [PATCH 07/21] 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 25c09d33b..a0f9805a9 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2906,6 +2906,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 1eee2ced644c2ad78955238d06c6cee86d40c569 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 27 Feb 2026 16:36:25 -0800 Subject: [PATCH 08/21] 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 a0f9805a9..a5fdda717 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2899,7 +2899,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; @@ -3717,7 +3717,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 1fdefb951a0262bf124510f62df4496f4cc76168 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 12:56:51 -0800 Subject: [PATCH 09/21] 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 0f45c4a8ff661698f5429ff7f06290a21f5b3942 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:07:07 -0800 Subject: [PATCH 10/21] 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 5ace0416a..fbf385b36 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -176,27 +176,11 @@ def find_openssl_library # added in 4.0.0 have_func("ASN1_BIT_STRING_set1(NULL, NULL, 0, 0)", "openssl/asn1.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 a5fdda717..fec856002 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1588,11 +1588,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 @@ -1741,7 +1741,7 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) if (!SSL_set_ex_data(ssl, ossl_ssl_ex_ptr_idx, (void *)self)) ossl_raise(eSSLError, "SSL_set_ex_data"); 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); @@ -2906,7 +2906,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); @@ -3059,7 +3059,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 @@ -3101,13 +3101,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); @@ -3117,7 +3112,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) { @@ -3125,10 +3120,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)); @@ -3192,7 +3185,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 @@ -3212,7 +3205,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 @@ -3230,7 +3223,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 @@ -3719,17 +3712,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 @@ -3760,7 +3747,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 7612f7ff32ec9a3c6e9a7ffb462d6cfb66507290 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:09:22 -0800 Subject: [PATCH 11/21] 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 fec856002..49e637951 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) @@ -1567,9 +1559,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+. @@ -1588,14 +1579,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 0e23a510c99b8c8a1f160f9616ddf2e0be3b4a98 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:15:43 -0800 Subject: [PATCH 12/21] 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 49e637951..e29f2be6a 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3697,47 +3697,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 5fbc352ddddc003afc320569aae781420c7e6ba0 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:21:19 -0800 Subject: [PATCH 13/21] 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 e29f2be6a..7331871fb 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2891,13 +2891,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); } @@ -3044,7 +3039,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 @@ -3095,9 +3089,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) { @@ -3168,9 +3160,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 @@ -3188,9 +3178,7 @@ ossl_ssl_listen(VALUE self) return self; } -#endif -#ifdef OSSL_USE_QUIC /* * call-seq: * ssl.accept_connection_queue_len => Integer @@ -3206,9 +3194,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 @@ -3228,7 +3214,6 @@ ossl_ssl_set_incoming_stream_policy(VALUE self, VALUE policy) return policy; } -#endif #endif /* OSSL_USE_QUIC */ #endif /* !defined(OPENSSL_NO_SOCK) */ From 6e28a69cbdac0f7ed78f035a247ec14ec9981981 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 13:23:58 -0800 Subject: [PATCH 14/21] 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 7331871fb..7483c335f 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1563,7 +1563,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 95e851d8538d8c2f15729034ca57e7b7aeb7362f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 28 Feb 2026 15:46:46 -0800 Subject: [PATCH 15/21] 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 7483c335f..2497f3d58 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -3214,6 +3214,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) */ @@ -3692,6 +3719,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 9b292e2c772b16edfd69a4fbce85e27d5b7d9b4b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 5 Mar 2026 16:20:05 -0800 Subject: [PATCH 16/21] 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 2497f3d58..517ec42a2 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1804,6 +1804,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 @@ -2833,8 +2850,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"); } @@ -2885,11 +2909,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 4aa5e5ea3d34afb1ebbb4ae2746dab543d0e8a5c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 5 Mar 2026 16:21:59 -0800 Subject: [PATCH 17/21] 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 517ec42a2..99d555e41 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2966,7 +2966,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 8babec16a97260fbed3137c869d26d99b1acd952 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 5 Mar 2026 17:53:39 -0800 Subject: [PATCH 18/21] 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 99d555e41..6972a0ac5 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2909,8 +2909,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 4ba604aacec5b8b03cee615e8d99229ab5c4958f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 6 Mar 2026 17:58:47 -0800 Subject: [PATCH 19/21] 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 6972a0ac5..3d2fec753 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2824,7 +2824,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 */ @@ -3095,15 +3096,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); @@ -3119,7 +3119,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 214f8a33a59be161e9304146e3d61f24e1a0b755 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 6 Mar 2026 18:20:34 -0800 Subject: [PATCH 20/21] 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 3d2fec753..258da0755 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -2879,11 +2879,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); } @@ -3146,11 +3155,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); } From 795b105198921085dd14532f3347919d61937344 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 10 Jun 2026 10:29:04 -0700 Subject: [PATCH 21/21] Don't block on blocking functions We need to call rb_io_wait with appropriate timeouts depending on the state of the QUIC engine. This commit ensures the QUIC engine makes progress --- ext/openssl/ossl_ssl.c | 92 +++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 258da0755..56877f016 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1804,23 +1804,6 @@ 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 @@ -1833,9 +1816,60 @@ ossl_ssl_quic_null_error(SSL *ssl, const char *funcname, VALUE opts) #endif +#ifdef OSSL_USE_QUIC +/* + * For QUIC connections, SSL_ERROR_WANT_{READ,WRITE} does not necessarily mean + * the underlying UDP socket is unreadable/unwritable, it reflects the QUIC + * engine's internal state. Use SSL_net_{read,write}_desired() to determine + * what the socket actually needs, and cap the wait at SSL_get_event_timeout() + * so QUIC timers (loss detection, PTO, etc.) can fire. Drive the engine with + * SSL_handle_events() after the wait. + */ +static void +quic_io_wait(VALUE io, SSL *ssl) +{ +#ifdef HAVE_RB_IO_MAYBE_WAIT + int events = 0; + if (SSL_net_read_desired(ssl)) + events |= RUBY_IO_READABLE; + if (SSL_net_write_desired(ssl)) + events |= RUBY_IO_WRITABLE; + + VALUE timeout = RUBY_IO_TIMEOUT_DEFAULT; + struct timeval tv; + int is_infinite; + if (SSL_get_event_timeout(ssl, &tv, &is_infinite) && !is_infinite) { + timeout = DBL2NUM((double)tv.tv_sec + (double)tv.tv_usec / 1000000.0); + } + + if (events) { + rb_io_wait(io, INT2NUM(events), timeout); + } else if (!NIL_P(timeout)) { + rb_thread_wait_for(tv); + } else { + /* Nothing to wait on; drive the engine and let the caller retry. */ + } +#else + rb_io_t *fptr; + GetOpenFile(io, fptr); + rb_thread_wait_fd(fptr->fd); +#endif + + if (!SSL_handle_events(ssl)) + ossl_raise(eSSLError, "SSL_handle_events"); +} +#else +# define SSL_is_quic(ssl) 0 +# define quic_io_wait(io, ssl) ((void)0) +#endif + static void -io_wait_writable(VALUE io) +io_wait_writable(VALUE io, SSL *ssl) { + if (SSL_is_quic(ssl)) { + quic_io_wait(io, ssl); + return; + } #ifdef HAVE_RB_IO_MAYBE_WAIT if (!rb_io_wait(io, INT2NUM(RUBY_IO_WRITABLE), RUBY_IO_TIMEOUT_DEFAULT)) { rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!"); @@ -1848,8 +1882,12 @@ io_wait_writable(VALUE io) } static void -io_wait_readable(VALUE io) +io_wait_readable(VALUE io, SSL *ssl) { + if (SSL_is_quic(ssl)) { + quic_io_wait(io, ssl); + return; + } #ifdef HAVE_RB_IO_MAYBE_WAIT if (!rb_io_wait(io, INT2NUM(RUBY_IO_READABLE), RUBY_IO_TIMEOUT_DEFAULT)) { rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!"); @@ -1892,12 +1930,12 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) case SSL_ERROR_WANT_WRITE: if (no_exception_p(opts)) { return sym_wait_writable; } write_would_block(nonblock); - io_wait_writable(io); + io_wait_writable(io, ssl); continue; case SSL_ERROR_WANT_READ: if (no_exception_p(opts)) { return sym_wait_readable; } read_would_block(nonblock); - io_wait_readable(io); + io_wait_readable(io, ssl); continue; case SSL_ERROR_SYSCALL: #ifdef __APPLE__ @@ -2094,14 +2132,14 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) if (no_exception_p(opts)) { return sym_wait_writable; } write_would_block(nonblock); } - io_wait_writable(io); + io_wait_writable(io, ssl); break; case SSL_ERROR_WANT_READ: if (nonblock) { if (no_exception_p(opts)) { return sym_wait_readable; } read_would_block(nonblock); } - io_wait_readable(io); + io_wait_readable(io, ssl); break; case SSL_ERROR_SYSCALL: if (!ERR_peek_error()) { @@ -2206,12 +2244,12 @@ ossl_ssl_write_internal_safe(VALUE _args) case SSL_ERROR_WANT_WRITE: if (no_exception_p(opts)) { return sym_wait_writable; } write_would_block(nonblock); - io_wait_writable(io); + io_wait_writable(io, ssl); continue; case SSL_ERROR_WANT_READ: if (no_exception_p(opts)) { return sym_wait_readable; } read_would_block(nonblock); - io_wait_readable(io); + io_wait_readable(io, ssl); continue; case SSL_ERROR_SYSCALL: #ifdef __APPLE__ @@ -2891,7 +2929,7 @@ ossl_ssl_accept_stream(VALUE self) * retryable error). */ while ((stream_ssl = SSL_accept_stream(ssl, SSL_ACCEPT_STREAM_NO_BLOCK)) == NULL) { - io_wait_readable(io); + io_wait_readable(io, ssl); } return ossl_ssl_wrap_stream(self, stream_ssl); @@ -3167,7 +3205,7 @@ ossl_ssl_accept_connection(VALUE self) * instead of a retryable error). */ while ((conn_ssl = SSL_accept_connection(ssl, SSL_ACCEPT_CONNECTION_NO_BLOCK)) == NULL) { - io_wait_readable(io); + io_wait_readable(io, ssl); } return ossl_ssl_wrap_connection(self, conn_ssl);