/* 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 "libnm-glib-aux/nm-c-list.h"
#include "libnm-glib-aux/nm-dbus-aux.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-platform/nm-platform.h"
#include "nm-utils.h"
#include "nm-dbus-manager.h"
#include "nm-manager.h"
#include "nm-l3-config-data.h"
#include "nm-setting-connection.h"
#include "devices/nm-device.h"
#include "NetworkManagerUtils.h"
#include "libnm-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";
static const char *const DBUS_OP_SET_LINK_DNS_OVER_TLS  = "SetLinkDNSOverTLS";
static const char *const DBUS_OP_SET_LINK_DNS_EX        = "SetLinkDNSEx";
static const char *const DBUS_OP_SET_LINK_DNSSEC        = "SetLinkDNSSEC";

/*****************************************************************************/

typedef struct {
    int        ifindex;
    GPtrArray *ip_data_list;
} InterfaceConfig;

typedef struct {
    CList                 request_queue_lst;
    const char           *operation;
    GVariant             *argument;
    NMDnsSystemdResolved *self;
    int                   ifindex;
    int                   ref_count;
} RequestItem;

struct _NMDnsSystemdResolvedResolveHandle {
    CList                 handle_lst;
    NMDnsSystemdResolved *self;
    GSource              *timeout_source;
    GCancellable         *handle_cancellable;
    gpointer              callback_user_data;
    guint                 timeout_msec;
    bool                  is_failing_on_idle;
    union {
        struct {
            NMDnsSystemdResolvedResolveAddressCallback callback;
            guint64                                    flags;
            int                                        ifindex;
            int                                        addr_family;
            NMIPAddr                                   addr;
        } r_address;
    };
};

/*****************************************************************************/

typedef struct {
    GDBusConnection *dbus_connection;
    GHashTable      *dirty_interfaces;
    GCancellable    *cancellable;
    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;

    /* Detect support for the respective D-Bus API. */
    NMTernary has_set_link_default_route : 3;
    NMTernary has_set_link_dns_over_tls : 3;
    NMTernary has_set_link_dns_ex : 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, NMDnsPlugin)

/*****************************************************************************/

#define _NMLOG_DOMAIN      LOGD_DNS
#define _NMLOG_PREFIX_NAME "dns-sd-resolved"

#define _NMLOG(level, ...) \
    __NMLOG_DEFAULT_WITH_ADDR(level, _NMLOG_DOMAIN, _NMLOG_PREFIX_NAME, __VA_ARGS__)

