Strip socket options on cross-cscf repeat listens (v0.7.2)

Make the shared-listen-include pattern work with `reuseport` and the
other socket-level listen options. Nginx core enforces at-most-once
per sockaddr on options that set lsopt.set=1 (reuseport, bind,
backlog=, rcvbuf=, sndbuf=, setfib=, fastopen=, accept_filter=,
deferred, ipv6only=, so_keepalive=) and emits "duplicate listen
options for <addr>" otherwise. That rule collides with a single
listens.conf included from every vhost — each vhost's include
re-submits the same options.

The listen wrapper now detects the cross-cscf case, strips those
options from cf->args before delegating to the core handler, and
logs one notice per stripped listen. The first cscf owns the
options on the kernel socket; later cscfs merge cleanly via
ngx_http_add_server. Protocol-level flags (ssl, http2, quic,
proxy_protocol) pass through untouched since nginx OR-merges those
across cscfs.

This unblocks `reuseport` for deployments that want better
new-connection spread across workers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 16:23:58 +02:00
parent badb684431
commit 7ed77f5b22
8 changed files with 168 additions and 18 deletions

View File

@@ -31,14 +31,16 @@ Module loads
Shared-listen-include across multiple server blocks
[Documentation] Three server blocks all pull in the same
... ipng-listens.inc (see docs/user-guide.md). nginx
... must start without "conflicting server name" or
... "duplicate listen options" warnings, and the
... module must end up with exactly one listening
... socket per address family on port 8080 (one for
... v4 wildcard, one for v6) — not one per (server
... block × device × family), which would exhaust
... the fd table on a real host.
... ipng-listens.inc (see docs/user-guide.md). The
... include also carries `reuseport` on every listen
... — nginx core would normally reject the second
... server block with "duplicate listen options", but
... the wrapper strips socket-level options on a
... repeat (cross-cscf) sockaddr so the first cscf
... owns the reuseport-cloned socket and the rest
... merge cleanly. With worker_processes unset
... (default 1), reuseport produces one socket per
... (worker × family), i.e. 2 on :8080 here.
${output} = Docker Exec ${SERVER} nginx -t 2>&1
Should Not Contain ${output} conflicting server name
Should Not Contain ${output} duplicate listen
@@ -46,6 +48,10 @@ Shared-listen-include across multiple server blocks
${count} = Get Regexp Matches ${listens} :8080\\s
Length Should Be ${count} 2
... Expected 2 listening sockets on port 8080 (v4+v6 wildcards); got ${count}
# Proves the cross-cscf option-stripping path actually fired for
# the 2nd and 3rd server blocks. `nginx -t` replays the whole
# config and emits the wrapper's NOTICE each time it strips.
Should Contain ${output} stripped socket options from duplicate listen
Prometheus scrape
[Documentation] Scrape returns HELP/TYPE preamble.

View File

@@ -5,8 +5,15 @@
# file from multiple server blocks to exercise the wrapper's dedup
# logic: a naive implementation would either error with "duplicate
# listen options" or create N * (devices × families) sockets.
#
# `reuseport` is present on every listen to exercise the wrapper's
# cross-cscf option-stripping path: nginx itself would reject the
# second server block's include with "duplicate listen options" if
# reuseport weren't stripped from the repeat calls. The first cscf's
# listen binds the reuseport-cloned kernel socket; subsequent ones
# merge cleanly into it.
listen 8080 device=eth1 ipng_source_tag=tag1;
listen [::]:8080 device=eth1 ipng_source_tag=tag1;
listen 8080 device=eth2 ipng_source_tag=tag2-v4;
listen [::]:8080 device=eth2 ipng_source_tag=tag2-v6;
listen 8080 reuseport device=eth1 ipng_source_tag=tag1;
listen [::]:8080 reuseport device=eth1 ipng_source_tag=tag1;
listen 8080 reuseport device=eth2 ipng_source_tag=tag2-v4;
listen [::]:8080 reuseport device=eth2 ipng_source_tag=tag2-v6;

View File

@@ -48,9 +48,14 @@ http {
# pair, so each server block gets its own cscf attached but no
# server block triggers nginx's "duplicate listen options"
# check;
# * strip socket-level options (reuseport, bind, backlog=, ...)
# from cross-cscf repeat sockaddrs — nginx enforces these
# at-most-once per sockaddr, and the first cscf already owns
# the single kernel socket that the remaining cscfs merge
# into;
# * dedup bindings globally on (sockaddr, device), so init_module
# creates exactly four sockets here (two families × two
# devices) rather than 3 × 4 = 12.
# creates exactly one binding per (device, family) rather than
# one per (server block × device × family).
# The default server owns the locations used by the traffic tests;
# the two extras exist only to exercise the shared-include pattern.
server {