/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2024 Red Hat, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-dns-dnsconfd.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 "NetworkManagerUtils.h"
#include "nm-l3-config-data.h"
#include "nm-manager.h"
#include "devices/nm-device.h"
#include "nm-active-connection.h"
#include "nm-l3cfg.h"

typedef enum {
    DNSCONFD_PLUGIN_IDLE             = 0,
    DNSCONFD_PLUGIN_WAIT_CONNECT     = 1,
    DNSCONFD_PLUGIN_WAIT_UPDATE_DONE = 2,
    DNSCONFD_PLUGIN_WAIT_SERIAL      = 3
} DnsconfdPluginState;

typedef struct {
    GDBusConnection *dbus_connection;
    GCancellable    *update_cancellable;
    char            *name_owner;
    guint            name_owner_changed_id;
    GCancellable    *name_owner_cancellable;
    bool             was_start_tried;
    GCancellable    *start_cancellable;
    GVariant        *latest_update_args;

    guint         awaited_configuration_serial;
    guint         present_configuration_serial;
    guint         properties_changed_id;
    GCancellable *serial_cancellable;

    DnsconfdPluginState plugin_state;
} NMDnsDnsconfdPrivate;

struct _NMDnsDnsconfd {
    NMDnsPlugin          parent;
    NMDnsDnsconfdPrivate _priv;
};

struct _NMDnsDnsconfdClass {
    NMDnsPluginClass parent;
};

G_DEFINE_TYPE(NMDnsDnsconfd, nm_dns_dnsconfd, NM_TYPE_DNS_PLUGIN)

#define NM_DNS_DNSCONFD_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMDnsDnsconfd, NM_IS_DNS_DNSCONFD, NMDnsPlugin)

#define _NMLOG_DOMAIN      LOGD_DNS
#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "dnsconfd", __VA_ARGS__)

#define DNSCONFD_DBUS_SERVICE "com.redhat.dnsconfd"

typedef enum {
    CONNECTION_FAIL,
    CONNECTION_SUCCESS,
    CONNECTION_WAIT,
} ConnectionState;

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

static void
dnsconfd_change_plugin_state(NMDnsDnsconfd *self, DnsconfdPluginState new_state)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    priv->plugin_state = new_state;
    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
}

static void
dnsconfd_serial_changed(NMDnsDnsconfd *self, guint new_serial)
{
    NMDnsDnsconfdPrivate *priv         = NM_DNS_DNSCONFD_GET_PRIVATE(self);
    priv->present_configuration_serial = new_serial;
    if (priv->plugin_state == DNSCONFD_PLUGIN_WAIT_SERIAL
        && priv->awaited_configuration_serial == new_serial) {
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_IDLE);
        /* Update finished, serials match */
        _LOGT("serials match, update finished");
    }
}

static void
dnsconfd_properties_changed(GDBusConnection *connection,
                            const char      *sender_name,
                            const char      *object_path,
                            const char      *interface_name,
                            const char      *signal_name,
                            GVariant        *parameters,
                            gpointer         user_data)
{
    NMDnsDnsconfd             *self               = user_data;
    gs_unref_variant GVariant *updated_properties = NULL;
    guint                      new_serial;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)"))) {
        _LOGW("received properties changed signal but the type is wrong");
        return;
    }

    g_variant_get(parameters, "(&s@a{sv}as)", NULL, &updated_properties, NULL);

    if (!g_variant_lookup(updated_properties, "configuration_serial", "u", &new_serial)) {
        _LOGT("properties changed but they do not contain new serial");
        return;
    }
    _LOGT("properties changed and contain new serial %u", new_serial);

    dnsconfd_serial_changed(self, new_serial);
}

static void
dnsconfd_serial_retrieval_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMDnsDnsconfd             *self;
    NMDnsDnsconfdPrivate      *priv;
    gs_free_error GError      *error              = NULL;
    gs_unref_variant GVariant *response           = NULL;
    gs_unref_variant GVariant *new_serial_variant = NULL;
    guint                      new_serial;

    response = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = user_data;
    priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    if (!response) {
        _LOGW("dnsconfd serial retrieval failed: %s", error->message);
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_IDLE);
        return;
    }

    nm_clear_g_cancellable(&priv->serial_cancellable);

    g_variant_get(response, "(v)", &new_serial_variant);

    g_variant_get(new_serial_variant, "u", &new_serial);

    _LOGT("serial retrieval done %u", new_serial);

    dnsconfd_serial_changed(self, new_serial);
}