#define _NMLOG2(level, handle, ...)                                                         \
    G_STMT_START                                                                            \
    {                                                                                       \
        const NMLogLevel _level = (level);                                                  \
                                                                                            \
        if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) {                                  \
            const NMDnsSystemdResolvedResolveHandle *const _handle = (handle);              \
                                                                                            \
            _nm_log(_level,                                                                 \
                    (_NMLOG_DOMAIN),                                                        \
                    0,                                                                      \
                    NULL,                                                                   \
                    NULL,                                                                   \
                    "%s[" NM_HASH_OBFUSCATE_PTR_FMT "]: request[" NM_HASH_OBFUSCATE_PTR_FMT \
                    "]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                               \
                    _NMLOG_PREFIX_NAME,                                                     \
                    NM_HASH_OBFUSCATE_PTR(self),                                            \
                    NM_HASH_OBFUSCATE_PTR(_handle) _NM_UTILS_MACRO_REST(__VA_ARGS__));      \
        }                                                                                   \
    }                                                                                       \
    G_STMT_END

/*****************************************************************************/

static void _resolve_complete_error(NMDnsSystemdResolvedResolveHandle *handle, GError *error);

static void _resolve_start(NMDnsSystemdResolved *self, NMDnsSystemdResolvedResolveHandle *handle);

static void send_updates(NMDnsSystemdResolved *self);

/*****************************************************************************/

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_unref(RequestItem *request_item)
{
    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);
}

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) {
        .ref_count = 1,
        .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_g_ptr_array_unref(config->ip_data_list);
    nm_g_slice_free(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;
    const char                  *operation;
    int                          ifindex;
    gboolean                     reconfigure = FALSE;

    request_item = user_data;
    self         = request_item->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 (operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
            if (priv->has_set_link_default_route == NM_TERNARY_DEFAULT) {
                priv->has_set_link_default_route = NM_TERNARY_TRUE;
                _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API supported");
            }
        } else if (operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
            if (priv->has_set_link_dns_over_tls == NM_TERNARY_DEFAULT) {
                priv->has_set_link_dns_over_tls = NM_TERNARY_TRUE;
                _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API supported");
            }
        } else if (operation == DBUS_OP_SET_LINK_DNS_EX) {
            if (priv->has_set_link_dns_ex == NM_TERNARY_DEFAULT) {
                priv->has_set_link_dns_ex = NM_TERNARY_TRUE;
                _LOGD("systemd-resolved support for SetLinkDNSEx(): API supported");
            }
        }
        priv->send_updates_warn_ratelimited = FALSE;
        goto out_dec_pending;
    }

    if (nm_g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
        if (operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
            if (priv->has_set_link_default_route == NM_TERNARY_DEFAULT) {
                priv->has_set_link_default_route = NM_TERNARY_FALSE;
                _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API not supported");
            }
        } else if (operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
            if (priv->has_set_link_dns_over_tls == NM_TERNARY_DEFAULT) {
                priv->has_set_link_dns_over_tls = NM_TERNARY_FALSE;
                _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API not supported");
            }
        } else if (operation == DBUS_OP_SET_LINK_DNS_EX) {
            if (priv->has_set_link_dns_ex == NM_TERNARY_DEFAULT) {
                priv->has_set_link_dns_ex = NM_TERNARY_FALSE;
                _LOGD("systemd-resolved support for SetLinkDNSEx(): API not supported");

                _LOGW("systemd-resolved does not support SetLinkDNSEx API (v246). "
                      "Cannot set DoT server name (SNI)");

                /* We need to reconfigure with the SetLinkDNS fallback.
                 *
                 * In the other cases above, there is no need to reconfigure anything.
                 * We won't retry SetLinkDefaultRoute/SetLinkDNSOverTLS anymore, but there
                 * is nothing else we can do about that. */
                reconfigure = TRUE;
            }
        }
        goto out_dec_pending;
    }

    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", 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);
    }

    if (reconfigure) {
        priv->send_updates_waiting = TRUE;
        send_updates(self);
    }
}

