summary refs log tree commit diff
path: root/clients/cloud-setup
diff options
context:
space:
mode:
Diffstat (limited to 'clients/cloud-setup')
-rw-r--r--clients/cloud-setup/main.c4
-rw-r--r--clients/cloud-setup/meson.build52
-rw-r--r--clients/cloud-setup/nm-cloud-setup-utils.c87
-rw-r--r--clients/cloud-setup/nm-cloud-setup-utils.h32
-rw-r--r--clients/cloud-setup/nm-cloud-setup.service.in4
-rw-r--r--clients/cloud-setup/nm-http-client.c4
-rw-r--r--clients/cloud-setup/nm-http-client.h2
-rw-r--r--clients/cloud-setup/nmcs-provider-azure.c342
-rw-r--r--clients/cloud-setup/nmcs-provider-azure.h2
-rw-r--r--clients/cloud-setup/nmcs-provider-ec2.c265
-rw-r--r--clients/cloud-setup/nmcs-provider-ec2.h2
-rw-r--r--clients/cloud-setup/nmcs-provider-gcp.c278
-rw-r--r--clients/cloud-setup/nmcs-provider-gcp.h2
-rw-r--r--clients/cloud-setup/nmcs-provider.c74
-rw-r--r--clients/cloud-setup/nmcs-provider.h42
-rw-r--r--clients/cloud-setup/tests/meson.build20
-rw-r--r--clients/cloud-setup/tests/test-cloud-setup-general.c117
17 files changed, 794 insertions, 535 deletions
diff --git a/clients/cloud-setup/main.c b/clients/cloud-setup/main.c
index 1baf0feb..06590176 100644
--- a/clients/cloud-setup/main.c
+++ b/clients/cloud-setup/main.c
@@ -1,6 +1,6 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nm-libnm-aux/nm-libnm-aux.h"
 
diff --git a/clients/cloud-setup/meson.build b/clients/cloud-setup/meson.build
index c29d73df..581dde6a 100644
--- a/clients/cloud-setup/meson.build
+++ b/clients/cloud-setup/meson.build
@@ -1,6 +1,4 @@
-# SPDX-License-Identifier: LGPL-2.1+
-
-name = 'nm-cloud-setup'
+# SPDX-License-Identifier: LGPL-2.1-or-later
 
 if install_systemdunitdir
 
@@ -23,30 +21,54 @@ if install_systemdunitdir
 
 endif
 
