The original hardcoded Cloudflare plus Google with no way to say otherwise, and
rewrote /etc/systemd/resolved.conf wholesale — discarding DNSSEC, DNSOverTLS,
Domains and Cache if anything had set them, without mentioning it had. The
settings are a drop-in now, and the resolver is picked from a list with a
"keep what is there" that is the default.
The part worth having explicit is which layer is being changed. With
systemd-resolved there are two:
per-link what DHCP handed each interface, and what Tailscale installs on its
own. These answer for that link's domains — the provider's internal
names, the tailnet — and are printed by this step precisely to show
they are NOT being touched. Overriding them is how private
networking quietly stops resolving.
global the resolver used when no link claims the query. This is the one
the step sets.
On this host that distinction is live: eth0 has Hetzner's resolvers and
tailscale0 has 100.100.100.100, which is what answers ts.pastilhas.dev. Both are
left alone.
The drop-in is named 99- because systemd reads drop-ins in lexical order and the
LAST value wins. That is the opposite of sshd, whose drop-in three files away in
this same directory has to sort FIRST. Both are stated where they are written,
because getting it backwards fails silently in either direction.
resolv.conf is checked for actually pointing at resolved's stub before the
drop-in is trusted to do anything — a machine where something replaced the
symlink with a static file bypasses resolved entirely.
Resolution is tested afterwards rather than assumed. A resolver that does not
answer makes every later step fail for a reason that has nothing to do with it,
so that failure is reported and recorded rather than swallowed.
The choice names what each provider actually is, including that a resolver sees
every name the machine looks up.
Verified both paths against this host: keep reports unchanged, Quad9 renders the
right addresses, and the per-link display shows Hetzner and Tailscale correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
4.4 KiB
Bash
109 lines
4.4 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# machine-setup — network configuration
|
|
# =============================================================================
|
|
#
|
|
# Definitions only, like the other lib/ files.
|
|
|
|
[[ -n "${MACHINE_SETUP_NETWORK_LOADED:-}" ]] && return 0
|
|
MACHINE_SETUP_NETWORK_LOADED=1
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# DNS
|
|
# -----------------------------------------------------------------------------
|
|
#
|
|
# ── What is actually being changed here ──
|
|
#
|
|
# On a machine running systemd-resolved there are two layers, and only one of
|
|
# them is ours to set:
|
|
#
|
|
# per-link what DHCP handed each interface, and what Tailscale installs on
|
|
# its own. These answer for that link's domains — the provider's
|
|
# internal names, and the tailnet — and are NOT touched here.
|
|
# Overriding them is how private networking quietly stops resolving.
|
|
#
|
|
# global the resolver used when no link claims the query. This is what the
|
|
# step sets.
|
|
#
|
|
# So this changes where public lookups go, and leaves the machine's own networks
|
|
# resolving exactly as they did.
|
|
#
|
|
# ── Drop-in, and note the sort order ──
|
|
#
|
|
# systemd reads drop-ins in lexical order and the LAST value wins, so 99- is what
|
|
# overrides. That is the opposite of sshd, three files away in this same
|
|
# directory, where the FIRST value wins and the drop-in has to sort early. Worth
|
|
# stating because getting it backwards fails silently in both directions.
|
|
#
|
|
# The original rewrote /etc/systemd/resolved.conf wholesale, which discards
|
|
# anything else in it — DNSSEC, DNSOverTLS, Domains, Cache — without mentioning
|
|
# that it had.
|
|
|
|
RESOLVED_DROPIN=/etc/systemd/resolved.conf.d/99-machine-setup.conf
|
|
|
|
resolved_is_active() { systemctl is-active --quiet systemd-resolved 2>/dev/null; }
|
|
|
|
# The global resolvers in force, space separated, or empty if none are set.
|
|
dns_current_global() {
|
|
if resolved_is_active; then
|
|
resolvectl status 2>/dev/null | awk '/^ *DNS Servers:/ { $1 = ""; $2 = ""; print; exit }' | xargs
|
|
else
|
|
awk '/^nameserver/ { printf "%s ", $2 }' /etc/resolv.conf 2>/dev/null | xargs
|
|
fi
|
|
}
|
|
|
|
# What each interface was handed. Printed, never changed — the point is to show
|
|
# that this step is not touching them.
|
|
dns_per_link() {
|
|
resolved_is_active || return 0
|
|
resolvectl status 2>/dev/null |
|
|
awk '/^Link [0-9]+ \(/ { link = $3; gsub(/[()]/, "", link) }
|
|
/^ *DNS Servers:/ && link { $1 = ""; $2 = ""; printf "%s:%s\n", link, $0; link = "" }'
|
|
}
|
|
|
|
dns_set_global() {
|
|
local primary="$1" fallback="$2"
|
|
|
|
if resolved_is_active; then
|
|
install -d -m 0755 "$(dirname "$RESOLVED_DROPIN")"
|
|
cat >"$RESOLVED_DROPIN" <<EOF
|
|
# Written by machine-setup. 99- so it sorts last: systemd drop-ins are
|
|
# last-value-wins. Only the GLOBAL resolvers are set here — per-link DNS from
|
|
# DHCP and from Tailscale is left alone, so internal names keep resolving.
|
|
[Resolve]
|
|
DNS=${primary}
|
|
FallbackDNS=${fallback}
|
|
EOF
|
|
chmod 644 "$RESOLVED_DROPIN"
|
|
|
|
# resolv.conf has to point at the stub for any of this to be consulted. A
|
|
# machine where something replaced the symlink with a static file bypasses
|
|
# resolved entirely, and the drop-in would have no effect at all.
|
|
local target
|
|
target="$(readlink -f /etc/resolv.conf 2>/dev/null || true)"
|
|
if [[ "$target" != /run/systemd/resolve/*resolv.conf ]]; then
|
|
cp -a /etc/resolv.conf "/etc/resolv.conf.before-machine-setup" 2>/dev/null || true
|
|
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
|
|
fi
|
|
|
|
systemctl restart systemd-resolved
|
|
else
|
|
# No resolved: write resolv.conf directly, and say plainly that anything
|
|
# managing the interface may put its own back.
|
|
cp -a /etc/resolv.conf "/etc/resolv.conf.before-machine-setup" 2>/dev/null || true
|
|
if lsattr /etc/resolv.conf 2>/dev/null | cut -c1-20 | grep -q i; then
|
|
chattr -i /etc/resolv.conf
|
|
fi
|
|
{
|
|
echo "# Written by machine-setup."
|
|
local ns
|
|
for ns in $primary $fallback; do echo "nameserver ${ns}"; done
|
|
} >/etc/resolv.conf
|
|
fi
|
|
}
|
|
|
|
# Does name resolution actually work now? Asked after the change rather than
|
|
# assumed, because a resolver that does not answer is the one failure that makes
|
|
# everything after it look broken for unrelated reasons.
|
|
dns_works() { getent hosts one.one.one.one >/dev/null 2>&1 || getent hosts example.com >/dev/null 2>&1; }
|