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

Reply via email to