From 388b12451341494b76976c40467b3785767eddf2 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Tue, 18 Jul 2023 12:43:42 +0200 Subject: [PATCH] net/dns: detect when libnss_resolve is used Having `127.0.0.53` is not the only way to use `systemd-resolved`. An alternative way is to enable `libnss_resolve` module, which seems to now be used by default on Debian 12 bookworm. Fixes #8549 Signed-off-by: Anton Tolchanov --- net/dns/manager_linux.go | 46 ++++++++++++++++++++---- net/dns/manager_linux_test.go | 66 +++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/net/dns/manager_linux.go b/net/dns/manager_linux.go index b61c36289..642fe99c5 100644 --- a/net/dns/manager_linux.go +++ b/net/dns/manager_linux.go @@ -139,7 +139,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) { // header, but doesn't actually point to resolved. We mustn't // try to program resolved in that case. // https://github.com/tailscale/tailscale/issues/2136 - if err := resolvedIsActuallyResolver(bs); err != nil { + if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil { logf("dns: resolvedIsActuallyResolver error: %v", err) dbg("resolved", "not-in-use") return "direct", nil @@ -225,7 +225,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) { dbg("rc", "nm") // Sometimes, NetworkManager owns the configuration but points // it at systemd-resolved. - if err := resolvedIsActuallyResolver(bs); err != nil { + if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil { logf("dns: resolvedIsActuallyResolver error: %v", err) dbg("resolved", "not-in-use") // You'd think we would use newNMManager here. However, as @@ -318,14 +318,23 @@ func nmIsUsingResolved() error { return nil } -// resolvedIsActuallyResolver reports whether the given resolv.conf -// bytes describe a configuration where systemd-resolved (127.0.0.53) -// is the only configured nameserver. +// resolvedIsActuallyResolver reports whether the system is using +// systemd-resolved as the resolver. There are two different ways to +// use systemd-resolved: +// - libnss_resolve, which requires adding `resolve` to the "hosts:" +// line in /etc/nsswitch.conf +// - setting the only nameserver configured in `resolv.conf` to +// systemd-resolved IP (127.0.0.53) // // Returns an error if the configuration is something other than // exclusively systemd-resolved, or nil if the config is only // systemd-resolved. -func resolvedIsActuallyResolver(bs []byte) error { +func resolvedIsActuallyResolver(logf logger.Logf, env newOSConfigEnv, dbg func(k, v string), bs []byte) error { + if err := isLibnssResolveUsed(env); err == nil { + dbg("resolved", "nss") + return nil + } + cfg, err := readResolv(bytes.NewBuffer(bs)) if err != nil { return err @@ -342,9 +351,34 @@ func resolvedIsActuallyResolver(bs []byte) error { return fmt.Errorf("resolv.conf doesn't point to systemd-resolved; points to %v", cfg.Nameservers) } } + dbg("resolved", "file") return nil } +// isLibnssResolveUsed reports whether libnss_resolve is used +// for resolving names. Returns nil if it is, and an error otherwise. +func isLibnssResolveUsed(env newOSConfigEnv) error { + bs, err := env.fs.ReadFile("/etc/nsswitch.conf") + if err != nil { + return fmt.Errorf("reading /etc/resolv.conf: %w", err) + } + for _, line := range strings.Split(string(bs), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 || fields[0] != "hosts:" { + continue + } + for _, module := range fields[1:] { + if module == "dns" { + return fmt.Errorf("dns with a higher priority than libnss_resolve") + } + if module == "resolve" { + return nil + } + } + } + return fmt.Errorf("libnss_resolve not used") +} + func dbusPing(name, objectPath string) error { conn, err := dbus.SystemBus() if err != nil { diff --git a/net/dns/manager_linux_test.go b/net/dns/manager_linux_test.go index edb9dc429..c936d7b02 100644 --- a/net/dns/manager_linux_test.go +++ b/net/dns/manager_linux_test.go @@ -70,7 +70,7 @@ func TestLinuxDNSMode(t *testing.T) { { name: "resolved_alone_without_ping", env: env(resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53")), - wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]", + wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -78,16 +78,46 @@ func TestLinuxDNSMode(t *testing.T) { env: env( resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, + { + name: "resolved_and_nsswitch_resolve", + env: env( + resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"), + resolvedRunning(), + nsswitchDotConf("hosts: files resolve [!UNAVAIL=return] dns"), + ), + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=nss nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + want: "systemd-resolved", + }, + { + name: "resolved_and_nsswitch_dns", + env: env( + resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"), + resolvedRunning(), + nsswitchDotConf("hosts: files dns resolve [!UNAVAIL=return]"), + ), + wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]", + want: "direct", + }, + { + name: "resolved_and_nsswitch_none", + env: env( + resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"), + resolvedRunning(), + nsswitchDotConf("hosts:"), + ), + wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]", + want: "direct", + }, { name: "resolved_and_networkmanager_not_using_resolved", env: env( resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.2.3", false)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -96,7 +126,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.26.2", true)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]", want: "network-manager", }, { @@ -105,7 +135,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.27.0", true)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -114,7 +144,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.22.0", true)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, // Regression tests for extreme corner cases below. @@ -140,7 +170,7 @@ func TestLinuxDNSMode(t *testing.T) { "nameserver 127.0.0.53", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -155,7 +185,7 @@ func TestLinuxDNSMode(t *testing.T) { "# run \"systemd-resolve --status\" to see details about the actual nameservers.", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -170,7 +200,7 @@ func TestLinuxDNSMode(t *testing.T) { "# 127.0.0.53 is the systemd-resolved stub resolver.", "# run \"systemd-resolve --status\" to see details about the actual nameservers.", "nameserver 127.0.0.53")), - wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]", + wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -182,7 +212,7 @@ func TestLinuxDNSMode(t *testing.T) { "options edns0 trust-ad"), resolvedRunning(), nmRunning("1.32.12", true)), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -193,7 +223,7 @@ func TestLinuxDNSMode(t *testing.T) { "nameserver 127.0.0.53", "options edns0 trust-ad"), nmRunning("1.32.12", true)), - wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]", + wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -205,7 +235,7 @@ func TestLinuxDNSMode(t *testing.T) { "options edns0 trust-ad"), resolvedRunning(), nmRunning("1.26.3", true)), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=yes ret=network-manager]", want: "network-manager", }, { @@ -216,7 +246,7 @@ func TestLinuxDNSMode(t *testing.T) { "nameserver 127.0.0.53", "options edns0 trust-ad"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -227,7 +257,7 @@ func TestLinuxDNSMode(t *testing.T) { "search lan", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -237,7 +267,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedDbusProperty(), )), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, } @@ -380,6 +410,12 @@ func resolvDotConf(ss ...string) envOption { }) } +func nsswitchDotConf(ss ...string) envOption { + return envOpt(func(b *envBuilder) { + b.fs["/etc/nsswitch.conf"] = strings.Join(ss, "\n") + }) +} + // resolvedRunning returns an option that makes resolved reply to a dbusPing // and the ResolvConfMode property. func resolvedRunning() envOption {