Commit Graph
10 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 3610e7b9ef fix(code-scanning): resolve the alerts that describe real code
Of the 76 open code scanning alerts, 39 point at code that can change.

svc-ai-mcp-adapter:
- A path argument that is empty or made only of dots is refused.
  Percent-encoding keeps a separator inside its segment, but "." and ".."
  need none, so an upstream that resolves dot segments could answer from a
  path the contract does not name.
- parse_duration splits on the units. The pattern it used before rescanned
  a long run of digits from every start position.

Tests and the README generators:
- Every Jinja environment built with autoescape=False and a "noqa: S701"
  uses select_autoescape now, as six test modules and
  utils/templating/jinja.py already did. It escapes templates named
  *.html, *.htm and *.xml, which none of these call sites loads.
- Dead code goes: a doubled "rel" block in the email lint, an unread
  _SKIP_DIRS, and a module that _resolve returned to no caller.
- Two "except ...: pass" blocks become contextlib.suppress and
  assertRaises.
- Nine test modules import unittest.mock one way instead of two, and one
  lookup test imports its module once.

Closes #807

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:18:00 +02:00
kevinveenbirkenbachandClaude Opus 5 b12ff8d0c4 fix(svc-net-tor): tie the dnsmasq reload to the branch that writes its drop-in
The systemd drop-in is written inside a block gated on TOR_DNSMASQ_OWNS_LISTENER, which turns false as soon as a foreign file sits in /etc/dnsmasq.d. The start task read that block's register unconditionally, so the expression's correctness depended on ansible defining a register for a task it skipped. Short-circuiting on the same condition means the register is never reached when the block did not run, and the question stops mattering.

The test asserted the literal declaration string, which exercises nothing. It now renders the expression read from the role with ansible's own bool filter and a changed test, in both states: owning the listener with a rewritten drop-in yields True, not owning it yields False without touching the register. The previous form raises UndefinedError under the second case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:27:04 +02:00
kevinveenbirkenbachandClaude Opus 5 4f44eff128 fix(svc-net-tor): bind dnsmasq statically so a failed re-enumeration cannot drop every listener
Run 34421944328 Nextcloud#13 lost the dashboard's Matomo tracker. The
dns53 sampler on mgr-01 shows no :53 socket from 01:39:52 to 01:48:44
while the dnsmasq PID stayed the same, six seconds after a short-lived
container's veth on docker0. In that window the manager could not
resolve its own onion vhosts, so sys-front-inj-matomo's site checks for
web-svc-logout (01:45:35) and web-app-dashboard (01:47:18) never reached
OpenResty, the injection was disabled as "API not ready", and the
Playwright check found no matomo.js. The other four swarm managers of
the run that carried the same listener kept it throughout.

Under bind-dynamic, dnsmasq 2.91 re-enumerates interfaces on every
netlink address event and then garbage-collects every listener it did
not re-find (network.c enumerate_interfaces), also when the enumeration
failed; release_listener has no other caller. 5aa7506e36 reproduced this
on the lab nodes and 11c98825aa already moved the bridge-less case to a
static bind. The bridge case now binds statically as well.

This supersedes two earlier rationales, both measured on the local stack:
- bb0e098a9f kept bind-dynamic because dnsmasq starts at boot before
  docker0 exists. A unit drop-in (files/dnsmasq-restart.conf,
  Restart=on-failure, RestartSec=5) now retries the start: with a missing
  listen-address the start exits 2, and dnsmasq came up 4s after the
  address appeared.
- 4739b2de99 called bind-dynamic load-bearing because docker0 can
  disappear across a docker restart. Only a start without the address is
  fatal; a bound socket survived the address being removed and re-added
  and the interface being recreated with a new ifindex, answering on the
  same socket inode each time.

The docker0 fact is re-gathered before torrc and the dnsmasq drop-in are
rendered. The play-start fact predates default-address-pools moving the
bridge (285ba1e76c), and a static bind on a stale address would make
dnsmasq exit. container_dns reads only the pool, so daemon.json is not
affected by the refresh.

The host resolv.conf moves from an inline copy content to
templates/resolv.conf.j2; both it and the drop-in render byte-identical
to what the local stack already runs.

A veth churn of 13.8k cycles did not reproduce the loss locally, under
either bind mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 12:01:51 +02:00
kevinveenbirkenbachandClaude Opus 5 7b4ea2eaf2 test(svc-net-tor): follow the resolv task into the block that now holds it
The resolv.conf assertion looked its task up with a flat scan over router.yml top level. Encapsulating the two TOR_DNSMASQ_OWNS_LISTENER tasks in a block moved that task one level down, so the generator ran dry and StopIteration failed the test. A recursive walk over block/rescue/always restores the lookup while leaving what the test asserts untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 03:39:06 +02:00
kevinveenbirkenbachandClaude Opus 5 11c98825aa fix(svc-net-tor): bind the loopback listener statically when no bridge is expected
9ae0c7eff6 replaced bind-interfaces with bind-dynamic when it taught the role to bind the node address for swarm, an address that appears after dnsmasq starts. Without a bridge address there is nothing to wait for, and the rebuild bind-dynamic performs on every netlink event can then only drop the listener: on the DiD host of run 33698844864 job 100479883964 the sampler lost dnsmasq's 127.0.0.1:53 for four consecutive samples one second after a veth unregistered.