-sources = files(
-  'main.c',
-  'nm-cloud-setup-utils.c',
-  'nm-http-client.c',
-  'nmcs-provider-ec2.c',
-  'nmcs-provider-gcp.c',
-  'nmcs-provider-azure.c',
-  'nmcs-provider.c',
+libnm_cloud_setup_core = static_library(
+  'nm-cloud-setup-core',
+  sources: files(
+    'nm-cloud-setup-utils.c',
+    'nm-http-client.c',
+    'nmcs-provider-ec2.c',
+    'nmcs-provider-gcp.c',
+    'nmcs-provider-azure.c',
+    'nmcs-provider.c',
+  ),
+  dependencies: [
+    libnmc_base_dep,
+    libnmc_dep,
+    libcurl_dep,
+    libnm_libnm_aux_dep,
+  ],
+  c_args: [
+    '-DG_LOG_DOMAIN="nm-cloud-setup"',
+  ],
+)
+
+libnm_cloud_setup_core_dep = declare_dependency(
+  include_directories: include_directories('.'),
+  link_with: libnm_cloud_setup_core,
 )
 
 executable(
-  name,
-  sources,
+  'nm-cloud-setup',
+  files(
+    'main.c',
+  ),
   dependencies: [
     libnmc_base_dep,
     libnmc_dep,
     libcurl_dep,
     libnm_libnm_aux_dep,
+    libnm_cloud_setup_core_dep,
+  ],
+  c_args: [
+    '-DG_LOG_DOMAIN="nm-cloud-setup"',
   ],
-  c_args: clients_c_flags +
-          ['-DG_LOG_DOMAIN="@0@"'.format(name)],
   link_with: libnm_systemd_logging_stub,
   link_args: ldflags_linker_script_binary,
   link_depends: linker_script_binary,
   install: true,
   install_dir: nm_libexecdir,
 )
+
+if enable_tests
+  subdir('tests')
+endif
diff --git a/clients/cloud-setup/nm-cloud-setup-utils.c b/clients/cloud-setup/nm-cloud-setup-utils.c
index 0820ed59..f5550a12 100644
--- a/clients/cloud-setup/nm-cloud-setup-utils.c
+++ b/clients/cloud-setup/nm-cloud-setup-utils.c
@@ -1,9 +1,12 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nm-cloud-setup-utils.h"
 
+#include <linux/if_ether.h>
+#include <linux/if_infiniband.h>
+
 #include "nm-glib-aux/nm-time-utils.h"
 #include "nm-glib-aux/nm-logging-base.h"
 #include "nm-glib-aux/nm-str-buf.h"
@@ -482,22 +485,39 @@ char *
 nmcs_utils_hwaddr_normalize(const char *hwaddr, gssize len)
 {
     gs_free char *hwaddr_clone = NULL;
+    char *        hw;
     guint8        buf[ETH_ALEN];
+    gsize         l;
 
     nm_assert(len >= -1);
 
     if (len < 0) {
         if (!hwaddr)
             return NULL;
+        l = strlen(hwaddr);
     } else {
-        if (len == 0)
+        l = len;
+        if (l > 0 && hwaddr[l - 1] == '\0') {
+            /* we accept one '\0' at the end of the string. */
+            l--;
+        }
+        if (memchr(hwaddr, '\0', l)) {
+            /* but we don't accept other NUL characters in the middle. */
             return NULL;
-        nm_assert(hwaddr);
-        hwaddr = nm_strndup_a(300, hwaddr, len, &hwaddr_clone);
+        }
     }
+
+    if (l == 0)
+        return NULL;
+
+    nm_assert(hwaddr);
+    hw = nm_strndup_a(300, hwaddr, l, &hwaddr_clone);
+
+    g_strstrip(hw);
+
     /* we cannot use _nm_utils_hwaddr_aton() because that requires a delimiter.
      * Azure exposes MAC addresses without delimiter, so accept that too. */
-    if (!nm_utils_hexstr2bin_full(hwaddr,
+    if (!nm_utils_hexstr2bin_full(hw,
                                   FALSE,
                                   FALSE,
                                   FALSE,
@@ -513,6 +533,59 @@ nmcs_utils_hwaddr_normalize(const char *hwaddr, gssize len)
 
 /*****************************************************************************/
 
+gboolean
+nmcs_utils_ipaddr_normalize_bin(int         addr_family,
+                                const char *addr,
+                                gssize      len,
+                                int *       out_addr_family,
+                                gpointer    out_addr_bin)
+{
+    gs_free char *addr_clone = NULL;
+    char *        ad;
+    gsize         l;
+
+    nm_assert(len >= -1);
+
+    if (len < 0) {
+        if (!addr)
+            return FALSE;
+        l = strlen(addr);
+    } else {
+        l = len;
+        if (l > 0 && addr[l - 1] == '\0') {
+            /* we accept one '\0' at the end of the string. */
+            l--;
+        }
+        if (memchr(addr, '\0', l)) {
+            /* but we don't accept other NUL characters in the middle. */
+            return FALSE;
+        }
+    }
+
+    if (l == 0)
+        return FALSE;
+
+    nm_assert(addr);
+    ad = nm_strndup_a(300, addr, l, &addr_clone);
+
+    g_strstrip(ad);
+
+    return nm_utils_parse_inaddr_bin(addr_family, ad, out_addr_family, out_addr_bin);
+}
+
+char *
+nmcs_utils_ipaddr_normalize(int addr_family, const char *addr, gssize len)
+{
+    NMIPAddr ipaddr;
+
+    if (!nmcs_utils_ipaddr_normalize_bin(addr_family, addr, len, &addr_family, &ipaddr))
+        return NULL;
+
+    return nm_utils_inet_ntop_dup(addr_family, &ipaddr);
+}
+
+/*****************************************************************************/
+
 const char *
 nmcs_utils_parse_memmem(GBytes *mem, const char *needle)
 {
@@ -631,7 +704,7 @@ nmcs_setting_ip_replace_ipv4_addresses(NMSettingIPConfig *s_ip,
 
         i_next++;
     }
-    if (any_changes) {
+    if (!any_changes) {
         while (i_next < num) {
             nm_setting_ip_config_remove_address(s_ip, --num);
             any_changes = TRUE;
diff --git a/clients/cloud-setup/nm-cloud-setup-utils.h b/clients/cloud-setup/nm-cloud-setup-utils.h
index 6980e752..52edd279 100644
--- a/clients/cloud-setup/nm-cloud-setup-utils.h
+++ b/clients/cloud-setup/nm-cloud-setup-utils.h
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #ifndef __NM_CLOUD_SETUP_UTILS_H__
 #define __NM_CLOUD_SETUP_UTILS_H__
@@ -78,6 +78,36 @@ gboolean nmcs_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data,
 
 char *nmcs_utils_hwaddr_normalize(const char *hwaddr, gssize len);
 
+static inline char *
+nmcs_utils_hwaddr_normalize_gbytes(GBytes *hwaddr)
+{
+    const char *str;
+    gsize       len;
+
+    str = g_bytes_get_data(hwaddr, &len);
+    return nmcs_utils_hwaddr_normalize(str, len);
+}
+
+/*****************************************************************************/
+
+gboolean nmcs_utils_ipaddr_normalize_bin(int         addr_family,
+                                         const char *addr,
+                                         gssize      len,
+                                         int *       out_addr_family,
+                                         gpointer    out_addr_bin);
+
+char *nmcs_utils_ipaddr_normalize(int addr_family, const char *addr, gssize len);
+
+static inline char *
+nmcs_utils_ipaddr_normalize_gbytes(int addr_family, GBytes *addr)
+{
+    const char *str;
+    gsize       len;
+
+    str = g_bytes_get_data(addr, &len);
+    return nmcs_utils_ipaddr_normalize(addr_family, str, len);
+}
+
 /*****************************************************************************/
 
 const char *nmcs_utils_parse_memmem(GBytes *mem, const char *needle);
diff --git a/clients/cloud-setup/nm-cloud-setup.service.in b/clients/cloud-setup/nm-cloud-setup.service.in
index 57f9e931..809f707d 100644
--- a/clients/cloud-setup/nm-cloud-setup.service.in
+++ b/clients/cloud-setup/nm-cloud-setup.service.in
@@ -1,5 +1,6 @@
 [Unit]
 Description=Automatically configure NetworkManager in cloud
+Documentation=man:nm-cloud-setup(8)
 After=NetworkManager.service
 
 [Service]
@@ -11,6 +12,9 @@ ExecStart=@libexecdir@/nm-cloud-setup
 # Cloud providers are disabled by default. You need to
 # Opt-in by setting the right environment variable for
 # the provider.
+#
+# Create a drop-in file to overwrite these variables or
+# use systemctl edit.
 #Environment=NM_CLOUD_SETUP_EC2=yes
 #Environment=NM_CLOUD_SETUP_GCP=yes
 #Environment=NM_CLOUD_SETUP_AZURE=yes
diff --git a/clients/cloud-setup/nm-http-client.c b/clients/cloud-setup/nm-http-client.c
index 221e8c71..144513fc 100644
--- a/clients/cloud-setup/nm-http-client.c
+++ b/clients/cloud-setup/nm-http-client.c
@@ -1,6 +1,6 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nm-http-client.h"
 
diff --git a/clients/cloud-setup/nm-http-client.h b/clients/cloud-setup/nm-http-client.h
index fc465e35..862b7338 100644
--- a/clients/cloud-setup/nm-http-client.h
+++ b/clients/cloud-setup/nm-http-client.h
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #ifndef __NM_HTTP_CLIENT_C__
 #define __NM_HTTP_CLIENT_C__
diff --git a/clients/cloud-setup/nmcs-provider-azure.c b/clients/cloud-setup/nmcs-provider-azure.c
index 1a80327c..0a5d522c 100644
--- a/clients/cloud-setup/nmcs-provider-azure.c
+++ b/clients/cloud-setup/nmcs-provider-azure.c
@@ -1,6 +1,6 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nmcs-provider-azure.h"
 
@@ -94,128 +94,97 @@ detect(NMCSProvider *provider, GTask *task)
 /*****************************************************************************/
 
 typedef struct {
-    NMCSProviderGetConfigTaskData *config_data;
-    guint                          n_ifaces_pending;
-    GError *                       error;
-} AzureData;
-
-typedef struct {
+    NMCSProviderGetConfigTaskData * get_config_data;
     NMCSProviderGetConfigIfaceData *iface_get_config;
-    AzureData *                     azure_data;
-    gssize                          iface_idx;
+    gssize                          intern_iface_idx;
+    gssize                          extern_iface_idx;
     guint                           n_ips_prefix_pending;
-    char *                          hwaddr;
+    const char *                    hwaddr;
 } AzureIfaceData;
 
 static void
-_azure_iface_data_free(AzureIfaceData *iface_data)
+_azure_iface_data_destroy(AzureIfaceData *iface_data)
 {
-    g_free(iface_data->hwaddr);
     nm_g_slice_free(iface_data);
 }
 
 static void
-_get_config_maybe_task_return(AzureData *azure_data, GError *error_take)
+_get_config_fetch_done_cb(NMHttpClient *  http_client,
+                          GAsyncResult *  result,
+                          AzureIfaceData *iface_data,
+                          gboolean        is_ipv4)
 {
-    NMCSProviderGetConfigTaskData *config_data = azure_data->config_data;
-
-    if (error_take) {
-        if (!azure_data->error)
-            azure_data->error = error_take;
-        else if (!nm_utils_error_is_cancelled(azure_data->error)
-                 && nm_utils_error_is_cancelled(error_take)) {
-            nm_clear_error(&azure_data->error);
-            azure_data->error = error_take;
-        } else
-            g_error_free(error_take);
-    }
+    NMCSProviderGetConfigTaskData * get_config_data;
+    NMCSProviderGetConfigIfaceData *iface_get_config;
+    gs_unref_bytes GBytes *response = NULL;
+    gs_free_error GError *error     = NULL;
+    const char *          fip_str   = NULL;
+    gsize                 fip_len;
+
+    nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error);
 
-    if (azure_data->n_ifaces_pending > 0)
+    if (nm_utils_error_is_cancelled(error))
         return;
 
-    if (azure_data->error) {
-        if (nm_utils_error_is_cancelled(azure_data->error))
-            _LOGD("get-config: cancelled");
-        else
-            _LOGD("get-config: failed: %s", azure_data->error->message);
-        g_task_return_error(config_data->task, g_steal_pointer(&azure_data->error));
-    } else {
-        _LOGD("get-config: success");
-        g_task_return_pointer(config_data->task,
-                              g_hash_table_ref(config_data->result_dict),
-                              (GDestroyNotify) g_hash_table_unref);
-    }
+    get_config_data = iface_data->get_config_data;
 
-    nm_g_slice_free(azure_data);
-    g_object_unref(config_data->task);
-}
+    if (error)
+        goto out_done;
 
-static void
-_get_config_fetch_done_cb(NMHttpClient *http_client,
-                          GAsyncResult *result,
-                          gpointer      user_data,
-                          gboolean      is_ipv4)
-{
-    NMCSProviderGetConfigIfaceData *iface_get_config;
-    gs_unref_bytes GBytes *response   = NULL;
-    AzureIfaceData *       iface_data = user_data;
-    gs_free_error GError *error       = NULL;
-    const char *          fip_str     = NULL;
-    AzureData *           azure_data;
+    fip_str = g_bytes_get_data(response, &fip_len);
+    nm_assert(fip_str[fip_len] == '\0');
 
-    azure_data = iface_data->azure_data;
+    iface_data->iface_get_config =
+        g_hash_table_lookup(get_config_data->result_dict, iface_data->hwaddr);
+    iface_get_config = iface_data->iface_get_config;
 
-    nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error);
+    if (is_ipv4) {
+        char      tmp_addr_str[NM_UTILS_INET_ADDRSTRLEN];
+        in_addr_t tmp_addr;
 
-    if (error)
-        goto done;
+        if (!nmcs_utils_ipaddr_normalize_bin(AF_INET, fip_str, fip_len, NULL, &tmp_addr)) {
+            error =
+                nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "ip is not a valid private ip address");
+            goto out_done;
+        }
+        _LOGD("interface[%" G_GSSIZE_FORMAT "]: adding private ip %s",
+              iface_data->intern_iface_idx,
+              _nm_utils_inet4_ntop(tmp_addr, tmp_addr_str));
+        iface_get_config->ipv4s_arr[iface_get_config->ipv4s_len] = tmp_addr;
+        iface_get_config->has_ipv4s                              = TRUE;
+        iface_get_config->ipv4s_len++;
+    } else {
+        int tmp_prefix = -1;
 
-    if (!error) {
-        in_addr_t tmp_addr;
-        int       tmp_prefix;
-
-        fip_str = g_bytes_get_data(response, NULL);
-        iface_data->iface_get_config =
-            g_hash_table_lookup(azure_data->config_data->result_dict, iface_data->hwaddr);
-        iface_get_config            = iface_data->iface_get_config;
-        iface_get_config->iface_idx = iface_data->iface_idx;
-
-        if (is_ipv4) {
-            if (!nm_utils_parse_inaddr_bin(AF_INET, fip_str, NULL, &tmp_addr)) {
-                error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN,
-                                           "ip is not a valid private ip address");
-                goto done;
-            }
-            _LOGD("interface[%" G_GSSIZE_FORMAT "]: adding private ip %s",
-                  iface_data->iface_idx,
-                  fip_str);
-            iface_get_config->ipv4s_arr[iface_get_config->ipv4s_len] = tmp_addr;
-            iface_get_config->has_ipv4s                              = TRUE;
-            iface_get_config->ipv4s_len++;
-        } else {
-            tmp_prefix = (_nm_utils_ascii_str_to_int64(fip_str, 10, 0, 32, -1));
-
-            if (tmp_prefix == -1) {
-                _LOGD("interface[%" G_GSSIZE_FORMAT "]: invalid prefix %d",
-                      iface_data->iface_idx,
-                      tmp_prefix);
-                goto done;
-            }
-            _LOGD("interface[%" G_GSSIZE_FORMAT "]: adding prefix %d",
-                  iface_data->iface_idx,
-                  tmp_prefix);
-            iface_get_config->cidr_prefix = tmp_prefix;
-            iface_get_config->has_cidr    = TRUE;
+        if (fip_len > 0 && memchr(fip_str, '\0', fip_len - 1)) {
+            /* we have an embedded "\0" inside the string (except trailing). That is not
+             * allowed*/
+        } else
+            tmp_prefix = _nm_utils_ascii_str_to_int64(fip_str, 10, 0, 32, -1);
+
+        if (tmp_prefix == -1) {
+            _LOGD("interface[%" G_GSSIZE_FORMAT "]: invalid prefix", iface_data->intern_iface_idx);
+            error =
+                nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "subnet does not give a valid prefix");
+            goto out_done;
         }
+
+        _LOGD("interface[%" G_GSSIZE_FORMAT "]: adding prefix %d",
+              iface_data->intern_iface_idx,
+              tmp_prefix);
+        iface_get_config->cidr_prefix = tmp_prefix;
+        iface_get_config->has_cidr    = TRUE;
     }
 
-done:
-    --iface_data->n_ips_prefix_pending;
-    if (iface_data->n_ips_prefix_pending == 0) {
-        _azure_iface_data_free(iface_data);
-        --azure_data->n_ifaces_pending;
-        _get_config_maybe_task_return(azure_data, g_steal_pointer(&error));
+out_done:
+    if (!error) {
+        --iface_data->n_ips_prefix_pending;
+        if (iface_data->n_ips_prefix_pending > 0)
+            return;
     }
+
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
@@ -235,20 +204,24 @@ _get_config_fetch_done_cb_subnet_cidr_prefix(GObject *     source,
 static void
 _get_config_ips_prefix_list_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    gs_unref_bytes GBytes *response    = NULL;
-    AzureIfaceData *       iface_data  = user_data;
-    gs_free_error GError *error        = NULL;
-    const char *          response_str = NULL;
-    gsize                 response_len;
-    AzureData *           azure_data;
-    const char *          line;
-    gsize                 line_len;
-
-    azure_data = iface_data->azure_data;
+    gs_unref_bytes GBytes *response             = NULL;
+    AzureIfaceData *       iface_data           = user_data;
+    gs_free_error GError *         error        = NULL;
+    const char *                   response_str = NULL;
+    gsize                          response_len;
+    NMCSProviderGetConfigTaskData *get_config_data;
+    const char *                   line;
+    gsize                          line_len;
 
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data = iface_data->get_config_data;
+
     if (error)
-        goto done;
+        goto out_error;
 
     response_str = g_bytes_get_data(response, &response_len);
     /* NMHttpClient guarantees that there is a trailing NUL after the data. */
@@ -263,7 +236,7 @@ _get_config_ips_prefix_list_cb(GObject *source, GAsyncResult *result, gpointer u
 
         if (line_len == 0)
             continue;
-        /* Truncate the string. It's safe to do, because we own @response_data an it has an
+        /* Truncate the string. It's safe to do, because we own @response an it has an
          * extra NULL character after the buffer. */
         ((char *) line)[line_len] = '\0';
 
@@ -286,14 +259,14 @@ _get_config_ips_prefix_list_cb(GObject *source, GAsyncResult *result, gpointer u
                 (uri = _azure_uri_interfaces(nm_sprintf_buf(
                      buf,
                      "%" G_GSSIZE_FORMAT "/ipv4/ipAddress/%" G_GINT64_FORMAT "/privateIpAddress",
-                     iface_data->iface_idx,
+                     iface_data->intern_iface_idx,
                      ips_prefix_idx))),
                 HTTP_TIMEOUT_MS,
                 512 * 1024,
                 10000,
                 1000,
                 NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
-                g_task_get_cancellable(azure_data->config_data->task),
+                get_config_data->intern_cancellable,
                 NULL,
                 NULL,
                 _get_config_fetch_done_cb_private_ipv4s,
@@ -312,14 +285,14 @@ _get_config_ips_prefix_list_cb(GObject *source, GAsyncResult *result, gpointer u
         nm_http_client_poll_get(
             NM_HTTP_CLIENT(source),
             (uri = _azure_uri_interfaces(
-                 nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT, iface_data->iface_idx),
+                 nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT, iface_data->intern_iface_idx),
                  "/ipv4/subnet/0/prefix/")),
             HTTP_TIMEOUT_MS,
             512 * 1024,
             10000,
             1000,
             NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
-            g_task_get_cancellable(azure_data->config_data->task),
+            get_config_data->intern_cancellable,
             NULL,
             NULL,
             _get_config_fetch_done_cb_subnet_cidr_prefix,
@@ -327,56 +300,75 @@ _get_config_ips_prefix_list_cb(GObject *source, GAsyncResult *result, gpointer u
     }
     return;
 
-done:
-    _azure_iface_data_free(iface_data);
-    --azure_data->n_ifaces_pending;
-    _get_config_maybe_task_return(azure_data, g_steal_pointer(&error));
+out_error:
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
 _get_config_iface_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
+    NMCSProviderGetConfigTaskData *get_config_data;
     gs_unref_bytes GBytes *response   = NULL;
     AzureIfaceData *       iface_data = user_data;
+    gs_free char *         v_hwaddr   = NULL;
     gs_free_error GError *error       = NULL;
     gs_free const char *  uri         = NULL;
     char                  buf[100];
-    AzureData *           azure_data;
-
-    azure_data = iface_data->azure_data;
 
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
 
-    if (error)
-        goto done;
+    if (nm_utils_error_is_cancelled(error))
+        return;
 
-    iface_data->hwaddr = nmcs_utils_hwaddr_normalize(g_bytes_get_data(response, NULL), -1);
+    get_config_data = iface_data->get_config_data;
 
-    if (!iface_data->hwaddr) {
-        goto done;
+    if (error)
+        goto out_done;
+
+    v_hwaddr = nmcs_utils_hwaddr_normalize_gbytes(response);
+    if (!v_hwaddr) {
+        _LOGI("interface[%" G_GSSIZE_FORMAT "]: invalid MAC address returned",
+              iface_data->intern_iface_idx);
+        error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN,
+                                   "invalid MAC address for index %" G_GSSIZE_FORMAT,
+                                   iface_data->intern_iface_idx);
+        goto out_done;
     }
 
-    iface_data->iface_get_config =
-        g_hash_table_lookup(azure_data->config_data->result_dict, iface_data->hwaddr);
-
-    if (!iface_data->iface_get_config) {
-        if (!iface_data->azure_data->config_data->any) {
-            _LOGD("interface[%" G_GSSIZE_FORMAT "]: ignore hwaddr %s",
-                  iface_data->iface_idx,
-                  iface_data->hwaddr);
-            goto done;
+    if (!g_hash_table_lookup_extended(get_config_data->result_dict,
+                                      v_hwaddr,
+                                      (gpointer *) &iface_data->hwaddr,
+                                      (gpointer *) &iface_data->iface_get_config)) {
+        if (!get_config_data->any) {
+            _LOGD("get-config: skip fetching meta data for %s (%" G_GSSIZE_FORMAT ")",
+                  v_hwaddr,
+                  iface_data->intern_iface_idx);
+            goto out_done;
         }
         iface_data->iface_get_config = nmcs_provider_get_config_iface_data_new(FALSE);
-        g_hash_table_insert(azure_data->config_data->result_dict,
-                            g_strdup(iface_data->hwaddr),
+        g_hash_table_insert(get_config_data->result_dict,
+                            (char *) (iface_data->hwaddr = g_steal_pointer(&v_hwaddr)),
                             iface_data->iface_get_config);
+    } else {
+        if (iface_data->iface_get_config->iface_idx >= 0) {
+            _LOGI("interface[%" G_GSSIZE_FORMAT "]: duplicate MAC address %s returned",
+                  iface_data->intern_iface_idx,
+                  iface_data->hwaddr);
+            error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN,
+                                       "duplicate MAC address for index %" G_GSSIZE_FORMAT,
+                                       iface_data->intern_iface_idx);
+            goto out_done;
+        }
     }
 
+    iface_data->iface_get_config->iface_idx = iface_data->extern_iface_idx;
+
     _LOGD("interface[%" G_GSSIZE_FORMAT "]: found a matching device with hwaddr %s",
-          iface_data->iface_idx,
+          iface_data->intern_iface_idx,
           iface_data->hwaddr);
 
-    nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/ipv4/ipAddress/", iface_data->iface_idx);
+    nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/ipv4/ipAddress/", iface_data->intern_iface_idx);
 
     nm_http_client_poll_get(NM_HTTP_CLIENT(source),
                             (uri = _azure_uri_interfaces(buf)),
@@ -385,36 +377,41 @@ _get_config_iface_cb(GObject *source, GAsyncResult *result, gpointer user_data)
                             10000,
                             1000,
                             NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
-                            g_task_get_cancellable(azure_data->config_data->task),
+                            get_config_data->intern_cancellable,
                             NULL,
                             NULL,
                             _get_config_ips_prefix_list_cb,
                             iface_data);
     return;
 
-done:
-    nm_g_slice_free(iface_data);
-    --azure_data->n_ifaces_pending;
-    _get_config_maybe_task_return(azure_data, g_steal_pointer(&error));
+out_done:
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
 _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
+    NMCSProviderGetConfigTaskData *get_config_data;
     gs_unref_ptrarray GPtrArray *ifaces_arr = NULL;
     gs_unref_bytes GBytes *response         = NULL;
     gs_free_error GError *error             = NULL;
-    AzureData *           azure_data        = user_data;
     const char *          response_str;
     gsize                 response_len;
     const char *          line;
     gsize                 line_len;
     guint                 i;
+    gssize                extern_iface_idx_cnt = 0;
 
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
 
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data = user_data;
+
     if (error) {
-        _get_config_maybe_task_return(azure_data, g_steal_pointer(&error));
+        _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
         return;
     }
 
@@ -422,31 +419,32 @@ _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
     /* NMHttpClient guarantees that there is a trailing NUL after the data. */
     nm_assert(response_str[response_len] == 0);
 
-    ifaces_arr = g_ptr_array_new();
+    ifaces_arr = g_ptr_array_new_with_free_func((GDestroyNotify) _azure_iface_data_destroy);
 
     while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) {
         AzureIfaceData *iface_data;
-        gssize          iface_idx;
+        gssize          intern_iface_idx;
 
         if (line_len == 0)
             continue;
 
-        /* Truncate the string. It's safe to do, because we own @response_data an it has an
+        /* Truncate the string. It's safe to do, because we own @response an it has an
          * extra NULL character after the buffer. */
         ((char *) line)[line_len] = '\0';
 
         if (line[line_len - 1] == '/' && line_len != 0)
             ((char *) line)[--line_len] = '\0';
 
-        iface_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXSSIZE, -1);
-        if (iface_idx < 0)
+        intern_iface_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXSSIZE, -1);
+        if (intern_iface_idx < 0)
             continue;
 
         iface_data  = g_slice_new(AzureIfaceData);
         *iface_data = (AzureIfaceData){
+            .get_config_data      = get_config_data,
             .iface_get_config     = NULL,
-            .azure_data           = azure_data,
-            .iface_idx            = iface_idx,
+            .intern_iface_idx     = intern_iface_idx,
+            .extern_iface_idx     = extern_iface_idx_cnt++,
             .n_ips_prefix_pending = 0,
             .hwaddr               = NULL,
         };
@@ -456,21 +454,23 @@ _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
     _LOGD("found azure interfaces: %u", ifaces_arr->len);
 
     if (ifaces_arr->len == 0) {
-        error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "no Azure interfaces found");
-        _get_config_maybe_task_return(azure_data, g_steal_pointer(&error));
+        _nmcs_provider_get_config_task_maybe_return(
+            get_config_data,
+            nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "no Azure interfaces found"));
         return;
     }
 
     for (i = 0; i < ifaces_arr->len; ++i) {
-        AzureIfaceData *    data = ifaces_arr->pdata[i];
-        gs_free const char *uri  = NULL;
+        AzureIfaceData *    iface_data = ifaces_arr->pdata[i];
+        gs_free const char *uri        = NULL;
         char                buf[100];
 
-        _LOGD("azure interface[%" G_GSSIZE_FORMAT "]: retrieving configuration", data->iface_idx);
+        _LOGD("azure interface[%" G_GSSIZE_FORMAT "]: retrieving configuration",
+              iface_data->intern_iface_idx);
 
-        nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/macAddress", data->iface_idx);
+        nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/macAddress", iface_data->intern_iface_idx);
 
-        azure_data->n_ifaces_pending++;
+        get_config_data->n_pending++;
         nm_http_client_poll_get(NM_HTTP_CLIENT(source),
                                 (uri = _azure_uri_interfaces(buf)),
                                 HTTP_TIMEOUT_MS,
@@ -478,25 +478,21 @@ _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
                                 10000,
                                 1000,
                                 NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
-                                g_task_get_cancellable(azure_data->config_data->task),
+                                get_config_data->intern_cancellable,
                                 NULL,
                                 NULL,
                                 _get_config_iface_cb,
-                                data);
+                                iface_data);
     }
+
+    get_config_data->extra_data_destroy = (GDestroyNotify) g_ptr_array_unref;
+    get_config_data->extra_data         = g_steal_pointer(&ifaces_arr);
 }
 
 static void
 get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data)
 {
     gs_free const char *uri = NULL;
-    AzureData *         azure_data;
-
-    azure_data  = g_slice_new(AzureData);
-    *azure_data = (AzureData){
-        .config_data      = get_config_data,
-        .n_ifaces_pending = 0,
-    };
 
     nm_http_client_poll_get(nmcs_provider_get_http_client(provider),
                             (uri = _azure_uri_interfaces()),
@@ -505,11 +501,11 @@ get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_dat
                             15000,
                             1000,
                             NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
-                            g_task_get_cancellable(get_config_data->task),
+                            get_config_data->intern_cancellable,
                             NULL,
                             NULL,
                             _get_net_ifaces_list_cb,
-                            azure_data);
+                            get_config_data);
 }
 
 /*****************************************************************************/
diff --git a/clients/cloud-setup/nmcs-provider-azure.h b/clients/cloud-setup/nmcs-provider-azure.h
index 849e94c1..be78768f 100644
--- a/clients/cloud-setup/nmcs-provider-azure.h
+++ b/clients/cloud-setup/nmcs-provider-azure.h
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #ifndef __NMCS_PROVIDER_AZURE_H__
 #define __NMCS_PROVIDER_AZURE_H__
diff --git a/clients/cloud-setup/nmcs-provider-ec2.c b/clients/cloud-setup/nmcs-provider-ec2.c
index fb82914f..1e060034 100644
--- a/clients/cloud-setup/nmcs-provider-ec2.c
+++ b/clients/cloud-setup/nmcs-provider-ec2.c
@@ -1,6 +1,6 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nmcs-provider-ec2.h"
 
@@ -72,11 +72,11 @@ G_DEFINE_TYPE(NMCSProviderEC2, nmcs_provider_ec2, NMCS_TYPE_PROVIDER);
 
 static gboolean
 _detect_get_meta_data_check_cb(long     response_code,
-                               GBytes * response_data,
+                               GBytes * response,
                                gpointer check_user_data,
                                GError **error)
 {
-    return response_code == 200 && nmcs_utils_parse_get_full_line(response_data, "ami-id");
+    return response_code == 200 && nmcs_utils_parse_get_full_line(response, "ami-id");
 }
 
 static void
@@ -129,114 +129,69 @@ detect(NMCSProvider *provider, GTask *task)
 
 /*****************************************************************************/
 
-typedef struct {
-    NMCSProviderGetConfigTaskData *get_config_data;
-    GError *                       error;
-    GCancellable *                 cancellable;
-    gulong                         cancelled_id;
-    guint                          n_pending;
-} GetConfigIfaceData;
-
 static void
-_get_config_task_maybe_return(GetConfigIfaceData *iface_data, GError *error_take)
+_get_config_fetch_done_cb(NMHttpClient *http_client,
+                          GAsyncResult *result,
+                          gpointer      user_data,
+                          gboolean      is_local_ipv4)
 {
-    NMCSProviderGetConfigTaskData *get_config_data = iface_data->get_config_data;
-
-    if (error_take) {
-        if (!iface_data->error)
-            iface_data->error = error_take;
-        else if (!nm_utils_error_is_cancelled(iface_data->error)
-                 && nm_utils_error_is_cancelled(error_take)) {
-            nm_clear_error(&iface_data->error);
-            iface_data->error = error_take;
-        } else
-            g_error_free(error_take);
-    }
+    NMCSProviderGetConfigTaskData *get_config_data;
+    const char *                   hwaddr = NULL;
+    gs_unref_bytes GBytes *response       = NULL;
+    gs_free_error GError *          error = NULL;
+    NMCSProviderGetConfigIfaceData *config_iface_data;
+    in_addr_t                       tmp_addr;
+    int                             tmp_prefix;
 
-    if (iface_data->n_pending > 0)
-        return;
+    nm_utils_user_data_unpack(user_data, &get_config_data, &hwaddr);
 
-    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(get_config_data->task),
-                                      &iface_data->cancelled_id);
+    nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error);
 
-    nm_clear_g_cancellable(&iface_data->cancellable);
+    if (nm_utils_error_is_cancelled(error))
+        return;
 
-    if (iface_data->error) {
-        if (nm_utils_error_is_cancelled(iface_data->error))
-            _LOGD("get-config: cancelled");
-        else
-            _LOGD("get-config: failed: %s", iface_data->error->message);
-        g_task_return_error(get_config_data->task, g_steal_pointer(&iface_data->error));
-    } else {
-        _LOGD("get-config: success");
-        g_task_return_pointer(get_config_data->task,
-                              g_hash_table_ref(get_config_data->result_dict),
-                              (GDestroyNotify) g_hash_table_unref);
-    }
+    if (error)
+        goto out;
 
-    nm_g_slice_free(iface_data);
-    g_object_unref(get_config_data->task);
-}
+    config_iface_data = g_hash_table_lookup(get_config_data->result_dict, hwaddr);
 
-static void
-_get_config_fetch_done_cb(NMHttpClient *http_client,
-                          GAsyncResult *result,
-                          gpointer      user_data,
-                          gboolean      is_local_ipv4)
-{
-    GetConfigIfaceData *iface_data;
-    const char *        hwaddr           = NULL;
-    gs_unref_bytes GBytes *response_data = NULL;
-    gs_free_error GError *error          = NULL;
+    if (is_local_ipv4) {
+        gs_free const char **s_addrs = NULL;
+        gsize                i, len;
 
-    nm_utils_user_data_unpack(user_data, &iface_data, &hwaddr);
+        s_addrs = nm_utils_strsplit_set_full(g_bytes_get_data(response, NULL),
+                                             "\n",
+                                             NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
+        len     = NM_PTRARRAY_LEN(s_addrs);
 
-    nm_http_client_poll_get_finish(http_client, result, NULL, &response_data, &error);
+        nm_assert(!config_iface_data->has_ipv4s);
+        nm_assert(!config_iface_data->ipv4s_arr);
+        config_iface_data->has_ipv4s = TRUE;
+        config_iface_data->ipv4s_len = 0;
+        if (len > 0) {
+            config_iface_data->ipv4s_arr = g_new(in_addr_t, len);
 
-    if (!error) {
-        NMCSProviderGetConfigIfaceData *config_iface_data;
-        in_addr_t                       tmp_addr;
-        int                             tmp_prefix;
-
-        config_iface_data = g_hash_table_lookup(iface_data->get_config_data->result_dict, hwaddr);
-
-        if (is_local_ipv4) {
-            gs_free const char **s_addrs = NULL;
-            gsize                i, len;
-
-            s_addrs = nm_utils_strsplit_set_full(g_bytes_get_data(response_data, NULL),
-                                                 "\n",
-                                                 NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
-            len     = NM_PTRARRAY_LEN(s_addrs);
-
-            nm_assert(!config_iface_data->has_ipv4s);
-            nm_assert(!config_iface_data->ipv4s_arr);
-            config_iface_data->has_ipv4s = TRUE;
-            config_iface_data->ipv4s_len = 0;
-            if (len > 0) {
-                config_iface_data->ipv4s_arr = g_new(in_addr_t, len);
-
-                for (i = 0; i < len; i++) {
-                    if (nm_utils_parse_inaddr_bin(AF_INET, s_addrs[i], NULL, &tmp_addr))
-                        config_iface_data->ipv4s_arr[config_iface_data->ipv4s_len++] = tmp_addr;
-                }
-            }
-        } else {
-            if (nm_utils_parse_inaddr_prefix_bin(AF_INET,
-                                                 g_bytes_get_data(response_data, NULL),
-                                                 NULL,
-                                                 &tmp_addr,
-                                                 &tmp_prefix)) {
-                nm_assert(!config_iface_data->has_cidr);
-                config_iface_data->has_cidr    = TRUE;
-                config_iface_data->cidr_prefix = tmp_prefix;
-                config_iface_data->cidr_addr   = tmp_addr;
+            for (i = 0; i < len; i++) {
+                if (nm_utils_parse_inaddr_bin(AF_INET, s_addrs[i], NULL, &tmp_addr))
+                    config_iface_data->ipv4s_arr[config_iface_data->ipv4s_len++] = tmp_addr;
             }
         }
+    } else {
+        if (nm_utils_parse_inaddr_prefix_bin(AF_INET,
+                                             g_bytes_get_data(response, NULL),
+                                             NULL,
+                                             &tmp_addr,
+                                             &tmp_prefix)) {
+            nm_assert(!config_iface_data->has_cidr);
+            config_iface_data->has_cidr    = TRUE;
+            config_iface_data->cidr_prefix = tmp_prefix;
+            config_iface_data->cidr_addr   = tmp_addr;
+        }
     }
 
-    iface_data->n_pending--;
-    _get_config_task_maybe_return(iface_data, g_steal_pointer(&error));
+out:
+    get_config_data->n_pending--;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
@@ -253,24 +208,6 @@ _get_config_fetch_done_cb_local_ipv4s(GObject *source, GAsyncResult *result, gpo
     _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), result, user_data, TRUE);
 }
 
-static void
-_get_config_fetch_cancelled_cb(GObject *object, gpointer user_data)
-{
-    GetConfigIfaceData *iface_data = user_data;
-
-    if (iface_data->cancelled_id == 0)
-        return;
-
-    nm_clear_g_signal_handler(g_task_get_cancellable(iface_data->get_config_data->task),
-                              &iface_data->cancelled_id);
-    _get_config_task_maybe_return(iface_data, nm_utils_error_new_cancelled(FALSE, NULL));
-}
-
-typedef struct {
-    NMCSProviderGetConfigTaskData *get_config_data;
-    GHashTable *                   response_parsed;
-} GetConfigMetadataData;
-
 typedef struct {
     gssize iface_idx;
     char   path[0];
@@ -279,60 +216,33 @@ typedef struct {
 static void
 _get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    GetConfigMetadataData *        metadata_data = user_data;
-    GetConfigIfaceData *           iface_data;
-    NMCSProviderGetConfigTaskData *get_config_data = metadata_data->get_config_data;
-    gs_unref_hashtable GHashTable *response_parsed =
-        g_steal_pointer(&metadata_data->response_parsed);
-    gs_free_error GError *error = NULL;
-    GCancellable *        cancellable;
+    NMCSProviderGetConfigTaskData *get_config_data;
+    gs_unref_hashtable GHashTable *response_parsed = NULL;
+    gs_free_error GError *error                    = NULL;
     GetConfigMetadataMac *v_mac_data;
     const char *          v_hwaddr;
     GHashTableIter        h_iter;
     NMHttpClient *        http_client;
 
-    nm_g_slice_free(metadata_data);
-
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &error);
 
-    iface_data  = g_slice_new(GetConfigIfaceData);
-    *iface_data = (GetConfigIfaceData){
-        .get_config_data = get_config_data,
-        .n_pending       = 0,
-    };
-
-    if (nm_utils_error_is_cancelled(error)) {
-        _get_config_task_maybe_return(iface_data, g_steal_pointer(&error));
+    if (nm_utils_error_is_cancelled(error))
         return;
-    }
+
+    get_config_data = user_data;
+
+    response_parsed                     = g_steal_pointer(&get_config_data->extra_data);
+    get_config_data->extra_data_destroy = NULL;
 
     /* We ignore errors. Only if we got no response at all, it's a problem.
      * Otherwise, we proceed with whatever we could fetch. */
     if (!response_parsed) {
-        _get_config_task_maybe_return(
-            iface_data,
+        _nmcs_provider_get_config_task_maybe_return(
+            get_config_data,
             nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "meta data for interfaces not found"));
         return;
     }
 
-    cancellable = g_task_get_cancellable(get_config_data->task);
-    if (cancellable) {
-        gulong cancelled_id;
-
-        cancelled_id = g_cancellable_connect(cancellable,
-                                             G_CALLBACK(_get_config_fetch_cancelled_cb),
-                                             iface_data,
-                                             NULL);
-        if (cancelled_id == 0) {
-            _get_config_task_maybe_return(iface_data, nm_utils_error_new_cancelled(FALSE, NULL));
-            return;
-        }
-
-        iface_data->cancelled_id = cancelled_id;
-    }
-
-    iface_data->cancellable = g_cancellable_new();
-
     http_client = nmcs_provider_get_http_client(g_task_get_source_object(get_config_data->task));
 
     g_hash_table_iter_init(&h_iter, response_parsed);
@@ -359,6 +269,7 @@ _get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer us
         }
 
         nm_assert(config_iface_data->iface_idx == -1);
+
         config_iface_data->iface_idx = v_mac_data->iface_idx;
 
         _LOGD("get-config: start fetching meta data for #%" G_GSSIZE_FORMAT ", %s (%s)",
@@ -366,7 +277,7 @@ _get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer us
               hwaddr,
               v_mac_data->path);
 
-        iface_data->n_pending++;
+        get_config_data->n_pending++;
         nm_http_client_poll_get(
             http_client,
             (uri1 = _ec2_uri_interfaces(v_mac_data->path,
@@ -377,13 +288,13 @@ _get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer us
             10000,
             1000,
             NULL,
-            iface_data->cancellable,
+            get_config_data->intern_cancellable,
             NULL,
             NULL,
             _get_config_fetch_done_cb_subnet_ipv4_cidr_block,
-            nm_utils_user_data_pack(iface_data, hwaddr));
+            nm_utils_user_data_pack(get_config_data, hwaddr));
 
-        iface_data->n_pending++;
+        get_config_data->n_pending++;
         nm_http_client_poll_get(
             http_client,
             (uri2 = _ec2_uri_interfaces(v_mac_data->path,
@@ -394,23 +305,23 @@ _get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer us
             10000,
             1000,
             NULL,
-            iface_data->cancellable,
+            get_config_data->intern_cancellable,
             NULL,
             NULL,
             _get_config_fetch_done_cb_local_ipv4s,
-            nm_utils_user_data_pack(iface_data, hwaddr));
+            nm_utils_user_data_pack(get_config_data, hwaddr));
     }
 
-    _get_config_task_maybe_return(iface_data, NULL);
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, NULL);
 }
 
 static gboolean
 _get_config_metadata_ready_check(long     response_code,
-                                 GBytes * response_data,
+                                 GBytes * response,
                                  gpointer check_user_data,
                                  GError **error)
 {
-    GetConfigMetadataData *metadata_data           = check_user_data;
+    NMCSProviderGetConfigTaskData *get_config_data = check_user_data;
     gs_unref_hashtable GHashTable *response_parsed = NULL;
     const guint8 *                 r_data;
     const char *                   cur_line;
@@ -421,12 +332,12 @@ _get_config_metadata_ready_check(long     response_code,
     const char *                   c_hwaddr;
     gssize                         iface_idx_counter = 0;
 
-    if (response_code != 200 || !response_data) {
+    if (response_code != 200 || !response) {
         /* we wait longer. */
         return FALSE;
     }
 
-    r_data = g_bytes_get_data(response_data, &r_len);
+    r_data = g_bytes_get_data(response, &r_len);
     /* NMHttpClient guarantees that there is a trailing NUL after the data. */
     nm_assert(r_data[r_len] == 0);
 
@@ -437,7 +348,7 @@ _get_config_metadata_ready_check(long     response_code,
         if (cur_line_len == 0)
             continue;
 
-        /* Truncate the string. It's safe to do, because we own @response_data an it has an
+        /* Truncate the string. It's safe to do, because we own @response an it has an
          * extra NUL character after the buffer. */
         ((char *) cur_line)[cur_line_len] = '\0';
 
@@ -454,11 +365,12 @@ _get_config_metadata_ready_check(long     response_code,
         mac_data->iface_idx = iface_idx_counter++;
         memcpy(mac_data->path, cur_line, cur_line_len + 1u);
 
+        /* here we will ignore duplicate responses. */
         g_hash_table_insert(response_parsed, hwaddr, mac_data);
     }
 
     has_all = TRUE;
-    g_hash_table_iter_init(&h_iter, metadata_data->get_config_data->result_dict);
+    g_hash_table_iter_init(&h_iter, get_config_data->result_dict);
     while (g_hash_table_iter_next(&h_iter, (gpointer *) &c_hwaddr, NULL)) {
         if (!response_parsed || !g_hash_table_contains(response_parsed, c_hwaddr)) {
             has_all = FALSE;
@@ -466,21 +378,18 @@ _get_config_metadata_ready_check(long     response_code,
         }
     }
 
-    nm_clear_pointer(&metadata_data->response_parsed, g_hash_table_unref);
-    metadata_data->response_parsed = g_steal_pointer(&response_parsed);
+    nm_clear_pointer(&get_config_data->extra_data, g_hash_table_unref);
+    if (response_parsed) {
+        get_config_data->extra_data         = g_steal_pointer(&response_parsed);
+        get_config_data->extra_data_destroy = (GDestroyNotify) g_hash_table_unref;
+    }
     return has_all;
 }
 
 static void
 get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data)
 {
-    gs_free char *         uri = NULL;
-    GetConfigMetadataData *metadata_data;
-
-    metadata_data  = g_slice_new(GetConfigMetadataData);
-    *metadata_data = (GetConfigMetadataData){
-        .get_config_data = get_config_data,
-    };
+    gs_free char *uri = NULL;
 
     /* First we fetch the "macs/". If the caller requested some particular
      * MAC addresses, then we poll until we see them. They might not yet be
@@ -493,11 +402,11 @@ get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_dat
                             15000,
                             1000,
                             NULL,
-                            g_task_get_cancellable(get_config_data->task),
+                            get_config_data->intern_cancellable,
                             _get_config_metadata_ready_check,
-                            metadata_data,
+                            get_config_data,
                             _get_config_metadata_ready_cb,
-                            metadata_data);
+                            get_config_data);
 }
 
 /*****************************************************************************/
diff --git a/clients/cloud-setup/nmcs-provider-ec2.h b/clients/cloud-setup/nmcs-provider-ec2.h
index b5f178f5..8f8b6400 100644
--- a/clients/cloud-setup/nmcs-provider-ec2.h
+++ b/clients/cloud-setup/nmcs-provider-ec2.h
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #ifndef __NMCS_PROVIDER_EC2_H__
 #define __NMCS_PROVIDER_EC2_H__
diff --git a/clients/cloud-setup/nmcs-provider-gcp.c b/clients/cloud-setup/nmcs-provider-gcp.c
index b4398fb8..1deaea10 100644
--- a/clients/cloud-setup/nmcs-provider-gcp.c
+++ b/clients/cloud-setup/nmcs-provider-gcp.c
@@ -1,6 +1,6 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nmcs-provider-gcp.h"
 
@@ -89,126 +89,97 @@ detect(NMCSProvider *provider, GTask *task)
 /*****************************************************************************/
 
 typedef struct {
-    NMCSProviderGetConfigTaskData *config_data;
-    guint                          n_ifaces_pending;
-    GError *                       error;
-} GCPData;
-
-typedef struct {
+    NMCSProviderGetConfigTaskData * get_config_data;
     NMCSProviderGetConfigIfaceData *iface_get_config;
-    GCPData *                       gcp_data;
-    gssize                          iface_idx;
+    gssize                          intern_iface_idx;
+    gssize                          extern_iface_idx;
     guint                           n_fips_pending;
 } GCPIfaceData;
 
 static void
-_get_config_maybe_task_return(GCPData *gcp_data, GError *error_take)
+_gcp_iface_data_destroy(GCPIfaceData *iface_data)
 {
-    NMCSProviderGetConfigTaskData *config_data = gcp_data->config_data;
-
-    if (error_take) {
-        if (!gcp_data->error)
-            gcp_data->error = error_take;
-        else if (!nm_utils_error_is_cancelled(gcp_data->error)
-                 && nm_utils_error_is_cancelled(error_take)) {
-            nm_clear_error(&gcp_data->error);
-            gcp_data->error = error_take;
-        } else
-            g_error_free(error_take);
-    }
-
-    if (gcp_data->n_ifaces_pending > 0)
-        return;
-
-    if (gcp_data->error) {
-        if (nm_utils_error_is_cancelled(gcp_data->error))
-            _LOGD("get-config: cancelled");
-        else
-            _LOGD("get-config: failed: %s", gcp_data->error->message);
-        g_task_return_error(config_data->task, g_steal_pointer(&gcp_data->error));
-    } else {
-        _LOGD("get-config: success");
-        g_task_return_pointer(config_data->task,
-                              g_hash_table_ref(config_data->result_dict),
-                              (GDestroyNotify) g_hash_table_unref);
-    }
-
-    nm_g_slice_free(gcp_data);
-    g_object_unref(config_data->task);
+    nm_g_slice_free(iface_data);
 }
 
 static void
 _get_config_fip_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
+    NMCSProviderGetConfigTaskData * get_config_data;
     NMCSProviderGetConfigIfaceData *iface_get_config;
     gs_unref_bytes GBytes *response   = NULL;
     GCPIfaceData *         iface_data = user_data;
     gs_free_error GError *error       = NULL;
-    const char *          fip_str     = NULL;
+    gs_free char *        ipaddr      = NULL;
     NMIPRoute **          routes_arr;
     NMIPRoute *           route_new;
-    GCPData *             gcp_data;
-
-    gcp_data = iface_data->gcp_data;
 
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
 
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data = iface_data->get_config_data;
+
     if (error)
-        goto iface_done;
+        goto out_done;
 
-    fip_str = g_bytes_get_data(response, NULL);
-    if (!nm_utils_ipaddr_valid(AF_INET, fip_str)) {
+    ipaddr = nmcs_utils_ipaddr_normalize_gbytes(AF_INET, response);
+    if (!ipaddr) {
         error =
             nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "forwarded-ip is not a valid ip address");
-        goto iface_done;
+        goto out_done;
     }
 
     _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: adding forwarded-ip %s",
-          iface_data->iface_idx,
-          fip_str);
+          iface_data->intern_iface_idx,
+          ipaddr);
 
-    iface_get_config            = iface_data->iface_get_config;
-    iface_get_config->iface_idx = iface_data->iface_idx;
-    routes_arr                  = iface_get_config->iproutes_arr;
+    iface_get_config = iface_data->iface_get_config;
+    routes_arr       = iface_get_config->iproutes_arr;
 
-    route_new = nm_ip_route_new(AF_INET, fip_str, 32, NULL, 100, &error);
+    route_new = nm_ip_route_new(AF_INET, ipaddr, 32, NULL, 100, &error);
     if (error)
-        goto iface_done;
+        goto out_done;
 
     nm_ip_route_set_attribute(route_new, NM_IP_ROUTE_ATTRIBUTE_TYPE, g_variant_new_string("local"));
     routes_arr[iface_get_config->iproutes_len] = route_new;
     ++iface_get_config->iproutes_len;
 
-iface_done:
-    --iface_data->n_fips_pending;
-    if (iface_data->n_fips_pending == 0) {
-        nm_g_slice_free(iface_data);
-        --gcp_data->n_ifaces_pending;
+out_done:
+    if (!error) {
+        --iface_data->n_fips_pending;
+        if (iface_data->n_fips_pending > 0)
+            return;
     }
 
-    _get_config_maybe_task_return(gcp_data, g_steal_pointer(&error));
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
 _get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
+    NMCSProviderGetConfigTaskData *get_config_data;
     gs_unref_ptrarray GPtrArray *uri_arr = NULL;
     gs_unref_bytes GBytes *response      = NULL;
     GCPIfaceData *         iface_data    = user_data;
     gs_free_error GError *error          = NULL;
     const char *          response_str   = NULL;
     gsize                 response_len;
-    GCPData *             gcp_data;
     const char *          line;
     gsize                 line_len;
     guint                 i;
 
-    gcp_data = iface_data->gcp_data;
-
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
 
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data = iface_data->get_config_data;
+
     if (error)
-        goto fips_error;
+        goto out_error;
 
     response_str = g_bytes_get_data(response, &response_len);
     /* NMHttpClient guarantees that there is a trailing NUL after the data. */
@@ -218,7 +189,7 @@ _get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
     while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) {
         gint64 fip_index;
 
-        /* Truncate the string. It's safe to do, because we own @response_data an it has an
+        /* Truncate the string. It's safe to do, because we own @response an it has an
          * extra NUL character after the buffer. */
         ((char *) line)[line_len] = '\0';
 
@@ -228,19 +199,19 @@ _get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
 
         g_ptr_array_add(uri_arr,
                         g_strdup_printf("%" G_GSSIZE_FORMAT "/forwarded-ips/%" G_GINT64_FORMAT,
-                                        iface_data->iface_idx,
+                                        iface_data->intern_iface_idx,
                                         fip_index));
     }
 
     iface_data->n_fips_pending = uri_arr->len;
 
     _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: found %u forwarded ips",
-          iface_data->iface_idx,
+          iface_data->intern_iface_idx,
           iface_data->n_fips_pending);
 
     if (iface_data->n_fips_pending == 0) {
         error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "found no forwarded ip");
-        goto fips_error;
+        goto out_error;
     }
 
     iface_data->iface_get_config->iproutes_arr = g_new(NMIPRoute *, iface_data->n_fips_pending);
@@ -256,7 +227,7 @@ _get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
                                 HTTP_POLL_TIMEOUT_MS,
                                 HTTP_RATE_LIMIT_MS,
                                 NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
-                                g_task_get_cancellable(gcp_data->config_data->task),
+                                get_config_data->intern_cancellable,
                                 NULL,
                                 NULL,
                                 _get_config_fip_cb,
@@ -264,45 +235,80 @@ _get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
     }
     return;
 
-fips_error:
-    nm_g_slice_free(iface_data);
-    --gcp_data->n_ifaces_pending;
-    _get_config_maybe_task_return(gcp_data, g_steal_pointer(&error));
+out_error:
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
 _get_config_iface_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    gs_unref_bytes GBytes *response   = NULL;
-    GCPIfaceData *         iface_data = user_data;
-    gs_free_error GError *error       = NULL;
-    gs_free const char *  hwaddr      = NULL;
-    gs_free const char *  uri         = NULL;
-    char                  sbuf[100];
-    GCPData *             gcp_data;
-
-    gcp_data = iface_data->gcp_data;
+    gs_unref_bytes GBytes *response         = NULL;
+    GCPIfaceData *         iface_data       = user_data;
+    gs_free_error GError *         error    = NULL;
+    gs_free char *                 v_hwaddr = NULL;
+    const char *                   hwaddr   = NULL;
+    gs_free const char *           uri      = NULL;
+    char                           sbuf[100];
+    NMCSProviderGetConfigTaskData *get_config_data;
+    gboolean                       is_requested;
 
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
 
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data = iface_data->get_config_data;
+
     if (error)
-        goto iface_error;
+        goto out_done;
 
-    hwaddr = nmcs_utils_hwaddr_normalize(g_bytes_get_data(response, NULL), -1);
-    iface_data->iface_get_config = g_hash_table_lookup(gcp_data->config_data->result_dict, hwaddr);
-    if (!iface_data->iface_get_config) {
-        _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: did not find a matching device",
-              iface_data->iface_idx);
+    v_hwaddr = nmcs_utils_hwaddr_normalize_gbytes(response);
+    if (!v_hwaddr) {
+        _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: invalid MAC address returned",
+              iface_data->intern_iface_idx);
         error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN,
-                                   "no matching hwaddr found for GCP interface");
-        goto iface_error;
+                                   "invalid MAC address for index %" G_GSSIZE_FORMAT,
+                                   iface_data->intern_iface_idx);
+        goto out_done;
+    }
+
+    if (!g_hash_table_lookup_extended(get_config_data->result_dict,
+                                      v_hwaddr,
+                                      (gpointer *) &hwaddr,
+                                      (gpointer *) &iface_data->iface_get_config)) {
+        if (!get_config_data->any) {
+            _LOGD("get-config: skip fetching meta data for %s (%" G_GSSIZE_FORMAT ")",
+                  v_hwaddr,
+                  iface_data->intern_iface_idx);
+            goto out_done;
+        }
+        iface_data->iface_get_config = nmcs_provider_get_config_iface_data_new(FALSE);
+        g_hash_table_insert(get_config_data->result_dict,
+                            (char *) (hwaddr = g_steal_pointer(&v_hwaddr)),
+                            iface_data->iface_get_config);
+        is_requested = FALSE;
+    } else {
+        if (iface_data->iface_get_config->iface_idx >= 0) {
+            _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: duplicate MAC address %s returned",
+                  iface_data->intern_iface_idx,
+                  hwaddr);
+            error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN,
+                                       "duplicate MAC address for index %" G_GSSIZE_FORMAT,
+                                       iface_data->intern_iface_idx);
+            goto out_done;
+        }
+        is_requested = TRUE;
     }
 
-    _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: found a matching device with hwaddr %s",
-          iface_data->iface_idx,
+    iface_data->iface_get_config->iface_idx = iface_data->extern_iface_idx;
+
+    _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: found a %sdevice with hwaddr %s",
+          iface_data->intern_iface_idx,
+          is_requested ? "requested " : "",
           hwaddr);
 
