Repository navigation
feature: add the server_name_resolver_by_lua_block directive. - #2548
Draft
climagabriel wants to merge 2 commits into
Draft
climagabriel wants to merge 2 commits into
climagabriel wants to merge 2 commits into
Conversation
The Lua chunk maps a host name that matches no exact server_name to an
exact server_name while nginx selects the server{} block, for the TLS SNI
name and for the request host. It runs in a new non-yielding context,
NGX_HTTP_LUA_CONTEXT_SERVER_NAME_RESOLVER.
The directive needs the server_name_resolver hook in
ngx_http_core_main_conf_t, which is proposed for the nginx core and is not
in any nginx release. The config script detects the hook; without it the
directive is rejected when the configuration is loaded.
This was referenced Nov 12, 2026
… from being reused. ngx_get_connection() for the fake connection may reuse connections when the worker runs out of them. A QUIC connection is reusable during its handshake, and the chunk runs in that handshake's SNI callback, so the connection being served could be closed under its own callback. The handler now clears c->reusable around ngx_http_lua_create_fake_connection(). On HTTP/2 "c" is the stream's copy of the client connection, and ngx_reusable_connection() on a copy with the reusable flag set corrupts the reusable connections queue. The config script therefore enables the directive only when the nginx core has both the server_name_resolver hook and "fc->reusable = 0" in ngx_http_v2_create_stream().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
server_name_resolver_by_lua_block. Duringserver {}selection, when the host name (TLS SNI,Hostor:authority) matches no exactserver_name, a Lua chunk can name theserver {}block to select. The request's host is not changed.The chunk runs before wildcard and regex matching, for every host that has no exact
server_name, on every request (see the TLS exception below). It cannot yield.Draft. Requires both commits of nginx/nginx#1972, which is not merged. The first adds a
server_name_resolvercallback tongx_http_core_main_conf_t, called after the exact-name lookup misses and before wildcard names. The second setsfc->reusable = 0on HTTP/2 stream connections. If that PR is rejected, this one is void. Without openresty/lua-resty-core#549 (lua-resty-core),ngx.get_phase()raisesunknown phase: 524288in the chunk (0x00080000, the new context bit); with it, it returns"server_name_resolver". The tests here do not call it.The directive:
httpcontext, one per configuration, block form only...., in lower case, and returns a server name. The answer is lower-cased and must be an exactserver_nameon the listening address of the connection.server_name.Hostequals the SNI name. Tested here: the case where it does not run again. The other cases are tested at the callback level in HTTP: added a server name resolver hook for modules nginx/nginx#1972.The implementation:
configsearchessrc/http/ngx_http_core_module.hfor the textserver_name_resolverandsrc/http/v2/ngx_http_v2.cfor the textfc->reusable = 0;, and definesNGX_HTTP_LUA_HAVE_SERVER_NAME_RESOLVERwhen it finds both. On an nginx without either, the module builds, and the directive is rejected with an[emerg]message when the configuration is loaded.NGX_HTTP_LUA_CONTEXT_SERVER_NAME_RESOLVER(0x00080000). No existing API lists that context, so APIs guarded byngx_http_lua_check_context()should fail withAPI disabled in the context of server_name_resolver_by_lua*. Tested:ngx.sleepis refused;ngx.logandngx.shared.DICT:getwork.ngx_http_lua_log_by_chunk().ngx_http_lua_set_by_chunk()and the other shared runners are not changed.c->reusable:c->reusable, callsngx_reusable_connection(c, 0)beforengx_http_lua_create_fake_connection(), which callsngx_get_connection(), and callsngx_reusable_connection(c, saved)when it returns, before the chunk runs.ngx_get_connection()then cannot reuse the connection being served.worker_connections are not enough, reusing connectionsbefore the exit and from the calls removing the crash.c) is the stream'sngx_connection_t, a struct copy of the client connection. With the second nginx commit the copy hasreusable = 0, and both calls do nothing. The client connection stays in the reusable queue while the handler runs. Not tested: HTTP/2 with the worker out of connections at that moment. By code reading nginx then sends GOAWAY on the client connection, because the stream is already counted inh2c->processing.reusable = 1. The first call unlinks through the copiedqueuelinks, which removes the client connection from the queue while its flag stays set. The second inserts the copy, and the copy is freed with the HTTP/2 connection's pool while linked.clies insidengx_cycle->connections.Other open points:
_filevariant..ttest covers the HTTP/2 or the HTTP/3 crash. Both clients are Python scripts, in the collapsed section below.ngx.var,ngx.ctx,ngx.req.*) are not tested on the fake request.t/199-server-name-resolver.tskips when the nginx binary contains the textrequires nginx with the server_name_resolver hook, which is compiled in only whenconfigdid not find both nginx changes. If they reach an nginx release, a version check should replace that, and the two text searches inconfig.Testing, on nginx 1.31.8 built by
util/build.sh(this repository's CI configure flags, including the no-pool patch), OpenSSL 3.5.9. Pass means the assertions hold and no worker exits on a signal; nothing was run under valgrind or ASan.t/199-server-name-resolver.tpasses (60 assertions). It covers an exact name (chunk not run), a mapped name, a mapped name that a wildcard also matches, an unmapped name under the wildcard, an unknown name, answers that are not an exact name or not a string, lower-casing, a shared dict lookup, a Lua error,ngx.sleep(refused), TLS with the SNI name mapped, and a duplicate directive. The position of regex names in the selection order is tested in HTTP: added a server name resolver hook for modules nginx/nginx#1972, not here.t/001-set.t,t/086-init-by.t,t/124-init-worker.t,t/162-exit-worker.t,t/166-ssl-client-hello.tandt/062-count.tpass. I ran these 6 of the existing test files, not the whole suite.h2_split_headers.pybelow,worker_connections 32. Sequence: open an idle HTTP/1 connection; send aHEADERSframe with a mapped:authorityin two writes, to a location that sleeps one second; meanwhile send a request on the HTTP/1 connection; after the HTTP/2 response, open 40 idle connections, and the worker logsworker_connections are not enough, reusing connections. With both nginx commits: the response comes from the mapped server and no worker exits on a signal (2 runs). With the first nginx commit only: the worker exits on SIGSEGV (2 of 2 runs). With the frame sent in one write: no crash either way. The HTTP/1 steps are in the sequence by code reading; I did not run it without them.quic_draining_worker.pybelow,worker_connections 16, both nginx commits. Sequence: hold N half-sent HTTP/1 requests; send an HTTP/3 request for a mapped name; release the held requests; send a second HTTP/3 request. Without the two calls, crashes per runs by N: 8: 2/2; 9: 3/3; 10: 3/3; 11: 1/2; 12: 1/2; 13: 1/1; 14: 1/1. With the two calls: none in the same 14 runs; the first HTTP/3 request times out at the client while nginx logsworker_connections are not enough while handling frames, and the second returns 200 from the mapped server. With N of 0, 6 or 7 nothing crashes in one run each, with or without the calls.t/199-server-name-resolver.tis skipped, andnginx -trejects the directive withserver_name_resolver_by_lua_block requires nginx with the server_name_resolver hook in ngx_http_core_main_conf_t and with the reusable flag cleared in HTTP/2 stream connections. An nginx with neither change takes the same code path; I ran that build before the message was extended.HTTP/2 and HTTP/3 clients and their nginx configurations
python3 h2_split_headers.pysends the frame in two writes;python3 h2_split_headers.py wholesends it in one.quic_draining_worker.py; it needs aioquic and takes the counts of held requests as arguments:quic_draining_worker.conf:I hereby granted the copyright of the changes in this pull request
to the authors of this lua-nginx-module project.