chore: first commit

This commit is contained in:
Clément Désiles
2025-07-25 20:23:54 +02:00
parent 5c4016357f
commit c612cc7839
88 changed files with 3255 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
# Testing
## DNS leaks
```
browse https://www.dnsleaktest.com/
```
## DNSSEC
Testing DNSSEC validation
At this point we have a working server with supposedly working DNSSEC validation. Obviously we work on trust, but verify. To check that we have indeed a working validating server, we can run the following command:
```sh
dig www.nic.cz. +dnssec
```
The header section of the result should look like this:
```
; <<>> DiG 9.4.2-P2 <<>> www.nic.cz. +dnssec
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18417
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
```
See the bolded ad in the flags line? Now compare this to the output of the same command, but run on my MacBook using the ISPs resolver:
```
; <<>> DiG 9.10.6 <<>> www.nic.cz. +dnssec
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12527
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
```
The ISPs resolver doesnt support DNSSEC in this case, so you can see the ad flag missing. That flag indicates that the result from the upstream server validated.
# Race condition with wireguard
On unbound side:
```
systemd[1]: Starting unbound.service - Unbound DNS server...
unbound[74430]: [1747167722] unbound[74430:0] error: can't bind socket: Cannot assign requested address for 192.168.27.1>
unbound[74430]: [1747167722] unbound[74430:0] fatal error: could not open ports
systemd[1]: unbound.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: unbound.service: Failed with result 'exit-code'.
systemd[1]: Failed to start unbound.service - Unbound DNS server.
```
On wireguard side:
```
systemd[1]: Starting wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0...
wg-quick[72187]: [#] ip link add wg0 type wireguard
wg-quick[72187]: [#] wg setconf wg0 /dev/fd/63
wg-quick[72187]: [#] ip -4 address add 192.168.27.1/27 dev wg0
wg-quick[72187]: [#] ip link set mtu 1420 up dev wg0
wg-quick[72215]: [#] resolvconf -a tun.wg0 -m 0 -x
wg-quick[72261]: [1747167556] unbound-control[72261:0] warning: control-enable is 'no' in the config file.
wg-quick[72261]: [1747167556] unbound-control[72261:0] error: connect: Connection refused for 127.0.0.1 port 8953
wg-quick[72217]: run-parts: /etc/resolvconf/update.d/unbound exited with return code 1
wg-quick[72187]: [#] ip link delete dev wg0
systemd[1]: wg-quick@wg0.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: wg-quick@wg0.service: Failed with result 'exit-code'.
systemd[1]: Failed to start wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0.
```
+16
View File
@@ -0,0 +1,16 @@
unbound_config_base_path: /etc/unbound
unbound_config_path: "{{ unbound_config_base_path }}/unbound.conf"
unbound_root_hints_path: "{{ unbound_config_base_path }}/root.hints"
unbound_anchor_root_key: "{{ unbound_config_base_path }}/root.key"
unbound_ad_servers_config_path: "{{ unbound_config_base_path }}/ad_servers.conf"
unbound_custom_lan_config_path: "{{ unbound_config_base_path }}/lan.conf"
unbound_custom_vpn_config_path: "{{ unbound_config_base_path }}/vpn.conf"
unbound_custom_lan_domain: "example.lan"
unbound_port: 53
unbound_apparmor_profile_path: /etc/apparmor.d/usr.sbin.unbound
unbound_firewall_allowed_sources:
- 192.168.1.0/24 # lan0
- 192.168.27.0/27 # wg0
unbound_custom_lan_records:
"example.lan":
v4: 192.168.1.2
+16
View File
@@ -0,0 +1,16 @@
---
- name: Check Unbound config syntax
ansible.builtin.command: unbound-checkconf "{{ unbound_config_path }}"
register: unbound_validation
changed_when: false
failed_when: unbound_validation.rc != 0
- name: Reload systemd and restart unbound
ansible.builtin.systemd:
name: unbound
state: restarted
daemon_reload: yes
- name: Reload AppArmor profile
ansible.builtin.command: apparmor_parser -r {{ unbound_apparmor_profile_path }}
when: ansible_facts.apparmor.status == "enabled"
+131
View File
@@ -0,0 +1,131 @@
---
# see: https://calomel.org/unbound_dns.html
# see: https://wiki.archlinux.org/title/Unbound
- name: install unbound
package:
name: unbound
state: present
# Note: on archlinux this is already shipped within unbound
- name: install unbound-anchor on debian/ubuntu
package:
name: unbound-anchor
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: ensure unbound configuration is owned by unbound
ansible.builtin.shell: |
find "{{ unbound_config_base_path }}" -type d -exec chmod 755 {} \;
find "{{ unbound_config_base_path }}" -type f -exec chmod 644 {} \;
chown -R unbound:unbound "{{ unbound_config_base_path }}"
args:
executable: /bin/bash
- name: ensure apparmor profile for unbound exists
ansible.builtin.copy:
dest: /etc/apparmor.d/usr.sbin.unbound
content: |
/etc/unbound/** r,
/var/lib/unbound/** rwk,
owner: root
group: root
mode: "0644"
when: ansible_facts.apparmor.status == "enabled"
notify:
- Reload AppArmor profile
- name: check if root.hints exists
stat:
path: "{{ unbound_root_hints_path }}"
register: root_hints
- name: update root.hints (if older than 6 months or missing)
block:
- name: download latest root hints from internic
ansible.builtin.get_url:
url: https://www.internic.net/domain/named.root
dest: "{{ unbound_root_hints_path }}"
owner: unbound
group: unbound
mode: "0644"
when: >
(not root_hints.stat.exists) or
(ansible_date_time.epoch | int - root_hints.stat.mtime > 15552000)
- name: check if unbound ad_servers configuration exists
stat:
path: "{{ unbound_ad_servers_config_path }}"
register: ad_servers
- name: update the ad_servers list if older than 2 weeks or missing
block:
- name: download stevenblack's hosts file
ansible.builtin.get_url:
url: https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
dest: /tmp/hosts.txt
mode: "0644"
- name: convert hosts file to unbound format
ansible.builtin.shell: |
grep '^0\.0\.0\.0' /tmp/hosts.txt | awk '{print "local-zone: \""$2"\" always_nxdomain"}' > "{{ unbound_ad_servers_config_path }}" &&
chown unbound:unbound "{{ unbound_ad_servers_config_path }}"
args:
executable: /bin/bash
- name: clean up temporary file
ansible.builtin.file:
path: /tmp/hosts.txt
state: absent
when: >
(not ad_servers.stat.exists) or
(ansible_date_time.epoch | int - ad_servers.stat.mtime > 1209600)
- name: initialize dnssec trust anchor if missing
ansible.builtin.command: unbound-anchor -a {{ unbound_anchor_root_key }}
args:
creates: "{{ unbound_anchor_root_key }}"
- name: install unbound config
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: unbound
group: unbound
loop:
- { src: unbound.conf.j2, dest: "{{ unbound_config_path }}" }
- { src: custom-lan.conf.j2, dest: "{{ unbound_custom_lan_config_path }}" }
- { src: custom-vpn.conf.j2, dest: "{{ unbound_custom_vpn_config_path }}" }
notify:
- Check Unbound config syntax
- Reload systemd and restart unbound
- name: make sure unbound starts after wg-quick@wg0
block:
- name: ensure unbound.service.d directory exists
ansible.builtin.file:
path: /etc/systemd/system/unbound.service.d
state: directory
mode: "0755"
- name: configure unbound systemd service
ansible.builtin.copy:
dest: /etc/systemd/system/unbound.service.d/override.conf
content: |
[Unit]
After=wg-quick@wg0.service
Requires=wg-quick@wg0.service
notify: Reload systemd and restart unbound
- name: enables unbound service
ansible.builtin.service:
name: unbound
enabled: yes
state: started
- name: firewall ufw rules for unbound
community.general.ufw:
rule: allow
port: "{{ unbound_port }}"
proto: any
src: "{{ item }}"
direction: in
loop: "{{ unbound_firewall_allowed_sources | default([]) }}"
@@ -0,0 +1,9 @@
# {{ ansible_managed }}
view:
name: "lan"
{% for host, ips in unbound_custom_lan_records.items() %}
local-data: "{{ host }}. IN A {{ ips.v4 }}"
{% if ips.v6 is defined %}
local-data: "{{ host }}. IN AAAA {{ ips.v6 }}"
{% endif %}
{% endfor %}
@@ -0,0 +1,9 @@
# {{ ansible_managed }}
view:
name: "vpn"
{% for host, ips in unbound_custom_vpn_records.items() %}
local-data: "{{ host }}. IN A {{ ips.v4 }}"
{% if ips.v6 is defined %}
local-data: "{{ host }}. IN AAAA {{ ips.v6 }}"
{% endif %}
{% endfor %}
+110
View File
@@ -0,0 +1,110 @@
# {{ ansible_managed }}
# see: https://unix.stackexchange.com/a/617194
# see: https://unix.stackexchange.com/questions/617091/can-you-specify-a-different-configuration-for-different-interfaces-in-unbound
# see: https://www.dnsleaktest.com
server:
verbosity: 0
# listening port
port: {{ unbound_port }}
# Define interfaces binds
interface: lo
interface: lan0
interface: wg0
# Define access controls (note that ufw might be also configured)
access-control: 0.0.0.0/0 refuse
access-control: 192.168.1.0/24 allow # lan0 interface
access-control: 192.168.27.0/27 allow # wg0 interface
access-control: ::0/0 refuse
access-control: ::1 allow
# Specify custom local answers for each interface by using views:
access-control-view: 192.168.1.56/24 lan
access-control-view: 192.168.27.1/27 vpn
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: yes
# You want to leave this to no unless you have *native* IPv6. With 6to4 and
# Terredo tunnels your web browser should favor IPv4 for the same reasons
prefer-ip6: no
# Use this only when you downloaded the list of primary root servers!
# If you use the default dns-root-data package, unbound will find it automatically
root-hints: "{{ unbound_root_hints_path }}"
# enable to not answer id.server and hostname.bind queries.
hide-identity: yes
# enable to not answer version.server and version.bind queries.
hide-version: yes
# Trust glue only if it is within the server's authority
harden-glue: yes
# Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
harden-dnssec-stripped: yes
# Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
# see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
use-caps-for-id: no
# the time to live (TTL) value lower bound, in seconds. Default 0.
# If more than an hour could easily give trouble due to stale data.
cache-min-ttl: 3600
# the time to live (TTL) value cap for RRsets and messages in the
# cache. Items are not cached for longer. In seconds.
cache-max-ttl: 86400
# Reduce EDNS reassembly buffer size.
# IP fragmentation is unreliable on the Internet today, and can cause
# transmission failures when large DNS messages are sent via UDP. Even
# when fragmentation does work, it may not be secure; it is theoretically
# possible to spoof parts of a fragmented DNS message, without easy
# detection at the receiving end. Recently, there was an excellent study
# >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
# by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
# in collaboration with NLnet Labs explored DNS using real world data from the
# the RIPE Atlas probes and the researchers suggested different values for
# IPv4 and IPv6 and in different scenarios. They advise that servers should
# be configured to limit DNS messages sent over UDP to a size that will not
# trigger fragmentation on typical network links. DNS servers can switch
# from UDP to TCP when a DNS response is too big to fit in this limited
# buffer size. This value has also been suggested in DNS Flag Day 2020.
edns-buffer-size: 1232
# Perform prefetching of close to expired message cache entries
# This only applies to domains that have been frequently queried
prefetch: yes
# One thread should be sufficient, can be increased on beefy machines.
# In reality for most users running on small networks or on a single machine,
# it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
num-threads: 1
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
so-rcvbuf: 1m
# Ensure privacy of local IP ranges
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
# Allow the domain (and its subdomains) to contain private addresses.
# local-data statements are allowed to contain private addresses too.
private-domain: "{{ unbound_custom_lan_domain }}"
# Enable DNSSEC
auto-trust-anchor-file: "{{ unbound_anchor_root_key }}"
include: "{{ unbound_ad_servers_config_path }}"
include: "{{ unbound_custom_lan_config_path }}"
include: "{{ unbound_custom_vpn_config_path }}"