-    nm_sprintf_buf(sbuf, "%" G_GSSIZE_FORMAT "/forwarded-ips/", iface_data->iface_idx);
+    nm_sprintf_buf(sbuf, "%" G_GSSIZE_FORMAT "/forwarded-ips/", iface_data->intern_iface_idx);
 
     nm_http_client_poll_get(NM_HTTP_CLIENT(source),
                             (uri = _gcp_uri_interfaces(sbuf)),
@@ -311,17 +317,16 @@ _get_config_iface_cb(GObject *source, GAsyncResult *result, gpointer user_data)
                             HTTP_POLL_TIMEOUT_MS,
                             HTTP_RATE_LIMIT_MS,
                             NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
-                            g_task_get_cancellable(gcp_data->config_data->task),
+                            get_config_data->intern_cancellable,
                             NULL,
                             NULL,
                             _get_config_ips_list_cb,
                             iface_data);
     return;
 
-iface_error:
-    nm_g_slice_free(iface_data);
-    --gcp_data->n_ifaces_pending;
-    _get_config_maybe_task_return(gcp_data, g_steal_pointer(&error));
+out_done:
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
 }
 
 static void
@@ -329,18 +334,24 @@ _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
 {
     gs_unref_ptrarray GPtrArray *ifaces_arr = NULL;
     gs_unref_bytes GBytes *response         = NULL;
-    gs_free_error GError *error             = NULL;
-    GCPData *             gcp_data          = user_data;
-    const char *          response_str;
-    gsize                 response_len;
-    const char *          line;
-    gsize                 line_len;
-    guint                 i;
+    gs_free_error GError *         error    = NULL;
+    NMCSProviderGetConfigTaskData *get_config_data;
+    const char *                   response_str;
+    gsize                          response_len;
+    const char *                   line;
+    gsize                          line_len;
+    guint                          i;
+    gssize                         extern_iface_idx_cnt = 0;
 
     nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
 
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data = user_data;
+
     if (error) {
-        _get_config_maybe_task_return(gcp_data, g_steal_pointer(&error));
+        _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
         return;
     }
 
@@ -348,47 +359,56 @@ _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
     /* NMHttpClient guarantees that there is a trailing NUL after the data. */
     nm_assert(response_str[response_len] == 0);
 
-    ifaces_arr = g_ptr_array_new();
+    ifaces_arr = g_ptr_array_new_with_free_func((GDestroyNotify) _gcp_iface_data_destroy);
 
     while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) {
         GCPIfaceData *iface_data;
-        gssize        iface_idx;
+        gssize        intern_iface_idx;
 
         if (line_len == 0)
             continue;
 
-        /* Truncate the string. It's safe to do, because we own @response_data an it has an
+        /* Truncate the string. It's safe to do, because we own @response an it has an
          * extra NUL character after the buffer. */
         ((char *) line)[line_len] = '\0';
         if (line[line_len - 1] == '/')
             ((char *) line)[--line_len] = '\0';
 
-        iface_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXSSIZE, -1);
-        if (iface_idx < 0)
+        intern_iface_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXSSIZE, -1);
+        if (intern_iface_idx < 0)
             continue;
 
         iface_data  = g_slice_new(GCPIfaceData);
         *iface_data = (GCPIfaceData){
+            .get_config_data  = get_config_data,
             .iface_get_config = NULL,
-            .gcp_data         = gcp_data,
-            .iface_idx        = iface_idx,
+            .intern_iface_idx = intern_iface_idx,
+            .extern_iface_idx = extern_iface_idx_cnt++,
             .n_fips_pending   = 0,
         };
         g_ptr_array_add(ifaces_arr, iface_data);
     }
 
