diff --git a/net/dns/manager_linux.go b/net/dns/manager_linux.go index 727b24e30..b1ad90520 100644 --- a/net/dns/manager_linux.go +++ b/net/dns/manager_linux.go @@ -8,6 +8,8 @@ import "tailscale.com/types/logger" func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator { switch { + case isNMActive(): + return newNMManager(interfaceName) // TODO: rework NetworkManager and resolved support. // case isResolvedActive(): // return newResolvedManager() diff --git a/net/dns/nm.go b/net/dns/nm.go index 35f593f6f..9e5e9d60f 100644 --- a/net/dns/nm.go +++ b/net/dns/nm.go @@ -14,7 +14,7 @@ import ( "context" "fmt" "os" - "os/exec" + "time" "github.com/godbus/dbus/v5" "tailscale.com/util/endian" @@ -22,11 +22,21 @@ import ( // isNMActive determines if NetworkManager is currently managing system DNS settings. func isNMActive() bool { - // This is somewhat tricky because NetworkManager supports a number - // of DNS configuration modes. In all cases, we expect it to be installed - // and /etc/resolv.conf to contain a mention of NetworkManager in the comments. - _, err := exec.LookPath("NetworkManager") + ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) + defer cancel() + + conn, err := dbus.SystemBus() if err != nil { + // Probably no DBus on this system. Either way, we can't + // control NM without DBus. + return false + } + + // Try to ping NetworkManager's DnsManager object. If it responds, + // NM is running and we're allowed to touch it. + nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")) + call := nm.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0) + if call.Err != nil { return false } @@ -67,8 +77,24 @@ func (m nmManager) SetDNS(config OSConfig) error { ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) defer cancel() - // conn is a shared connection whose lifecycle is managed by the dbus package. - // We should not interfere with that by closing it. + // NetworkManager only lets you set DNS settings on "active" + // connections, which requires an assigned IP address. This got + // configured before the DNS manager was invoked, but it might + // take a little time for the netlink notifications to propagate + // up. So, keep retrying for the duration of the reconfigTimeout. + var err error + for ctx.Err() == nil { + err = m.trySet(ctx, config) + if err == nil { + break + } + time.Sleep(10 * time.Millisecond) + } + + return err +} + +func (m nmManager) trySet(ctx context.Context, config OSConfig) error { conn, err := dbus.SystemBus() if err != nil { return fmt.Errorf("connecting to system bus: %w", err) diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 6073e5495..fcec5b738 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -1010,15 +1010,18 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, } if routerChanged { - e.logf("wgengine: Reconfig: configuring DNS") - err := e.dns.Set(*dnsCfg) - health.SetDNSHealth(err) + e.logf("wgengine: Reconfig: configuring router") + err := e.router.Set(routerCfg) + health.SetRouterHealth(err) if err != nil { return err } - e.logf("wgengine: Reconfig: configuring router") - err = e.router.Set(routerCfg) - health.SetRouterHealth(err) + // Keep DNS configuration after router configuration, as some + // DNS managers refuse to apply settings if the device has no + // assigned address. + e.logf("wgengine: Reconfig: configuring DNS") + err = e.dns.Set(*dnsCfg) + health.SetDNSHealth(err) if err != nil { return err }