Skip to content

fix: reach IPv6 collectors, and modernise the license metadata (F-10, F-15) - #54

Merged
404SecNotFound merged 2 commits into
mainfrom
fix/ipv6-and-packaging-metadata
Aug 3, 2026
Merged

fix: reach IPv6 collectors, and modernise the license metadata (F-10, F-15)#54
404SecNotFound merged 2 commits into
mainfrom
fix/ipv6-and-packaging-metadata

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

F-10 IPv6

Every socket in the transport was created with a hardcoded socket.AF_INET, so a collector with
only an IPv6 address could not be reached at all. resolve_endpoint now asks getaddrinfo with
AF_UNSPEC, so a v4 literal, a v6 literal and a name all work with nothing to configure. Six call
sites converted.

The resolved sockaddr is kept on the emitter rather than re-derived per send, because an IPv6
sockaddr is a 4-tuple: rebuilding (host, port) for sendto would have worked on v4 and silently
broken v6.

Safety rule 1 unchanged: resolution asks about the configured collector and no other host.
route_for and segment_claim stay IPv4-only and already decline v6 rather than guessing, so the
path disclosure degrades to "route not determined" instead of lying.

F-15 packaging metadata

The deprecated license = { text = ... } table becomes the PEP 639 SPDX expression. Verified by
building a wheel and reading METADATA:

Metadata-Version: 2.4
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE

Note this raises build-system.requires to setuptools>=77, which is what PEP 639 needs. That is
a build-time requirement only; it does not change requires-python.

F-14 — stopped deliberately, needs your decision

npm audit fix applied the one non-breaking upgrade (postcss). 6 advisories down to 5.
Production audit was and remains clean (npm audit --omit=dev → 0). Every remaining advisory
is dev-only, so this is a contributor-workstation risk, not a shipped one.

The five that remain all require major bumps:

package severity fix
vitest critical vitest 4.1.10 (breaking)
vite high vite 8.2.0 (breaking)
@vitest/mocker moderate via vitest 4
vite-node moderate via vitest 4
esbuild moderate via vite 8

Why I stopped. Both upgrades drop Node 18:

  • vite@8 engines: ^20.19.0 || >=22.12.0
  • vitest@4 engines: ^20.0.0 || ^22.0.0 || >=24.0.0

The CI matrix builds Node 18 and 20, and its own comment says "18 is the floor the installer
enforces"
. So taking these upgrades means raising the minimum Node an operator needs and changing
what scripts/install.sh accepts. That is a supported-platform commitment, not a mechanical bump,
and it is yours to make rather than mine to make at 3am.

Node 18 went end-of-life on 2025-04-30, so the case for dropping it is strong. But it should be a
decision with a changelog line, not a side effect of an audit fix.

If you say go, the change is: bump both deps, set the CI matrix to ["20", "22"], raise the
installer's Node floor, and note it in the CHANGELOG.

…ress

Three defects that each made a run report less than it already knew.

**F-12: the run stream was destructive.** Every consumer read the same
queue.Queue, and `get()` removes the item, so two browser tabs on one run each
received a random subset of the lines and neither saw the whole stream. Each
consumer now gets its own queue. That also isolates a stalled reader: their
queue fills while everyone else keeps up, instead of the backlog being shared.

**A failed run reported nothing.** The orchestrator writes a manifest on every
exit path (F-02) and re-raises the original exception unchanged, which is
correct. But the web runner had no way to reach that manifest, so it published
`manifest: None, count: 0` while a complete partial record sat on disk. Half of
what F-02 guaranteed was invisible from the surface most likely to need it. The
record is now attached to the exception rather than wrapped in a new one,
because the whole point of re-raising unchanged is that the operator sees their
real error. Attaching is best effort: a slotted builtin exception refuses new
attributes, and a diagnostic that raises while reporting a failure would replace
the operator's error with its own.

**Progress never reported its last word.** The callback fired on multiples of
100, so a 250-event run reported 200 and stopped, leaving every consumer at 80%
on a finished run. The final call is in `finally`, so a stopped or failed run
also gets a true count rather than the last round number.

One regression caught and fixed in the same change, worth recording because it
was invisible in the design: the client starts a run and only THEN opens the
stream, so a short run routinely finishes before any subscriber exists. The old
shared queue handled that by accident, since nothing had drained it. Fan-out
removed the accident and an existing test immediately received an empty body.
Subscribers are now seeded with a bounded history, and both the replay and its
bound have their own guards.

Also fixed while typing it: inside RunHandle the field named `queue` shadows the
`queue` module, so every later annotation in the class body resolved to the
field. mypy caught it; it would have been a confusing runtime error later.

865 py. black, ruff, mypy clean.
**F-10, IPv6.** Every socket in the transport was created with a hardcoded
`socket.AF_INET`, so a collector with only an IPv6 address could not be reached
at all, and failed with an error naming neither the cause nor the fix.
Dual-stack SIEM deployments are ordinary.

`resolve_endpoint` asks `getaddrinfo` with AF_UNSPEC, so a literal 10.0.20.125,
a literal ::1 and a name all work with nothing to configure. Six call sites
converted: the route probe, both connect-test paths, and all three emitter
transports.

The resolved sockaddr is kept on the emitter rather than re-derived per send,
because an IPv6 sockaddr is a 4-tuple: rebuilding `(host, port)` for `sendto`
would have worked on v4 and silently broken v6, which is the kind of asymmetry
that only shows up in someone else's lab.

Safety rule 1 is unchanged: resolution asks about the configured collector and
no other host, and it is not egress to it. `route_for` and `segment_claim` stay
IPv4-only and already decline v6 rather than guessing, so the path disclosure
degrades to "route not determined" instead of lying.

Tests cover udp, tcp and the connect probe over ::1, and assert IPv4 is
unchanged, which is the regression that would matter most. They skip cleanly on
a host with no v6 loopback rather than failing.

**F-15, packaging metadata.** `license = { text = "Apache-2.0" }` is the
deprecated table form; setuptools stops accepting it on 2027-02-18. Now the
PEP 639 SPDX expression, which needs setuptools>=77 in build-system.requires,
plus `license-files` so LICENSE and NOTICE actually ship. Verified by building
a wheel and reading its METADATA: `License-Expression: Apache-2.0`,
Metadata-Version 2.4, both files present under dist-info/licenses/.
The now-redundant License classifier is removed, as PEP 639 requires.

**F-14, partially.** `npm audit fix` applied the one non-breaking upgrade
(postcss), 6 advisories down to 5. The rest are NOT done and are not mine to
decide: see the PR body.

871 py + 136 fe. black, ruff, mypy clean.
@404SecNotFound
404SecNotFound merged commit 3466b3b into main Aug 3, 2026
10 checks passed
@404SecNotFound
404SecNotFound deleted the fix/ipv6-and-packaging-metadata branch August 3, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant