#!/bin/sh
#
# alloy-dns - point systemd-resolved at a chosen resolver, or back at the
# network's own.
#
# Run by `alloy settings`' System tab through `run0`, because the setting is a
# file in /etc/systemd/resolved.conf.d and writing there is root's. There is no
# `resolvectl` verb that persists across a reboot, and a DNS setting that
# quietly reverts is worse than one that never applied.
#
# THE RESOLVER TABLE IS NOT HERE, ON PURPOSE. The console passes the servers on
# the command line and this writes what it is given. Keeping a second copy of
# the addresses in shell is how the two would start to disagree, and the log
# pane showing the whole argv is the console's promise about every action it
# takes: what is written is what is on screen.
#
# Usage:
#   alloy-dns network                     back to whatever the network hands out
#   alloy-dns <id> <server> [server...]   name a resolver
#   alloy-dns --report <id> [server...]   say what would happen, write nothing
#
# A server is systemd-resolved's `DNS=` syntax, `address#hostname`, where the
# hostname is the certificate name. Without it `DNSOverTLS=yes` has nothing to
# validate and the encryption only proves something answered.

set -eu

DROP_IN=/etc/systemd/resolved.conf.d/20-alloy-resolver.conf
SHIPPED=/etc/systemd/resolved.conf.d/10-alloy-dns.conf
REPORT=0

die() { printf 'alloy-dns: %s\n' "$*" >&2; exit 1; }

[ $# -ge 1 ] || die "usage: alloy-dns [--report] <id|network> [server...]"
case "$1" in
  --report|-n|--dry-run) REPORT=1; shift ;;
esac
[ $# -ge 1 ] || die "no resolver named"

id="$1"; shift

# The id is written into a file and read back, so it is bookkeeping rather than
# free text. Refusing anything else keeps a hostile value out of the drop-in.
case "$id" in
  *[!a-z0-9-]*|'') die "not a resolver id: $id" ;;
esac

if [ "$id" = network ]; then
  [ $# -eq 0 ] || die "the network's own resolver takes no servers"
  if [ "$REPORT" = 1 ]; then
    if [ -e "$DROP_IN" ]; then
      echo "would remove $DROP_IN, leaving the shipped $SHIPPED"
      echo "would restart systemd-resolved"
    else
      echo "nothing to do: no resolver is set, so the network's own is in use"
    fi
    exit 0
  fi
  if [ -e "$DROP_IN" ]; then
    rm -f "$DROP_IN"
    echo "removed $DROP_IN"
  else
    echo "no resolver was set; the network's own was already in use"
  fi
  systemctl restart systemd-resolved
  echo "restarted systemd-resolved"
  exit 0
fi

[ $# -ge 1 ] || die "$id needs at least one server, as address#hostname"

# Validate every server before writing any of them. A half-written drop-in
# naming one good server and one malformed line is a resolver that half works,
# which is harder to diagnose than one that was refused.
for server in "$@"; do
  case "$server" in
    *'#'*) ;;
    *) die "$server has no #hostname, so there is no certificate name to check" ;;
  esac
  case "$server" in
    *[!0-9a-fA-F.:#a-z-]*) die "$server is not an address#hostname" ;;
  esac
done

if [ "$REPORT" = 1 ]; then
  echo "would write $DROP_IN naming $id:"
  for server in "$@"; do echo "  DNS=$server"; done
  echo "  DNSOverTLS=yes"
  echo "would restart systemd-resolved"
  exit 0
fi

# Written beside the file rather than in /tmp, so the rename is within one
# filesystem and resolved never reads a partial drop-in.
tmp="$(mktemp "${DROP_IN}.XXXXXX")"
trap 'rm -f "$tmp"' EXIT

{
  printf '# Written by alloy-dns. Chosen in `alloy settings`, System tab.\n'
  printf '#\n'
  printf '# The line below is how the choice is read back; it is a comment because\n'
  printf '# resolved would reject an unknown key.\n'
  printf '# alloy-resolver: %s\n' "$id"
  printf '#\n'
  printf '# DNSOverTLS=yes rather than opportunistic, and only because a resolver\n'
  printf '# was named: these servers are known to speak it, so falling back to\n'
  printf '# plaintext would give away the whole point of choosing one. The shipped\n'
  printf '# %s stays opportunistic for the network default,\n' "$SHIPPED"
  printf '# where the uplink usually cannot.\n'
  printf '[Resolve]\n'
  for server in "$@"; do printf 'DNS=%s\n' "$server"; done
  printf 'DNSOverTLS=yes\n'
} > "$tmp"

chmod 0644 "$tmp"
mv -f "$tmp" "$DROP_IN"
trap - EXIT

# /etc is SELinux-labelled and enforcing on an installed machine, and mv
# carries the temp file's label rather than the one the policy wants. Same
# correction build/check-installed.sh exists to catch elsewhere in /etc.
if command -v restorecon >/dev/null 2>&1; then
  restorecon -F "$DROP_IN" 2>/dev/null || true
fi

echo "wrote $DROP_IN naming $id"
systemctl restart systemd-resolved
echo "restarted systemd-resolved"