-    gcp_data->n_ifaces_pending = ifaces_arr->len;
     _LOGI("found GCP interfaces: %u", ifaces_arr->len);
 
+    if (ifaces_arr->len == 0) {
+        _nmcs_provider_get_config_task_maybe_return(
+            get_config_data,
+            nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "no GCP interfaces found"));
+        return;
+    }
+
     for (i = 0; i < ifaces_arr->len; ++i) {
         GCPIfaceData *      data = ifaces_arr->pdata[i];
         gs_free const char *uri  = NULL;
         char                sbuf[100];
 
-        _LOGD("GCP interface[%" G_GSSIZE_FORMAT "]: retrieving configuration", data->iface_idx);
+        _LOGD("GCP interface[%" G_GSSIZE_FORMAT "]: retrieving configuration",
+              data->intern_iface_idx);
 
-        nm_sprintf_buf(sbuf, "%" G_GSSIZE_FORMAT "/mac", data->iface_idx);
+        nm_sprintf_buf(sbuf, "%" G_GSSIZE_FORMAT "/mac", data->intern_iface_idx);
 
+        get_config_data->n_pending++;
         nm_http_client_poll_get(NM_HTTP_CLIENT(source),
                                 (uri = _gcp_uri_interfaces(sbuf)),
                                 HTTP_TIMEOUT_MS,
@@ -396,31 +416,21 @@ _get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat
                                 HTTP_POLL_TIMEOUT_MS,
                                 HTTP_RATE_LIMIT_MS,
                                 NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
-                                g_task_get_cancellable(gcp_data->config_data->task),
+                                get_config_data->intern_cancellable,
                                 NULL,
                                 NULL,
                                 _get_config_iface_cb,
                                 data);
     }
 