interface=lo is deliberately not added. dnsmasq unions interface= and listen-address= rather than intersecting them, measured on 2.93: naming lo additionally seizes 127.0.0.53 and ::1, and against a distro stub dnsmasq exits 2, which any_errors_fatal turns into a failed deploy.

The resolv.conf keeps both nameservers. bb0e098a9f added the second one from a measured total outage where dnsmasq held no socket at all, and that failure mode is untouched by this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 12:44:05 +02:00
kevinveenbirkenbachandClaude Opus 5 ce43263745 fix(container-dns): follow the live bridge, not a configured address
The swarm inventory carried the manager's docker bridge gateway as a literal under networks.internet.dns, so one field meant two things at once and the value went stale the moment default-address-pools moved the bridge. The daemon kept handing out the new address while the inventory kept naming the old one, and the DinD diagnostic probed that same literal, so it failed in step with the defect instead of catching it.

The inventory now declares only the clearnet resolver. The lookup hands containers the bridge whenever the host resolves solely through a loopback listener, which is how the simulated cluster nodes are wired and which docker strips from a container's resolv.conf. The diagnostic reads the gateway from the running daemon instead of being told it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:06:42 +02:00
kevinveenbirkenbachandClaude Opus 5 54aeba7e06 fix(svc-net-tor): name the clearnet upstream
The egress router scraped /etc/resolv.conf once and persisted whatever it found. Inside a container that is 127.0.0.11, docker's embedded resolver, which answered REFUSED to recursive queries in CI run 31927586060 and left the node with no clearnet DNS. The upstream now comes from networks.internet.dns, the same source lookup('container_dns') already gives the docker daemon, and an assert demands it before the role touches the host.

no-resolv, the clearnet server= lines and the /etc/resolv.conf pin now hang on TOR_DNSMASQ_OWNS_LISTENER. A dnsmasq the role does not own keeps its upstreams and the node keeps its resolver; only the onion routing is added there. Without that split a swarm node would end up with no-resolv, no upstream, and its resolver pinned at itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:02:30 +02:00
kevinveenbirkenbachandClaude Opus 5 4739b2de99 fix(svc-net-tor): stop imposing a bind policy on a foreign dnsmasq
bind-dynamic sat outside the {% if TOR_DNSMASQ_OWNS_LISTENER %} guard that
gates the listen-address lines, and router.yml:72 forces that flag false in
swarm. So on a stack host the drop-in declared it does not own the listener
and then dictated its bind mode anyway -- naming no address at all.

That converts a wildcard-bound instance's sockets into enumeration-derived
ones. When an IPv6 netlink dump aborts, dnsmasq skips the AF_INET pass and
its GC closes every IPv4 listener, silently: the message is LOG_DEBUG and
bind-dynamic bind failures are non-fatal. The node was left with 10 IPv6
listeners and zero IPv4 while four IPv4 addresses existed, and router.yml
had already pointed /etc/resolv.conf at 127.0.0.1 -- so dockerd got ICMP
port-unreachable and could not resolve ghcr.io. 33 failures in 11 groups,
none before the drop-in landed. The two worker nodes, which never received
it, kept 0.0.0.0:53 bound throughout.

Ownership is the right axis, not deployment mode. Where the role owns the
listener, bind-dynamic is load-bearing: a listen-address naming docker0 can
disappear across a docker restart, and without OPT_CLEVERBIND that bind is
fatal. Where it does not own the instance, the directive is pure collateral
-- and next to a base config carrying bind-interfaces it aborts startup
outright.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 10:10:12 +02:00
kevinveenbirkenbachandClaude Opus 5 52b798094a style(container_dns): satisfy ruff on the onion dns bundle
Drop a return-only temporary and state autoescape explicitly — the test
renders a dnsmasq config, where html escaping would corrupt the output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 09:24:38 +02:00
kevinveenbirkenbachandClaude Opus 5 9d91fbea83 feat(svc-net-tor): route onion names through tor via dns, not per-app proxies
Reaching an onion address required each role to point its own http client
at the tor socks proxy, which then carried clearnet traffic too — fatal
in the test network, which has no exit, and the reason nextcloud needed a
per-host exclusion list.

The egress router already redirects tor's virtual range to the TransPort
and dnsmasq already forwards .onion to tor's DNSPort; the piece missing
was reachability. dnsmasq listened on loopback only, which docker
discards, so containers never saw it. It now also answers on the docker
bridge, the daemon points there with the clearnet resolver kept as
fallback, and the role's own onion resolves to a container-reachable
backend instead of loopback.

Nextcloud drops its proxy block and the exclusion list; every other role
gets the same behaviour without configuring anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 01:13:33 +02:00