static gboolean
update_add_ip_config(NMDnsSystemdResolved    *self,
                     const NMDnsConfigIPData *ip_data,
                     GVariantBuilder         *dns,
                     GVariantBuilder         *dns_ex,
                     GVariantBuilder         *domains,
                     gboolean                *out_require_dns_ex)
{
    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
    gsize                        addr_size;
    guint                        n;
    guint                        i;
    gboolean                     is_routing;
    const char                  *domain;
    gboolean                     has_config = FALSE;
    const char *const           *strarr;

    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) {
        /* 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;
    }

    strarr = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &n);
    for (i = 0; i < n; i++) {
        NMDnsServer dns_server;

        if (!nm_dns_uri_parse(ip_data->addr_family, strarr[i], &dns_server, NULL))
            continue;

        if (!NM_IN_SET(dns_server.scheme,
                       NM_DNS_URI_SCHEME_TLS,
                       NM_DNS_URI_SCHEME_NONE,
                       NM_DNS_URI_SCHEME_UDP)) {
            /* In systemd-resolved, the use of DNS-over-TLS can't be controlled
             * for each name server; it is controlled via a per-link knob.
             * Therefore, we pass all the addresses we know about and then let
             * systemd-resolved decide whether to use DoT, based on the
             * "connection.dns-over-tls" property. */
            continue;
        }

        if (dns_server.servername) {
            NM_SET_OUT(out_require_dns_ex, TRUE);
            if (priv->has_set_link_dns_ex == FALSE) {
                /* The caller won't care about this result anymore. We can skip setting it. */
                dns = NULL;
            }
        }

        if (dns_ex) {
            g_variant_builder_open(dns_ex, G_VARIANT_TYPE("(iayqs)"));
            g_variant_builder_add(dns_ex, "i", ip_data->addr_family);
            g_variant_builder_add_value(
                dns_ex,
                nm_g_variant_new_ay((gconstpointer) &dns_server.addr, addr_size));
            g_variant_builder_add(dns_ex, "q", 0);
            g_variant_builder_add(dns_ex, "s", dns_server.servername ?: "");
            g_variant_builder_close(dns_ex);
        }
        if (dns) {
            g_variant_builder_open(dns, G_VARIANT_TYPE("(iay)"));
            g_variant_builder_add(dns, "i", ip_data->addr_family);
            g_variant_builder_add_value(
                dns,
                nm_g_variant_new_ay((gconstpointer) &dns_server.addr, addr_size));
            g_variant_builder_close(dns);
        }
        has_config = TRUE;
    }

    if (!has_config || domains) {
        if (!ip_data->domains.has_default_route_explicit
            && ip_data->domains.has_default_route_exclusive) {
            g_variant_builder_add(domains, "(sb)", ".", TRUE);
            has_config = TRUE;
        }
        if (ip_data->domains.search) {
            for (i = 0; ip_data->domains.search[i]; i++) {
                domain = nm_utils_parse_dns_domain(ip_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))) {
        c_list_unlink(&request_item->request_queue_lst);
        _request_item_unref(request_item);
    }
}

static gboolean
prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
{
    NMDnsSystemdResolvedPrivate  *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
    GVariantBuilder               dns;
    GVariantBuilder               dns_ex;
    GVariantBuilder               domains;
    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;
    NMSettingConnectionDnssec     dnssec            = NM_SETTING_CONNECTION_DNSSEC_DEFAULT;
    const char                   *mdns_arg          = NULL;
    const char                   *llmnr_arg         = NULL;
    const char                   *dns_over_tls_arg  = NULL;
    const char                   *dnssec_arg        = NULL;
    gboolean                      has_config        = FALSE;
    gboolean                      has_default_route = FALSE;
    guint                         i;
    gboolean                      require_dns_ex = FALSE;

    nm_assert(ic->ifindex != NM_LOOPBACK_IFINDEX);

    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)"));

    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];

            if (update_add_ip_config(self, ip_data, &dns, NULL, &domains, &require_dns_ex))
                has_config = 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));
                dnssec = NM_MAX(dnssec, nm_l3_config_data_get_dnssec(ip_data->l3cd));
            }
        }
    }

    g_variant_builder_close(&dns);
    g_variant_builder_close(&domains);

    if (!require_dns_ex) {
        /* No need to use the new API. SetLinkDNS() is sufficient. */
    } else if (!priv->has_set_link_dns_ex) {
        /* API to set server name is not supported. Nothing we can do. */
        require_dns_ex = FALSE;
    } else {
        g_variant_builder_init(&dns_ex, G_VARIANT_TYPE("(ia(iayqs))"));
        g_variant_builder_add(&dns_ex, "i", ic->ifindex);
        g_variant_builder_open(&dns_ex, G_VARIANT_TYPE("a(iayqs)"));
        for (i = 0; i < ic->ip_data_list->len; i++) {
            const NMDnsConfigIPData *ip_data = ic->ip_data_list->pdata[i];

            update_add_ip_config(self, ip_data, NULL, &dns_ex, NULL, NULL);
        }
        g_variant_builder_close(&dns_ex);
    }

    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);

    switch (dns_over_tls) {
    case NM_SETTING_CONNECTION_DNS_OVER_TLS_NO:
        dns_over_tls_arg = "no";
        break;
    case NM_SETTING_CONNECTION_DNS_OVER_TLS_OPPORTUNISTIC:
        dns_over_tls_arg = "opportunistic";
        break;
    case NM_SETTING_CONNECTION_DNS_OVER_TLS_YES:
        dns_over_tls_arg = "yes";
        break;
    case NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT:
        dns_over_tls_arg = "";
        break;
    }
    nm_assert(dns_over_tls_arg);

    switch (dnssec) {
    case NM_SETTING_CONNECTION_DNSSEC_NO:
        dnssec_arg = "no";
        break;
    case NM_SETTING_CONNECTION_DNSSEC_ALLOW_DOWNGRADE:
        dnssec_arg = "allow-downgrade";
        break;
    case NM_SETTING_CONNECTION_DNSSEC_YES:
        dnssec_arg = "yes";
        break;
    case NM_SETTING_CONNECTION_DNSSEC_DEFAULT:
        dnssec_arg = "";
        break;
    }
    nm_assert(dnssec_arg);

    if (!nm_str_is_empty(mdns_arg) || !nm_str_is_empty(llmnr_arg)
        || !nm_str_is_empty(dns_over_tls_arg) || !nm_str_is_empty(dnssec_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 ?: ""));
    if (require_dns_ex) {
        _request_item_append(self,
                             DBUS_OP_SET_LINK_DNS_EX,
                             ic->ifindex,
                             g_variant_builder_end(&dns_ex));
        g_variant_builder_clear(&dns);
    } else
        _request_item_append(self, "SetLinkDNS", ic->ifindex, g_variant_builder_end(&dns));
    _request_item_append(self,
                         DBUS_OP_SET_LINK_DNS_OVER_TLS,
                         ic->ifindex,
                         g_variant_new("(is)", ic->ifindex, dns_over_tls_arg ?: ""));
    _request_item_append(self,
                         DBUS_OP_SET_LINK_DNSSEC,
                         ic->ifindex,
                         g_variant_new("(is)", ic->ifindex, dnssec_arg ?: ""));

    return has_config;
}