-    if (ifaces_arr->len == 0) {
-        error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "no GCP interfaces found");
-        _get_config_maybe_task_return(gcp_data, g_steal_pointer(&error));
-    }
+    get_config_data->extra_data         = g_steal_pointer(&ifaces_arr);
+    get_config_data->extra_data_destroy = (GDestroyNotify) g_ptr_array_unref;
 }
 
 static void
 get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data)
 {
     gs_free const char *uri = NULL;
-    GCPData *           gcp_data;
-
-    gcp_data  = g_slice_new(GCPData);
-    *gcp_data = (GCPData){
-        .config_data      = get_config_data,
-        .n_ifaces_pending = 0,
-        .error            = NULL,
-    };
 
     nm_http_client_poll_get(nmcs_provider_get_http_client(provider),
                             (uri = _gcp_uri_interfaces()),
@@ -429,11 +439,11 @@ get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_dat
                             HTTP_POLL_TIMEOUT_MS,
                             HTTP_RATE_LIMIT_MS,
                             NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
-                            g_task_get_cancellable(gcp_data->config_data->task),
+                            get_config_data->intern_cancellable,
                             NULL,
                             NULL,
                             _get_net_ifaces_list_cb,
-                            gcp_data);
+                            get_config_data);
 }
 
 /*****************************************************************************/
