From 4739b2de99b81ef09f4d030f6a4b7227e7cbd8ab Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 10 Aug 2026 10:10:12 +0200 Subject: [PATCH] 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) --- roles/svc-net-tor/templates/dnsmasq-tor-onion.conf.j2 | 2 +- .../roles/svc-net-tor/test_container_dns_wiring.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/roles/svc-net-tor/templates/dnsmasq-tor-onion.conf.j2 b/roles/svc-net-tor/templates/dnsmasq-tor-onion.conf.j2 index 79b268ab02..d001608461 100644 --- a/roles/svc-net-tor/templates/dnsmasq-tor-onion.conf.j2 +++ b/roles/svc-net-tor/templates/dnsmasq-tor-onion.conf.j2 @@ -3,8 +3,8 @@ listen-address=127.0.0.1 {% if TOR_CONTAINER_DNS_HOST | length > 0 %} listen-address={{ TOR_CONTAINER_DNS_HOST }} {% endif %} -{% endif %} bind-dynamic +{% endif %} no-hosts no-resolv filter-AAAA diff --git a/tests/integration/roles/svc-net-tor/test_container_dns_wiring.py b/tests/integration/roles/svc-net-tor/test_container_dns_wiring.py index 222779d17a..d92a79fa7a 100644 --- a/tests/integration/roles/svc-net-tor/test_container_dns_wiring.py +++ b/tests/integration/roles/svc-net-tor/test_container_dns_wiring.py @@ -38,6 +38,13 @@ class TestDnsmasqListeners(unittest.TestCase): self.assertIn("listen-address=127.0.0.1", rendered) self.assertNotIn("listen-address=172.17", rendered) + def test_no_bind_policy_when_the_listener_is_not_ours(self): + rendered = _render_dnsmasq( + TOR_DNSMASQ_OWNS_LISTENER=False, TOR_CONTAINER_DNS_HOST="" + ) + self.assertNotIn("bind-dynamic", rendered) + self.assertNotIn("listen-address", rendered) + def test_onion_queries_go_to_the_tor_resolver(self): self.assertIn("server=/onion/127.0.0.1#9053", _render_dnsmasq())