From 942e05c61d50923b4d7a83ae6b27801aedc44a90 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 20 Feb 2026 20:02:57 -0800 Subject: [PATCH 1/2] 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 | 654 ++++++++++++++++++++++++++++++++++++++ lib/openssl/buffering.rb | 10 +- lib/openssl/ssl.rb | 35 ++ test/openssl/test_quic.rb | 162 ++++++++++ 6 files changed, 885 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..c51255082 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 */ @@ -2723,6 +2780,529 @@ 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.blocking_mode = true_or_false + * + * Sets the application-level blocking mode for this QUIC SSL object. + */ +static VALUE +ossl_ssl_set_blocking_mode(VALUE self, VALUE mode) +{ + SSL *ssl; + + GetSSL(self, ssl); + if (!SSL_set_blocking_mode(ssl, RTEST(mode) ? 1 : 0)) + ossl_raise(eSSLError, "SSL_set_blocking_mode"); + + return mode; +} + +/* + * call-seq: + * ssl.blocking_mode? => true or false + * + * Returns the application-level blocking mode for this QUIC SSL object. + */ +static VALUE +ossl_ssl_get_blocking_mode(VALUE self) +{ + SSL *ssl; + + GetSSL(self, ssl); + return SSL_get_blocking_mode(ssl) ? Qtrue : Qfalse; +} + +/* + * 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 +3644,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 +3752,74 @@ 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, "blocking_mode=", ossl_ssl_set_blocking_mode, 1); + rb_define_method(cSSLSocket, "blocking_mode?", ossl_ssl_get_blocking_mode, 0); + 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 +3977,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 716df15adc0602648bf28ad775c619f5cb203556 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sun, 22 Feb 2026 16:40:26 -0800 Subject: [PATCH 2/2] 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 c51255082..aa90ef3cd 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) @@ -3278,7 +3286,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; }