diff --git a/clients/cloud-setup/nmcs-provider-gcp.h b/clients/cloud-setup/nmcs-provider-gcp.h
index 4ead2191..b0329935 100644
--- a/clients/cloud-setup/nmcs-provider-gcp.h
+++ b/clients/cloud-setup/nmcs-provider-gcp.h
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #ifndef __NMCS_PROVIDER_GCP_H__
 #define __NMCS_PROVIDER_GCP_H__
diff --git a/clients/cloud-setup/nmcs-provider.c b/clients/cloud-setup/nmcs-provider.c
index e9e79a35..c700d8e1 100644
--- a/clients/cloud-setup/nmcs-provider.c
+++ b/clients/cloud-setup/nmcs-provider.c
@@ -1,6 +1,6 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include "nm-default.h"
+#include "libnm/nm-default-client.h"
 
 #include "nmcs-provider.h"
 
@@ -114,19 +114,61 @@ _iface_data_free(gpointer data)
 }
 
 static void
-_get_config_data_free(gpointer data)
+_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data, GError *error_take)
 {
-    NMCSProviderGetConfigTaskData *get_config_data = data;
+    gs_free_error GError *error = error_take;
 
-    if (get_config_data->extra_destroy)
-        get_config_data->extra_destroy(get_config_data->extra_data);
+    nm_assert(get_config_data);
+    nm_assert(G_IS_TASK(get_config_data->task));
+
+    if (!error) {
+        if (get_config_data->n_pending > 0)
+            return;
+    }
+
+    g_cancellable_cancel(get_config_data->intern_cancellable);
+
+    if (error) {
+        if (nm_utils_error_is_cancelled(error))
+            _LOGD("get-config: cancelled");
+        else
+            _LOGD("get-config: failed: %s", error->message);
+        g_task_return_error(get_config_data->task, g_steal_pointer(&error));
+    } else {
+        _LOGD("get-config: success");
+        g_task_return_pointer(get_config_data->task,
+                              g_hash_table_ref(get_config_data->result_dict),
+                              (GDestroyNotify) g_hash_table_unref);
+    }
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(get_config_data->task),
+                              &get_config_data->extern_cancelled_id);
+
+    if (get_config_data->extra_data_destroy)
+        get_config_data->extra_data_destroy(get_config_data->extra_data);
 
     nm_clear_pointer(&get_config_data->result_dict, g_hash_table_unref);
 
