mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
fix(svc-net-tor): bind the loopback listener statically when no bridge is expected
9ae0c7eff6replaced 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.bb0e098a9fadded 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>
This commit is contained in:
1 parent
b0ca8784e2
commit
11c98825aa
2 files changed
+47
-1
No files matched your search
@@ -2,8 +2,10 @@
|
||||
listen-address=127.0.0.1
|
||||
{% if TOR_CONTAINER_DNS_HOST | length > 0 %}
|
||||
listen-address={{ TOR_CONTAINER_DNS_HOST }}
|
||||
{% endif %}
|
||||
bind-dynamic
|
||||
{% else %}
|
||||
bind-interfaces
|
||||
{% endif %}
|
||||
no-resolv
|
||||
{% endif %}
|
||||
no-hosts
|
||||
|
||||
@@ -3,6 +3,11 @@ import unittest
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from tests.integration.roles import PROJECT_ROOT
|
||||
from utils.cache.files import read_text
|
||||
from utils.cache.yaml import load_yaml_str
|
||||
|
||||
ROUTER = PROJECT_ROOT / "roles/svc-net-tor/tasks/router.yml"
|
||||
RESOLV_TASK = "🔀 Egress router | Point the host resolver at dnsmasq (127.0.0.1)"
|
||||
|
||||
|
||||
def _render_dnsmasq(**overrides):
|
||||
@@ -38,6 +43,24 @@ class TestDnsmasqListeners(unittest.TestCase):
|
||||
self.assertIn("listen-address=127.0.0.1", rendered)
|
||||
self.assertNotIn("listen-address=172.17", rendered)
|
||||
|
||||
def test_loopback_only_binds_statically(self):
|
||||
"""A loopback-only listener has no address to wait for, and the rebuild
|
||||
that bind-dynamic performs on every netlink event can then only drop it."""
|
||||
rendered = _render_dnsmasq(TOR_CONTAINER_DNS_HOST="")
|
||||
self.assertIn("bind-interfaces", rendered)
|
||||
self.assertNotIn("bind-dynamic", rendered)
|
||||
|
||||
def test_the_static_bind_claims_no_address_it_was_not_given(self):
|
||||
"""interface= and listen-address= are a union in dnsmasq, so naming lo
|
||||
would additionally seize 127.0.0.53 and ::1, where a distro stub sits."""
|
||||
rendered = _render_dnsmasq(TOR_CONTAINER_DNS_HOST="")
|
||||
self.assertNotIn("interface=", rendered)
|
||||
|
||||
def test_a_bridge_listener_still_binds_dynamically(self):
|
||||
rendered = _render_dnsmasq()
|
||||
self.assertIn("bind-dynamic", rendered)
|
||||
self.assertNotIn("bind-interfaces", 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=""
|
||||
@@ -69,5 +92,26 @@ class TestDnsmasqListeners(unittest.TestCase):
|
||||
self.assertIn("address=/example.onion/172.17.0.1", _render_dnsmasq())
|
||||
|
||||
|
||||
class TestHostResolver(unittest.TestCase):
|
||||
"""glibc reads resolv.conf per query and keeps no server state, so the
|
||||
clearnet entry costs nothing while dnsmasq answers and carries the host when
|
||||
it stops. systemd-resolved does keep state, which is why the role has to
|
||||
stay out of its way rather than drop the entry."""
|
||||
|
||||
def _resolv_conf(self) -> str:
|
||||
tasks = load_yaml_str(read_text(str(ROUTER)))
|
||||
task = next(t for t in tasks if t.get("name") == RESOLV_TASK)
|
||||
return task["ansible.builtin.copy"]["content"]
|
||||
|
||||
def test_dnsmasq_answers_first_and_the_upstream_carries_the_host(self):
|
||||
servers = [
|
||||
line.split()[1]
|
||||
for line in self._resolv_conf().splitlines()
|
||||
if line.startswith("nameserver ")
|
||||
]
|
||||
self.assertEqual(len(servers), 2)
|
||||
self.assertEqual(servers[0], "127.0.0.1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user