static void
start_resolved_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    gs_unref_variant GVariant         *res   = NULL;
    gs_free_error GError              *error = NULL;
    NMDnsSystemdResolved              *self;
    NMDnsSystemdResolvedPrivate       *priv;
    NMDnsSystemdResolvedResolveHandle *handle;

    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);

    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 *local = NULL;

            if (handle->is_failing_on_idle)
                continue;

            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");

    _update_pending_maybe_changed(self);
}

static NMTernary
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;

    if (!priv->dbus_owner) {
        if (priv->try_start_blocked) {
            /* we have no name owner and we already tried poking the service to
             * autostart. */
            return NM_TERNARY_FALSE;
        }

        _LOGT("try D-Bus activating systemd-resolved...");
        priv->try_start_blocked = TRUE;
        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,
                                                      4000,
                                                      priv->service_start_cancellable,
                                                      start_resolved_cb,
                                                      self);
        _update_pending_maybe_changed(self);
        return NM_TERNARY_DEFAULT;
    }

    return NM_TERNARY_TRUE;
}

static void
send_updates(NMDnsSystemdResolved *self)
{
    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
    RequestItem                       *request_item;
    NMDnsSystemdResolvedResolveHandle *handle;

    if (!priv->send_updates_waiting) {
        /* nothing to do. */
        return;
    }

    if (ensure_resolved_running(self) != NM_TERNARY_TRUE)
        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->send_updates_waiting = FALSE;
        goto start_resolve;
    }

    priv->cancellable = g_cancellable_new();

    priv->send_updates_waiting = FALSE;

    _LOGT("send-updates: start %zu requests", c_list_length(&priv->request_queue_lst_head));

    c_list_for_each_entry (request_item, &priv->request_queue_lst_head, request_queue_lst) {
        gs_free char *ss = NULL;

        if ((request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
             && priv->has_set_link_default_route == NM_TERNARY_FALSE)
            || (request_item->operation == DBUS_OP_SET_LINK_DNS_OVER_TLS
                && priv->has_set_link_dns_over_tls == NM_TERNARY_FALSE)) {
            /* The "SetLinkDefaultRoute" API is only supported since v240.
             * The "SetLinkDNSOverTLS" API is only supported since v239.
             * We detected whether they are supported, and skip the calls. There
             * is no special workaround, because in this case we rely on systemd-resolved
             * to do the right thing automatically. */
            continue;
        }

        _LOGT("send-updates: %s ( %s )",
              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,
                               SYSTEMD_RESOLVED_MANAGER_IFACE,
                               request_item->operation,
                               request_item->argument,
                               NULL,
                               G_DBUS_CALL_FLAGS_NONE,
                               -1,
                               priv->cancellable,
                               call_done,
                               _request_item_ref(request_item));
    }

