Skip to main content

max / alloy

4.5 KB · 129 lines History Blame Raw
1 #!/bin/sh
2 #
3 # alloy-dns - point systemd-resolved at a chosen resolver, or back at the
4 # network's own.
5 #
6 # Run by `alloy settings`' System tab through `run0`, because the setting is a
7 # file in /etc/systemd/resolved.conf.d and writing there is root's. There is no
8 # `resolvectl` verb that persists across a reboot, and a DNS setting that
9 # quietly reverts is worse than one that never applied.
10 #
11 # THE RESOLVER TABLE IS NOT HERE, ON PURPOSE. The console passes the servers on
12 # the command line and this writes what it is given. Keeping a second copy of
13 # the addresses in shell is how the two would start to disagree, and the log
14 # pane showing the whole argv is the console's promise about every action it
15 # takes: what is written is what is on screen.
16 #
17 # Usage:
18 # alloy-dns network back to whatever the network hands out
19 # alloy-dns <id> <server> [server...] name a resolver
20 # alloy-dns --report <id> [server...] say what would happen, write nothing
21 #
22 # A server is systemd-resolved's `DNS=` syntax, `address#hostname`, where the
23 # hostname is the certificate name. Without it `DNSOverTLS=yes` has nothing to
24 # validate and the encryption only proves something answered.
25
26 set -eu
27
28 DROP_IN=/etc/systemd/resolved.conf.d/20-alloy-resolver.conf
29 SHIPPED=/etc/systemd/resolved.conf.d/10-alloy-dns.conf
30 REPORT=0
31
32 die() { printf 'alloy-dns: %s\n' "$*" >&2; exit 1; }
33
34 [ $# -ge 1 ] || die "usage: alloy-dns [--report] <id|network> [server...]"
35 case "$1" in
36 --report|-n|--dry-run) REPORT=1; shift ;;
37 esac
38 [ $# -ge 1 ] || die "no resolver named"
39
40 id="$1"; shift
41
42 # The id is written into a file and read back, so it is bookkeeping rather than
43 # free text. Refusing anything else keeps a hostile value out of the drop-in.
44 case "$id" in
45 *[!a-z0-9-]*|'') die "not a resolver id: $id" ;;
46 esac
47
48 if [ "$id" = network ]; then
49 [ $# -eq 0 ] || die "the network's own resolver takes no servers"
50 if [ "$REPORT" = 1 ]; then
51 if [ -e "$DROP_IN" ]; then
52 echo "would remove $DROP_IN, leaving the shipped $SHIPPED"
53 echo "would restart systemd-resolved"
54 else
55 echo "nothing to do: no resolver is set, so the network's own is in use"
56 fi
57 exit 0
58 fi
59 if [ -e "$DROP_IN" ]; then
60 rm -f "$DROP_IN"
61 echo "removed $DROP_IN"
62 else
63 echo "no resolver was set; the network's own was already in use"
64 fi
65 systemctl restart systemd-resolved
66 echo "restarted systemd-resolved"
67 exit 0
68 fi
69
70 [ $# -ge 1 ] || die "$id needs at least one server, as address#hostname"
71
72 # Validate every server before writing any of them. A half-written drop-in
73 # naming one good server and one malformed line is a resolver that half works,
74 # which is harder to diagnose than one that was refused.
75 for server in "$@"; do
76 case "$server" in
77 *'#'*) ;;
78 *) die "$server has no #hostname, so there is no certificate name to check" ;;
79 esac
80 case "$server" in
81 *[!0-9a-fA-F.:#a-z-]*) die "$server is not an address#hostname" ;;
82 esac
83 done
84
85 if [ "$REPORT" = 1 ]; then
86 echo "would write $DROP_IN naming $id:"
87 for server in "$@"; do echo " DNS=$server"; done
88 echo " DNSOverTLS=yes"
89 echo "would restart systemd-resolved"
90 exit 0
91 fi
92
93 # Written beside the file rather than in /tmp, so the rename is within one
94 # filesystem and resolved never reads a partial drop-in.
95 tmp="$(mktemp "${DROP_IN}.XXXXXX")"
96 trap 'rm -f "$tmp"' EXIT
97
98 {
99 printf '# Written by alloy-dns. Chosen in `alloy settings`, System tab.\n'
100 printf '#\n'
101 printf '# The line below is how the choice is read back; it is a comment because\n'
102 printf '# resolved would reject an unknown key.\n'
103 printf '# alloy-resolver: %s\n' "$id"
104 printf '#\n'
105 printf '# DNSOverTLS=yes rather than opportunistic, and only because a resolver\n'
106 printf '# was named: these servers are known to speak it, so falling back to\n'
107 printf '# plaintext would give away the whole point of choosing one. The shipped\n'
108 printf '# %s stays opportunistic for the network default,\n' "$SHIPPED"
109 printf '# where the uplink usually cannot.\n'
110 printf '[Resolve]\n'
111 for server in "$@"; do printf 'DNS=%s\n' "$server"; done
112 printf 'DNSOverTLS=yes\n'
113 } > "$tmp"
114
115 chmod 0644 "$tmp"
116 mv -f "$tmp" "$DROP_IN"
117 trap - EXIT
118
119 # /etc is SELinux-labelled and enforcing on an installed machine, and mv
120 # carries the temp file's label rather than the one the policy wants. Same
121 # correction build/check-installed.sh exists to catch elsewhere in /etc.
122 if command -v restorecon >/dev/null 2>&1; then
123 restorecon -F "$DROP_IN" 2>/dev/null || true
124 fi
125
126 echo "wrote $DROP_IN naming $id"
127 systemctl restart systemd-resolved
128 echo "restarted systemd-resolved"
129