From d6bb11b5bf0f0c84deb7bf20e94d10c1274a7ee7 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 13 Apr 2021 17:10:30 -0700 Subject: [PATCH] net/dns: implement correct manager detection on linux. Part of #953. Signed-off-by: David Anderson --- net/dns/manager_linux.go | 156 +++++++++++++++++++++++++++++++++++++-- net/dns/nm.go | 9 +++ net/dns/osconfig.go | 30 ++++++++ 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/net/dns/manager_linux.go b/net/dns/manager_linux.go index 15fd6c860..93d5b81e2 100644 --- a/net/dns/manager_linux.go +++ b/net/dns/manager_linux.go @@ -4,17 +4,161 @@ package dns -import "tailscale.com/types/logger" +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + "time" + + "github.com/godbus/dbus/v5" + "tailscale.com/types/logger" +) func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) { - switch { - case isResolvedActive(): - return newResolvedManager(logf) - case isNMActive(): + bs, err := ioutil.ReadFile("/etc/resolv.conf") + if os.IsNotExist(err) { + return newDirectManager() + } + + switch resolvOwner(bs) { + case "systemd-resolved": + if err := dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1"); err != nil { + return newDirectManager() + } + if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil { + return newResolvedManager(logf) + } + if err := nmIsUsingResolved(); err != nil { + return newResolvedManager(logf) + } return newNMManager(interfaceName) - case isResolvconfActive(): + case "resolvconf": + if err := resolvconfSourceIsNM(bs); err == nil { + if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil { + return newNMManager(interfaceName) + } + } + if _, err := exec.LookPath("resolvconf"); err != nil { + return newDirectManager() + } return newResolvconfManager(logf) + case "NetworkManager": + if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil { + return newDirectManager() + } + return newNMManager(interfaceName) default: return newDirectManager() } } + +func resolvconfSourceIsNM(resolvDotConf []byte) error { + b := bytes.NewBuffer(resolvDotConf) + cfg, err := readResolv(b) + if err != nil { + return fmt.Errorf("parsing /etc/resolv.conf: %w", err) + } + + var ( + paths = []string{ + "/etc/resolvconf/run/interface/NetworkManager", + "/run/resolvconf/interface/NetworkManager", + "/var/run/resolvconf/interface/NetworkManager", + "/run/resolvconf/interfaces/NetworkManager", + "/var/run/resolvconf/interfaces/NetworkManager", + } + nmCfg OSConfig + found bool + ) + for _, path := range paths { + nmCfg, err = readResolvFile(path) + if os.IsNotExist(err) { + continue + } else if err != nil { + return err + } + found = true + break + } + if !found { + return errors.New("NetworkManager resolvconf snippet not found") + } + + if !nmCfg.Equal(cfg) { + return errors.New("NetworkManager config not applied by resolvconf") + } + + return nil +} + +func nmIsUsingResolved() error { + conn, err := dbus.SystemBus() + if err != nil { + // DBus probably not running. + return err + } + + nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")) + v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode") + if err != nil { + return fmt.Errorf("getting NM mode: %w", err) + } + mode, ok := v.Value().(string) + if !ok { + return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value()) + } + if mode != "systemd-resolved" { + return errors.New("NetworkManager is not using systemd-resolved for DNS") + } + return nil +} + +func dbusPing(name, objectPath string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + conn, err := dbus.SystemBus() + if err != nil { + // DBus probably not running. + return err + } + + obj := conn.Object(name, dbus.ObjectPath(objectPath)) + call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0) + return call.Err +} + +// resolvOwner returns the apparent owner of the resolv.conf +// configuration in bs - one of "resolvconf", "systemd-resolved" or +// "NetworkManager", or "" if no known owner was found. +func resolvOwner(bs []byte) string { + b := bytes.NewBuffer(bs) + for { + line, err := b.ReadString('\n') + if err != nil { + return "" + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + if line[0] != '#' { + // First non-empty, non-comment line. Assume the owner + // isn't hiding further down. + return "" + } + + if strings.Contains(line, "systemd-resolved") { + return "systemd-resolved" + } else if strings.Contains(line, "NetworkManager") { + return "NetworkManager" + } else if strings.Contains(line, "resolvconf") { + return "resolvconf" + } + } +} diff --git a/net/dns/nm.go b/net/dns/nm.go index 02ce4c918..41d5e158c 100644 --- a/net/dns/nm.go +++ b/net/dns/nm.go @@ -25,6 +25,7 @@ import ( const ( highestPriority = int32(-1 << 31) + mediumPriority = int32(1) // Highest priority that doesn't hard-override lowerPriority = int32(200) // lower than all builtin auto priorities ) @@ -189,6 +190,10 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error { // We should only request priority if we have nameservers to set. if len(dnsv4) == 0 { ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority) + } else if len(config.MatchDomains) > 0 { + // Set a fairly high priority, but don't override all other + // configs when in split-DNS mode. + ipv4Map["dns-priority"] = dbus.MakeVariant(mediumPriority) } else { // Negative priority means only the settings from the most // negative connection get used. The way this mixes with @@ -220,6 +225,10 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error { ipv6Map["dns-search"] = dbus.MakeVariant(config.SearchDomains) if len(dnsv6) == 0 { ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority) + } else if len(config.MatchDomains) > 0 { + // Set a fairly high priority, but don't override all other + // configs when in split-DNS mode. + ipv6Map["dns-priority"] = dbus.MakeVariant(mediumPriority) } else { ipv6Map["dns-priority"] = dbus.MakeVariant(highestPriority) } diff --git a/net/dns/osconfig.go b/net/dns/osconfig.go index 77e4b30f0..af7850b6f 100644 --- a/net/dns/osconfig.go +++ b/net/dns/osconfig.go @@ -52,6 +52,36 @@ type OSConfig struct { MatchDomains []dnsname.FQDN } +func (a OSConfig) Equal(b OSConfig) bool { + if len(a.Nameservers) != len(b.Nameservers) { + return false + } + if len(a.SearchDomains) != len(b.SearchDomains) { + return false + } + if len(a.MatchDomains) != len(b.MatchDomains) { + return false + } + + for i := range a.Nameservers { + if a.Nameservers[i] != b.Nameservers[i] { + return false + } + } + for i := range a.SearchDomains { + if a.SearchDomains[i] != b.SearchDomains[i] { + return false + } + } + for i := range a.MatchDomains { + if a.MatchDomains[i] != b.MatchDomains[i] { + return false + } + } + + return true +} + // ErrGetBaseConfigNotSupported is the error // OSConfigurator.GetBaseConfig returns when the OSConfigurator // doesn't support reading the underlying configuration out of the OS.