diff --git a/dns/platform/network_manager.go b/dns/platform/network_manager.go index a95f180..e127036 100644 --- a/dns/platform/network_manager.go +++ b/dns/platform/network_manager.go @@ -16,12 +16,18 @@ import ( const ( // NetworkManager D-Bus constants - networkManagerDest = "org.freedesktop.NetworkManager" - networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager" - networkManagerDbusDNSManagerInterface = "org.freedesktop.NetworkManager.DnsManager" - networkManagerDbusDNSManagerObjectNode = networkManagerDbusObjectNode + "/DnsManager" - networkManagerDbusDNSManagerModeProperty = networkManagerDbusDNSManagerInterface + ".Mode" - networkManagerDbusVersionProperty = "org.freedesktop.NetworkManager.Version" + networkManagerDest = "org.freedesktop.NetworkManager" + networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager" + networkManagerDbusDNSManagerInterface = "org.freedesktop.NetworkManager.DnsManager" + networkManagerDbusDNSManagerObjectNode = networkManagerDbusObjectNode + "/DnsManager" + networkManagerDbusDNSManagerModeProperty = networkManagerDbusDNSManagerInterface + ".Mode" + networkManagerDbusVersionProperty = "org.freedesktop.NetworkManager.Version" + networkManagerDbusActiveConnsProperty = networkManagerDest + ".ActiveConnections" + networkManagerDbusActiveInterface = "org.freedesktop.NetworkManager.Connection.Active" + networkManagerDbusActiveIP4ConfigProperty = networkManagerDbusActiveInterface + ".Ip4Config" + networkManagerDbusActiveIP6ConfigProperty = networkManagerDbusActiveInterface + ".Ip6Config" + networkManagerDbusIP4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config" + networkManagerDbusIP6ConfigInterface = "org.freedesktop.NetworkManager.IP6Config" // NetworkManager dispatcher script path networkManagerDispatcherDir = "/etc/NetworkManager/dispatcher.d" @@ -301,6 +307,89 @@ func GetNetworkManagerDNSMode() (string, error) { return mode, nil } +// GetNetworkManagerNameservers returns the DNS servers NetworkManager has +// computed for every active connection, read live via D-Bus from each +// connection's IP4Config/IP6Config NameserverData property. +// +// Unlike reading /etc/resolv.conf, this reflects NetworkManager's own view of +// DNS regardless of its DnsManager.Mode: in "dnsmasq" or "unbound" mode +// /etc/resolv.conf only contains a loopback stub address pointing at +// NetworkManager's local resolver, not the real upstream servers, and even in +// "default" mode this stays live and unaffected if something else (such as +// olm's own override) has since overwritten /etc/resolv.conf directly instead +// of going through NetworkManager. +func GetNetworkManagerNameservers() ([]netip.Addr, error) { + conn, err := dbus.SystemBus() + if err != nil { + return nil, fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + nm := conn.Object(networkManagerDest, networkManagerDbusObjectNode) + + activeVariant, err := nm.GetProperty(networkManagerDbusActiveConnsProperty) + if err != nil { + return nil, fmt.Errorf("get active connections: %w", err) + } + activePaths, ok := activeVariant.Value().([]dbus.ObjectPath) + if !ok { + return nil, errors.New("ActiveConnections is not a list of object paths") + } + + ipConfigSources := []struct { + activeProperty string + configIface string + }{ + {networkManagerDbusActiveIP4ConfigProperty, networkManagerDbusIP4ConfigInterface}, + {networkManagerDbusActiveIP6ConfigProperty, networkManagerDbusIP6ConfigInterface}, + } + + seen := make(map[netip.Addr]bool) + var servers []netip.Addr + for _, activePath := range activePaths { + active := conn.Object(networkManagerDest, activePath) + for _, src := range ipConfigSources { + cfgVariant, err := active.GetProperty(src.activeProperty) + if err != nil { + continue + } + cfgPath, ok := cfgVariant.Value().(dbus.ObjectPath) + if !ok || cfgPath == "" || cfgPath == "/" { + continue + } + + nsVariant, err := conn.Object(networkManagerDest, cfgPath).GetProperty(src.configIface + ".NameserverData") + if err != nil { + continue + } + entries, ok := nsVariant.Value().([]map[string]dbus.Variant) + if !ok { + continue + } + for _, entry := range entries { + addrVariant, ok := entry["address"] + if !ok { + continue + } + addrStr, ok := addrVariant.Value().(string) + if !ok { + continue + } + addr, err := netip.ParseAddr(addrStr) + if err != nil || addr.IsLoopback() || addr.IsLinkLocalUnicast() { + continue + } + if !seen[addr] { + seen[addr] = true + servers = append(servers, addr) + } + } + } + } + + return servers, nil +} + // GetNetworkManagerVersion returns the version of NetworkManager func GetNetworkManagerVersion() (string, error) { conn, err := dbus.SystemBus() diff --git a/dns/sysresolver_linux.go b/dns/sysresolver_linux.go index 6a48e17..a622a3b 100644 --- a/dns/sysresolver_linux.go +++ b/dns/sysresolver_linux.go @@ -8,6 +8,8 @@ import ( "net/netip" "os" "strings" + + platform "github.com/fosrl/olm/dns/platform" ) // readSystemDNS returns the current system DNS servers in "host:53" format. @@ -16,9 +18,18 @@ import ( // 1. /run/systemd/resolve/resolv.conf — maintained by systemd-resolved with // the real per-link DNS servers; updated on every DHCP change and never // touched by olm's D-Bus DNS override. -// 2. /etc/resolv.conf.olm.backup — written by olm before it overrides -// /etc/resolv.conf on non-systemd systems. -// 3. /etc/resolv.conf — plain fallback. +// 2. NetworkManager, queried live over D-Bus — NetworkManager's own view of +// each active connection's DNS servers, independent of what is currently +// written to /etc/resolv.conf. This covers NetworkManager's "dnsmasq" and +// "unbound" DNS modes, where /etc/resolv.conf only contains a loopback +// stub address, and stays accurate even if olm's own override has +// directly overwritten /etc/resolv.conf, without going stale the way a +// one-time backup snapshot would if the real DNS changes mid-override +// (e.g. the user switches WiFi networks). +// 3. /etc/resolv.conf.olm.backup — written by olm before it overrides +// /etc/resolv.conf on non-systemd systems, for when NetworkManager isn't +// in use at all. +// 4. /etc/resolv.conf — plain fallback. // // Loopback and link-local addresses (e.g. 127.0.0.53, ::1) are excluded // because they are stub resolver addresses, not real upstream servers. @@ -28,6 +39,10 @@ func readSystemDNS() []string { return servers } + if servers := readNetworkManagerDNS(); len(servers) > 0 { + return servers + } + // If olm has already overridden /etc/resolv.conf the backup holds the // original pre-override DNS servers. if _, err := os.Stat("/etc/resolv.conf.olm.backup"); err == nil { @@ -39,6 +54,22 @@ func readSystemDNS() []string { return parseResolvConf("/etc/resolv.conf") } +// readNetworkManagerDNS returns the DNS servers NetworkManager reports over +// D-Bus for its active connections, in "host:53" format. Returns nil if +// NetworkManager isn't running or reports nothing usable. +func readNetworkManagerDNS() []string { + addrs, err := platform.GetNetworkManagerNameservers() + if err != nil || len(addrs) == 0 { + return nil + } + + result := make([]string, 0, len(addrs)) + for _, addr := range addrs { + result = append(result, addrToHostPort(addr)) + } + return result +} + // parseResolvConf reads nameserver lines from a resolv.conf-style file, // skipping loopback and link-local addresses. func parseResolvConf(path string) []string {