---
- 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: # nocheck: package-install only on the egress router this file is included for
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.