summary refs log tree commit diff
path: root/src/core/dns
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-02-12 13:46:50 +0100
committerMichael Biebl <biebl@debian.org>2025-02-12 13:46:50 +0100
commit8bdf070ff046f482f6eb5e2b15ebc216f5d1e3da (patch)
treef706478d189d54c6532e8863d4b0d5ff5575af60 /src/core/dns
parent818258cf34b83fbc754633295e1052d4752d7b15 (diff)
New upstream version 1.51.90 upstream/1.51.90
Diffstat (limited to 'src/core/dns')
-rw-r--r--src/core/dns/nm-dns-dnsconfd.c802
-rw-r--r--src/core/dns/nm-dns-dnsconfd.h29
-rw-r--r--src/core/dns/nm-dns-dnsmasq.c19
-rw-r--r--src/core/dns/nm-dns-manager.c43
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.c39
5 files changed, 891 insertions, 41 deletions
diff --git a/src/core/dns/nm-dns-dnsconfd.c b/src/core/dns/nm-dns-dnsconfd.c
new file mode 100644
index 00000000..b356c2f9
--- /dev/null
+++ b/src/core/dns/nm-dns-dnsconfd.c
@@ -0,0 +1,802 @@
+/* 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;
+    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_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) {
+        priv->plugin_state = DNSCONFD_PLUGIN_IDLE;
+        /* Update finished, serials match */
+        _LOGT("serials match, update finished");
+    }
+
+    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+}
+
+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);
+
+    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);
+
+    /* 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);
+        priv->plugin_state = DNSCONFD_PLUGIN_IDLE;
+        _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+        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 */
+        priv->plugin_state = DNSCONFD_PLUGIN_IDLE;
+        _LOGT("after update serials match");
+    } else {
+        priv->plugin_state = DNSCONFD_PLUGIN_WAIT_SERIAL;
+        _LOGT("after update serials don't match, waiting");
+    }
+
+    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+}
+
+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,
+                                     const char      *connection_id,
+                                     const char      *connection_uuid,
+                                     const char      *dbus_path)
+{
+    if (connection_id) {
+        g_variant_builder_add(argument_builder,
+                              "{sv}",
+                              "connection-id",
+                              g_variant_new("s", connection_id));
+    }
+    if (connection_uuid) {
+        g_variant_builder_add(argument_builder,
+                              "{sv}",
+                              "connection-uuid",
+                              g_variant_new("s", connection_uuid));
+    }
+    if (dbus_path) {
+        g_variant_builder_add(argument_builder,
+                              "{sv}",
+                              "connection-object",
+                              g_variant_new("s", dbus_path));
+    }
+    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)
+{
+    NMDnsServer dns_server;
+    gsize       addr_size;
+
+    if (!nm_dns_uri_parse(address_family, address_string, &dns_server))
+        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 (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));
+    }
+    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)) {
+                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 */
+            priv->plugin_state = DNSCONFD_PLUGIN_WAIT_CONNECT;
+            _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+        }
+        return;
+    }
+
+    _LOGT("D-Bus name for dnsconfd got owner %s", name_owner);
+
+    if (!subscribe_serial(self)) {
+        /* This means that in time between new name and subscribe serial call
+         * we lost the name again thus wait again */
+        priv->plugin_state = DNSCONFD_PLUGIN_WAIT_CONNECT;
+        _LOGT("subscription failed, waiting to connect");
+    } else {
+        priv->plugin_state = DNSCONFD_PLUGIN_WAIT_UPDATE_DONE;
+        _LOGT("sending update and waiting for its finish");
+        send_dnsconfd_update(self);
+    }
+
+    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(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 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);
+    }
+
+    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;
+    const char           *ifname;
+    NMDevice             *device;
+    NMActiveConnection   *active_connection;
+    NMSettingsConnection *settings_connection;
+    NMActRequest         *act_request;
+    const char           *connection_id;
+    const char           *connection_uuid;
+    const char           *dbus_path;
+    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);
+        device      = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, ip_data->data->ifindex);
+        act_request = nm_device_get_act_request(device);
+        active_connection = NM_ACTIVE_CONNECTION(act_request);
+
+        /* Presume that when we have server of this interface then the interface has to have
+         * an active connection */
+        nm_assert(active_connection);
+
+        settings_connection = nm_active_connection_get_settings_connection(active_connection);
+        connection_id       = nm_settings_connection_get_id(settings_connection);
+        connection_uuid     = nm_settings_connection_get_uuid(settings_connection);
+        dbus_path           = nm_dbus_object_get_path_still_exported(NM_DBUS_OBJECT(act_request));
+
+        /* dbus_path also should be set, because if we are parsing this connection then we
+         * expect it to be active and exported on dbus */
+        nm_assert(dbus_path && dbus_path[0] != 0);
+
+        gather_interface_domains(ip_data, explicit_default, &routing_domains, &search_domains);
+        get_networks(ip_data, &networks);
+
+        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)) {
+                server_builder_append_interface_info(argument_builder,
+                                                     ifname,
+                                                     networks,
+                                                     connection_id,
+                                                     connection_uuid,
+                                                     dbus_path);
+            }
+        }
+    }
+}
+
+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_SUCCESS) {
+        priv->plugin_state = DNSCONFD_PLUGIN_WAIT_CONNECT;
+        _LOGT("not connected, waiting to connect");
+    } else {
+        priv->plugin_state = 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);
+
+    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(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_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;
+}
diff --git a/src/core/dns/nm-dns-dnsconfd.h b/src/core/dns/nm-dns-dnsconfd.h
new file mode 100644
index 00000000..1f71b507
--- /dev/null
+++ b/src/core/dns/nm-dns-dnsconfd.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DNS_DNSCONFD_H__
+#define __NETWORKMANAGER_DNS_DNSCONFD_H__
+
+#include "nm-dns-plugin.h"
+#include "nm-dns-manager.h"
+
+#define NM_TYPE_DNS_DNSCONFD (nm_dns_dnsconfd_get_type())
+#define NM_DNS_DNSCONFD(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_DNSCONFD, NMDnsDnsconfd))
+#define NM_DNS_DNSCONFD_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNS_DNSCONFD, NMDnsDnsconfdClass))
+#define NM_IS_DNS_DNSCONFD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DNS_DNSCONFD))
+#define NM_IS_DNS_DNSCONFD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DNS_DNSCONFD))
+#define NM_DNS_DNSCONFD_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DNS_DNSCONFD, NMDnsDnsconfdClass))
+
+typedef struct _NMDnsDnsconfd      NMDnsDnsconfd;
+typedef struct _NMDnsDnsconfdClass NMDnsDnsconfdClass;
+
+GType nm_dns_dnsconfd_get_type(void);
+
+NMDnsPlugin *nm_dns_dnsconfd_new(void);
+
+#endif /* __NETWORKMANAGER_DNS_DNSCONFD_H__ */
diff --git a/src/core/dns/nm-dns-dnsmasq.c b/src/core/dns/nm-dns-dnsmasq.c
index 53e40f59..8e3c10c9 100644
--- a/src/core/dns/nm-dns-dnsmasq.c
+++ b/src/core/dns/nm-dns-dnsmasq.c
@@ -263,7 +263,7 @@ handle_kill:
           PIDFILE);
 
     gl_pid.kill_external_data  = g_slice_new(GlPidKillExternalData);