static gboolean
subscribe_serial(NMDnsDnsconfd *self)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    priv->properties_changed_id =
        g_dbus_connection_signal_subscribe(priv->dbus_connection,
                                           priv->name_owner,
                                           "org.freedesktop.DBus.Properties",
                                           "PropertiesChanged",
                                           "/com/redhat/dnsconfd",
                                           "com.redhat.dnsconfd.Manager",
                                           G_DBUS_SIGNAL_FLAGS_NONE,
                                           dnsconfd_properties_changed,
                                           self,
                                           NULL);
    if (!priv->properties_changed_id) {
        return FALSE;
    }

    nm_clear_g_cancellable(&priv->serial_cancellable);

    g_dbus_connection_call(
        priv->dbus_connection,
        priv->name_owner,
        "/com/redhat/dnsconfd",
        "org.freedesktop.DBus.Properties",
        "Get",
        g_variant_new("(ss)", "com.redhat.dnsconfd.Manager", "configuration_serial"),
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        priv->serial_cancellable,
        dnsconfd_serial_retrieval_done,
        self);
    return TRUE;
}

static void
dnsconfd_update_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMDnsDnsconfd             *self;
    NMDnsDnsconfdPrivate      *priv;
    gs_free_error GError      *error    = NULL;
    gs_unref_variant GVariant *response = NULL;
    guint                      awaited_serial;
    char                      *dnsconfd_message;

    response = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);

    if (nm_utils_error_is_cancelled(error))
        return;

    self = user_data;
    priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    nm_clear_g_cancellable(&priv->update_cancellable);

    if (!response) {
        _LOGW("dnsconfd update failed: %s", error->message);
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_IDLE);
        return;
    }

    /* By using &s we will get pointer to char data contained
     * in variant and thus no freing of dnsconfd_message is required */
    g_variant_get(response, "(u&s)", &awaited_serial, &dnsconfd_message);

    if (!awaited_serial) {
        _LOGW("dnsconfd refused update: %s", dnsconfd_message);
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_IDLE);
        return;
    }

    priv->awaited_configuration_serial = awaited_serial;
    _LOGT("dnsconfd accepted update, awaited serial is %u", awaited_serial);

    if (priv->awaited_configuration_serial == priv->present_configuration_serial) {
        /* Serials match, update finished */
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_IDLE);
        _LOGT("after update serials match");
    } else {
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_WAIT_SERIAL);
        _LOGT("after update serials don't match, waiting");
    }
}

static gboolean
is_default_interface_explicit(const CList *ip_data_lst_head)
{
    guint              n_domains;
    const char *const *strv_domains;
    NMDnsConfigIPData *ip_data;
    gboolean           is_routing;

    /* If there is "~." specified in any connection's active ipvX.search setting then default
     * interface is explicit */

    c_list_for_each_entry (ip_data, ip_data_lst_head, ip_data_lst) {
        strv_domains =
            nm_l3_config_data_get_searches(ip_data->l3cd, ip_data->addr_family, &n_domains);
        for (guint i = 0; i < n_domains; i++) {
            if (nm_streq(nm_utils_parse_dns_domain(strv_domains[i], &is_routing), ".")) {
                return TRUE;
            }
        }
    }
    /* AFAIK it should not be passible to pass "." through DHCP and thus we will check only
     * searches */
    return FALSE;
}