+    nm_g_object_unref(get_config_data->intern_cancellable);
+    g_object_unref(get_config_data->task);
     nm_g_slice_free(get_config_data);
 }
 
 void
+_nmcs_provider_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data,
+                                            GError *                       error_take)
+{
+    nm_assert(!error_take || !nm_utils_error_is_cancelled(error_take));
+    _get_config_task_maybe_return(get_config_data, error_take);
+}
+
+static void
+_get_config_cancelled_cb(GObject *object, gpointer user_data)
+{
+    _get_config_task_maybe_return(user_data, nm_utils_error_new_cancelled(FALSE, NULL));
+}
+
+void
 nmcs_provider_get_config(NMCSProvider *      self,
                          gboolean            any,
                          const char *const * hwaddrs,
@@ -139,6 +181,8 @@ nmcs_provider_get_config(NMCSProvider *      self,
     g_return_if_fail(NMCS_IS_PROVIDER(self));
     g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable));
 
+    _LOGD("get-config: starting");
+
     get_config_data  = g_slice_new(NMCSProviderGetConfigTaskData);
     *get_config_data = (NMCSProviderGetConfigTaskData){
         .task = nm_g_task_new(self, cancellable, nmcs_provider_get_config, callback, user_data),
@@ -146,8 +190,6 @@ nmcs_provider_get_config(NMCSProvider *      self,
         .result_dict = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, _iface_data_free),
     };
 
