Package: dhcpcd-base
Version: 1:10.1.0-9
Description:
When using dhcpcd-base together with systemd-resolved, the name server settings
for an interface are initially configured correctly from DHCP but may then
subsequently disappear, leaving no DNS server configured.
This is because the dhcpcd hook scripts contain support for resolvconf, but the
implementation of resolvconf provided by systemd-resolved is very limited and
does not support the full semantics required (see below).
Steps to Reproduce:
1. Install systemd-resolved (257.5-2) along with dhcpcd-base (1:10.1.0-9) and
ifupdown (0.8.44)
2. Configure an interface for DHCP in /etc/network/interfaces.
As an example I am using a usermode (slirp) interface in QEMU:
allow-hotplug enp1s0
iface enp1s0 inet dhcp
3. Reboot, or restart the interface with ifdown/ifup.
4. Observe the DNS server configuration with resolvectl, repeating a few
minutes later.
Expected Behaviour:
resolvectl should show the DNS server address configured by DHCP. Name
resolution should work.
Observed Behaviour:
Shortly after the interface is brought up, resolvectl does indeed show that a
DNS server has been configured, e.g.:
Global
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Link 2 (enp1s0)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 172.17.2.3
DNS Servers: 172.17.2.3
Default Route: yes
But a few minutes later, the server seems to have disappeared again and DNS is
no longer working:
Global
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Link 2 (enp1s0)
Current Scopes: none
Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Default Route: no
Investigation:
Turning on the debug option for dhcpcd shows a number of hook script calls
after the interface has been configured, e.g:
Apr 20 21:07:36 redacted ifup[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks PREINIT
Apr 20 21:07:36 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks PREINIT
Apr 20 21:07:36 redacted ifup[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks NOCARRIER
Apr 20 21:07:36 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks NOCARRIER
Apr 20 21:07:36 redacted ifup[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks CARRIER
Apr 20 21:07:36 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks CARRIER
Apr 20 21:07:39 redacted ifup[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks ROUTERADVERT
Apr 20 21:07:39 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks ROUTERADVERT
Apr 20 21:07:41 redacted ifup[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks BOUND
Apr 20 21:07:41 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks BOUND
Apr 20 21:12:20 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks ROUTERADVERT
Apr 20 21:21:56 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks ROUTERADVERT
Apr 20 21:28:29 redacted dhcpcd[1015]: enp1s0: executing:
/usr/lib/dhcpcd/dhcpcd-run-hooks ROUTERADVERT
Examining the hook script /usr/lib/dhcpcd/dhcpcd-hooks/20-resolv.conf, shows
that when a resolvconf binary is present, each hook call will ultimately invoke
resolvconf to add or delete the DNS settings for a particular protocol on the
interface. This results in a series of invocations such as the following:
resolvconf -d enp1s0.dhcp -f
resolvconf -d enp1s0.link -f
resolvconf -d enp1s0.link -f
resolvconf -d enp1s0.ra -f
resolvconf -a enp1s0.dhcp (with a resolv.conf-formatted file piped to
stdin)
resolvconf -d enp1s0.ra -f
This is where the difference in semantics becomes apparent between the versions
of Debian resolvconf/openresolv and the resolvconf provided by systemd-resolved:
The "traditional" resolvconf maintains a separate state for each protocol on
the interface, and generates a superposition of the information from all
protocols. By contrast, the systemd version ignores the protocol suffix
completely and stores a single state for all protocols. The last information
supplied for any protocol typically wins out.
In the particular example above, it appears that an IPv6 router advertisement
for "enp1s0.ra" has deleted the DNS configuration previously set for
"enp1s0.ra". I expect that various other sequences of events may lead to a
similar result.
Possible Solutions:
I attach a patch which modifies the dhcpcd hook scripts to work around the
problem: We detect when the systemd version of resolvconf is being used and
provide it with a resolv.conf fragment that contains the superposition of all
the protocols on a given interface (retrieved from state stored under
/run/dhcpcd).
I have not tested the patch extensively, although it has worked well enough in
my test cases.
Note that the ifupdown and isc-dhcp-client packages already have a their own
hook scripts (borrowed from Ubuntu) for dealing with systemctl-resolved. These
scripts call resolvectl to set the DNS servers, and there is a shared state
that is maintained by both packages so they are to some extent integrated
together. Since dhcpcd has now replaced isc-dhcp-client as the default DHCP
client for trixie, this integration now seems to be rather pointless. See as
follows:
https://sources.debian.org/src/ifupdown/0.8.44/debian/if-up.d/resolved/
https://sources.debian.org/src/isc-dhcp/4.4.3-P1-7/debian/resolved/
Remarks:
It seems to me that the need for all these hacky hook scripts has come about
because systemd's implementation of resolvconf is too limited to be of use as a
drop-in replacement for resolvconf with the various "legacy" networking
packages. Rather than propagating these bespoke workarounds into different
packages, it may be better if they could all continue to use the "traditional"
resolvconf/openresolv system to configure resolved.
I think this could be accomplished fairly cleanly and easily if the
systemd-resolved package were to simply omit installing the "resolvconf"
symbolic link, and drop the "Provides: resolvconf" and "Conflicts: resolvconf"
dependences, allowing it to be installed alongside the "traditional"
resolvconf. At the same time, resolvconf/openresolv could be equipped with a
unified hook script to detect systemd-resolved and call resolvectl. Then all
the hacks elsewhere could go away!
If anyone can suggest a better forum to raise that wider issue then please let
me know and I will do so.
diff --git a/hooks/20-resolv.conf b/hooks/20-resolv.conf
index 7c29e276..a8231ece 100644
--- a/hooks/20-resolv.conf
+++ b/hooks/20-resolv.conf
@@ -17,6 +17,72 @@ else
have_resolvconf=false
fi
+# Detect the limited resolvconf compatibility provided by systemd-resolved
+if [ "$(basename "$(readlink -f "$(which "$resolvconf")")")" = "resolvectl" ]; then
+ have_systemd_resolved=true
+ have_resolvconf=false
+else
+ have_systemd_resolved=false
+fi
+
+update_systemd_resolved()
+{
+ # Build a list of interface.protocol names for this interface
+ interfaces=
+ for x in "$resolv_conf_dir/$interface".*; do
+ [ -f "$x" ] && interfaces="$interfaces${interfaces:+ }${x##*/}"
+ done
+ uniqify $interfaces
+
+ # Obtain name servers and search domains for all protocols on this interface
+ search=
+ servers=
+ if [ -n "$interfaces" ]; then
+ # Build the search list
+ domain=$(cd "$resolv_conf_dir"; \
+ key_get_value "domain " ${interfaces})
+ search=$(cd "$resolv_conf_dir"; \
+ key_get_value "search " ${interfaces})
+ search="$(uniqify $domain $search)"
+ [ -n "$search" ] && search="search $search$NL"
+
+ # Build the nameserver list
+ srvs=$(cd "$resolv_conf_dir"; \
+ key_get_value "nameserver " ${interfaces})
+ for x in $(uniqify $srvs); do
+ servers="${servers}nameserver $x$NL"
+ done
+ fi
+
+ # Submit the configuration for this interface to (systemd's) resolvconf
+ if [ -n "$servers" ] ; then
+ conf="$search$servers"
+ syslog debug "$resolvconf -a $interface <<< $conf"
+ printf %s "$conf" | "$resolvconf" -a "$interface"
+ else
+ syslog debug "$resolvconf -d $interface"
+ "$resolvconf" -d "$interface"
+ fi
+}
+
+add_systemd_resolved()
+{
+ if [ -e "$resolv_conf_dir/$ifname" ]; then
+ rm -f "$resolv_conf_dir/$ifname"
+ fi
+ [ -d "$resolv_conf_dir" ] || mkdir -p "$resolv_conf_dir"
+ printf %s "$conf" > "$resolv_conf_dir/$ifname"
+ update_systemd_resolved
+}
+
+remove_systemd_resolved()
+{
+ if [ -e "$resolv_conf_dir/$ifname" ]; then
+ rm -f "$resolv_conf_dir/$ifname"
+ fi
+ update_systemd_resolved
+}
+
build_resolv_conf()
{
cf="$state_dir/resolv.conf.$ifname"
@@ -170,6 +236,12 @@ add_resolv_conf()
for x in ${new_domain_name_servers}; do
conf="${conf}nameserver $x$NL"
done
+
+ if $have_systemd_resolved; then
+ add_systemd_resolved
+ return $?
+ fi
+
if $have_resolvconf; then
[ -n "$ifmetric" ] && export IF_METRIC="$ifmetric"
printf %s "$conf" | "$resolvconf" -a "$ifname"
@@ -186,6 +258,11 @@ add_resolv_conf()
remove_resolv_conf()
{
+ if $have_systemd_resolved; then
+ remove_systemd_resolved
+ return $?
+ fi
+
if $have_resolvconf; then
"$resolvconf" -d "$ifname" -f
else