static void
gather_interface_domains(NMDnsConfigIPData *ip_data,
                         gboolean           is_default_explicit,
                         const char      ***routing_domains,
                         const char      ***search_domains)
{
    guint              n_domains;
    const char *const *strv_domains;
    gboolean           is_routing;
    const char        *cur_domain;
    GPtrArray         *routing_ptr_array = g_ptr_array_sized_new(5);
    GPtrArray         *search_ptr_array  = g_ptr_array_sized_new(5);

    /* Searches have higher priority than domains (dynamically retrieved) */
    strv_domains = nm_l3_config_data_get_searches(ip_data->l3cd, ip_data->addr_family, &n_domains);
    if (!n_domains) {
        strv_domains =
            nm_l3_config_data_get_domains(ip_data->l3cd, ip_data->addr_family, &n_domains);
    }

    for (int i = 0; i < n_domains; i++) {
        cur_domain = nm_utils_parse_dns_domain(strv_domains[i], &is_routing);
        g_ptr_array_add(routing_ptr_array, (char *) cur_domain);
        if (!is_routing) {
            g_ptr_array_add(search_ptr_array, (char *) cur_domain);
        }
    }

    /* If there has been specified search like "~." then we will not be adding "." and respect
     * users wishes */
    if (!is_default_explicit
        && nm_l3_config_data_get_best_default_route(ip_data->l3cd, ip_data->addr_family)) {
        g_ptr_array_add(routing_ptr_array, ".");
    }
    g_ptr_array_add(routing_ptr_array, NULL);
    g_ptr_array_add(search_ptr_array, NULL);

    /* When array would be empty we will simply return NULL */
    *routing_domains =
        (const char **) g_ptr_array_free(routing_ptr_array, (routing_ptr_array->len == 1));
    *search_domains =
        (const char **) g_ptr_array_free(search_ptr_array, (search_ptr_array->len == 1));
}

static void
get_networks(NMDnsConfigIPData *ip_data, char ***networks)
{
    NMDedupMultiIter ipconf_iter;
    const NMPObject *obj;
    char             s_address[INET6_ADDRSTRLEN];
    /* +4 because INET6_ADDRSTRLEN already contains byte for end of string and we need 4 bytes
     * to store max 3 characters of mask and slash (/128 for example) */
    char                     network_buffer[INET6_ADDRSTRLEN + 4];
    const NMPlatformIPRoute *route;
    GPtrArray               *ptr_array   = g_ptr_array_sized_new(5);
    int                      addr_family = ip_data->addr_family;
    guint                    IS_IPv4     = NM_IS_IPv4(addr_family);

    nm_l3_config_data_iter_obj_for_each (&ipconf_iter,
                                         ip_data->l3cd,
                                         &obj,
                                         NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)) {
        route = NMP_OBJECT_CAST_IP_ROUTE(obj);
        if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route)
            || route->table_coerced == NM_DNS_ROUTES_FWMARK_TABLE_PRIO) {
            continue;
        }
        nm_inet_ntop(addr_family, route->network_ptr, s_address);
        nm_sprintf_buf(network_buffer, "%s/%u", s_address, route->plen);
        g_ptr_array_add(ptr_array, g_strdup(network_buffer));
    }

    g_ptr_array_add(ptr_array, NULL);
    /* If array would be empty then return NULL */
    *networks = (char **) g_ptr_array_free(ptr_array, ptr_array->len == 1);
}

static void
server_builder_append_interface_info(GVariantBuilder *argument_builder,
                                     const char      *interface,
                                     char           **networks)
{
    if (interface) {
        g_variant_builder_add(argument_builder, "{sv}", "interface", g_variant_new("s", interface));
    }
    if (networks) {
        g_variant_builder_add(argument_builder,
                              "{sv}",
                              "networks",
                              g_variant_new_strv((const char *const *) networks, -1));
    }
    g_variant_builder_close(argument_builder);
}

static gboolean
server_builder_append_base(GVariantBuilder   *argument_builder,
                           int                address_family,
                           const char        *address_string,
                           const char *const *routing_domains,
                           const char *const *search_domains,
                           const char        *ca,
                           int                priority)
{
    NMDnsServer dns_server;
    gsize       addr_size;

    if (!nm_dns_uri_parse(address_family, address_string, &dns_server, NULL))
        return FALSE;
    addr_size = nm_utils_addr_family_to_size(dns_server.addr_family);

    g_variant_builder_open(argument_builder, G_VARIANT_TYPE("a{sv}"));

    /* No freeing needed in this section as builder takes ownership of all data */

    g_variant_builder_add(argument_builder,
                          "{sv}",
                          "address",
                          nm_g_variant_new_ay((gconstpointer) &dns_server.addr, addr_size));
    if (dns_server.scheme == NM_DNS_URI_SCHEME_TLS)
        g_variant_builder_add(argument_builder, "{sv}", "protocol", g_variant_new("s", "dns+tls"));
    if (dns_server.servername)
        g_variant_builder_add(argument_builder,
                              "{sv}",
                              "name",
                              g_variant_new("s", dns_server.servername));
    if (dns_server.port != NM_DNS_PORT_UNDEFINED) {
        g_variant_builder_add(argument_builder,
                              "{sv}",
                              "port",
                              g_variant_new("i", dns_server.port));
    }
    if (routing_domains) {
        g_variant_builder_add(argument_builder,
                              "{sv}",
                              "routing_domains",
                              g_variant_new_strv(routing_domains, -1));
    }
    if (search_domains) {
        g_variant_builder_add(argument_builder,
                              "{sv}",
                              "search_domains",
                              g_variant_new_strv(search_domains, -1));
    }
    if (ca) {
        g_variant_builder_add(argument_builder, "{sv}", "ca", g_variant_new("s", ca));
    }
    /* dnsconfd defines priority as bigger number equals bigger priority, while NM
     * uses the exact opposite, thus use -priority */
    g_variant_builder_add(argument_builder, "{sv}", "priority", g_variant_new("i", -priority));
    return TRUE;
}

