wren-linux / bin / secure-dns-switch
#!/bin/bash
# secure-dns-switch on|off|status: written by Wren Linux parts/secure-dns.sh
# Installed as /usr/local/libexec/wren-secure-dns, run as root. Your user
# may run "on" and "off" without a password (a rule in /etc/sudoers.d): the
# Wren menu's Secure DNS line and the secure-dns command use it.
# on every lookup goes to Unbound on this computer (encrypted, checked
# with DNSSEC), and a firewall rule lets only Unbound send DNS out.
# If names don't resolve that way (a Wi-Fi sign-in page, port 853
# blocked), it switches off again and says so.
# off DNS goes to the network's own server again, unencrypted: for Wi-Fi
# with a sign-in page (or use the Wi-Fi sign-in window, which does
# this for you). Wren Linux's Firefox then encrypts its own lookups
# (it reads this at its start: bin/firefox-session).
# status on or off
set -euo pipefail
umask 022
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
RESOLV=/etc/resolv.conf
BACKUP=/etc/resolv.conf.wren-secure-dns.bak
CONF=/etc/unbound/unbound.conf.d/wren-secure-dns.conf
NM_CONF=/etc/NetworkManager/conf.d/90-wren-secure-dns.conf
DHCLIENT_HOOK=/etc/dhcp/dhclient-enter-hooks.d/wren-secure-dns
DHCPCD_CONF=/etc/dhcpcd.conf
DHCPCD_BEGIN='# >>> Wren Linux secure-dns >>>'
DHCPCD_END='# <<< Wren Linux secure-dns <<<'
UNIT=wren-secure-dns-firewall.service
# What /etc/resolv.conf says while it is on (an older Wren Linux wrote "extras/")
MARK='^# Written by Wren Linux (parts|extras)/secure-dns\.sh'
say() { printf 'secure-dns: %s\n' "$*"; }
die() { printf 'secure-dns: %s\n' "$*" >&2; exit 1; }
is_on() { grep -qsE "$MARK" "$RESOLV" && [ ! -L "$RESOLV" ]; }
reload_nm() {
if systemctl is-active --quiet NetworkManager; then nmcli general reload conf >/dev/null 2>&1 || true; fi
}
# The things that would rewrite /etc/resolv.conf with the network's DNS server
keep_hands_off() {
printf '%s\n' '# Written by Wren Linux parts/secure-dns.sh: NetworkManager leaves' \
'# /etc/resolv.conf alone; it points at Unbound on this computer.' \
'[main]' 'dns=none' 'rc-manager=unmanaged' |
install -D -m 0644 /dev/stdin "$NM_CONF"
# dhclient (ifupdown's wired connections) calls make_resolv_conf; an enter
# hook can replace it with nothing
printf '%s\n' '# Written by Wren Linux parts/secure-dns.sh: leave /etc/resolv.conf alone;' \
'# it points at Unbound on this computer.' 'make_resolv_conf() { :; }' |
install -D -m 0644 /dev/stdin "$DHCLIENT_HOOK"
if [ -f "$DHCPCD_CONF" ] && ! grep -qxF "$DHCPCD_BEGIN" "$DHCPCD_CONF"; then
printf '%s\n' "$DHCPCD_BEGIN" 'nohook resolv.conf' "$DHCPCD_END" >>"$DHCPCD_CONF"
fi
reload_nm
}
hands_back() {
rm -f "$NM_CONF" "$DHCLIENT_HOOK"
if [ -f "$DHCPCD_CONF" ]; then
sed -i "\\|^$DHCPCD_BEGIN\$|,\\|^$DHCPCD_END\$|d" "$DHCPCD_CONF"
fi
reload_nm
}
# resolv.conf back: the copy, for a computer without NetworkManager; with it,
# NetworkManager writes it again, as the copy is from where secure DNS was
# switched on and this may be another network by now (a cafe, with the home
# router's address in the copy, and its sign-in page out of reach)
resolv_back() {
if [ -e "$BACKUP" ]; then
mv "$BACKUP" "$RESOLV"
elif is_on; then
rm -f "$RESOLV"
else
return 0
fi
if systemctl is-active --quiet NetworkManager; then
nmcli general reload dns-full >/dev/null 2>&1 || systemctl restart NetworkManager
elif [ ! -e "$RESOLV" ]; then
say "There was no copy of the old $RESOLV: no DNS server until this computer reconnects."
fi
}
firewall_on() { systemctl enable "$UNIT" >/dev/null 2>&1 && systemctl restart "$UNIT"; }
firewall_off() {
systemctl disable --now "$UNIT" >/dev/null 2>&1 || true
nft delete table inet wren_secure_dns 2>/dev/null || true
}
# resolves: names work now (a few tries: Unbound may still be connecting)
resolves() {
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
getent hosts deb.debian.org >/dev/null 2>&1 && return 0
[ "$i" = 10 ] || sleep 2
done
return 1
}
switch_off() {
firewall_off
hands_back
resolv_back
}
[ "$(id -u)" = 0 ] || die "this runs as root (the secure-dns command calls it through sudo)."
[ $# = 1 ] || die "usage: secure-dns-switch on|off|status"
case $1 in
status)
if is_on; then echo on; else echo off; fi ;;
off)
if ! is_on; then
say "already off."
exit 0
fi
switch_off
say "off: DNS goes to the network's own server, unencrypted. Firefox encrypts its own (restart it)." ;;
on)
[ -f "$CONF" ] || die "Unbound isn't set up for it: wrenctl part secure-dns"
[ ! -L "$RESOLV" ] || die "$RESOLV is a link ($(readlink "$RESOLV")): something else runs DNS here."
if is_on; then
say "already on."
exit 0
fi
systemctl is-active --quiet unbound || systemctl restart unbound ||
die "Unbound didn't start (journalctl -u unbound says why); DNS stays as it was."
# A copy of what is there now, to put back when it is switched off
if [ -e "$RESOLV" ]; then cp -a "$RESOLV" "$BACKUP"; fi
keep_hands_off
printf '%s\n' '# Written by Wren Linux parts/secure-dns.sh. Every name goes to Unbound on this' \
'# computer, which asks your chosen resolver over an encrypted connection.' \
'nameserver 127.0.0.1' 'options edns0 trust-ad' >"$RESOLV.wren-new"
chmod 0644 "$RESOLV.wren-new"
mv "$RESOLV.wren-new" "$RESOLV"
if ! resolves; then
switch_off
die "names don't resolve through the encrypted resolver (a Wi-Fi sign-in page, port 853 blocked, or the resolver is down), so it stays off. Try again once you are signed in."
fi
if ! firewall_on || ! resolves; then
firewall_off
say "the firewall rule that keeps DNS from going around Unbound didn't load; encrypted DNS is on without it (systemctl status $UNIT)."
fi
say "on: every lookup goes to Unbound, encrypted and checked with DNSSEC. Restart Firefox to follow." ;;
*) die "usage: secure-dns-switch on|off|status" ;;
esac