mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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>
This commit is contained in:
1 parent
7125cb9d2c
commit
54aeba7e06
4 files changed
+32
-39
No files matched your search
@@ -1,4 +1,11 @@
|
||||
---
|
||||
- name: "✅ Egress router | Assert a clearnet resolver is configured"
|
||||
ansible.builtin.assert:
|
||||
that: networks.internet.dns | length > 0
|
||||
fail_msg: >-
|
||||
networks.internet.dns must be set on a Tor node: dnsmasq runs with
|
||||
no-resolv and would answer .onion only.
|
||||
when: DEPLOYMENT_MODE == 'compose'
|
||||
- name: "📦 Egress router | Install dnsmasq (host split-DNS forwarder)"
|
||||
become: true
|
||||
package_install:
|
||||
@@ -20,45 +27,15 @@
|
||||
group: root
|
||||
mode: "0644"
|
||||
register: _tor_dnsmasq_confdir
|
||||
- name: "📁 Egress router | Ensure state directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ [ DIR_VAR_LIB, application_id ] | path_join }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
- name: "🔎 Egress router | Check for persisted clearnet upstreams"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ TOR_DNS_UPSTREAMS_FILE }}"
|
||||
register: _tor_upstreams_stat
|
||||
- name: "📖 Egress router | Read the current resolver (to capture upstreams once)"
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/resolv.conf
|
||||
register: _tor_resolv_now
|
||||
when: not _tor_upstreams_stat.stat.exists
|
||||
- name: "💾 Egress router | Persist clearnet upstreams (first setup only)"
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ TOR_DNS_UPSTREAMS_FILE }}"
|
||||
content: "{{ (_tor_upstream_list | join('\n')) ~ '\n' }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
vars:
|
||||
_tor_upstream_list: >-
|
||||
{{ (_tor_resolv_now.content | b64decode)
|
||||
| regex_findall('(?m)^nameserver\s+(\S+)')
|
||||
| reject('equalto', '127.0.0.1') | list }}
|
||||
when: not _tor_upstreams_stat.stat.exists
|
||||
- name: "📥 Egress router | Load captured upstreams"
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ TOR_DNS_UPSTREAMS_FILE }}"
|
||||
register: _tor_dns_upstreams_raw
|
||||
- name: "🔎 Egress router | Detect foreign dnsmasq drop-ins (shared instance)"
|
||||
ansible.builtin.find:
|
||||
paths: "{{ TOR_DNSMASQ_CONF | dirname }}"
|
||||
patterns: "*.conf"
|
||||
excludes: "{{ TOR_DNSMASQ_CONF | basename }}"
|
||||
register: _tor_dnsmasq_foreign
|
||||
- name: "🔧 Egress router | Decide whether this dnsmasq is ours to reshape"
|
||||
ansible.builtin.set_fact:
|
||||
TOR_DNSMASQ_OWNS_LISTENER: "{{ _tor_dnsmasq_foreign.matched == 0 and DEPLOYMENT_MODE == 'compose' }}"
|
||||
|
||||
- name: "📝 Egress router | Render dnsmasq .onion split-DNS drop-in"
|
||||
ansible.builtin.template: # nocheck: stack-host-template (per-host dnsmasq system config in /etc/dnsmasq.d)
|
||||
@@ -68,8 +45,7 @@
|
||||
group: root
|
||||
mode: "0644"
|
||||
vars:
|
||||
TOR_DNS_UPSTREAMS: "{{ (_tor_dns_upstreams_raw.content | b64decode).split('\n') | map('trim') | reject('equalto', '') | list }}"
|
||||
TOR_DNSMASQ_OWNS_LISTENER: "{{ _tor_dnsmasq_foreign.matched == 0 and DEPLOYMENT_MODE == 'compose' }}"
|
||||
TOR_DNS_UPSTREAMS: "{{ [ networks.internet.dns ] }}"
|
||||
register: _tor_dnsmasq_conf
|
||||
- name: "🚀 Egress router | Enable + (re)start dnsmasq"
|
||||
ansible.builtin.systemd:
|
||||
@@ -86,6 +62,7 @@
|
||||
group: root
|
||||
mode: "0644"
|
||||
unsafe_writes: true
|
||||
when: TOR_DNSMASQ_OWNS_LISTENER | bool
|
||||
- name: "🔀 Egress router | Redirect .onion virtual-IP TCP to Tor TransPort"
|
||||
ansible.builtin.iptables:
|
||||
table: nat
|
||||
|
||||
@@ -4,15 +4,17 @@ listen-address=127.0.0.1
|
||||
listen-address={{ TOR_CONTAINER_DNS_HOST }}
|
||||
{% endif %}
|
||||
bind-dynamic
|
||||
no-resolv
|
||||
{% endif %}
|
||||
no-hosts
|
||||
no-resolv
|
||||
filter-AAAA
|
||||
{% if TOR_NODE_ONION | length > 0 %}
|
||||
address=/{{ TOR_NODE_ONION }}/{{ TOR_HS_BACKEND_HOST }}
|
||||
local=/{{ TOR_NODE_ONION }}/
|
||||
{% endif %}
|
||||
server=/onion/127.0.0.1#{{ TOR_DNS_PORT }}
|
||||
{% if TOR_DNSMASQ_OWNS_LISTENER | default(true) %}
|
||||
{% for _ns in TOR_DNS_UPSTREAMS %}
|
||||
server={{ _ns }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -16,7 +16,6 @@ TOR_DNSMASQ_CONF: "/etc/dnsmasq.d/tor-onion.conf"
|
||||
TOR_CONTAINER_DNS_HOST: "{{ ansible_facts['docker0']['ipv4']['address'] | default('', true) if DEPLOYMENT_MODE == 'compose' else '' }}"
|
||||
TOR_HS_BACKEND_HOST: "{{ ansible_facts['default_ipv4']['address'] if DEPLOYMENT_MODE == 'swarm' else (TOR_CONTAINER_DNS_HOST | ternary(TOR_CONTAINER_DNS_HOST, '127.0.0.1'))
|
||||
}}"
|
||||
TOR_DNS_UPSTREAMS_FILE: "{{ [ DIR_VAR_LIB, application_id, 'dns-upstreams' ] | path_join }}"
|
||||
|
||||
TOR_DATA_DIR: "{{ [ DIR_VAR_LIB, application_id, 'data' ] | path_join }}"
|
||||
TOR_CONFIG_DIR: "{{ [ DIR_VAR_LIB, application_id, 'config' ] | path_join }}"
|
||||
|
||||
@@ -12,7 +12,7 @@ def _render_dnsmasq(**overrides):
|
||||
"TOR_NODE_ONION": "example.onion",
|
||||
"TOR_HS_BACKEND_HOST": "172.17.0.1",
|
||||
"TOR_DNS_PORT": 9053,
|
||||
"TOR_DNS_UPSTREAMS": ["127.0.0.11"],
|
||||
"TOR_DNS_UPSTREAMS": ["172.30.0.53"],
|
||||
}
|
||||
variables.update(overrides)
|
||||
env = Environment(
|
||||
@@ -49,7 +49,22 @@ class TestDnsmasqListeners(unittest.TestCase):
|
||||
self.assertIn("server=/onion/127.0.0.1#9053", _render_dnsmasq())
|
||||
|
||||
def test_clearnet_upstreams_are_kept(self):
|
||||
self.assertIn("server=127.0.0.11", _render_dnsmasq())
|
||||
self.assertIn("server=172.30.0.53", _render_dnsmasq())
|
||||
|
||||
def test_upstream_policy_stays_off_a_listener_we_do_not_own(self):
|
||||
"""A shared dnsmasq brings its own upstreams, and in swarm
|
||||
networks.internet.dns is the docker bridge that very dnsmasq answers
|
||||
on. Writing a server= there points the resolver at itself, and
|
||||
no-resolv would cut the upstreams its owner configured."""
|
||||
rendered = _render_dnsmasq(TOR_DNSMASQ_OWNS_LISTENER=False)
|
||||
self.assertNotIn("server=172.30.0.53", rendered)
|
||||
self.assertNotIn("no-resolv", rendered)
|
||||
self.assertIn("server=/onion/127.0.0.1#9053", rendered)
|
||||
|
||||
def test_upstream_policy_applies_to_our_own_listener(self):
|
||||
rendered = _render_dnsmasq()
|
||||
self.assertIn("no-resolv", rendered)
|
||||
self.assertIn("server=172.30.0.53", rendered)
|
||||
|
||||
def test_own_onion_resolves_to_a_container_reachable_backend(self):
|
||||
self.assertIn("address=/example.onion/172.17.0.1", _render_dnsmasq())
|
||||
|
||||
Reference in new issue
Block a user