start_resolve:
    c_list_for_each_entry (handle, &priv->handle_lst_head, handle_lst) {
        if (handle->handle_cancellable)
            continue;
        if (handle->is_failing_on_idle)
            continue;
        _resolve_start(self, handle);
    }
}

static gboolean
update(NMDnsPlugin             *plugin,
       const NMGlobalDnsConfig *global_config,
       const CList             *ip_data_lst_head,
       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;
    const NMUtilsNamedValue       *interfaces_arr;
    NMUtilsNamedValue              interfaces_arr_stack[50];
    gs_free NMUtilsNamedValue     *interfaces_arr_heap = NULL;
    guint                          interfaces_len;
    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;
        int              ifindex = ip_data->data->ifindex;

        nm_assert(ifindex == nm_l3_config_data_get_ifindex(ip_data->l3cd));

        if (ifindex == NM_LOOPBACK_IFINDEX) {
            /* systemd-resolved API is per-link, and loopback is not supported.
             * Unclear what to do about DNS configuration on loopback. Just skip
             * it here. */
            continue;
        }

        ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(ifindex));
        if (!ic) {
            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);
        }

        g_ptr_array_add(ic->ip_data_list, ip_data);
    }

    free_pending_updates(self);

    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++) {
        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));
        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, &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 = nm_g_array_index(dirty_array, int, i);
            InterfaceConfig ic;

            _LOGT("clear previously configured ifindex %d", ifindex);
            ic = (InterfaceConfig) {
                .ifindex      = ifindex,
                .ip_data_list = NULL,
            };
            prepare_one_interface(self, &ic);
        }
    }

    priv->send_updates_waiting = TRUE;
    send_updates(self);
    _update_pending_maybe_changed(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);

    nm_clear_g_cancellable(&priv->service_start_cancellable);
    nm_strdup_reset(&priv->dbus_owner, owner);

    if (owner) {
        priv->try_start_blocked    = FALSE;
        priv->send_updates_waiting = TRUE;
    } else {
        priv->has_set_link_default_route = NM_TERNARY_DEFAULT;
        priv->has_set_link_dns_over_tls  = NM_TERNARY_DEFAULT;
        priv->has_set_link_dns_ex        = NM_TERNARY_DEFAULT;
    }

    send_updates(self);
    _update_pending_maybe_changed(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);
        _LOGT("D-Bus connection is ready");
    }

    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;
    _LOGT("D-Bus connection is ready");

    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_owner || !priv->try_start_blocked);
}

/*****************************************************************************/

static void
_resolve_complete(NMDnsSystemdResolvedResolveHandle       *handle,
                  const NMDnsSystemdResolvedAddressResult *names,
                  guint                                    names_len,
                  guint64                                  flags,
                  GError                                  *error)
{
    NMDnsSystemdResolved        *self;
    NMDnsSystemdResolvedPrivate *priv;

    g_return_if_fail(handle && NM_IS_DNS_SYSTEMD_RESOLVED(handle->self));

    self = handle->self;
    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);

    nm_assert(c_list_contains(&priv->handle_lst_head, &handle->handle_lst));

    c_list_unlink(&handle->handle_lst);

    handle->self = NULL;

    nm_clear_g_source_inst(&handle->timeout_source);
    nm_clear_g_cancellable(&handle->handle_cancellable);

    handle->r_address
        .callback(self, handle, names, names_len, flags, error, handle->callback_user_data);

    nm_g_slice_free(handle);
}

