summary refs log tree commit diff
path: root/src/core/dns
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/dns')
-rw-r--r--src/core/dns/nm-dns-dnsmasq.c123
-rw-r--r--src/core/dns/nm-dns-manager.c202
-rw-r--r--src/core/dns/nm-dns-manager.h9
-rw-r--r--src/core/dns/nm-dns-plugin.c161
-rw-r--r--src/core/dns/nm-dns-plugin.h13
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.c425
-rw-r--r--src/core/dns/nm-dns-unbound.c84
-rw-r--r--src/core/dns/nm-dns-unbound.h27
8 files changed, 722 insertions, 322 deletions
diff --git a/src/core/dns/nm-dns-dnsmasq.c b/src/core/dns/nm-dns-dnsmasq.c
index 43426882..7d0f0490 100644
--- a/src/core/dns/nm-dns-dnsmasq.c
+++ b/src/core/dns/nm-dns-dnsmasq.c
@@ -678,19 +678,23 @@ typedef struct {
 
     char *name_owner;
 
+    GSource *main_timeout_source;
+    GSource *burst_retry_timeout_source;
+
     gint64 burst_start_at;
 
     GPid process_pid;
 
     guint name_owner_changed_id;
-    guint main_timeout_id;
-
-    guint burst_retry_timeout_id;
 
     guint8 burst_count;
 
     bool is_stopped : 1;
 
+    bool set_server_ex_args_dirty : 1;
+
+    bool update_pending : 1;
+
 } NMDnsDnsmasqPrivate;
 
 struct _NMDnsDnsmasq {
@@ -704,7 +708,8 @@ struct _NMDnsDnsmasqClass {
 
 G_DEFINE_TYPE(NMDnsDnsmasq, nm_dns_dnsmasq, NM_TYPE_DNS_PLUGIN)
 
-#define NM_DNS_DNSMASQ_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDnsDnsmasq, NM_IS_DNS_DNSMASQ)
+#define NM_DNS_DNSMASQ_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDnsDnsmasq, NM_IS_DNS_DNSMASQ, NMDnsPlugin)
 
 /*****************************************************************************/
 
@@ -717,6 +722,55 @@ static gboolean start_dnsmasq(NMDnsDnsmasq *self, gboolean force_start, GError *
 
 /*****************************************************************************/
 
+static gboolean
+_update_pending_detect(NMDnsDnsmasq *self)
+{
+    NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+
+    if (priv->is_stopped)
+        return FALSE;
+    if (priv->main_timeout_source) {
+        /* we are waiting for dnsmasq to start. */
+        return TRUE;
+    }
+    if (priv->update_cancellable) {
+        /* An update is in progress. Busy. */
+        return TRUE;
+    }
+    if (priv->set_server_ex_args_dirty) {
+        /* the args just changed and were not yet sent. Busy. */
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+_update_pending_maybe_changed(NMDnsDnsmasq *self)
+{
+    NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+    gboolean             update_pending;
+
+    update_pending = _update_pending_detect(self);
+    if (priv->update_pending == update_pending)
+        return;
+
+    priv->update_pending = update_pending;
+    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+}
+
+static gboolean
+get_update_pending(NMDnsPlugin *plugin)
+{
+    NMDnsDnsmasq        *self = NM_DNS_DNSMASQ(plugin);
+    NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+
+    nm_assert(priv->update_pending == _update_pending_detect(self));
+    return priv->update_pending;
+}
+
+/*****************************************************************************/
+
 static void
 add_dnsmasq_nameserver(NMDnsDnsmasq    *self,
                        GVariantBuilder *servers,
@@ -871,6 +925,7 @@ static void
 dnsmasq_update_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
 {
     NMDnsDnsmasq              *self;
+    NMDnsDnsmasqPrivate       *priv;
     gs_free_error GError      *error    = NULL;
     gs_unref_variant GVariant *response = NULL;
 
@@ -880,10 +935,16 @@ dnsmasq_update_done(GObject *source_object, GAsyncResult *res, gpointer user_dat
         return;
 
     self = user_data;
+    priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->update_cancellable);
+
     if (!response)
         _LOGW("dnsmasq update failed: %s", error->message);
     else
         _LOGD("dnsmasq update successful");
+
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -899,6 +960,8 @@ send_dnsmasq_update(NMDnsDnsmasq *self)
     nm_clear_g_cancellable(&priv->update_cancellable);
     priv->update_cancellable = g_cancellable_new();
 
+    priv->set_server_ex_args_dirty = FALSE;
+
     g_dbus_connection_call(priv->dbus_connection,
                            priv->name_owner,
                            DNSMASQ_DBUS_PATH,
@@ -911,6 +974,8 @@ send_dnsmasq_update(NMDnsDnsmasq *self)
                            priv->update_cancellable,
                            dnsmasq_update_done,
                            self);
+
+    _update_pending_maybe_changed(self);
 }
 
 /*****************************************************************************/
@@ -928,17 +993,19 @@ _main_cleanup(NMDnsDnsmasq *self, gboolean emit_failed)
 
     nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
 
-    nm_clear_g_source(&priv->main_timeout_id);
+    nm_clear_g_source_inst(&priv->main_timeout_source);
     nm_clear_g_cancellable(&priv->update_cancellable);
 
     /* cancelling the main_cancellable will also cause _gl_pid_spawn*() to terminate the
      * process in the background. */
     nm_clear_g_cancellable(&priv->main_cancellable);
 
-    if (!priv->is_stopped && priv->burst_retry_timeout_id == 0) {
+    if (!priv->is_stopped && !priv->burst_retry_timeout_source) {
         start_dnsmasq(self, FALSE, NULL);
         send_dnsmasq_update(self);
     }
+
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -961,8 +1028,10 @@ name_owner_changed(NMDnsDnsmasq *self, const char *name_owner)
     }
 
     _LOGT("D-Bus name for dnsmasq got owner %s", name_owner);
-    nm_clear_g_source(&priv->main_timeout_id);
+    nm_clear_g_source_inst(&priv->main_timeout_source);
     send_dnsmasq_update(self);
+
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -1047,11 +1116,11 @@ _burst_retry_timeout_cb(gpointer user_data)
     NMDnsDnsmasq        *self = user_data;
     NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
 
-    priv->burst_retry_timeout_id = 0;
+    nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
 
     start_dnsmasq(self, TRUE, NULL);
     send_dnsmasq_update(self);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static gboolean
@@ -1090,33 +1159,35 @@ start_dnsmasq(NMDnsDnsmasq *self, gboolean force_start, GError **error)
         || priv->burst_start_at + RATELIMIT_INTERVAL_MSEC <= now) {
         priv->burst_start_at = now;
         priv->burst_count    = 1;
-        nm_clear_g_source(&priv->burst_retry_timeout_id);
+        nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
         _LOGT("rate-limit: start burst interval of %d seconds %s",
               RATELIMIT_INTERVAL_MSEC / 1000,
               force_start ? " (force)" : "");
     } else if (priv->burst_count < RATELIMIT_BURST) {
-        nm_assert(priv->burst_retry_timeout_id == 0);
+        nm_assert(!priv->burst_retry_timeout_source);
         priv->burst_count++;
         _LOGT("rate-limit: %u try within burst interval of %d seconds",
               (guint) priv->burst_count,
               RATELIMIT_INTERVAL_MSEC / 1000);
     } else {
-        if (priv->burst_retry_timeout_id == 0) {
+        if (!priv->burst_retry_timeout_source) {
             _LOGW("dnsmasq dies and gets respawned too quickly. Back off. Something is very wrong");
-            priv->burst_retry_timeout_id =
-                g_timeout_add_seconds((2 * RATELIMIT_INTERVAL_MSEC) / 1000,
-                                      _burst_retry_timeout_cb,
-                                      self);
+            priv->burst_retry_timeout_source =
+                nm_g_timeout_add_seconds_source((2 * RATELIMIT_INTERVAL_MSEC) / 1000,
+                                                _burst_retry_timeout_cb,
+                                                self);
         } else
             _LOGT("rate-limit: currently rate-limited from restart");
         return TRUE;
     }
 
-    priv->main_timeout_id = g_timeout_add(10000, spawn_timeout_cb, self);
+    priv->main_timeout_source = nm_g_timeout_add_source(10000, spawn_timeout_cb, self);
 
     priv->main_cancellable = g_cancellable_new();
 
     _gl_pid_spawn(dm_binary, priv->main_cancellable, spawn_notify, self);
+
+    _update_pending_maybe_changed(self);
     return TRUE;
 }
 
@@ -1136,8 +1207,11 @@ update(NMDnsPlugin             *plugin,
     nm_clear_pointer(&priv->set_server_ex_args, g_variant_unref);
     priv->set_server_ex_args =
         g_variant_ref_sink(create_update_args(self, global_config, ip_data_lst_head, hostdomain));
+    priv->set_server_ex_args_dirty = TRUE;
 
     send_dnsmasq_update(self);
+
+    _update_pending_maybe_changed(self);
     return TRUE;
 }
 
@@ -1151,11 +1225,13 @@ stop(NMDnsPlugin *plugin)
 
     priv->is_stopped     = TRUE;
     priv->burst_start_at = 0;
-    nm_clear_g_source(&priv->burst_retry_timeout_id);
+    nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
 
     /* Cancelling the cancellable will also terminate the
      * process (in the background). */
     _main_cleanup(self, FALSE);
+
+    _update_pending_maybe_changed(self);
 }
 
 /*****************************************************************************/
@@ -1178,7 +1254,7 @@ dispose(GObject *object)
 
     priv->is_stopped = TRUE;
 
-    nm_clear_g_source(&priv->burst_retry_timeout_id);
+    nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
 
     _main_cleanup(self, FALSE);
 
@@ -1197,8 +1273,9 @@ nm_dns_dnsmasq_class_init(NMDnsDnsmasqClass *dns_class)
 
     object_class->dispose = dispose;
 
-    plugin_class->plugin_name = "dnsmasq";
-    plugin_class->is_caching  = TRUE;
-    plugin_class->stop        = stop;
-    plugin_class->update      = update;
+    plugin_class->plugin_name        = "dnsmasq";
+    plugin_class->is_caching         = TRUE;
+    plugin_class->stop               = stop;
+    plugin_class->update             = update;
+    plugin_class->get_update_pending = get_update_pending;
 }
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index 566f3d66..1e54452a 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -26,7 +26,6 @@
 
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-glib-aux/nm-str-buf.h"
-#include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 #include "NetworkManagerUtils.h"
 #include "devices/nm-device.h"
@@ -35,7 +34,6 @@
 #include "nm-dns-dnsmasq.h"
 #include "nm-dns-plugin.h"
 #include "nm-dns-systemd-resolved.h"
-#include "nm-dns-unbound.h"
 #include "nm-ip-config.h"
 #include "nm-l3-config-data.h"
 #include "nm-manager.h"
@@ -57,6 +55,8 @@
 #define HAS_NETCONFIG 1
 #endif
 
+#define UPDATE_PENDING_UNBLOCK_TIMEOUT_MSEC 5000
+
 /*****************************************************************************/
 
 typedef enum { SR_SUCCESS, SR_NOTFOUND, SR_ERROR } SpawnResult;
@@ -78,7 +78,11 @@ enum {
     LAST_SIGNAL
 };
 
-NM_GOBJECT_PROPERTIES_DEFINE(NMDnsManager, PROP_MODE, PROP_RC_MANAGER, PROP_CONFIGURATION, );
+NM_GOBJECT_PROPERTIES_DEFINE(NMDnsManager,
+                             PROP_MODE,
+                             PROP_RC_MANAGER,
+                             PROP_CONFIGURATION,
+                             PROP_UPDATE_PENDING, );
 
 static guint signals[LAST_SIGNAL] = {0};
 
@@ -89,6 +93,11 @@ typedef struct {
     CList     ip_data_lst_head;
     GVariant *config_variant;
 
+    /* A DNS plugin should not be marked as pending indefinitely.
+     * We are only blocked if "update_pending" is TRUE and we have
+     * "update_pending_unblock" timer ticking. */
+    GSource *update_pending_unblock;
+
     bool ip_data_lst_need_sort : 1;
 
     bool configs_lst_need_sort : 1;
@@ -98,6 +107,8 @@ typedef struct {
 
     bool config_changed : 1;
 
+    bool update_pending : 1;
+
     char *hostdomain;
     guint updates_queue;
 
@@ -109,6 +120,9 @@ typedef struct {
     NMDnsPlugin                  *sd_resolve_plugin;
     NMDnsPlugin                  *plugin;
 
+    gulong update_changed_signal_id_sd;
+    gulong update_changed_signal_id;
+
     NMConfig *config;
 
     struct {
@@ -137,28 +151,23 @@ NM_DEFINE_SINGLETON_GETTER(NMDnsManager, nm_dns_manager_get, NM_TYPE_DNS_MANAGER
 
 #define _NMLOG_PREFIX_NAME "dns-mgr"
 #define _NMLOG_DOMAIN      LOGD_DNS
-#define _NMLOG(level, ...)                                           \
-    G_STMT_START                                                     \
-    {                                                                \
-        const NMLogLevel __level = (level);                          \
-                                                                     \
-        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {            \
-            char                      __prefix[20];                  \
-            const NMDnsManager *const __self = (self);               \
-                                                                     \
-            _nm_log(__level,                                         \
-                    _NMLOG_DOMAIN,                                   \
-                    0,                                               \
-                    NULL,                                            \
-                    NULL,                                            \
-                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),     \
-                    _NMLOG_PREFIX_NAME,                              \
-                    ((!__self || __self == singleton_instance)       \
-                         ? ""                                        \
-                         : nm_sprintf_buf(__prefix, "[%p]", __self)) \
-                        _NM_UTILS_MACRO_REST(__VA_ARGS__));          \
-        }                                                            \
-    }                                                                \
+#define _NMLOG(level, ...)                                                 \
+    G_STMT_START                                                           \
+    {                                                                      \
+        const NMLogLevel __level = (level);                                \
+                                                                           \
+        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                  \
+            _nm_unused const NMDnsManager *const __self = (self);          \
+                                                                           \
+            _nm_log(__level,                                               \
+                    _NMLOG_DOMAIN,                                         \
+                    0,                                                     \
+                    NULL,                                                  \
+                    NULL,                                                  \
+                    "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),             \
+                    _NMLOG_PREFIX_NAME _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                  \
+    }                                                                      \
     G_STMT_END
 
 /*****************************************************************************/
@@ -207,6 +216,85 @@ static NM_UTILS_LOOKUP_STR_DEFINE(
 
 /*****************************************************************************/
 
+static gboolean
+_update_pending_detect(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    if (priv->plugin && nm_dns_plugin_get_update_pending(priv->plugin))
+        return TRUE;
+    if (priv->sd_resolve_plugin && nm_dns_plugin_get_update_pending(priv->sd_resolve_plugin))
+        return TRUE;
+    return FALSE;
+}
+
+static gboolean
+_update_pending_unblock_cb(gpointer user_data)
+{
+    NMDnsManager        *self = user_data;
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(priv->update_pending);
+    nm_assert(priv->update_pending_unblock);
+    nm_assert(_update_pending_detect(self));
+
+    nm_clear_g_source_inst(&priv->update_pending_unblock);
+
+    _LOGW(
+        "update-pending changed: DNS plugin did not become ready again. Assume something is wrong");
+
+    _notify(self, PROP_UPDATE_PENDING);
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_update_pending_maybe_changed(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+    gboolean             update_pending;
+
+    update_pending = _update_pending_detect(self);
+    if (priv->update_pending == update_pending)
+        return;
+
+    if (update_pending) {
+        nm_assert(!priv->update_pending_unblock);
+        priv->update_pending_unblock = nm_g_timeout_add_source(UPDATE_PENDING_UNBLOCK_TIMEOUT_MSEC,
+                                                               _update_pending_unblock_cb,
+                                                               self);
+    } else
+        nm_clear_g_source_inst(&priv->update_pending_unblock);
+
+    priv->update_pending = update_pending;
+    _LOGD("update-pending changed: %spending", update_pending ? "" : "not ");
+    _notify(self, PROP_UPDATE_PENDING);
+}
+
+static void
+_update_pending_changed_cb(NMDnsPlugin *plugin, gboolean update_pending, NMDnsManager *self)
+{
+    _update_pending_maybe_changed(self);
+}
+
+gboolean
+nm_dns_manager_get_update_pending(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DNS_MANAGER(self), FALSE);
+
+    priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+    nm_assert(priv->update_pending == _update_pending_detect(self));
+    nm_assert(priv->update_pending || !priv->update_pending_unblock);
+
+    /* update-pending can only be TRUE for a certain time (before we assume
+     * something is really wrong with the plugin). That is, as long as
+     * update_pending_unblock is ticking. */
+    return !!priv->update_pending_unblock;
+}
+
+/*****************************************************************************/
+
 static int
 _dns_config_ip_data_get_dns_priority1(const NML3ConfigData *l3cd, int addr_family)
 {
@@ -2015,7 +2103,7 @@ nm_dns_manager_set_hostname(NMDnsManager *self, const char *hostname, gboolean s
                 domain = hostname;
             }
 
-            if (!nm_sd_hostname_is_valid(domain, FALSE))
+            if (!nm_hostname_is_valid(domain, FALSE))
                 domain = NULL;
         }
     }
@@ -2120,6 +2208,7 @@ _clear_plugin(NMDnsManager *self)
     nm_clear_g_source(&priv->plugin_ratelimit.timer);
 
     if (priv->plugin) {
+        nm_clear_g_signal_handler(priv->plugin, &priv->update_changed_signal_id);
         nm_dns_plugin_stop(priv->plugin);
         g_clear_object(&priv->plugin);
         return TRUE;
@@ -2127,6 +2216,20 @@ _clear_plugin(NMDnsManager *self)
     return FALSE;
 }
 
+static gboolean
+_clear_sd_resolved_plugin(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    if (priv->sd_resolve_plugin) {
+        nm_clear_g_signal_handler(priv->sd_resolve_plugin, &priv->update_changed_signal_id_sd);
+        nm_dns_plugin_stop(priv->sd_resolve_plugin);
+        g_clear_object(&priv->sd_resolve_plugin);
+        return TRUE;
+    }
+    return FALSE;
+}
+
 static NMDnsManagerResolvConfManager
 _check_resconf_immutable(NMDnsManagerResolvConfManager rc_manager)
 {
@@ -2313,16 +2416,14 @@ again:
             priv->plugin   = nm_dns_dnsmasq_new();
             plugin_changed = TRUE;
         }
-    } else if (nm_streq0(mode, "unbound")) {
-        if (force_reload_plugin || !NM_IS_DNS_UNBOUND(priv->plugin)) {
-            _clear_plugin(self);
-            priv->plugin   = nm_dns_unbound_new();
-            plugin_changed = TRUE;
-        }
     } else {
         if (!NM_IN_STRSET(mode, "none", "default")) {
-            if (mode)
-                _LOGW("init: unknown dns mode '%s'", mode);
+            if (mode) {
+                if (nm_streq(mode, "unbound"))
+                    _LOGW("init: ns mode 'unbound' was removed. Update your configuration");
+                else
+                    _LOGW("init: unknown dns mode '%s'", mode);
+            }
             mode = "default";
         }
         if (_clear_plugin(self))
@@ -2359,7 +2460,7 @@ again:
             priv->sd_resolve_plugin  = nm_dns_systemd_resolved_new();
             systemd_resolved_changed = TRUE;
         }
-    } else if (nm_clear_g_object(&priv->sd_resolve_plugin))
+    } else if (_clear_sd_resolved_plugin(self))
         systemd_resolved_changed = TRUE;
 
     g_object_freeze_notify(G_OBJECT(self));
@@ -2390,6 +2491,23 @@ again:
                                   ""));
     }
 
+    if (plugin_changed && priv->plugin && priv->update_changed_signal_id == 0) {
+        priv->update_changed_signal_id = g_signal_connect(priv->plugin,
+                                                          NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED,
+                                                          G_CALLBACK(_update_pending_changed_cb),
+                                                          self);
+    }
+
+    if (systemd_resolved_changed && priv->sd_resolve_plugin
+        && priv->update_changed_signal_id_sd == 0) {
+        priv->update_changed_signal_id_sd = g_signal_connect(priv->sd_resolve_plugin,
+                                                             NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED,
+                                                             G_CALLBACK(_update_pending_changed_cb),
+                                                             self);
+    }
+
+    _update_pending_maybe_changed(self);
+
     g_object_thaw_notify(G_OBJECT(self));
 }
 
@@ -2594,6 +2712,9 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_CONFIGURATION:
         g_value_set_variant(value, _get_config_variant(self));
         break;
+    case PROP_UPDATE_PENDING:
+        g_value_set_boolean(value, nm_dns_manager_get_update_pending(self));
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -2641,9 +2762,11 @@ dispose(GObject *object)
     if (priv->config)
         g_signal_handlers_disconnect_by_func(priv->config, config_changed_cb, self);
 
-    g_clear_object(&priv->sd_resolve_plugin);
+    _clear_sd_resolved_plugin(self);
     _clear_plugin(self);
 
+    nm_clear_g_source_inst(&priv->update_pending_unblock);
+
     c_list_for_each_entry_safe (ip_data, ip_data_safe, &priv->ip_data_lst_head, ip_data_lst)
         _dns_config_ip_data_free(ip_data);
 
@@ -2719,6 +2842,13 @@ nm_dns_manager_class_init(NMDnsManagerClass *klass)
                              NULL,
                              G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_UPDATE_PENDING] =
+        g_param_spec_boolean(NM_DNS_MANAGER_UPDATE_PENDING,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
     signals[CONFIG_CHANGED] = g_signal_new(NM_DNS_MANAGER_CONFIG_CHANGED,
diff --git a/src/core/dns/nm-dns-manager.h b/src/core/dns/nm-dns-manager.h
index c30d4b3a..210f9f6c 100644
--- a/src/core/dns/nm-dns-manager.h
+++ b/src/core/dns/nm-dns-manager.h
@@ -80,9 +80,10 @@ typedef struct _NMDnsConfigData {
     (G_TYPE_INSTANCE_GET_CLASS((o), NM_TYPE_DNS_MANAGER, NMDnsManagerClass))
 
 /* properties */
-#define NM_DNS_MANAGER_MODE          "mode"
-#define NM_DNS_MANAGER_RC_MANAGER    "rc-manager"
-#define NM_DNS_MANAGER_CONFIGURATION "configuration"
+#define NM_DNS_MANAGER_MODE           "mode"
+#define NM_DNS_MANAGER_RC_MANAGER     "rc-manager"
+#define NM_DNS_MANAGER_CONFIGURATION  "configuration"
+#define NM_DNS_MANAGER_UPDATE_PENDING "update-pending"
 
 /* internal signals */
 #define NM_DNS_MANAGER_CONFIG_CHANGED "config-changed"
@@ -149,6 +150,8 @@ void nm_dns_manager_stop(NMDnsManager *self);
 
 NMDnsPlugin *nm_dns_manager_get_systemd_resolved(NMDnsManager *self);
 
+gboolean nm_dns_manager_get_update_pending(NMDnsManager *self);
+
 /*****************************************************************************/
 
 char *nmtst_dns_create_resolv_conf(const char *const *searches,
diff --git a/src/core/dns/nm-dns-plugin.c b/src/core/dns/nm-dns-plugin.c
index 847d7839..41a0dbc1 100644
--- a/src/core/dns/nm-dns-plugin.c
+++ b/src/core/dns/nm-dns-plugin.c
@@ -17,11 +17,16 @@
 
 /*****************************************************************************/
 
+enum {
+    UPDATE_PENDING_CHANGED,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
 typedef struct _NMDnsPluginPrivate {
-    GPid  pid;
-    guint watch_id;
-    char *progname;
-    char *pidfile;
+    bool update_pending_inited : 1;
+    bool update_pending : 1;
 } NMDnsPluginPrivate;
 
 G_DEFINE_ABSTRACT_TYPE(NMDnsPlugin, nm_dns_plugin, G_TYPE_OBJECT)
@@ -32,26 +37,29 @@ G_DEFINE_ABSTRACT_TYPE(NMDnsPlugin, nm_dns_plugin, G_TYPE_OBJECT)
 
 #define _NMLOG_PREFIX_NAME "dns-plugin"
 #define _NMLOG_DOMAIN      LOGD_DNS
-#define _NMLOG(level, ...)                                                    \
-    G_STMT_START                                                              \
-    {                                                                         \
-        const NMLogLevel __level = (level);                                   \
-                                                                              \
-        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                     \
-            char                     __prefix[20];                            \
-            const NMDnsPlugin *const __self = (self);                         \
-                                                                              \
-            _nm_log(__level,                                                  \
-                    _NMLOG_DOMAIN,                                            \
-                    0,                                                        \
-                    NULL,                                                     \
-                    NULL,                                                     \
-                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),              \
-                    _NMLOG_PREFIX_NAME,                                       \
-                    (!__self ? "" : nm_sprintf_buf(__prefix, "[%p]", __self)) \
-                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                   \
-        }                                                                     \
-    }                                                                         \
+#define _NMLOG(level, ...)                                                                      \
+    G_STMT_START                                                                                \
+    {                                                                                           \
+        const NMLogLevel __level = (level);                                                     \
+                                                                                                \
+        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                                       \
+            char                     __prefix[20];                                              \
+            const NMDnsPlugin *const __self = (self);                                           \
+                                                                                                \
+            _nm_log(__level,                                                                    \
+                    _NMLOG_DOMAIN,                                                              \
+                    0,                                                                          \
+                    NULL,                                                                       \
+                    NULL,                                                                       \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
+                    _NMLOG_PREFIX_NAME,                                                         \
+                    (!__self ? ""                                                               \
+                             : nm_sprintf_buf(__prefix,                                         \
+                                              "[" NM_HASH_OBFUSCATE_PTR_FMT "]",                \
+                                              NM_HASH_OBFUSCATE_PTR(                            \
+                                                  __self))) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                                       \
+    }                                                                                           \
     G_STMT_END
 
 /*****************************************************************************/
@@ -104,10 +112,109 @@ nm_dns_plugin_stop(NMDnsPlugin *self)
 
 /*****************************************************************************/
 
+static gboolean
+_get_update_pending(NMDnsPlugin *self)
+{
+    NMDnsPluginClass *klass;
+
+    nm_assert(NM_IS_DNS_PLUGIN(self));
+
+    klass = NM_DNS_PLUGIN_GET_CLASS(self);
+    if (klass->get_update_pending) {
+        if (klass->get_update_pending(self))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+gboolean
+nm_dns_plugin_get_update_pending(NMDnsPlugin *self)
+{
+    NMDnsPluginPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DNS_PLUGIN(self), FALSE);
+
+    priv = NM_DNS_PLUGIN_GET_PRIVATE(self);
+
+    /* We cache the boolean and rely on the subclass to call
+     * _nm_dns_plugin_update_pending_maybe_changed(). The subclass
+     * anyway must get it right to notify us when the value (maybe)
+     * changes. By caching the value, the subclass is free to notify
+     * even if the value did not actually change.
+     *
+     * Also, this allows the base implementation to combine multiple
+     * sources/reasons (if we need that in the future). */
+
+    if (!priv->update_pending_inited) {
+        priv->update_pending_inited = TRUE;
+        priv->update_pending        = _get_update_pending(self);
+        _LOGD("[%s] update-pending changed (%spending)",
+              nm_dns_plugin_get_name(self),
+              priv->update_pending ? "" : "not ");
+    } else
+        nm_assert(priv->update_pending == _get_update_pending(self));
+
+    return priv->update_pending;
+}
+
+void
+_nm_dns_plugin_update_pending_maybe_changed(NMDnsPlugin *self)
+{
+    NMDnsPluginPrivate *priv;
+    gboolean            v;
+
+    g_return_if_fail(NM_IS_DNS_PLUGIN(self));
+
+    priv = NM_DNS_PLUGIN_GET_PRIVATE(self);
+
+    v = _get_update_pending(self);
+
+    if (!priv->update_pending_inited)
+        priv->update_pending_inited = TRUE;
+    else if (priv->update_pending == v)
+        return;
+
+    priv->update_pending = v;
+
+    _LOGD("[%s] update-pending changed (%spending)",
+          nm_dns_plugin_get_name(self),
+          priv->update_pending ? "" : "not ");
+
+    g_signal_emit(self, signals[UPDATE_PENDING_CHANGED], 0, (gboolean) priv->update_pending);
+}
+
+/*****************************************************************************/
+
 static void
 nm_dns_plugin_init(NMDnsPlugin *self)
-{}
+{
+    NMDnsPluginPrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DNS_PLUGIN, NMDnsPluginPrivate);
+
+    self->_priv = priv;
+
+    nm_assert(priv->update_pending_inited == FALSE);
+    nm_assert(priv->update_pending == FALSE);
+
+    nm_shutdown_wait_obj_register_object(self, "dns-plugin");
+}
 
 static void
-nm_dns_plugin_class_init(NMDnsPluginClass *plugin_class)
-{}
+nm_dns_plugin_class_init(NMDnsPluginClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMDnsPluginPrivate));
+
+    signals[UPDATE_PENDING_CHANGED] = g_signal_new(NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED,
+                                                   G_OBJECT_CLASS_TYPE(klass),
+                                                   G_SIGNAL_RUN_FIRST,
+                                                   0,
+                                                   NULL,
+                                                   NULL,
+                                                   NULL,
+                                                   G_TYPE_NONE,
+                                                   1,
+                                                   G_TYPE_BOOLEAN);
+}
diff --git a/src/core/dns/nm-dns-plugin.h b/src/core/dns/nm-dns-plugin.h
index f9c424ab..24d6083b 100644
--- a/src/core/dns/nm-dns-plugin.h
+++ b/src/core/dns/nm-dns-plugin.h
@@ -19,8 +19,13 @@
 #define NM_DNS_PLUGIN_GET_CLASS(obj) \
     (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DNS_PLUGIN, NMDnsPluginClass))
 
+#define NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED "update-pending-changed"
+
+struct _NMDnsPluginPrivate;
+
 typedef struct {
-    GObject parent;
+    GObject                     parent;
+    struct _NMDnsPluginPrivate *_priv;
 } NMDnsPlugin;
 
 typedef struct {
@@ -39,6 +44,8 @@ typedef struct {
 
     void (*stop)(NMDnsPlugin *self);
 
+    gboolean (*get_update_pending)(NMDnsPlugin *self);
+
     const char *plugin_name;
 
     /* Types should set to TRUE if they start a local caching nameserver
@@ -63,4 +70,8 @@ gboolean nm_dns_plugin_update(NMDnsPlugin             *self,
 
 void nm_dns_plugin_stop(NMDnsPlugin *self);
 
+gboolean nm_dns_plugin_get_update_pending(NMDnsPlugin *self);
+
+void _nm_dns_plugin_update_pending_maybe_changed(NMDnsPlugin *self);
+
 #endif /* __NM_DNS_PLUGIN_H__ */
diff --git a/src/core/dns/nm-dns-systemd-resolved.c b/src/core/dns/nm-dns-systemd-resolved.c
index c4993884..e0b94647 100644
--- a/src/core/dns/nm-dns-systemd-resolved.c
+++ b/src/core/dns/nm-dns-systemd-resolved.c
@@ -40,8 +40,8 @@ static const char *const DBUS_OP_SET_LINK_DNS_OVER_TLS  = "SetLinkDNSOverTLS";
 /*****************************************************************************/
 
 typedef struct {
-    int   ifindex;
-    CList configs_lst_head;
+    int        ifindex;
+    GPtrArray *ip_data_list;
 } InterfaceConfig;
 
 typedef struct {
@@ -50,6 +50,7 @@ typedef struct {
     GVariant             *argument;
     NMDnsSystemdResolved *self;
     int                   ifindex;
+    int                   ref_count;
 } RequestItem;
 
 struct _NMDnsSystemdResolvedResolveHandle {
@@ -77,15 +78,18 @@ typedef struct {
     GDBusConnection *dbus_connection;
     GHashTable      *dirty_interfaces;
     GCancellable    *cancellable;
-    GSource         *try_start_timeout_source;
+    GCancellable    *service_start_cancellable;
     CList            request_queue_lst_head;
     char            *dbus_owner;
     CList            handle_lst_head;
     guint            name_owner_changed_id;
+    guint            n_pending;
     bool             send_updates_warn_ratelimited : 1;
     bool             try_start_blocked : 1;
+    bool             stopped : 1;
     bool             dbus_initied : 1;
     bool             send_updates_waiting : 1;
+    bool             update_pending : 1;
     /* These two variables ensure that the log is not spammed with
      * API (not) supported messages.
      * They can be removed when no distro uses systemd-resolved < v240 anymore
@@ -106,7 +110,7 @@ struct _NMDnsSystemdResolvedClass {
 G_DEFINE_TYPE(NMDnsSystemdResolved, nm_dns_systemd_resolved, NM_TYPE_DNS_PLUGIN)
 
 #define NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self) \
-    _NM_GET_PRIVATE(self, NMDnsSystemdResolved, NM_IS_DNS_SYSTEMD_RESOLVED)
+    _NM_GET_PRIVATE(self, NMDnsSystemdResolved, NM_IS_DNS_SYSTEMD_RESOLVED, NMDnsPlugin)
 
 /*****************************************************************************/
 
@@ -146,10 +150,88 @@ static void _resolve_start(NMDnsSystemdResolved *self, NMDnsSystemdResolvedResol
 
 /*****************************************************************************/
 
+static gboolean
+_update_pending_detect(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    if (priv->n_pending > 0) {
+        /* we have pending calls. We definitely want to wait for them to complete. */
+        return TRUE;
+    }
+    if (!priv->dbus_initied) {
+        if (!priv->dbus_connection)
+            return FALSE;
+        /* D-Bus not yet initialized (and we don't know the name owner yet). Pending. */
+        return TRUE;
+    }
+    if (priv->service_start_cancellable) {
+        /* We are waiting to D-Bus activate resolved. Pending. */
+        return TRUE;
+    }
+    if (priv->try_start_blocked) {
+        /* We earlier tried to start resolved, but are rate limited. We are not pending an update
+         * (that we expect to complete any time soon). */
+        return FALSE;
+    }
+    if (priv->send_updates_waiting) {
+        /* we wait to send updates. We are pending. */
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static void
+_update_pending_maybe_changed(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gboolean                     update_pending;
+
+    /* Important: we need to make sure that we call _update_pending_maybe_changed(), when
+     * the state changes. */
+
+    update_pending = _update_pending_detect(self);
+    if (priv->update_pending != update_pending) {
+        priv->update_pending = update_pending;
+        _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+    }
+}
+
+static gboolean
+get_update_pending(NMDnsPlugin *plugin)
+{
+    NMDnsSystemdResolved        *self = NM_DNS_SYSTEMD_RESOLVED(plugin);
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    nm_assert(priv->update_pending == _update_pending_detect(self));
+    return priv->update_pending;
+}
+
+/*****************************************************************************/
+
+static RequestItem *
+_request_item_ref(RequestItem *request_item)
+{
+    nm_assert(request_item);
+    nm_assert(request_item->ref_count > 0);
+    nm_assert(request_item->ref_count < G_MAXINT);
+    nm_assert(!c_list_is_empty(&request_item->request_queue_lst));
+
+    request_item->ref_count++;
+    return request_item;
+}
+
 static void
-_request_item_free(RequestItem *request_item)
+_request_item_unref(RequestItem *request_item)
 {
-    c_list_unlink_stale(&request_item->request_queue_lst);
+    nm_assert(request_item);
+    nm_assert(request_item->ref_count > 0);
+
+    if (--request_item->ref_count > 0)
+        return;
+
+    nm_assert(c_list_is_empty(&request_item->request_queue_lst));
+
     g_variant_unref(request_item->argument);
     nm_g_slice_free(request_item);
 }
@@ -165,6 +247,7 @@ _request_item_append(NMDnsSystemdResolved *self,
 
     request_item  = g_slice_new(RequestItem);
     *request_item = (RequestItem){
+        .ref_count = 1,
         .operation = operation,
         .argument  = g_variant_ref_sink(argument),
         .self      = self,
@@ -178,8 +261,8 @@ _request_item_append(NMDnsSystemdResolved *self,
 static void
 _interface_config_free(InterfaceConfig *config)
 {
-    nm_c_list_elem_free_all(&config->configs_lst_head, NULL);
-    g_slice_free(InterfaceConfig, config);
+    nm_g_ptr_array_unref(config->ip_data_list);
+    nm_g_slice_free(config);
 }
 
 static void
@@ -191,42 +274,48 @@ call_done(GObject *source, GAsyncResult *r, gpointer user_data)
     NMDnsSystemdResolvedPrivate *priv;
     RequestItem                 *request_item;
     NMLogLevel                   log_level;
-
-    v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), r, &error);
-    if (nm_utils_error_is_cancelled(error))
-        return;
+    const char                  *operation;
+    int                          ifindex;
 
     request_item = user_data;
     self         = request_item->self;
-    priv         = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    operation    = request_item->operation;
+    ifindex      = request_item->ifindex;
+    _request_item_unref(request_item);
+
+    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), r, &error);
+    if (nm_utils_error_is_cancelled(error))
+        goto out_dec_pending;
 
     if (v) {
-        if (request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
+        if (operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
             && priv->has_link_default_route == NM_TERNARY_DEFAULT) {
             priv->has_link_default_route = NM_TERNARY_TRUE;
             _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API supported");
         }
-        if (request_item->operation == DBUS_OP_SET_LINK_DNS_OVER_TLS
+        if (operation == DBUS_OP_SET_LINK_DNS_OVER_TLS
             && priv->has_link_dns_over_tls == NM_TERNARY_DEFAULT) {
             priv->has_link_dns_over_tls = NM_TERNARY_TRUE;
             _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API supported");
         }
         priv->send_updates_warn_ratelimited = FALSE;
-        return;
+        goto out_dec_pending;
     }
 
     if (nm_g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
         if (priv->has_link_default_route == NM_TERNARY_DEFAULT
-            && request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
+            && operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
             priv->has_link_default_route = NM_TERNARY_FALSE;
             _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API not supported");
         }
         if (priv->has_link_dns_over_tls == NM_TERNARY_DEFAULT
-            && request_item->operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
+            && operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
             priv->has_link_dns_over_tls = NM_TERNARY_FALSE;
             _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API not supported");
         }
-        return;
+        goto out_dec_pending;
     }
 
     log_level = LOGL_DEBUG;
@@ -234,18 +323,25 @@ call_done(GObject *source, GAsyncResult *r, gpointer user_data)
         priv->send_updates_warn_ratelimited = TRUE;
         log_level                           = LOGL_WARN;
     }
-    _NMLOG(log_level,
-           "send-updates %s@%d failed: %s",
-           request_item->operation,
-           request_item->ifindex,
-           error->message);
+    _NMLOG(log_level, "send-updates %s@%d failed: %s", operation, ifindex, error->message);
+
+out_dec_pending:
+    nm_assert(priv->n_pending > 0);
+    if (--priv->n_pending <= 0) {
+        _update_pending_maybe_changed(self);
+        /* We keep @self alive while pending operations are in progress. It's simpler
+         * to implement. But this requires that we implement "stop()" signal to cancel
+         * all pending requests. Cancelling is necessary, because during shutdown,
+         * we must wrap up fast, and not hang an undefined amount time. */
+        g_object_unref(self);
+    }
 }
 
 static gboolean
-update_add_ip_config(NMDnsSystemdResolved *self,
-                     GVariantBuilder      *dns,
-                     GVariantBuilder      *domains,
-                     NMDnsConfigIPData    *ip_data)
+update_add_ip_config(NMDnsSystemdResolved    *self,
+                     GVariantBuilder         *dns,
+                     GVariantBuilder         *domains,
+                     const NMDnsConfigIPData *ip_data)
 {
     gsize         addr_size;
     guint         n;
@@ -258,8 +354,12 @@ update_add_ip_config(NMDnsSystemdResolved *self,
     addr_size = nm_utils_addr_family_to_size(ip_data->addr_family);
 
     if ((!ip_data->domains.search || !ip_data->domains.search[0])
-        && !ip_data->domains.has_default_route_exclusive && !ip_data->domains.has_default_route)
+        && !ip_data->domains.has_default_route_exclusive && !ip_data->domains.has_default_route) {
+        /* we have no search domain (which systemd-resolved uses to routing the request), but
+         * also the "DefaultRoute" is not set on the interface. This setting has no effect and
+         * gets ignored. */
         return FALSE;
+    }
 
     nameservers = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &n);
     for (i = 0; i < n; i++) {
@@ -295,23 +395,28 @@ free_pending_updates(NMDnsSystemdResolved *self)
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
     RequestItem                 *request_item;
 
-    while ((request_item =
-                c_list_first_entry(&priv->request_queue_lst_head, RequestItem, request_queue_lst)))
-        _request_item_free(request_item);
+    while (
+        (request_item =
+             c_list_first_entry(&priv->request_queue_lst_head, RequestItem, request_queue_lst))) {
+        c_list_unlink(&request_item->request_queue_lst);
+        _request_item_unref(request_item);
+    }
 }
 
 static gboolean
-prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
+prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
 {
     GVariantBuilder               dns;
     GVariantBuilder               domains;
-    NMCListElem                  *elem;
-    NMSettingConnectionMdns       mdns         = NM_SETTING_CONNECTION_MDNS_DEFAULT;
-    NMSettingConnectionLlmnr      llmnr        = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
-    NMSettingConnectionDnsOverTls dns_over_tls = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT;
-    const char                   *mdns_arg = NULL, *llmnr_arg = NULL, *dns_over_tls_arg = NULL;
+    NMSettingConnectionMdns       mdns              = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+    NMSettingConnectionLlmnr      llmnr             = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
+    NMSettingConnectionDnsOverTls dns_over_tls      = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT;
+    const char                   *mdns_arg          = NULL;
+    const char                   *llmnr_arg         = NULL;
+    const char                   *dns_over_tls_arg  = NULL;
     gboolean                      has_config        = FALSE;
     gboolean                      has_default_route = FALSE;
+    guint                         i;
 
     g_variant_builder_init(&dns, G_VARIANT_TYPE("(ia(iay))"));
     g_variant_builder_add(&dns, "i", ic->ifindex);
@@ -321,18 +426,22 @@ prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
     g_variant_builder_add(&domains, "i", ic->ifindex);
     g_variant_builder_open(&domains, G_VARIANT_TYPE("a(sb)"));
 
-    c_list_for_each_entry (elem, &ic->configs_lst_head, lst) {
-        NMDnsConfigIPData *ip_data = elem->data;
+    if (ic->ip_data_list) {
+        for (i = 0; i < ic->ip_data_list->len; i++) {
+            const NMDnsConfigIPData *ip_data = ic->ip_data_list->pdata[i];
 
-        has_config |= update_add_ip_config(self, &dns, &domains, ip_data);
+            if (update_add_ip_config(self, &dns, &domains, ip_data))
+                has_config = TRUE;
 
-        if (ip_data->domains.has_default_route)
-            has_default_route = TRUE;
+            if (ip_data->domains.has_default_route)
+                has_default_route = TRUE;
 
-        if (NM_IS_IPv4(ip_data->addr_family)) {
-            mdns         = NM_MAX(mdns, nm_l3_config_data_get_mdns(ip_data->l3cd));
-            llmnr        = NM_MAX(llmnr, nm_l3_config_data_get_llmnr(ip_data->l3cd));
-            dns_over_tls = NM_MAX(dns_over_tls, nm_l3_config_data_get_dns_over_tls(ip_data->l3cd));
+            if (NM_IS_IPv4(ip_data->addr_family)) {
+                mdns  = NM_MAX(mdns, nm_l3_config_data_get_mdns(ip_data->l3cd));
+                llmnr = NM_MAX(llmnr, nm_l3_config_data_get_llmnr(ip_data->l3cd));
+                dns_over_tls =
+                    NM_MAX(dns_over_tls, nm_l3_config_data_get_dns_over_tls(ip_data->l3cd));
+            }
         }
     }
 
@@ -413,33 +522,45 @@ prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
     return has_config;
 }
 
-static gboolean
-_ensure_resolved_running_timeout(gpointer user_data)
+static void
+start_resolved_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    NMDnsSystemdResolved              *self = user_data;
-    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gs_unref_variant GVariant         *res   = NULL;
+    gs_free_error GError              *error = NULL;
+    NMDnsSystemdResolved              *self;
+    NMDnsSystemdResolvedPrivate       *priv;
     NMDnsSystemdResolvedResolveHandle *handle;
 
-    nm_clear_g_source_inst(&priv->try_start_timeout_source);
+    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    nm_clear_g_cancellable(&priv->service_start_cancellable);
 
-    _LOGT("timeout waiting to D-Bus activate systemd-resolved. Systemd-resolved won't be "
-          "used until it appears on the bus");
+    if (!res) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGD("error activating systemd-resolved: %s", error->message);
 
 again:
-    c_list_for_each_entry (handle, &priv->handle_lst_head, handle_lst) {
-        gs_free_error GError *error = NULL;
+        c_list_for_each_entry (handle, &priv->handle_lst_head, handle_lst) {
+            gs_free_error GError *local = NULL;
 
-        if (handle->is_failing_on_idle)
-            continue;
+            if (handle->is_failing_on_idle)
+                continue;
 
-        nm_utils_error_set_literal(&error,
-                                   NM_UTILS_ERROR_NOT_READY,
-                                   "timeout waiting for systemd-resolved to start");
-        _resolve_complete_error(handle, error);
-        goto again;
-    }
+            nm_utils_error_set(&local,
+                               NM_UTILS_ERROR_NOT_READY,
+                               "error activating systemd-resolved: %s",
+                               error->message);
+            _resolve_complete_error(handle, local);
+            goto again;
+        }
+    } else
+        _LOGD("systemd-resolved successfully started");
 
-    return G_SOURCE_CONTINUE;
+    _update_pending_maybe_changed(self);
 }
 
 static NMTernary
@@ -447,6 +568,9 @@ ensure_resolved_running(NMDnsSystemdResolved *self)
 {
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
 
+    if (priv->stopped)
+        return NM_TERNARY_FALSE;
+
     if (!priv->dbus_initied)
         return NM_TERNARY_DEFAULT;
 
@@ -459,16 +583,15 @@ ensure_resolved_running(NMDnsSystemdResolved *self)
 
         _LOGT("try D-Bus activating systemd-resolved...");
         priv->try_start_blocked = TRUE;
-
-        priv->try_start_timeout_source =
-            nm_g_timeout_add_source(4000, _ensure_resolved_running_timeout, self);
-
+        nm_clear_g_cancellable(&priv->service_start_cancellable);
+        priv->service_start_cancellable = g_cancellable_new();
         nm_dbus_connection_call_start_service_by_name(priv->dbus_connection,
                                                       SYSTEMD_RESOLVED_DBUS_SERVICE,
-                                                      -1,
-                                                      NULL,
-                                                      NULL,
-                                                      NULL);
+                                                      4000,
+                                                      priv->service_start_cancellable,
+                                                      start_resolved_cb,
+                                                      self);
+        _update_pending_maybe_changed(self);
         return NM_TERNARY_DEFAULT;
     }
 
@@ -523,6 +646,12 @@ send_updates(NMDnsSystemdResolved *self)
               request_item->operation,
               (ss = g_variant_print(request_item->argument, FALSE)));
 
+        if (priv->n_pending++ == 0) {
+            /* We are inside send_updates(). All callers are already calling
+             * _update_pending_maybe_changed() afterwards. */
+            g_object_ref(self);
+        }
+
         g_dbus_connection_call(priv->dbus_connection,
                                priv->dbus_owner,
                                SYSTEMD_RESOLVED_DBUS_PATH,
@@ -534,7 +663,7 @@ send_updates(NMDnsSystemdResolved *self)
                                -1,
                                priv->cancellable,
                                call_done,
-                               request_item);
+                               _request_item_ref(request_item));
     }
 
 start_resolve:
@@ -554,43 +683,54 @@ update(NMDnsPlugin             *plugin,
        const char              *hostdomain,
        GError                 **error)
 {
-    NMDnsSystemdResolved          *self            = NM_DNS_SYSTEMD_RESOLVED(plugin);
-    NMDnsSystemdResolvedPrivate   *priv            = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
-    gs_unref_hashtable GHashTable *interfaces      = NULL;
-    gs_free gpointer              *interfaces_keys = NULL;
+    NMDnsSystemdResolved          *self       = NM_DNS_SYSTEMD_RESOLVED(plugin);
+    NMDnsSystemdResolvedPrivate   *priv       = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gs_unref_hashtable GHashTable *interfaces = NULL;
+    const NMUtilsNamedValue       *interfaces_arr;
+    NMUtilsNamedValue              interfaces_arr_stack[50];
+    gs_free NMUtilsNamedValue     *interfaces_arr_heap = NULL;
     guint                          interfaces_len;
-    int                            ifindex;
     gpointer                       pointer;
     NMDnsConfigIPData             *ip_data;
     GHashTableIter                 iter;
+    gs_unref_array GArray         *dirty_array = NULL;
     guint                          i;
 
+    nm_assert(!priv->stopped);
+
+    /* Group configs by ifindex/interfaces. */
     interfaces =
         g_hash_table_new_full(nm_direct_hash, NULL, NULL, (GDestroyNotify) _interface_config_free);
 
     c_list_for_each_entry (ip_data, ip_data_lst_head, ip_data_lst) {
-        InterfaceConfig *ic = NULL;
+        InterfaceConfig *ic      = NULL;
+        int              ifindex = ip_data->data->ifindex;
 
-        ifindex = ip_data->data->ifindex;
         nm_assert(ifindex == nm_l3_config_data_get_ifindex(ip_data->l3cd));
 
         ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(ifindex));
         if (!ic) {
-            ic          = g_slice_new(InterfaceConfig);
-            ic->ifindex = ifindex;
-            c_list_init(&ic->configs_lst_head);
+            ic  = g_slice_new(InterfaceConfig);
+            *ic = (InterfaceConfig){
+                .ifindex      = ifindex,
+                .ip_data_list = g_ptr_array_sized_new(4),
+            };
             g_hash_table_insert(interfaces, GINT_TO_POINTER(ifindex), ic);
         }
 
-        c_list_link_tail(&ic->configs_lst_head, &nm_c_list_elem_new_stale(ip_data)->lst);
+        g_ptr_array_add(ic->ip_data_list, ip_data);
     }
 
     free_pending_updates(self);
 
-    interfaces_keys =
-        nm_utils_hash_keys_to_array(interfaces, nm_cmp_int2ptr_p_with_data, NULL, &interfaces_len);
+    interfaces_arr = nm_utils_hash_to_array_with_buffer(interfaces,
+                                                        &interfaces_len,
+                                                        nm_cmp_int2ptr_p_with_data,
+                                                        NULL,
+                                                        interfaces_arr_stack,
+                                                        &interfaces_arr_heap);
     for (i = 0; i < interfaces_len; i++) {
-        InterfaceConfig *ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(interfaces_keys[i]));
+        const InterfaceConfig *ic = interfaces_arr[i].value_ptr;
 
         if (prepare_one_interface(self, ic))
             g_hash_table_add(priv->dirty_interfaces, GINT_TO_POINTER(ic->ifindex));
@@ -602,23 +742,38 @@ update(NMDnsPlugin             *plugin,
      * resolved, and the current update doesn't contain that interface,
      * reset the resolved configuration for that ifindex. */
     g_hash_table_iter_init(&iter, priv->dirty_interfaces);
-    while (g_hash_table_iter_next(&iter, (gpointer *) &pointer, NULL)) {
-        ifindex = GPOINTER_TO_INT(pointer);
-        if (!g_hash_table_contains(interfaces, GINT_TO_POINTER(ifindex))) {
+    while (g_hash_table_iter_next(&iter, &pointer, NULL)) {
+        int ifindex = GPOINTER_TO_INT(pointer);
+
+        if (g_hash_table_contains(interfaces, GINT_TO_POINTER(ifindex))) {
+            /* the interface is still tracked and still dirty. Keep. */
+            continue;
+        }
+
+        if (!dirty_array)
+            dirty_array = g_array_new(FALSE, FALSE, sizeof(int));
+        g_array_append_val(dirty_array, ifindex);
+
+        g_hash_table_iter_remove(&iter);
+    }
+    if (dirty_array) {
+        g_array_sort_with_data(dirty_array, nm_cmp_int2ptr_p_with_data, NULL);
+        for (i = 0; i < dirty_array->len; i++) {
+            int             ifindex = g_array_index(dirty_array, int, i);
             InterfaceConfig ic;
 
             _LOGT("clear previously configured ifindex %d", ifindex);
             ic = (InterfaceConfig){
-                .ifindex          = ifindex,
-                .configs_lst_head = C_LIST_INIT(ic.configs_lst_head),
+                .ifindex      = ifindex,
+                .ip_data_list = NULL,
             };
             prepare_one_interface(self, &ic);
-            g_hash_table_iter_remove(&iter);
         }
     }
 
     priv->send_updates_waiting = TRUE;
     send_updates(self);
+    _update_pending_maybe_changed(self);
     return TRUE;
 }
 
@@ -636,8 +791,7 @@ name_owner_changed(NMDnsSystemdResolved *self, const char *owner)
     else
         _LOGT("D-Bus name for systemd-resolved has owner %s", owner);
 
-    nm_clear_g_source_inst(&priv->try_start_timeout_source);
-
+    nm_clear_g_cancellable(&priv->service_start_cancellable);
     nm_strdup_reset(&priv->dbus_owner, owner);
 
     if (owner) {
@@ -649,6 +803,7 @@ name_owner_changed(NMDnsSystemdResolved *self, const char *owner)
     }
 
     send_updates(self);
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -957,6 +1112,48 @@ nm_dns_systemd_resolved_resolve_cancel(NMDnsSystemdResolvedResolveHandle *handle
 /*****************************************************************************/
 
 static void
+stop(NMDnsPlugin *plugin)
+{
+    NMDnsSystemdResolved              *self = NM_DNS_SYSTEMD_RESOLVED(plugin);
+    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    NMDnsSystemdResolvedResolveHandle *handle;
+
+    /* This function must be re-entrant!!
+     *
+     * Currently there is no concept of unregistering/shutting down. It's not
+     * clear whether we should de-configure anything in systemd-resolved, we
+     * don't.
+     *
+     * Implementing stop() is important because pending operations take a
+     * reference on @self. We can only cancel (fast shutdown) the instance
+     * by cancelling those requests. */
+
+    priv->stopped           = TRUE;
+    priv->try_start_blocked = TRUE;
+
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    nm_clear_g_free(&priv->dbus_owner);
+
+    while ((handle = c_list_first_entry(&priv->handle_lst_head,
+                                        NMDnsSystemdResolvedResolveHandle,
+                                        handle_lst))) {
+        gs_free_error GError *error = NULL;
+
+        nm_utils_error_set_cancelled(&error, TRUE, "NMDnsSystemdResolved");
+        _resolve_complete_error(handle, error);
+    }
+
+    free_pending_updates(self);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
+
+    nm_clear_g_cancellable(&priv->service_start_cancellable);
+}
+
+/*****************************************************************************/
+
+static void
 nm_dns_systemd_resolved_init(NMDnsSystemdResolved *self)
 {
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
@@ -974,6 +1171,8 @@ nm_dns_systemd_resolved_init(NMDnsSystemdResolved *self)
         return;
     }
 
+    priv->update_pending = TRUE;
+
     priv->name_owner_changed_id =
         nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
                                                                SYSTEMD_RESOLVED_DBUS_SERVICE,
@@ -998,33 +1197,15 @@ nm_dns_systemd_resolved_new(void)
 static void
 dispose(GObject *object)
 {
-    NMDnsSystemdResolved              *self = NM_DNS_SYSTEMD_RESOLVED(object);
-    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
-    NMDnsSystemdResolvedResolveHandle *handle;
-
-    while ((handle = c_list_first_entry(&priv->handle_lst_head,
-                                        NMDnsSystemdResolvedResolveHandle,
-                                        handle_lst))) {
-        gs_free_error GError *error = NULL;
-
-        nm_utils_error_set_cancelled(&error, TRUE, "NMDnsSystemdResolved");
-        _resolve_complete_error(handle, error);
-    }
-
-    free_pending_updates(self);
-
-    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
-
-    nm_clear_g_cancellable(&priv->cancellable);
+    NMDnsSystemdResolved        *self = NM_DNS_SYSTEMD_RESOLVED(object);
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
 
-    nm_clear_g_source_inst(&priv->try_start_timeout_source);
+    stop(NM_DNS_PLUGIN(self));
 
     g_clear_object(&priv->dbus_connection);
-    nm_clear_pointer(&priv->dirty_interfaces, g_hash_table_unref);
+    nm_clear_pointer(&priv->dirty_interfaces, g_hash_table_destroy);
 
     G_OBJECT_CLASS(nm_dns_systemd_resolved_parent_class)->dispose(object);
-
-    nm_clear_g_free(&priv->dbus_owner);
 }
 
 static void
@@ -1035,7 +1216,9 @@ nm_dns_systemd_resolved_class_init(NMDnsSystemdResolvedClass *dns_class)
 
     object_class->dispose = dispose;
 
-    plugin_class->plugin_name = "systemd-resolved";
-    plugin_class->is_caching  = TRUE;
-    plugin_class->update      = update;
+    plugin_class->plugin_name        = "systemd-resolved";
+    plugin_class->is_caching         = TRUE;
+    plugin_class->stop               = stop;
+    plugin_class->update             = update;
+    plugin_class->get_update_pending = get_update_pending;
 }
diff --git a/src/core/dns/nm-dns-unbound.c b/src/core/dns/nm-dns-unbound.c
deleted file mode 100644
index 8a75cf08..00000000
--- a/src/core/dns/nm-dns-unbound.c
+++ /dev/null
@@ -1,84 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2014 Red Hat, Inc.
- * Author: Pavel Šimerda <psimerda@redhat.com>
- */
-
-#include "src/core/nm-default-daemon.h"
-
-#include "nm-dns-unbound.h"
-
-#include "NetworkManagerUtils.h"
-
-/*****************************************************************************/
-
-struct _NMDnsUnbound {
-    NMDnsPlugin parent;
-};
-
-struct _NMDnsUnboundClass {
-    NMDnsPluginClass parent;
-};
-
-G_DEFINE_TYPE(NMDnsUnbound, nm_dns_unbound, NM_TYPE_DNS_PLUGIN)
-
-/*****************************************************************************/
-
-static gboolean
-update(NMDnsPlugin             *plugin,
-       const NMGlobalDnsConfig *global_config,
-       const CList             *ip_config_lst_head,
-       const char              *hostdomain,
-       GError                 **error)
-{
-    char                 *argv[] = {DNSSEC_TRIGGER_PATH, "--async", "--update", NULL};
-    gs_free_error GError *local  = NULL;
-    int                   status;
-
-    /* TODO: We currently call a script installed with the dnssec-trigger
-     * package that queries all information itself. Later, the dependency
-     * on that package will be optional and the only hard dependency will
-     * be unbound.
-     *
-     * Unbound configuration should be later handled by this plugin directly,
-     * without calling custom scripts. The dnssec-trigger functionality
-     * may be eventually merged into NetworkManager.
-     */
-    if (!g_spawn_sync("/", argv, NULL, 0, NULL, NULL, NULL, NULL, &status, &local)) {
-        nm_utils_error_set(error,
-                           NM_UTILS_ERROR_UNKNOWN,
-                           "error spawning dns-trigger: %s",
-                           local->message);
-        return FALSE;
-    }
-    if (status != 0) {
-        nm_utils_error_set(error,
-                           NM_UTILS_ERROR_UNKNOWN,
-                           "dns-trigger exited with error code %d",
-                           status);
-        return FALSE;
-    }
-    return TRUE;
-}
-
-/*****************************************************************************/
-
-static void
-nm_dns_unbound_init(NMDnsUnbound *unbound)
-{}
-
-NMDnsPlugin *
-nm_dns_unbound_new(void)
-{
-    return g_object_new(NM_TYPE_DNS_UNBOUND, NULL);
-}
-
-static void
-nm_dns_unbound_class_init(NMDnsUnboundClass *klass)
-{
-    NMDnsPluginClass *plugin_class = NM_DNS_PLUGIN_CLASS(klass);
-
-    plugin_class->plugin_name = "unbound";
-    plugin_class->is_caching  = TRUE;
-    plugin_class->update      = update;
-}
diff --git a/src/core/dns/nm-dns-unbound.h b/src/core/dns/nm-dns-unbound.h
deleted file mode 100644
index feb33099..00000000
--- a/src/core/dns/nm-dns-unbound.h
+++ /dev/null
@@ -1,27 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2014 Red Hat, Inc.
- */
-
-#ifndef __NETWORKMANAGER_DNS_UNBOUND_H__
-#define __NETWORKMANAGER_DNS_UNBOUND_H__
-
-#include "nm-dns-plugin.h"
-
-#define NM_TYPE_DNS_UNBOUND (nm_dns_unbound_get_type())
-#define NM_DNS_UNBOUND(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_UNBOUND, NMDnsUnbound))
-#define NM_DNS_UNBOUND_CLASS(klass) \
-    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNS_UNBOUND, NMDnsUnboundClass))
-#define NM_IS_DNS_UNBOUND(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DNS_UNBOUND))
-#define NM_IS_DNS_UNBOUND_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DNS_UNBOUND))
-#define NM_DNS_UNBOUND_GET_CLASS(obj) \
-    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DNS_UNBOUND, NMDnsUnboundClass))
-
-typedef struct _NMDnsUnbound      NMDnsUnbound;
-typedef struct _NMDnsUnboundClass NMDnsUnboundClass;
-
-GType nm_dns_unbound_get_type(void);
-
-NMDnsPlugin *nm_dns_unbound_new(void);
-
-#endif /* __NETWORKMANAGER_DNS_UNBOUND_H__ */