Allow plain and device-tagged listens to share a sockaddr (v0.7.1)
The previous wrapper skipped nginx's duplicate-listen check only
for listens that carried device=, so a `listen 80;` next to a
`listen 80 device=eth0 ...;` in the same server block was
rejected at config time. Under SO_BINDTODEVICE that restriction
tracked a real kernel constraint (device-tagged listens created
separate sockets, a bare listen alongside them was genuinely
ambiguous). Under the IP_PKTINFO model introduced in 450391a
the constraint no longer exists — all same-sockaddr listens
collapse to one wildcard kernel socket and attribution is a
per-connection cmsg readback — but the wrapper kept enforcing
the old rule by accident.
Extend the (cscf, sockaddr) dedup in the listen wrapper to
cover plain listens too: the first occurrence at a given
(server, sockaddr) pair calls nginx's handler and registers the
kernel socket, and every subsequent sibling — plain or
device-tagged — is accepted without tripping nginx's
duplicate-listen check. Device-tagged siblings additionally
push a binding into the attribution table as before; plain
siblings contribute only the seen-list entry. No code path
exercised by the existing 22 e2e tests changes behavior.
Update FR-1.5, the user-guide "shared port" section, the
module's top-of-function comments, and the test nginx.conf
comment to describe the relaxed rule. Bump VERSION and add a
debian/changelog entry for 0.7.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,10 +84,12 @@ Each requirement carries a unique identifier (`FR-X.Y` or `NFR-X.Y`) so that lat
|
||||
- **FR-1.3** A listening socket with neither `device=` nor `ipng_source_tag=` MUST be tagged with the configured default source string (see
|
||||
`ipng_stats_default_source`, FR-5.3). The default default is the literal string `direct`.
|
||||
- **FR-1.4** A listening socket with `device=X` but no `ipng_source_tag=` MUST be tagged with the interface name `X`.
|
||||
- **FR-1.5** Two `listen` directives that share `address:port` but differ in `device=` MUST coexist. Since no `SO_BINDTODEVICE`
|
||||
is applied, the kernel delivers all matching SYNs to a single wildcard listening socket and the module distinguishes them by
|
||||
reading `ifindex` from the per-connection cmsg — so "multiple device-tagged listens at the same port" at the config level
|
||||
collapses to one kernel socket at runtime without any userspace contortions.
|
||||
- **FR-1.5** Two or more `listen` directives sharing `address:port` MUST coexist regardless of whether each carries `device=`.
|
||||
Since no `SO_BINDTODEVICE` is applied, the kernel delivers all matching SYNs to a single wildcard listening socket and the
|
||||
module distinguishes them by reading `ifindex` from the per-connection cmsg. The listen wrapper therefore deduplicates on
|
||||
`(server block, sockaddr)` across both plain and device-tagged listens: the first occurrence registers the kernel socket,
|
||||
and subsequent same-sockaddr siblings (plain or device-tagged) are accepted without tripping nginx's duplicate-listen check.
|
||||
Device-tagged siblings additionally register an entry in the attribution table.
|
||||
- **FR-1.6** A `listen` directive that uses a wildcard address (`80`, `[::]:80`) together with `device=<ifname>` MUST attribute
|
||||
every connection whose ingress interface is `<ifname>` — regardless of which local address the client addressed — to that
|
||||
listen's source tag. Traffic on other interfaces MUST fall back to the configured default source (see FR-1.3).
|
||||
|
||||
@@ -112,8 +112,9 @@ http {
|
||||
ipng_stats_flush_interval 1s;
|
||||
ipng_stats_default_source direct;
|
||||
|
||||
# Attributed vhost. Every listen on this port must be device-tagged —
|
||||
# see "All listens on a shared port must be device-tagged" below.
|
||||
# Attributed vhost. Wildcard listens below register one binding
|
||||
# per (device, family); all collapse to a single kernel socket
|
||||
# under the IP_PKTINFO attribution model.
|
||||
server {
|
||||
include /etc/nginx/ipng-stats/listens.conf;
|
||||
|
||||
@@ -176,22 +177,14 @@ You do not need to enumerate VIPs in `listen`. A wildcard `listen 80 device=gre-
|
||||
served through the `gre-mg1` interface, and nginx routes per-request to the right vhost by `server_name` / `Host:` header. Adding a new
|
||||
VIP is a `server_name` change; adding a new interface is an append to `listens.conf`.
|
||||
|
||||
### All listens on a shared port must be device-tagged
|
||||
|
||||
If you use multiple `listen` directives on the same port (e.g. port 80), **every one of them must carry `device=<ifname>`**. Mixing
|
||||
a device-tagged listen with a plain `listen 80;` or with an address-specific `listen 192.0.2.1:80;` on the same port is **not
|
||||
supported** — nginx's config-level dedup rejects same-sockaddr listens within a server block, and the module's wrapper only
|
||||
exempts directives that carry `device=`.
|
||||
|
||||
For "direct" traffic — clients hitting the host on a non-attributed interface — use a **separate port** on the direct interface
|
||||
(e.g. `listen 198.51.100.1:8081;`). That listen then has no `device=`, so it falls back to the tag set by
|
||||
`ipng_stats_default_source` (`direct` by default).
|
||||
|
||||
### Sharing a single port across address families and devices
|
||||
|
||||
Within the device-tagged set, you're free to share port numbers freely across devices and address families: as long as each listen
|
||||
has a distinct `device=`, the kernel keeps them apart, and within one device you can either reuse a single tag or split by family.
|
||||
For example:
|
||||
Under the `IP_PKTINFO` attribution model, all listens at a given sockaddr collapse to a single wildcard kernel socket at runtime —
|
||||
the kernel stamps every accepted connection with its ingress ifindex, and the module looks that up in the table of `device=`
|
||||
bindings registered by the listen wrapper. Multiple device-tagged wildcard listens on port 80 are therefore not "multiple
|
||||
sockets"; they're one wildcard socket plus N entries in the attribution table.
|
||||
|
||||
A device can reuse one tag across address families or split into per-family tags — whichever reads better in the scrape output:
|
||||
|
||||
```nginx
|
||||
listen 80 device=gre-mg1 ipng_source_tag=mg1;
|
||||
@@ -200,6 +193,12 @@ listen 80 device=gre-mg2 ipng_source_tag=mg2-v4;
|
||||
listen [::]:80 device=gre-mg2 ipng_source_tag=mg2-v6; # per-family tags
|
||||
```
|
||||
|
||||
A plain `listen 80;` can sit alongside device-tagged listens in the same server block; the wrapper treats the first occurrence
|
||||
at a given `(server, sockaddr)` pair as the one that registers the kernel socket and lets subsequent device-tagged siblings
|
||||
register bindings without tripping nginx's duplicate-listen check. Traffic arriving on an interface that has no binding falls
|
||||
back to `ipng_stats_default_source` (`direct` by default). Keeping "direct" traffic on its own port — e.g.
|
||||
`listen 198.51.100.1:8081;` — remains a fine pattern when you want a hard split, but it's no longer required.
|
||||
|
||||
## 4. Verify with curl
|
||||
|
||||
Generate some traffic (or wait for real traffic), then scrape the endpoint locally:
|
||||
|
||||
Reference in New Issue
Block a user