From f09eec62948baebe688caa6a7b45e42260b48b1b Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 13:09:53 +0300 Subject: [PATCH 01/13] Inherit the HTTP/3 certificate from mod_ssl, select it by SNI A host with "h3" in Protocols now serves HTTP/3 with the certificate mod_ssl already resolved for it (SSLCertificateFile and mod_md alike), the way mod_http2 rides mod_ssl. The certificate and key are loaded in the ap_ssl_add_cert_files hook, during startup while httpd still runs privileged, so a key readable only by root loads for QUIC as it does for mod_ssl instead of failing in the unprivileged child. H3CertificatePath and H3CertificateKeyPath are removed; drop them from existing configurations. Each virtual host on a shared port now presents its own certificate over HTTP/3, chosen by the client SNI; an unmatched name gets the listener default. Names come from ServerName, ServerAlias and the wildcard ServerAlias names that httpd keeps in wild_names, and every entry is matched with ap_strcasecmp_match, the matcher httpd uses for its own virtual hosts. A cert_cb on the listener context applies the matched host certificate, key and chain to the connection: SSL_set_SSL_CTX does not switch the certificate of a QUIC connection. Landing it exposed that the module ran with AP_MODULE_FLAG_NONE, so a host without H3 directives shared the main server configuration; per-host H3AltSvc and H3AltSvcMaxAge were silently ignored. Set ALWAYS_MERGE. --- changes-entries/h3_mod_ssl_certificate.txt | 23 ++++ modules/http3/h3_config.c | 138 +++++++++++++------- modules/http3/h3_hooks.c | 2 +- modules/http3/h3_io.c | 4 +- modules/http3/h3_server.c | 2 +- modules/http3/include/h3_config.h | 33 +++-- modules/http3/include/quic/detail/h3q_tls.h | 20 ++- modules/http3/include/quic/h3q.h | 7 +- modules/http3/mod_http3.c | 8 +- modules/http3/quic/detail/h3q_tls.c | 19 ++- modules/http3/quic/h3q.c | 5 +- 11 files changed, 176 insertions(+), 85 deletions(-) create mode 100644 changes-entries/h3_mod_ssl_certificate.txt diff --git a/changes-entries/h3_mod_ssl_certificate.txt b/changes-entries/h3_mod_ssl_certificate.txt new file mode 100644 index 00000000000..bf5e087385e --- /dev/null +++ b/changes-entries/h3_mod_ssl_certificate.txt @@ -0,0 +1,23 @@ + *) 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. + [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/modules/http3/h3_config.c b/modules/http3/h3_config.c index cde39bd8e31..ffbfa8f888d 100644 --- a/modules/http3/h3_config.c +++ b/modules/http3/h3_config.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ #include "h3_os.h" #include "h3_request.h" #include "mod_http3.h" +#include "quic/detail/h3q_tls.h" apr_port_t get_server_port(const server_rec* s) { @@ -58,8 +60,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; @@ -93,28 +93,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) @@ -583,8 +561,64 @@ 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; @@ -594,11 +628,33 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool 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) { @@ -681,34 +737,24 @@ 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; } - conf = vc; - break; + if (!conf) + { + conf = vc; /* the first host owns the listener; the rest still advertise it */ + ap_log_error(APLOG_MARK, APLOG_INFO, 0, vs, "h3_post_config: pid=%d h3_port=%d mpm=%s threaded=%d forked=%d max_threads=%d", h3_getpid(), (int)vc->h3_port, ap_show_mpm(), mpm_query(AP_MPMQ_IS_THREADED), mpm_query(AP_MPMQ_IS_FORKED), mpm_query(AP_MPMQ_MAX_THREADS)); + } } } - 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; - 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)); return OK; } @@ -725,8 +771,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)"), 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..66ad2abac40 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -131,10 +131,8 @@ 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, .address_validation = (conf->h3_address_validation != H3_FLAG_OFF), - .session_tickets = (conf->h3_session_tickets != H3_FLAG_OFF), }; io->qengine = h3q_engine_create(&qcfg, udp_fd, qerr, sizeof(qerr)); if (!io->qengine) diff --git a/modules/http3/h3_server.c b/modules/http3/h3_server.c index 9a7ef5fb121..24785060cf4 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -84,7 +84,7 @@ 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) + if (tmp && tmp->ssl_ctx && !conf) { conf = tmp; conf->host_port = get_server_port(current); diff --git a/modules/http3/include/h3_config.h b/modules/http3/include/h3_config.h index b22f5b3ba0f..ef8a56cbc8e 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; @@ -81,8 +82,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 +111,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/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..1398b310ecb 100644 --- a/modules/http3/include/quic/h3q.h +++ b/modules/http3/include/quic/h3q.h @@ -32,16 +32,15 @@ typedef struct h3q_stream h3q_stream; * a per-connection setting, applied by h3q_conn_prepare(). */ 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; unsigned address_validation : 1; - unsigned session_tickets : 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. + * @param cfg TLS context and address validation. * @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. 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_tls.c b/modules/http3/quic/detail/h3q_tls.c index 1ed6125b0d2..32d8a46d59f 100644 --- a/modules/http3/quic/detail/h3q_tls.c +++ b/modules/http3/quic/detail/h3q_tls.c @@ -77,9 +77,10 @@ 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 (!ssl_ctx) @@ -92,17 +93,21 @@ SSL_CTX* h3q_tls_ctx_create(const h3q_config* cfg, char* err, size_t errlen) 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) + 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..879a6058135 100644 --- a/modules/http3/quic/h3q.c +++ b/modules/http3/quic/h3q.c @@ -44,12 +44,13 @@ h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size } engine->peer_addr_ex_index = -1; - engine->ssl_ctx = h3q_tls_ctx_create(cfg, err, errlen); - if (!engine->ssl_ctx) + if (!cfg->ssl_ctx || !SSL_CTX_up_ref(cfg->ssl_ctx)) { + h3q_tls_error(err, errlen, "no TLS context to serve from"); h3q_engine_destroy(engine); return NULL; } + engine->ssl_ctx = cfg->ssl_ctx; BIO_METHOD* bm = BIO_meth_new(BIO_TYPE_FILTER | BIO_get_new_index(), "h3q_peer_addr"); if (!bm) From 5e3610e7593719243878b051b6066c71c76621a3 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Thu, 5 Nov 2026 01:51:21 +0200 Subject: [PATCH 02/13] Serve HTTP/3 from the first virtual host defined, as mod_ssl does httpd prepends virtual hosts to the server list, so the first host with an HTTP/3 context in that list is the last one defined. The listener took that host's settings and certificate, and a client whose SNI matched no host got that certificate, where mod_ssl serves the first virtual host defined for the address. Take the last match in both h3_post_config and find_h3_server instead. --- changes-entries/h3_mod_ssl_certificate.txt | 3 ++- modules/http3/h3_config.c | 11 ++++++----- modules/http3/h3_server.c | 9 ++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/changes-entries/h3_mod_ssl_certificate.txt b/changes-entries/h3_mod_ssl_certificate.txt index bf5e087385e..ab44463450c 100644 --- a/changes-entries/h3_mod_ssl_certificate.txt +++ b/changes-entries/h3_mod_ssl_certificate.txt @@ -12,7 +12,8 @@ *) 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. + 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 diff --git a/modules/http3/h3_config.c b/modules/http3/h3_config.c index ffbfa8f888d..0a2db763fea 100644 --- a/modules/http3/h3_config.c +++ b/modules/http3/h3_config.c @@ -622,6 +622,7 @@ int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, 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) { @@ -737,11 +738,10 @@ int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, { vc->h3_idle_timeout = H3_IDLE_TIMEOUT_DEFAULT; } - if (!conf) - { - conf = vc; /* the first host owns the listener; the rest still advertise it */ - ap_log_error(APLOG_MARK, APLOG_INFO, 0, vs, "h3_post_config: pid=%d h3_port=%d mpm=%s threaded=%d forked=%d max_threads=%d", h3_getpid(), (int)vc->h3_port, ap_show_mpm(), mpm_query(AP_MPMQ_IS_THREADED), mpm_query(AP_MPMQ_IS_FORKED), mpm_query(AP_MPMQ_MAX_THREADS)); - } + /* 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; + owner = vs; } } @@ -750,6 +750,7 @@ int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, 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; } + 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)); /* 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); diff --git a/modules/http3/h3_server.c b/modules/http3/h3_server.c index 24785060cf4..562a2a165a8 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -84,15 +84,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->ssl_ctx && !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; } From ac2d8106ee66ec1e111d90bf7464379f76bb7c55 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 02:15:34 +0300 Subject: [PATCH 03/13] Bind the QUIC socket in the parent, before the privilege drop Each child bound the UDP port itself after httpd dropped root, so a port below 1024 needed CAP_NET_BIND_SERVICE on the httpd binary. On Linux the parent now binds the socket in post_config, while it still runs as root, and keeps it in the process pool, so it stays bound across graceful restarts. The children inherit it. One child at a time serves it: the child that binds an abstract Unix socket name owns the port, and the kernel releases the name when that child exits. Other platforms keep the old per-child bind. --- changes-entries/h3_parent_socket.txt | 5 ++ modules/http3/h3_config.c | 2 + modules/http3/h3_server.c | 113 +++++++++++++++++++++++++-- modules/http3/include/h3_server.h | 7 ++ 4 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 changes-entries/h3_parent_socket.txt 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/modules/http3/h3_config.c b/modules/http3/h3_config.c index 0a2db763fea..76e1d3e1dcd 100644 --- a/modules/http3/h3_config.c +++ b/modules/http3/h3_config.c @@ -34,6 +34,7 @@ #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" @@ -756,6 +757,7 @@ int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, SSL_CTX_set_cert_cb(conf->ssl_ctx, h3_sni_select_cert, sni); h3_request_init(); + h3_server_post_config(s); return OK; } diff --git a/modules/http3/h3_server.c b/modules/http3/h3_server.c index 562a2a165a8..b9e405716e2 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -32,6 +32,12 @@ #include "h3_socket.h" #include "mod_http3.h" +#ifdef __linux__ + #include + #include + #include +#endif + static volatile int child_stopping = 0; static apr_thread_t* port_acquire_thread = NULL; @@ -43,13 +49,71 @@ struct port_acquire_args h3_server_conf* conf; }; +#ifdef __linux__ +/* The parent binds the QUIC socket while still privileged and keeps it across restarts. */ +typedef struct +{ + apr_pool_t* pool; + int fd; + apr_port_t port; +} h3_parent_socket; + +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 it; the child that binds this abstract name owns it, and exit releases it. */ +static int owner_lock = -1; + +static int own_port(apr_port_t port) +{ + struct sockaddr_un a = {.sun_family = AF_UNIX}; + int n = snprintf(a.sun_path + 1, sizeof(a.sun_path) - 1, "mod_http3.%d", (int)port); + 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->fd >= 0 && ps->port == conf->h3_port) + { + *fd = ps->fd; + return own_port(ps->port) ? APR_SUCCESS : APR_EAGAIN; + } +#endif + return h3_socket_open(conf->h3_port, conf->h3_socket_buffer_size, pchild, 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 +126,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()); @@ -110,7 +174,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()); @@ -134,8 +198,47 @@ 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); + } +} + +void h3_server_post_config(server_rec* s) +{ +#ifdef __linux__ + h3_parent_socket* ps = parent_socket(s); + if (!ps) + { + ps = apr_pcalloc(s->process->pool, sizeof(*ps)); + ps->fd = -1; + 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); + if (ps->fd >= 0 && (!conf || conf->h3_port != ps->port)) + { + apr_pool_destroy(ps->pool); /* closes the old socket */ + ps->pool = NULL; + ps->fd = -1; + } + if (!conf || ps->fd >= 0) + { + return; } + if (apr_pool_create(&ps->pool, s->process->pool) != APR_SUCCESS || h3_socket_open(conf->h3_port, conf->h3_socket_buffer_size, ps->pool, &ps->fd) != APR_SUCCESS) + { + 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; + ps->fd = -1; + return; + } + 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/include/h3_server.h b/modules/http3/include/h3_server.h index b18172e8066..2eb747a039c 100644 --- a/modules/http3/include/h3_server.h +++ b/modules/http3/include/h3_server.h @@ -33,6 +33,13 @@ */ 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); + /** * Child-stopping hook: stop retrying for the QUIC port, then either drain the * live connections (graceful) or tear the listener down at once (immediate). From a5d8348340c1235f3724283038558b4e0483258e Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 02:43:40 +0300 Subject: [PATCH 04/13] Bind one SO_REUSEPORT QUIC socket per httpd listener bucket With ListenCoresBucketsRatio, httpd splits its TCP listeners into buckets. The parent now binds one SO_REUSEPORT UDP socket per bucket, and each child serves the first bucket that no other child owns. So more than one child serves HTTP/3. The parent keeps the sockets while children start and stop, so the kernel hash does not change and each peer stays on the same socket. Without buckets, there is one socket as before. --- changes-entries/h3_reuseport_buckets.txt | 6 +++ modules/http3/h3_server.c | 52 ++++++++++++++++-------- modules/http3/h3_socket.c | 12 +++++- modules/http3/include/h3_socket.h | 7 ++-- 4 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 changes-entries/h3_reuseport_buckets.txt 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/modules/http3/h3_server.c b/modules/http3/h3_server.c index b9e405716e2..60686e51cef 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -16,6 +16,7 @@ #include +#include #include #include @@ -50,11 +51,13 @@ struct port_acquire_args }; #ifdef __linux__ -/* The parent binds the QUIC socket while still privileged and keeps it across restarts. */ +/* 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 fd; + int* fds; + int n; apr_port_t port; } h3_parent_socket; @@ -65,13 +68,13 @@ static h3_parent_socket* parent_socket(server_rec* s) return ps; } -/* The children inherit it; the child that binds this abstract name owns it, and exit releases it. */ +/* 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) +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", (int)port); + 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); @@ -85,13 +88,20 @@ static apr_status_t acquire_port(apr_pool_t* pchild, server_rec* s, h3_server_co { #ifdef __linux__ h3_parent_socket* ps = parent_socket(s); - if (ps && ps->fd >= 0 && ps->port == conf->h3_port) + if (ps && ps->n && ps->port == conf->h3_port) { - *fd = ps->fd; - return own_port(ps->port) ? APR_SUCCESS : APR_EAGAIN; + 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, fd); + 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. */ @@ -209,22 +219,32 @@ void h3_server_post_config(server_rec* s) if (!ps) { ps = apr_pcalloc(s->process->pool, sizeof(*ps)); - ps->fd = -1; 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); - if (ps->fd >= 0 && (!conf || conf->h3_port != ps->port)) + int n = ap_num_listen_buckets > 0 ? ap_num_listen_buckets : 1; + /* 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 socket */ + apr_pool_destroy(ps->pool); /* closes the old sockets */ ps->pool = NULL; - ps->fd = -1; + ps->n = 0; } - if (!conf || ps->fd >= 0) + if (!conf || ps->n) { return; } - if (apr_pool_create(&ps->pool, s->process->pool) != APR_SUCCESS || h3_socket_open(conf->h3_port, conf->h3_socket_buffer_size, ps->pool, &ps->fd) != APR_SUCCESS) + 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) @@ -232,9 +252,9 @@ void h3_server_post_config(server_rec* s) apr_pool_destroy(ps->pool); } ps->pool = NULL; - ps->fd = -1; return; } + ps->n = n; ps->port = conf->h3_port; #else (void)s; 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/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. From db89aabe41693aaad7b4c19c05909160ccdc9634 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 03:20:10 +0300 Subject: [PATCH 05/13] Count the QUIC buckets from ListenCoresBucketsRatio in post_config Some MPMs count their listener buckets in the run step, after post_config, so ap_num_listen_buckets was still 1 there. The module now uses the same count as httpd: online cores / ratio, capped at the MPM daemon limit, because a bucket with no child loses its peers. --- modules/http3/h3_server.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/http3/h3_server.c b/modules/http3/h3_server.c index 60686e51cef..1ac3ed07689 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -17,8 +17,10 @@ #include #include +#include #include #include +#include #include #include @@ -36,7 +38,9 @@ #ifdef __linux__ #include #include + #include #include + #include #endif static volatile int child_stopping = 0; @@ -61,6 +65,24 @@ typedef struct 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; @@ -223,7 +245,7 @@ void h3_server_post_config(server_rec* s) } server_rec* vhost = NULL; h3_server_conf* conf = find_h3_server(s, &vhost); - int n = ap_num_listen_buckets > 0 ? ap_num_listen_buckets : 1; + 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)) { From c036667df861012563500984c5b7d5b11bb0f1ac Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 11:01:46 +0300 Subject: [PATCH 06/13] Lock the session allocator: workers and the event thread share it Worker threads allocate from stream subpools while the event thread allocates from the session pool. All of them use one allocator, which had no mutex, so concurrent allocations corrupted the heap and crashed the child under load. Set a mutex on the allocator, as mod_http2 does. --- changes-entries/h3_session_allocator_race.txt | 3 +++ modules/http3/h3_io.c | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 changes-entries/h3_session_allocator_race.txt 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/modules/http3/h3_io.c b/modules/http3/h3_io.c index 66ad2abac40..729a7c48b1d 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -267,6 +267,15 @@ static apr_status_t spawn_serviced_session(h3_io_t* io, h3q_conn* conn) } apr_allocator_owner_set(allocator, session_pool); 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) From 0b1724fb8a59e2138f50911c4d473c490dd2634f Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 11:01:56 +0300 Subject: [PATCH 07/13] Advance pending handshakes while draining A pending handshake counts as an MPM connection. The event thread advanced pending handshakes only when not draining, so a handshake in flight at a graceful restart did not finish or time out. The old child then never reached zero connections and kept the port. --- changes-entries/h3_drain_pending_handshakes.txt | 4 ++++ modules/http3/h3_threads.c | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 changes-entries/h3_drain_pending_handshakes.txt 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/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); } From 12c6b18dc003f47161ee68a64e48d21b57446cd0 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 11:01:57 +0300 Subject: [PATCH 08/13] Block a write-blocked stream again each time nghttp3 offers it nghttp3_conn_resume_stream puts a blocked stream back in the write queue. The module blocked the stream only on the first refusal, so when the peer kept its window shut, nghttp3 offered the stream again and again and the event thread used 100% CPU. --- changes-entries/h3_blocked_stream_spin.txt | 4 ++++ modules/http3/h3_stream.c | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changes-entries/h3_blocked_stream_spin.txt 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/modules/http3/h3_stream.c b/modules/http3/h3_stream.c index b078ad7bae9..fc67e04048d 100644 --- a/modules/http3/h3_stream.c +++ b/modules/http3/h3_stream.c @@ -126,13 +126,14 @@ void flush_nghttp3(h3_session* session) 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; } } From 6c7f43b51633cf0bd442d9ff444d613d8e217284 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Thu, 5 Nov 2026 11:21:32 +0200 Subject: [PATCH 09/13] Move the QUIC transport to ngtcp2 and add H3EarlyData for 0-RTT OpenSSL's own QUIC server drops 0-RTT packets, so 0-RTT cannot work on it. This change runs the QUIC transport on ngtcp2 and keeps OpenSSL only for the TLS 1.3 handshake, through its QUIC TLS API. This is the same split that nginx uses. The h3q_ API does not change. Its implementation now reads the UDP socket, maps connection IDs to connections, answers Retry and Version Negotiation, and runs the ngtcp2 timers on a monotonic clock. The BIO filter that recovered peer addresses from the OpenSSL listener is removed. The engine does not copy stream data: it reports each acknowledgement through stream_acked, and nghttp3 releases the buffer when the peer acknowledges it. Connection close follows RFC 9000 section 10. A local close keeps its CONNECTION_CLOSE packet and resends it, at most once per PTO, for three PTOs. An idle timeout closes silently. The transport idle timer runs 2 seconds past H3IdleTimeout, so the module's clean close comes first. H3EarlyData (default off) turns on 0-RTT. With it on, a returning client sends its first request with the handshake 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). The TLS layer sets SSL_OP_NO_ANTI_REPLAY, because OpenSSL's own anti-replay is built for TLS over TCP and refuses QUIC resumption with early data. ngtcp2 >= 1.22.1 with its OpenSSL crypto helper (ngtcp2_crypto_ossl) is a new build dependency: --with-ngtcp2 for configure, NGTCP2_INCLUDE_DIR and NGTCP2_LIBRARIES for CMake. --- CMakeLists.txt | 22 +- changes-entries/h3_ngtcp2_early_data.txt | 14 + modules/http3/config2.m4 | 56 +- modules/http3/h3_config.c | 14 + modules/http3/h3_io.c | 33 +- modules/http3/h3_session.c | 1 + modules/http3/h3_stream.c | 12 +- modules/http3/include/h3_config.h | 1 + modules/http3/include/h3_stream.h | 3 + modules/http3/include/quic/detail/h3q_addr.h | 112 ---- modules/http3/include/quic/detail/h3q_impl.h | 110 ++++ modules/http3/include/quic/h3q.h | 31 +- modules/http3/include/quic/h3q_conn.h | 21 +- modules/http3/include/quic/h3q_stream.h | 12 +- modules/http3/quic/detail/h3q_addr.c | 326 ----------- modules/http3/quic/detail/h3q_tls.c | 16 +- modules/http3/quic/h3q.c | 383 ++++++++----- modules/http3/quic/h3q_conn.c | 544 +++++++++++++++++-- modules/http3/quic/h3q_stream.c | 221 ++++++-- 19 files changed, 1201 insertions(+), 731 deletions(-) create mode 100644 changes-entries/h3_ngtcp2_early_data.txt delete mode 100644 modules/http3/include/quic/detail/h3q_addr.h create mode 100644 modules/http3/include/quic/detail/h3q_impl.h delete mode 100644 modules/http3/quic/detail/h3q_addr.c 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_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/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 76e1d3e1dcd..2f262fee546 100644 --- a/modules/http3/h3_config.c +++ b/modules/http3/h3_config.c @@ -74,6 +74,7 @@ void* h3_merge_server_config(apr_pool_t* p, void* base_conf, void* new_conf) 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_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; @@ -520,6 +521,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); @@ -727,6 +736,10 @@ int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, { 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; @@ -787,6 +800,7 @@ const command_rec h3_cmds[] = { 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_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_io.c b/modules/http3/h3_io.c index 729a7c48b1d..07056c33ec3 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -132,6 +133,11 @@ 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 = { .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, + .early_data = (conf->h3_early_data == H3_FLAG_ON), .address_validation = (conf->h3_address_validation != H3_FLAG_OFF), }; io->qengine = h3q_engine_create(&qcfg, udp_fd, qerr, sizeof(qerr)); @@ -339,14 +345,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; @@ -370,13 +376,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(); @@ -388,6 +387,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) @@ -460,7 +466,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; } @@ -508,6 +516,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_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_stream.c b/modules/http3/h3_stream.c index fc67e04048d..2b69d854133 100644 --- a/modules/http3/h3_stream.c +++ b/modules/http3/h3_stream.c @@ -123,7 +123,6 @@ 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. @@ -452,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/include/h3_config.h b/modules/http3/include/h3_config.h index ef8a56cbc8e..4c974596625 100644 --- a/modules/http3/include/h3_config.h +++ b/modules/http3/include/h3_config.h @@ -52,6 +52,7 @@ struct h3_server_conf apr_uint32_t h3_idle_timeout; apr_size_t h3_socket_buffer_size; 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; 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..7b32696ce92 --- /dev/null +++ b/modules/http3/include/quic/detail/h3q_impl.h @@ -0,0 +1,110 @@ +/* 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_RETRY_TOKEN_TIMEOUT (10 * NGTCP2_SECONDS) + +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; + uint8_t secret[32]; + unsigned address_validation : 1; + unsigned early_data : 1; +}; + +struct h3q_conn +{ + h3q_engine* engine; + apr_pool_t* pool; /* CID hash keys */ + ngtcp2_conn* qconn; + ngtcp2_crypto_conn_ref conn_ref; + ngtcp2_crypto_ossl_ctx* ossl_ctx; + SSL* ssl; + ngtcp2_path_storage path; + apr_array_header_t* cids; /* every CID put in the engine map, to remove on free */ + uint8_t* close_pkt; /* CONNECTION_CLOSE, resent while closing (RFC 9000 10.2.1) */ + 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, const uint8_t* buf, size_t len); +h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2_cid* odcid, const ngtcp2_cid* retry_scid, const struct sockaddr* peer, socklen_t peerlen); +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 struct sockaddr* peer, socklen_t peerlen); +h3q_stream* h3q_stream_get(h3q_conn* conn, int64_t id); + +#endif /* H3Q_DETAIL_IMPL_H */ diff --git a/modules/http3/include/quic/h3q.h b/modules/http3/include/quic/h3q.h index 1398b310ecb..db44147ee55 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,19 +30,27 @@ 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 { /** 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; unsigned address_validation : 1; + /** Accept 0-RTT request data on resumed connections. */ + unsigned early_data : 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 TLS context 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. @@ -50,7 +60,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); @@ -59,13 +69,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, @@ -74,14 +85,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/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 32d8a46d59f..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 @@ -82,14 +83,18 @@ SSL_CTX* h3q_tls_ctx_create(const char* const* cert_files, size_t ncerts, const 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); @@ -107,6 +112,13 @@ SSL_CTX* h3q_tls_ctx_create(const char* const* cert_files, size_t ncerts, const static const unsigned char sid_ctx[] = "mod_http3"; SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); + /* 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. */ diff --git a/modules/http3/quic/h3q.c b/modules/http3/quic/h3q.c index 879a6058135..2f45bf0aec5 100644 --- a/modules/http3/quic/h3q.c +++ b/modules/http3/quic/h3q.c @@ -19,103 +19,216 @@ #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 _WIN32 +typedef int h3q_iolen; /* winsock takes int lengths */ +#else +typedef size_t h3q_iolen; +#endif -h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size_t errlen) +/* 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 +} + +static void send_raw(h3q_engine* engine, const struct sockaddr* dst, socklen_t dstlen, const uint8_t* buf, size_t len) +{ + /* A failed send is a lost datagram; loss recovery resends it. */ + (void)sendto(engine->fd, (const char*)buf, (h3q_iolen)len, 0, dst, dstlen); +} + +void h3q_send(h3q_engine* engine, const ngtcp2_path* path, const uint8_t* buf, size_t len) +{ + send_raw(engine, (const struct sockaddr*)path->remote.addr, (socklen_t)path->remote.addrlen, buf, len); +} + +/* 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 struct sockaddr* peer, socklen_t peerlen) +{ + 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) { - h3q_tls_error(err, errlen, "allocating the engine failed"); - return NULL; + return; } - engine->peer_addr_ex_index = -1; + 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) + { + send_raw(engine, peer, peerlen, buf, (size_t)n); + } +} - if (!cfg->ssl_ctx || !SSL_CTX_up_ref(cfg->ssl_ctx)) +static void send_retry(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const struct sockaddr* peer, socklen_t peerlen) +{ + 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) { - h3q_tls_error(err, errlen, "no TLS context to serve from"); - h3q_engine_destroy(engine); - return NULL; + return; } - engine->ssl_ctx = cfg->ssl_ctx; + ngtcp2_ssize tokenlen = ngtcp2_crypto_generate_retry_token2(token, engine->secret, sizeof(engine->secret), hd->version, (const ngtcp2_sockaddr*)peer, (ngtcp2_socklen)peerlen, &scid, &hd->dcid, h3q_now()); + if (tokenlen < 0) + { + return; + } + ngtcp2_ssize n = ngtcp2_crypto_write_retry(buf, sizeof(buf), hd->version, &hd->scid, &scid, &hd->dcid, token, (size_t)tokenlen); + if (n > 0) + { + send_raw(engine, peer, peerlen, buf, (size_t)n); + } +} - BIO_METHOD* bm = BIO_meth_new(BIO_TYPE_FILTER | BIO_get_new_index(), "h3q_peer_addr"); - if (!bm) +static h3q_conn* accept_initial(h3q_engine* engine, const uint8_t* pkt, size_t pktlen, const struct sockaddr* peer, socklen_t peerlen) +{ + ngtcp2_pkt_hd hd; + if (ngtcp2_accept(&hd, pkt, pktlen) != 0) { - h3q_tls_error(err, errlen, "BIO_meth_new failed"); - h3q_engine_destroy(engine); return NULL; } - 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 (!engine->address_validation) { - h3q_tls_error(err, errlen, "installing the peer address BIO handlers failed"); - h3q_engine_destroy(engine); - return NULL; + return h3q_conn_new(engine, &hd, NULL, NULL, peer, peerlen); } - - 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) + 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, (const ngtcp2_sockaddr*)peer, (ngtcp2_socklen)peerlen, &hd.dcid, H3Q_RETRY_TOKEN_TIMEOUT, h3q_now()) != 0) { - h3q_tls_error(err, errlen, "initializing peer address recovery failed"); - h3q_engine_destroy(engine); + send_retry(engine, &hd, peer, peerlen); return NULL; } + return h3q_conn_new(engine, &hd, &odcid, &hd.dcid, peer, peerlen); +} - SSL_CTX_set_new_pending_conn_cb(engine->ssl_ctx, h3q_new_pending_conn_cb, engine); +static void process_dgram(h3q_engine* engine, uint8_t* buf, size_t len, struct sockaddr_storage* peer, socklen_t peerlen) +{ + ngtcp2_version_cid vc; + int rv = ngtcp2_pkt_decode_version_cid(&vc, buf, len, H3Q_SCIDLEN); + if (rv != 0) + { + if (rv == NGTCP2_ERR_VERSION_NEGOTIATION) + { + send_version_negotiation(engine, &vc, (struct sockaddr*)peer, peerlen); + } + return; + } + h3q_conn* conn = apr_hash_get(engine->conns, vc.dcid, (apr_ssize_t)vc.dcidlen); + if (!conn) + { + conn = accept_initial(engine, buf, len, (struct sockaddr*)peer, peerlen); + } + if (conn && conn->close_len) + { + h3q_conn_resend_close(conn, (struct sockaddr*)peer, peerlen); + return; + } + if (!conn || conn->closed) + { + return; + } + ngtcp2_path path = { + .local = {.addr = (ngtcp2_sockaddr*)&engine->local, .addrlen = (ngtcp2_socklen)engine->local_len}, + .remote = {.addr = (ngtcp2_sockaddr*)peer, .addrlen = (ngtcp2_socklen)peerlen}, + }; + ngtcp2_pkt_info pi = {0}; + 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) + { + send_retry(engine, &hd, (struct sockaddr*)peer, peerlen); + } + 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; + } +} - 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) +/* 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) { - h3q_tls_error(err, errlen, "SSL_new_listener failed"); - h3q_engine_destroy(engine); - return NULL; + return 0; } + return (e == WSAEINTR || e == WSAECONNRESET || e == WSAEMSGSIZE) ? 1 : -1; +#else + if (errno == EAGAIN) + { + return 0; + } + return (errno == EINTR || errno == ECONNREFUSED) ? 1 : -1; +#endif +} - BIO* bio = BIO_new_dgram(udp_fd, BIO_NOCLOSE); - if (!bio) +h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size_t errlen) +{ + CHECK(cfg); + if (!cfg->ssl_ctx || !cfg->pool) { - h3q_tls_error(err, errlen, "BIO_new_dgram failed for fd=%d", udp_fd); - h3q_engine_destroy(engine); + h3q_tls_error(err, errlen, "no TLS context to serve from"); return NULL; } - - BIO* filter_bio = BIO_new(bm); - if (!filter_bio) + h3q_engine* engine = calloc(1, sizeof(*engine)); + if (!engine || apr_pool_create(&engine->pool, cfg->pool) != APR_SUCCESS) { - h3q_tls_error(err, errlen, "BIO_new(h3q_peer_addr) failed"); - BIO_free(bio); - h3q_engine_destroy(engine); + free(engine); + h3q_tls_error(err, errlen, "allocating the engine failed"); return NULL; } - - 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)) + 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->address_validation = cfg->address_validation; + engine->early_data = cfg->early_data; + engine->local_len = (socklen_t)sizeof(engine->local); + if (getsockname(udp_fd, (struct sockaddr*)&engine->local, &engine->local_len) != 0 || RAND_bytes(engine->secret, (int)sizeof(engine->secret)) != 1 || !SSL_CTX_up_ref(cfg->ssl_ctx)) { - h3q_tls_error(err, errlen, "SSL_listen failed"); + 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; return engine; } @@ -125,19 +238,11 @@ void h3q_engine_destroy(h3q_engine* engine) { return; } - h3q_peer_addr_queue_clear(engine); - if (engine->ssl_listener) - { - SSL_free(engine->ssl_listener); - } - if (engine->current_peer_addr) + while (engine->conns_head) { - BIO_ADDR_free(engine->current_peer_addr); - } - if (engine->peer_addr_bio_method) - { - BIO_meth_free(engine->peer_addr_bio_method); + h3q_conn_free(engine->conns_head); } + apr_pool_destroy(engine->pool); if (engine->ssl_ctx) { SSL_CTX_free(engine->ssl_ctx); @@ -147,117 +252,103 @@ void h3q_engine_destroy(h3q_engine* engine) int h3q_engine_pump(h3q_engine* engine) { - int rounds = 0; - int had_queued; - - if (!engine || !engine->ssl_listener) + if (!engine) { return 0; } - had_queued = (engine->peer_rx_head != NULL); - if (SSL_handle_events(engine->ssl_listener) != 1) + uint8_t buf[65536]; + int work = 0; + for (int i = 0; i < H3Q_RECV_BUDGET; i++) { - return -1; - } - while (engine->peer_rx_head && ++rounds < H3Q_PUMP_INNER_BUDGET) - { - if (SSL_handle_events(engine->ssl_listener) != 1) + struct sockaddr_storage peer; + socklen_t peerlen = (socklen_t)sizeof(peer); + int n = (int)recvfrom(engine->fd, (char*)buf, (h3q_iolen)sizeof(buf), 0, (struct sockaddr*)&peer, &peerlen); + if (n < 0) + { + int why = recv_failed(); + if (why == 0) + { + break; + } + if (why < 0) + { + return -1; + } + continue; + } + work = 1; + if (n > 0) { - return -1; + process_dgram(engine, buf, (size_t)n, &peer, peerlen); } } - if (engine->peer_rx_head) + ngtcp2_tstamp now = h3q_now(); + for (h3q_conn* conn = engine->conns_head; conn; conn = conn->next) { - return 1; + if (conn->closed || ngtcp2_conn_get_expiry(conn->qconn) > now) + { + continue; + } + int rv = ngtcp2_conn_handle_expiry(conn->qconn, now); + if (rv != 0) + { + h3q_conn_fail(conn, rv); + continue; + } + h3q_conn_flush(conn); } - return (had_queued || rounds > 0) ? 1 : 0; + return work; } void h3q_engine_want(h3q_engine* engine, int* want_read, int* want_write, int* timeout_ms) { - if (!engine || !engine->ssl_listener) + *want_read = 1; + *want_write = 0; + if (!engine) { - *want_read = 0; - *want_write = 0; - *timeout_ms = 1000; 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) + ngtcp2_tstamp now = h3q_now(); + for (h3q_conn* conn = engine->conns_head; conn; conn = conn->next) { - /* 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) + 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; } } - /* A zero would spin the caller's wait when a timer reports as due. */ - if (*timeout_ms < 1) - { - *timeout_ms = 1; - } } h3q_conn* h3q_engine_accept_conn(h3q_engine* engine) { - if (!engine || !engine->ssl_listener) + if (!engine || !engine->accept_head) { return NULL; } - SSL* conn = SSL_accept_connection(engine->ssl_listener, SSL_ACCEPT_CONNECTION_NO_BLOCK); - return (h3q_conn*)conn; + 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) { - /* Report it: losing a client address is not worth aborting the child. */ - if (!engine || !conn || !addr || !addr_len) + (void)engine; + if (!conn || !addr || !addr_len || conn->path.path.remote.addrlen == 0 || conn->path.path.remote.addrlen > sizeof(*addr)) { return 0; } - if (engine->peer_addr_ex_index < 0) - { - return 0; - } - - /* 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) - { - return 0; - } - - memset(addr, 0, sizeof(*addr)); - size_t rawlen = 0; - int family = BIO_ADDR_family(peer); - if (family == AF_INET) - { - struct sockaddr_in* sin = (struct sockaddr_in*)addr; - if (!BIO_ADDR_rawaddress(peer, &sin->sin_addr, &rawlen) || rawlen != sizeof(sin->sin_addr)) - { - return 0; - } - sin->sin_family = AF_INET; - sin->sin_port = BIO_ADDR_rawport(peer); - *addr_len = sizeof(*sin); - return 1; - } - if (family == AF_INET6) - { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr; - if (!BIO_ADDR_rawaddress(peer, &sin6->sin6_addr, &rawlen) || rawlen != sizeof(sin6->sin6_addr)) - { - return 0; - } - sin6->sin6_family = AF_INET6; - sin6->sin6_port = BIO_ADDR_rawport(peer); - *addr_len = sizeof(*sin6); - return 1; - } - 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..30433743414 100644 --- a/modules/http3/quic/h3q_conn.c +++ b/modules/http3/quic/h3q_conn.c @@ -20,65 +20,400 @@ #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) +/* CID bytes are copied into the connection pool, so the hash key outlives the callback. */ +static void cid_add(h3q_conn* conn, const ngtcp2_cid* cid) { - SSL* ssl_conn = (SSL*)conn; - if (!ssl_conn || !SSL_set_blocking_mode(ssl_conn, 0)) + apr_hash_set(conn->engine->conns, apr_pmemdup(conn->pool, cid->data, cid->datalen), (apr_ssize_t)cid->datalen, conn); + APR_ARRAY_PUSH(conn->cids, ngtcp2_cid) = *cid; +} + +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; + } + cid_add(conn, cid); + return 0; +} + +static int cb_remove_connection_id(ngtcp2_conn* qconn, const ngtcp2_cid* cid, void* user_data) +{ + (void)qconn; + h3q_conn* conn = user_data; + apr_hash_set(conn->engine->conns, cid->data, (apr_ssize_t)cid->datalen, NULL); + 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; + } + ngtcp2_crypto_ossl_ctx_set_ssl(conn->ossl_ctx, conn->ssl); + if (ngtcp2_crypto_ossl_configure_server_session(conn->ssl) != 0) { 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); + 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 struct sockaddr* peer, socklen_t peerlen) +{ + 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 (apr_pool_create(&conn->pool, engine->pool) != APR_SUCCESS || RAND_bytes(scid.data, (int)scid.datalen) != 1) + { + h3q_conn_free(conn); + return NULL; + } + conn->cids = apr_array_make(conn->pool, 4, sizeof(ngtcp2_cid)); + ngtcp2_path_storage_init(&conn->path, (const ngtcp2_sockaddr*)&engine->local, (ngtcp2_socklen)engine->local_len, (const ngtcp2_sockaddr*)peer, (ngtcp2_socklen)peerlen, NULL); + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.initial_ts = h3q_now(); + + 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.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, 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, buf, (size_t)n); + } + /* 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 +422,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; + } + conn->close_pkt = apr_palloc(conn->pool, H3Q_PKT_BUF); + 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) + { + 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, conn->close_pkt, conn->close_len); + } +} + +/* 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) { - SSL_SHUTDOWN_EX_ARGS args = {.quic_error_code = app_error, .quic_reason = reason}; - ret = SSL_shutdown_ex(ssl_conn, flags, &args, sizeof(args)); + conn->closed = 1; + return; } - else if (flags != 0) + ngtcp2_ccerr ccerr; + if (liberr == NGTCP2_ERR_CRYPTO) { - SSL_SHUTDOWN_EX_ARGS args = {0}; - ret = SSL_shutdown_ex(ssl_conn, flags, &args, sizeof(args)); + 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 struct sockaddr* peer, socklen_t peerlen) +{ + ngtcp2_tstamp now = h3q_now(); + if (now < conn->close_next) + { + return; } + conn->close_next = now + ngtcp2_conn_get_pto(conn->qconn); + ngtcp2_path path = {.remote = {.addr = (ngtcp2_sockaddr*)peer, .addrlen = (ngtcp2_socklen)peerlen}}; + h3q_send(conn->engine, &path, conn->close_pkt, conn->close_len); +} - 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) + { + return; + } + h3q_engine* engine = conn->engine; + for (h3q_conn** slot = &engine->conns_head; *slot; slot = &(*slot)->next) { - SSL_free((SSL*)conn); + 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; conn->cids && i < conn->cids->nelts; i++) + { + const ngtcp2_cid* cid = &APR_ARRAY_IDX(conn->cids, i, ngtcp2_cid); + if (apr_hash_get(engine->conns, cid->data, (apr_ssize_t)cid->datalen) == conn) + { + apr_hash_set(engine->conns, cid->data, (apr_ssize_t)cid->datalen, NULL); + } + } + 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; + } + if (conn->pool) + { + apr_pool_destroy(conn->pool); + } + free(conn); } void h3q_conn_close_reason(h3q_conn* conn, char* buf, size_t buflen) @@ -152,18 +581,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..73ee84b47bb 100644 --- a/modules/http3/quic/h3q_stream.c +++ b/modules/http3/quic/h3q_stream.c @@ -19,109 +19,236 @@ #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, 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; } - if (rv == 1 || SSL_get_error(ssl, rv) == SSL_ERROR_ZERO_RETURN) + 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) { - *fin = 1; + st->rx_off = 0; + st->rx_len = 0; + *fin = st->fin; } - return 0; + /* 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); + 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); } From b844d12f54d497453848a9a749e12f829bfc1bb4 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Thu, 8 Oct 2026 23:27:58 +0300 Subject: [PATCH 10/13] Grow flow-control windows by autotuning, capped by H3MaxWindow The upload windows were fixed at 1 MiB per connection and 256 KiB per stream, so an upload on a long, fast path stalled on flow control. Turn on ngtcp2 window autotuning: when a client fills a window within a few round trips, ngtcp2 doubles it. H3MaxWindow (default 6 MiB, the ngtcp2 example default) caps the growth, per connection and per stream. The windows still start at 1 MiB and 256 KiB. --- changes-entries/h3_flow_window_autotune.txt | 5 +++++ modules/http3/h3_config.c | 19 +++++++++++++++++++ modules/http3/h3_io.c | 1 + modules/http3/include/h3.h | 4 ++++ modules/http3/include/h3_config.h | 1 + modules/http3/include/quic/detail/h3q_impl.h | 1 + modules/http3/include/quic/h3q.h | 2 ++ modules/http3/quic/h3q.c | 1 + modules/http3/quic/h3q_conn.c | 3 +++ 9 files changed, 37 insertions(+) create mode 100644 changes-entries/h3_flow_window_autotune.txt 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/modules/http3/h3_config.c b/modules/http3/h3_config.c index 2f262fee546..ecaed9468d5 100644 --- a/modules/http3/h3_config.c +++ b/modules/http3/h3_config.c @@ -73,6 +73,7 @@ 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; @@ -199,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) @@ -687,6 +701,10 @@ int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, { 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; @@ -799,6 +817,7 @@ 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)"), diff --git a/modules/http3/h3_io.c b/modules/http3/h3_io.c index 07056c33ec3..6dcb40955f0 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -137,6 +137,7 @@ apr_status_t h3_io_listen_start(apr_pool_t* pchild, server_rec* s, h3_server_con .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, .early_data = (conf->h3_early_data == H3_FLAG_ON), .address_validation = (conf->h3_address_validation != H3_FLAG_OFF), }; 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 4c974596625..ddbeea0e009 100644 --- a/modules/http3/include/h3_config.h +++ b/modules/http3/include/h3_config.h @@ -51,6 +51,7 @@ 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; diff --git a/modules/http3/include/quic/detail/h3q_impl.h b/modules/http3/include/quic/detail/h3q_impl.h index 7b32696ce92..5edce2855b9 100644 --- a/modules/http3/include/quic/detail/h3q_impl.h +++ b/modules/http3/include/quic/detail/h3q_impl.h @@ -48,6 +48,7 @@ struct h3q_engine 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]; unsigned address_validation : 1; unsigned early_data : 1; diff --git a/modules/http3/include/quic/h3q.h b/modules/http3/include/quic/h3q.h index db44147ee55..422129ed953 100644 --- a/modules/http3/include/quic/h3q.h +++ b/modules/http3/include/quic/h3q.h @@ -43,6 +43,8 @@ typedef struct h3q_config 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; unsigned address_validation : 1; /** Accept 0-RTT request data on resumed connections. */ unsigned early_data : 1; diff --git a/modules/http3/quic/h3q.c b/modules/http3/quic/h3q.c index 2f45bf0aec5..555bf19c977 100644 --- a/modules/http3/quic/h3q.c +++ b/modules/http3/quic/h3q.c @@ -219,6 +219,7 @@ h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size 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); diff --git a/modules/http3/quic/h3q_conn.c b/modules/http3/quic/h3q_conn.c index 30433743414..9a72fb5d9ce 100644 --- a/modules/http3/quic/h3q_conn.c +++ b/modules/http3/quic/h3q_conn.c @@ -235,6 +235,9 @@ h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2 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); From e8cdb3aa4f1027791ffb0ae833b05be6cfb82e71 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 02:41:03 +0300 Subject: [PATCH 11/13] Batch QUIC packets with UDP GSO and read with UDP GRO on Linux The engine sends the packets of one flush to one peer with one sendmsg and UDP_SEGMENT. Reads use UDP_GRO, and the engine splits a joined read into datagrams. If the kernel or the device refuses GSO, the engine turns it off and sends each packet alone. A stream read now sends the new flow-control window at once. Before, the update waited for the next packet from the peer. A blocked peer sends no packet, and with GRO an upload could stop. --- changes-entries/h3_udp_gso_gro.txt | 5 + modules/http3/include/quic/detail/h3q_impl.h | 11 ++ modules/http3/quic/h3q.c | 118 ++++++++++++++++++- modules/http3/quic/h3q_conn.c | 3 + modules/http3/quic/h3q_stream.c | 4 + 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 changes-entries/h3_udp_gso_gro.txt 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/include/quic/detail/h3q_impl.h b/modules/http3/include/quic/detail/h3q_impl.h index 5edce2855b9..dd39b349aaa 100644 --- a/modules/http3/include/quic/detail/h3q_impl.h +++ b/modules/http3/include/quic/detail/h3q_impl.h @@ -32,6 +32,8 @@ #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) struct h3q_engine @@ -50,6 +52,14 @@ struct h3q_engine 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; + unsigned gso : 1; unsigned address_validation : 1; unsigned early_data : 1; }; @@ -101,6 +111,7 @@ struct h3q_stream ngtcp2_tstamp h3q_now(void); void h3q_send(h3q_engine* engine, const ngtcp2_path* path, 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 struct sockaddr* peer, socklen_t peerlen); void h3q_conn_flush(h3q_conn* conn); void h3q_conn_close(h3q_conn* conn, const ngtcp2_ccerr* ccerr); diff --git a/modules/http3/quic/h3q.c b/modules/http3/quic/h3q.c index 555bf19c977..f48c0f68fbe 100644 --- a/modules/http3/quic/h3q.c +++ b/modules/http3/quic/h3q.c @@ -33,6 +33,21 @@ #include "quic/h3q.h" #include "quic/h3q_conn.h" +#ifdef __linux__ + #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 + #ifdef _WIN32 typedef int h3q_iolen; /* winsock takes int lengths */ #else @@ -62,7 +77,95 @@ static void send_raw(h3q_engine* engine, const struct sockaddr* dst, socklen_t d void h3q_send(h3q_engine* engine, const ngtcp2_path* path, const uint8_t* buf, size_t len) { - send_raw(engine, (const struct sockaddr*)path->remote.addr, (socklen_t)path->remote.addrlen, buf, len); + const struct sockaddr* dst = (const struct sockaddr*)path->remote.addr; + socklen_t dstlen = (socklen_t)path->remote.addrlen; + if (!engine->gso) + { + send_raw(engine, dst, dstlen, buf, len); + return; + } + /* A new peer, 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 || 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_seg = len; + } + memcpy(engine->tx + engine->tx_len, buf, len); + engine->tx_len += len; + engine->tx_n++; + if (len < engine->tx_seg) + { + h3q_tx_flush(engine); /* only the last segment may be shorter */ + } +} + +void h3q_tx_flush(h3q_engine* engine) +{ + if (!engine->tx_n) + { + return; + } + const struct sockaddr* dst = (const struct sockaddr*)&engine->tx_dst; + int sent = engine->tx_n == 1; + if (sent) + { + send_raw(engine, dst, engine->tx_dstlen, engine->tx, engine->tx_len); + } +#ifdef __linux__ + else + { + char ctl[CMSG_SPACE(sizeof(uint16_t))] = {0}; + struct iovec iov = {.iov_base = engine->tx, .iov_len = engine->tx_len}; + struct msghdr m = {.msg_name = &engine->tx_dst, .msg_namelen = engine->tx_dstlen, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = ctl, .msg_controllen = sizeof(ctl)}; + struct cmsghdr* c = CMSG_FIRSTHDR(&m); + uint16_t seg = (uint16_t)engine->tx_seg; + c->cmsg_level = SOL_UDP; + c->cmsg_type = UDP_SEGMENT; + c->cmsg_len = CMSG_LEN(sizeof(seg)); + memcpy(CMSG_DATA(c), &seg, sizeof(seg)); + /* No GSO in this kernel or device: turn it off and send one by one. */ + sent = sendmsg(engine->fd, &m, 0) >= 0 || (errno != EIO && errno != EINVAL && errno != ENOPROTOOPT); + engine->gso = sent ? 1u : 0u; + } +#endif + for (size_t off = 0; !sent && off < engine->tx_len; off += engine->tx_seg) + { + size_t n = engine->tx_len - off < engine->tx_seg ? engine->tx_len - off : engine->tx_seg; + send_raw(engine, dst, engine->tx_dstlen, engine->tx + off, n); + } + 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. */ +static int recv_dgram(h3q_engine* engine, uint8_t* buf, size_t cap, struct sockaddr_storage* peer, socklen_t* peerlen, size_t* seg) +{ +#ifdef __linux__ + char ctl[CMSG_SPACE(sizeof(int))]; + 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, .msg_controllen = sizeof(ctl)}; + 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)) + { + int gro = 0; + if (c->cmsg_level == SOL_UDP && c->cmsg_type == UDP_GRO && (memcpy(&gro, CMSG_DATA(c), sizeof(gro)), gro > 0)) + { + *seg = (size_t)gro; + } + } + return n; +#else + 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 } /* A peer that offers an unknown version is told which versions we speak. */ @@ -230,6 +333,12 @@ h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size 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 */ + engine->tx = apr_palloc(engine->pool, H3Q_GSO_BYTES); + engine->gso = 1; +#endif return engine; } @@ -263,7 +372,8 @@ int h3q_engine_pump(h3q_engine* engine) { struct sockaddr_storage peer; socklen_t peerlen = (socklen_t)sizeof(peer); - int n = (int)recvfrom(engine->fd, (char*)buf, (h3q_iolen)sizeof(buf), 0, (struct sockaddr*)&peer, &peerlen); + size_t seg = 0; + int n = recv_dgram(engine, buf, sizeof(buf), &peer, &peerlen, &seg); if (n < 0) { int why = recv_failed(); @@ -278,9 +388,9 @@ int h3q_engine_pump(h3q_engine* engine) continue; } work = 1; - if (n > 0) + for (size_t off = 0; off < (size_t)n; off += seg) { - process_dgram(engine, buf, (size_t)n, &peer, peerlen); + process_dgram(engine, buf + off, (size_t)n - off < seg ? (size_t)n - off : seg, &peer, peerlen); } } ngtcp2_tstamp now = h3q_now(); diff --git a/modules/http3/quic/h3q_conn.c b/modules/http3/quic/h3q_conn.c index 9a72fb5d9ce..fa0488cdc5b 100644 --- a/modules/http3/quic/h3q_conn.c +++ b/modules/http3/quic/h3q_conn.c @@ -360,6 +360,7 @@ void h3q_conn_flush(h3q_conn* conn) } h3q_send(conn->engine, &ps.path, 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()); } @@ -459,6 +460,7 @@ void h3q_conn_close(h3q_conn* conn, const ngtcp2_ccerr* ccerr) conn->close_until = h3q_now() + 3 * pto; conn->close_next = h3q_now() + pto; h3q_send(conn->engine, &ps.path, conn->close_pkt, conn->close_len); + h3q_tx_flush(conn->engine); } } @@ -498,6 +500,7 @@ void h3q_conn_resend_close(h3q_conn* conn, const struct sockaddr* peer, socklen_ conn->close_next = now + ngtcp2_conn_get_pto(conn->qconn); ngtcp2_path path = {.remote = {.addr = (ngtcp2_sockaddr*)peer, .addrlen = (ngtcp2_socklen)peerlen}}; h3q_send(conn->engine, &path, conn->close_pkt, conn->close_len); + h3q_tx_flush(conn->engine); } int h3q_conn_shutdown(h3q_conn* conn, int is_rapid, uint64_t app_error, const char* reason) diff --git a/modules/http3/quic/h3q_stream.c b/modules/http3/quic/h3q_stream.c index 73ee84b47bb..0dd6cf34626 100644 --- a/modules/http3/quic/h3q_stream.c +++ b/modules/http3/quic/h3q_stream.c @@ -186,6 +186,10 @@ int h3q_stream_read(h3q_stream* st, unsigned char* buf, size_t read_size, size_t /* 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) + { + h3q_conn_flush(st->conn); /* Send the window now. A blocked peer sends nothing that causes a flush. */ + } return 1; } From 7637b643d48d65e464bb4342ae5ce8f46263d6f9 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Thu, 5 Nov 2026 11:21:45 +0200 Subject: [PATCH 12/13] Reply from the right address, ECN, one token key, no migration across buckets On Linux the [::] socket now reads the address each packet came to (IP_PKTINFO, IPV6_PKTINFO) and sends the reply from it. Before, the kernel picked the source, and a host with more than one address could reply from another one, which a client drops. ECN: the ECN mark of each packet goes to ngtcp2, which reports it in ACK_ECN frames, and the mark ngtcp2 asks for goes on each sent packet. The parent makes the key for Retry and stateless reset tokens and keeps it across graceful restarts, so a token from one child is good in all. With more than one SO_REUSEPORT socket the server sends disable_active_migration. --- changes-entries/h3_pktinfo_ecn_token_key.txt | 21 ++ modules/http3/h3_io.c | 3 + modules/http3/h3_server.c | 30 +++ modules/http3/include/h3_server.h | 12 + modules/http3/include/quic/detail/h3q_impl.h | 11 +- modules/http3/include/quic/h3q.h | 4 + modules/http3/quic/h3q.c | 264 +++++++++++++++---- modules/http3/quic/h3q_conn.c | 16 +- modules/http3/quic/h3q_stream.c | 2 +- 9 files changed, 298 insertions(+), 65 deletions(-) create mode 100644 changes-entries/h3_pktinfo_ecn_token_key.txt 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/modules/http3/h3_io.c b/modules/http3/h3_io.c index 6dcb40955f0..47466fa30a8 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -36,6 +36,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" @@ -138,8 +139,10 @@ apr_status_t h3_io_listen_start(apr_pool_t* pchild, server_rec* s, h3_server_con .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), + .disable_migration = h3_server_buckets(s) > 1, }; io->qengine = h3q_engine_create(&qcfg, udp_fd, qerr, sizeof(qerr)); if (!io->qengine) diff --git a/modules/http3/h3_server.c b/modules/http3/h3_server.c index 1ac3ed07689..96ef424803f 100644 --- a/modules/http3/h3_server.c +++ b/modules/http3/h3_server.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -234,8 +235,37 @@ void h3_child_init(apr_pool_t* pchild, server_rec* s) } } +#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) diff --git a/modules/http3/include/h3_server.h b/modules/http3/include/h3_server.h index 2eb747a039c..7dfab19f641 100644 --- a/modules/http3/include/h3_server.h +++ b/modules/http3/include/h3_server.h @@ -40,6 +40,18 @@ void h3_child_init(apr_pool_t* pchild, server_rec* s); */ 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/quic/detail/h3q_impl.h b/modules/http3/include/quic/detail/h3q_impl.h index dd39b349aaa..809725a0eee 100644 --- a/modules/http3/include/quic/detail/h3q_impl.h +++ b/modules/http3/include/quic/detail/h3q_impl.h @@ -59,9 +59,14 @@ struct h3q_engine 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 @@ -110,13 +115,13 @@ struct h3q_stream }; ngtcp2_tstamp h3q_now(void); -void h3q_send(h3q_engine* engine, const ngtcp2_path* path, const uint8_t* buf, size_t len); +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 struct sockaddr* peer, socklen_t peerlen); +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 struct sockaddr* peer, socklen_t peerlen); +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/h3q.h b/modules/http3/include/quic/h3q.h index 422129ed953..958690333d8 100644 --- a/modules/http3/include/quic/h3q.h +++ b/modules/http3/include/quic/h3q.h @@ -45,9 +45,13 @@ typedef struct h3q_config 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; /** 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; /** diff --git a/modules/http3/quic/h3q.c b/modules/http3/quic/h3q.c index f48c0f68fbe..72e0cf473c6 100644 --- a/modules/http3/quic/h3q.c +++ b/modules/http3/quic/h3q.c @@ -14,6 +14,10 @@ * limitations under the License. */ +#if defined(__linux__) && !defined(_GNU_SOURCE) + #define _GNU_SOURCE /* struct in6_pktinfo */ +#endif + #ifdef _WIN32 #include #include @@ -34,6 +38,7 @@ #include "quic/h3q_conn.h" #ifdef __linux__ + #include #include #include /* Kernel ABI values, for libc headers older than GSO and GRO; old kernels fall back at run time. */ @@ -69,23 +74,114 @@ ngtcp2_tstamp h3q_now(void) #endif } -static void send_raw(h3q_engine* engine, const struct sockaddr* dst, socklen_t dstlen, const uint8_t* buf, size_t len) +#ifdef __linux__ +static int is_wildcard(const struct sockaddr_storage* a) +{ + if (a->ss_family == AF_INET6) + { + return IN6_IS_ADDR_UNSPECIFIED(&((const struct sockaddr_in6*)a)->sin6_addr); + } + return a->ss_family == AF_INET && ((const struct sockaddr_in*)a)->sin_addr.s_addr == htonl(INADDR_ANY); +} + +/* 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 + { + 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)); + } + else if (a6) + { + 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)); + } + if (seg) + { + 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)); + } + m.msg_controllen = used; + if (!used) + { + m.msg_control = NULL; + } + return sendmsg(engine->fd, &m, 0); +} + +/* 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) { - /* A failed send is a lost datagram; loss recovery resends it. */ + int tos = (int)ecn; + if (tos == engine->sock_ecn) + { + return; + } + engine->sock_ecn = tos; + if (engine->local.ss_family == AF_INET6) + { + (void)setsockopt(engine->fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + } + (void)setsockopt(engine->fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); +} +#endif + +/* 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) + { + (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 } -void h3q_send(h3q_engine* engine, const ngtcp2_path* path, const uint8_t* buf, size_t len) +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; - if (!engine->gso) + struct sockaddr_storage src; + memset(&src, 0, sizeof(src)); + if (engine->pktinfo && path->local.addrlen && path->local.addrlen <= sizeof(src)) { - send_raw(engine, dst, dstlen, buf, len); - return; + memcpy(&src, path->local.addr, path->local.addrlen); } - /* A new peer, 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 || len > engine->tx_seg || engine->tx_len + len > H3Q_GSO_BYTES || engine->tx_n == H3Q_GSO_SEGS)) + /* 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); } @@ -93,12 +189,14 @@ void h3q_send(h3q_engine* engine, const ngtcp2_path* path, const uint8_t* buf, s { 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 (len < engine->tx_seg) + if (!engine->gso || len < engine->tx_seg) { h3q_tx_flush(engine); /* only the last segment may be shorter */ } @@ -111,57 +209,96 @@ void h3q_tx_flush(h3q_engine* engine) return; } 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) { - send_raw(engine, dst, engine->tx_dstlen, engine->tx, engine->tx_len); + send_one(engine, dst, engine->tx_dstlen, src, engine->tx, engine->tx_len); } #ifdef __linux__ else { - char ctl[CMSG_SPACE(sizeof(uint16_t))] = {0}; - struct iovec iov = {.iov_base = engine->tx, .iov_len = engine->tx_len}; - struct msghdr m = {.msg_name = &engine->tx_dst, .msg_namelen = engine->tx_dstlen, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = ctl, .msg_controllen = sizeof(ctl)}; - struct cmsghdr* c = CMSG_FIRSTHDR(&m); - uint16_t seg = (uint16_t)engine->tx_seg; - c->cmsg_level = SOL_UDP; - c->cmsg_type = UDP_SEGMENT; - c->cmsg_len = CMSG_LEN(sizeof(seg)); - memcpy(CMSG_DATA(c), &seg, sizeof(seg)); /* No GSO in this kernel or device: turn it off and send one by one. */ - sent = sendmsg(engine->fd, &m, 0) >= 0 || (errno != EIO && errno != EINVAL && errno != ENOPROTOOPT); + 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; } #endif for (size_t off = 0; !sent && off < engine->tx_len; off += engine->tx_seg) { size_t n = engine->tx_len - off < engine->tx_seg ? engine->tx_len - off : engine->tx_seg; - send_raw(engine, dst, engine->tx_dstlen, engine->tx + off, n); + send_one(engine, dst, engine->tx_dstlen, src, engine->tx + off, n); } 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. */ -static int recv_dgram(h3q_engine* engine, uint8_t* buf, size_t cap, struct sockaddr_storage* peer, socklen_t* peerlen, size_t* seg) +/* 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__ - char ctl[CMSG_SPACE(sizeof(int))]; + union + { + 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, .msg_controllen = sizeof(ctl)}; + 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)) { - int gro = 0; - if (c->cmsg_level == SOL_UDP && c->cmsg_type == UDP_GRO && (memcpy(&gro, CMSG_DATA(c), sizeof(gro)), gro > 0)) + 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) { - *seg = (size_t)gro; + *ecn = (uint32_t)*(const uint8_t*)CMSG_DATA(c) & 0x3; } } 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; @@ -169,7 +306,7 @@ static int recv_dgram(h3q_engine* engine, uint8_t* buf, size_t cap, struct socka } /* 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 struct sockaddr* peer, socklen_t peerlen) +static void send_version_negotiation(h3q_engine* engine, const ngtcp2_version_cid* vc, const ngtcp2_path* path) { static const uint32_t versions[] = {NGTCP2_PROTO_VER_V1, NGTCP2_PROTO_VER_V2}; uint8_t rnd = 0; @@ -181,11 +318,12 @@ static void send_version_negotiation(h3q_engine* engine, const ngtcp2_version_ci 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) { - send_raw(engine, peer, peerlen, buf, (size_t)n); + h3q_send(engine, path, NGTCP2_ECN_NOT_ECT, buf, (size_t)n); + h3q_tx_flush(engine); } } -static void send_retry(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const struct sockaddr* peer, socklen_t peerlen) +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]; @@ -194,7 +332,7 @@ static void send_retry(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const struct { return; } - ngtcp2_ssize tokenlen = ngtcp2_crypto_generate_retry_token2(token, engine->secret, sizeof(engine->secret), hd->version, (const ngtcp2_sockaddr*)peer, (ngtcp2_socklen)peerlen, &scid, &hd->dcid, h3q_now()); + 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; @@ -202,11 +340,12 @@ static void send_retry(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const struct ngtcp2_ssize n = ngtcp2_crypto_write_retry(buf, sizeof(buf), hd->version, &hd->scid, &scid, &hd->dcid, token, (size_t)tokenlen); if (n > 0) { - send_raw(engine, peer, peerlen, buf, (size_t)n); + 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 struct sockaddr* peer, socklen_t peerlen) +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) @@ -215,18 +354,18 @@ static h3q_conn* accept_initial(h3q_engine* engine, const uint8_t* pkt, size_t p } if (!engine->address_validation) { - return h3q_conn_new(engine, &hd, NULL, NULL, peer, peerlen); + return h3q_conn_new(engine, &hd, NULL, NULL, path); } 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, (const ngtcp2_sockaddr*)peer, (ngtcp2_socklen)peerlen, &hd.dcid, H3Q_RETRY_TOKEN_TIMEOUT, h3q_now()) != 0) + 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, peer, peerlen); + send_retry(engine, &hd, path); return NULL; } - return h3q_conn_new(engine, &hd, &odcid, &hd.dcid, peer, peerlen); + return h3q_conn_new(engine, &hd, &odcid, &hd.dcid, path); } -static void process_dgram(h3q_engine* engine, uint8_t* buf, size_t len, struct sockaddr_storage* peer, socklen_t peerlen) +static void process_dgram(h3q_engine* engine, uint8_t* buf, size_t len, const ngtcp2_path* path, uint32_t ecn) { ngtcp2_version_cid vc; int rv = ngtcp2_pkt_decode_version_cid(&vc, buf, len, H3Q_SCIDLEN); @@ -234,30 +373,26 @@ static void process_dgram(h3q_engine* engine, uint8_t* buf, size_t len, struct s { if (rv == NGTCP2_ERR_VERSION_NEGOTIATION) { - send_version_negotiation(engine, &vc, (struct sockaddr*)peer, peerlen); + send_version_negotiation(engine, &vc, path); } return; } h3q_conn* conn = apr_hash_get(engine->conns, vc.dcid, (apr_ssize_t)vc.dcidlen); if (!conn) { - conn = accept_initial(engine, buf, len, (struct sockaddr*)peer, peerlen); + conn = accept_initial(engine, buf, len, path); } if (conn && conn->close_len) { - h3q_conn_resend_close(conn, (struct sockaddr*)peer, peerlen); + h3q_conn_resend_close(conn, path); return; } if (!conn || conn->closed) { return; } - ngtcp2_path path = { - .local = {.addr = (ngtcp2_sockaddr*)&engine->local, .addrlen = (ngtcp2_socklen)engine->local_len}, - .remote = {.addr = (ngtcp2_sockaddr*)peer, .addrlen = (ngtcp2_socklen)peerlen}, - }; - ngtcp2_pkt_info pi = {0}; - rv = ngtcp2_conn_read_pkt(conn->qconn, &path, &pi, buf, len, h3q_now()); + 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: @@ -268,7 +403,7 @@ static void process_dgram(h3q_engine* engine, uint8_t* buf, size_t len, struct s ngtcp2_pkt_hd hd; if (ngtcp2_accept(&hd, buf, len) == 0) { - send_retry(engine, &hd, (struct sockaddr*)peer, peerlen); + send_retry(engine, &hd, path); } return; } @@ -326,7 +461,13 @@ h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size engine->address_validation = cfg->address_validation; engine->early_data = cfg->early_data; engine->local_len = (socklen_t)sizeof(engine->local); - if (getsockname(udp_fd, (struct sockaddr*)&engine->local, &engine->local_len) != 0 || RAND_bytes(engine->secret, (int)sizeof(engine->secret)) != 1 || !SSL_CTX_up_ref(cfg->ssl_ctx)) + 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); @@ -336,9 +477,19 @@ h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size #ifdef __linux__ int on = 1; (void)setsockopt(udp_fd, SOL_UDP, UDP_GRO, &on, sizeof(on)); /* GRO is optional */ - engine->tx = apr_palloc(engine->pool, H3Q_GSO_BYTES); + /* 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; } @@ -372,8 +523,11 @@ int h3q_engine_pump(h3q_engine* engine) { 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; - int n = recv_dgram(engine, buf, sizeof(buf), &peer, &peerlen, &seg); + 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(); @@ -388,9 +542,13 @@ int h3q_engine_pump(h3q_engine* engine) 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) { - process_dgram(engine, buf + off, (size_t)n - off < seg ? (size_t)n - off : seg, &peer, peerlen); + process_dgram(engine, buf + off, (size_t)n - off < seg ? (size_t)n - off : seg, &path, ecn); } } ngtcp2_tstamp now = h3q_now(); diff --git a/modules/http3/quic/h3q_conn.c b/modules/http3/quic/h3q_conn.c index fa0488cdc5b..4741ed8eb52 100644 --- a/modules/http3/quic/h3q_conn.c +++ b/modules/http3/quic/h3q_conn.c @@ -213,7 +213,7 @@ static int tls_init(h3q_conn* conn) 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 struct sockaddr* peer, socklen_t peerlen) +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) @@ -230,7 +230,7 @@ h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2 return NULL; } conn->cids = apr_array_make(conn->pool, 4, sizeof(ngtcp2_cid)); - ngtcp2_path_storage_init(&conn->path, (const ngtcp2_sockaddr*)&engine->local, (ngtcp2_socklen)engine->local_len, (const ngtcp2_sockaddr*)peer, (ngtcp2_socklen)peerlen, 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); @@ -255,6 +255,7 @@ h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2 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 = { @@ -326,7 +327,7 @@ static void retry_pending_fins(h3q_conn* conn) { continue; /* congestion limited; the next expiry retries */ } - h3q_send(conn->engine, &ps.path, buf, (size_t)n); + h3q_send(conn->engine, &ps.path, pi.ecn, buf, (size_t)n); if (ndatalen >= 0) { st->fin_pending = 0; @@ -358,7 +359,7 @@ void h3q_conn_flush(h3q_conn* conn) { break; } - h3q_send(conn->engine, &ps.path, buf, (size_t)n); + 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. */ @@ -459,7 +460,7 @@ void h3q_conn_close(h3q_conn* conn, const ngtcp2_ccerr* ccerr) 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, conn->close_pkt, conn->close_len); + h3q_send(conn->engine, &ps.path, pi.ecn, conn->close_pkt, conn->close_len); h3q_tx_flush(conn->engine); } } @@ -490,7 +491,7 @@ void h3q_conn_fail(h3q_conn* conn, int liberr) } /* Closing period: answer the peer with the stored close, at most once per PTO. */ -void h3q_conn_resend_close(h3q_conn* conn, const struct sockaddr* peer, socklen_t peerlen) +void h3q_conn_resend_close(h3q_conn* conn, const ngtcp2_path* path) { ngtcp2_tstamp now = h3q_now(); if (now < conn->close_next) @@ -498,8 +499,7 @@ void h3q_conn_resend_close(h3q_conn* conn, const struct sockaddr* peer, socklen_ return; } conn->close_next = now + ngtcp2_conn_get_pto(conn->qconn); - ngtcp2_path path = {.remote = {.addr = (ngtcp2_sockaddr*)peer, .addrlen = (ngtcp2_socklen)peerlen}}; - h3q_send(conn->engine, &path, conn->close_pkt, conn->close_len); + h3q_send(conn->engine, path, NGTCP2_ECN_NOT_ECT, conn->close_pkt, conn->close_len); h3q_tx_flush(conn->engine); } diff --git a/modules/http3/quic/h3q_stream.c b/modules/http3/quic/h3q_stream.c index 0dd6cf34626..2a374eda3c4 100644 --- a/modules/http3/quic/h3q_stream.c +++ b/modules/http3/quic/h3q_stream.c @@ -103,7 +103,7 @@ h3q_write_result h3q_stream_write(h3q_stream* st, const h3q_vec* vec, size_t nve } if (n > 0) { - h3q_send(conn->engine, &ps.path, buf, (size_t)n); + h3q_send(conn->engine, &ps.path, pi.ecn, buf, (size_t)n); } if (ndatalen > 0) { From c57f897e22c02cf6db26241af1a57183f3178487 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 30 Oct 2026 17:29:26 +0200 Subject: [PATCH 13/13] Less memory per connection, safe CID map The QUIC connection keeps its connection IDs in 16 slots of its own, not in an APR pool of 8 KB or more. It makes the CONNECTION_CLOSE buffer only when it closes. The session and stream allocators keep at most MaxMemFree, as in mod_http2. cid_add does not add a CID that another connection has. Before, the map could keep a key that pointed into the pool of a freed connection. --- changes-entries/h3_memory_cid_map.txt | 11 ++++ modules/http3/h3_io.c | 2 + modules/http3/h3_request.c | 2 + modules/http3/include/quic/detail/h3q_impl.h | 6 +- modules/http3/quic/h3q_conn.c | 61 ++++++++++++++------ 5 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 changes-entries/h3_memory_cid_map.txt 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/modules/http3/h3_io.c b/modules/http3/h3_io.c index 47466fa30a8..e43c108f0bd 100644 --- a/modules/http3/h3_io.c +++ b/modules/http3/h3_io.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -276,6 +277,7 @@ 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; 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/include/quic/detail/h3q_impl.h b/modules/http3/include/quic/detail/h3q_impl.h index 809725a0eee..28538bc5e06 100644 --- a/modules/http3/include/quic/detail/h3q_impl.h +++ b/modules/http3/include/quic/detail/h3q_impl.h @@ -35,6 +35,7 @@ #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 { @@ -72,14 +73,13 @@ struct h3q_engine struct h3q_conn { h3q_engine* engine; - apr_pool_t* pool; /* CID hash keys */ ngtcp2_conn* qconn; ngtcp2_crypto_conn_ref conn_ref; ngtcp2_crypto_ossl_ctx* ossl_ctx; SSL* ssl; ngtcp2_path_storage path; - apr_array_header_t* cids; /* every CID put in the engine map, to remove on free */ - uint8_t* close_pkt; /* CONNECTION_CLOSE, resent while closing (RFC 9000 10.2.1) */ + 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; diff --git a/modules/http3/quic/h3q_conn.c b/modules/http3/quic/h3q_conn.c index 4741ed8eb52..c4961375449 100644 --- a/modules/http3/quic/h3q_conn.c +++ b/modules/http3/quic/h3q_conn.c @@ -30,11 +30,33 @@ #include "quic/detail/h3q_impl.h" #include "quic/h3q_conn.h" -/* CID bytes are copied into the connection pool, so the hash key outlives the callback. */ -static void cid_add(h3q_conn* conn, const ngtcp2_cid* cid) +/* 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) { - apr_hash_set(conn->engine->conns, apr_pmemdup(conn->pool, cid->data, cid->datalen), (apr_ssize_t)cid->datalen, conn); - APR_ARRAY_PUSH(conn->cids, ngtcp2_cid) = *cid; + 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) @@ -173,15 +195,20 @@ static int cb_get_new_connection_id(ngtcp2_conn* qconn, ngtcp2_cid* cid, uint8_t { return NGTCP2_ERR_CALLBACK_FAILURE; } - cid_add(conn, cid); - return 0; + 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; - apr_hash_set(conn->engine->conns, cid->data, (apr_ssize_t)cid->datalen, NULL); + 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; } @@ -224,12 +251,11 @@ h3q_conn* h3q_conn_new(h3q_engine* engine, const ngtcp2_pkt_hd* hd, const ngtcp2 conn->next = engine->conns_head; engine->conns_head = conn; ngtcp2_cid scid = {.datalen = H3Q_SCIDLEN}; - if (apr_pool_create(&conn->pool, engine->pool) != APR_SUCCESS || RAND_bytes(scid.data, (int)scid.datalen) != 1) + if (RAND_bytes(scid.data, (int)scid.datalen) != 1) { h3q_conn_free(conn); return NULL; } - conn->cids = apr_array_make(conn->pool, 4, sizeof(ngtcp2_cid)); ngtcp2_path_storage_init(&conn->path, path->local.addr, path->local.addrlen, path->remote.addr, path->remote.addrlen, NULL); ngtcp2_settings settings; @@ -449,7 +475,10 @@ void h3q_conn_close(h3q_conn* conn, const ngtcp2_ccerr* ccerr) { return; } - conn->close_pkt = apr_palloc(conn->pool, H3Q_PKT_BUF); + if (!(conn->close_pkt = malloc(H3Q_PKT_BUF))) + { + return; + } ngtcp2_path_storage ps; ngtcp2_pkt_info pi; ngtcp2_path_storage_zero(&ps); @@ -546,12 +575,11 @@ void h3q_conn_free(h3q_conn* conn) break; } } - for (int i = 0; conn->cids && i < conn->cids->nelts; i++) + for (int i = 0; i < H3Q_CID_SLOTS; i++) { - const ngtcp2_cid* cid = &APR_ARRAY_IDX(conn->cids, i, ngtcp2_cid); - if (apr_hash_get(engine->conns, cid->data, (apr_ssize_t)cid->datalen) == conn) + if (conn->cids[i].datalen) { - apr_hash_set(engine->conns, cid->data, (apr_ssize_t)cid->datalen, NULL); + cid_remove(conn, &conn->cids[i]); } } if (conn->qconn) @@ -574,10 +602,7 @@ void h3q_conn_free(h3q_conn* conn) free(conn->streams_head); conn->streams_head = next; } - if (conn->pool) - { - apr_pool_destroy(conn->pool); - } + free(conn->close_pkt); free(conn); }