static void
_resolve_complete_error(NMDnsSystemdResolvedResolveHandle *handle, GError *error)
{
    NMDnsSystemdResolved *self = handle->self;

    nm_assert(error);
    _LOG2T(handle, "request failed: %s", error->message);
    _resolve_complete(handle, NULL, 0, 0, error);
}

static void
_resolve_handle_call_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    gs_unref_variant GVariant         *v     = NULL;
    gs_free_error GError              *error = NULL;
    NMDnsSystemdResolvedResolveHandle *handle;
    NMDnsSystemdResolved              *self;
    GVariantIter                      *v_names_iter;
    guint64                            v_flags;
    int                                v_ifindex;
    char                              *v_name;
    gs_unref_array GArray             *v_names = NULL;
    gs_free char                      *ss      = NULL;

    v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    handle = user_data;
    self   = handle->self;

    if (error) {
        gs_free char *remote_error = NULL;

        remote_error = g_dbus_error_get_remote_error(error);
        if (nm_streq0(remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) {
            _LOG2T(handle, "request failed due to service stop. Retry");
            g_clear_object(&handle->handle_cancellable);
            _resolve_start(self, handle);
            return;
        }

        _resolve_complete_error(handle, error);
        return;
    }

    _LOG2T(handle, "request completed: %s", (ss = g_variant_print(v, FALSE)));

    v_names = g_array_new(FALSE, FALSE, sizeof(NMDnsSystemdResolvedAddressResult));

    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMDnsSystemdResolvedAddressResult, name) == 0);
    g_array_set_clear_func(v_names, nm_indirect_g_free);

    g_variant_get(v, "(a(is)t)", &v_names_iter, &v_flags);

    while (g_variant_iter_next(v_names_iter, "(is)", &v_ifindex, &v_name)) {
        NMDnsSystemdResolvedAddressResult *n;

        n  = nm_g_array_append_new(v_names, NMDnsSystemdResolvedAddressResult);
        *n = (NMDnsSystemdResolvedAddressResult) {
            .name    = g_steal_pointer(&v_name),
            .ifindex = v_ifindex,
        };
    }
    g_variant_iter_free(v_names_iter);

    _resolve_complete(handle,
                      nm_g_array_first_p(v_names, NMDnsSystemdResolvedAddressResult),
                      v_names->len,
                      v_flags,
                      NULL);
}

static gboolean
_resolve_failing_on_idle(gpointer user_data)
{
    NMDnsSystemdResolvedResolveHandle *handle = user_data;
    gs_free_error GError              *error  = NULL;

    nm_utils_error_set_literal(&error,
                               NM_UTILS_ERROR_NOT_READY,
                               "systemd-resolved is not available");
    _resolve_complete_error(handle, error);
    return G_SOURCE_CONTINUE;
}

static gboolean
_resolve_handle_timeout(gpointer user_data)
{
    NMDnsSystemdResolvedResolveHandle *handle = user_data;
    gs_free_error GError              *error  = NULL;

    nm_utils_error_set_literal(&error, NM_UTILS_ERROR_UNKNOWN, "timeout for request");
    _resolve_complete_error(handle, error);
    return G_SOURCE_CONTINUE;
}

