phongn commented on code in PR #13186:
URL: https://github.com/apache/trafficserver/pull/13186#discussion_r3320435786


##########
src/iocore/net/OpenSSLQUICNetVConnection.cc:
##########
@@ -0,0 +1,1046 @@
+/** @file
+
+  OpenSSL native QUIC NetVConnection support.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "P_SSLUtils.h"
+#include "P_QUICNetVConnection.h"
+#include "P_QUICPacketHandler.h"
+#include "P_UnixNet.h"
+#include "api/APIHook.h"
+#include "iocore/eventsystem/EThread.h"
+#include "iocore/net/QUICMultiCertConfigLoader.h"
+#include "iocore/net/SSLAPIHooks.h"
+#include "iocore/net/quic/QUICApplicationMap.h"
+#include "iocore/net/quic/QUICEvents.h"
+#include "iocore/net/quic/QUICGlobals.h"
+#include "iocore/net/quic/QUICStream.h"
+#include "iocore/net/quic/QUICStreamManager.h"
+#include "tscore/ink_config.h"
+
+#include <openssl/err.h>
+#include <openssl/quic.h>
+#include <openssl/ssl.h>
+
+#include <algorithm>
+#include <cerrno>
+#include <cstring>
+
+namespace
+{
+constexpr ink_hrtime OPENSSL_QUIC_EVENT_INTERVAL = HRTIME_MSECONDS(2);
+
+DbgCtl dbg_ctl_quic_net{"quic_net"};
+DbgCtl dbg_ctl_v_quic_net{"v_quic_net"};
+
+class OpenSSLQUICStreamManager : public QUICStreamManager
+{
+public:
+  OpenSSLQUICStreamManager(QUICContext *context, QUICApplicationMap *app_map, 
QUICNetVConnection *vc)
+    : QUICStreamManager(context, app_map), _vc(vc)
+  {
+  }
+
+  QUICConnectionErrorUPtr
+  create_uni_stream(QUICStreamId &new_stream_id) override
+  {
+    return this->_vc->create_openssl_stream(SSL_STREAM_FLAG_UNI | 
SSL_STREAM_FLAG_NO_BLOCK, new_stream_id);
+  }
+
+  QUICConnectionErrorUPtr
+  create_bidi_stream(QUICStreamId &new_stream_id) override
+  {
+    return this->_vc->create_openssl_stream(SSL_STREAM_FLAG_NO_BLOCK, 
new_stream_id);
+  }
+
+private:
+  QUICNetVConnection *_vc = nullptr;
+};
+
+} // end anonymous namespace
+
+#define QUICConDebug(fmt, ...)  Dbg(dbg_ctl_quic_net, "[%s] " fmt, 
this->cids().data(), ##__VA_ARGS__)
+#define QUICConVDebug(fmt, ...) Dbg(dbg_ctl_v_quic_net, "[%s] " fmt, 
this->cids().data(), ##__VA_ARGS__)
+
+ClassAllocator<QUICNetVConnection, false> 
quicNetVCAllocator("quicNetVCAllocator");
+
+QUICNetVConnection::QUICNetVConnection()
+{
+  this->_set_service(static_cast<ALPNSupport *>(this));
+  this->_set_service(static_cast<TLSBasicSupport *>(this));
+  this->_set_service(static_cast<TLSEventSupport *>(this));
+  this->_set_service(static_cast<TLSCertSwitchSupport *>(this));
+  this->_set_service(static_cast<TLSSNISupport *>(this));
+  this->_set_service(static_cast<TLSSessionResumptionSupport *>(this));
+  this->_set_service(static_cast<QUICSupport *>(this));
+}
+
+QUICNetVConnection::~QUICNetVConnection() {}
+
+void
+QUICNetVConnection::init(SSL *ssl, QUICPacketHandler *packet_handler)
+{
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::acceptEvent);
+
+  this->_ssl            = ssl;
+  this->_packet_handler = packet_handler;
+  this->_quic_connection_id.randomize();
+  this->_initial_source_connection_id = this->_quic_connection_id;
+  this->_cid_text                     = this->_quic_connection_id.hex();
+
+  SSL_set_ex_data(ssl, QUIC::ssl_quic_qc_index, static_cast<QUICConnection 
*>(this));
+  SSL_set_blocking_mode(ssl, 0);
+  SSL_set_event_handling_mode(ssl, SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+  SSL_set_incoming_stream_policy(ssl, SSL_INCOMING_STREAM_POLICY_ACCEPT, 0);
+  SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE);
+
+  QUICConfig::scoped_config params;
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_IDLE_TIMEOUT, 
params->no_activity_timeout_in());
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_STREAM_BIDI_LOCAL_AVAIL, 
params->initial_max_streams_bidi_in());
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_STREAM_UNI_LOCAL_AVAIL, 
params->initial_max_streams_uni_in());
+
+  this->_bindSSLObject();
+}
+
+void
+QUICNetVConnection::set_quic_endpoints(IpEndpoint const &local, IpEndpoint 
const &remote)
+{
+  this->local_addr      = local;
+  this->remote_addr     = remote;
+  this->got_local_addr  = true;
+  this->got_remote_addr = true;
+}
+
+QUICConnectionErrorUPtr
+QUICNetVConnection::create_openssl_stream(uint64_t flags, QUICStreamId 
&new_stream_id)
+{
+  if (this->_ssl == nullptr) {
+    return 
std::make_unique<QUICConnectionError>(QUICTransErrorCode::INTERNAL_ERROR, "QUIC 
connection is not initialized");
+  }
+
+  SSL *stream_ssl = SSL_new_stream(this->_ssl, flags | 
SSL_STREAM_FLAG_NO_BLOCK);
+  if (stream_ssl == nullptr) {
+    return 
std::make_unique<QUICConnectionError>(QUICTransErrorCode::STREAM_LIMIT_ERROR, 
"Failed to create QUIC stream");
+  }
+
+  SSL_set_blocking_mode(stream_ssl, 0);
+  SSL_set_event_handling_mode(stream_ssl, 
SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+  SSL_set_mode(stream_ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | 
SSL_MODE_ENABLE_PARTIAL_WRITE);
+
+  new_stream_id = SSL_get_stream_id(stream_ssl);
+  this->_openssl_streams.emplace(new_stream_id, stream_ssl);
+
+  [[maybe_unused]] QUICConnectionError err;
+  this->_stream_manager->create_stream(new_stream_id, err);
+
+  return nullptr;
+}
+
+void
+QUICNetVConnection::free()
+{
+  this->free_thread(this_ethread());
+}
+
+void
+QUICNetVConnection::remove_connection_ids()
+{
+}
+
+void
+QUICNetVConnection::destroy(EThread *t)
+{
+  QUICConDebug("Destroy connection");
+  if (from_accept_thread) {
+    quicNetVCAllocator.free(this);
+  } else {
+    THREAD_FREE(this, quicNetVCAllocator, t);
+  }
+}
+
+void
+QUICNetVConnection::set_local_addr()
+{
+}
+
+void
+QUICNetVConnection::free_thread(EThread * /* t ATS_UNUSED */)
+{
+  QUICConDebug("Free connection");
+
+  this->_unschedule_openssl_event();
+
+  for (auto &[stream_id, stream_ssl] : this->_openssl_streams) {
+    SSL_free(stream_ssl);
+  }
+  this->_openssl_streams.clear();
+
+  if (this->_ssl != nullptr) {
+    this->_unbindSSLObject();
+    SSL_free(this->_ssl);
+    this->_ssl = nullptr;
+  }
+
+  this->_application_map.reset();
+  this->_stream_manager.reset();
+
+  super::clear();
+  ALPNSupport::clear();
+  TLSBasicSupport::clear();
+  TLSEventSupport::clear();
+  TLSCertSwitchSupport::_clear();
+
+  if (this->_packet_handler != nullptr) {
+    this->_packet_handler->close_connection(this);
+    this->_packet_handler = nullptr;
+  }
+}
+
+void
+QUICNetVConnection::reenable(VIO * /* vio ATS_UNUSED */)
+{
+}
+
+int
+QUICNetVConnection::state_handshake(int event, Event *data)
+{
+  if (data == this->_packet_write_ready) {
+    this->_packet_write_ready = nullptr;
+  }
+
+  switch (event) {
+  case EVENT_IMMEDIATE:
+  case EVENT_INTERVAL:
+  case QUIC_EVENT_PACKET_READ_READY:
+  case QUIC_EVENT_PACKET_WRITE_READY:
+    this->_handle_openssl_events();
+    break;
+  case VC_EVENT_EOS:
+  case VC_EVENT_ERROR:
+  case VC_EVENT_ACTIVE_TIMEOUT:
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+    this->_unschedule_openssl_event();
+    this->_propagate_event(event);
+    this->closed = 1;
+    break;
+  default:
+    QUICConDebug("Unhandled event: %d", event);
+    break;
+  }
+
+  if (this->closed != 1 && SSL_is_init_finished(this->_ssl)) {
+    this->_switch_to_established_state();
+    this->_handle_openssl_events();
+  }
+
+  if (this->closed != 1 && this->_openssl_connection_closed()) {
+    this->_schedule_closing_event();
+  } else if (this->closed != 1) {
+    this->_schedule_openssl_event();
+  }
+
+  return EVENT_DONE;
+}
+
+int
+QUICNetVConnection::state_established(int event, Event *data)
+{
+  if (this->_ssl == nullptr) {
+    return EVENT_DONE;
+  }
+
+  if (data == this->_packet_write_ready) {
+    this->_packet_write_ready = nullptr;
+  }
+
+  switch (event) {
+  case EVENT_IMMEDIATE:
+  case EVENT_INTERVAL:
+  case QUIC_EVENT_PACKET_READ_READY:
+  case QUIC_EVENT_PACKET_WRITE_READY:
+    this->_handle_openssl_events();
+    break;
+  case VC_EVENT_EOS:
+  case VC_EVENT_ERROR:
+  case VC_EVENT_ACTIVE_TIMEOUT:
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+    this->_unschedule_openssl_event();
+    this->_propagate_event(event);
+    this->closed = 1;
+    break;
+  default:
+    QUICConDebug("Unhandled event: %d", event);
+    break;
+  }
+
+  if (this->closed != 1 && this->_openssl_connection_closed()) {
+    this->_schedule_closing_event();
+  } else if (this->closed != 1) {
+    this->_schedule_openssl_event();
+  }
+
+  return EVENT_DONE;
+}
+
+void
+QUICNetVConnection::_switch_to_established_state()
+{
+  QUICConDebug("Enter state_connection_established");
+  this->_record_tls_handshake_end_time();
+  this->_update_end_of_handshake_stats();
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::state_established);
+  this->_handshake_completed = true;
+  this->_start_application();
+}
+
+void
+QUICNetVConnection::_start_application()
+{
+  if (this->_application_started) {
+    return;
+  }
+
+  this->_application_started = true;
+
+  unsigned char const *app_name     = nullptr;
+  unsigned int         app_name_len = 0;
+  SSL_get0_alpn_selected(this->_ssl, &app_name, &app_name_len);
+
+  if (app_name == nullptr || app_name_len == 0) {
+    app_name     = reinterpret_cast<unsigned char const 
*>(IP_PROTO_TAG_HTTP_QUIC.data());
+    app_name_len = IP_PROTO_TAG_HTTP_QUIC.size();
+  }
+
+  this->_negotiated_alpn.assign(reinterpret_cast<char const *>(app_name), 
app_name_len);
+  this->set_negotiated_protocol_id(this->_negotiated_alpn);
+
+  if (netvc_context == NET_VCONNECTION_IN) {
+    if (this->setSelectedProtocol(app_name, app_name_len)) {
+      this->endpoint()->handleEvent(NET_EVENT_ACCEPT, this);
+    }
+  } else {
+    this->action_.continuation->handleEvent(NET_EVENT_OPEN, this);
+  }
+}
+
+void
+QUICNetVConnection::_propagate_event(int event)
+{
+  QUICConVDebug("Propagating: %d", event);
+  if (this->read.vio.cont && this->read.vio.mutex == 
this->read.vio.cont->mutex) {
+    this->read.vio.cont->handleEvent(event, &this->read.vio);
+  } else if (this->write.vio.cont && this->write.vio.mutex == 
this->write.vio.cont->mutex) {
+    this->write.vio.cont->handleEvent(event, &this->write.vio);
+  } else {
+    QUICConVDebug("Session does not exist");
+  }
+}
+
+bool
+QUICNetVConnection::shouldDestroy()
+{
+  return this->refcount() == 0;
+}
+
+VIO *
+QUICNetVConnection::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
+{
+  auto vio           = super::do_io_read(c, nbytes, buf);
+  this->read.enabled = 1;
+  return vio;
+}
+
+VIO *
+QUICNetVConnection::do_io_write(Continuation *c, int64_t nbytes, 
IOBufferReader *buf, bool /* owner ATS_UNUSED */)
+{
+  auto vio            = super::do_io_write(c, nbytes, buf);
+  this->write.enabled = 1;
+  this->_schedule_openssl_event(false);
+  return vio;
+}
+
+int
+QUICNetVConnection::acceptEvent(int event, Event *e)
+{
+  EThread    *t = (e == nullptr) ? this_ethread() : e->ethread;
+  NetHandler *h = get_NetHandler(t);
+
+  MUTEX_TRY_LOCK(lock, h->mutex, t);
+  if (!lock.is_locked()) {
+    if (event == EVENT_NONE) {
+      t->schedule_in(this, HRTIME_MSECONDS(net_retry_delay));
+      return EVENT_DONE;
+    } else {
+      e->schedule_in(HRTIME_MSECONDS(net_retry_delay));
+      return EVENT_CONT;
+    }
+  }
+
+  this->_context         = std::make_unique<QUICContext>(this);
+  this->_application_map = std::make_unique<QUICApplicationMap>();
+  this->_stream_manager  = 
std::make_unique<OpenSSLQUICStreamManager>(this->_context.get(), 
this->_application_map.get(), this);
+
+  ink_assert(this->thread == this_ethread());
+
+  if (h->startIO(this) < 0) {
+    this->free_thread(t);
+    return EVENT_DONE;
+  }
+
+  this->read.enabled  = 1;
+  this->write.enabled = 1;
+
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::state_handshake);
+
+  nh->startCop(this);
+  this->set_default_inactivity_timeout(0);
+
+  if (inactivity_timeout_in) {
+    this->set_inactivity_timeout(inactivity_timeout_in);
+  }
+
+  if (active_timeout_in) {
+    set_active_timeout(active_timeout_in);
+  }
+
+  action_.continuation->handleEvent(NET_EVENT_ACCEPT, this);
+  this->_schedule_openssl_event(false);
+
+  return EVENT_DONE;
+}
+
+int
+QUICNetVConnection::connectUp(EThread * /* t ATS_UNUSED */, int /* fd 
ATS_UNUSED */)
+{
+  return 0;
+}
+
+QUICStreamManager *
+QUICNetVConnection::stream_manager()
+{
+  return this->_stream_manager.get();
+}
+
+void
+QUICNetVConnection::close_quic_connection(QUICConnectionErrorUPtr error)
+{
+  if (this->_ssl != nullptr) {
+    SSL_SHUTDOWN_EX_ARGS args = {};
+    if (error != nullptr) {
+      args.quic_error_code = error->code;
+      args.quic_reason     = error->msg;
+    }
+    SSL_shutdown_ex(this->_ssl, SSL_SHUTDOWN_FLAG_NO_BLOCK, &args, 
sizeof(args));
+  }
+}
+
+void
+QUICNetVConnection::reset_quic_connection()
+{
+  if (this->_ssl != nullptr) {
+    SSL_shutdown_ex(this->_ssl, SSL_SHUTDOWN_FLAG_RAPID | 
SSL_SHUTDOWN_FLAG_NO_BLOCK, nullptr, 0);
+  }
+}
+
+void
+QUICNetVConnection::handle_received_packet(UDPPacket * /* packet ATS_UNUSED */)
+{
+}
+
+void
+QUICNetVConnection::ping()
+{
+}
+
+QUICConnectionId
+QUICNetVConnection::peer_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::original_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::first_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::retry_source_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::initial_source_connection_id() const
+{
+  return this->_initial_source_connection_id;
+}
+
+QUICConnectionId
+QUICNetVConnection::connection_id() const
+{
+  return this->_quic_connection_id;
+}
+
+std::string_view
+QUICNetVConnection::cids() const
+{
+  return this->_cid_text;
+}
+
+QUICFiveTuple const
+QUICNetVConnection::five_tuple() const
+{
+  return QUICFiveTuple(this->remote_addr, this->local_addr, IPPROTO_UDP);
+}
+
+uint32_t
+QUICNetVConnection::pmtu() const
+{
+  return 0;
+}
+
+NetVConnectionContext_t
+QUICNetVConnection::direction() const
+{
+  return NET_VCONNECTION_IN;
+}
+
+QUICVersion
+QUICNetVConnection::negotiated_version() const
+{
+  return QUIC_SUPPORTED_VERSIONS[0];
+}
+
+std::string_view
+QUICNetVConnection::negotiated_application_name() const
+{
+  return this->_negotiated_alpn;
+}
+
+void
+QUICNetVConnection::on_stream_updated()
+{
+  this->_schedule_openssl_event(false);
+}
+
+bool
+QUICNetVConnection::is_closed() const
+{
+  return this->_openssl_connection_closed();
+}
+
+bool
+QUICNetVConnection::is_at_anti_amplification_limit() const
+{
+  return false;
+}
+
+bool
+QUICNetVConnection::is_address_validation_completed() const
+{
+  return true;
+}
+
+bool
+QUICNetVConnection::is_handshake_completed() const
+{
+  return this->_handshake_completed;
+}
+
+void
+QUICNetVConnection::net_read_io(NetHandler * /* nh ATS_UNUSED */)
+{
+  SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
+  this->handleEvent(QUIC_EVENT_PACKET_READ_READY, nullptr);
+}
+
+int64_t
+QUICNetVConnection::load_buffer_and_write(int64_t /* towrite ATS_UNUSED */, 
MIOBufferAccessor & /* buf ATS_UNUSED */,
+                                          int64_t & /* total_written 
ATS_UNUSED */, int & /* needs ATS_UNUSED */)
+{
+  return 0;
+}
+
+void
+QUICNetVConnection::_bindSSLObject()
+{
+  TLSBasicSupport::bind(this->_ssl, this);
+  TLSEventSupport::bind(this->_ssl, this);
+  ALPNSupport::bind(this->_ssl, this);
+  TLSSessionResumptionSupport::bind(this->_ssl, this);
+  TLSSNISupport::bind(this->_ssl, this);
+  TLSCertSwitchSupport::bind(this->_ssl, this);
+  QUICSupport::bind(this->_ssl, this);
+}
+
+void
+QUICNetVConnection::_unbindSSLObject()
+{
+  TLSBasicSupport::unbind(this->_ssl);
+  TLSEventSupport::unbind(this->_ssl);
+  ALPNSupport::unbind(this->_ssl);
+  TLSSessionResumptionSupport::unbind(this->_ssl);
+  TLSSNISupport::unbind(this->_ssl);
+  TLSCertSwitchSupport::unbind(this->_ssl);
+  QUICSupport::unbind(this->_ssl);
+}
+
+void
+QUICNetVConnection::_schedule_packet_write_ready(bool delay)
+{
+  this->_schedule_openssl_event(delay);
+}
+
+void
+QUICNetVConnection::_unschedule_packet_write_ready()
+{
+  this->_unschedule_openssl_event();
+}
+
+void
+QUICNetVConnection::_close_packet_write_ready(Event *data)
+{
+  if (this->_packet_write_ready == data) {
+    this->_packet_write_ready = nullptr;
+  }
+}
+
+void
+QUICNetVConnection::_schedule_quiche_timeout()
+{
+}
+
+void
+QUICNetVConnection::_unschedule_quiche_timeout()
+{
+}
+
+void
+QUICNetVConnection::_close_quiche_timeout(Event * /* data ATS_UNUSED */)
+{
+}
+
+void
+QUICNetVConnection::_schedule_closing_event()
+{
+  QUICConDebug("Scheduling closing event");
+  SSL_CONN_CLOSE_INFO info = {};
+  if (this->_ssl != nullptr && SSL_get_conn_close_info(this->_ssl, &info, 
sizeof(info)) == 1) {
+    QUICConDebug("QUIC close info: error_code=%" PRIu64 " frame_type=%" PRIu64 
" flags=0x%x reason=\"%.*s\"", info.error_code,
+                 info.frame_type, info.flags, 
static_cast<int>(info.reason_len), info.reason == nullptr ? "" : info.reason);
+    if (info.error_code == OSSL_QUIC_LOCAL_ERR_IDLE_TIMEOUT) {
+      QUICConDebug("QUIC Idle timeout detected");
+      this->thread->schedule_imm(this, VC_EVENT_INACTIVITY_TIMEOUT);
+      return;
+    }
+  }
+
+  this->thread->schedule_imm(this, VC_EVENT_EOS);
+}
+
+void
+QUICNetVConnection::_handle_read_ready()
+{
+  this->_handle_openssl_events();
+}
+
+void
+QUICNetVConnection::_handle_write_ready()
+{
+  this->_handle_openssl_events();
+}
+
+void
+QUICNetVConnection::_handle_interval()
+{
+  this->_handle_openssl_events();
+}
+
+void
+QUICNetVConnection::_handle_openssl_events()
+{
+  if (this->_ssl == nullptr) {
+    return;
+  }
+
+  if (SSL_handle_events(this->_ssl) != 1) {
+    QUICConDebug("SSL_handle_events failed: %s", 
ERR_error_string(ERR_peek_error(), nullptr));
+  }
+
+  if (this->_stream_manager != nullptr && this->_application_started) {
+    this->_accept_openssl_streams();
+    this->_process_openssl_streams();
+  }
+
+  this->netActivity();
+}
+
+void
+QUICNetVConnection::_accept_openssl_streams()
+{
+  while (SSL *stream_ssl = SSL_accept_stream(this->_ssl, 
SSL_ACCEPT_STREAM_NO_BLOCK)) {
+    SSL_set_blocking_mode(stream_ssl, 0);
+    SSL_set_event_handling_mode(stream_ssl, 
SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+    SSL_set_mode(stream_ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | 
SSL_MODE_ENABLE_PARTIAL_WRITE);
+
+    QUICStreamId stream_id = SSL_get_stream_id(stream_ssl);
+    this->_openssl_streams.emplace(stream_id, stream_ssl);
+
+    if (this->_stream_manager->find_stream(stream_id) == nullptr) {
+      [[maybe_unused]] QUICConnectionError err;
+      this->_stream_manager->create_stream(stream_id, err);
+    }
+  }
+}
+
+void
+QUICNetVConnection::_process_openssl_streams()
+{
+  for (auto &[stream_id, stream_ssl] : this->_openssl_streams) {
+    QUICStream *stream = static_cast<QUICStream 
*>(this->_stream_manager->find_stream(stream_id));
+    if (stream == nullptr) {
+      continue;
+    }
+
+    int stream_type = SSL_get_stream_type(stream_ssl);
+    if ((stream_type & SSL_STREAM_TYPE_READ) != 0) {
+      stream->receive_data(*this);
+    }
+    if ((stream_type & SSL_STREAM_TYPE_WRITE) != 0 || 
stream->has_data_to_send()) {
+      if (stream->has_data_to_send()) {
+        while (stream->has_data_to_send() && stream->send_data(*this) > 0) {}
+      } else {
+        stream->send_data(*this);
+      }
+    }
+  }
+}
+
+void
+QUICNetVConnection::_schedule_openssl_event(bool delay)
+{
+  if (!delay && this->_packet_write_ready != nullptr) {
+    this->_packet_write_ready->cancel();
+    this->_packet_write_ready = nullptr;
+  }
+
+  if (this->_packet_write_ready == nullptr && this->thread != nullptr) {
+    if (delay) {
+      this->_packet_write_ready = this->thread->schedule_in(this, 
OPENSSL_QUIC_EVENT_INTERVAL);
+    } else {
+      this->_packet_write_ready = this->thread->schedule_imm(this, 
QUIC_EVENT_PACKET_WRITE_READY);
+    }
+  }
+}
+
+void
+QUICNetVConnection::_unschedule_openssl_event()
+{
+  if (this->_packet_write_ready != nullptr) {
+    this->_packet_write_ready->cancel();
+    this->_packet_write_ready = nullptr;
+  }
+}
+
+bool
+QUICNetVConnection::_openssl_connection_closed() const
+{
+  if (this->_ssl == nullptr) {
+    return true;
+  }
+
+  SSL_CONN_CLOSE_INFO info = {};
+  return SSL_get_conn_close_info(this->_ssl, &info, sizeof(info)) == 1;
+}
+
+int
+QUICNetVConnection::populate_protocol(std::string_view *results, int n) const
+{
+  int retval = 0;
+  if (n > retval) {
+    results[retval++] = IP_PROTO_TAG_QUIC;
+    if (n > retval) {
+      results[retval++] = IP_PROTO_TAG_TLS_1_3;
+      if (n > retval) {
+        retval += super::populate_protocol(results + retval, n - retval);
+      }
+    }
+  }
+  return retval;
+}
+
+char const *
+QUICNetVConnection::protocol_contains(std::string_view prefix) const
+{
+  char const *retval = nullptr;
+  if (prefix.size() <= IP_PROTO_TAG_QUIC.size() && 
strncmp(IP_PROTO_TAG_QUIC.data(), prefix.data(), prefix.size()) == 0) {
+    retval = IP_PROTO_TAG_QUIC.data();
+  } else if (prefix.size() <= IP_PROTO_TAG_TLS_1_3.size() &&
+             strncmp(IP_PROTO_TAG_TLS_1_3.data(), prefix.data(), 
prefix.size()) == 0) {
+    retval = IP_PROTO_TAG_TLS_1_3.data();
+  } else {
+    retval = super::protocol_contains(prefix);
+  }
+  return retval;
+}
+
+QUICConnection *
+QUICNetVConnection::get_quic_connection()
+{
+  return static_cast<QUICConnection *>(this);
+}
+
+int64_t
+QUICNetVConnection::read_stream(QUICStreamId stream_id, uint8_t *buf, size_t 
len, bool &fin, QUICStreamIO::ErrorCode &error_code)
+{
+  auto it = this->_openssl_streams.find(stream_id);
+  if (it == this->_openssl_streams.end()) {
+    error_code = ENOENT;
+    return -1;
+  }
+
+  SSL_handle_events(it->second);
+
+  size_t read_len = 0;
+  fin             = false;
+  if (SSL_read_ex(it->second, buf, len, &read_len) == 1) {
+    fin = this->stream_read_finished(stream_id);
+    return read_len;
+  }
+
+  int ssl_error = SSL_get_error(it->second, 0);
+  if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) {
+    return 0;
+  }
+  if (ssl_error == SSL_ERROR_ZERO_RETURN) {
+    fin = true;
+    return 0;
+  }
+
+  error_code = ssl_error;
+  return -1;
+}
+
+bool
+QUICNetVConnection::stream_read_finished(QUICStreamId stream_id)
+{
+  auto it = this->_openssl_streams.find(stream_id);
+  if (it == this->_openssl_streams.end()) {
+    return true;
+  }
+
+  int state = SSL_get_stream_read_state(it->second);
+  return state == SSL_STREAM_STATE_FINISHED || state == 
SSL_STREAM_STATE_RESET_REMOTE || state == SSL_STREAM_STATE_CONN_CLOSED;
+}
+
+int64_t
+QUICNetVConnection::stream_write_capacity(QUICStreamId stream_id)
+{
+  auto it = this->_openssl_streams.find(stream_id);
+  if (it == this->_openssl_streams.end()) {
+    return -1;
+  }
+
+  uint64_t avail = 0;
+  if (SSL_get_stream_write_buf_avail(it->second, &avail) == 1) {
+    return std::min<uint64_t>(avail, 16 * 1024);
+  }
+
+  return 16 * 1024;

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   When `SSL_get_stream_write_buf_avail()` fails (stream reset / concluded / 
closed), this returns a fabricated 16 KB of capacity. `QUICStream::send_data()` 
treats that as positive "room to write" and will attempt a write that then 
fails. Suggest returning `0` (or `-1`) on the failure path so a non-writable 
stream isn't reported as writable.



##########
src/iocore/net/OpenSSLQUICNetVConnection.cc:
##########
@@ -0,0 +1,1046 @@
+/** @file
+
+  OpenSSL native QUIC NetVConnection support.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "P_SSLUtils.h"
+#include "P_QUICNetVConnection.h"
+#include "P_QUICPacketHandler.h"
+#include "P_UnixNet.h"
+#include "api/APIHook.h"
+#include "iocore/eventsystem/EThread.h"
+#include "iocore/net/QUICMultiCertConfigLoader.h"
+#include "iocore/net/SSLAPIHooks.h"
+#include "iocore/net/quic/QUICApplicationMap.h"
+#include "iocore/net/quic/QUICEvents.h"
+#include "iocore/net/quic/QUICGlobals.h"
+#include "iocore/net/quic/QUICStream.h"
+#include "iocore/net/quic/QUICStreamManager.h"
+#include "tscore/ink_config.h"
+
+#include <openssl/err.h>
+#include <openssl/quic.h>
+#include <openssl/ssl.h>
+
+#include <algorithm>
+#include <cerrno>
+#include <cstring>
+
+namespace
+{
+constexpr ink_hrtime OPENSSL_QUIC_EVENT_INTERVAL = HRTIME_MSECONDS(2);
+
+DbgCtl dbg_ctl_quic_net{"quic_net"};
+DbgCtl dbg_ctl_v_quic_net{"v_quic_net"};
+
+class OpenSSLQUICStreamManager : public QUICStreamManager
+{
+public:
+  OpenSSLQUICStreamManager(QUICContext *context, QUICApplicationMap *app_map, 
QUICNetVConnection *vc)
+    : QUICStreamManager(context, app_map), _vc(vc)
+  {
+  }
+
+  QUICConnectionErrorUPtr
+  create_uni_stream(QUICStreamId &new_stream_id) override
+  {
+    return this->_vc->create_openssl_stream(SSL_STREAM_FLAG_UNI | 
SSL_STREAM_FLAG_NO_BLOCK, new_stream_id);
+  }
+
+  QUICConnectionErrorUPtr
+  create_bidi_stream(QUICStreamId &new_stream_id) override
+  {
+    return this->_vc->create_openssl_stream(SSL_STREAM_FLAG_NO_BLOCK, 
new_stream_id);
+  }
+
+private:
+  QUICNetVConnection *_vc = nullptr;
+};
+
+} // end anonymous namespace
+
+#define QUICConDebug(fmt, ...)  Dbg(dbg_ctl_quic_net, "[%s] " fmt, 
this->cids().data(), ##__VA_ARGS__)
+#define QUICConVDebug(fmt, ...) Dbg(dbg_ctl_v_quic_net, "[%s] " fmt, 
this->cids().data(), ##__VA_ARGS__)
+
+ClassAllocator<QUICNetVConnection, false> 
quicNetVCAllocator("quicNetVCAllocator");
+
+QUICNetVConnection::QUICNetVConnection()
+{
+  this->_set_service(static_cast<ALPNSupport *>(this));
+  this->_set_service(static_cast<TLSBasicSupport *>(this));
+  this->_set_service(static_cast<TLSEventSupport *>(this));
+  this->_set_service(static_cast<TLSCertSwitchSupport *>(this));
+  this->_set_service(static_cast<TLSSNISupport *>(this));
+  this->_set_service(static_cast<TLSSessionResumptionSupport *>(this));
+  this->_set_service(static_cast<QUICSupport *>(this));
+}
+
+QUICNetVConnection::~QUICNetVConnection() {}
+
+void
+QUICNetVConnection::init(SSL *ssl, QUICPacketHandler *packet_handler)
+{
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::acceptEvent);
+
+  this->_ssl            = ssl;
+  this->_packet_handler = packet_handler;
+  this->_quic_connection_id.randomize();
+  this->_initial_source_connection_id = this->_quic_connection_id;
+  this->_cid_text                     = this->_quic_connection_id.hex();
+
+  SSL_set_ex_data(ssl, QUIC::ssl_quic_qc_index, static_cast<QUICConnection 
*>(this));
+  SSL_set_blocking_mode(ssl, 0);
+  SSL_set_event_handling_mode(ssl, SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+  SSL_set_incoming_stream_policy(ssl, SSL_INCOMING_STREAM_POLICY_ACCEPT, 0);
+  SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE);
+
+  QUICConfig::scoped_config params;
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_IDLE_TIMEOUT, 
params->no_activity_timeout_in());
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_STREAM_BIDI_LOCAL_AVAIL, 
params->initial_max_streams_bidi_in());
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_STREAM_UNI_LOCAL_AVAIL, 
params->initial_max_streams_uni_in());
+
+  this->_bindSSLObject();
+}
+
+void
+QUICNetVConnection::set_quic_endpoints(IpEndpoint const &local, IpEndpoint 
const &remote)
+{
+  this->local_addr      = local;
+  this->remote_addr     = remote;
+  this->got_local_addr  = true;
+  this->got_remote_addr = true;
+}
+
+QUICConnectionErrorUPtr
+QUICNetVConnection::create_openssl_stream(uint64_t flags, QUICStreamId 
&new_stream_id)
+{
+  if (this->_ssl == nullptr) {
+    return 
std::make_unique<QUICConnectionError>(QUICTransErrorCode::INTERNAL_ERROR, "QUIC 
connection is not initialized");
+  }
+
+  SSL *stream_ssl = SSL_new_stream(this->_ssl, flags | 
SSL_STREAM_FLAG_NO_BLOCK);

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   Minor: `SSL_STREAM_FLAG_NO_BLOCK` is already OR'd in by both callers 
(`OpenSSLQUICStreamManager::create_uni_stream`/`create_bidi_stream`), so OR-ing 
it again here is redundant. Separately, on `SSL_new_stream` failure this always 
reports `STREAM_LIMIT_ERROR`, even when the cause isn't a stream-limit 
condition.



##########
src/proxy/http3/Http3Frame.cc:
##########
@@ -251,6 +251,12 @@ Http3DataFrame::payload_length() const
   return this->_length;
 }
 
+bool
+Http3DataFrame::_parse()

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   This gates a DATA frame on its *entire* payload being buffered before 
dispatch (the dispatcher's cloned reader pins the stream buffer blocks until 
then, and `handle_frame` copies the whole payload into `_buffer` in one shot). 
Since a peer may send a body as a single arbitrarily-large DATA frame, peak 
per-stream memory now scales with the largest single frame (held transiently in 
both the stream buffer and `_buffer`).
   
   It does fix a real bug — the old inherited `_parse() { return true; }` 
re-dispatched a DATA frame once per arriving byte (that's the 5→1 change in 
`test_Http3FrameDispatcher`). And `finalize()` already flushed the whole body 
at once in both old and new code, so this isn't replacing streaming with 
buffering. Just worth confirming the whole-frame buffering is acceptable for 
large uploads/downloads — the tests top out at 300 KB.



##########
src/iocore/net/OpenSSLQUICNetVConnection.cc:
##########
@@ -0,0 +1,1046 @@
+/** @file
+
+  OpenSSL native QUIC NetVConnection support.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "P_SSLUtils.h"
+#include "P_QUICNetVConnection.h"
+#include "P_QUICPacketHandler.h"
+#include "P_UnixNet.h"
+#include "api/APIHook.h"
+#include "iocore/eventsystem/EThread.h"
+#include "iocore/net/QUICMultiCertConfigLoader.h"
+#include "iocore/net/SSLAPIHooks.h"
+#include "iocore/net/quic/QUICApplicationMap.h"
+#include "iocore/net/quic/QUICEvents.h"
+#include "iocore/net/quic/QUICGlobals.h"
+#include "iocore/net/quic/QUICStream.h"
+#include "iocore/net/quic/QUICStreamManager.h"
+#include "tscore/ink_config.h"
+
+#include <openssl/err.h>
+#include <openssl/quic.h>
+#include <openssl/ssl.h>
+
+#include <algorithm>
+#include <cerrno>
+#include <cstring>
+
+namespace
+{
+constexpr ink_hrtime OPENSSL_QUIC_EVENT_INTERVAL = HRTIME_MSECONDS(2);
+
+DbgCtl dbg_ctl_quic_net{"quic_net"};
+DbgCtl dbg_ctl_v_quic_net{"v_quic_net"};
+
+class OpenSSLQUICStreamManager : public QUICStreamManager
+{
+public:
+  OpenSSLQUICStreamManager(QUICContext *context, QUICApplicationMap *app_map, 
QUICNetVConnection *vc)
+    : QUICStreamManager(context, app_map), _vc(vc)
+  {
+  }
+
+  QUICConnectionErrorUPtr
+  create_uni_stream(QUICStreamId &new_stream_id) override
+  {
+    return this->_vc->create_openssl_stream(SSL_STREAM_FLAG_UNI | 
SSL_STREAM_FLAG_NO_BLOCK, new_stream_id);
+  }
+
+  QUICConnectionErrorUPtr
+  create_bidi_stream(QUICStreamId &new_stream_id) override
+  {
+    return this->_vc->create_openssl_stream(SSL_STREAM_FLAG_NO_BLOCK, 
new_stream_id);
+  }
+
+private:
+  QUICNetVConnection *_vc = nullptr;
+};
+
+} // end anonymous namespace
+
+#define QUICConDebug(fmt, ...)  Dbg(dbg_ctl_quic_net, "[%s] " fmt, 
this->cids().data(), ##__VA_ARGS__)
+#define QUICConVDebug(fmt, ...) Dbg(dbg_ctl_v_quic_net, "[%s] " fmt, 
this->cids().data(), ##__VA_ARGS__)
+
+ClassAllocator<QUICNetVConnection, false> 
quicNetVCAllocator("quicNetVCAllocator");
+
+QUICNetVConnection::QUICNetVConnection()
+{
+  this->_set_service(static_cast<ALPNSupport *>(this));
+  this->_set_service(static_cast<TLSBasicSupport *>(this));
+  this->_set_service(static_cast<TLSEventSupport *>(this));
+  this->_set_service(static_cast<TLSCertSwitchSupport *>(this));
+  this->_set_service(static_cast<TLSSNISupport *>(this));
+  this->_set_service(static_cast<TLSSessionResumptionSupport *>(this));
+  this->_set_service(static_cast<QUICSupport *>(this));
+}
+
+QUICNetVConnection::~QUICNetVConnection() {}
+
+void
+QUICNetVConnection::init(SSL *ssl, QUICPacketHandler *packet_handler)
+{
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::acceptEvent);
+
+  this->_ssl            = ssl;
+  this->_packet_handler = packet_handler;
+  this->_quic_connection_id.randomize();
+  this->_initial_source_connection_id = this->_quic_connection_id;
+  this->_cid_text                     = this->_quic_connection_id.hex();
+
+  SSL_set_ex_data(ssl, QUIC::ssl_quic_qc_index, static_cast<QUICConnection 
*>(this));
+  SSL_set_blocking_mode(ssl, 0);
+  SSL_set_event_handling_mode(ssl, SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+  SSL_set_incoming_stream_policy(ssl, SSL_INCOMING_STREAM_POLICY_ACCEPT, 0);
+  SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE);
+
+  QUICConfig::scoped_config params;
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_IDLE_TIMEOUT, 
params->no_activity_timeout_in());
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_STREAM_BIDI_LOCAL_AVAIL, 
params->initial_max_streams_bidi_in());
+  SSL_set_feature_request_uint(ssl, SSL_VALUE_QUIC_STREAM_UNI_LOCAL_AVAIL, 
params->initial_max_streams_uni_in());
+
+  this->_bindSSLObject();
+}
+
+void
+QUICNetVConnection::set_quic_endpoints(IpEndpoint const &local, IpEndpoint 
const &remote)
+{
+  this->local_addr      = local;
+  this->remote_addr     = remote;
+  this->got_local_addr  = true;
+  this->got_remote_addr = true;
+}
+
+QUICConnectionErrorUPtr
+QUICNetVConnection::create_openssl_stream(uint64_t flags, QUICStreamId 
&new_stream_id)
+{
+  if (this->_ssl == nullptr) {
+    return 
std::make_unique<QUICConnectionError>(QUICTransErrorCode::INTERNAL_ERROR, "QUIC 
connection is not initialized");
+  }
+
+  SSL *stream_ssl = SSL_new_stream(this->_ssl, flags | 
SSL_STREAM_FLAG_NO_BLOCK);
+  if (stream_ssl == nullptr) {
+    return 
std::make_unique<QUICConnectionError>(QUICTransErrorCode::STREAM_LIMIT_ERROR, 
"Failed to create QUIC stream");
+  }
+
+  SSL_set_blocking_mode(stream_ssl, 0);
+  SSL_set_event_handling_mode(stream_ssl, 
SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+  SSL_set_mode(stream_ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | 
SSL_MODE_ENABLE_PARTIAL_WRITE);
+
+  new_stream_id = SSL_get_stream_id(stream_ssl);
+  this->_openssl_streams.emplace(new_stream_id, stream_ssl);
+
+  [[maybe_unused]] QUICConnectionError err;
+  this->_stream_manager->create_stream(new_stream_id, err);
+
+  return nullptr;
+}
+
+void
+QUICNetVConnection::free()
+{
+  this->free_thread(this_ethread());
+}
+
+void
+QUICNetVConnection::remove_connection_ids()
+{
+}
+
+void
+QUICNetVConnection::destroy(EThread *t)
+{
+  QUICConDebug("Destroy connection");
+  if (from_accept_thread) {
+    quicNetVCAllocator.free(this);
+  } else {
+    THREAD_FREE(this, quicNetVCAllocator, t);
+  }
+}
+
+void
+QUICNetVConnection::set_local_addr()
+{
+}
+
+void
+QUICNetVConnection::free_thread(EThread * /* t ATS_UNUSED */)
+{
+  QUICConDebug("Free connection");
+
+  this->_unschedule_openssl_event();
+
+  for (auto &[stream_id, stream_ssl] : this->_openssl_streams) {
+    SSL_free(stream_ssl);
+  }
+  this->_openssl_streams.clear();
+
+  if (this->_ssl != nullptr) {
+    this->_unbindSSLObject();
+    SSL_free(this->_ssl);
+    this->_ssl = nullptr;
+  }
+
+  this->_application_map.reset();
+  this->_stream_manager.reset();
+
+  super::clear();
+  ALPNSupport::clear();
+  TLSBasicSupport::clear();
+  TLSEventSupport::clear();
+  TLSCertSwitchSupport::_clear();
+
+  if (this->_packet_handler != nullptr) {
+    this->_packet_handler->close_connection(this);
+    this->_packet_handler = nullptr;
+  }
+}
+
+void
+QUICNetVConnection::reenable(VIO * /* vio ATS_UNUSED */)
+{
+}
+
+int
+QUICNetVConnection::state_handshake(int event, Event *data)
+{
+  if (data == this->_packet_write_ready) {
+    this->_packet_write_ready = nullptr;
+  }
+
+  switch (event) {
+  case EVENT_IMMEDIATE:
+  case EVENT_INTERVAL:
+  case QUIC_EVENT_PACKET_READ_READY:
+  case QUIC_EVENT_PACKET_WRITE_READY:
+    this->_handle_openssl_events();
+    break;
+  case VC_EVENT_EOS:
+  case VC_EVENT_ERROR:
+  case VC_EVENT_ACTIVE_TIMEOUT:
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+    this->_unschedule_openssl_event();
+    this->_propagate_event(event);
+    this->closed = 1;
+    break;
+  default:
+    QUICConDebug("Unhandled event: %d", event);
+    break;
+  }
+
+  if (this->closed != 1 && SSL_is_init_finished(this->_ssl)) {
+    this->_switch_to_established_state();
+    this->_handle_openssl_events();
+  }
+
+  if (this->closed != 1 && this->_openssl_connection_closed()) {
+    this->_schedule_closing_event();
+  } else if (this->closed != 1) {
+    this->_schedule_openssl_event();
+  }
+
+  return EVENT_DONE;
+}
+
+int
+QUICNetVConnection::state_established(int event, Event *data)
+{
+  if (this->_ssl == nullptr) {
+    return EVENT_DONE;
+  }
+
+  if (data == this->_packet_write_ready) {
+    this->_packet_write_ready = nullptr;
+  }
+
+  switch (event) {
+  case EVENT_IMMEDIATE:
+  case EVENT_INTERVAL:
+  case QUIC_EVENT_PACKET_READ_READY:
+  case QUIC_EVENT_PACKET_WRITE_READY:
+    this->_handle_openssl_events();
+    break;
+  case VC_EVENT_EOS:
+  case VC_EVENT_ERROR:
+  case VC_EVENT_ACTIVE_TIMEOUT:
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+    this->_unschedule_openssl_event();
+    this->_propagate_event(event);
+    this->closed = 1;
+    break;
+  default:
+    QUICConDebug("Unhandled event: %d", event);
+    break;
+  }
+
+  if (this->closed != 1 && this->_openssl_connection_closed()) {
+    this->_schedule_closing_event();
+  } else if (this->closed != 1) {
+    this->_schedule_openssl_event();
+  }
+
+  return EVENT_DONE;
+}
+
+void
+QUICNetVConnection::_switch_to_established_state()
+{
+  QUICConDebug("Enter state_connection_established");
+  this->_record_tls_handshake_end_time();
+  this->_update_end_of_handshake_stats();
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::state_established);
+  this->_handshake_completed = true;
+  this->_start_application();
+}
+
+void
+QUICNetVConnection::_start_application()
+{
+  if (this->_application_started) {
+    return;
+  }
+
+  this->_application_started = true;
+
+  unsigned char const *app_name     = nullptr;
+  unsigned int         app_name_len = 0;
+  SSL_get0_alpn_selected(this->_ssl, &app_name, &app_name_len);
+
+  if (app_name == nullptr || app_name_len == 0) {
+    app_name     = reinterpret_cast<unsigned char const 
*>(IP_PROTO_TAG_HTTP_QUIC.data());
+    app_name_len = IP_PROTO_TAG_HTTP_QUIC.size();
+  }
+
+  this->_negotiated_alpn.assign(reinterpret_cast<char const *>(app_name), 
app_name_len);
+  this->set_negotiated_protocol_id(this->_negotiated_alpn);
+
+  if (netvc_context == NET_VCONNECTION_IN) {
+    if (this->setSelectedProtocol(app_name, app_name_len)) {
+      this->endpoint()->handleEvent(NET_EVENT_ACCEPT, this);
+    }
+  } else {
+    this->action_.continuation->handleEvent(NET_EVENT_OPEN, this);
+  }
+}
+
+void
+QUICNetVConnection::_propagate_event(int event)
+{
+  QUICConVDebug("Propagating: %d", event);
+  if (this->read.vio.cont && this->read.vio.mutex == 
this->read.vio.cont->mutex) {
+    this->read.vio.cont->handleEvent(event, &this->read.vio);
+  } else if (this->write.vio.cont && this->write.vio.mutex == 
this->write.vio.cont->mutex) {
+    this->write.vio.cont->handleEvent(event, &this->write.vio);
+  } else {
+    QUICConVDebug("Session does not exist");
+  }
+}
+
+bool
+QUICNetVConnection::shouldDestroy()
+{
+  return this->refcount() == 0;
+}
+
+VIO *
+QUICNetVConnection::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
+{
+  auto vio           = super::do_io_read(c, nbytes, buf);
+  this->read.enabled = 1;
+  return vio;
+}
+
+VIO *
+QUICNetVConnection::do_io_write(Continuation *c, int64_t nbytes, 
IOBufferReader *buf, bool /* owner ATS_UNUSED */)
+{
+  auto vio            = super::do_io_write(c, nbytes, buf);
+  this->write.enabled = 1;
+  this->_schedule_openssl_event(false);
+  return vio;
+}
+
+int
+QUICNetVConnection::acceptEvent(int event, Event *e)
+{
+  EThread    *t = (e == nullptr) ? this_ethread() : e->ethread;
+  NetHandler *h = get_NetHandler(t);
+
+  MUTEX_TRY_LOCK(lock, h->mutex, t);
+  if (!lock.is_locked()) {
+    if (event == EVENT_NONE) {
+      t->schedule_in(this, HRTIME_MSECONDS(net_retry_delay));
+      return EVENT_DONE;
+    } else {
+      e->schedule_in(HRTIME_MSECONDS(net_retry_delay));
+      return EVENT_CONT;
+    }
+  }
+
+  this->_context         = std::make_unique<QUICContext>(this);
+  this->_application_map = std::make_unique<QUICApplicationMap>();
+  this->_stream_manager  = 
std::make_unique<OpenSSLQUICStreamManager>(this->_context.get(), 
this->_application_map.get(), this);
+
+  ink_assert(this->thread == this_ethread());
+
+  if (h->startIO(this) < 0) {
+    this->free_thread(t);
+    return EVENT_DONE;
+  }
+
+  this->read.enabled  = 1;
+  this->write.enabled = 1;
+
+  SET_HANDLER((NetVConnHandler)&QUICNetVConnection::state_handshake);
+
+  nh->startCop(this);
+  this->set_default_inactivity_timeout(0);
+
+  if (inactivity_timeout_in) {
+    this->set_inactivity_timeout(inactivity_timeout_in);
+  }
+
+  if (active_timeout_in) {
+    set_active_timeout(active_timeout_in);
+  }
+
+  action_.continuation->handleEvent(NET_EVENT_ACCEPT, this);
+  this->_schedule_openssl_event(false);
+
+  return EVENT_DONE;
+}
+
+int
+QUICNetVConnection::connectUp(EThread * /* t ATS_UNUSED */, int /* fd 
ATS_UNUSED */)
+{
+  return 0;
+}
+
+QUICStreamManager *
+QUICNetVConnection::stream_manager()
+{
+  return this->_stream_manager.get();
+}
+
+void
+QUICNetVConnection::close_quic_connection(QUICConnectionErrorUPtr error)
+{
+  if (this->_ssl != nullptr) {
+    SSL_SHUTDOWN_EX_ARGS args = {};
+    if (error != nullptr) {
+      args.quic_error_code = error->code;
+      args.quic_reason     = error->msg;
+    }
+    SSL_shutdown_ex(this->_ssl, SSL_SHUTDOWN_FLAG_NO_BLOCK, &args, 
sizeof(args));
+  }
+}
+
+void
+QUICNetVConnection::reset_quic_connection()
+{
+  if (this->_ssl != nullptr) {
+    SSL_shutdown_ex(this->_ssl, SSL_SHUTDOWN_FLAG_RAPID | 
SSL_SHUTDOWN_FLAG_NO_BLOCK, nullptr, 0);
+  }
+}
+
+void
+QUICNetVConnection::handle_received_packet(UDPPacket * /* packet ATS_UNUSED */)
+{
+}
+
+void
+QUICNetVConnection::ping()
+{
+}
+
+QUICConnectionId
+QUICNetVConnection::peer_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::original_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::first_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::retry_source_connection_id() const
+{
+  return {};
+}
+
+QUICConnectionId
+QUICNetVConnection::initial_source_connection_id() const
+{
+  return this->_initial_source_connection_id;
+}
+
+QUICConnectionId
+QUICNetVConnection::connection_id() const
+{
+  return this->_quic_connection_id;
+}
+
+std::string_view
+QUICNetVConnection::cids() const
+{
+  return this->_cid_text;
+}
+
+QUICFiveTuple const
+QUICNetVConnection::five_tuple() const
+{
+  return QUICFiveTuple(this->remote_addr, this->local_addr, IPPROTO_UDP);
+}
+
+uint32_t
+QUICNetVConnection::pmtu() const
+{
+  return 0;
+}
+
+NetVConnectionContext_t
+QUICNetVConnection::direction() const
+{
+  return NET_VCONNECTION_IN;
+}
+
+QUICVersion
+QUICNetVConnection::negotiated_version() const
+{
+  return QUIC_SUPPORTED_VERSIONS[0];
+}
+
+std::string_view
+QUICNetVConnection::negotiated_application_name() const
+{
+  return this->_negotiated_alpn;
+}
+
+void
+QUICNetVConnection::on_stream_updated()
+{
+  this->_schedule_openssl_event(false);
+}
+
+bool
+QUICNetVConnection::is_closed() const
+{
+  return this->_openssl_connection_closed();
+}
+
+bool
+QUICNetVConnection::is_at_anti_amplification_limit() const
+{
+  return false;
+}
+
+bool
+QUICNetVConnection::is_address_validation_completed() const
+{
+  return true;
+}
+
+bool
+QUICNetVConnection::is_handshake_completed() const
+{
+  return this->_handshake_completed;
+}
+
+void
+QUICNetVConnection::net_read_io(NetHandler * /* nh ATS_UNUSED */)
+{
+  SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
+  this->handleEvent(QUIC_EVENT_PACKET_READ_READY, nullptr);
+}
+
+int64_t
+QUICNetVConnection::load_buffer_and_write(int64_t /* towrite ATS_UNUSED */, 
MIOBufferAccessor & /* buf ATS_UNUSED */,
+                                          int64_t & /* total_written 
ATS_UNUSED */, int & /* needs ATS_UNUSED */)
+{
+  return 0;
+}
+
+void
+QUICNetVConnection::_bindSSLObject()
+{
+  TLSBasicSupport::bind(this->_ssl, this);
+  TLSEventSupport::bind(this->_ssl, this);
+  ALPNSupport::bind(this->_ssl, this);
+  TLSSessionResumptionSupport::bind(this->_ssl, this);
+  TLSSNISupport::bind(this->_ssl, this);
+  TLSCertSwitchSupport::bind(this->_ssl, this);
+  QUICSupport::bind(this->_ssl, this);
+}
+
+void
+QUICNetVConnection::_unbindSSLObject()
+{
+  TLSBasicSupport::unbind(this->_ssl);
+  TLSEventSupport::unbind(this->_ssl);
+  ALPNSupport::unbind(this->_ssl);
+  TLSSessionResumptionSupport::unbind(this->_ssl);
+  TLSSNISupport::unbind(this->_ssl);
+  TLSCertSwitchSupport::unbind(this->_ssl);
+  QUICSupport::unbind(this->_ssl);
+}
+
+void
+QUICNetVConnection::_schedule_packet_write_ready(bool delay)
+{
+  this->_schedule_openssl_event(delay);
+}
+
+void
+QUICNetVConnection::_unschedule_packet_write_ready()
+{
+  this->_unschedule_openssl_event();
+}
+
+void
+QUICNetVConnection::_close_packet_write_ready(Event *data)
+{
+  if (this->_packet_write_ready == data) {
+    this->_packet_write_ready = nullptr;
+  }
+}
+
+void
+QUICNetVConnection::_schedule_quiche_timeout()
+{
+}
+
+void
+QUICNetVConnection::_unschedule_quiche_timeout()
+{
+}
+
+void
+QUICNetVConnection::_close_quiche_timeout(Event * /* data ATS_UNUSED */)
+{
+}
+
+void
+QUICNetVConnection::_schedule_closing_event()
+{
+  QUICConDebug("Scheduling closing event");
+  SSL_CONN_CLOSE_INFO info = {};
+  if (this->_ssl != nullptr && SSL_get_conn_close_info(this->_ssl, &info, 
sizeof(info)) == 1) {
+    QUICConDebug("QUIC close info: error_code=%" PRIu64 " frame_type=%" PRIu64 
" flags=0x%x reason=\"%.*s\"", info.error_code,
+                 info.frame_type, info.flags, 
static_cast<int>(info.reason_len), info.reason == nullptr ? "" : info.reason);
+    if (info.error_code == OSSL_QUIC_LOCAL_ERR_IDLE_TIMEOUT) {
+      QUICConDebug("QUIC Idle timeout detected");
+      this->thread->schedule_imm(this, VC_EVENT_INACTIVITY_TIMEOUT);
+      return;
+    }
+  }
+
+  this->thread->schedule_imm(this, VC_EVENT_EOS);
+}
+
+void
+QUICNetVConnection::_handle_read_ready()
+{
+  this->_handle_openssl_events();
+}
+
+void
+QUICNetVConnection::_handle_write_ready()
+{
+  this->_handle_openssl_events();
+}
+
+void
+QUICNetVConnection::_handle_interval()
+{
+  this->_handle_openssl_events();
+}
+
+void
+QUICNetVConnection::_handle_openssl_events()
+{
+  if (this->_ssl == nullptr) {
+    return;
+  }
+
+  if (SSL_handle_events(this->_ssl) != 1) {
+    QUICConDebug("SSL_handle_events failed: %s", 
ERR_error_string(ERR_peek_error(), nullptr));
+  }
+
+  if (this->_stream_manager != nullptr && this->_application_started) {
+    this->_accept_openssl_streams();
+    this->_process_openssl_streams();
+  }
+
+  this->netActivity();
+}
+
+void
+QUICNetVConnection::_accept_openssl_streams()
+{
+  while (SSL *stream_ssl = SSL_accept_stream(this->_ssl, 
SSL_ACCEPT_STREAM_NO_BLOCK)) {
+    SSL_set_blocking_mode(stream_ssl, 0);
+    SSL_set_event_handling_mode(stream_ssl, 
SSL_VALUE_EVENT_HANDLING_MODE_IMPLICIT);
+    SSL_set_mode(stream_ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | 
SSL_MODE_ENABLE_PARTIAL_WRITE);
+
+    QUICStreamId stream_id = SSL_get_stream_id(stream_ssl);
+    this->_openssl_streams.emplace(stream_id, stream_ssl);
+
+    if (this->_stream_manager->find_stream(stream_id) == nullptr) {
+      [[maybe_unused]] QUICConnectionError err;
+      this->_stream_manager->create_stream(stream_id, err);
+    }
+  }
+}
+
+void
+QUICNetVConnection::_process_openssl_streams()
+{
+  for (auto &[stream_id, stream_ssl] : this->_openssl_streams) {

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   Maintenance heads-up: this range-`for` over `_openssl_streams` is only safe 
because nothing in `receive_data()`/`send_data()` synchronously re-enters 
`create_openssl_stream()` — which does `_openssl_streams.emplace(...)` and 
could rehash, invalidating this iterator (UB). That invariant holds today 
(server-initiated streams are all created in `Http3App::start()` before the 
first call here, and the data paths only `schedule_imm()`), but it's implicit. 
Consider snapshotting the stream ids before the loop, or adding a comment 
documenting the invariant, so a future change that opens a stream from within a 
data callback doesn't silently introduce UB.



##########
src/iocore/net/quic/OpenSSLQuicCompat.cc:
##########
@@ -0,0 +1,477 @@
+/** @file
+
+  Bridges quiche's legacy QUIC TLS callback API to OpenSSL 3.5's third-party
+  QUIC TLS callback API.
+
+  This compatibility layer exports the quictls/BoringSSL-style symbols that
+  quiche expects while ATS links against upstream OpenSSL 3.5. It stores
+  per-SSL callback state in OpenSSL ex-data and translates CRYPTO data,
+  encryption secrets, alerts, and transport parameters between the two APIs.
+
+  Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+  agreements.  See the NOTICE file distributed with this work for additional 
information regarding
+  copyright ownership.  The ASF licenses this file to you under the Apache 
License, Version 2.0
+  (the "License"); you may not use this file except in compliance with the 
License.  You may obtain
+  a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software 
distributed under the License
+  is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+  or implied. See the License for the specific language governing permissions 
and limitations under
+  the License.
+ */
+
+#include <openssl/core_dispatch.h>
+#include <openssl/crypto.h>
+#include <openssl/ssl.h>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <deque>
+#include <utility>
+#include <vector>
+
+enum ssl_encryption_level_t {
+  ssl_encryption_initial = 0,
+  ssl_encryption_early_data,
+  ssl_encryption_handshake,
+  ssl_encryption_application,
+};
+
+struct ssl_quic_method_st {
+  int (*set_encryption_secrets)(SSL *ssl, ssl_encryption_level_t level, 
uint8_t const *read_secret, uint8_t const *write_secret,
+                                size_t secret_len);
+  int (*add_handshake_data)(SSL *ssl, ssl_encryption_level_t level, uint8_t 
const *data, size_t len);
+  int (*flush_flight)(SSL *ssl);
+  int (*send_alert)(SSL *ssl, ssl_encryption_level_t level, uint8_t alert);
+};
+
+using SSL_QUIC_METHOD = ssl_quic_method_st;
+
+namespace
+{
+
+constexpr auto read_secret_direction  = 0;
+constexpr auto write_secret_direction = 1;
+constexpr auto quic_level_count       = 4;
+
+struct PendingSecret {
+  std::vector<uint8_t> read;
+  std::vector<uint8_t> write;
+  bool                 have_read{false};
+  bool                 have_write{false};
+  bool                 delivered{false};
+};
+
+struct QuicCompatState {
+  SSL_QUIC_METHOD const                                         
*method{nullptr};
+  std::array<std::deque<std::vector<uint8_t>>, quic_level_count> crypto_data;
+  std::array<PendingSecret, quic_level_count>                    secrets;
+  ssl_encryption_level_t                                         
read_level{ssl_encryption_initial};
+  ssl_encryption_level_t                                         
write_level{ssl_encryption_initial};
+  ssl_encryption_level_t                                         
active_recv_level{ssl_encryption_initial};
+  bool                                                           
active_recv{false};
+  std::vector<uint8_t>                                           
local_transport_params;
+  std::vector<uint8_t>                                           
peer_transport_params;
+};
+
+void
+free_quic_ex_data(void *, void *ptr, CRYPTO_EX_DATA *, int, long, void *)
+{
+  delete static_cast<QuicCompatState *>(ptr);
+}
+
+int
+quic_ex_data_index()
+{
+  static int index = SSL_get_ex_new_index(0, nullptr, nullptr, nullptr, 
free_quic_ex_data);
+
+  return index;
+}
+
+bool
+is_valid_level(ssl_encryption_level_t level)
+{
+  auto index = static_cast<int>(level);
+
+  return index >= 0 && index < quic_level_count;
+}
+
+size_t
+level_index(ssl_encryption_level_t level)
+{
+  return static_cast<size_t>(level);
+}
+
+uint8_t const *
+data_or_null(std::vector<uint8_t> const &data)
+{
+  return data.empty() ? nullptr : data.data();
+}
+
+bool
+assign_bytes(std::vector<uint8_t> &dst, uint8_t const *data, size_t len)
+{
+  if (len == 0) {
+    dst.clear();
+    return true;
+  }
+  if (data == nullptr) {
+    return false;
+  }
+
+  dst.assign(data, data + len);
+  return true;
+}
+
+QuicCompatState *
+get_state(SSL const *ssl)
+{
+  if (ssl == nullptr) {
+    return nullptr;
+  }
+
+  int const index = quic_ex_data_index();
+  return index < 0 ? nullptr : static_cast<QuicCompatState 
*>(SSL_get_ex_data(ssl, index));
+}
+
+QuicCompatState *
+get_or_create_state(SSL *ssl)
+{
+  if (ssl == nullptr) {
+    return nullptr;
+  }
+
+  if (auto *state = get_state(ssl); state != nullptr) {
+    return state;
+  }
+
+  auto     *state = new QuicCompatState;
+  int const index = quic_ex_data_index();
+  if (index < 0 || SSL_set_ex_data(ssl, index, state) != 1) {
+    delete state;
+    return nullptr;
+  }
+
+  return state;
+}
+
+bool
+compat_level_from_protection(uint32_t prot_level, ssl_encryption_level_t 
&level)
+{
+  switch (prot_level) {
+  case OSSL_RECORD_PROTECTION_LEVEL_NONE:
+    level = ssl_encryption_initial;
+    return true;
+  case OSSL_RECORD_PROTECTION_LEVEL_EARLY:
+    level = ssl_encryption_early_data;
+    return true;
+  case OSSL_RECORD_PROTECTION_LEVEL_HANDSHAKE:
+    level = ssl_encryption_handshake;
+    return true;
+  case OSSL_RECORD_PROTECTION_LEVEL_APPLICATION:
+    level = ssl_encryption_application;
+    return true;
+  default:
+    return false;
+  }
+}
+
+bool
+deliver_0rtt_secret(SSL *ssl, QuicCompatState &state, ssl_encryption_level_t 
level, int direction,
+                    std::vector<uint8_t> const &secret)
+{
+  if (state.method == nullptr || state.method->set_encryption_secrets == 
nullptr) {
+    return false;
+  }
+
+  bool const is_server = SSL_is_server(ssl) == 1;
+  if (direction == read_secret_direction && is_server) {
+    return state.method->set_encryption_secrets(ssl, level, 
data_or_null(secret), nullptr, secret.size()) == 1;
+  }
+
+  if (direction == write_secret_direction && !is_server) {
+    return state.method->set_encryption_secrets(ssl, level, nullptr, 
data_or_null(secret), secret.size()) == 1;
+  }
+
+  return true;
+}
+
+bool
+deliver_paired_secrets(SSL *ssl, QuicCompatState &state, 
ssl_encryption_level_t level)
+{
+  if (state.method == nullptr || state.method->set_encryption_secrets == 
nullptr) {
+    return false;
+  }
+
+  auto &pending = state.secrets[level_index(level)];
+  if (!pending.have_read || !pending.have_write || pending.delivered) {
+    return true;
+  }
+  if (pending.read.size() != pending.write.size()) {
+    return false;
+  }
+
+  int const result =
+    state.method->set_encryption_secrets(ssl, level, 
data_or_null(pending.read), data_or_null(pending.write), pending.read.size());
+  if (result == 1) {
+    pending.delivered = true;
+  }
+
+  return result == 1;
+}
+
+int
+crypto_send_cb(SSL *ssl, unsigned char const *buf, size_t buf_len, size_t 
*consumed, void *)
+{
+  auto *state = get_state(ssl);
+  if (state == nullptr || state->method == nullptr || 
state->method->add_handshake_data == nullptr) {
+    return 0;
+  }
+
+  if (consumed != nullptr) {
+    *consumed = 0;
+  }
+
+  static constexpr uint8_t empty_data = 0;
+  if (buf == nullptr && buf_len > 0) {
+    return 0;
+  }
+
+  auto const *data = buf_len == 0 ? &empty_data : reinterpret_cast<uint8_t 
const *>(buf);
+  if (state->method->add_handshake_data(ssl, state->write_level, data, 
buf_len) != 1) {
+    return 0;
+  }
+  if (state->method->flush_flight != nullptr && 
state->method->flush_flight(ssl) != 1) {
+    return 0;
+  }
+
+  if (consumed != nullptr) {
+    *consumed = buf_len;
+  }
+
+  return 1;
+}
+
+int
+crypto_recv_rcd_cb(SSL *ssl, unsigned char const **buf, size_t *bytes_read, 
void *)
+{
+  auto *state = get_state(ssl);
+  if (state == nullptr || buf == nullptr || bytes_read == nullptr) {
+    return 0;
+  }
+
+  *buf        = nullptr;
+  *bytes_read = 0;
+
+  if (state->active_recv) {
+    auto &active_queue = 
state->crypto_data[level_index(state->active_recv_level)];
+    if (!active_queue.empty()) {
+      *buf        = active_queue.front().data();
+      *bytes_read = active_queue.front().size();
+    }
+    return 1;
+  }
+
+  auto &queue = state->crypto_data[level_index(state->read_level)];
+  if (queue.empty()) {
+    return 1;
+  }
+
+  state->active_recv       = true;
+  state->active_recv_level = state->read_level;
+  *buf                     = queue.front().data();
+  *bytes_read              = queue.front().size();
+
+  return 1;
+}
+
+int
+crypto_release_rcd_cb(SSL *ssl, size_t bytes_read, void *)
+{
+  auto *state = get_state(ssl);
+  if (state == nullptr || !state->active_recv) {
+    return 1;
+  }
+
+  auto &queue = state->crypto_data[level_index(state->active_recv_level)];
+  if (!queue.empty()) {
+    if (bytes_read >= queue.front().size()) {
+      queue.pop_front();
+    } else {
+      queue.front().erase(queue.front().begin(), queue.front().begin() + 
bytes_read);
+    }
+  }
+  state->active_recv = false;
+
+  return 1;
+}
+
+int
+yield_secret_cb(SSL *ssl, uint32_t prot_level, int direction, unsigned char 
const *secret, size_t secret_len, void *)
+{
+  auto *state = get_state(ssl);
+  if (state == nullptr) {
+    return 0;
+  }
+
+  ssl_encryption_level_t level = ssl_encryption_initial;
+  if (!compat_level_from_protection(prot_level, level)) {
+    return 0;
+  }
+
+  if (direction == read_secret_direction) {
+    state->read_level = level;
+  } else if (direction == write_secret_direction) {
+    state->write_level = level;
+  } else {
+    return 0;
+  }
+
+  std::vector<uint8_t> secret_copy;
+  if (!assign_bytes(secret_copy, reinterpret_cast<uint8_t const *>(secret), 
secret_len)) {
+    return 0;
+  }
+
+  if (level == ssl_encryption_early_data) {
+    return deliver_0rtt_secret(ssl, *state, level, direction, secret_copy) ? 1 
: 0;
+  }
+
+  auto &pending = state->secrets[level_index(level)];
+  if (direction == read_secret_direction) {
+    pending.read      = std::move(secret_copy);
+    pending.have_read = true;
+  } else {
+    pending.write      = std::move(secret_copy);
+    pending.have_write = true;
+  }
+  pending.delivered = false;
+
+  return deliver_paired_secrets(ssl, *state, level) ? 1 : 0;
+}
+
+int
+got_transport_params_cb(SSL *ssl, unsigned char const *params, size_t 
params_len, void *)
+{
+  auto *state = get_state(ssl);
+  if (state == nullptr) {
+    return 0;
+  }
+
+  if (!assign_bytes(state->peer_transport_params, reinterpret_cast<uint8_t 
const *>(params), params_len)) {
+    return 0;
+  }
+
+  return 1;
+}
+
+int
+alert_cb(SSL *ssl, unsigned char alert_code, void *)
+{
+  auto *state = get_state(ssl);
+  if (state == nullptr || state->method == nullptr || 
state->method->send_alert == nullptr) {
+    return 0;
+  }
+
+  return state->method->send_alert(ssl, state->write_level, alert_code);
+}
+
+OSSL_DISPATCH const quic_tls_callbacks[] = {
+  {OSSL_FUNC_SSL_QUIC_TLS_CRYPTO_SEND,          reinterpret_cast<void 
(*)(void)>(crypto_send_cb)         },
+  {OSSL_FUNC_SSL_QUIC_TLS_CRYPTO_RECV_RCD,      reinterpret_cast<void 
(*)(void)>(crypto_recv_rcd_cb)     },
+  {OSSL_FUNC_SSL_QUIC_TLS_CRYPTO_RELEASE_RCD,   reinterpret_cast<void 
(*)(void)>(crypto_release_rcd_cb)  },
+  {OSSL_FUNC_SSL_QUIC_TLS_YIELD_SECRET,         reinterpret_cast<void 
(*)(void)>(yield_secret_cb)        },
+  {OSSL_FUNC_SSL_QUIC_TLS_GOT_TRANSPORT_PARAMS, reinterpret_cast<void 
(*)(void)>(got_transport_params_cb)},
+  {OSSL_FUNC_SSL_QUIC_TLS_ALERT,                reinterpret_cast<void 
(*)(void)>(alert_cb)               },
+  {0,                                           nullptr                        
                          },
+};
+
+} // namespace
+
+extern "C" int
+SSL_set_quic_method(SSL *ssl, SSL_QUIC_METHOD const *quic_method)

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   This compatibility shim is the most security-sensitive part of the PR — it 
bridges TLS secret material (`yield_secret_cb`, the crypto send/recv/release 
callbacks) between quiche's legacy QUIC-TLS API and OpenSSL 3.5's third-party 
callback API. On my read the read/write secret pairing is correct for TLS 1.3 
(equal-length secrets, paired delivery via `deliver_paired_secrets`), and 
`SSL_set_quic_use_legacy_codepoint` being a no-op is fine for 3.5's standard 
transport-param codepoint.
   
   No specific defect to report here — flagging it for a dedicated, focused 
review pass plus an interop matrix (quiche peer ↔ this shim, both client and 
server roles), given the blast radius if secret translation is subtly wrong.



##########
src/iocore/net/quic/QUICStreamManager.cc:
##########
@@ -113,14 +113,38 @@ QUICStreamManager::create_stream(QUICStreamId stream_id, 
QUICConnectionError & /
 }
 
 QUICConnectionErrorUPtr
-QUICStreamManager::create_uni_stream(QUICStreamId /* new_stream_id ATS_UNUSED 
*/)
+QUICStreamManager::create_uni_stream(QUICStreamId &new_stream_id)

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   Behavior change worth flagging on the **quiche** path. Previously 
`create_uni_stream`/`create_bidi_stream` were no-op stubs (param unused, 
returned `nullptr` without setting the id), so `Http3App::start()` ended up 
registering the control stream under id `0`. This now allocates a real 
server-initiated id (`SERVER_UNI + 4n`) and calls `create_stream()`. That's an 
improvement, but it modifies the shared base class used by the quiche backend — 
not just the new OpenSSL path. Please confirm the quiche backend 
(`ci-fedora-quiche`) is regression-tested: the H3 control stream now genuinely 
opens where it effectively didn't before.



##########
src/iocore/net/QUICMultiCertConfigLoader.cc:
##########
@@ -88,12 +90,191 @@ QUICCertConfig::release(SSLCertLookup *lookup)
 SSL_CTX *
 QUICMultiCertConfigLoader::default_server_ssl_ctx()
 {
-  return quic_new_ssl_ctx();
+  return quic_new_server_ssl_ctx();
 }
 
+#if TS_HAS_OPENSSL_QUIC
+namespace
+{
+DbgCtl dbg_ctl_quic{"quic"};
+} // end anonymous namespace
+
+namespace
+{
 bool
-QUICMultiCertConfigLoader::_setup_session_cache(SSL_CTX * /* ctx ATS_UNUSED */)
+apply_quic_sni_certificate(SSL *ssl, SSL_CTX *ctx)
+{
+  X509     *cert = SSL_CTX_get0_certificate(ctx);
+  EVP_PKEY *key  = SSL_CTX_get0_privatekey(ctx);
+
+  if (cert == nullptr && key == nullptr) {
+    return true;
+  }
+  if (cert == nullptr || key == nullptr) {
+    return false;
+  }
+
+  if (SSL_use_certificate(ssl, cert) != 1 || SSL_use_PrivateKey(ssl, key) != 
1) {
+    return false;
+  }
+
+  STACK_OF(X509) *chain = nullptr;
+  if (SSL_CTX_get0_chain_certs(ctx, &chain) == 1 && chain != nullptr && 
SSL_set1_chain(ssl, chain) != 1) {
+    return false;
+  }
+
+  return true;
+}
+
+bool
+select_quic_sni_context(SSL *ssl, std::string_view servername)
+{
+  if (servername.empty()) {
+    return true;
+  }
+
+  QUICCertConfig::scoped_config lookup;
+  if (!lookup) {
+    Dbg(dbg_ctl_quic, "OpenSSL QUIC SNI context selection could not acquire 
cert lookup");
+    return false;
+  }
+
+  SSLCertContext *cert_context = lookup->find(std::string(servername));
+  if (cert_context == nullptr) {
+    return true;
+  }
+
+  shared_SSL_CTX const selected_ctx = cert_context->getCtx();
+  if (selected_ctx == nullptr) {
+    Dbg(dbg_ctl_quic, "OpenSSL QUIC SNI context selection found null context 
for SNI '%.*s'", static_cast<int>(servername.size()),
+        servername.data());
+    return false;
+  }
+
+  SSL_set_SSL_CTX(ssl, selected_ctx.get());
+  if (!apply_quic_sni_certificate(ssl, selected_ctx.get())) {
+    Dbg(dbg_ctl_quic, "OpenSSL QUIC SNI context selection failed to apply 
SSL_CTX %p certificate to ssl=%p", selected_ctx.get(),
+        ssl);
+    return false;
+  }
+
+  return true;
+}
+
+std::string_view
+servername_from_client_hello(SSL *ssl)
 {
+  unsigned char const *p         = nullptr;
+  size_t               remaining = 0;
+  if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &p, &remaining) 
!= 1 || remaining <= 2) {
+    return {};
+  }
+
+  size_t len  = (static_cast<size_t>(*p++) << 8);
+  len        += *p++;
+  if (len + 2 != remaining) {
+    return {};
+  }
+  remaining = len;
+
+  if (remaining <= 3 || *p++ != TLSEXT_NAMETYPE_host_name) {
+    return {};
+  }
+  --remaining;
+
+  len  = (static_cast<size_t>(*p++) << 8);
+  len += *p++;
+  if (len + 2 > remaining) {
+    return {};
+  }
+
+  return {reinterpret_cast<char const *>(p), len};
+}
+
+int
+quic_client_hello_callback(SSL *ssl, int *, void *)
+{
+  return select_quic_sni_context(ssl, servername_from_client_hello(ssl)) ? 
SSL_CLIENT_HELLO_SUCCESS : SSL_CLIENT_HELLO_ERROR;
+}
+
+int
+quic_servername_callback(SSL *ssl, int *, void *)
+{
+  char const *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+  return select_quic_sni_context(ssl, servername == nullptr ? 
std::string_view{} : std::string_view{servername}) ?
+           SSL_TLSEXT_ERR_OK :
+           SSL_TLSEXT_ERR_ALERT_FATAL;
+}
+} // end anonymous namespace
+
+void
+QUICMultiCertConfigLoader::_set_handshake_callbacks(SSL_CTX *ctx)
+{
+  Dbg(dbg_ctl_quic, "installing OpenSSL QUIC cert callback on SSL_CTX %p", 
ctx);
+  SSL_CTX_set_client_hello_cb(ctx, quic_client_hello_callback, nullptr);

Review Comment:
   🤖 _Automated review by **Claude Code**, posted on behalf of @phongn._
   
   `_set_handshake_callbacks` installs three overlapping callbacks that all 
funnel into `select_quic_sni_context`: `SSL_CTX_set_client_hello_cb`, 
`SSL_CTX_set_tlsext_servername_callback`, and `SSL_CTX_set_cert_cb`. They'll 
each fire per handshake, repeating the `SSL_set_SSL_CTX` + manual cert 
re-application up to three times. The client-hello path also uses a hand-rolled 
SNI-extension parser (`servername_from_client_hello`) that re-implements what 
OpenSSL already provides via the servername callback.
   
   Recommend collapsing to a single callback (the `cert_cb` or 
`client_hello_cb` alone is sufficient for cert switching) and dropping the 
bespoke parser — smaller attack surface and no risk of the two SNI paths 
diverging. (The parser's bounds checks look correct on my read; this is about 
redundancy, not a parsing defect.)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to