static void
parse_global_config(const NMGlobalDnsConfig *global_config,
                    GVariantBuilder         *argument_builder,
                    guint                   *resolve_mode,
                    const char             **ca)
{
    NMGlobalDnsDomain *domain;
    const char *const *servers;
    const char        *name;
    const char        *routing_domains[2] = {0};
    const char *const *searches           = nm_global_dns_config_get_searches(global_config);
    guint              num_domains        = nm_global_dns_config_get_num_domains(global_config);
    /* CA can be specified only in global config, but if it is, then we must set it for
     * all servers the same, because we do not support multiple certification authorities
     * (backend limitation) */
    *ca           = nm_global_dns_config_get_certification_authority(global_config);
    *resolve_mode = nm_global_dns_config_get_resolve_mode(global_config);

    for (guint i = 0; i < num_domains; i++) {
        domain  = nm_global_dns_config_get_domain(global_config, i);
        servers = nm_global_dns_domain_get_servers(domain);
        if (!servers) {
            continue;
        }
        name               = nm_global_dns_domain_get_name(domain);
        routing_domains[0] = nm_streq(name, "*") ? "." : name;

        for (gsize j = 0; servers[j]; j++) {
            if (server_builder_append_base(argument_builder,
                                           AF_UNSPEC,
                                           servers[j],
                                           routing_domains,
                                           searches,
                                           *ca,
                                           NM_DNS_PRIORITY_DEFAULT_NORMAL)) {
                g_variant_builder_close(argument_builder);
            }
        }
    }
}

static void
send_dnsconfd_update(NMDnsDnsconfd *self)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    nm_clear_g_cancellable(&priv->update_cancellable);
    priv->update_cancellable = g_cancellable_new();

    g_dbus_connection_call(priv->dbus_connection,
                           priv->name_owner,
                           "/com/redhat/dnsconfd",
                           "com.redhat.dnsconfd.Manager",
                           "Update",
                           priv->latest_update_args,
                           NULL,
                           G_DBUS_CALL_FLAGS_NONE,
                           20000,
                           priv->update_cancellable,
                           dnsconfd_update_done,
                           self);
}

static void
name_owner_changed(NMDnsDnsconfd *self, const char *name_owner)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    name_owner = nm_str_not_empty(name_owner);

    if (nm_streq0(priv->name_owner, name_owner))
        return;

    g_free(priv->name_owner);
    priv->name_owner = g_strdup(name_owner);

    if (!name_owner) {
        _LOGD("D-Bus name for dnsconfd disappeared");
        if (priv->plugin_state == DNSCONFD_PLUGIN_WAIT_UPDATE_DONE
            || priv->plugin_state == DNSCONFD_PLUGIN_WAIT_SERIAL) {
            /* We were waiting for either serial or confirmation of update and name
             * disappeared, thus we need to retransmit */
            dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_WAIT_CONNECT);
        }
        return;
    }

    _LOGT("D-Bus name for dnsconfd got owner %s", name_owner);
    priv->was_start_tried = FALSE;

    if (!subscribe_serial(self)) {
        /* This means that in time between new name and subscribe serial call
         * we lost the name again thus wait again */
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_WAIT_CONNECT);
        _LOGT("subscription failed, waiting to connect");
    } else {
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_WAIT_UPDATE_DONE);
        _LOGT("sending update and waiting for its finish");
        send_dnsconfd_update(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)
{
    NMDnsDnsconfd *self = user_data;
    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);

    name_owner_changed(self, new_owner);
}

static void
get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
{
    if (nm_utils_error_is_cancelled(error))
        return;

    name_owner_changed(user_data, name_owner);
}

