Pim van Pelt df05bae8a3 Support multiple device-pinned listens sharing a single port
Nginx's config-level duplicate-listen check rejected the
documented pattern of `listen 80 device=X ipng_source_tag=A;
listen 80 device=Y ipng_source_tag=B;` with "a duplicate listen
0.0.0.0:80", and even when the dedup was bypassed the kernel
refused the second bind() because the first socket was already
holding the port without SO_BINDTODEVICE.

The listen wrapper now detects same-sockaddr duplicates before
the core handler sees them and records them with `needs_clone=1`.
In init_module, phase 1 clones an ngx_listening_t for each such
duplicate, phase 3 closes every inherited naked fd, and phase 4
rebinds every target with SO_REUSEADDR + SO_REUSEPORT +
SO_BINDTODEVICE set before bind(). SO_REUSEPORT keeps
`nginx -s reload` from colliding with the still-bound sockets
held by old workers during graceful drain; IPV6_V6ONLY matches
nginx's default so the IPv6 listen doesn't claim the IPv4
wildcard and collide with sibling IPv4-specific listens.

Restructure 01-module to cover the pattern end-to-end: four
device-pinned listens on port 8080 (eth1 shares tag `tag1`
across v4 and v6; eth2 splits into `tag2-v4` / `tag2-v6`),
clients and server both get IPv6 addresses, and a new
"Per-(device, family) request count accuracy" case proves that
10 requests on each of the four combinations yields tag1=20,
tag2-v4=10, tag2-v6=10. Mgmt/direct traffic moves to port 9180
so it no longer clashes with the shared-port wildcards.

Document the constraint in docs/user-guide.md: all listens on
a given port must carry `device=`, and direct traffic belongs
on a separate port.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:45:40 +02:00

nginx-ipng-stats-plugin

Per-VIP, per-device traffic counters for nginx. Ships as a dynamic nginx module and a Debian package that loads into stock upstream nginx on Debian Trixie.

The module attributes every HTTP request to the interface it arrived on, using Linux SO_BINDTODEVICE on per-interface listening sockets. Counters — requests, status codes, bytes, latency histograms — are exposed as Prometheus text or JSON from a single HTTP scrape endpoint, filtered per-source. This is useful for any deployment where traffic arrives on distinct interfaces — GRE tunnels, VLANs, bonded links, or plain ethernet — and per-interface observability is needed.

Without any device=/ipng_source_tag= parameters, the module still counts and exposes per-VIP traffic under the configurable default source tag (direct), which makes it a useful plain observability module for any nginx host.

See docs/design.md for the full design, including the attribution model, data flow, and requirements.

Quick start

make install-deps      # install build and test dependencies (apt)
make build             # build the .so out-of-tree
make pkg-deb           # build a .deb package
make robot-test        # run end-to-end tests via containerlab

Installing

sudo dpkg -i build/*.deb

The package installs the .so into /usr/lib/nginx/modules, drops a load_module stanza into /etc/nginx/modules-enabled/, and runs nginx -t before completing.

Configuring

See docs/user-guide.md for an end-to-end walkthrough and docs/config-guide.md for the directive and listen parameter reference.

License

Apache-2.0. See LICENSE.

Description
No description provided
Readme Apache-2.0 920 KiB
Languages
C 72.7%
RobotFramework 14.9%
Makefile 7.4%
Shell 4%
C++ 0.6%
Other 0.4%