summary refs log tree commit diff
path: root/src/core/dns/nm-dns-systemd-resolved.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/dns/nm-dns-systemd-resolved.c')
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.c640
1 files changed, 640 insertions, 0 deletions
diff --git a/src/core/dns/nm-dns-systemd-resolved.c b/src/core/dns/nm-dns-systemd-resolved.c
new file mode 100644
index 00000000..f6e116f3
--- /dev/null
+++ b/src/core/dns/nm-dns-systemd-resolved.c
@@ -0,0 +1,640 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2010 Dan Williams <dcbw@redhat.com>
+ * Copyright (C) 2016 Sjoerd Simons <sjoerd@luon.net>
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-dns-systemd-resolved.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <arpa/inet.h>
+#include <sys/stat.h>
+#include <linux/if.h>
+
+#include "nm-glib-aux/nm-c-list.h"
+#include "nm-glib-aux/nm-dbus-aux.h"
+#include "nm-core-internal.h"
+#include "platform/nm-platform.h"
+#include "nm-utils.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+#include "nm-dbus-manager.h"
+#include "nm-manager.h"
+#include "nm-setting-connection.h"
+#include "devices/nm-device.h"
+#include "NetworkManagerUtils.h"
+#include "nm-std-aux/nm-dbus-compat.h"
+
+#define SYSTEMD_RESOLVED_DBUS_SERVICE  "org.freedesktop.resolve1"
+#define SYSTEMD_RESOLVED_MANAGER_IFACE "org.freedesktop.resolve1.Manager"
+#define SYSTEMD_RESOLVED_DBUS_PATH     "/org/freedesktop/resolve1"
+
+/* define a variable, so that we can compare the operation with pointer equality. */
+static const char *const DBUS_OP_SET_LINK_DEFAULT_ROUTE = "SetLinkDefaultRoute";
+
+/*****************************************************************************/
+
+typedef struct {
+    int   ifindex;
+    CList configs_lst_head;
+} InterfaceConfig;
+
+typedef struct {
+    CList                 request_queue_lst;
+    const char *          operation;
+    GVariant *            argument;
+    NMDnsSystemdResolved *self;
+    int                   ifindex;
+} RequestItem;
+
+/*****************************************************************************/
+
+typedef struct {
+    GDBusConnection *dbus_connection;
+    GHashTable *     dirty_interfaces;
+    GCancellable *   cancellable;
+    CList            request_queue_lst_head;
+    guint            name_owner_changed_id;
+    bool             send_updates_warn_ratelimited : 1;
+    bool             try_start_blocked : 1;
+    bool             dbus_has_owner : 1;
+    bool             dbus_initied : 1;
+    bool             request_queue_to_send : 1;
+    NMTernary        has_link_default_route : 3;
+} NMDnsSystemdResolvedPrivate;
+
+struct _NMDnsSystemdResolved {
+    NMDnsPlugin                 parent;
+    NMDnsSystemdResolvedPrivate _priv;
+};
+
+struct _NMDnsSystemdResolvedClass {
+    NMDnsPluginClass parent;
+};
+
+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)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN LOGD_DNS
+#define _NMLOG(level, ...) \
+    __NMLOG_DEFAULT_WITH_ADDR(level, _NMLOG_DOMAIN, "dns-sd-resolved", __VA_ARGS__)
+
+/*****************************************************************************/
+
+static void
+_request_item_free(RequestItem *request_item)
+{
+    c_list_unlink_stale(&request_item->request_queue_lst);
+    g_variant_unref(request_item->argument);
+    nm_g_slice_free(request_item);
+}
+
+static void
+_request_item_append(NMDnsSystemdResolved *self,
+                     const char *          operation,
+                     int                   ifindex,
+                     GVariant *            argument)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    RequestItem *                request_item;
+
+    request_item  = g_slice_new(RequestItem);
+    *request_item = (RequestItem){
+        .operation = operation,
+        .argument  = g_variant_ref_sink(argument),
+        .self      = self,
+        .ifindex   = ifindex,
+    };
+    c_list_link_tail(&priv->request_queue_lst_head, &request_item->request_queue_lst);
+}
+
+/*****************************************************************************/
+
+static void
+_interface_config_free(InterfaceConfig *config)
+{
+    nm_c_list_elem_free_all(&config->configs_lst_head, NULL);
+    g_slice_free(InterfaceConfig, config);
+}
+
+static void
+call_done(GObject *source, GAsyncResult *r, gpointer user_data)
+{
+    gs_unref_variant GVariant *v       = NULL;
+    gs_free_error GError *       error = NULL;
+    NMDnsSystemdResolved *       self;
+    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;
+
+    request_item = user_data;
+    self         = request_item->self;
+    priv         = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    if (v) {
+        if (request_item->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");
+        }
+        priv->send_updates_warn_ratelimited = FALSE;
+        return;
+    }
+
+    if (request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
+        && nm_g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
+        if (priv->has_link_default_route == NM_TERNARY_DEFAULT) {
+            priv->has_link_default_route = NM_TERNARY_FALSE;
+            _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API not supported");
+        }
+        return;
+    }
+
+    log_level = LOGL_DEBUG;
+    if (!priv->send_updates_warn_ratelimited) {
+        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);
+}
+
+static gboolean
+update_add_ip_config(NMDnsSystemdResolved *self,
+                     GVariantBuilder *     dns,
+                     GVariantBuilder *     domains,
+                     NMDnsConfigIPData *   data)
+{
+    int         addr_family;
+    gsize       addr_size;
+    guint       i, n;
+    gboolean    is_routing;
+    const char *domain;
+    gboolean    has_config = FALSE;
+
+    addr_family = nm_ip_config_get_addr_family(data->ip_config);
+    addr_size   = nm_utils_addr_family_to_size(addr_family);
+
+    if ((!data->domains.search || !data->domains.search[0])
+        && !data->domains.has_default_route_exclusive && !data->domains.has_default_route)
+        return FALSE;
+
+    n = nm_ip_config_get_num_nameservers(data->ip_config);
+    for (i = 0; i < n; i++) {
+        g_variant_builder_open(dns, G_VARIANT_TYPE("(iay)"));
+        g_variant_builder_add(dns, "i", addr_family);
+        g_variant_builder_add_value(
+            dns,
+            g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE,
+                                      nm_ip_config_get_nameserver(data->ip_config, i),
+                                      addr_size,
+                                      1));
+        g_variant_builder_close(dns);
+        has_config = TRUE;
+    }
+
+    if (!data->domains.has_default_route_explicit && data->domains.has_default_route_exclusive) {
+        g_variant_builder_add(domains, "(sb)", ".", TRUE);
+        has_config = TRUE;
+    }
+    if (data->domains.search) {
+        for (i = 0; data->domains.search[i]; i++) {
+            domain = nm_utils_parse_dns_domain(data->domains.search[i], &is_routing);
+            g_variant_builder_add(domains, "(sb)", domain[0] ? domain : ".", is_routing);
+            has_config = TRUE;
+        }
+    }
+
+    return has_config;
+}
+
+static void
+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);
+}
+
+static gboolean
+prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
+{
+    GVariantBuilder          dns;
+    GVariantBuilder          domains;
+    NMCListElem *            elem;
+    NMSettingConnectionMdns  mdns     = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+    NMSettingConnectionLlmnr llmnr    = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
+    const char *             mdns_arg = NULL, *llmnr_arg = NULL;
+    gboolean                 has_config        = FALSE;
+    gboolean                 has_default_route = FALSE;
+
+    g_variant_builder_init(&dns, G_VARIANT_TYPE("(ia(iay))"));
+    g_variant_builder_add(&dns, "i", ic->ifindex);
+    g_variant_builder_open(&dns, G_VARIANT_TYPE("a(iay)"));
+
+    g_variant_builder_init(&domains, G_VARIANT_TYPE("(ia(sb))"));
+    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 *data      = elem->data;
+        NMIPConfig *       ip_config = data->ip_config;
+
+        has_config |= update_add_ip_config(self, &dns, &domains, data);
+
+        if (data->domains.has_default_route)
+            has_default_route = TRUE;
+
+        if (NM_IS_IP4_CONFIG(ip_config)) {
+            mdns  = NM_MAX(mdns, nm_ip4_config_mdns_get(NM_IP4_CONFIG(ip_config)));
+            llmnr = NM_MAX(llmnr, nm_ip4_config_llmnr_get(NM_IP4_CONFIG(ip_config)));
+        }
+    }
+
+    g_variant_builder_close(&dns);
+    g_variant_builder_close(&domains);
+
+    switch (mdns) {
+    case NM_SETTING_CONNECTION_MDNS_NO:
+        mdns_arg = "no";
+        break;
+    case NM_SETTING_CONNECTION_MDNS_RESOLVE:
+        mdns_arg = "resolve";
+        break;
+    case NM_SETTING_CONNECTION_MDNS_YES:
+        mdns_arg = "yes";
+        break;
+    case NM_SETTING_CONNECTION_MDNS_DEFAULT:
+        mdns_arg = "";
+        break;
+    }
+    nm_assert(mdns_arg);
+
+    switch (llmnr) {
+    case NM_SETTING_CONNECTION_LLMNR_NO:
+        llmnr_arg = "no";
+        break;
+    case NM_SETTING_CONNECTION_LLMNR_RESOLVE:
+        llmnr_arg = "resolve";
+        break;
+    case NM_SETTING_CONNECTION_LLMNR_YES:
+        llmnr_arg = "yes";
+        break;
+    case NM_SETTING_CONNECTION_LLMNR_DEFAULT:
+        llmnr_arg = "";
+        break;
+    }
+    nm_assert(llmnr_arg);
+
+    if (!nm_str_is_empty(mdns_arg) || !nm_str_is_empty(llmnr_arg))
+        has_config = TRUE;
+
+    _request_item_append(self, "SetLinkDomains", ic->ifindex, g_variant_builder_end(&domains));
+    _request_item_append(self,
+                         DBUS_OP_SET_LINK_DEFAULT_ROUTE,
+                         ic->ifindex,
+                         g_variant_new("(ib)", ic->ifindex, has_default_route));
+    _request_item_append(self,
+                         "SetLinkMulticastDNS",
+                         ic->ifindex,
+                         g_variant_new("(is)", ic->ifindex, mdns_arg ?: ""));
+    _request_item_append(self,
+                         "SetLinkLLMNR",
+                         ic->ifindex,
+                         g_variant_new("(is)", ic->ifindex, llmnr_arg ?: ""));
+    _request_item_append(self, "SetLinkDNS", ic->ifindex, g_variant_builder_end(&dns));
+
+    return has_config;
+}
+
+static void
+send_updates(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    RequestItem *                request_item;
+
+    if (!priv->request_queue_to_send) {
+        /* nothing to do. */
+        return;
+    }
+
+    if (!priv->dbus_initied) {
+        _LOGT("send-updates: D-Bus connection not ready");
+        return;
+    }
+
+    if (!priv->dbus_has_owner) {
+        if (priv->try_start_blocked) {
+            /* we have no name owner and we already tried poking the service to
+             * autostart. */
+            _LOGT("send-updates: no name owner");
+            return;
+        }
+
+        _LOGT("send-updates: no name owner. Try start service...");
+        priv->try_start_blocked = TRUE;
+
+        nm_dbus_connection_call_start_service_by_name(priv->dbus_connection,
+                                                      SYSTEMD_RESOLVED_DBUS_SERVICE,
+                                                      -1,
+                                                      NULL,
+                                                      NULL,
+                                                      NULL);
+        return;
+    }
+
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    if (c_list_is_empty(&priv->request_queue_lst_head)) {
+        _LOGT("send-updates: no requests to send");
+        priv->request_queue_to_send = FALSE;
+        return;
+    }
+
+    _LOGT("send-updates: start %lu requests", c_list_length(&priv->request_queue_lst_head));
+
+    priv->cancellable = g_cancellable_new();
+
+    priv->request_queue_to_send = FALSE;
+
+    c_list_for_each_entry (request_item, &priv->request_queue_lst_head, request_queue_lst) {
+        if (request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
+            && priv->has_link_default_route == NM_TERNARY_FALSE) {
+            /* The "SetLinkDefaultRoute" API is only supported since v240.
+             * We detected that it is not supported, and skip the call. There
+             * is no special workaround, because in this case we rely on systemd-resolved
+             * to do the right thing automatically. */
+            continue;
+        }
+
+        /* Above we explicitly call "StartServiceByName" trying to avoid D-Bus activating systmd-resolved
+         * multiple times. There is still a race, were we might hit this line although actually
+         * the service just quit this very moment. In that case, we would try to D-Bus activate the
+         * service multiple times during each call (something we wanted to avoid).
+         *
+         * But this is hard to avoid, because we'd have to check the error failure to detect the reason
+         * and retry. The race is not critical, because at worst it results in logging a warning
+         * about failure to start systemd.resolved. */
+        g_dbus_connection_call(priv->dbus_connection,
+                               SYSTEMD_RESOLVED_DBUS_SERVICE,
+                               SYSTEMD_RESOLVED_DBUS_PATH,
+                               SYSTEMD_RESOLVED_MANAGER_IFACE,
+                               request_item->operation,
+                               request_item->argument,
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NONE,
+                               -1,
+                               priv->cancellable,
+                               call_done,
+                               request_item);
+    }
+}
+
+static gboolean
+update(NMDnsPlugin *            plugin,
+       const NMGlobalDnsConfig *global_config,
+       const CList *            ip_config_lst_head,
+       const char *             hostname,
+       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;
+    guint              interfaces_len;
+    int                ifindex;
+    gpointer           pointer;
+    NMDnsConfigIPData *ip_data;
+    GHashTableIter     iter;
+    guint              i;
+
+    interfaces =
+        g_hash_table_new_full(nm_direct_hash, NULL, NULL, (GDestroyNotify) _interface_config_free);
+
+    c_list_for_each_entry (ip_data, ip_config_lst_head, ip_config_lst) {
+        InterfaceConfig *ic = NULL;
+
+        ifindex = ip_data->data->ifindex;
+        nm_assert(ifindex == nm_ip_config_get_ifindex(ip_data->ip_config));
+
+        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);
+            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);
+    }
+
+    free_pending_updates(self);
+
+    interfaces_keys =
+        nm_utils_hash_keys_to_array(interfaces, nm_cmp_int2ptr_p_with_data, NULL, &interfaces_len);
+    for (i = 0; i < interfaces_len; i++) {
+        InterfaceConfig *ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(interfaces_keys[i]));
+
+        if (prepare_one_interface(self, ic))
+            g_hash_table_add(priv->dirty_interfaces, GINT_TO_POINTER(ic->ifindex));
+        else
+            g_hash_table_remove(priv->dirty_interfaces, GINT_TO_POINTER(ic->ifindex));
+    }
+
+    /* If we previously configured an ifindex with non-empty values in
+     * 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))) {
+            InterfaceConfig ic;
+
+            _LOGT("clear previously configured ifindex %d", ifindex);
+            ic = (InterfaceConfig){
+                .ifindex          = ifindex,
+                .configs_lst_head = C_LIST_INIT(ic.configs_lst_head),
+            };
+            prepare_one_interface(self, &ic);
+            g_hash_table_iter_remove(&iter);
+        }
+    }
+
+    priv->request_queue_to_send = TRUE;
+    send_updates(self);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+name_owner_changed(NMDnsSystemdResolved *self, const char *owner)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    owner = nm_str_not_empty(owner);
+
+    if (!owner)
+        _LOGT("D-Bus name for systemd-resolved has no owner");
+    else
+        _LOGT("D-Bus name for systemd-resolved has owner %s", owner);
+
+    priv->dbus_has_owner = !!owner;
+    if (owner) {
+        priv->try_start_blocked     = FALSE;
+        priv->request_queue_to_send = TRUE;
+    } else
+        priv->has_link_default_route = NM_TERNARY_DEFAULT;
+
+    send_updates(self);
+}
+
+static void
+name_owner_changed_cb(GDBusConnection *connection,
+                      const char *     sender_name,
+                      const char *     object_path,
+                      const char *     interface_name,
+                      const char *     signal_name,
+                      GVariant *       parameters,
+                      gpointer         user_data)
+{
+    NMDnsSystemdResolved *       self = user_data;
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    const char *                 new_owner;
+
+    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
+        return;
+
+    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);
+
+    if (!priv->dbus_initied) {
+        /* There was a race and we got a NameOwnerChanged signal before GetNameOwner
+         * returns. */
+        priv->dbus_initied = TRUE;
+        nm_clear_g_cancellable(&priv->cancellable);
+    }
+
+    name_owner_changed(user_data, new_owner);
+}
+
+static void
+get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
+{
+    NMDnsSystemdResolved *       self;
+    NMDnsSystemdResolvedPrivate *priv;
+
+    if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    g_clear_object(&priv->cancellable);
+
+    priv->dbus_initied = TRUE;
+
+    name_owner_changed(self, name_owner);
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_dns_systemd_resolved_is_running(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DNS_SYSTEMD_RESOLVED(self), FALSE);
+
+    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    return priv->dbus_initied && (priv->dbus_has_owner || !priv->try_start_blocked);
+}
+
+/*****************************************************************************/
+
+static void
+nm_dns_systemd_resolved_init(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    priv->has_link_default_route = NM_TERNARY_DEFAULT;
+
+    c_list_init(&priv->request_queue_lst_head);
+    priv->dirty_interfaces = g_hash_table_new(nm_direct_hash, NULL);
+
+    priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET);
+    if (!priv->dbus_connection) {
+        _LOGD("no D-Bus connection");
+        return;
+    }
+
+    priv->name_owner_changed_id =
+        nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
+                                                               SYSTEMD_RESOLVED_DBUS_SERVICE,
+                                                               name_owner_changed_cb,
+                                                               self,
+                                                               NULL);
+    priv->cancellable = g_cancellable_new();
+    nm_dbus_connection_call_get_name_owner(priv->dbus_connection,
+                                           SYSTEMD_RESOLVED_DBUS_SERVICE,
+                                           -1,
+                                           priv->cancellable,
+                                           get_name_owner_cb,
+                                           self);
+}
+
+NMDnsPlugin *
+nm_dns_systemd_resolved_new(void)
+{
+    return g_object_new(NM_TYPE_DNS_SYSTEMD_RESOLVED, NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDnsSystemdResolved *       self = NM_DNS_SYSTEMD_RESOLVED(object);
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    free_pending_updates(self);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
+
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    g_clear_object(&priv->dbus_connection);
+    nm_clear_pointer(&priv->dirty_interfaces, g_hash_table_unref);
+
+    G_OBJECT_CLASS(nm_dns_systemd_resolved_parent_class)->dispose(object);
+}
+
+static void
+nm_dns_systemd_resolved_class_init(NMDnsSystemdResolvedClass *dns_class)
+{
+    NMDnsPluginClass *plugin_class = NM_DNS_PLUGIN_CLASS(dns_class);
+    GObjectClass *    object_class = G_OBJECT_CLASS(dns_class);
+
+    object_class->dispose = dispose;
+
+    plugin_class->plugin_name = "systemd-resolved";
+    plugin_class->is_caching  = TRUE;
+    plugin_class->update      = update;
+}