static void
dnsconfd_start_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMDnsDnsconfd             *self;
    NMDnsDnsconfdPrivate      *priv;
    gs_free_error GError      *error    = NULL;
    gs_unref_variant GVariant *response = NULL;

    response = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = user_data;
    priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);
    nm_clear_g_cancellable(&priv->start_cancellable);

    if (!res) {
        g_dbus_error_strip_remote_error(error);
        _LOGW("failed to start Dnsconfd %s", error->message);
    } else {
        _LOGT("successfully started Dnsconfd");
    }

    /* No update maybe changed or state change, as this is handled by the name owner callbacks
     * which should be triggered right after this */
}

static ConnectionState
ensure_all_connected(NMDnsDnsconfd *self)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    if (!priv->dbus_connection) {
        priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET);
        if (!priv->dbus_connection) {
            return CONNECTION_FAIL;
        }
    }

    if (priv->name_owner) {
        return CONNECTION_SUCCESS;
    }

    if (!priv->name_owner_changed_id) {
        priv->name_owner_changed_id =
            nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
                                                                   DNSCONFD_DBUS_SERVICE,
                                                                   name_owner_changed_cb,
                                                                   self,
                                                                   NULL);
    }

    if (!priv->name_owner_cancellable) {
        nm_clear_g_cancellable(&priv->name_owner_cancellable);
        priv->name_owner_cancellable = g_cancellable_new();

        nm_dbus_connection_call_get_name_owner(priv->dbus_connection,
                                               DNSCONFD_DBUS_SERVICE,
                                               -1,
                                               priv->name_owner_cancellable,
                                               get_name_owner_cb,
                                               self);
    }

    if (!priv->was_start_tried) {
        _LOGT("attempting to start Dnsconfd via DBus");
        priv->was_start_tried = TRUE;
        nm_clear_g_cancellable(&priv->start_cancellable);
        priv->start_cancellable = g_cancellable_new();
        nm_dbus_connection_call_start_service_by_name(priv->dbus_connection,
                                                      DNSCONFD_DBUS_SERVICE,
                                                      -1,
                                                      priv->start_cancellable,
                                                      dnsconfd_start_done,
                                                      self);
    }

    return CONNECTION_WAIT;
}

static void
parse_all_interface_config(GVariantBuilder *argument_builder,
                           const CList     *ip_data_lst_head,
                           const char      *ca)
{
    NMDnsConfigIPData *ip_data;
    const char *const *dns_server_strings;
    guint              nameserver_count;
    int                priority;
    const char        *ifname;
    gboolean           explicit_default = is_default_interface_explicit(ip_data_lst_head);

    c_list_for_each_entry (ip_data, ip_data_lst_head, ip_data_lst) {
        /* No need to free insides of routing and search domains, as they point to data
         * owned elsewhere on the other hand networks are created by us and thus we need to also
         * free the data */
        gs_free const char **routing_domains = NULL;
        gs_free const char **search_domains  = NULL;
        gs_strfreev char   **networks        = NULL;

        dns_server_strings = nm_l3_config_data_get_nameservers(ip_data->l3cd,
                                                               ip_data->addr_family,
                                                               &nameserver_count);
        if (!nameserver_count)
            continue;
        ifname = nm_platform_link_get_name(NM_PLATFORM_GET, ip_data->data->ifindex);

        gather_interface_domains(ip_data, explicit_default, &routing_domains, &search_domains);
        get_networks(ip_data, &networks);
        if (!nm_l3_config_data_get_dns_priority(ip_data->l3cd, ip_data->addr_family, &priority)) {
            priority = NM_DNS_PRIORITY_DEFAULT_NORMAL;
        }

        for (guint i = 0; i < nameserver_count; i++) {
            if (server_builder_append_base(argument_builder,
                                           ip_data->addr_family,
                                           dns_server_strings[i],
                                           routing_domains,
                                           search_domains,
                                           ca,
                                           priority)) {
                server_builder_append_interface_info(argument_builder, ifname, networks);
            }
        }
    }
}

