diff --git a/net/dns/direct.go b/net/dns/direct.go index aaff18fcb..1a31fd496 100644 --- a/net/dns/direct.go +++ b/net/dns/direct.go @@ -495,6 +495,7 @@ type wholeFileFS interface { ReadFile(name string) ([]byte, error) Remove(name string) error Rename(oldName, newName string) error + Readlink(name string) (string, error) Stat(name string) (isRegular bool, err error) Truncate(name string) error WriteFile(name string, contents []byte, perm os.FileMode) error @@ -519,6 +520,10 @@ func (fs directFS) Stat(name string) (isRegular bool, err error) { return fi.Mode().IsRegular(), nil } +func (fs directFS) Readlink(name string) (string, error) { + return os.Readlink(fs.path(name)) +} + func (fs directFS) Chmod(name string, mode os.FileMode) error { return os.Chmod(fs.path(name), mode) } diff --git a/net/dns/manager_linux.go b/net/dns/manager_linux.go index 3ba3022b6..a0cb41cea 100644 --- a/net/dns/manager_linux.go +++ b/net/dns/manager_linux.go @@ -137,15 +137,34 @@ func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret case "systemd-resolved": dbg("rc", "resolved") + // If systemd-resolved says that the we don't have a stub resolver, but + // /etc/resolv.conf is symlinked to /run/systemd/resolve/resolv.conf, + // then systemd-resolved is managing DNS but the logic in + // resolvedIsActuallyResolver will not detect it. + // + // Check for this case and return early if we find it. See: + // https://github.com/tailscale/tailscale/issues/11342 + var isResolvedNoStub bool + mode, err := env.dbusReadString("org.freedesktop.resolve1", "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager", "DNSStubListener") + if err == nil && mode == "no" { + target, err := env.fs.Readlink("/etc/resolv.conf") + if err == nil && target == "/run/systemd/resolve/resolv.conf" { + dbg("resolved", "no-stub") + isResolvedNoStub = true + } + } + // Some systems, for reasons known only to them, have a // resolv.conf that has the word "systemd-resolved" in its // 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(logf, env, dbg, bs); err != nil { - logf("dns: resolvedIsActuallyResolver error: %v", err) - dbg("resolved", "not-in-use") - return "direct", nil + if !isResolvedNoStub { + if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil { + logf("dns: resolvedIsActuallyResolver error: %v", err) + dbg("resolved", "not-in-use") + return "direct", nil + } } if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil { dbg("nm", "no") diff --git a/net/dns/manager_linux_test.go b/net/dns/manager_linux_test.go index 605344c06..58b39b34c 100644 --- a/net/dns/manager_linux_test.go +++ b/net/dns/manager_linux_test.go @@ -313,6 +313,17 @@ func (m memFS) Stat(name string) (isRegular bool, err error) { return false, nil } +func (m memFS) Readlink(name string) (string, error) { + v, ok := m[name] + if !ok { + return "", fs.ErrNotExist + } + if s, ok := v.(string); ok { + return s, nil + } + panic("unexpected") +} + func (m memFS) Chmod(name string, mode os.FileMode) error { panic("TODO") } func (m memFS) Rename(oldName, newName string) error { panic("TODO") } func (m memFS) Remove(name string) error { panic("TODO") } diff --git a/net/dns/wsl_windows.go b/net/dns/wsl_windows.go index 8b0780f55..a7946a88e 100644 --- a/net/dns/wsl_windows.go +++ b/net/dns/wsl_windows.go @@ -159,6 +159,12 @@ func (fs wslFS) Stat(name string) (isRegular bool, err error) { return true, nil } +func (fs wslFS) Readlink(name string) (string, error) { + // As of 2024-07-01, this function is only used on Linux. We can return + // the original path and no error here. + return name, nil +} + func (fs wslFS) Chmod(name string, perm os.FileMode) error { return wslRun(fs.cmd("chmod", "--", fmt.Sprintf("%04o", perm), name)) }