static void
_resolve_start(NMDnsSystemdResolved *self, NMDnsSystemdResolvedResolveHandle *handle)
{
    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
    NMTernary                    is_running;

    nm_assert(!handle->handle_cancellable);
    nm_assert(!handle->is_failing_on_idle);

    is_running = ensure_resolved_running(self);

    if (is_running == NM_TERNARY_FALSE) {
        /* Systemd-resolved is not is_running and shall not be used. We fail
         * on an idle handler. */
        _LOG2T(handle, "systemd-resolved not running. Failing on idle...");
        nm_assert(!handle->timeout_source);
        handle->is_failing_on_idle = TRUE;
        handle->timeout_source     = nm_g_idle_add_source(_resolve_failing_on_idle, handle);
        return;
    }

    if (!handle->timeout_source) {
        handle->timeout_source =
            nm_g_timeout_add_source(handle->timeout_msec, _resolve_handle_timeout, handle);
    }

    if (is_running == NM_TERNARY_DEFAULT) {
        /* we are D-Bus activating systemd-resolved. Wait for it... */
        _LOG2T(handle, "waiting for systemd-resolved to start...");
        return;
    }

    nm_assert(!priv->send_updates_waiting);

    handle->handle_cancellable = g_cancellable_new();

    _LOG2T(handle, "start D-Bus request...");
    g_dbus_connection_call(priv->dbus_connection,
                           priv->dbus_owner,
                           SYSTEMD_RESOLVED_DBUS_PATH,
                           SYSTEMD_RESOLVED_MANAGER_IFACE,
                           "ResolveAddress",
                           g_variant_new("(ii@ayt)",
                                         handle->r_address.ifindex,
                                         handle->r_address.addr_family,
                                         nm_g_variant_new_ay_inaddr(handle->r_address.addr_family,
                                                                    &handle->r_address.addr),
                                         handle->r_address.flags),
                           G_VARIANT_TYPE("(a(is)t)"),
                           G_DBUS_CALL_FLAGS_NONE,
                           handle->timeout_msec + 1000u,
                           handle->handle_cancellable,
                           _resolve_handle_call_cb,
                           handle);
}

NMDnsSystemdResolvedResolveHandle *
nm_dns_systemd_resolved_resolve_address(NMDnsSystemdResolved                      *self,
                                        int                                        ifindex,
                                        int                                        addr_family,
                                        const NMIPAddr                            *addr,
                                        guint64                                    flags,
                                        guint                                      timeout_msec,
                                        NMDnsSystemdResolvedResolveAddressCallback callback,
                                        gpointer                                   user_data)
{
    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
    NMDnsSystemdResolvedResolveHandle *handle;
    char                               addr_str[NM_INET_ADDRSTRLEN];

    g_return_val_if_fail(NM_IS_DNS_SYSTEMD_RESOLVED(self), NULL);
    nm_assert_addr_family(addr_family);
    nm_assert(addr);
    nm_assert(callback);

    handle  = g_slice_new(NMDnsSystemdResolvedResolveHandle);
    *handle = (NMDnsSystemdResolvedResolveHandle) {
        .self               = self,
        .timeout_msec       = timeout_msec,
        .callback_user_data = user_data,
        .r_address =
            {
                .ifindex     = ifindex,
                .addr_family = addr_family,
                .addr        = *addr,
                .flags       = flags,
                .callback    = callback,
            },
    };
    c_list_link_tail(&priv->handle_lst_head, &handle->handle_lst);

    _LOG2T(handle,
           "resolve-address(ifindex=%d, %s, flags=%" G_GINT64_MODIFIER "x): new request",
           handle->r_address.ifindex,
           nm_inet_ntop(handle->r_address.addr_family, &handle->r_address.addr, addr_str),
           handle->r_address.flags);

    _resolve_start(self, handle);

    return handle;
}

void
nm_dns_systemd_resolved_resolve_cancel(NMDnsSystemdResolvedResolveHandle *handle)
{
    gs_free_error GError *error = NULL;

    nm_utils_error_set_cancelled(&error, FALSE, "NMDnsSystemdResolved");
    _resolve_complete_error(handle, error);
}

/*****************************************************************************/

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);

    priv->has_set_link_default_route = NM_TERNARY_DEFAULT;
    priv->has_set_link_dns_over_tls  = NM_TERNARY_DEFAULT;
    priv->has_set_link_dns_ex        = NM_TERNARY_DEFAULT;

    c_list_init(&priv->request_queue_lst_head);
    c_list_init(&priv->handle_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->update_pending = TRUE;

    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);

    stop(NM_DNS_PLUGIN(self));

    g_clear_object(&priv->dbus_connection);
    nm_clear_pointer(&priv->dirty_interfaces, g_hash_table_destroy);

    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->stop               = stop;
    plugin_class->update             = update;
    plugin_class->get_update_pending = get_update_pending;
}
