mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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>
110 lines
4.2 KiB
YAML
110 lines
4.2 KiB
YAML
---
|
|
- 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.
|
|
- name: "📦 Egress router | Install dnsmasq (host split-DNS forwarder)"
|
|
become: true
|
|
package_install:
|
|
id: dnsmasq
|
|
- name: "📁 Egress router | Ensure the dnsmasq drop-in directory exists"
|
|
ansible.builtin.file:
|
|
path: "{{ TOR_DNSMASQ_CONF | dirname }}"
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
- name: "🔧 Egress router | Ensure dnsmasq reads the drop-in directory"
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/dnsmasq.conf
|
|
regexp: "^conf-dir="
|
|
line: "conf-dir={{ TOR_DNSMASQ_CONF | dirname }}/,*.conf"
|
|
create: true
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
register: _tor_dnsmasq_confdir
|
|
- 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 }}"
|
|
|
|
- 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)
|
|
src: dnsmasq-tor-onion.conf.j2
|
|
dest: "{{ TOR_DNSMASQ_CONF }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
vars:
|
|
TOR_DNS_UPSTREAMS: "{{ [ networks.internet.dns ] }}"
|
|
register: _tor_dnsmasq_conf
|
|
- name: "🔁 Egress router | Retry dnsmasq until docker0 exists after a boot"
|
|
when: TOR_DNSMASQ_OWNS_LISTENER | bool
|
|
block:
|
|
- name: "📁 Egress router | Ensure the dnsmasq unit drop-in directory exists"
|
|
ansible.builtin.file:
|
|
path: "{{ TOR_DNSMASQ_DROPIN | dirname }}"
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
- name: "📝 Egress router | Restart dnsmasq on failure"
|
|
ansible.builtin.copy:
|
|
src: dnsmasq-restart.conf
|
|
dest: "{{ TOR_DNSMASQ_DROPIN }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
register: _tor_dnsmasq_dropin
|
|
- name: "🚀 Egress router | Enable + (re)start dnsmasq"
|
|
ansible.builtin.systemd:
|
|
name: dnsmasq
|
|
enabled: true
|
|
daemon_reload: "{{ TOR_DNSMASQ_OWNS_LISTENER | bool and _tor_dnsmasq_dropin is changed }}"
|
|
state: "{{ 'restarted' if (_tor_dnsmasq_conf is changed or _tor_dnsmasq_confdir is changed) else 'started' }}"
|
|
- name: "🔀 Egress router | Route host name resolution through dnsmasq"
|
|
when: TOR_DNSMASQ_OWNS_LISTENER | bool
|
|
block:
|
|
- name: "🔀 Egress router | Point the host resolver at dnsmasq (127.0.0.1)"
|
|
ansible.builtin.template: # nocheck: stack-host-template (per-host resolver config in /etc/resolv.conf)
|
|
src: resolv.conf.j2
|
|
dest: /etc/resolv.conf
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
unsafe_writes: true
|
|
- name: "🔀 Egress router | Let a name systemd-resolved refuses reach dnsmasq"
|
|
become: true
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/nsswitch.conf
|
|
regexp: "^(hosts:.*\\bresolve)\\s*\\[!UNAVAIL=return\\](.*)$"
|
|
line: "\\1 [SUCCESS=return NOTFOUND=continue TRYAGAIN=return]\\2"
|
|
backrefs: true
|
|
- name: "🔀 Egress router | Route .onion traffic and bound the ports that carry no policy"
|
|
ansible.builtin.include_role:
|
|
name: svc-net-firewall
|
|
|
|
- name: "🛡️ Egress guard | Read back the chain that bounds the ports"
|
|
become: true
|
|
ansible.builtin.command:
|
|
argv: [nft, list, chain, inet, "{{ firewall_loaded_table }}", guard]
|
|
register: tor_egress_guard_chain
|
|
changed_when: false
|
|
|
|
- name: "🛡️ Egress guard | Refuse a guard that drops nothing"
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'drop' in tor_egress_guard_chain.stdout"
|
|
fail_msg: >-
|
|
The guard chain in {{ firewall_loaded_table }} carries no drop, so
|
|
TransPort and DNSPort are reachable from every source. These two ports
|
|
have no access policy of their own; this chain is the only boundary.
|