-    *gl_pid.kill_external_data = (GlPidKillExternalData){
+    *gl_pid.kill_external_data = (GlPidKillExternalData) {
         .shutdown_wait_handle = nm_shutdown_wait_obj_register_handle_full(
             g_strdup_printf("kill-external-dnsmasq-process-%" G_PID_FORMAT, pid),
             TRUE),
@@ -623,7 +623,7 @@ _gl_pid_spawn(const char           *dm_binary,
         nm_assert(notify);
         nm_assert(G_IS_CANCELLABLE(cancellable));
         gl_pid.spawn_data  = g_slice_new(GlPidSpawnAsyncData);
-        *gl_pid.spawn_data = (GlPidSpawnAsyncData){
+        *gl_pid.spawn_data = (GlPidSpawnAsyncData) {
             .dm_binary        = dm_binary,
             .notify           = notify,
             .notify_user_data = notify_user_data,
@@ -853,13 +853,16 @@ add_global_config(NMDnsDnsmasq            *self,
         const char *const *servers = nm_global_dns_domain_get_servers(domain);
         const char        *name    = nm_global_dns_domain_get_name(domain);
 
-        g_return_if_fail(name);
+        nm_assert(name);
 
         for (j = 0; servers && servers[j]; j++) {
-            if (!strcmp(name, "*"))
-                add_dnsmasq_nameserver(self, dnsmasq_servers, servers[j], NULL);
-            else
-                add_dnsmasq_nameserver(self, dnsmasq_servers, servers[j], name);
+            char str[NM_INET_ADDRSTRLEN];
+
+            /* TODO: support IPv6 link-local addresses with scope id */
+            if (!nm_dns_uri_parse_plain(AF_UNSPEC, servers[j], str, NULL))
+                continue;
+
+            add_dnsmasq_nameserver(self, dnsmasq_servers, str, nm_streq(name, "*") ? NULL : name);
         }
     }
 }
@@ -881,7 +884,7 @@ add_ip_config(NMDnsDnsmasq *self, GVariantBuilder *servers, const NMDnsConfigIPD
     for (i = 0; i < num; i++) {
         NMIPAddr a;
 
-        if (!nm_utils_dnsname_parse_assert(ip_data->addr_family, strarr[i], NULL, &a, NULL))
+        if (!nm_dns_uri_parse_plain(ip_data->addr_family, strarr[i], NULL, &a))
             continue;
 
         ip_addr_to_string(ip_data->addr_family, &a, iface, ip_addr_to_string_buf);
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index 8f87fec1..d47590dc 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -32,6 +32,7 @@
 #include "nm-config.h"
 #include "nm-dbus-object.h"
 #include "nm-dns-dnsmasq.h"
+#include "nm-dns-dnsconfd.h"
 #include "nm-dns-plugin.h"
 #include "nm-dns-systemd-resolved.h"
 #include "nm-ip-config.h"
@@ -401,7 +402,7 @@ _dns_config_ip_data_new(NMDnsConfigData      *data,
     nm_assert(ip_config_type != NM_DNS_IP_CONFIG_TYPE_REMOVED);
 
     ip_data  = g_slice_new(NMDnsConfigIPData);
-    *ip_data = (NMDnsConfigIPData){
+    *ip_data = (NMDnsConfigIPData) {
         .data           = data,
         .source_tag     = source_tag,
         .l3cd           = nm_l3_config_data_ref_and_seal(l3cd),
@@ -600,7 +601,7 @@ merge_one_l3cd(NMResolvConfData *rc, int addr_family, int ifindex, const NML3Con
     for (i = 0; i < num_nameservers; i++) {
         NMIPAddr a;
 
-        if (!nm_utils_dnsname_parse_assert(addr_family, strarr[i], NULL, &a, NULL))
+        if (!nm_dns_uri_parse_plain(addr_family, strarr[i], NULL, &a))
             continue;
 
         if (addr_family == AF_INET)
@@ -1291,8 +1292,15 @@ merge_global_dns_config(NMResolvConfData *rc, NMGlobalDnsConfig *global_conf)
     if (!servers)
         return TRUE;
 
-    for (i = 0; servers[i]; i++)
-        add_string_item(rc->nameservers, servers[i], TRUE);
+    for (i = 0; servers[i]; i++) {
+        char addrstr[NM_INET_ADDRSTRLEN];
+
+        /* TODO: support IPv6 link-local addresses with scope id */
+        if (!nm_dns_uri_parse_plain(AF_UNSPEC, servers[i], addrstr, NULL))
+            continue;
+
+        add_string_item(rc->nameservers, addrstr, TRUE);
+    }
 
     return TRUE;
 }
@@ -1300,7 +1308,6 @@ merge_global_dns_config(NMResolvConfData *rc, NMGlobalDnsConfig *global_conf)
 static const char *
 get_nameserver_list(int addr_family, const NML3ConfigData *l3cd, NMStrBuf *tmp_strbuf)
 {
-    char               buf[NM_INET_ADDRSTRLEN];
     guint              num;
     guint              i;
     const char *const *strarr;
@@ -1309,15 +1316,9 @@ get_nameserver_list(int addr_family, const NML3ConfigData *l3cd, NMStrBuf *tmp_s
 
     strarr = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num);
     for (i = 0; i < num; i++) {
-        NMIPAddr a;
-
-        if (!nm_utils_dnsname_parse_assert(addr_family, strarr[i], NULL, &a, NULL))
-            continue;
-
-        nm_inet_ntop(addr_family, &a, buf);
         if (i > 0)
             nm_str_buf_append_c(tmp_strbuf, ' ');
-        nm_str_buf_append(tmp_strbuf, buf);
+        nm_str_buf_append(tmp_strbuf, strarr[i]);
     }
 
     nm_str_buf_maybe_expand(tmp_strbuf, 1, FALSE);
@@ -2108,7 +2109,7 @@ nm_dns_manager_set_ip_config(NMDnsManager         *self,
 
     if (!data) {
         data  = g_slice_new(NMDnsConfigData);
-        *data = (NMDnsConfigData){
+        *data = (NMDnsConfigData) {
             .ifindex       = ifindex,
             .self          = self,
             .data_lst_head = C_LIST_INIT(data->data_lst_head),
@@ -2525,6 +2526,12 @@ again:
             priv->plugin   = nm_dns_dnsmasq_new();
             plugin_changed = TRUE;
         }
+    } else if (nm_streq0(mode, "dnsconfd")) {
+        if (force_reload_plugin || !NM_IS_DNS_DNSCONFD(priv->plugin)) {
+            _clear_plugin(self);
+            priv->plugin   = nm_dns_dnsconfd_new();
+            plugin_changed = TRUE;
+        }
     } else {
         if (!NM_IN_STRSET(mode, "none", "default")) {
             if (mode) {
@@ -2541,7 +2548,7 @@ again:
 
     if (rc_manager == NM_DNS_MANAGER_RESOLV_CONF_MAN_AUTO) {
         rc_manager_was_auto = TRUE;
-        if (nm_streq(mode, "systemd-resolved"))
+        if (nm_streq(mode, "systemd-resolved") || nm_streq(mode, "dnsconfd"))
             rc_manager = NM_DNS_MANAGER_RESOLV_CONF_MAN_UNMANAGED;
         else if (HAS_RESOLVCONF && g_file_test(RESOLVCONF_PATH, G_FILE_TEST_IS_EXECUTABLE)) {
             /* We detect /sbin/resolvconf only at this stage. That means, if you install
@@ -2730,7 +2737,6 @@ _get_config_variant(NMDnsManager *self)
         guint              num_domains;
         guint              num_searches;
         guint              i;
-        char               buf[NM_INET_ADDRSTRLEN];
         const char        *ifname;
         const char *const *strarr;
 
@@ -2742,12 +2748,7 @@ _get_config_variant(NMDnsManager *self)
 
         g_variant_builder_init(&strv_builder, G_VARIANT_TYPE("as"));
         for (i = 0; i < num; i++) {
-            NMIPAddr a;
-
-            if (!nm_utils_dnsname_parse_assert(ip_data->addr_family, strarr[i], NULL, &a, NULL))
-                continue;
-
-            g_variant_builder_add(&strv_builder, "s", nm_inet_ntop(ip_data->addr_family, &a, buf));
+            g_variant_builder_add(&strv_builder, "s", strarr[i]);
         }
         g_variant_builder_add(&entry_builder,
                               "{sv}",
diff --git a/src/core/dns/nm-dns-systemd-resolved.c b/src/core/dns/nm-dns-systemd-resolved.c
index 24c7c774..0701a1dc 100644
--- a/src/core/dns/nm-dns-systemd-resolved.c
+++ b/src/core/dns/nm-dns-systemd-resolved.c
@@ -248,7 +248,7 @@ _request_item_append(NMDnsSystemdResolved *self,
     RequestItem                 *request_item;
 
     request_item  = g_slice_new(RequestItem);
-    *request_item = (RequestItem){
+    *request_item = (RequestItem) {
         .ref_count = 1,
         .operation = operation,
         .argument  = g_variant_ref_sink(argument),
@@ -396,13 +396,24 @@ update_add_ip_config(NMDnsSystemdResolved    *self,
 
     strarr = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &n);
     for (i = 0; i < n; i++) {
-        const char *server_name;
-        NMIPAddr    a;
+        NMDnsServer dns_server;
 
-        if (!nm_utils_dnsname_parse_assert(ip_data->addr_family, strarr[i], NULL, &a, &server_name))
+        if (!nm_dns_uri_parse(ip_data->addr_family, strarr[i], &dns_server))
             continue;
 
-        if (server_name) {
+        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. */
@@ -413,15 +424,19 @@ update_add_ip_config(NMDnsSystemdResolved    *self,
         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) &a, addr_size));
+            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", server_name ?: "");
+            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) &a, addr_size));
+            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;
@@ -803,7 +818,7 @@ update(NMDnsPlugin             *plugin,
         ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(ifindex));
         if (!ic) {
             ic  = g_slice_new(InterfaceConfig);
-            *ic = (InterfaceConfig){
+            *ic = (InterfaceConfig) {
                 .ifindex      = ifindex,
                 .ip_data_list = g_ptr_array_sized_new(4),
             };
@@ -855,7 +870,7 @@ update(NMDnsPlugin             *plugin,
             InterfaceConfig ic;
 
             _LOGT("clear previously configured ifindex %d", ifindex);
-            ic = (InterfaceConfig){
+            ic = (InterfaceConfig) {
                 .ifindex      = ifindex,
                 .ip_data_list = NULL,
             };
@@ -1053,7 +1068,7 @@ _resolve_handle_call_cb(GObject *source, GAsyncResult *result, gpointer user_dat
         NMDnsSystemdResolvedAddressResult *n;
 
         n  = nm_g_array_append_new(v_names, NMDnsSystemdResolvedAddressResult);
-        *n = (NMDnsSystemdResolvedAddressResult){
+        *n = (NMDnsSystemdResolvedAddressResult) {
             .name    = g_steal_pointer(&v_name),
             .ifindex = v_ifindex,
         };
@@ -1167,7 +1182,7 @@ nm_dns_systemd_resolved_resolve_address(NMDnsSystemdResolved
     nm_assert(callback);
 
     handle  = g_slice_new(NMDnsSystemdResolvedResolveHandle);
-    *handle = (NMDnsSystemdResolvedResolveHandle){
+    *handle = (NMDnsSystemdResolvedResolveHandle) {
         .self               = self,
         .timeout_msec       = timeout_msec,
         .callback_user_data = user_data,