-    g_task_set_task_data(get_config_data->task, get_config_data, _get_config_data_free);
-
     nmcs_wait_for_objects_register(get_config_data->task);
 
     for (; hwaddrs && hwaddrs[0]; hwaddrs++) {
@@ -156,7 +198,21 @@ nmcs_provider_get_config(NMCSProvider *      self,
                             nmcs_provider_get_config_iface_data_new(TRUE));
     }
 
-    _LOGD("get-config: starting");
+    if (cancellable) {
+        gulong cancelled_id;
+
+        cancelled_id = g_cancellable_connect(cancellable,
+                                             G_CALLBACK(_get_config_cancelled_cb),
+                                             get_config_data,
+                                             NULL);
+        if (cancelled_id == 0) {
+            /* the callback was already invoked synchronously and the task already returned. */
+            return;
+        }
+
+        get_config_data->extern_cancelled_id = cancelled_id;
+        get_config_data->intern_cancellable  = g_cancellable_new();
+    }
 
     NMCS_PROVIDER_GET_CLASS(self)->get_config(self, get_config_data);
 }
diff --git a/clients/cloud-setup/nmcs-provider.h b/clients/cloud-setup/nmcs-provider.h
index 876b0a37..3b0c2529 100644
--- a/clients/cloud-setup/nmcs-provider.h
+++ b/clients/cloud-setup/nmcs-provider.h
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: LGPL-2.1+ */
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #ifndef __NMCS_PROVIDER_H__
 #define __NMCS_PROVIDER_H__
@@ -12,11 +12,16 @@
 typedef struct {
     in_addr_t *ipv4s_arr;
     gsize      ipv4s_len;
-    gssize     iface_idx;
-    in_addr_t  cidr_addr;
-    guint8     cidr_prefix;
-    bool       has_ipv4s : 1;
-    bool       has_cidr : 1;
+
+    /* If the interface was seen, get_config() should set this to a
+     * unique, increasing, positive index. If the interface is requested,
+     * it is initialized to -1. */
+    gssize iface_idx;
+
+    in_addr_t cidr_addr;
+    guint8    cidr_prefix;
+    bool      has_ipv4s : 1;
+    bool      has_cidr : 1;
 
     NMIPRoute **iproutes_arr;
     gsize       iproutes_len;
@@ -37,11 +42,25 @@ nmcs_provider_get_config_iface_data_is_valid(const NMCSProviderGetConfigIfaceDat
 NMCSProviderGetConfigIfaceData *nmcs_provider_get_config_iface_data_new(gboolean was_requested);
 
 typedef struct {
-    GTask *        task;
-    GHashTable *   result_dict;
+    GTask *task;
+
+    GHashTable *result_dict;
+
+    /* this cancellable should be used for the provider implementation
+     * to listen for cancellation. */
+    GCancellable *intern_cancellable;
+
+    /* the provider implementation may attach extra data. */
     gpointer       extra_data;
-    GDestroyNotify extra_destroy;
-    bool           any : 1;
+    GDestroyNotify extra_data_destroy;
+
+    gulong extern_cancelled_id;
+
+    /* the provider implementation may use this field to track the number of pending
+     * operations. */
+    guint n_pending;
+
+    bool any : 1;
 } NMCSProviderGetConfigTaskData;
 
 #define NMCS_TYPE_PROVIDER (nmcs_provider_get_type())
@@ -93,6 +112,9 @@ gboolean nmcs_provider_detect_finish(NMCSProvider *provider, GAsyncResult *resul
 
 /*****************************************************************************/
 
+void _nmcs_provider_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data,
+                                                 GError *                       error_take);
+
 void nmcs_provider_get_config(NMCSProvider *      provider,
                               gboolean            any,
                               const char *const * hwaddrs,
diff --git a/clients/cloud-setup/tests/meson.build b/clients/cloud-setup/tests/meson.build
new file mode 100644
index 00000000..1c5e6ec6
--- /dev/null
+++ b/clients/cloud-setup/tests/meson.build
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+exe = executable(
+  'test-cloud-setup-general',
+  'test-cloud-setup-general.c',
+  dependencies: [
+    libnmc_base_dep,
+    libnmc_dep,
+    libnm_cloud_setup_core_dep,
+  ],
+  c_args: [
+    '-DG_LOG_DOMAIN="test"',
+  ],
+)
+
+test(
+  'clients/cloud-setup/tests/test-cloud-setup-general',
+  test_script,
+  args: test_args + [exe.full_path()],
+)
diff --git a/clients/cloud-setup/tests/test-cloud-setup-general.c b/clients/cloud-setup/tests/test-cloud-setup-general.c
new file mode 100644
index 00000000..63f3c13d
--- /dev/null
+++ b/clients/cloud-setup/tests/test-cloud-setup-general.c
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "libnm/nm-default-client.h"
+
+#include "nm-cloud-setup-utils.h"
+#include "nm-libnm-core-intern/nm-libnm-core-utils.h"
+
+#include "nm-utils/nm-test-utils.h"
+
+/*****************************************************************************/
+
+static NMSettingIPConfig *
+_ri4a_new_s_ip(const char *const *addrs)
+{
+    NMSettingIPConfig *s_ip;
+
+    s_ip = NM_SETTING_IP_CONFIG(nm_setting_ip4_config_new());
+
+    for (; addrs && addrs[0]; addrs++) {
+        nm_auto_unref_ip_address NMIPAddress *a = nmtst_ip_address_new(AF_INET, addrs[0]);
+
+        nm_setting_ip_config_add_address(s_ip, a);
+    }
+    return s_ip;
+}
+
+static GPtrArray *
+_ri4a_new_arr(const char *const *addrs)
+{
+    GPtrArray *arr;
+
+    arr = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+
+    for (; addrs && addrs[0]; addrs++)
+        g_ptr_array_add(arr, nmtst_ip_address_new(AF_INET, addrs[0]));
+
+    return arr;
+}
+
+static void
+_test_ri4a(const char *const *addrs_before, const char *const *addrs_new)
+{
+    gs_unref_object NMSettingIPConfig *s_ip = NULL;
+    gs_unref_ptrarray GPtrArray *arr        = NULL;
+    const gsize                  n          = NM_PTRARRAY_LEN(addrs_new);
+    gboolean                     changed;
+    gsize                        i;
+
+    s_ip = _ri4a_new_s_ip(addrs_before);
+    arr  = _ri4a_new_arr(addrs_new);
+
+    g_assert_cmpint(nm_setting_ip_config_get_num_addresses(s_ip),
+                    ==,
+                    NM_PTRARRAY_LEN(addrs_before));
+    g_assert_cmpint(arr->len, ==, n);
+
+    changed = nmcs_setting_ip_replace_ipv4_addresses(s_ip, (NMIPAddress **) arr->pdata, arr->len);
+
+    g_assert_cmpint(changed, !=, nm_utils_strv_equal(addrs_before, addrs_new));
+    g_assert_cmpint(nm_setting_ip_config_get_num_addresses(s_ip), ==, n);
+
+    for (i = 0; i < n; i++) {
+        NMIPAddress *a = arr->pdata[i];
+        NMIPAddress *b = nm_setting_ip_config_get_address(s_ip, i);
+
+        if (nmtst_get_rand_bool())
+            NM_SWAP(&a, &b);
+
+        g_assert(nm_ip_address_cmp_full(a, b, NM_IP_ADDRESS_CMP_FLAGS_WITH_ATTRS) == 0);
+    }
+}
+
+static void
+test_replace_ipv4_addresses(void)
+{
+    const char *const *const LISTS[] = {
+        NM_MAKE_STRV(),
+        NM_MAKE_STRV("192.168.5.1/24"),
+        NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"),
+        NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24", "192.168.5.3/24"),
+        NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24", "192.168.5.3/24", "192.168.5.4/24"),
+    };
+    int i_run;
+
+    for (i_run = 0; i_run < 20; i_run++) {
+        gs_free const char **addrs_before = NULL;
+        gs_free const char **addrs_new    = NULL;
+
+        addrs_before = nmtst_rand_perm_strv(LISTS[nmtst_get_rand_uint32() % G_N_ELEMENTS(LISTS)]);
+        addrs_new    = nmtst_rand_perm_strv(LISTS[nmtst_get_rand_uint32() % G_N_ELEMENTS(LISTS)]);
+
+        _test_ri4a(addrs_before, addrs_new);
+    }
+
+    _test_ri4a(NM_MAKE_STRV(), NM_MAKE_STRV());
+    _test_ri4a(NM_MAKE_STRV(), NM_MAKE_STRV("192.168.5.1/24"));
+    _test_ri4a(NM_MAKE_STRV(), NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"));
+    _test_ri4a(NM_MAKE_STRV("192.168.5.1/24"), NM_MAKE_STRV());
+    _test_ri4a(NM_MAKE_STRV("192.168.5.1/24"), NM_MAKE_STRV("192.168.5.1/24"));
+    _test_ri4a(NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), NM_MAKE_STRV());
+    _test_ri4a(NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), NM_MAKE_STRV("192.168.5.1/24"));
+    _test_ri4a(NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), NM_MAKE_STRV("192.168.5.2/24"));
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init(&argc, &argv, TRUE);
+
+    g_test_add_func("/cloud-setup/general/replace-ipv4-addresses", test_replace_ipv4_addresses);
+
+    return g_test_run();
+}