I was motivated to try fixing this after installing systemd-resolved on bookworm and finding that my DNS was completely broken.
I have tested the Ubuntu hook scripts with DHCP enabled for both IPv4 and/or IPv6. They work well for me so far. I have attached a source patch against isc-dhcp master on Salsa. This just adds the verbatim hook scripts from Ubuntu. The enter hook script simply disables the make_resolv_conf() shell function if resolved is enabled. This is useful by itself, since the default make_resolv_conf() chases down symbolic links from /etc/resolv.conf and trashes the stub resolver config in /run/systemd/resolved. The exit hook script does the work of feeding DNS information to resolvectl: Note that it doesn't bother with using the resolvconf emulation provided by systemd-resolved. It's a bit of a brittle solution but it does seem to work. The script assumes it has been sourced from within a function (it uses local and return statements without defining a function itself), but presumably the Ubuntu folks were OK with that. Also I suspect there might be the possibility of a race condition if both DHCPv4 and v6 are configured at the same time with separate dhclient processes (this is possible with ifupdown). This might result in the DNS config from one process getting clobbered by the other. That's just from eyeballing the script, I can't be sure. Note that a similar script is already being used for static DNS configuration by the ifupdown package, see bug #1031236. These scripts modify the same state files so they will need to keep in step, although problems are only likely if you try to configure DNS separately for both inet and inet6 on the same interface. I've tried various simple combinations of config for static/dhcp and inet/inet6 configuration against the patch given here along with that on bug #1031236 and they do seem to work well together.
diff --git a/debian/isc-dhcp-client.install b/debian/isc-dhcp-client.install index c2896c5..d85108b 100644 --- a/debian/isc-dhcp-client.install +++ b/debian/isc-dhcp-client.install @@ -3,5 +3,7 @@ dhclient sbin debian/dhclient.conf etc/dhcp debian/debug etc/dhcp +debian/resolved-enter etc/dhcp/dhclient-enter-hooks.d/ +debian/resolved etc/dhcp/dhclient-exit-hooks.d/ debian/apparmor/sbin.dhclient etc/apparmor.d diff --git a/debian/resolved b/debian/resolved new file mode 100644 index 0000000..48b0fc3 --- /dev/null +++ b/debian/resolved @@ -0,0 +1,138 @@ +#!/bin/sh +## Sourced, not exec'ed, but helps syntax highlighting +# +# Script fragment to make dhclient supply nameserver information to resolved +# + +# Tips: +# * Be careful about changing the environment since this is sourced +# * This script fragment uses bash features +# * As of isc-dhcp-client 4.2 the "reason" (for running the script) can be one of the following. +# (Listed on man page:) MEDIUM(0) PREINIT(0) BOUND(M) RENEW(M) REBIND(M) REBOOT(M) EXPIRE(D) FAIL(D) RELEASE(D) STOP(D) NBI(-) TIMEOUT(M) +# (Also used in master script:) ARPCHECK(0), ARPSEND(0) +# (Also used in master script:) PREINIT6(0) BOUND6(M) RENEW6(M) REBIND6(M) DEPREF6(0) EXPIRE6(D) RELEASE6(D) STOP6(D) +# (0) = master script does not run make_resolv_conf +# (M) = master script runs make_resolv_conf +# (D) = master script downs interface +# (-) = master script does nothing with this + +if systemctl is-enabled systemd-resolved > /dev/null 2>&1; then + local mystatedir statedir ifindex + + if [ ! "$interface" ] ; then + return + fi + ifindex=$(cat "/sys/class/net/$interface/ifindex") + if [ ! "$ifindex" ]; then + return + fi + mystatedir=/run/network + mkdir -p $mystatedir + + statedir=/run/systemd/resolve/netif + mkdir -p $statedir + chown systemd-resolve:systemd-resolve $statedir + + local oldstate="$(mktemp)" + # ignore errors due to nonexistent file + md5sum "$mystatedir/isc-dhcp-v4-$interface" "$mystatedir/isc-dhcp-v6-$interface" "$mystatedir/ifupdown-inet-$interface" "$mystatedir/ifupdown-inet6-$interface" > "$oldstate" 2> /dev/null || true + + case "$reason" in + BOUND|RENEW|REBIND|REBOOT|TIMEOUT|BOUND6|RENEW6|REBIND6) + if [ -n "$new_domain_name_servers" ] ; then + cat <<EOF >"$mystatedir/isc-dhcp-v4-$interface" +DNS="$new_domain_name_servers" +EOF + if [ -n "$new_domain_name" ] || [ -n "$new_domain_search" ] ; then + cat <<EOF >>"$mystatedir/isc-dhcp-v4-$interface" +DOMAINS="$new_domain_search $new_domain_name" +EOF + fi + fi + if [ -n "$new_dhcp6_name_servers" ] ; then + cat <<EOF >"$mystatedir/isc-dhcp-v6-$interface" +DNS6="$new_dhcp6_name_servers" +EOF + if [ -n "$new_dhcp6_domain_search" ] ; then + cat <<EOF >>"$mystatedir/isc-dhcp-v6-$interface" +DOMAINS6="$new_dhcp6_domain_search" +EOF + fi + fi + ;; + + EXPIRE|FAIL|RELEASE|STOP) + rm -f "/run/network/isc-dhcp-v4-$interface" + ;; + EXPIRE6|RELEASE6|STOP6) + rm -f "/run/network/isc-dhcp-v6-$interface" + ;; + esac + + local newstate="$(mktemp)" + # ignore errors due to nonexistent file + md5sum "$mystatedir/isc-dhcp-v4-$interface" "$mystatedir/isc-dhcp-v6-$interface" "$mystatedir/ifupdown-inet-$interface" "$mystatedir/ifupdown-inet6-$interface" > "$newstate" 2> /dev/null || true + if ! cmp --silent "$oldstate" "$newstate" 2>/dev/null; then + local DNS DNS6 DOMAINS DOMAINS6 DEFAULT_ROUTE + # v4 first + if [ -e "$mystatedir/isc-dhcp-v4-$interface" ]; then + . "$mystatedir/isc-dhcp-v4-$interface" + fi + # v4 manual config overrides + if [ -e "$mystatedir/ifupdown-inet-$interface" ]; then + . "$mystatedir/ifupdown-inet-$interface" + fi + # v6 preffered + if [ -e "$mystatedir/isc-dhcp-v6-$interface" ]; then + . "$mystatedir/isc-dhcp-v6-$interface" + fi + # v6 manual config overrides + if [ -e "$mystatedir/ifupdown-inet6-$interface" ]; then + . "$mystatedir/ifupdown-inet6-$interface" + fi + local resolvectl_failed= + if [ "$DNS" ] || [ "$DNS6" ] ; then + cat <<EOF >"$statedir/$ifindex" +# This is private data. Do not parse. +LLMNR=yes +MDNS=no +SERVERS=$(echo $DNS6 $DNS) +DOMAINS=$(echo $DOMAINS6 $DOMAINS) +EOF + if [ -n "$DEFAULT_ROUTE" ]; then + cat <<EOF >>"$statedir/$ifindex" +DEFAULT_ROUTE=$DEFAULT_ROUTE +EOF + fi + chown systemd-resolve:systemd-resolve "$statedir/$ifindex" + # In addition to creating the state file (needed if we run before + # resolved is started), also feed the information directly to + # resolved. + if systemctl --quiet is-active systemd-resolved; then + resolvectl llmnr "$ifindex" yes || resolvectl_failed=$? + resolvectl mdns "$ifindex" no || resolvectl_failed=$? + if [ "$DOMAINS6" ] || [ "$DOMAINS" ]; then + resolvectl domain "$ifindex" $DOMAINS6 $DOMAINS || resolvectl_failed=$? + else + resolvectl domain "$ifindex" "" || resolvectl_failed=$? + fi + resolvectl dns "$ifindex" $DNS6 $DNS || resolvectl_failed=$? + if [ "$DEFAULT_ROUTE" ]; then + resolvectl default-route "$ifindex" $DEFAULT_ROUTE || resolvectl_failed=$? + fi + fi + else + rm -f "$statedir/$ifindex" + if systemctl --quiet is-active systemd-resolved; then + resolvectl revert "$ifindex" || resolvectl_failed=$? + fi + fi + + # resolved was running, but without dbus, it means state files + # will not be read & resolvectl commands failed, restart it + if [ "$resolvectl_failed" ]; then + systemctl try-restart systemd-resolved + fi + fi + rm -f "$oldstate" "$newstate" +fi diff --git a/debian/resolved-enter b/debian/resolved-enter new file mode 100644 index 0000000..19d4612 --- /dev/null +++ b/debian/resolved-enter @@ -0,0 +1,12 @@ +#!/bin/sh +## Sourced, not exec'ed, but helps syntax highlighting +# +# Script fragment disabling make_resolv_conf(), +# to be used in conjunction with dhclient-exit-hooks.d/resolved +# + +if systemctl is-enabled systemd-resolved > /dev/null 2>&1; then + # Undefine the nasty default make_resolv_conf() to avoid it overriding + # /etc/resolv.conf (or /run/systemd/resolve/stub-resolv.conf) + make_resolv_conf() { : ; } +fi