diff --git a/CMakeLists.txt b/CMakeLists.txt index 375f1d700ac..5894f2c7511 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,8 @@ SET(NGHTTP2_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/include" CACHE STRING "Direct SET(NGHTTP2_LIBRARIES ${default_nghttp2_libraries} CACHE STRING "NGHTTP2 libraries to link with") SET(NGHTTP3_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/include" CACHE STRING "Directory with NGHTTP3 include files within nghttp3 subdirectory") SET(NGHTTP3_LIBRARIES ${default_nghttp3_libraries} CACHE STRING "NGHTTP3 libraries to link with") +SET(NGTCP2_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/include" CACHE STRING "Directory with NGTCP2 include files within ngtcp2 subdirectory") +SET(NGTCP2_LIBRARIES "${CMAKE_INSTALL_PREFIX}/lib/ngtcp2.lib" "${CMAKE_INSTALL_PREFIX}/lib/ngtcp2_crypto_ossl.lib" CACHE STRING "NGTCP2 libraries to link with") SET(PCRE_CFLAGS "${default_pcre_cflags}" CACHE STRING "PCRE flags for util_pcre.c compilation") SET(PCRE_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/include" CACHE STRING "Directory with PCRE include files") SET(PCRE_LIBRARIES ${default_pcre_libraries} CACHE STRING "PCRE libraries to link with") @@ -244,6 +246,18 @@ ELSE() SET(NGHTTP3_FOUND FALSE) ENDIF() +# See if ngtcp2 exists in a configured or defaulted location +SET(NGTCP2_FOUND TRUE) +IF(EXISTS "${NGTCP2_INCLUDE_DIR}/ngtcp2/ngtcp2_crypto_ossl.h") + FOREACH(onelib ${NGTCP2_LIBRARIES}) + IF(NOT EXISTS ${onelib}) + SET(NGTCP2_FOUND FALSE) + ENDIF() + ENDFOREACH() +ELSE() + SET(NGTCP2_FOUND FALSE) +ENDIF() + # See if we have Brotli SET(BROTLI_FOUND TRUE) IF(EXISTS "${BROTLI_INCLUDE_DIR}/brotli/encode.h") @@ -288,6 +302,7 @@ MESSAGE(STATUS "LIBXML2_FOUND ............ : ${LIBXML2_FOUND}") MESSAGE(STATUS "LUA51_FOUND .............. : ${LUA51_FOUND}") MESSAGE(STATUS "NGHTTP2_FOUND ............ : ${NGHTTP2_FOUND}") MESSAGE(STATUS "NGHTTP3_FOUND ............ : ${NGHTTP3_FOUND}") +MESSAGE(STATUS "NGTCP2_FOUND ............. : ${NGTCP2_FOUND}") MESSAGE(STATUS "OPENSSL_FOUND ............ : ${OPENSSL_FOUND}") MESSAGE(STATUS "ZLIB_FOUND ............... : ${ZLIB_FOUND}") MESSAGE(STATUS "BROTLI_FOUND ............. : ${BROTLI_FOUND}") @@ -575,9 +590,9 @@ SET(mod_http2_extra_sources modules/http2/h2_util.c modules/http2/h2_workers.c modules/http2/h2_ws.c ) -SET(mod_http3_requires NGHTTP3_FOUND) -SET(mod_http3_extra_includes ${NGHTTP3_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/modules/http3/include ${OPENSSL_INCLUDE_DIR}) -SET(mod_http3_extra_libs ${NGHTTP3_LIBRARIES} ${OPENSSL_LIBRARIES}) +SET(mod_http3_requires NGHTTP3_FOUND NGTCP2_FOUND) +SET(mod_http3_extra_includes ${NGHTTP3_INCLUDE_DIR} ${NGTCP2_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/modules/http3/include ${OPENSSL_INCLUDE_DIR}) +SET(mod_http3_extra_libs ${NGHTTP3_LIBRARIES} ${NGTCP2_LIBRARIES} ${OPENSSL_LIBRARIES}) SET(mod_http3_main_source modules/http3/mod_http3.c) SET(mod_http3_extra_sources modules/http3/h3_callbacks.c modules/http3/h3_config.c @@ -588,7 +603,6 @@ SET(mod_http3_extra_sources modules/http3/h3_stream.c modules/http3/h3_threads.c modules/http3/quic/h3q.c modules/http3/quic/h3q_conn.c modules/http3/quic/h3q_stream.c - modules/http3/quic/detail/h3q_addr.c modules/http3/quic/detail/h3q_tls.c ) SET(mod_ldap_extra_defines LDAP_DECLARE_EXPORT) diff --git a/changes-entries/h3_blocked_stream_spin.txt b/changes-entries/h3_blocked_stream_spin.txt new file mode 100644 index 00000000000..1b230bfa5d4 --- /dev/null +++ b/changes-entries/h3_blocked_stream_spin.txt @@ -0,0 +1,4 @@ + *) mod_http3: Fixed an event-thread spin while a client holds its + flow-control window shut: a blocked stream is now blocked again each time + nghttp3 offers it. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_drain_pending_handshakes.txt b/changes-entries/h3_drain_pending_handshakes.txt new file mode 100644 index 00000000000..c38c37f7041 --- /dev/null +++ b/changes-entries/h3_drain_pending_handshakes.txt @@ -0,0 +1,4 @@ + *) mod_http3: Fixed a graceful restart that never ended: a handshake still + pending when the child started to drain did not finish or time out, so + the old child kept its connection count and the port. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_flow_window_autotune.txt b/changes-entries/h3_flow_window_autotune.txt new file mode 100644 index 00000000000..baa54d0ec19 --- /dev/null +++ b/changes-entries/h3_flow_window_autotune.txt @@ -0,0 +1,5 @@ + *) mod_http3: Added H3MaxWindow (default 6 MiB). Flow-control windows for + client uploads now grow when a client fills them within a few round + trips, up to this cap, so uploads on long, fast paths are no longer held + to a fixed 1 MiB window. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_memory_cid_map.txt b/changes-entries/h3_memory_cid_map.txt new file mode 100644 index 00000000000..6646a15cf0a --- /dev/null +++ b/changes-entries/h3_memory_cid_map.txt @@ -0,0 +1,11 @@ + *) mod_http3: Less memory per connection. The QUIC connection keeps its + connection IDs in 16 slots of its own, not in an APR pool (8 KB at + least), and makes the CONNECTION_CLOSE buffer only when it closes. The + session and stream allocators keep at most MaxMemFree, as in mod_http2, + so an idle connection gives back its peak memory. + [Alexander Gerasimov ] + + *) mod_http3: A client can no longer put an Initial DCID that is a CID of + another connection into the CID map. Before, the map could keep a key + that pointed into the pool of a freed connection. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_mod_ssl_certificate.txt b/changes-entries/h3_mod_ssl_certificate.txt new file mode 100644 index 00000000000..ab44463450c --- /dev/null +++ b/changes-entries/h3_mod_ssl_certificate.txt @@ -0,0 +1,24 @@ + *) mod_http3: Load the HTTP/3 certificate and key in post_config, while + httpd still runs privileged, so a root-only key no longer fails in the + unprivileged child and both mod_ssl and mod_http3 read the same files. + [Alexander Gerasimov ] + + *) mod_http3: A host with "h3" in Protocols now serves HTTP/3 with the + certificate mod_ssl resolved for it (SSLCertificateFile and mod_md + alike), the way mod_http2 rides mod_ssl. H3CertificatePath and + H3CertificateKeyPath are removed; drop them from existing configurations. + [Alexander Gerasimov ] + + *) mod_http3: Select the HTTP/3 certificate by SNI, so each virtual host on + a shared port serves its own certificate instead of the first host's. + Names are matched as httpd matches virtual hosts, wildcard ServerAlias + included. A name that matches no host gets the certificate of the first + virtual host defined, as with mod_ssl. + [Alexander Gerasimov ] + + *) mod_http3: Give every virtual host its own mod_http3 configuration + (AP_MODULE_FLAG_ALWAYS_MERGE). A host without H3 directives used to share + the main server's, so per-host settings such as H3AltSvc and + H3AltSvcMaxAge were silently ignored and hosts could not carry their own + certificate. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_ngtcp2_early_data.txt b/changes-entries/h3_ngtcp2_early_data.txt new file mode 100644 index 00000000000..23c3695fe36 --- /dev/null +++ b/changes-entries/h3_ngtcp2_early_data.txt @@ -0,0 +1,14 @@ + *) mod_http3: Added H3EarlyData (default off). A resumed client may send its + first request as 0-RTT data and gets the response one round trip sooner. + Only safe methods run before the handshake completes; other methods wait + for it, because 0-RTT data can be replayed (RFC 8470). + [Alexander Gerasimov ] + + *) mod_http3: Moved the QUIC transport from the OpenSSL QUIC server to + ngtcp2, with OpenSSL as the TLS backend through its QUIC TLS API. OpenSSL + still drops server-side 0-RTT packets. ngtcp2 >= 1.22.1 is a new build + dependency (--with-ngtcp2). Stream data is no longer copied: nghttp3 + releases a buffer when the peer acknowledges it. H3IdleTimeout now always + closes with NO_ERROR; the QUIC idle timer runs 2 seconds longer and + closes silently. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_parent_socket.txt b/changes-entries/h3_parent_socket.txt new file mode 100644 index 00000000000..664b0aad378 --- /dev/null +++ b/changes-entries/h3_parent_socket.txt @@ -0,0 +1,5 @@ + *) mod_http3: On Linux the parent binds the QUIC socket while it still runs + as root, so H3Port 443 works without CAP_NET_BIND_SERVICE. The socket + stays bound across graceful restarts and the children inherit it; one + child at a time serves it. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_pktinfo_ecn_token_key.txt b/changes-entries/h3_pktinfo_ecn_token_key.txt new file mode 100644 index 00000000000..7dc04eaec5b --- /dev/null +++ b/changes-entries/h3_pktinfo_ecn_token_key.txt @@ -0,0 +1,21 @@ + *) mod_http3: Linux: on a [::] socket, a reply now goes out from the address + the client sent to (IP_PKTINFO, IPV6_PKTINFO). Before, the kernel picked + the source, and on a host with more than one address a client could get + the reply from another address and drop it. + [Alexander Gerasimov ] + + *) mod_http3: Linux: ECN. The module reads the ECN mark of each packet and + gives it to ngtcp2, which reports it back to the client in ACK_ECN + frames, and it puts the mark that ngtcp2 asks for on each packet it + sends. + [Alexander Gerasimov ] + + *) mod_http3: With ListenCoresBucketsRatio and more than one QUIC socket, + the server sends disable_active_migration, so clients do not move a + connection to a new address that another child can get. + [Alexander Gerasimov ] + + *) mod_http3: The parent makes the key for Retry and stateless reset tokens + and keeps it across graceful restarts. A Retry token from a child is now + good in every other child and in the next generation. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_reuseport_buckets.txt b/changes-entries/h3_reuseport_buckets.txt new file mode 100644 index 00000000000..6943718875a --- /dev/null +++ b/changes-entries/h3_reuseport_buckets.txt @@ -0,0 +1,6 @@ + *) mod_http3: On Linux, with ListenCoresBucketsRatio, the parent binds one + SO_REUSEPORT QUIC socket per listener bucket and one child serves each, + so more than one child serves HTTP/3. The sockets stay bound when + children start and stop, so the kernel keeps each peer on the same + socket. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_session_allocator_race.txt b/changes-entries/h3_session_allocator_race.txt new file mode 100644 index 00000000000..87ebb36e22b --- /dev/null +++ b/changes-entries/h3_session_allocator_race.txt @@ -0,0 +1,3 @@ + *) mod_http3: Fixed a heap race that crashed the child: worker threads and + the event thread allocated from one session allocator that had no mutex. + [Alexander Gerasimov ] diff --git a/changes-entries/h3_udp_gso_gro.txt b/changes-entries/h3_udp_gso_gro.txt new file mode 100644 index 00000000000..14e1df5b7be --- /dev/null +++ b/changes-entries/h3_udp_gso_gro.txt @@ -0,0 +1,5 @@ + *) mod_http3: Linux: QUIC packets to one peer now go out in one sendmsg with + UDP_SEGMENT (GSO), and reads use UDP_GRO. Without kernel support the + module sends one packet at a time. A stream read now sends the new + flow-control window at once, so a blocked client does not wait. + [Alexander Gerasimov ] diff --git a/modules/http3/config2.m4 b/modules/http3/config2.m4 index eb155cd70b3..45ed2cc75da 100644 --- a/modules/http3/config2.m4 +++ b/modules/http3/config2.m4 @@ -36,7 +36,6 @@ h3_threads.lo dnl h3q.lo dnl h3q_conn.lo dnl h3q_stream.lo dnl -h3q_addr.lo dnl h3q_tls.lo dnl " @@ -158,11 +157,57 @@ AC_DEFUN([APACHE_CHECK_NGHTTP3],[ ]) +dnl +dnl APACHE_CHECK_NGTCP2 +dnl +dnl Configure for ngtcp2 and its OpenSSL crypto helper, giving preference +dnl to "--with-ngtcp2=" if it was specified. +dnl +AC_DEFUN([APACHE_CHECK_NGTCP2],[ + AC_CACHE_CHECK([for ngtcp2], [ac_cv_ngtcp2], [ + ac_cv_ngtcp2=no + ap_ngtcp2_base="" + AC_ARG_WITH(ngtcp2, APACHE_HELP_STRING(--with-ngtcp2=PATH, ngtcp2 installation directory), [ + if test "x$withval" != "xyes" -a "x$withval" != "x"; then + ap_ngtcp2_base="`cd $withval ; pwd`" + fi + ]) + + saved_CPPFLAGS="$CPPFLAGS" + saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" + if test "x$ap_ngtcp2_base" != "x"; then + PKG_CONFIG_PATH="${ap_ngtcp2_base}/lib/pkgconfig:${ap_ngtcp2_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + fi + if test -n "$PKGCONFIG" && $PKGCONFIG --exists libngtcp2 libngtcp2_crypto_ossl; then + ap_ngtcp2_cflags="`$PKGCONFIG --cflags libngtcp2 libngtcp2_crypto_ossl`" + ap_ngtcp2_libs="`$PKGCONFIG --libs libngtcp2 libngtcp2_crypto_ossl`" + elif test "x$ap_ngtcp2_base" != "x"; then + ap_ngtcp2_cflags="-I$ap_ngtcp2_base/include" + ap_ngtcp2_libs="-L$ap_ngtcp2_base/lib -lngtcp2_crypto_ossl -lngtcp2" + else + ap_ngtcp2_cflags="" + ap_ngtcp2_libs="-lngtcp2_crypto_ossl -lngtcp2" + fi + PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH" + + CPPFLAGS="$CPPFLAGS $ap_ngtcp2_cflags" + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include ]],[[ +#if NGTCP2_VERSION_NUM < 0x011601 +#error "ngtcp2 1.22.1 or later is required" +#endif]])], + [ac_cv_ngtcp2=yes + APR_ADDTO(MOD_CFLAGS, [$ap_ngtcp2_cflags]) + APR_ADDTO(MOD_LDFLAGS, [$ap_ngtcp2_libs])]) + CPPFLAGS="$saved_CPPFLAGS" + ]) +]) + dnl # hook module into the Autoconf mechanism (--enable-http3) APACHE_MODULE(http3, [HTTP/3 protocol handling in addition to HTTP protocol -handling. Implemented by mod_http3. This module requires a libnghttp3 -installation and OpenSSL with QUIC support. -See --with-nghttp3 on how to manage non-standard locations. This module +handling. Implemented by mod_http3. This module requires libnghttp3, +libngtcp2 with its OpenSSL crypto helper, and OpenSSL with the QUIC TLS API. +See --with-nghttp3 and --with-ngtcp2 on how to manage non-standard locations. This module is usually linked shared and requires loading. ], $http3_objs, , no, [ APACHE_CHECK_OPENSSL if test "$ac_cv_openssl" = "yes" ; then @@ -173,7 +218,8 @@ is usually linked shared and requires loading. ], $http3_objs, , no, [ if test "$enable_http3" != "no" ; then APACHE_CHECK_NGHTTP3 - if test "$ac_cv_nghttp3" = "yes" ; then + APACHE_CHECK_NGTCP2 + if test "$ac_cv_nghttp3" = "yes" -a "$ac_cv_ngtcp2" = "yes" ; then APR_ADDTO(MOD_CFLAGS, [-I\$(top_srcdir)/$modpath_current/include]) if test "x$enable_http3" = "xshared"; then # The only symbol which needs to be exported is the module diff --git a/modules/http3/h3_config.c b/modules/http3/h3_config.c index cde39bd8e31..ecaed9468d5 100644 --- a/modules/http3/h3_config.c +++ b/modules/http3/h3_config.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -33,7 +34,9 @@ #include "h3_config.h" #include "h3_os.h" #include "h3_request.h" +#include "h3_server.h" #include "mod_http3.h" +#include "quic/detail/h3q_tls.h" apr_port_t get_server_port(const server_rec* s) { @@ -58,8 +61,6 @@ void* h3_merge_server_config(apr_pool_t* p, void* base_conf, void* new_conf) h3_server_conf* base = (h3_server_conf*)base_conf; h3_server_conf* new = (h3_server_conf*)new_conf; - merged->h3_cert_path = new->h3_cert_path ? new->h3_cert_path : base->h3_cert_path; - merged->h3_key_path = new->h3_key_path ? new->h3_key_path : base->h3_key_path; merged->h3_port = new->h3_port ? new->h3_port : base->h3_port; merged->h3_max_concurrent_streams = new->h3_max_concurrent_streams ? new->h3_max_concurrent_streams : base->h3_max_concurrent_streams; merged->h3_max_connections = new->h3_max_connections ? new->h3_max_connections : base->h3_max_connections; @@ -72,7 +73,9 @@ void* h3_merge_server_config(apr_pool_t* p, void* base_conf, void* new_conf) merged->h3_handshake_timeout = new->h3_handshake_timeout ? new->h3_handshake_timeout : base->h3_handshake_timeout; merged->h3_idle_timeout = new->h3_idle_timeout ? new->h3_idle_timeout : base->h3_idle_timeout; merged->h3_socket_buffer_size = new->h3_socket_buffer_size ? new->h3_socket_buffer_size : base->h3_socket_buffer_size; + merged->h3_max_window = new->h3_max_window ? new->h3_max_window : base->h3_max_window; merged->h3_session_tickets = new->h3_session_tickets != H3_FLAG_UNSET ? new->h3_session_tickets : base->h3_session_tickets; + merged->h3_early_data = new->h3_early_data != H3_FLAG_UNSET ? new->h3_early_data : base->h3_early_data; merged->h3_stream_timeout = new->h3_stream_timeout ? new->h3_stream_timeout : base->h3_stream_timeout; merged->h3_max_stream_errors = new->h3_max_stream_errors ? new->h3_max_stream_errors : base->h3_max_stream_errors; merged->h3_qpack_capacity_set = new->h3_qpack_capacity_set ? new->h3_qpack_capacity_set : base->h3_qpack_capacity_set; @@ -93,28 +96,6 @@ static int mpm_query(int code) return ap_mpm_query(code, &value) == APR_SUCCESS ? value : -1; } -static const char* set_string(cmd_parms* cmd, const char* arg, const char* field) -{ - h3_server_conf* conf = ap_get_module_config(cmd->server->module_config, &http3_module); - if (!conf) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, cmd->server, "mod_http3: server config missing in directive"); - return "mod_http3: internal error: no server config"; - } - *(const char**)((char*)conf + (apr_size_t)field) = apr_pstrdup(cmd->pool, arg); - return NULL; -} - -static const char* set_h3_cert_path(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) -{ - return set_string(cmd, arg, (const char*)offsetof(h3_server_conf, h3_cert_path)); -} - -static const char* set_h3_key_path(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) -{ - return set_string(cmd, arg, (const char*)offsetof(h3_server_conf, h3_key_path)); -} - static const char* set_h3_port(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) { if (!arg || !*arg) @@ -219,6 +200,19 @@ static const char* set_h3_stream_buffer_size(cmd_parms* cmd, void* dummy H3_UNUS return NULL; } +static const char* set_h3_max_window(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) +{ + apr_uint64_t val = 0; + if (apr_cstr_atoui64(&val, arg) != APR_SUCCESS || val < H3_MAX_WINDOW_MIN || val > H3_MAX_WINDOW_MAX) + { + return apr_psprintf(cmd->pool, "H3MaxWindow: '%s' must be a byte count from %lu to %lu", arg, (unsigned long)H3_MAX_WINDOW_MIN, (unsigned long)H3_MAX_WINDOW_MAX); + } + h3_server_conf* conf = ap_get_module_config(cmd->server->module_config, &http3_module); + CHECK(conf); + conf->h3_max_window = (apr_size_t)val; + return NULL; +} + static const char* set_h3_socket_buffer_size(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) { if (!arg || !*arg) @@ -541,6 +535,14 @@ static const char* set_h3_session_tickets(cmd_parms* cmd, void* dummy H3_UNUSED, return NULL; } +static const char* set_h3_early_data(cmd_parms* cmd, void* dummy H3_UNUSED, int flag) +{ + h3_server_conf* conf = ap_get_module_config(cmd->server->module_config, &http3_module); + CHECK(conf); + conf->h3_early_data = flag ? H3_FLAG_ON : H3_FLAG_OFF; + return NULL; +} + static const char* set_h3_alt_svc(cmd_parms* cmd, void* dummy H3_UNUSED, int flag) { h3_server_conf* conf = ap_get_module_config(cmd->server->module_config, &http3_module); @@ -583,22 +585,101 @@ static const char* set_h3_alt_svc_max_age(cmd_parms* cmd, void* dummy H3_UNUSED, return NULL; } -int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, server_rec* s) +static apr_status_t ssl_ctx_cleanup(void* data) +{ + SSL_CTX_free(data); + return APR_SUCCESS; +} + +int h3_ssl_add_cert_files(server_rec* s, apr_pool_t* p, apr_array_header_t* cert_files, apr_array_header_t* key_files) +{ + CHECK(s && p && cert_files && key_files, return DECLINED;); + h3_server_conf* conf = ap_get_module_config(s->module_config, &http3_module); + if (!conf || ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG || !ap_is_allowed_protocol(NULL, NULL, s, "h3") || cert_files->nelts == 0) + { + return DECLINED; + } + /* Still privileged here, so a root-only key loads the way it does for mod_ssl. */ + char err[H3Q_ERRLEN] = {0}; + conf->ssl_ctx = h3q_tls_ctx_create((const char* const*)cert_files->elts, (size_t)cert_files->nelts, (const char* const*)key_files->elts, (size_t)key_files->nelts, conf->h3_session_tickets != H3_FLAG_OFF, err, sizeof(err)); + if (!conf->ssl_ctx) + { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: %s", err); + return HTTP_INTERNAL_SERVER_ERROR; + } + apr_pool_cleanup_register(p, conf->ssl_ctx, ssl_ctx_cleanup, apr_pool_cleanup_null); + ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "mod_http3: serving HTTP/3 with mod_ssl certificate %s", APR_ARRAY_IDX(cert_files, 0, const char*)); + return DECLINED; +} + +/** One HTTP/3 host name and the TLS context that carries its certificate. */ +typedef struct { + const char* name; + SSL_CTX* ctx; +} h3_sni_host; + +/// cert_cb: serve each host its own certificate over one listener, selected by SNI. +/// SSL_set_SSL_CTX does not switch the certificate of a QUIC connection; applying +/// the matched host's certificate, key and chain to the connection does. +static int h3_sni_select_cert(SSL* ssl, void* arg) +{ + const apr_array_header_t* hosts = arg; + const char* sni = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + for (int i = 0; sni && i < hosts->nelts; i++) + { + const h3_sni_host* h = &APR_ARRAY_IDX(hosts, i, h3_sni_host); + /* httpd's ServerAlias matcher: wildcards for wild_names, exact otherwise. */ + if (ap_strcasecmp_match(sni, h->name) == 0) + { + STACK_OF(X509)* chain = NULL; + SSL_CTX_get0_chain_certs(h->ctx, &chain); + return SSL_use_certificate(ssl, SSL_CTX_get0_certificate(h->ctx)) == 1 && SSL_use_PrivateKey(ssl, SSL_CTX_get0_privatekey(h->ctx)) == 1 && (!chain || SSL_set1_chain(ssl, chain) == 1); + } + } + return 1; /* no match: the listener's own certificate */ +} + +int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, server_rec* s) +{ + CHECK(p); CHECK(ptemp); CHECK(s); h3_server_conf* conf = NULL; + server_rec* owner = NULL; if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG) { return OK; } + /* Names of every HTTP/3 host, so the listener can pick a certificate by SNI. */ + apr_array_header_t* sni = apr_array_make(p, 4, sizeof(h3_sni_host)); + for (server_rec* vs = s; vs; vs = vs->next) { h3_server_conf* vc = ap_get_module_config(vs->module_config, &http3_module); - if (vc->h3_cert_path && vc->h3_key_path) + if (vc->ssl_ctx) { + if (vs->server_hostname) + { + h3_sni_host* e = apr_array_push(sni); + e->name = vs->server_hostname; + e->ctx = vc->ssl_ctx; + } + /* ServerAlias: httpd keeps exact names in names and wildcards in wild_names. */ + for (int i = 0; vs->names && i < vs->names->nelts; i++) + { + h3_sni_host* e = apr_array_push(sni); + e->name = APR_ARRAY_IDX(vs->names, i, const char*); + e->ctx = vc->ssl_ctx; + } + for (int i = 0; vs->wild_names && i < vs->wild_names->nelts; i++) + { + h3_sni_host* e = apr_array_push(sni); + e->name = APR_ARRAY_IDX(vs->wild_names, i, const char*); + e->ctx = vc->ssl_ctx; + } vc->host_port = get_server_port(vs); if (vc->h3_port == 0) { @@ -620,6 +701,10 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool { vc->h3_socket_buffer_size = H3_SOCKET_BUFFER_SIZE_DEFAULT; } + if (vc->h3_max_window == 0) + { + vc->h3_max_window = H3_MAX_WINDOW_DEFAULT; + } if (vc->h3_max_stream_errors == 0) { vc->h3_max_stream_errors = H3_MAX_STREAM_ERRORS_DEFAULT; @@ -669,6 +754,10 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool { vc->h3_session_tickets = H3_FLAG_ON; } + if (vc->h3_early_data == H3_FLAG_UNSET) + { + vc->h3_early_data = H3_FLAG_OFF; + } if (vc->h3_alt_svc_max_age == 0) { vc->h3_alt_svc_max_age = H3_ALT_SVC_MAX_AGE_DEFAULT; @@ -681,34 +770,25 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool { vc->h3_idle_timeout = H3_IDLE_TIMEOUT_DEFAULT; } + /* httpd prepends virtual hosts, so the last match is the first one defined: + * it owns the listener, as it is mod_ssl's default; the rest still advertise it. */ conf = vc; - break; + owner = vs; } } - CHECK(conf && conf->h3_cert_path && conf->h3_key_path, return HTTP_INTERNAL_SERVER_ERROR;); - - /* Validate cert and key files are readable */ - apr_file_t* f = NULL; - if (apr_file_open(&f, conf->h3_cert_path, APR_READ, APR_OS_DEFAULT, ptemp) != APR_SUCCESS) + if (!conf) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: H3CertificatePath not readable: %s", conf->h3_cert_path); + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: no host serves HTTP/3: add h3 to Protocols on a host with SSLEngine on"); return HTTP_INTERNAL_SERVER_ERROR; } - apr_file_close(f); - f = NULL; + ap_log_error(APLOG_MARK, APLOG_INFO, 0, owner, "h3_post_config: pid=%d h3_port=%d mpm=%s threaded=%d forked=%d max_threads=%d", h3_getpid(), (int)conf->h3_port, ap_show_mpm(), mpm_query(AP_MPMQ_IS_THREADED), mpm_query(AP_MPMQ_IS_FORKED), mpm_query(AP_MPMQ_MAX_THREADS)); - if (apr_file_open(&f, conf->h3_key_path, APR_READ, APR_OS_DEFAULT, ptemp) != APR_SUCCESS) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: H3CertificateKeyPath not readable: %s", conf->h3_key_path); - return HTTP_INTERNAL_SERVER_ERROR; - } - apr_file_close(f); + /* The listener serves conf's certificate by default and swaps by SNI. */ + SSL_CTX_set_cert_cb(conf->ssl_ctx, h3_sni_select_cert, sni); h3_request_init(); - - ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "h3_post_config: pid=%d cert=%s key=%s h3_port=%d mpm=%s threaded=%d forked=%d max_threads=%d", h3_getpid(), conf->h3_cert_path, conf->h3_key_path, (int)conf->h3_port, ap_show_mpm(), mpm_query(AP_MPMQ_IS_THREADED), mpm_query(AP_MPMQ_IS_FORKED), - mpm_query(AP_MPMQ_MAX_THREADS)); + h3_server_post_config(s); return OK; } @@ -725,8 +805,6 @@ void* h3_merge_dir_config(apr_pool_t* p H3_UNUSED, void* base, void* add H3_UNUS } const command_rec h3_cmds[] = { - AP_INIT_TAKE1("H3CertificatePath", set_h3_cert_path, NULL, RSRC_CONF, "Path to the SSL certificate file for HTTP/3"), - AP_INIT_TAKE1("H3CertificateKeyPath", set_h3_key_path, NULL, RSRC_CONF, "Path to the SSL certificate key file for HTTP/3"), AP_INIT_TAKE1("H3Port", set_h3_port, NULL, RSRC_CONF, "UDP port to listen on for QUIC/HTTP-3 (default: same as main server)"), AP_INIT_TAKE1("H3MaxConcurrentStreams", set_h3_max_concurrent_streams, NULL, RSRC_CONF, "Maximum number of concurrent HTTP/3 streams per connection (default: 100)"), AP_INIT_TAKE1("H3MaxConnections", set_h3_max_connections, NULL, RSRC_CONF, "Maximum concurrent QUIC/HTTP/3 connections per child process (default: 256)"), @@ -739,7 +817,9 @@ const command_rec h3_cmds[] = { AP_INIT_TAKE1("H3MaxResponseBodySize", set_h3_max_response_body_size, NULL, RSRC_CONF, "Maximum HTTP/3 response body size in bytes; an explicit limit enables bounded whole-response buffering (default: unlimited streaming)"), AP_INIT_FLAG("H3AddressValidation", set_h3_address_validation, NULL, RSRC_CONF, "Whether to validate client addresses with a QUIC Retry packet before accepting a connection (default: on)"), AP_INIT_TAKE1("H3SocketBufferSize", set_h3_socket_buffer_size, NULL, RSRC_CONF, "Bytes requested for the QUIC socket send and receive buffers; the OS may grant less (default: 2097152)"), + AP_INIT_TAKE1("H3MaxWindow", set_h3_max_window, NULL, RSRC_CONF, "Largest flow-control window, per connection and per stream, that autotuning may grow a client upload window to (default: 6291456)"), AP_INIT_FLAG("H3SessionTickets", set_h3_session_tickets, NULL, RSRC_CONF, "Whether to issue TLS session tickets so returning clients can resume instead of running a full handshake (default: on)"), + AP_INIT_FLAG("H3EarlyData", set_h3_early_data, NULL, RSRC_CONF, "Whether to accept 0-RTT request data on resumed connections; unsafe methods wait for the handshake (default: off)"), AP_INIT_TAKE1("H3StreamTimeout", set_h3_stream_timeout, NULL, RSRC_CONF, "Seconds a response may make no progress before the stream is aborted (default: the server Timeout)"), AP_INIT_TAKE1("H3MaxStreamErrors", set_h3_max_stream_errors, NULL, RSRC_CONF, "Stream errors one connection may cause before it is closed with H3_EXCESSIVE_LOAD (default: 8)"), AP_INIT_TAKE1("H3QpackTableCapacity", set_h3_qpack_table_capacity, NULL, RSRC_CONF, "Bytes of QPACK dynamic table a client may use for request headers; 0 disables it (default: 4096)"), diff --git a/modules/http3/h3_hooks.c b/modules/http3/h3_hooks.c index 87d99c977c0..709bd505ed4 100644 --- a/modules/http3/h3_hooks.c +++ b/modules/http3/h3_hooks.c @@ -82,7 +82,7 @@ int h3_hook_fixups(request_rec* r) h3_server_conf* conf = ap_get_module_config(r->server->module_config, &http3_module); - if (!conf || !conf->h3_cert_path || !conf->h3_key_path || conf->h3_port == 0) + if (!conf || !conf->ssl_ctx || conf->h3_port == 0) { return DECLINED; } diff --git a/modules/http3/h3_io.c b/modules/http3/h3_io.c index 22f618b98ec..e43c108f0bd 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include @@ -35,6 +37,7 @@ #include "h3_io.h" #include "h3_os.h" #include "h3_request.h" +#include "h3_server.h" #include "h3_session.h" #include "h3_socket.h" #include "h3_stream.h" @@ -131,10 +134,16 @@ apr_status_t h3_io_listen_start(apr_pool_t* pchild, server_rec* s, h3_server_con char qerr[H3Q_ERRLEN] = {0}; h3q_config qcfg = { - .cert_path = conf->h3_cert_path, - .key_path = conf->h3_key_path, + .ssl_ctx = conf->ssl_ctx, + .pool = pchild, + .stream_acked = h3_stream_acked, + .idle_timeout_secs = conf->h3_idle_timeout, + .max_streams_bidi = conf->h3_max_concurrent_streams, + .max_window = conf->h3_max_window, + .secret = h3_server_secret(s), + .early_data = (conf->h3_early_data == H3_FLAG_ON), .address_validation = (conf->h3_address_validation != H3_FLAG_OFF), - .session_tickets = (conf->h3_session_tickets != H3_FLAG_OFF), + .disable_migration = h3_server_buckets(s) > 1, }; io->qengine = h3q_engine_create(&qcfg, udp_fd, qerr, sizeof(qerr)); if (!io->qengine) @@ -268,7 +277,17 @@ static apr_status_t spawn_serviced_session(h3_io_t* io, h3q_conn* conn) return APR_EGENERAL; } apr_allocator_owner_set(allocator, session_pool); + apr_allocator_max_free_set(allocator, ap_max_mem_free); /* MaxMemFree: an idle connection gives back its peak */ apr_pool_tag(session_pool, "h3_session"); + /* Workers allocate from stream subpools while this thread makes its own: share the allocator safely. */ + apr_thread_mutex_t* alloc_mutex = NULL; + if (apr_thread_mutex_create(&alloc_mutex, APR_THREAD_MUTEX_DEFAULT, session_pool) != APR_SUCCESS) + { + apr_pool_destroy(session_pool); + h3q_conn_free(conn); + return APR_EGENERAL; + } + apr_allocator_mutex_set(allocator, alloc_mutex); h3_session* session = NULL; if (h3_session_create(&session, io->server, conn, session_pool) != APR_SUCCESS) @@ -332,14 +351,14 @@ void progress_pending_handshakes(h3_io_t* io) rv = -1; why = "peer closed the connection during the handshake"; } - else if (h3q_conn_is_handshake_done(conn)) + else if (h3q_conn_is_handshake_done(conn) || h3q_conn_has_early_data(conn)) { rv = 1; } if (rv == 1) { - ap_log_error(APLOG_MARK, APLOG_INFO, 0, io->server, "QUIC handshake complete"); + ap_log_error(APLOG_MARK, APLOG_INFO, 0, io->server, h3q_conn_is_handshake_done(conn) ? "QUIC handshake complete" : "QUIC 0-RTT request before the handshake"); spawn_serviced_session(io, conn); remove_pending_handshake(io, i, 0); finished = 1; @@ -363,13 +382,6 @@ void progress_pending_handshakes(h3_io_t* io) int prepare_accepted_connection(h3_io_t* io, h3q_conn* conn) { - h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module); - if (!h3q_conn_prepare(conn, conf->h3_idle_timeout)) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "h3q_conn_prepare failed for accepted connection - dropping it"); - return 0; - } - h3_pending_handshake* pending = (h3_pending_handshake*)apr_array_push(io->pending_handshakes); pending->conn = conn; pending->accepted_at = apr_time_now(); @@ -381,6 +393,13 @@ int prepare_accepted_connection(h3_io_t* io, h3q_conn* conn) return 1; } +/* RFC 9110 section 9.2.1. */ +static int h3_method_is_safe(const char* method) +{ + int m = method ? ap_method_number_of(method) : M_INVALID; + return m == M_GET || m == M_OPTIONS || m == M_TRACE; +} + static void abandon_stalled_responses(h3_session* session, apr_interval_time_t stall_timeout) { if (stall_timeout <= 0) @@ -453,7 +472,9 @@ int service_session_pass(h3_io_t* io, h3_session* session) if (h3q_conn_is_closed(conn)) { - ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "QUIC connection terminated (idle timeout, peer close, or transport error)"); + char detail[H3Q_ERRLEN] = {0}; + h3q_conn_close_reason(conn, detail, sizeof(detail)); + ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "QUIC connection terminated (idle timeout, peer close, or transport error) close=[%s]", detail); session->aborted = 1; return 0; } @@ -501,6 +522,11 @@ int service_session_pass(h3_io_t* io, h3_session* session) for (int i = 0; i < completed->nelts; i++) { h3_stream* h3s = ((h3_stream**)completed->elts)[i]; + /* 0-RTT data can be a replay: unsafe methods wait for the handshake (RFC 8470 section 3). */ + if (h3s->qstream && h3q_stream_is_early(h3s->qstream) && !h3q_conn_is_handshake_done(conn) && !h3_method_is_safe(h3s->method)) + { + continue; + } h3_process_request(session, h3s); } diff --git a/modules/http3/h3_request.c b/modules/http3/h3_request.c index a41d49bf81d..e63a4aaf4fe 100644 --- a/modules/http3/h3_request.c +++ b/modules/http3/h3_request.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -448,6 +449,7 @@ void h3_process_request(h3_session* session, h3_stream* h3s) return; } apr_allocator_owner_set(c_alloc, cpool); + apr_allocator_max_free_set(c_alloc, ap_max_mem_free); conn_rec* c = apr_pcalloc(cpool, sizeof(*c)); *c = *session->c; diff --git a/modules/http3/h3_server.c b/modules/http3/h3_server.c index 9a7ef5fb121..96ef424803f 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -16,9 +16,13 @@ #include +#include +#include #include #include +#include +#include #include #include #include @@ -32,6 +36,14 @@ #include "h3_socket.h" #include "mod_http3.h" +#ifdef __linux__ + #include + #include + #include + #include + #include +#endif + static volatile int child_stopping = 0; static apr_thread_t* port_acquire_thread = NULL; @@ -43,13 +55,98 @@ struct port_acquire_args h3_server_conf* conf; }; +#ifdef __linux__ +/* The parent binds the QUIC sockets while still privileged and keeps them across restarts. + * One SO_REUSEPORT socket per httpd listener bucket (ListenCoresBucketsRatio), so the kernel hash stays stable when children come and go. */ +typedef struct +{ + apr_pool_t* pool; + int* fds; + int n; + apr_port_t port; +} h3_parent_socket; + +/* The same count as httpd for ListenCoresBucketsRatio: online cores / ratio. Some MPMs count their buckets after post_config. + * ponytail: top-level directive only, not one inside . */ +static int listen_buckets(void) +{ + for (ap_directive_t* d = ap_conftree; d; d = d->next) + { + if (ap_have_so_reuseport > 0 && !ap_cstr_casecmp(d->directive, "ListenCoresBucketsRatio")) + { + long ratio = atol(d->args); + long n = ratio > 0 ? sysconf(_SC_NPROCESSORS_ONLN) / ratio : 1; + int limit = 1; + ap_mpm_query(AP_MPMQ_HARD_LIMIT_DAEMONS, &limit); /* A bucket with no child loses its peers. */ + return n < 1 ? 1 : n > limit ? limit : (int)n; + } + } + return 1; +} + +static h3_parent_socket* parent_socket(server_rec* s) +{ + void* ps = NULL; + apr_pool_userdata_get(&ps, "mod_http3.parent_socket", s->process->pool); + return ps; +} + +/* The children inherit them; the child that binds the abstract name of a bucket owns its socket, and exit releases it. */ +static int owner_lock = -1; + +static int own_port(apr_port_t port, int bucket) +{ + struct sockaddr_un a = {.sun_family = AF_UNIX}; + int n = snprintf(a.sun_path + 1, sizeof(a.sun_path) - 1, "mod_http3.%d.%d", (int)port, bucket); + if (owner_lock < 0) + { + owner_lock = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); + } + return owner_lock >= 0 && bind(owner_lock, (struct sockaddr*)&a, (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + (size_t)n)) == 0; +} +#endif + +/* Get this child the QUIC socket, or APR_EAGAIN while another child has it. */ +static apr_status_t acquire_port(apr_pool_t* pchild, server_rec* s, h3_server_conf* conf, int* fd) +{ +#ifdef __linux__ + h3_parent_socket* ps = parent_socket(s); + if (ps && ps->n && ps->port == conf->h3_port) + { + for (int i = 0; i < ps->n; i++) + { + if (own_port(ps->port, i)) + { + *fd = ps->fds[i]; + return APR_SUCCESS; + } + } + return APR_EAGAIN; + } +#endif + return h3_socket_open(conf->h3_port, conf->h3_socket_buffer_size, pchild, 0, fd); +} + +/* Give the port back when this child cannot serve it after all. */ +static void release_port(int fd) +{ + h3_socket_close(fd); +#ifdef __linux__ + if (owner_lock >= 0) + { + close(owner_lock); + owner_lock = -1; + } +#endif +} + static void* APR_THREAD_FUNC port_acquire_thread_fn(apr_thread_t* thread H3_UNUSED, void* data) { struct port_acquire_args* args = data; while (!child_stopping) { int udp_fd = -1; - apr_status_t rv = h3_socket_open(args->conf->h3_port, args->conf->h3_socket_buffer_size, args->pchild, &udp_fd); + apr_status_t rv = acquire_port(args->pchild, args->vhost, args->conf, &udp_fd); if (rv == APR_EAGAIN) { apr_sleep(apr_time_from_msec(H3_PORT_ACQUIRE_RETRY_MS)); @@ -62,13 +159,13 @@ static void* APR_THREAD_FUNC port_acquire_thread_fn(apr_thread_t* thread H3_UNUS } if (child_stopping) { - h3_socket_close(udp_fd); + release_port(udp_fd); break; } if (h3_io_listen_start(args->pchild, args->vhost, args->conf, udp_fd) != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, args->vhost, "port acquirer: h3_io_listen_start failed"); - h3_socket_close(udp_fd); + release_port(udp_fd); break; } ap_log_error(APLOG_MARK, APLOG_INFO, 0, args->vhost, "port acquirer: pid=%d acquired QUIC port after retrying", h3_getpid()); @@ -84,15 +181,18 @@ static h3_server_conf* find_h3_server(server_rec* s, server_rec** out_server) while (current) { h3_server_conf* tmp = ap_get_module_config(current->module_config, &http3_module); - if (tmp && tmp->h3_cert_path && tmp->h3_key_path && !conf) + /* httpd prepends virtual hosts: the last match is the first one defined, mod_ssl's default. */ + if (tmp && tmp->ssl_ctx) { conf = tmp; - conf->host_port = get_server_port(current); *out_server = current; - break; } current = current->next; } + if (conf) + { + conf->host_port = get_server_port(*out_server); + } return conf; } @@ -107,7 +207,7 @@ void h3_child_init(apr_pool_t* pchild, server_rec* s) } int udp_fd = -1; - apr_status_t rv = h3_socket_open(conf->h3_port, conf->h3_socket_buffer_size, pchild, &udp_fd); + apr_status_t rv = acquire_port(pchild, vhost, conf, &udp_fd); if (rv == APR_EAGAIN) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, vhost, "h3_child_init: pid=%d port already owned, will keep retrying in background", h3_getpid()); @@ -131,8 +231,86 @@ void h3_child_init(apr_pool_t* pchild, server_rec* s) if (h3_io_listen_start(pchild, vhost, conf, udp_fd) != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, vhost, "h3_child_init: h3_io_listen_start failed"); - h3_socket_close(udp_fd); + release_port(udp_fd); + } +} + +#define H3_SECRET_KEY "mod_http3.secret" +#define H3_SECRET_LEN 32 + +const unsigned char* h3_server_secret(server_rec* s) +{ + void* secret = NULL; + apr_pool_userdata_get(&secret, H3_SECRET_KEY, s->process->pool); + return secret; +} + +int h3_server_buckets(server_rec* s) +{ +#ifdef __linux__ + h3_parent_socket* ps = parent_socket(s); + return ps && ps->n > 1 ? ps->n : 1; +#else + (void)s; + return 1; +#endif +} + +void h3_server_post_config(server_rec* s) +{ + if (!h3_server_secret(s)) + { + unsigned char* secret = apr_palloc(s->process->pool, H3_SECRET_LEN); + if (apr_generate_random_bytes(secret, H3_SECRET_LEN) == APR_SUCCESS) + { + apr_pool_userdata_set(secret, H3_SECRET_KEY, apr_pool_cleanup_null, s->process->pool); + } + } +#ifdef __linux__ + h3_parent_socket* ps = parent_socket(s); + if (!ps) + { + ps = apr_pcalloc(s->process->pool, sizeof(*ps)); + apr_pool_userdata_set(ps, "mod_http3.parent_socket", apr_pool_cleanup_null, s->process->pool); + } + server_rec* vhost = NULL; + h3_server_conf* conf = find_h3_server(s, &vhost); + int n = listen_buckets(); + /* ponytail: a new port or bucket count binds again. An old single socket without SO_REUSEPORT blocks this until its children stop. */ + if (ps->n && (!conf || conf->h3_port != ps->port || n != ps->n)) + { + apr_pool_destroy(ps->pool); /* closes the old sockets */ + ps->pool = NULL; + ps->n = 0; + } + if (!conf || ps->n) + { + return; + } + int ok = apr_pool_create(&ps->pool, s->process->pool) == APR_SUCCESS; + if (ok) + { + ps->fds = apr_palloc(ps->pool, sizeof(int) * (size_t)n); + } + for (int i = 0; ok && i < n; i++) + { + ok = h3_socket_open(conf->h3_port, conf->h3_socket_buffer_size, ps->pool, n > 1, &ps->fds[i]) == APR_SUCCESS; } + if (!ok) + { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, vhost, "mod_http3: binding UDP port %d before the privilege drop failed; the children bind it themselves", (int)conf->h3_port); + if (ps->pool) + { + apr_pool_destroy(ps->pool); + } + ps->pool = NULL; + return; + } + ps->n = n; + ps->port = conf->h3_port; +#else + (void)s; +#endif } void h3_c1_child_stopping(apr_pool_t* p H3_UNUSED, int graceful) diff --git a/modules/http3/h3_session.c b/modules/http3/h3_session.c index bf16172ef6c..3420d213392 100644 --- a/modules/http3/h3_session.c +++ b/modules/http3/h3_session.c @@ -66,6 +66,7 @@ apr_status_t h3_session_create(h3_session** psession, server_rec* s, h3q_conn* q session->s = s; session->pool = pool; session->qconn = qconn; + h3q_conn_set_user(qconn, session); session->streams = apr_hash_make(pool); session->pending_free = apr_array_make(pool, 8, sizeof(h3q_stream*)); session->last_activity = apr_time_now(); diff --git a/modules/http3/h3_socket.c b/modules/http3/h3_socket.c index 0a8b2ddae68..b894d8b0944 100644 --- a/modules/http3/h3_socket.c +++ b/modules/http3/h3_socket.c @@ -55,7 +55,7 @@ static void tune_buffers(apr_socket_t* sock, apr_size_t want, apr_port_t port, a } } -apr_status_t h3_socket_open(apr_port_t port, apr_size_t buffer_size, apr_pool_t* pool, int* out_fd) +apr_status_t h3_socket_open(apr_port_t port, apr_size_t buffer_size, apr_pool_t* pool, int reuse_port, int* out_fd) { CHECK(pool); CHECK(out_fd); @@ -68,6 +68,16 @@ apr_status_t h3_socket_open(apr_port_t port, apr_size_t buffer_size, apr_pool_t* } apr_socket_opt_set(sock, APR_IPV6_V6ONLY, 0); tune_buffers(sock, buffer_size, port, pool); +#ifdef SO_REUSEPORT + apr_os_sock_t reuse_sock = -1; + int on = 1; + if (reuse_port && apr_os_sock_get(&reuse_sock, sock) == APR_SUCCESS) + { + (void)setsockopt(reuse_sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&on, sizeof(on)); + } +#else + (void)reuse_port; +#endif apr_sockaddr_t* addr = NULL; rv = apr_sockaddr_info_get(&addr, NULL, APR_INET6, port, 0, pool); if (rv != APR_SUCCESS) diff --git a/modules/http3/h3_stream.c b/modules/http3/h3_stream.c index b078ad7bae9..2b69d854133 100644 --- a/modules/http3/h3_stream.c +++ b/modules/http3/h3_stream.c @@ -123,16 +123,16 @@ void flush_nghttp3(h3_session* session) continue; } nghttp3_conn_add_write_offset(session->ngh3, sid, res.accepted); - nghttp3_conn_add_ack_offset(session->ngh3, sid, res.accepted); if (res.blocked) { - /* Send buffer full: skip this stream instead of busy-looping on the same vec. */ + /* Send buffer full: skip this stream instead of busy-looping on the same vec. + * Block it again every time: resume_stream reschedules a blocked stream. */ if (!h3s->write_blocked) { h3s->write_blocked = 1; session->blocked_streams++; - nghttp3_conn_block_stream(session->ngh3, sid); } + nghttp3_conn_block_stream(session->ngh3, sid); continue; } } @@ -451,3 +451,14 @@ apr_array_header_t* drain_ready_streams(h3_session* session, apr_pool_t* loop_po return completed; } + +void h3_stream_acked(void* user, int64_t stream_id, size_t len) +{ + h3_session* session = user; + apr_thread_mutex_lock(session->lock); + if (!session->ngh3_dead) + { + (void)nghttp3_conn_add_ack_offset(session->ngh3, stream_id, len); + } + apr_thread_mutex_unlock(session->lock); +} diff --git a/modules/http3/h3_threads.c b/modules/http3/h3_threads.c index 7d186e0fa39..6c992e3c747 100644 --- a/modules/http3/h3_threads.c +++ b/modules/http3/h3_threads.c @@ -96,6 +96,10 @@ void* APR_THREAD_FUNC h3_event_thread(apr_thread_t* thread, void* data) h3q_conn_free(conn); } } + } + if (io->thread_running) + { + /* Also while draining: a pending handshake counts as an MPM connection, so it must finish or time out. */ progress_pending_handshakes(io); } diff --git a/modules/http3/include/h3.h b/modules/http3/include/h3.h index 638fa9a0d2b..5650a872219 100644 --- a/modules/http3/include/h3.h +++ b/modules/http3/include/h3.h @@ -55,6 +55,10 @@ /* Asked of SO_RCVBUF/SO_SNDBUF on the QUIC socket; the OS may grant less. */ #define H3_SOCKET_BUFFER_SIZE_DEFAULT (2 * 1024 * 1024) #define H3_SOCKET_BUFFER_SIZE_MAX (64UL * 1024 * 1024) +/* Cap for flow-control window autotuning. The window starts at 1 MiB per connection. */ +#define H3_MAX_WINDOW_DEFAULT (6 * 1024 * 1024) +#define H3_MAX_WINDOW_MIN (1024UL * 1024) +#define H3_MAX_WINDOW_MAX (1024UL * 1024 * 1024) /* Unset leaves the field 0, which reads as the server's Timeout; the directive itself takes 1 or more. */ #define H3_STREAM_TIMEOUT_MAX 86400 diff --git a/modules/http3/include/h3_config.h b/modules/http3/include/h3_config.h index b22f5b3ba0f..ddbeea0e009 100644 --- a/modules/http3/include/h3_config.h +++ b/modules/http3/include/h3_config.h @@ -36,8 +36,9 @@ struct h3_server_conf { apr_port_t host_port; - const char* h3_cert_path; - const char* h3_key_path; + /** QUIC TLS context, built from mod_ssl's certificate when the host lists h3 in Protocols. */ + struct ssl_ctx_st* ssl_ctx; + apr_port_t h3_port; apr_uint32_t h3_max_concurrent_streams; apr_uint32_t h3_max_connections; @@ -50,7 +51,9 @@ struct h3_server_conf apr_uint32_t h3_handshake_timeout; apr_uint32_t h3_idle_timeout; apr_size_t h3_socket_buffer_size; + apr_size_t h3_max_window; h3_tri_flag h3_session_tickets; + h3_tri_flag h3_early_data; apr_uint32_t h3_stream_timeout; apr_uint32_t h3_max_stream_errors; int h3_qpack_capacity_set; @@ -81,8 +84,7 @@ void* h3_create_server_config(apr_pool_t* p, server_rec* s); /** * ap_merge_server_config callback: produce a child vhost config that - * inherits each unset field from the parent. cert/key/h3_port use the - * new value if non-NULL/non-zero, else the base. + * inherits each unset field from the parent. * @param p Pool for the merged config. * @param base_conf Parent h3_server_conf. * @param new_conf Child h3_server_conf. @@ -111,16 +113,27 @@ void* h3_create_dir_config(apr_pool_t* p, char* dir); void* h3_merge_dir_config(apr_pool_t* p, void* base, void* add); /** - * ap_post_config hook: resolve cert/key/h3_port for the listening vhost - * and log the resolved values. No-op in AP_SQ_MS_CREATE_PRE_CONFIG - * (pre-config phase). Returns OK if a fully-configured vhost is found, - * HTTP_INTERNAL_SERVER_ERROR otherwise. - * @param p Config pool (unused). + * ap_ssl_add_cert_files hook: mod_ssl runs it for every SSLEngine vhost with + * the certificate and key files it is about to load (SSLCertificateFile plus + * anything mod_md added). When the vhost lists h3 in Protocols, builds its + * QUIC TLS context from them, here, before the server drops privileges. + * @param s The vhost being configured. + * @param p Config pool; owns the context. + * @param cert_files Certificate chain files, const char* elements. + * @param key_files Private key files, const char* elements. + * @return DECLINED, or HTTP_INTERNAL_SERVER_ERROR if the files do not load. + */ +int h3_ssl_add_cert_files(server_rec* s, apr_pool_t* p, apr_array_header_t* cert_files, apr_array_header_t* key_files); + +/** + * ap_post_config hook: fill in defaults on every vhost that serves HTTP/3; + * the first one owns the listener, and an SNI callback swaps in each other + * host's certificate by name. No-op in AP_SQ_MS_CREATE_PRE_CONFIG. + * @param p Config pool; owns the SNI host table. * @param plog Log pool (unused). * @param ptemp Temp pool (unused). * @param s The first server_rec in the configuration. - * @return OK, or HTTP_INTERNAL_SERVER_ERROR if no vhost has both - * H3CertificatePath and H3CertificateKeyPath set. + * @return OK, or HTTP_INTERNAL_SERVER_ERROR if no vhost serves HTTP/3. */ int h3_post_config(apr_pool_t* p, apr_pool_t* plog, apr_pool_t* ptemp, server_rec* s); diff --git a/modules/http3/include/h3_server.h b/modules/http3/include/h3_server.h index b18172e8066..7dfab19f641 100644 --- a/modules/http3/include/h3_server.h +++ b/modules/http3/include/h3_server.h @@ -33,6 +33,25 @@ */ void h3_child_init(apr_pool_t* pchild, server_rec* s); +/** + * Bind the QUIC socket in the parent while it is still privileged (Linux), and + * keep it across graceful restarts for the children to inherit. + * @param s Main server. + */ +void h3_server_post_config(server_rec* s); + +/** + * The QUIC token key. The parent makes it once and keeps it across restarts, + * so a token from one child is good in every other child. + * @return 32 bytes, or NULL if no key could be made. + */ +const unsigned char* h3_server_secret(server_rec* s); + +/** + * @return The number of QUIC sockets the parent holds (Linux), at least 1. + */ +int h3_server_buckets(server_rec* s); + /** * Child-stopping hook: stop retrying for the QUIC port, then either drain the * live connections (graceful) or tear the listener down at once (immediate). diff --git a/modules/http3/include/h3_socket.h b/modules/http3/include/h3_socket.h index 50a9ad6630c..91728ce823a 100644 --- a/modules/http3/include/h3_socket.h +++ b/modules/http3/include/h3_socket.h @@ -24,20 +24,21 @@ /** * Open a non-blocking IPv6 dual-stack UDP socket and bind it to the given port. - * Sets SO_REUSEADDR; on non-Windows also tries SO_REUSEPORT so multiple - * children can share the port. + * With @p reuse_port, sets SO_REUSEPORT (where the OS has it), so several + * sockets can share the port and the kernel spreads the peers over them. * Also asks for @p buffer_size on the send and receive buffers, which the OS * may cap; a refused or capped buffer is logged, never fatal. * @param port Port to bind; 0 lets the OS pick. * @param buffer_size Bytes requested for SO_RCVBUF and SO_SNDBUF; 0 keeps the * OS default. * @param pool Pool used for the underlying apr_socket_t lifetime. + * @param reuse_port Nonzero to set SO_REUSEPORT. * @param out_fd Out parameter: the resulting OS-level fd (suitable for * SSL_set_fd on OpenSSL QUIC). * @return APR_SUCCESS, APR_EAGAIN if the port is already bound, or another * APR error code. */ -apr_status_t h3_socket_open(apr_port_t port, apr_size_t buffer_size, apr_pool_t* pool, int* out_fd); +apr_status_t h3_socket_open(apr_port_t port, apr_size_t buffer_size, apr_pool_t* pool, int reuse_port, int* out_fd); /** * Close a UDP socket previously returned by h3_socket_open. diff --git a/modules/http3/include/h3_stream.h b/modules/http3/include/h3_stream.h index f5bc7fe0191..184a2c5f15a 100644 --- a/modules/http3/include/h3_stream.h +++ b/modules/http3/include/h3_stream.h @@ -62,4 +62,7 @@ apr_array_header_t* drain_ready_streams(h3_session* session, apr_pool_t* loop_po */ h3_stream* h3_stream_find(h3_session* session, int64_t sid); +/** h3q stream_acked callback: hand acknowledged bytes to nghttp3. */ +void h3_stream_acked(void* user, int64_t stream_id, size_t len); + #endif /* H3_STREAM_H */ diff --git a/modules/http3/include/quic/detail/h3q_addr.h b/modules/http3/include/quic/detail/h3q_addr.h deleted file mode 100644 index 33d5b2585c2..00000000000 --- a/modules/http3/include/quic/detail/h3q_addr.h +++ /dev/null @@ -1,112 +0,0 @@ -/* 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. - */ - -#ifndef H3Q_DETAIL_ADDR_H -#define H3Q_DETAIL_ADDR_H - -#include -#include - -#include -#include - -#include "quic/h3q.h" - -typedef struct h3q_datagram h3q_datagram; - -/** The listener's state. All but the two SSL handles exist to recover a - * connection's peer address, which OpenSSL 3.5 does not otherwise expose; - * that is why the definition lives beside the machinery that fills it. */ -struct h3q_engine -{ - SSL_CTX* ssl_ctx; - SSL* ssl_listener; - - BIO_METHOD* peer_addr_bio_method; - BIO_ADDR* current_peer_addr; - int peer_addr_ex_index; - h3q_datagram* peer_rx_head; - h3q_datagram* peer_rx_tail; -}; - -/** - * Drop every datagram still queued on the engine. - * @param engine Engine whose receive queue is emptied. - */ -void h3q_peer_addr_queue_clear(h3q_engine* engine); - -/** - * BIO_meth_set_ctrl handler for the peer-address filter BIO. - * @param bio Filter BIO receiving the control operation. - * @param cmd Control command, forwarded to the underlying BIO. - * @param num Command-specific numeric argument. - * @param ptr Command-specific pointer argument. - * @return Whatever the underlying BIO returns for @p cmd. - */ -long h3q_peer_addr_bio_ctrl(BIO* bio, int cmd, long num, void* ptr); - -/** - * BIO_meth_set_sendmmsg handler, forwarding to the underlying BIO. - * @param bio Filter BIO the datagrams are written through. - * @param msg Array of messages to send. - * @param stride Size of one entry in @p msg. - * @param num_msg Number of entries in @p msg. - * @param flags Flags passed through to the underlying BIO. - * @param msgs_processed Out: how many messages were sent. - * @return 1 on success, 0 on failure. - */ -int h3q_peer_addr_bio_sendmmsg(BIO* bio, BIO_MSG* msg, size_t stride, size_t num_msg, uint64_t flags, size_t* msgs_processed); - -/** - * BIO_meth_set_recvmmsg handler, recording each datagram's peer address. - * @param bio Filter BIO the datagrams are read through. - * @param msg Array receiving the messages. - * @param stride Size of one entry in @p msg. - * @param num_msg Capacity of @p msg. - * @param flags Flags passed through to the underlying BIO. - * @param msgs_processed Out: how many messages were received. - * @return 1 on success, 0 on failure. - */ -int h3q_peer_addr_bio_recvmmsg(BIO* bio, BIO_MSG* msg, size_t stride, size_t num_msg, uint64_t flags, size_t* msgs_processed); - -/** - * BIO_meth_set_destroy handler, clearing the datagram queue. - * @param bio Filter BIO being destroyed. - * @return 1 on success. - */ -int h3q_peer_addr_bio_destroy(BIO* bio); - -/** - * SSL ex_data free callback for a connection's stored peer address. - * @param parent Object the ex_data belongs to. - * @param ptr The stored BIO_ADDR, freed here. - * @param ad ex_data store being torn down. - * @param idx Index the value was stored at. - * @param argl Long argument registered with the index. - * @param argp Pointer argument registered with the index. - */ -void h3q_peer_addr_ex_free(void* parent, void* ptr, CRYPTO_EX_DATA* ad, int idx, long argl, void* argp); - -/** - * SSL_CTX new-pending-conn callback, attaching the peer address to @p conn. - * @param ctx Context the connection was created on. - * @param conn Newly pending connection. - * @param arg The owning h3q_engine. - * @return 1 to accept the connection, 0 to reject it. - */ -int h3q_new_pending_conn_cb(SSL_CTX* ctx, SSL* conn, void* arg); - -#endif /* H3Q_DETAIL_ADDR_H */ diff --git a/modules/http3/include/quic/detail/h3q_impl.h b/modules/http3/include/quic/detail/h3q_impl.h new file mode 100644 index 00000000000..28538bc5e06 --- /dev/null +++ b/modules/http3/include/quic/detail/h3q_impl.h @@ -0,0 +1,127 @@ +/* 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. + */ + +#ifndef H3Q_DETAIL_IMPL_H +#define H3Q_DETAIL_IMPL_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "quic/h3q.h" + +#define H3Q_SCIDLEN 18 +#define H3Q_PKT_BUF 1500 +#define H3Q_RECV_BUDGET 64 +#define H3Q_GSO_SEGS 64 /* kernel UDP_MAX_SEGMENTS */ +#define H3Q_GSO_BYTES 65000 +#define H3Q_RETRY_TOKEN_TIMEOUT (10 * NGTCP2_SECONDS) +#define H3Q_CID_SLOTS 16 /* ngtcp2 keeps at most 8 server CIDs, live and retired, plus 2 client DCIDs */ + +struct h3q_engine +{ + SSL_CTX* ssl_ctx; + int fd; + apr_pool_t* pool; + apr_hash_t* conns; /* CID bytes -> h3q_conn */ + h3q_conn* conns_head; + h3q_conn* accept_head; + h3q_conn* accept_tail; + struct sockaddr_storage local; + socklen_t local_len; + void (*stream_acked)(void* user, int64_t stream_id, size_t len); + uint32_t idle_timeout_secs; + uint32_t max_streams_bidi; + size_t max_window; + uint8_t secret[32]; + /* GSO batch: equal-size packets to one peer, sent with one sendmsg (Linux). */ + uint8_t* tx; + size_t tx_len; + size_t tx_seg; + int tx_n; + struct sockaddr_storage tx_dst; + socklen_t tx_dstlen; + struct sockaddr_storage tx_src; /* the address the peer sent to; ss_family 0: the kernel picks */ + uint32_t tx_ecn; + int sock_ecn; /* ECN codepoint now set on the socket */ + unsigned gso : 1; + unsigned pktinfo : 1; /* wildcard socket: reply from the address each packet came to */ + unsigned address_validation : 1; + unsigned early_data : 1; + unsigned disable_migration : 1; +}; + +struct h3q_conn +{ + h3q_engine* engine; + ngtcp2_conn* qconn; + ngtcp2_crypto_conn_ref conn_ref; + ngtcp2_crypto_ossl_ctx* ossl_ctx; + SSL* ssl; + ngtcp2_path_storage path; + ngtcp2_cid cids[H3Q_CID_SLOTS]; /* keys of the engine map; datalen 0: free slot */ + uint8_t* close_pkt; /* CONNECTION_CLOSE, resent while closing (RFC 9000 10.2.1); malloc */ + size_t close_len; + ngtcp2_tstamp close_until; + ngtcp2_tstamp close_next; + int liberr; /* first local ngtcp2 error, for the log */ + h3q_stream* streams_head; + h3q_stream* accept_head; + h3q_stream* accept_tail; + void* user; + h3q_conn* next; + h3q_conn* next_accept; + unsigned handshake_done : 1; + unsigned closed : 1; + unsigned queued_accept : 1; +}; + +struct h3q_stream +{ + h3q_conn* conn; + int64_t id; + unsigned char* rx; + size_t rx_len; + size_t rx_cap; + size_t rx_off; + h3q_stream* next; + h3q_stream* next_accept; + unsigned fin : 1; + unsigned early : 1; + unsigned read_reset : 1; + unsigned write_closed : 1; + unsigned queued_accept : 1; + unsigned engine_closed : 1; + unsigned fin_pending : 1; /* FIN refused by ngtcp2 once; retried on flush */ +}; + +ngtcp2_tstamp h3q_now(void); +void h3q_send(h3q_engine* engine, const ngtcp2_path* path, uint32_t ecn, const uint8_t* buf, size_t len); +void h3q_tx_flush(h3q_engine* engine); +h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2_cid* odcid, const ngtcp2_cid* retry_scid, const ngtcp2_path* path); +void h3q_conn_flush(h3q_conn* conn); +void h3q_conn_close(h3q_conn* conn, const ngtcp2_ccerr* ccerr); +void h3q_conn_fail(h3q_conn* conn, int liberr); +void h3q_conn_resend_close(h3q_conn* conn, const ngtcp2_path* path); +h3q_stream* h3q_stream_get(h3q_conn* conn, int64_t id); + +#endif /* H3Q_DETAIL_IMPL_H */ diff --git a/modules/http3/include/quic/detail/h3q_tls.h b/modules/http3/include/quic/detail/h3q_tls.h index 0621442995a..956448f257c 100644 --- a/modules/http3/include/quic/detail/h3q_tls.h +++ b/modules/http3/include/quic/detail/h3q_tls.h @@ -24,15 +24,21 @@ #include "quic/h3q.h" /** - * Build the TLS context the listener serves from: certificate and key from - * @p cfg, "h3" as the only ALPN protocol, and a key log when SSLKEYLOGFILE is - * set. - * @param cfg Configuration supplying the certificate and key paths. - * @param err Buffer receiving the reason on failure; may be NULL. - * @param errlen Capacity of @p err. + * Build the TLS context the listener serves from: the certificate chain files + * with their keys (one per key type, as mod_ssl allows; a missing key file + * means the key sits in the chain file), "h3" as the only ALPN protocol, and + * a key log when SSLKEYLOGFILE is set. Safe before the server forks and drops + * privileges; children inherit the loaded keys. + * @param cert_files Certificate chain files; at least one. + * @param ncerts Number of entries in @p cert_files. + * @param key_files Private key files, matched by index to @p cert_files. + * @param nkeys Number of entries in @p key_files; may be fewer. + * @param session_tickets Non-zero to issue TLS 1.3 session tickets. + * @param err Buffer receiving the reason on failure; may be NULL. + * @param errlen Capacity of @p err. * @return New context, or NULL on failure. */ -SSL_CTX* h3q_tls_ctx_create(const h3q_config* cfg, char* err, size_t errlen); +SSL_CTX* h3q_tls_ctx_create(const char* const* cert_files, size_t ncerts, const char* const* key_files, size_t nkeys, int session_tickets, char* err, size_t errlen); /** * Record a message in a caller-supplied error buffer, appending the OpenSSL diff --git a/modules/http3/include/quic/h3q.h b/modules/http3/include/quic/h3q.h index 88c90f0ead3..958690333d8 100644 --- a/modules/http3/include/quic/h3q.h +++ b/modules/http3/include/quic/h3q.h @@ -20,6 +20,8 @@ #include #include +#include + #include "h3_os.h" typedef struct h3q_engine h3q_engine; @@ -28,20 +30,33 @@ typedef struct h3q_stream h3q_stream; #define H3Q_ERRLEN 256 -/** Everything the listener needs to exist. The idle timeout is not here: it is - * a per-connection setting, applied by h3q_conn_prepare(). */ +/** Everything the listener needs to exist. */ typedef struct h3q_config { - const char* cert_path; - const char* key_path; + /** TLS context from h3q_tls_ctx_create(); the engine takes its own reference. */ + struct ssl_ctx_st* ssl_ctx; + /** Parent of the engine's own pool. */ + apr_pool_t* pool; + /** Called when the peer acknowledges @p len bytes of stream @p stream_id. */ + void (*stream_acked)(void* user, int64_t stream_id, size_t len); + /** QUIC max_idle_timeout, applied to every connection. */ + uint32_t idle_timeout_secs; + /** Bidirectional streams a client may have open at once. */ + uint32_t max_streams_bidi; + /** Cap for flow-control window autotuning, per connection and per stream. */ + size_t max_window; + /** 32-byte key for Retry and stateless reset tokens, shared by all children; NULL makes a random one. */ + const unsigned char* secret; unsigned address_validation : 1; - unsigned session_tickets : 1; + /** Accept 0-RTT request data on resumed connections. */ + unsigned early_data : 1; + /** Tell clients not to migrate: with more than one SO_REUSEPORT socket a new address can reach another child. */ + unsigned disable_migration : 1; } h3q_config; /** - * Build the QUIC listener on @p udp_fd, together with the filter BIO that - * recovers peer addresses from OpenSSL's accept queue. - * @param cfg Certificate, key and address validation. + * Build the QUIC listener on @p udp_fd. + * @param cfg TLS context, limits, callbacks and feature flags. * @param udp_fd Pre-opened non-blocking UDP socket bound to the listen port, * borrowed for the engine's lifetime. * @param err Buffer receiving the reason on failure; may be NULL. @@ -51,7 +66,7 @@ typedef struct h3q_config h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size_t errlen); /** - * Tear down the listener, its TLS context and any datagrams still queued. + * Tear down the listener, its TLS context and any connections still open. * @param engine Engine to destroy; NULL is ignored. */ void h3q_engine_destroy(h3q_engine* engine); @@ -60,13 +75,14 @@ void h3q_engine_destroy(h3q_engine* engine); * Drive one round of listener work: read datagrams, run timers, send. * @param engine Engine to pump; NULL reports no work. * @return 1 if work was done and another pass may be useful, 0 if idle, and - * -1 if the listener stopped processing events. + * -1 if the socket stopped delivering datagrams. */ int h3q_engine_pump(h3q_engine* engine); /** * Report what the engine needs from the next event-loop wait. - * @param engine Engine to query; NULL asks for neither, on a one-second wait. + * @param engine Engine to query; NULL keeps the defaults: reads, on a + * one-second wait. * @param want_read Out: non-zero if the socket should be polled for reads. * @param want_write Out: non-zero if the socket should be polled for writes. * @param timeout_ms In/out: lowered to the next timer when one is due sooner, @@ -75,14 +91,14 @@ int h3q_engine_pump(h3q_engine* engine); void h3q_engine_want(h3q_engine* engine, int* want_read, int* want_write, int* timeout_ms); /** - * Take the next handshaken connection off the accept queue. + * Take the next new connection off the accept queue. * @param engine Engine to accept from; NULL yields NULL. * @return Accepted connection, or NULL if none is ready. */ h3q_conn* h3q_engine_accept_conn(h3q_engine* engine); /** - * Recover the peer address the filter BIO recorded for @p conn. + * Copy the peer address from the connection's network path. * @param engine Engine owning @p conn; NULL reports failure. * @param conn Connection to inspect. * @param addr Out: peer socket address. diff --git a/modules/http3/include/quic/h3q_conn.h b/modules/http3/include/quic/h3q_conn.h index 11829fd1de8..9c4e58d686a 100644 --- a/modules/http3/include/quic/h3q_conn.h +++ b/modules/http3/include/quic/h3q_conn.h @@ -41,13 +41,11 @@ typedef struct h3q_tls_info int h3q_conn_tls_info(h3q_conn* conn, h3q_tls_info* out); /** - * Apply the stream modes, incoming-stream policy and idle timeout a freshly - * accepted connection needs before its handshake is driven. - * @param conn Connection to prepare; NULL reports failure. - * @param idle_timeout_secs Idle timeout to apply, in seconds. - * @return 1 on success, 0 on failure. + * Set the pointer the engine hands to the stream_acked callback. + * @param conn Connection to label; NULL is ignored. + * @param user Caller context; acknowledgements are dropped while it is NULL. */ -int h3q_conn_prepare(h3q_conn* conn, uint32_t idle_timeout_secs); +void h3q_conn_set_user(h3q_conn* conn, void* user); /** * Open a server-initiated unidirectional stream. @@ -71,6 +69,13 @@ h3q_stream* h3q_conn_accept_stream(h3q_conn* conn); */ int h3q_conn_is_handshake_done(h3q_conn* conn); +/** + * Whether 0-RTT request data waits while the handshake is still running. + * @param conn Connection to query; NULL reports 0. + * @return Non-zero when an early stream is ready before the handshake. + */ +int h3q_conn_has_early_data(h3q_conn* conn); + /** * Whether the connection has finished closing. * @param conn Connection to query; NULL counts as closed. @@ -96,8 +101,8 @@ void h3q_conn_free(h3q_conn* conn); /** * Describe why a connection closed, for logging. Reports the peer's error - * code, frame type and reason string where OpenSSL has them, and whatever the - * error queue holds otherwise. + * code, frame type and reason string where ngtcp2 has them, and whatever the + * OpenSSL error queue holds otherwise. * @param conn Connection to inspect; NULL reports the error queue alone. * @param buf Buffer receiving the description, always NUL-terminated. * @param buflen Capacity of @p buf; zero is ignored. diff --git a/modules/http3/include/quic/h3q_stream.h b/modules/http3/include/quic/h3q_stream.h index 8c3f6ef6c2f..2c8883f6390 100644 --- a/modules/http3/include/quic/h3q_stream.h +++ b/modules/http3/include/quic/h3q_stream.h @@ -51,8 +51,9 @@ int64_t h3q_stream_id(h3q_stream* st); * @param nvec Number of buffers in @p vec. * @param fin Non-zero to close the stream after these bytes. * @return What was accepted, and whether the write blocked or broke. - * @note Bytes count as acknowledged once accepted: OpenSSL reports no - * per-stream acknowledgements. + * @note ngtcp2 does not copy: accepted bytes must stay in place until the + * stream_acked callback reports them, since loss recovery resends + * from the caller's buffers. */ h3q_write_result h3q_stream_write(h3q_stream* st, const h3q_vec* vec, size_t nvec, int fin); @@ -82,6 +83,13 @@ int h3q_stream_read(h3q_stream* st, unsigned char* buf, size_t read_size, size_t */ void h3q_stream_is_read_finished(h3q_stream* st, int* read_finished, int* write_finished); +/** + * Whether any of the stream's data arrived as 0-RTT, so it may be a replay. + * @param st Stream to query; NULL reports 0. + * @return Non-zero for a stream with 0-RTT data. + */ +int h3q_stream_is_early(h3q_stream* st); + /** * Abort the sending half of a stream. * @param st Stream to reset; NULL is ignored. diff --git a/modules/http3/mod_http3.c b/modules/http3/mod_http3.c index fb8db48ccb7..a5d71f7ed0a 100644 --- a/modules/http3/mod_http3.c +++ b/modules/http3/mod_http3.c @@ -40,7 +40,11 @@ static void register_hooks(apr_pool_t* p H3_UNUSED) { ap_hook_handler(h3_status_handler, NULL, NULL, APR_HOOK_MIDDLE); - ap_hook_post_config(h3_post_config, NULL, NULL, APR_HOOK_MIDDLE); + /* mod_ssl hands out the certificate files it resolved before it loads them; + * post_config runs after it so those files are known by then. */ + static const char* const after_ssl[] = {"mod_ssl.c", NULL}; + ap_hook_ssl_add_cert_files(h3_ssl_add_cert_files, NULL, NULL, APR_HOOK_LAST); + ap_hook_post_config(h3_post_config, after_ssl, NULL, APR_HOOK_MIDDLE); ap_hook_create_request(h3_hook_http_create_request, NULL, NULL, APR_HOOK_REALLY_FIRST); ap_hook_pre_read_request(h3_hook_pre_read_request, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_post_read_request(h3_hook_post_read_request, NULL, NULL, APR_HOOK_REALLY_FIRST); @@ -72,5 +76,5 @@ HTTP3_PUBLIC module http3_module = { h3_merge_server_config, /* merge per-server config structures */ h3_cmds, /* command apr_table_t */ register_hooks, /* register hooks */ - AP_MODULE_FLAG_NONE /* flags */ + AP_MODULE_FLAG_ALWAYS_MERGE /* every vhost gets its own config, H3 directives or not */ }; diff --git a/modules/http3/quic/detail/h3q_addr.c b/modules/http3/quic/detail/h3q_addr.c deleted file mode 100644 index 0018a2cc953..00000000000 --- a/modules/http3/quic/detail/h3q_addr.c +++ /dev/null @@ -1,326 +0,0 @@ -/* 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. - */ - -#ifdef _WIN32 -#include -#include -#endif - -#include -#include - -#include -#include -#include - -#include "h3_os.h" -#include "quic/detail/h3q_addr.h" - -#ifdef H3Q_DCID_DEBUG - -static void h3q_hex(char* out, const unsigned char* data, size_t len) -{ - size_t i; - for (i = 0; i < len; i++) - sprintf(out + i * 2, "%02x", data[i]); - out[len * 2] = '\0'; -} - -static void h3q_trace_quic(const char* direction, const unsigned char* data, size_t data_len) -{ - unsigned char dcid[20]; - unsigned char scid[20]; - size_t dcid_len = 0; - size_t scid_len = 0; - size_t off; - char dcid_hex[41]; - char scid_hex[41]; - FILE* f; - - if (data_len < 1) - return; - - if (data[0] & 0x80) { - /* Long header: [flags(1)][version(4)][dcid_len(1)][dcid...][scid_len(1)][scid...] */ - if (data_len < 6) - return; - dcid_len = data[5]; - if (dcid_len > 20 || data_len < 6 + dcid_len) - return; - memcpy(dcid, data + 6, dcid_len); - - off = 6 + dcid_len; - if (data_len > off) { - scid_len = data[off]; - if (scid_len <= 20 && data_len >= off + 1 + scid_len) - memcpy(scid, data + off + 1, scid_len); - else - scid_len = 0; - } - - h3q_hex(dcid_hex, dcid, dcid_len); - h3q_hex(scid_hex, scid, scid_len); - f = fopen("/tmp/h3q_dcid.log", "a"); - if (f) { - fprintf(f, "[h3q] %s long DCID(%zu): %s SCID(%zu): %s\n", - direction, dcid_len, dcid_hex, scid_len, scid_hex); - fclose(f); - } - } else { - /* Short header: [flags(1)][dcid...] */ - dcid_len = 8; - if (data_len < 1 + dcid_len) - return; - memcpy(dcid, data + 1, dcid_len); - - h3q_hex(dcid_hex, dcid, dcid_len); - f = fopen("/tmp/h3q_dcid.log", "a"); - if (f) { - fprintf(f, "[h3q] %s short DCID(%zu): %s\n", - direction, dcid_len, dcid_hex); - fclose(f); - } - } -} - -#endif /* H3Q_DCID_DEBUG */ - -struct h3q_datagram -{ - unsigned char* data; - size_t data_len; - BIO_ADDR* peer; - BIO_ADDR* local; - h3q_datagram* next; -}; - -static void h3q_datagram_free(h3q_datagram* item) -{ - OPENSSL_free(item->data); - BIO_ADDR_free(item->peer); - BIO_ADDR_free(item->local); - OPENSSL_free(item); -} - -static void h3q_queue_drop_head(h3q_engine* engine) -{ - h3q_datagram* item = engine->peer_rx_head; - engine->peer_rx_head = item->next; - if (!engine->peer_rx_head) - { - engine->peer_rx_tail = NULL; - } - h3q_datagram_free(item); -} - -void h3q_peer_addr_queue_clear(h3q_engine* engine) -{ - h3q_datagram* item = engine->peer_rx_head; - while (item) - { - h3q_datagram* next = item->next; - h3q_datagram_free(item); - item = next; - } - engine->peer_rx_head = NULL; - engine->peer_rx_tail = NULL; -} - -static int h3q_queue_fill(h3q_engine* engine, BIO_MSG* msg, size_t stride, size_t count) -{ - for (size_t i = 0; i < count; i++) - { - BIO_MSG* source = (BIO_MSG*)((unsigned char*)msg + i * stride); - h3q_datagram* item = OPENSSL_zalloc(sizeof(*item)); - if (!item || !source->data || source->data_len == 0) - { - OPENSSL_free(item); - h3q_peer_addr_queue_clear(engine); - return 0; - } -#ifdef H3Q_DCID_DEBUG - h3q_trace_quic("RECV", source->data, source->data_len); -#endif - item->data = OPENSSL_memdup(source->data, source->data_len); - item->data_len = source->data_len; - item->peer = source->peer ? BIO_ADDR_dup(source->peer) : NULL; - item->local = source->local ? BIO_ADDR_dup(source->local) : NULL; - if (!item->data || (source->peer && !item->peer) || (source->local && !item->local)) - { - OPENSSL_free(item->data); - BIO_ADDR_free(item->peer); - BIO_ADDR_free(item->local); - OPENSSL_free(item); - h3q_peer_addr_queue_clear(engine); - return 0; - } - if (engine->peer_rx_tail) - { - engine->peer_rx_tail->next = item; - } - else - { - engine->peer_rx_head = item; - } - engine->peer_rx_tail = item; - } - return 1; -} - -static int h3q_queue_pop(h3q_engine* engine, BIO_MSG* msg) -{ - h3q_datagram* item = engine->peer_rx_head; - if (!item || !msg || !msg->data) - { - return 0; - } - if (msg->data_len < item->data_len) - { - /* Drop it, or the queue head never clears and the pump spins. */ - h3q_queue_drop_head(engine); - return 0; - } - memcpy(msg->data, item->data, item->data_len); - msg->data_len = item->data_len; - if (msg->peer && item->peer) - { - BIO_ADDR_copy(msg->peer, item->peer); - } - if (msg->local && item->local) - { - BIO_ADDR_copy(msg->local, item->local); - } - h3q_queue_drop_head(engine); - return 1; -} - -long h3q_peer_addr_bio_ctrl(BIO* bio, int cmd, long num, void* ptr) -{ - BIO* next = BIO_next(bio); - return next ? BIO_ctrl(next, cmd, num, ptr) : 0; -} - -int h3q_peer_addr_bio_sendmmsg(BIO* bio, BIO_MSG* msg, size_t stride, size_t num_msg, uint64_t flags, size_t* msgs_processed) -{ - BIO* next = BIO_next(bio); - -#ifdef H3Q_DCID_DEBUG - { - size_t i; - for (i = 0; i < num_msg; i++) { - BIO_MSG* m = (BIO_MSG*)((unsigned char*)msg + i * stride); - if (m->data && m->data_len > 0) - h3q_trace_quic("SEND", m->data, m->data_len); - } - } -#endif - - return next ? BIO_sendmmsg(next, msg, stride, num_msg, flags, msgs_processed) : 0; -} - -int h3q_peer_addr_bio_recvmmsg(BIO* bio, BIO_MSG* msg, size_t stride, size_t num_msg, uint64_t flags, size_t* msgs_processed) -{ - h3q_engine* engine = BIO_get_data(bio); - BIO* next = BIO_next(bio); - size_t count; - - if (!engine || !next || !msg || !msgs_processed || num_msg == 0) - { - return 0; - } - - BIO_ADDR_clear(engine->current_peer_addr); - - if (!engine->peer_rx_head) - { - size_t received = 0; - int rv = BIO_recvmmsg(next, msg, stride, num_msg, flags, &received); - if (!rv || received == 0) - { - *msgs_processed = received; - return rv; - } - if (!h3q_queue_fill(engine, msg, stride, received)) - { - *msgs_processed = 0; - return 0; - } - } - - /* Pop from queue: batch short-header packets (existing connections); - * long-header packets go one at a time for peer-address capture. */ - count = 0; - while (count < num_msg && engine->peer_rx_head) - { - h3q_datagram* head = engine->peer_rx_head; - int is_long = (head->data_len > 0 && (head->data[0] & 0x80)); - - if (is_long && count > 0) - { - break; - } - - BIO_MSG* dst = (BIO_MSG*)((unsigned char*)msg + count * stride); - if (!h3q_queue_pop(engine, dst)) - { - break; - } - count++; - - if (is_long) - { - if (dst->peer) - { - BIO_ADDR_copy(engine->current_peer_addr, dst->peer); - } - break; - } - } - *msgs_processed = count; - return count > 0 ? 1 : 0; -} - -int h3q_peer_addr_bio_destroy(BIO* bio) -{ - h3q_engine* engine = BIO_get_data(bio); - if (engine) - { - h3q_peer_addr_queue_clear(engine); - } - return 1; -} - -void h3q_peer_addr_ex_free(void* parent H3_UNUSED, void* ptr, CRYPTO_EX_DATA* ad H3_UNUSED, int idx H3_UNUSED, long argl H3_UNUSED, void* argp H3_UNUSED) -{ - BIO_ADDR_free(ptr); -} - -int h3q_new_pending_conn_cb(SSL_CTX* ctx H3_UNUSED, SSL* conn, void* arg) -{ - h3q_engine* engine = arg; - if (!engine || engine->peer_addr_ex_index < 0 || BIO_ADDR_family(engine->current_peer_addr) == AF_UNSPEC) - { - return 1; - } - - BIO_ADDR* peer = BIO_ADDR_dup(engine->current_peer_addr); - if (!peer || !SSL_set_ex_data(conn, engine->peer_addr_ex_index, peer)) - { - BIO_ADDR_free(peer); - return 0; - } - return 1; -} diff --git a/modules/http3/quic/detail/h3q_tls.c b/modules/http3/quic/detail/h3q_tls.c index 1ed6125b0d2..b4ff7929b76 100644 --- a/modules/http3/quic/detail/h3q_tls.c +++ b/modules/http3/quic/detail/h3q_tls.c @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -77,32 +78,48 @@ static void h3q_tls_keylog_cb(const SSL* ssl, const char* line) } } -SSL_CTX* h3q_tls_ctx_create(const h3q_config* cfg, char* err, size_t errlen) +SSL_CTX* h3q_tls_ctx_create(const char* const* cert_files, size_t ncerts, const char* const* key_files, size_t nkeys, int session_tickets, char* err, size_t errlen) { - CHECK(cfg); + CHECK(cert_files && ncerts > 0); + CHECK(key_files || nkeys == 0); - SSL_CTX* ssl_ctx = SSL_CTX_new(OSSL_QUIC_server_method()); + if (ngtcp2_crypto_ossl_init() != 0) + { + h3q_tls_error(err, errlen, "ngtcp2_crypto_ossl_init failed; OpenSSL lacks the QUIC TLS API"); + return NULL; + } + SSL_CTX* ssl_ctx = SSL_CTX_new(TLS_server_method()); if (!ssl_ctx) { h3q_tls_error(err, errlen, "SSL_CTX_new failed"); return NULL; } - SSL_CTX_set_domain_flags(ssl_ctx, SSL_DOMAIN_FLAG_MULTI_THREAD); SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_3_VERSION); SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION); - if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cfg->cert_path) <= 0 || SSL_CTX_use_PrivateKey_file(ssl_ctx, cfg->key_path, SSL_FILETYPE_PEM) <= 0) + for (size_t i = 0; i < ncerts; i++) { - h3q_tls_error(err, errlen, "loading the certificate or private key failed"); - SSL_CTX_free(ssl_ctx); - return NULL; + const char* key_file = i < nkeys ? key_files[i] : cert_files[i]; + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_files[i]) <= 0 || SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM) <= 0) + { + h3q_tls_error(err, errlen, "loading certificate %s with key %s failed", cert_files[i], key_file); + SSL_CTX_free(ssl_ctx); + return NULL; + } } static const unsigned char sid_ctx[] = "mod_http3"; SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); - if (!cfg->session_tickets) + /* H3EarlyData is per connection: SSL_set_quic_tls_early_data_enabled makes a + * ticket advertise 0-RTT (0xffffffff, RFC 9001 4.6.1), so a ticket issued + * with it off invites none. OpenSSL's anti-replay is built for TLS over TCP + * and refuses QUIC resumption with early data; QUIC 0-RTT replay safety comes + * from accepting only safe methods before the handshake (RFC 8470), in h3_io. */ + SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_ANTI_REPLAY); + + if (!session_tickets) { /* TLS 1.3 resumption travels in tickets, so issuing none turns it off. */ SSL_CTX_set_num_tickets(ssl_ctx, 0); diff --git a/modules/http3/quic/h3q.c b/modules/http3/quic/h3q.c index 10f88b4a842..72e0cf473c6 100644 --- a/modules/http3/quic/h3q.c +++ b/modules/http3/quic/h3q.c @@ -14,249 +14,610 @@ * limitations under the License. */ +#if defined(__linux__) && !defined(_GNU_SOURCE) + #define _GNU_SOURCE /* struct in6_pktinfo */ +#endif + #ifdef _WIN32 #include #include #endif +#include #include #include +#include -#include -#include +#include #include "h3_check.h" #include "h3_os.h" -#include "quic/detail/h3q_addr.h" +#include "quic/detail/h3q_impl.h" #include "quic/detail/h3q_tls.h" #include "quic/h3q.h" +#include "quic/h3q_conn.h" -#define H3Q_PUMP_INNER_BUDGET 64 +#ifdef __linux__ + #include + #include + #include + /* Kernel ABI values, for libc headers older than GSO and GRO; old kernels fall back at run time. */ + #ifndef SOL_UDP + #define SOL_UDP 17 + #endif + #ifndef UDP_SEGMENT + #define UDP_SEGMENT 103 + #endif + #ifndef UDP_GRO + #define UDP_GRO 104 + #endif +#endif -h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size_t errlen) +#ifdef _WIN32 +typedef int h3q_iolen; /* winsock takes int lengths */ +#else +typedef size_t h3q_iolen; +#endif + +/* ngtcp2 aborts if time goes back, so use a monotonic clock, never the wall clock. */ +ngtcp2_tstamp h3q_now(void) { - CHECK(cfg); - h3q_engine* engine = calloc(1, sizeof(*engine)); - if (!engine) +#ifdef _WIN32 + LARGE_INTEGER f, c; + QueryPerformanceFrequency(&f); + QueryPerformanceCounter(&c); + return (ngtcp2_tstamp)(c.QuadPart / f.QuadPart) * NGTCP2_SECONDS + (ngtcp2_tstamp)(c.QuadPart % f.QuadPart) * NGTCP2_SECONDS / (ngtcp2_tstamp)f.QuadPart; +#else + struct timespec tp; + clock_gettime(CLOCK_MONOTONIC, &tp); + return (ngtcp2_tstamp)tp.tv_sec * NGTCP2_SECONDS + (ngtcp2_tstamp)tp.tv_nsec; +#endif +} + +#ifdef __linux__ +static int is_wildcard(const struct sockaddr_storage* a) +{ + if (a->ss_family == AF_INET6) { - h3q_tls_error(err, errlen, "allocating the engine failed"); - return NULL; + return IN6_IS_ADDR_UNSPECIFIED(&((const struct sockaddr_in6*)a)->sin6_addr); } - engine->peer_addr_ex_index = -1; + return a->ss_family == AF_INET && ((const struct sockaddr_in*)a)->sin_addr.s_addr == htonl(INADDR_ANY); +} - engine->ssl_ctx = h3q_tls_ctx_create(cfg, err, errlen); - if (!engine->ssl_ctx) +/* One sendmsg. src picks the source address; seg > 0 asks GSO to cut buf into seg-byte datagrams. + * An IPv4-mapped source takes IP_PKTINFO: such a packet leaves by the IPv4 path. */ +static ssize_t send_msg(h3q_engine* engine, const struct sockaddr* dst, socklen_t dstlen, const struct sockaddr_storage* src, const uint8_t* buf, size_t len, uint16_t seg) +{ + union { - h3q_engine_destroy(engine); - return NULL; + char b[CMSG_SPACE(sizeof(struct in6_pktinfo)) + CMSG_SPACE(sizeof(uint16_t))]; + struct cmsghdr align; + } ctl; + memset(&ctl, 0, sizeof(ctl)); + struct iovec iov = {.iov_base = (void*)buf, .iov_len = len}; + struct msghdr m = {.msg_name = (void*)dst, .msg_namelen = dstlen, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = ctl.b}; + size_t used = 0; + const struct in6_addr* a6 = src && src->ss_family == AF_INET6 ? &((const struct sockaddr_in6*)src)->sin6_addr : NULL; + if ((src && src->ss_family == AF_INET) || (a6 && IN6_IS_ADDR_V4MAPPED(a6))) + { + struct in_pktinfo pi; + memset(&pi, 0, sizeof(pi)); + memcpy(&pi.ipi_spec_dst, a6 ? (const void*)&a6->s6_addr[12] : (const void*)&((const struct sockaddr_in*)src)->sin_addr, 4); + struct cmsghdr* c = (struct cmsghdr*)ctl.b; + c->cmsg_level = IPPROTO_IP; + c->cmsg_type = IP_PKTINFO; + c->cmsg_len = CMSG_LEN(sizeof(pi)); + memcpy(CMSG_DATA(c), &pi, sizeof(pi)); + used = CMSG_SPACE(sizeof(pi)); } - - BIO_METHOD* bm = BIO_meth_new(BIO_TYPE_FILTER | BIO_get_new_index(), "h3q_peer_addr"); - if (!bm) + else if (a6) { - h3q_tls_error(err, errlen, "BIO_meth_new failed"); - h3q_engine_destroy(engine); - return NULL; + struct in6_pktinfo pi; + memset(&pi, 0, sizeof(pi)); + pi.ipi6_addr = *a6; + struct cmsghdr* c = (struct cmsghdr*)ctl.b; + c->cmsg_level = IPPROTO_IPV6; + c->cmsg_type = IPV6_PKTINFO; + c->cmsg_len = CMSG_LEN(sizeof(pi)); + memcpy(CMSG_DATA(c), &pi, sizeof(pi)); + used = CMSG_SPACE(sizeof(pi)); } - engine->peer_addr_bio_method = bm; - /* An unset handler installs fine and then drops every datagram. */ - if (!BIO_meth_set_ctrl(bm, h3q_peer_addr_bio_ctrl) || !BIO_meth_set_sendmmsg(bm, h3q_peer_addr_bio_sendmmsg) || !BIO_meth_set_recvmmsg(bm, h3q_peer_addr_bio_recvmmsg) || !BIO_meth_set_destroy(bm, h3q_peer_addr_bio_destroy)) + if (seg) { - h3q_tls_error(err, errlen, "installing the peer address BIO handlers failed"); - h3q_engine_destroy(engine); - return NULL; + struct cmsghdr* c = (struct cmsghdr*)(ctl.b + used); + c->cmsg_level = SOL_UDP; + c->cmsg_type = UDP_SEGMENT; + c->cmsg_len = CMSG_LEN(sizeof(seg)); + memcpy(CMSG_DATA(c), &seg, sizeof(seg)); + used += CMSG_SPACE(sizeof(seg)); } - - engine->current_peer_addr = BIO_ADDR_new(); - engine->peer_addr_ex_index = SSL_get_ex_new_index(0, NULL, NULL, NULL, h3q_peer_addr_ex_free); - if (!engine->current_peer_addr || engine->peer_addr_ex_index < 0) + m.msg_controllen = used; + if (!used) { - h3q_tls_error(err, errlen, "initializing peer address recovery failed"); - h3q_engine_destroy(engine); - return NULL; + m.msg_control = NULL; } + return sendmsg(engine->fd, &m, 0); +} - SSL_CTX_set_new_pending_conn_cb(engine->ssl_ctx, h3q_new_pending_conn_cb, engine); - - uint64_t listener_flags = cfg->address_validation ? 0 : (uint64_t)SSL_LISTENER_FLAG_NO_VALIDATE; - engine->ssl_listener = SSL_new_listener(engine->ssl_ctx, listener_flags); - if (!engine->ssl_listener) +/* The ECN codepoint is a socket option, so set it only when it changes. IP_TOS covers IPv4-mapped peers. */ +static void set_ecn(h3q_engine* engine, uint32_t ecn) +{ + int tos = (int)ecn; + if (tos == engine->sock_ecn) { - h3q_tls_error(err, errlen, "SSL_new_listener failed"); - h3q_engine_destroy(engine); - return NULL; + return; } - - BIO* bio = BIO_new_dgram(udp_fd, BIO_NOCLOSE); - if (!bio) + engine->sock_ecn = tos; + if (engine->local.ss_family == AF_INET6) { - h3q_tls_error(err, errlen, "BIO_new_dgram failed for fd=%d", udp_fd); - h3q_engine_destroy(engine); - return NULL; + (void)setsockopt(engine->fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); } + (void)setsockopt(engine->fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); +} +#endif - BIO* filter_bio = BIO_new(bm); - if (!filter_bio) +/* A failed send is a lost datagram; loss recovery resends it. */ +static void send_one(h3q_engine* engine, const struct sockaddr* dst, socklen_t dstlen, const struct sockaddr_storage* src, const uint8_t* buf, size_t len) +{ +#ifdef __linux__ + if (send_msg(engine, dst, dstlen, src, buf, len, 0) < 0 && errno == EINVAL && src) { - h3q_tls_error(err, errlen, "BIO_new(h3q_peer_addr) failed"); - BIO_free(bio); - h3q_engine_destroy(engine); - return NULL; + (void)send_msg(engine, dst, dstlen, NULL, buf, len, 0); /* that address is gone: the kernel picks one */ } +#else + (void)src; + (void)sendto(engine->fd, (const char*)buf, (h3q_iolen)len, 0, dst, dstlen); +#endif +} - BIO_set_data(filter_bio, engine); - bio = BIO_push(filter_bio, bio); - SSL_set_bio(engine->ssl_listener, bio, bio); - - if (!SSL_listen(engine->ssl_listener) || !SSL_set_blocking_mode(engine->ssl_listener, 0)) +void h3q_send(h3q_engine* engine, const ngtcp2_path* path, uint32_t ecn, const uint8_t* buf, size_t len) +{ + const struct sockaddr* dst = (const struct sockaddr*)path->remote.addr; + socklen_t dstlen = (socklen_t)path->remote.addrlen; + struct sockaddr_storage src; + memset(&src, 0, sizeof(src)); + if (engine->pktinfo && path->local.addrlen && path->local.addrlen <= sizeof(src)) { - h3q_tls_error(err, errlen, "SSL_listen failed"); - h3q_engine_destroy(engine); - return NULL; + memcpy(&src, path->local.addr, path->local.addrlen); + } + /* A new peer, source or ECN mark, a larger packet or a full batch starts a new batch. */ + if (engine->tx_n && (dstlen != engine->tx_dstlen || memcmp(dst, &engine->tx_dst, (size_t)dstlen) != 0 || memcmp(&src, &engine->tx_src, sizeof(src)) != 0 || ecn != engine->tx_ecn || len > engine->tx_seg || engine->tx_len + len > H3Q_GSO_BYTES || engine->tx_n == H3Q_GSO_SEGS)) + { + h3q_tx_flush(engine); + } + if (!engine->tx_n) + { + memcpy(&engine->tx_dst, dst, (size_t)dstlen); + engine->tx_dstlen = dstlen; + engine->tx_src = src; + engine->tx_ecn = ecn; + engine->tx_seg = len; + } + memcpy(engine->tx + engine->tx_len, buf, len); + engine->tx_len += len; + engine->tx_n++; + if (!engine->gso || len < engine->tx_seg) + { + h3q_tx_flush(engine); /* only the last segment may be shorter */ } - - return engine; } -void h3q_engine_destroy(h3q_engine* engine) +void h3q_tx_flush(h3q_engine* engine) { - if (!engine) + if (!engine->tx_n) { return; } - h3q_peer_addr_queue_clear(engine); - if (engine->ssl_listener) + const struct sockaddr* dst = (const struct sockaddr*)&engine->tx_dst; + const struct sockaddr_storage* src = engine->tx_src.ss_family ? &engine->tx_src : NULL; + int sent = engine->tx_n == 1; +#ifdef __linux__ + set_ecn(engine, engine->tx_ecn); +#endif + if (sent) { - SSL_free(engine->ssl_listener); + send_one(engine, dst, engine->tx_dstlen, src, engine->tx, engine->tx_len); } - if (engine->current_peer_addr) +#ifdef __linux__ + else { - BIO_ADDR_free(engine->current_peer_addr); + /* No GSO in this kernel or device: turn it off and send one by one. */ + sent = send_msg(engine, dst, engine->tx_dstlen, src, engine->tx, engine->tx_len, (uint16_t)engine->tx_seg) >= 0 || (errno != EIO && errno != EINVAL && errno != ENOPROTOOPT); + engine->gso = sent ? 1u : 0u; } - if (engine->peer_addr_bio_method) +#endif + for (size_t off = 0; !sent && off < engine->tx_len; off += engine->tx_seg) { - BIO_meth_free(engine->peer_addr_bio_method); + size_t n = engine->tx_len - off < engine->tx_seg ? engine->tx_len - off : engine->tx_seg; + send_one(engine, dst, engine->tx_dstlen, src, engine->tx + off, n); } - if (engine->ssl_ctx) + engine->tx_n = 0; + engine->tx_len = 0; +} + +/* One read; with GRO the kernel may join datagrams of one peer, and *seg is their size. + * On Linux *local becomes the address the peer sent to, and *ecn its ECN mark. */ +static int recv_dgram(h3q_engine* engine, uint8_t* buf, size_t cap, struct sockaddr_storage* peer, socklen_t* peerlen, struct sockaddr_storage* local, size_t* seg, uint32_t* ecn) +{ + *ecn = NGTCP2_ECN_NOT_ECT; +#ifdef __linux__ + union { - SSL_CTX_free(engine->ssl_ctx); + char b[CMSG_SPACE(sizeof(int)) * 3 + CMSG_SPACE(sizeof(struct in6_pktinfo)) + CMSG_SPACE(sizeof(struct in_pktinfo))]; + struct cmsghdr align; + } ctl; + struct iovec iov = {.iov_base = buf, .iov_len = cap}; + struct msghdr m = {.msg_name = peer, .msg_namelen = *peerlen, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = ctl.b, .msg_controllen = sizeof(ctl.b)}; + int n = (int)recvmsg(engine->fd, &m, 0); + *peerlen = m.msg_namelen; + *seg = n > 0 ? (size_t)n : 0; + for (struct cmsghdr* c = CMSG_FIRSTHDR(&m); n > 0 && c; c = CMSG_NXTHDR(&m, c)) + { + if (c->cmsg_level == SOL_UDP && c->cmsg_type == UDP_GRO) + { + int gro = 0; + memcpy(&gro, CMSG_DATA(c), sizeof(gro)); + *seg = gro > 0 ? (size_t)gro : *seg; + } + else if (c->cmsg_level == IPPROTO_IPV6 && c->cmsg_type == IPV6_PKTINFO && local->ss_family == AF_INET6) + { + struct in6_pktinfo pi; + memcpy(&pi, CMSG_DATA(c), sizeof(pi)); + struct sockaddr_in6* l = (struct sockaddr_in6*)local; + l->sin6_addr = pi.ipi6_addr; + l->sin6_scope_id = IN6_IS_ADDR_LINKLOCAL(&pi.ipi6_addr) ? (uint32_t)pi.ipi6_ifindex : 0; + } + else if (c->cmsg_level == IPPROTO_IP && c->cmsg_type == IP_PKTINFO) + { + struct in_pktinfo pi; + memcpy(&pi, CMSG_DATA(c), sizeof(pi)); + if (local->ss_family == AF_INET6) + { + struct sockaddr_in6* l = (struct sockaddr_in6*)local; + memset(&l->sin6_addr, 0, sizeof(l->sin6_addr)); + l->sin6_addr.s6_addr[10] = 0xff; + l->sin6_addr.s6_addr[11] = 0xff; + memcpy(&l->sin6_addr.s6_addr[12], &pi.ipi_addr, 4); + l->sin6_scope_id = 0; + } + else + { + ((struct sockaddr_in*)local)->sin_addr = pi.ipi_addr; + } + } + else if (c->cmsg_level == IPPROTO_IPV6 && c->cmsg_type == IPV6_TCLASS) + { + int tclass = 0; + memcpy(&tclass, CMSG_DATA(c), sizeof(tclass)); + *ecn = (uint32_t)tclass & 0x3; + } + else if (c->cmsg_level == IPPROTO_IP && c->cmsg_type == IP_TOS) + { + *ecn = (uint32_t)*(const uint8_t*)CMSG_DATA(c) & 0x3; + } } - free(engine); + return n; +#else + (void)local; + int n = (int)recvfrom(engine->fd, (char*)buf, (h3q_iolen)cap, 0, (struct sockaddr*)peer, peerlen); + *seg = n > 0 ? (size_t)n : 0; + return n; +#endif } -int h3q_engine_pump(h3q_engine* engine) +/* A peer that offers an unknown version is told which versions we speak. */ +static void send_version_negotiation(h3q_engine* engine, const ngtcp2_version_cid* vc, const ngtcp2_path* path) { - int rounds = 0; - int had_queued; + static const uint32_t versions[] = {NGTCP2_PROTO_VER_V1, NGTCP2_PROTO_VER_V2}; + uint8_t rnd = 0; + uint8_t buf[H3Q_PKT_BUF]; + if (RAND_bytes(&rnd, 1) != 1) + { + return; + } + ngtcp2_ssize n = ngtcp2_pkt_write_version_negotiation(buf, sizeof(buf), rnd, vc->scid, vc->scidlen, vc->dcid, vc->dcidlen, versions, 2); + if (n > 0) + { + h3q_send(engine, path, NGTCP2_ECN_NOT_ECT, buf, (size_t)n); + h3q_tx_flush(engine); + } +} - if (!engine || !engine->ssl_listener) +static void send_retry(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2_path* path) +{ + ngtcp2_cid scid = {.datalen = H3Q_SCIDLEN}; + uint8_t token[NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN2]; + uint8_t buf[H3Q_PKT_BUF]; + if (RAND_bytes(scid.data, (int)scid.datalen) != 1) { - return 0; + return; } - had_queued = (engine->peer_rx_head != NULL); - if (SSL_handle_events(engine->ssl_listener) != 1) + ngtcp2_ssize tokenlen = ngtcp2_crypto_generate_retry_token2(token, engine->secret, sizeof(engine->secret), hd->version, path->remote.addr, path->remote.addrlen, &scid, &hd->dcid, h3q_now()); + if (tokenlen < 0) { - return -1; + return; } - while (engine->peer_rx_head && ++rounds < H3Q_PUMP_INNER_BUDGET) + ngtcp2_ssize n = ngtcp2_crypto_write_retry(buf, sizeof(buf), hd->version, &hd->scid, &scid, &hd->dcid, token, (size_t)tokenlen); + if (n > 0) { - if (SSL_handle_events(engine->ssl_listener) != 1) - { - return -1; - } + h3q_send(engine, path, NGTCP2_ECN_NOT_ECT, buf, (size_t)n); + h3q_tx_flush(engine); + } +} + +static h3q_conn* accept_initial(h3q_engine* engine, const uint8_t* pkt, size_t pktlen, const ngtcp2_path* path) +{ + ngtcp2_pkt_hd hd; + if (ngtcp2_accept(&hd, pkt, pktlen) != 0) + { + return NULL; } - if (engine->peer_rx_head) + if (!engine->address_validation) { - return 1; + return h3q_conn_new(engine, &hd, NULL, NULL, path); } - return (had_queued || rounds > 0) ? 1 : 0; + ngtcp2_cid odcid; + if (hd.tokenlen == 0 || hd.token[0] != NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY2 || ngtcp2_crypto_verify_retry_token2(&odcid, hd.token, hd.tokenlen, engine->secret, sizeof(engine->secret), hd.version, path->remote.addr, path->remote.addrlen, &hd.dcid, H3Q_RETRY_TOKEN_TIMEOUT, h3q_now()) != 0) + { + send_retry(engine, &hd, path); + return NULL; + } + return h3q_conn_new(engine, &hd, &odcid, &hd.dcid, path); } -void h3q_engine_want(h3q_engine* engine, int* want_read, int* want_write, int* timeout_ms) +static void process_dgram(h3q_engine* engine, uint8_t* buf, size_t len, const ngtcp2_path* path, uint32_t ecn) { - if (!engine || !engine->ssl_listener) + ngtcp2_version_cid vc; + int rv = ngtcp2_pkt_decode_version_cid(&vc, buf, len, H3Q_SCIDLEN); + if (rv != 0) { - *want_read = 0; - *want_write = 0; - *timeout_ms = 1000; + if (rv == NGTCP2_ERR_VERSION_NEGOTIATION) + { + send_version_negotiation(engine, &vc, path); + } return; } - *want_read = SSL_net_read_desired(engine->ssl_listener); - *want_write = SSL_net_write_desired(engine->ssl_listener); - - struct timeval tv = {0}; - int is_infinite = 0; - if (SSL_get_event_timeout(engine->ssl_listener, &tv, &is_infinite) && !is_infinite) + h3q_conn* conn = apr_hash_get(engine->conns, vc.dcid, (apr_ssize_t)vc.dcidlen); + if (!conn) { - /* long is 32-bit on Windows, so tv_sec * 1000 overflows it. */ - int64_t ms = (int64_t)tv.tv_sec * 1000 + (int64_t)tv.tv_usec / 1000; - if (ms >= 0 && ms < (int64_t)*timeout_ms) + conn = accept_initial(engine, buf, len, path); + } + if (conn && conn->close_len) + { + h3q_conn_resend_close(conn, path); + return; + } + if (!conn || conn->closed) + { + return; + } + ngtcp2_pkt_info pi = {.ecn = (uint8_t)ecn}; + rv = ngtcp2_conn_read_pkt(conn->qconn, path, &pi, buf, len, h3q_now()); + switch (rv) + { + case 0: + h3q_conn_flush(conn); + return; + case NGTCP2_ERR_RETRY: + { + ngtcp2_pkt_hd hd; + if (ngtcp2_accept(&hd, buf, len) == 0) { - *timeout_ms = (int)ms; + send_retry(engine, &hd, path); } + return; + } + case NGTCP2_ERR_DROP_CONN: + case NGTCP2_ERR_DRAINING: + case NGTCP2_ERR_CLOSING: + conn->closed = 1; + return; + default: + h3q_conn_fail(conn, rv); + return; + } +} + +/* 0: nothing to read now. 1: that datagram is lost, read on. -1: the socket is broken. */ +static int recv_failed(void) +{ +#ifdef _WIN32 + int e = WSAGetLastError(); + if (e == WSAEWOULDBLOCK) + { + return 0; } - /* A zero would spin the caller's wait when a timer reports as due. */ - if (*timeout_ms < 1) + return (e == WSAEINTR || e == WSAECONNRESET || e == WSAEMSGSIZE) ? 1 : -1; +#else + if (errno == EAGAIN) { - *timeout_ms = 1; + return 0; } + return (errno == EINTR || errno == ECONNREFUSED) ? 1 : -1; +#endif } -h3q_conn* h3q_engine_accept_conn(h3q_engine* engine) +h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size_t errlen) { - if (!engine || !engine->ssl_listener) + CHECK(cfg); + if (!cfg->ssl_ctx || !cfg->pool) + { + h3q_tls_error(err, errlen, "no TLS context to serve from"); + return NULL; + } + h3q_engine* engine = calloc(1, sizeof(*engine)); + if (!engine || apr_pool_create(&engine->pool, cfg->pool) != APR_SUCCESS) { + free(engine); + h3q_tls_error(err, errlen, "allocating the engine failed"); return NULL; } - SSL* conn = SSL_accept_connection(engine->ssl_listener, SSL_ACCEPT_CONNECTION_NO_BLOCK); - return (h3q_conn*)conn; + engine->conns = apr_hash_make(engine->pool); + engine->fd = udp_fd; + engine->stream_acked = cfg->stream_acked; + engine->idle_timeout_secs = cfg->idle_timeout_secs; + engine->max_streams_bidi = cfg->max_streams_bidi; + engine->max_window = cfg->max_window; + engine->address_validation = cfg->address_validation; + engine->early_data = cfg->early_data; + engine->local_len = (socklen_t)sizeof(engine->local); + engine->disable_migration = cfg->disable_migration; + engine->sock_ecn = -1; + if (cfg->secret) + { + memcpy(engine->secret, cfg->secret, sizeof(engine->secret)); + } + if (getsockname(udp_fd, (struct sockaddr*)&engine->local, &engine->local_len) != 0 || (!cfg->secret && RAND_bytes(engine->secret, (int)sizeof(engine->secret)) != 1) || !SSL_CTX_up_ref(cfg->ssl_ctx)) + { + h3q_tls_error(err, errlen, "reading the local address of fd=%d failed", udp_fd); + h3q_engine_destroy(engine); + return NULL; + } + engine->ssl_ctx = cfg->ssl_ctx; +#ifdef __linux__ + int on = 1; + (void)setsockopt(udp_fd, SOL_UDP, UDP_GRO, &on, sizeof(on)); /* GRO is optional */ + /* ECN marks in, also optional. IPv4-mapped peers report through the IPv4 option. */ + (void)setsockopt(udp_fd, IPPROTO_IP, IP_RECVTOS, &on, sizeof(on)); + (void)setsockopt(udp_fd, IPPROTO_IPV6, IPV6_RECVTCLASS, &on, sizeof(on)); + /* A wildcard socket must reply from the address each packet came to, or a host with more than one address replies from the wrong one. */ + if (is_wildcard(&engine->local)) + { + int v6 = engine->local.ss_family == AF_INET6 && setsockopt(udp_fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on, sizeof(on)) == 0; + int v4 = setsockopt(udp_fd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on)) == 0; + engine->pktinfo = (v6 || v4) ? 1u : 0u; + } + engine->gso = 1; +#endif + engine->tx = apr_palloc(engine->pool, H3Q_GSO_BYTES); + return engine; } -int h3q_engine_peer_addr(h3q_engine* engine, h3q_conn* conn, struct sockaddr_storage* addr, socklen_t* addr_len) +void h3q_engine_destroy(h3q_engine* engine) { - /* Report it: losing a client address is not worth aborting the child. */ - if (!engine || !conn || !addr || !addr_len) + if (!engine) { - return 0; + return; } - if (engine->peer_addr_ex_index < 0) + while (engine->conns_head) { - return 0; + h3q_conn_free(engine->conns_head); } + apr_pool_destroy(engine->pool); + if (engine->ssl_ctx) + { + SSL_CTX_free(engine->ssl_ctx); + } + free(engine); +} - /* Recorded by h3q_new_pending_conn_cb when the connection first appeared. */ - const BIO_ADDR* peer = SSL_get_ex_data((SSL*)conn, engine->peer_addr_ex_index); - if (!peer) +int h3q_engine_pump(h3q_engine* engine) +{ + if (!engine) { return 0; } - - memset(addr, 0, sizeof(*addr)); - size_t rawlen = 0; - int family = BIO_ADDR_family(peer); - if (family == AF_INET) + uint8_t buf[65536]; + int work = 0; + for (int i = 0; i < H3Q_RECV_BUDGET; i++) { - struct sockaddr_in* sin = (struct sockaddr_in*)addr; - if (!BIO_ADDR_rawaddress(peer, &sin->sin_addr, &rawlen) || rawlen != sizeof(sin->sin_addr)) + struct sockaddr_storage peer; + socklen_t peerlen = (socklen_t)sizeof(peer); + struct sockaddr_storage local; + memcpy(&local, &engine->local, (size_t)engine->local_len); + size_t seg = 0; + uint32_t ecn = NGTCP2_ECN_NOT_ECT; + int n = recv_dgram(engine, buf, sizeof(buf), &peer, &peerlen, &local, &seg, &ecn); + if (n < 0) + { + int why = recv_failed(); + if (why == 0) + { + break; + } + if (why < 0) + { + return -1; + } + continue; + } + work = 1; + ngtcp2_path path = { + .local = {.addr = (ngtcp2_sockaddr*)&local, .addrlen = (ngtcp2_socklen)engine->local_len}, + .remote = {.addr = (ngtcp2_sockaddr*)&peer, .addrlen = (ngtcp2_socklen)peerlen}, + }; + for (size_t off = 0; off < (size_t)n; off += seg) { - return 0; + process_dgram(engine, buf + off, (size_t)n - off < seg ? (size_t)n - off : seg, &path, ecn); } - sin->sin_family = AF_INET; - sin->sin_port = BIO_ADDR_rawport(peer); - *addr_len = sizeof(*sin); - return 1; } - if (family == AF_INET6) + ngtcp2_tstamp now = h3q_now(); + for (h3q_conn* conn = engine->conns_head; conn; conn = conn->next) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr; - if (!BIO_ADDR_rawaddress(peer, &sin6->sin6_addr, &rawlen) || rawlen != sizeof(sin6->sin6_addr)) + if (conn->closed || ngtcp2_conn_get_expiry(conn->qconn) > now) { - return 0; + continue; } - sin6->sin6_family = AF_INET6; - sin6->sin6_port = BIO_ADDR_rawport(peer); - *addr_len = sizeof(*sin6); - return 1; + int rv = ngtcp2_conn_handle_expiry(conn->qconn, now); + if (rv != 0) + { + h3q_conn_fail(conn, rv); + continue; + } + h3q_conn_flush(conn); + } + return work; +} + +void h3q_engine_want(h3q_engine* engine, int* want_read, int* want_write, int* timeout_ms) +{ + *want_read = 1; + *want_write = 0; + if (!engine) + { + return; + } + ngtcp2_tstamp now = h3q_now(); + for (h3q_conn* conn = engine->conns_head; conn; conn = conn->next) + { + if (conn->closed) + { + continue; + } + ngtcp2_tstamp expiry = ngtcp2_conn_get_expiry(conn->qconn); + int64_t ms = expiry <= now ? 1 : (int64_t)((expiry - now) / NGTCP2_MILLISECONDS) + 1; + if (ms < *timeout_ms) + { + *timeout_ms = (int)ms; + } + } +} + +h3q_conn* h3q_engine_accept_conn(h3q_engine* engine) +{ + if (!engine || !engine->accept_head) + { + return NULL; + } + h3q_conn* conn = engine->accept_head; + engine->accept_head = conn->next_accept; + if (!engine->accept_head) + { + engine->accept_tail = NULL; + } + conn->next_accept = NULL; + conn->queued_accept = 0; + return conn; +} + +int h3q_engine_peer_addr(h3q_engine* engine, h3q_conn* conn, struct sockaddr_storage* addr, socklen_t* addr_len) +{ + (void)engine; + if (!conn || !addr || !addr_len || conn->path.path.remote.addrlen == 0 || conn->path.path.remote.addrlen > sizeof(*addr)) + { + return 0; } - return 0; + memcpy(addr, conn->path.path.remote.addr, conn->path.path.remote.addrlen); + *addr_len = (socklen_t)conn->path.path.remote.addrlen; + return 1; } diff --git a/modules/http3/quic/h3q_conn.c b/modules/http3/quic/h3q_conn.c index 64ca406b72e..c4961375449 100644 --- a/modules/http3/quic/h3q_conn.c +++ b/modules/http3/quic/h3q_conn.c @@ -20,65 +20,431 @@ #endif #include +#include #include #include -#include +#include #include "h3_check.h" +#include "quic/detail/h3q_impl.h" #include "quic/h3q_conn.h" -int h3q_conn_prepare(h3q_conn* conn, uint32_t idle_timeout_secs) +/* The map key points into a slot of the connection, so a slot stays until its key leaves the map. + * A CID that another connection has stays with it: a client must not take over a connection with its DCID. */ +static int cid_add(h3q_conn* conn, const ngtcp2_cid* cid) { - SSL* ssl_conn = (SSL*)conn; - if (!ssl_conn || !SSL_set_blocking_mode(ssl_conn, 0)) + if (apr_hash_get(conn->engine->conns, cid->data, (apr_ssize_t)cid->datalen)) + { + return 1; + } + for (int i = 0; i < H3Q_CID_SLOTS; i++) + { + if (!conn->cids[i].datalen) + { + conn->cids[i] = *cid; + apr_hash_set(conn->engine->conns, conn->cids[i].data, (apr_ssize_t)cid->datalen, conn); + return 1; + } + } + return 0; +} + +static void cid_remove(h3q_conn* conn, ngtcp2_cid* slot) +{ + if (apr_hash_get(conn->engine->conns, slot->data, (apr_ssize_t)slot->datalen) == conn) + { + apr_hash_set(conn->engine->conns, slot->data, (apr_ssize_t)slot->datalen, NULL); + } + slot->datalen = 0; +} + +static h3q_stream* stream_lookup(h3q_conn* conn, int64_t id) +{ + return ngtcp2_conn_get_stream_user_data(conn->qconn, id); +} + +static int cb_handshake_completed(ngtcp2_conn* qconn, void* user_data) +{ + (void)qconn; + ((h3q_conn*)user_data)->handshake_done = 1; + return 0; +} + +static int cb_recv_stream_data(ngtcp2_conn* qconn, uint32_t flags, int64_t stream_id, uint64_t offset, const uint8_t* data, size_t datalen, void* user_data, void* stream_user_data) +{ + (void)qconn; + (void)offset; + (void)stream_user_data; + h3q_conn* conn = user_data; + h3q_stream* st = h3q_stream_get(conn, stream_id); + if (!st) + { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + if (datalen > 0) + { + size_t need = st->rx_len + datalen; + if (need > st->rx_cap) + { + size_t cap = st->rx_cap ? st->rx_cap : 4096; + while (cap < need) + { + cap *= 2; + } + unsigned char* grown = realloc(st->rx, cap); + if (!grown) + { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + st->rx = grown; + st->rx_cap = cap; + } + memcpy(st->rx + st->rx_len, data, datalen); + st->rx_len += datalen; + } + st->fin |= (flags & NGTCP2_STREAM_DATA_FLAG_FIN) != 0; + st->early |= (flags & NGTCP2_STREAM_DATA_FLAG_0RTT) != 0; + if (!st->queued_accept) + { + st->queued_accept = 1; + if (conn->accept_tail) + { + conn->accept_tail->next_accept = st; + } + else + { + conn->accept_head = st; + } + conn->accept_tail = st; + } + return 0; +} + +static int cb_acked_stream_data_offset(ngtcp2_conn* qconn, int64_t stream_id, uint64_t offset, uint64_t datalen, void* user_data, void* stream_user_data) +{ + (void)qconn; + (void)offset; + (void)stream_user_data; + h3q_conn* conn = user_data; + if (conn->user && conn->engine->stream_acked) + { + conn->engine->stream_acked(conn->user, stream_id, (size_t)datalen); + } + return 0; +} + +static int cb_stream_close(ngtcp2_conn* qconn, uint32_t flags, int64_t stream_id, uint64_t app_error_code, void* user_data, void* stream_user_data) +{ + (void)flags; + (void)app_error_code; + (void)stream_user_data; + h3q_stream* st = stream_lookup(user_data, stream_id); + if (st) + { + st->fin = 1; + st->write_closed = 1; + st->engine_closed = 1; + } + /* ngtcp2 leaves stream credit to the application: give the peer one stream back, or it stops after its initial MAX_STREAMS. */ + if (!ngtcp2_conn_is_local_stream(qconn, stream_id)) + { + if (ngtcp2_is_bidi_stream(stream_id)) + { + ngtcp2_conn_extend_max_streams_bidi(qconn, 1); + } + else + { + ngtcp2_conn_extend_max_streams_uni(qconn, 1); + } + } + return 0; +} + +static int cb_stream_reset(ngtcp2_conn* qconn, int64_t stream_id, uint64_t final_size, uint64_t app_error_code, void* user_data, void* stream_user_data) +{ + (void)qconn; + (void)final_size; + (void)app_error_code; + (void)stream_user_data; + h3q_stream* st = stream_lookup(user_data, stream_id); + if (st) + { + st->read_reset = 1; + st->fin = 1; + } + return 0; +} + +static void cb_rand(uint8_t* dest, size_t destlen, const ngtcp2_rand_ctx* rand_ctx) +{ + (void)rand_ctx; + RAND_bytes(dest, (int)destlen); +} + +static int cb_get_new_connection_id(ngtcp2_conn* qconn, ngtcp2_cid* cid, uint8_t* token, size_t cidlen, void* user_data) +{ + (void)qconn; + h3q_conn* conn = user_data; + if (RAND_bytes(cid->data, (int)cidlen) != 1) + { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token(token, conn->engine->secret, sizeof(conn->engine->secret), cid) != 0) + { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return cid_add(conn, cid) ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +static int cb_remove_connection_id(ngtcp2_conn* qconn, const ngtcp2_cid* cid, void* user_data) +{ + (void)qconn; + h3q_conn* conn = user_data; + for (int i = 0; i < H3Q_CID_SLOTS; i++) + { + if (ngtcp2_cid_eq(&conn->cids[i], cid)) + { + cid_remove(conn, &conn->cids[i]); + } + } + return 0; +} + +static ngtcp2_conn* conn_ref_get_conn(ngtcp2_crypto_conn_ref* conn_ref) +{ + return ((h3q_conn*)conn_ref->user_data)->qconn; +} + +static int tls_init(h3q_conn* conn) +{ + if (ngtcp2_crypto_ossl_ctx_new(&conn->ossl_ctx, NULL) != 0 || !(conn->ssl = SSL_new(conn->engine->ssl_ctx))) { return 0; } - SSL_set_default_stream_mode(ssl_conn, SSL_DEFAULT_STREAM_MODE_NONE); - SSL_set_incoming_stream_policy(ssl_conn, SSL_INCOMING_STREAM_POLICY_ACCEPT, 0); - SSL_set_generic_value_uint(ssl_conn, SSL_VALUE_QUIC_IDLE_TIMEOUT, (uint64_t)idle_timeout_secs * 1000); + ngtcp2_crypto_ossl_ctx_set_ssl(conn->ossl_ctx, conn->ssl); + if (ngtcp2_crypto_ossl_configure_server_session(conn->ssl) != 0) + { + return 0; + } + conn->conn_ref.get_conn = conn_ref_get_conn; + conn->conn_ref.user_data = conn; + SSL_set_app_data(conn->ssl, &conn->conn_ref); + SSL_set_accept_state(conn->ssl); + if (conn->engine->early_data) + { + SSL_set_quic_tls_early_data_enabled(conn->ssl, 1); + } + ngtcp2_conn_set_tls_native_handle(conn->qconn, conn->ossl_ctx); return 1; } +h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2_cid* odcid, const ngtcp2_cid* retry_scid, const ngtcp2_path* path) +{ + h3q_conn* conn = calloc(1, sizeof(*conn)); + if (!conn) + { + return NULL; + } + conn->engine = engine; + conn->next = engine->conns_head; + engine->conns_head = conn; + ngtcp2_cid scid = {.datalen = H3Q_SCIDLEN}; + if (RAND_bytes(scid.data, (int)scid.datalen) != 1) + { + h3q_conn_free(conn); + return NULL; + } + ngtcp2_path_storage_init(&conn->path, path->local.addr, path->local.addrlen, path->remote.addr, path->remote.addrlen, NULL); + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.initial_ts = h3q_now(); + /* Autotuning: ngtcp2 doubles a window that the peer fills within a few RTTs. */ + settings.max_window = engine->max_window; + settings.max_stream_window = engine->max_window; + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + /* 2s past H3IdleTimeout: the module's clean close (checked each second) must come first. */ + params.max_idle_timeout = ((ngtcp2_duration)engine->idle_timeout_secs + 2) * NGTCP2_SECONDS; + params.initial_max_data = 1024 * 1024; + params.initial_max_stream_data_bidi_remote = 256 * 1024; + params.initial_max_stream_data_uni = 256 * 1024; + params.initial_max_streams_bidi = engine->max_streams_bidi; + params.initial_max_streams_uni = 100; + params.original_dcid = odcid ? *odcid : hd->dcid; + params.original_dcid_present = 1; + if (retry_scid) + { + params.retry_scid = *retry_scid; + params.retry_scid_present = 1; + } + params.disable_active_migration = engine->disable_migration; + params.stateless_reset_token_present = ngtcp2_crypto_generate_stateless_reset_token(params.stateless_reset_token, engine->secret, sizeof(engine->secret), &scid) == 0; + + ngtcp2_callbacks cb = { + .recv_client_initial = ngtcp2_crypto_recv_client_initial_cb, + .recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = ngtcp2_crypto_hp_mask_cb, + .update_key = ngtcp2_crypto_update_key_cb, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .handshake_completed = cb_handshake_completed, + .recv_stream_data = cb_recv_stream_data, + .acked_stream_data_offset = cb_acked_stream_data_offset, + .stream_close = cb_stream_close, + .stream_reset = cb_stream_reset, + .rand = cb_rand, + .get_new_connection_id = cb_get_new_connection_id, + .remove_connection_id = cb_remove_connection_id, + }; + if (ngtcp2_conn_server_new(&conn->qconn, &hd->scid, &scid, &conn->path.path, hd->version, &cb, &settings, ¶ms, NULL, conn) != 0 || !tls_init(conn)) + { + h3q_conn_free(conn); + return NULL; + } + cid_add(conn, &scid); + /* A retransmitted Initial still carries the original DCID. */ + cid_add(conn, &hd->dcid); + conn->queued_accept = 1; + if (engine->accept_tail) + { + engine->accept_tail->next_accept = conn; + } + else + { + engine->accept_head = conn; + } + engine->accept_tail = conn; + return conn; +} + +/* nghttp3 offers a FIN once; one ngtcp2 refused is retried here. */ +static void retry_pending_fins(h3q_conn* conn) +{ + uint8_t buf[H3Q_PKT_BUF]; + for (h3q_stream* st = conn->streams_head; st; st = st->next) + { + if (!st->fin_pending || st->write_closed) + { + continue; + } + ngtcp2_ssize ndatalen = 0; + ngtcp2_pkt_info pi; + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + ngtcp2_ssize n = ngtcp2_conn_writev_stream(conn->qconn, &ps.path, &pi, buf, sizeof(buf), &ndatalen, NGTCP2_WRITE_STREAM_FLAG_FIN, st->id, NULL, 0, h3q_now()); + if (n < 0) + { + if (n != NGTCP2_ERR_STREAM_DATA_BLOCKED) + { + st->fin_pending = 0; + st->write_closed = 1; + } + continue; + } + if (n == 0) + { + continue; /* congestion limited; the next expiry retries */ + } + h3q_send(conn->engine, &ps.path, pi.ecn, buf, (size_t)n); + if (ndatalen >= 0) + { + st->fin_pending = 0; + st->write_closed = 1; + } + } +} + +void h3q_conn_flush(h3q_conn* conn) +{ + if (!conn || conn->closed) + { + return; + } + retry_pending_fins(conn); + uint8_t buf[H3Q_PKT_BUF]; + for (;;) + { + ngtcp2_path_storage ps; + ngtcp2_pkt_info pi; + ngtcp2_path_storage_zero(&ps); + ngtcp2_ssize n = ngtcp2_conn_write_pkt(conn->qconn, &ps.path, &pi, buf, sizeof(buf), h3q_now()); + if (n < 0) + { + h3q_conn_fail(conn, (int)n); + return; + } + if (n == 0) + { + break; + } + h3q_send(conn->engine, &ps.path, pi.ecn, buf, (size_t)n); + } + h3q_tx_flush(conn->engine); + /* Without this ngtcp2 never paces and bursts the whole window. */ + ngtcp2_conn_update_pkt_tx_time(conn->qconn, h3q_now()); +} + +void h3q_conn_set_user(h3q_conn* conn, void* user) +{ + if (conn) + { + conn->user = user; + } +} + h3q_stream* h3q_conn_open_uni_stream(h3q_conn* conn, int64_t* out_id) { - SSL* ssl_conn = (SSL*)conn; - CHECK(ssl_conn); + CHECK(conn); CHECK(out_id); - SSL* stream = SSL_new_stream(ssl_conn, SSL_STREAM_FLAG_UNI); - if (!stream) + if (ngtcp2_conn_open_uni_stream(conn->qconn, out_id, NULL) != 0) { return NULL; } - *out_id = (int64_t)SSL_get_stream_id(stream); - return (h3q_stream*)stream; + return h3q_stream_get(conn, *out_id); } h3q_stream* h3q_conn_accept_stream(h3q_conn* conn) { - SSL* ssl_conn = (SSL*)conn; - if (!ssl_conn) + if (!conn || !conn->accept_head) { return NULL; } - return (h3q_stream*)SSL_accept_stream(ssl_conn, SSL_ACCEPT_STREAM_NO_BLOCK); + h3q_stream* st = conn->accept_head; + conn->accept_head = st->next_accept; + if (!conn->accept_head) + { + conn->accept_tail = NULL; + } + st->next_accept = NULL; + st->queued_accept = 0; + return st; } int h3q_conn_is_handshake_done(h3q_conn* conn) { - SSL* ssl_conn = (SSL*)conn; - return ssl_conn ? SSL_is_init_finished(ssl_conn) : 0; + return conn ? (int)conn->handshake_done : 0; +} + +int h3q_conn_has_early_data(h3q_conn* conn) +{ + return conn && !conn->handshake_done && conn->accept_head != NULL; } int h3q_conn_tls_info(h3q_conn* conn, h3q_tls_info* out) { - SSL* ssl_conn = (SSL*)conn; - if (!ssl_conn || !out) + if (!conn || !out) { return 0; } - const SSL_CIPHER* cipher = SSL_get_current_cipher(ssl_conn); + const SSL_CIPHER* cipher = SSL_get_current_cipher(conn->ssl); if (!cipher) { return 0; @@ -87,63 +453,157 @@ int h3q_conn_tls_info(h3q_conn* conn, h3q_tls_info* out) out->cipher_bits = SSL_CIPHER_get_bits(cipher, &alg_bits); out->cipher_alg_bits = alg_bits; out->cipher = SSL_CIPHER_get_name(cipher); - const char* version = SSL_get_version(ssl_conn); - out->protocol = (version && strncmp(version, "TLS", 3) == 0) ? version : "TLSv1.3"; - out->resumed = SSL_session_reused(ssl_conn) ? 1u : 0u; + out->protocol = SSL_get_version(conn->ssl); + out->resumed = SSL_session_reused(conn->ssl) ? 1u : 0u; return 1; } int h3q_conn_is_closed(h3q_conn* conn) { - SSL* ssl_conn = (SSL*)conn; - return ssl_conn ? (SSL_get_shutdown(ssl_conn) != 0) : 1; + return !conn || conn->closed || ngtcp2_conn_in_closing_period(conn->qconn) || ngtcp2_conn_in_draining_period(conn->qconn); } -int h3q_conn_shutdown(h3q_conn* conn, int is_rapid, uint64_t app_error, const char* reason) +/* Send CONNECTION_CLOSE once and keep it, so the closing period can resend it. */ +void h3q_conn_close(h3q_conn* conn, const ngtcp2_ccerr* ccerr) { - SSL* ssl_conn = (SSL*)conn; - if (!ssl_conn) + if (conn->closed) { - return 1; + return; } - uint64_t flags = is_rapid ? (uint64_t)SSL_SHUTDOWN_FLAG_RAPID : 0; - int ret = 0; - if (reason) + conn->closed = 1; + if (ngtcp2_conn_in_closing_period(conn->qconn) || ngtcp2_conn_in_draining_period(conn->qconn)) + { + return; + } + if (!(conn->close_pkt = malloc(H3Q_PKT_BUF))) { - SSL_SHUTDOWN_EX_ARGS args = {.quic_error_code = app_error, .quic_reason = reason}; - ret = SSL_shutdown_ex(ssl_conn, flags, &args, sizeof(args)); + return; } - else if (flags != 0) + ngtcp2_path_storage ps; + ngtcp2_pkt_info pi; + ngtcp2_path_storage_zero(&ps); + ngtcp2_ssize n = ngtcp2_conn_write_connection_close(conn->qconn, &ps.path, &pi, conn->close_pkt, H3Q_PKT_BUF, ccerr, h3q_now()); + if (n > 0) { - SSL_SHUTDOWN_EX_ARGS args = {0}; - ret = SSL_shutdown_ex(ssl_conn, flags, &args, sizeof(args)); + ngtcp2_duration pto = ngtcp2_conn_get_pto(conn->qconn); + conn->close_len = (size_t)n; + conn->close_until = h3q_now() + 3 * pto; + conn->close_next = h3q_now() + pto; + h3q_send(conn->engine, &ps.path, pi.ecn, conn->close_pkt, conn->close_len); + h3q_tx_flush(conn->engine); + } +} + +/* Close on a local ngtcp2 error and keep the error for the log. */ +void h3q_conn_fail(h3q_conn* conn, int liberr) +{ + if (!conn->liberr) + { + conn->liberr = liberr; + } + /* Idle timeout and drop: close silently, never send CONNECTION_CLOSE (RFC 9000 10.1). */ + if (liberr == NGTCP2_ERR_IDLE_CLOSE || liberr == NGTCP2_ERR_DROP_CONN) + { + conn->closed = 1; + return; + } + ngtcp2_ccerr ccerr; + if (liberr == NGTCP2_ERR_CRYPTO) + { + ngtcp2_ccerr_set_tls_alert(&ccerr, ngtcp2_conn_get_tls_alert(conn->qconn), NULL, 0); } else { - ret = SSL_shutdown(ssl_conn); + ngtcp2_ccerr_set_liberr(&ccerr, liberr, NULL, 0); + } + h3q_conn_close(conn, &ccerr); +} + +/* Closing period: answer the peer with the stored close, at most once per PTO. */ +void h3q_conn_resend_close(h3q_conn* conn, const ngtcp2_path* path) +{ + ngtcp2_tstamp now = h3q_now(); + if (now < conn->close_next) + { + return; } + conn->close_next = now + ngtcp2_conn_get_pto(conn->qconn); + h3q_send(conn->engine, path, NGTCP2_ECN_NOT_ECT, conn->close_pkt, conn->close_len); + h3q_tx_flush(conn->engine); +} - if (ret == 1) +int h3q_conn_shutdown(h3q_conn* conn, int is_rapid, uint64_t app_error, const char* reason) +{ + if (!conn) { return 1; } - if (ret < 0) + ngtcp2_ccerr ccerr; + ngtcp2_ccerr_default(&ccerr); + if (reason) { - int err = SSL_get_error(ssl_conn, ret); - if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) - { - return 1; - } + ngtcp2_ccerr_set_application_error(&ccerr, app_error, (const uint8_t*)reason, strlen(reason)); } - return 0; + h3q_conn_close(conn, &ccerr); + return is_rapid || conn->close_len == 0 || h3q_now() >= conn->close_until; } void h3q_conn_free(h3q_conn* conn) { - if (conn) + if (!conn) { - SSL_free((SSL*)conn); + return; } + h3q_engine* engine = conn->engine; + for (h3q_conn** slot = &engine->conns_head; *slot; slot = &(*slot)->next) + { + if (*slot == conn) + { + *slot = conn->next; + break; + } + } + for (h3q_conn** slot = &engine->accept_head; *slot; slot = &(*slot)->next_accept) + { + if (*slot == conn) + { + *slot = conn->next_accept; + if (!*slot) + { + engine->accept_tail = NULL; + } + break; + } + } + for (int i = 0; i < H3Q_CID_SLOTS; i++) + { + if (conn->cids[i].datalen) + { + cid_remove(conn, &conn->cids[i]); + } + } + if (conn->qconn) + { + ngtcp2_conn_del(conn->qconn); + } + if (conn->ssl) + { + SSL_set_app_data(conn->ssl, NULL); + SSL_free(conn->ssl); + } + if (conn->ossl_ctx) + { + ngtcp2_crypto_ossl_ctx_del(conn->ossl_ctx); + } + while (conn->streams_head) + { + h3q_stream* next = conn->streams_head->next; + free(conn->streams_head->rx); + free(conn->streams_head); + conn->streams_head = next; + } + free(conn->close_pkt); + free(conn); } void h3q_conn_close_reason(h3q_conn* conn, char* buf, size_t buflen) @@ -152,18 +612,17 @@ void h3q_conn_close_reason(h3q_conn* conn, char* buf, size_t buflen) { return; } - buf[0] = '\0'; - char errbuf[H3Q_ERRLEN] = {0}; ERR_error_string_n(ERR_peek_last_error(), errbuf, sizeof(errbuf)); - - SSL_CONN_CLOSE_INFO cci = {0}; - if (conn && SSL_get_conn_close_info((SSL*)conn, &cci, sizeof(cci))) + if (conn && conn->liberr) + { + snprintf(buf, buflen, "local %s (%s)", ngtcp2_strerror(conn->liberr), errbuf); + return; + } + const ngtcp2_ccerr* cc = conn && conn->qconn ? ngtcp2_conn_get_ccerr(conn->qconn) : NULL; + if (cc) { - const char* origin = (cci.flags & SSL_CONN_CLOSE_FLAG_LOCAL) ? "local" : "remote"; - const char* layer = (cci.flags & SSL_CONN_CLOSE_FLAG_TRANSPORT) ? "transport" : "app"; - const char* reason = cci.reason ? cci.reason : ""; - snprintf(buf, buflen, "%s %s err=0x%llx frame=0x%llx reason=\"%.*s\" (%s)", origin, layer, (unsigned long long)cci.error_code, (unsigned long long)cci.frame_type, (int)cci.reason_len, reason, errbuf); + snprintf(buf, buflen, "type=%d err=0x%llx frame=0x%llx reason=\"%.*s\" (%s)", (int)cc->type, (unsigned long long)cc->error_code, (unsigned long long)cc->frame_type, (int)cc->reasonlen, cc->reason ? (const char*)cc->reason : "", errbuf); return; } snprintf(buf, buflen, "%s", errbuf); diff --git a/modules/http3/quic/h3q_stream.c b/modules/http3/quic/h3q_stream.c index 44ba7cb3263..2a374eda3c4 100644 --- a/modules/http3/quic/h3q_stream.c +++ b/modules/http3/quic/h3q_stream.c @@ -19,109 +19,240 @@ #include #endif -#include +#include +#include +#include +#include +#include "quic/detail/h3q_impl.h" #include "quic/h3q_stream.h" +static_assert(sizeof(h3q_vec) == sizeof(ngtcp2_vec) && offsetof(h3q_vec, len) == offsetof(ngtcp2_vec, len), "h3q_vec must match ngtcp2_vec"); + +h3q_stream* h3q_stream_get(h3q_conn* conn, int64_t id) +{ + h3q_stream* st = ngtcp2_conn_get_stream_user_data(conn->qconn, id); + if (st) + { + return st; + } + st = calloc(1, sizeof(*st)); + if (!st) + { + return NULL; + } + st->conn = conn; + st->id = id; + st->next = conn->streams_head; + conn->streams_head = st; + ngtcp2_conn_set_stream_user_data(conn->qconn, id, st); + return st; +} + +int64_t h3q_stream_id(h3q_stream* st) +{ + return st ? st->id : -1; +} + h3q_write_result h3q_stream_write(h3q_stream* st, const h3q_vec* vec, size_t nvec, int fin) { - SSL* ssl = (SSL*)st; h3q_write_result res = {0}; - size_t expected = 0; - for (size_t k = 0; k < nvec; k++) + if (!st || st->write_closed || st->conn->closed) { - expected += vec[k].len; + res.broken = 1; + return res; } - for (size_t k = 0; k < nvec; k++) + h3q_conn* conn = st->conn; + const ngtcp2_vec* datav = (const ngtcp2_vec*)vec; + size_t vi = 0; + size_t voff = 0; + uint8_t buf[H3Q_PKT_BUF]; + for (;;) { - size_t w = 0; - int wrv = SSL_write_ex(ssl, vec[k].base, vec[k].len, &w); - if (wrv <= 0) + ngtcp2_vec head = {0}; + size_t cnt = 0; + if (vi < nvec) + { + head.base = datav[vi].base + voff; + head.len = datav[vi].len - voff; + cnt = 1; + } + int last = vi + 1 >= nvec; + uint32_t flags = (fin && last) ? NGTCP2_WRITE_STREAM_FLAG_FIN : NGTCP2_WRITE_STREAM_FLAG_NONE; + ngtcp2_ssize ndatalen = 0; + ngtcp2_pkt_info pi; + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + ngtcp2_ssize n = ngtcp2_conn_writev_stream(conn->qconn, &ps.path, &pi, buf, sizeof(buf), &ndatalen, flags, st->id, cnt ? &head : NULL, cnt, h3q_now()); + if (n < 0) { - if (SSL_get_error(ssl, wrv) == SSL_ERROR_WANT_WRITE) + if (n == NGTCP2_ERR_STREAM_DATA_BLOCKED) { res.blocked = 1; } else { + st->write_closed = 1; res.broken = 1; + if (ngtcp2_err_is_fatal((int)n)) + { + h3q_conn_fail(conn, (int)n); + } } break; } - res.accepted += w; - if (w < vec[k].len) + if (n > 0) { - res.blocked = 1; + h3q_send(conn->engine, &ps.path, pi.ecn, buf, (size_t)n); + } + if (ndatalen > 0) + { + res.accepted += (size_t)ndatalen; + for (size_t left = (size_t)ndatalen; left > 0 && vi < nvec;) + { + size_t chunk = datav[vi].len - voff; + if (chunk > left) + { + voff += left; + left = 0; + } + else + { + left -= chunk; + vi++; + voff = 0; + } + } + } + if (n == 0) + { + /* Congestion limited. An owed FIN counts as blocked, or the stream never ends. */ + if (vi < nvec || fin) + { + res.blocked = 1; + st->fin_pending = vi >= nvec && fin; + } + break; + } + if (vi >= nvec) + { + if (flags & NGTCP2_WRITE_STREAM_FLAG_FIN) + { + /* ndatalen stays -1 when other frames crowded the STREAM frame out. */ + st->fin_pending = ndatalen < 0; + st->write_closed = ndatalen >= 0; + res.blocked |= ndatalen < 0; + } break; } } - if (fin && !res.blocked && !res.broken && res.accepted == expected) - { - SSL_stream_conclude(ssl, 0); - } + h3q_conn_flush(conn); return res; } int h3q_stream_is_write_blocked(h3q_stream* st) { - SSL* ssl = (SSL*)st; - uint64_t avail = 0; - if (ssl && SSL_get_generic_value_uint(ssl, SSL_VALUE_STREAM_WRITE_BUF_AVAIL, &avail) == 1 && avail == 0) + if (!st || st->write_closed || st->conn->closed) { return 1; } - return 0; + return ngtcp2_conn_get_max_data_left(st->conn->qconn) == 0 || ngtcp2_conn_get_max_stream_data_left(st->conn->qconn, st->id) == 0; } int h3q_stream_read(h3q_stream* st, unsigned char* buf, size_t read_size, size_t* nread, int* fin) { - SSL* ssl = (SSL*)st; + *nread = 0; *fin = 0; - int rv = SSL_read_ex(ssl, buf, read_size, nread); - if (rv == 1 && *nread > 0) + if (!st) { - return 1; + return 0; + } + size_t avail = st->rx_len - st->rx_off; + if (avail == 0) + { + *fin = st->fin; + return 0; + } + size_t n = read_size < avail ? read_size : avail; + memcpy(buf, st->rx + st->rx_off, n); + st->rx_off += n; + *nread = n; + if (st->rx_off == st->rx_len) + { + st->rx_off = 0; + st->rx_len = 0; + *fin = st->fin; } - if (rv == 1 || SSL_get_error(ssl, rv) == SSL_ERROR_ZERO_RETURN) + /* Consumed bytes give the peer window back. */ + ngtcp2_conn_extend_max_stream_offset(st->conn->qconn, st->id, n); + ngtcp2_conn_extend_max_offset(st->conn->qconn, n); + if (st->rx_len == 0) { - *fin = 1; + h3q_conn_flush(st->conn); /* Send the window now. A blocked peer sends nothing that causes a flush. */ } - return 0; + return 1; } void h3q_stream_is_read_finished(h3q_stream* st, int* read_finished, int* write_finished) { - SSL* ssl = (SSL*)st; - int rstate = SSL_get_stream_read_state(ssl); - *read_finished = (rstate == SSL_STREAM_STATE_FINISHED || rstate == SSL_STREAM_STATE_RESET_REMOTE || rstate == SSL_STREAM_STATE_CONN_CLOSED); - int wstate = SSL_STREAM_STATE_FINISHED; - if (rstate != SSL_STREAM_STATE_CONN_CLOSED && rstate != SSL_STREAM_STATE_RESET_REMOTE) + if (!st) { - wstate = SSL_get_stream_write_state(ssl); + *read_finished = 1; + *write_finished = 1; + return; } - *write_finished = (wstate == SSL_STREAM_STATE_FINISHED || wstate == SSL_STREAM_STATE_RESET_LOCAL); + *read_finished = st->read_reset || (st->fin && st->rx_off >= st->rx_len); + /* Not write_closed: ngtcp2 resends from our buffers until it closes the stream. */ + *write_finished = st->engine_closed || st->conn->closed; +} + +int h3q_stream_is_early(h3q_stream* st) +{ + return st ? (int)st->early : 0; } void h3q_stream_reset(h3q_stream* st, uint64_t err) { - if (!st) + if (st && !st->conn->closed) { - return; + ngtcp2_conn_shutdown_stream_write(st->conn->qconn, 0, st->id, err); + st->write_closed = 1; } - SSL_STREAM_RESET_ARGS args = {err}; - SSL_stream_reset((SSL*)st, &args, sizeof(args)); } void h3q_stream_free(h3q_stream* st) { - if (st) + if (!st) { - SSL_free((SSL*)st); + return; } -} - -int64_t h3q_stream_id(h3q_stream* st) -{ - SSL* ssl = (SSL*)st; - return ssl ? (int64_t)SSL_get_stream_id(ssl) : -1; + h3q_conn* conn = st->conn; + if (!st->engine_closed && !conn->closed) + { + /* Drop unsent data now: its owner frees it after this call. */ + ngtcp2_conn_shutdown_stream_write(conn->qconn, 0, st->id, 0); + } + ngtcp2_conn_set_stream_user_data(conn->qconn, st->id, NULL); + for (h3q_stream** slot = &conn->streams_head; *slot; slot = &(*slot)->next) + { + if (*slot == st) + { + *slot = st->next; + break; + } + } + for (h3q_stream** slot = &conn->accept_head; *slot; slot = &(*slot)->next_accept) + { + if (*slot == st) + { + *slot = st->next_accept; + if (!*slot) + { + conn->accept_tail = NULL; + } + break; + } + } + free(st->rx); + free(st); }