chore: first commit
This commit is contained in:
@@ -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 ISP’s 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 ISP’s resolver doesn’t 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.
|
||||
```
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 %}
|
||||
@@ -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 }}"
|
||||
Reference in New Issue
Block a user