router.ymlΒΆ

---
- 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.