static gboolean
update(NMDnsPlugin             *plugin,
       const NMGlobalDnsConfig *global_config,
       const CList             *ip_data_lst_head,
       const char              *hostdomain,
       GError                 **error)
{
    NMDnsDnsconfd        *self = NM_DNS_DNSCONFD(plugin);
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);
    GVariantBuilder       argument_builder;
    GVariant             *args;

    ConnectionState all_connected;
    const char     *ca           = NULL;
    guint           resolve_mode = 0;
    gs_free char   *debug_string = NULL;

    g_variant_builder_init(&argument_builder, G_VARIANT_TYPE("(aa{sv}u)"));
    g_variant_builder_open(&argument_builder, G_VARIANT_TYPE("aa{sv}"));

    if (global_config) {
        _LOGT("parsing global configuration");
        parse_global_config(global_config, &argument_builder, &resolve_mode, &ca);
    }
    _LOGT("parsing configuration of interfaces");
    parse_all_interface_config(&argument_builder, ip_data_lst_head, ca);

    g_variant_builder_close(&argument_builder);
    g_variant_builder_add(&argument_builder, "u", resolve_mode);

    args = g_variant_builder_end(&argument_builder);

    /* Knowing how the update looks will be immensely helpful during debugging */
    _LOGT("arguments variant is composed like: %s", (debug_string = g_variant_print(args, TRUE)));

    nm_clear_pointer(&priv->latest_update_args, g_variant_unref);
    priv->latest_update_args = g_variant_ref_sink(args);

    all_connected = ensure_all_connected(self);

    /* We need to consider only whether we are connected, because newer update call
     * overrides the old one */
    if (all_connected == CONNECTION_FAIL) {
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_IDLE);
        _LOGT("failed to connect");
    } else if (all_connected == CONNECTION_WAIT) {
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_WAIT_CONNECT);
        _LOGT("not connected, waiting to connect");
    } else {
        dnsconfd_change_plugin_state(self, DNSCONFD_PLUGIN_WAIT_UPDATE_DONE);
        _LOGT("connected, waiting for update to finish");
    }

    if (all_connected == CONNECTION_FAIL) {
        nm_utils_error_set(error,
                           NM_UTILS_ERROR_UNKNOWN,
                           "no D-Bus connection available to talk to dnsconfd");
        /* Not connected to dbus, can do nothing here */
        return FALSE;
    } else if (all_connected == CONNECTION_WAIT) {
        /* We do not have name owner yet, and have to wait */
        return TRUE;
    }

    send_dnsconfd_update(self);

    return TRUE;
}

static void
stop(NMDnsPlugin *plugin)
{
    NMDnsDnsconfd        *self = NM_DNS_DNSCONFD(plugin);
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    nm_clear_g_cancellable(&priv->update_cancellable);
    nm_clear_g_cancellable(&priv->name_owner_cancellable);
    nm_clear_g_cancellable(&priv->serial_cancellable);
    nm_clear_g_cancellable(&priv->start_cancellable);
    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->properties_changed_id);
}

static gboolean
_update_pending_detect(NMDnsDnsconfd *self)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);

    if (priv->plugin_state == DNSCONFD_PLUGIN_IDLE) {
        /* We are waiting for nothing */
        return FALSE;
    }

    /* Update in progress */
    return TRUE;
}

static gboolean
get_update_pending(NMDnsPlugin *plugin)
{
    NMDnsDnsconfd *self = NM_DNS_DNSCONFD(plugin);
    return _update_pending_detect(self);
}

static void
nm_dns_dnsconfd_init(NMDnsDnsconfd *self)
{}

NMDnsPlugin *
nm_dns_dnsconfd_new(void)
{
    return g_object_new(NM_TYPE_DNS_DNSCONFD, NULL);
}

static void
dispose(GObject *object)
{
    NMDnsDnsconfdPrivate *priv = NM_DNS_DNSCONFD_GET_PRIVATE(NM_DNS_DNSCONFD(object));

    _LOGT("disposing of Dnsconfd plugin");

    stop(NM_DNS_PLUGIN(object));
    if (priv->name_owner) {
        nm_clear_g_free(&priv->name_owner);
    }
    if (priv->latest_update_args) {
        nm_clear_pointer(&priv->latest_update_args, g_variant_unref);
    }

    G_OBJECT_CLASS(nm_dns_dnsconfd_parent_class)->dispose(object);

    g_clear_object(&priv->dbus_connection);
}

static void
nm_dns_dnsconfd_class_init(NMDnsDnsconfdClass *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        = "dnsconfd";
    plugin_class->is_caching         = TRUE;
    plugin_class->stop               = stop;
    plugin_class->update             = update;
    plugin_class->get_update_pending = get_update_pending;
}
