Pim van pelt 05d405aba5 Count discarded POST bodies against bytes_in
Counter coverage for nginx_ipng_bytes_in_total was missing most of
the wire for rate-limited POST workloads. Observed on a CT-log-style
box: the plugin was reporting ~300 KB/s inbound while btop showed
6+ MB/s on the NIC, a ~20x gap. Root cause is in nginx itself: when
a request is rejected before a handler reads the body (limit_req
403, auth_request denial, 413 on fixed Content-Length, early
`return 4xx;`), nginx routes the body through
ngx_http_discard_request_body / HTTP/2 skip_data. Both paths drop
the bytes without ever incrementing r->request_length, so
$request_length (and therefore the plugin's bytes_in counter)
reflects only the request line and headers.

Log handler now adds r->headers_in.content_length_n to bin_sz when
r->discard_body is set and the client advertised a Content-Length.
That's a tight upper bound on the body bytes actually received —
abusive clients like the CT-log hammer case typically send the
full declared body before nginx's RST propagates. Normal 200 POSTs
are unchanged (they go through the buffered body path, which does
update r->request_length, and r->discard_body stays 0).

docs/nginx-issues.md catalogs the full analysis — which nginx paths
update r->request_length and which don't, how 413 differs between
fixed-CL and chunked and HTTP/2, why r->connection->sent is safe to
accumulate (it's reset per-request on keepalive and per-stream on
HTTP/2), and what the residual gap looks like after this fix
(HTTP/2 framing overhead, TLS/TCP, chunked requests with no
Content-Length header). Option B (per-connection recv wrapping) is
sketched there for later if the residual matters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:57:45 +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, reading the ingress ifindex per connection from the kernel's IP_PKTINFO / IPV6_PKTINFO cmsg. Listening sockets stay plain wildcards, so outgoing packets follow the normal routing table — which is what makes this safe for DSR / maglev deployments where the SYN arrives via a GRE tunnel and the SYN-ACK must leave via the default route. 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%