about summary refs log tree commit diff
path: root/src/nm-cloud-setup
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
committerMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
commite74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch)
tree3469f17ea9af91f7ff169b890633bda68b0cf76e /src/nm-cloud-setup
parentbfe522304da217296e2a61040f58e35ec5d6f3f2 (diff)
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/nm-cloud-setup')
-rwxr-xr-xsrc/nm-cloud-setup/90-nm-cloud-setup.sh9
-rw-r--r--src/nm-cloud-setup/main.c683
-rw-r--r--src/nm-cloud-setup/meson.build67
-rw-r--r--src/nm-cloud-setup/nm-cloud-setup-utils.c916
-rw-r--r--src/nm-cloud-setup/nm-cloud-setup-utils.h154
-rw-r--r--src/nm-cloud-setup/nm-cloud-setup.service.in43
-rw-r--r--src/nm-cloud-setup/nm-cloud-setup.timer9
-rw-r--r--src/nm-cloud-setup/nm-http-client.c801
-rw-r--r--src/nm-cloud-setup/nm-http-client.h71
-rw-r--r--src/nm-cloud-setup/nmcs-provider-aliyun.c507
-rw-r--r--src/nm-cloud-setup/nmcs-provider-aliyun.h28
-rw-r--r--src/nm-cloud-setup/nmcs-provider-azure.c565
-rw-r--r--src/nm-cloud-setup/nmcs-provider-azure.h28
-rw-r--r--src/nm-cloud-setup/nmcs-provider-ec2.c408
-rw-r--r--src/nm-cloud-setup/nmcs-provider-ec2.h27
-rw-r--r--src/nm-cloud-setup/nmcs-provider-gcp.c462
-rw-r--r--src/nm-cloud-setup/nmcs-provider-gcp.h27
-rw-r--r--src/nm-cloud-setup/nmcs-provider.c380
-rw-r--r--src/nm-cloud-setup/nmcs-provider.h170
-rw-r--r--src/nm-cloud-setup/tests/meson.build25
-rw-r--r--src/nm-cloud-setup/tests/test-cloud-setup-general.c117
21 files changed, 5497 insertions, 0 deletions
diff --git a/src/nm-cloud-setup/90-nm-cloud-setup.sh b/src/nm-cloud-setup/90-nm-cloud-setup.sh
new file mode 100755
index 00000000..817e2555
--- /dev/null
+++ b/src/nm-cloud-setup/90-nm-cloud-setup.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+case "$2" in
+    up|dhcp4-change)
+        if systemctl -q is-enabled nm-cloud-setup.service ; then
+            exec systemctl --no-block restart nm-cloud-setup.service
+        fi
+        ;;
+esac
diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c
new file mode 100644
index 00000000..916f41da
--- /dev/null
+++ b/src/nm-cloud-setup/main.c
@@ -0,0 +1,683 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "libnm-client-aux-extern/nm-libnm-aux.h"
+
+#include <linux/rtnetlink.h>
+
+#include "nm-cloud-setup-utils.h"
+#include "nmcs-provider-ec2.h"
+#include "nmcs-provider-gcp.h"
+#include "nmcs-provider-azure.h"
+#include "nmcs-provider-aliyun.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainLoop *   main_loop;
+    GCancellable *cancellable;
+    NMCSProvider *provider_result;
+    guint         detect_count;
+} ProviderDetectData;
+
+static void
+_provider_detect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object NMCSProvider *provider = NMCS_PROVIDER(source);
+    gs_free_error GError *error            = NULL;
+    ProviderDetectData *  dd;
+    gboolean              success;
+
+    success = nmcs_provider_detect_finish(provider, result, &error);
+
+    nm_assert(success != (!!error));
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    dd = user_data;
+
+    nm_assert(dd->detect_count > 0);
+    dd->detect_count--;
+
+    if (error) {
+        _LOGI("provider %s not detected: %s", nmcs_provider_get_name(provider), error->message);
+        if (dd->detect_count > 0) {
+            /* wait longer. */
+            return;
+        }
+
+        _LOGI("no provider detected");
+        goto done;
+    }
+
+    _LOGI("provider %s detected", nmcs_provider_get_name(provider));
+    dd->provider_result = g_steal_pointer(&provider);
+
+done:
+    g_cancellable_cancel(dd->cancellable);
+    g_main_loop_quit(dd->main_loop);
+}
+
+static void
+_provider_detect_sigterm_cb(GCancellable *source, gpointer user_data)
+{
+    ProviderDetectData *dd = user_data;
+
+    g_cancellable_cancel(dd->cancellable);
+    g_clear_object(&dd->provider_result);
+    dd->detect_count = 0;
+    g_main_loop_quit(dd->main_loop);
+}
+
+static NMCSProvider *
+_provider_detect(GCancellable *sigterm_cancellable)
+{
+    nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
+    gs_unref_object GCancellable *cancellable    = g_cancellable_new();
+    gs_unref_object NMHttpClient *http_client    = NULL;
+    ProviderDetectData            dd             = {
+        .cancellable     = cancellable,
+        .main_loop       = main_loop,
+        .detect_count    = 0,
+        .provider_result = NULL,
+    };
+    const GType gtypes[] = {
+        NMCS_TYPE_PROVIDER_EC2,
+        NMCS_TYPE_PROVIDER_GCP,
+        NMCS_TYPE_PROVIDER_AZURE,
+        NMCS_TYPE_PROVIDER_ALIYUN,
+    };
+    int    i;
+    gulong cancellable_signal_id;
+
+    cancellable_signal_id = g_cancellable_connect(sigterm_cancellable,
+                                                  G_CALLBACK(_provider_detect_sigterm_cb),
+                                                  &dd,
+                                                  NULL);
+    if (!cancellable_signal_id)
+        goto out;
+
+    http_client = nmcs_wait_for_objects_register(nm_http_client_new());
+
+    for (i = 0; i < G_N_ELEMENTS(gtypes); i++) {
+        NMCSProvider *provider;
+
+        provider = g_object_new(gtypes[i], NMCS_PROVIDER_HTTP_CLIENT, http_client, NULL);
+        nmcs_wait_for_objects_register(provider);
+
+        _LOGD("start detecting %s provider...", nmcs_provider_get_name(provider));
+        dd.detect_count++;
+        nmcs_provider_detect(provider, cancellable, _provider_detect_cb, &dd);
+    }
+
+    if (dd.detect_count > 0)
+        g_main_loop_run(main_loop);
+
+out:
+    nm_clear_g_signal_handler(sigterm_cancellable, &cancellable_signal_id);
+    return dd.provider_result;
+}
+
+/*****************************************************************************/
+
+static char **
+_nmc_get_hwaddrs(NMClient *nmc)
+{
+    gs_unref_ptrarray GPtrArray *hwaddrs = NULL;
+    const GPtrArray *            devices;
+    char **                      hwaddrs_v;
+    gs_free char *               str = NULL;
+    guint                        i;
+
+    devices = nm_client_get_devices(nmc);
+
+    for (i = 0; i < devices->len; i++) {
+        NMDevice *  device = devices->pdata[i];
+        const char *hwaddr;
+        char *      s;
+
+        if (!NM_IS_DEVICE_ETHERNET(device))
+            continue;
+
+        if (nm_device_get_state(device) < NM_DEVICE_STATE_UNAVAILABLE)
+            continue;
+
+        hwaddr = nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(device));
+        if (!hwaddr)
+            continue;
+
+        s = nmcs_utils_hwaddr_normalize(hwaddr, -1);
+        if (!s)
+            continue;
+
+        if (!hwaddrs)
+            hwaddrs = g_ptr_array_new_with_free_func(g_free);
+        g_ptr_array_add(hwaddrs, s);
+    }
+
+    if (!hwaddrs) {
+        _LOGD("found interfaces: none");
+        return NULL;
+    }
+
+    g_ptr_array_add(hwaddrs, NULL);
+    hwaddrs_v = (char **) g_ptr_array_free(g_steal_pointer(&hwaddrs), FALSE);
+
+    _LOGD("found interfaces: %s", (str = g_strjoinv(", ", hwaddrs_v)));
+
+    return hwaddrs_v;
+}
+
+static NMDevice *
+_nmc_get_device_by_hwaddr(NMClient *nmc, const char *hwaddr)
+{
+    const GPtrArray *devices;
+    guint            i;
+
+    devices = nm_client_get_devices(nmc);
+
+    for (i = 0; i < devices->len; i++) {
+        NMDevice *    device = devices->pdata[i];
+        const char *  hwaddr_dev;
+        gs_free char *s = NULL;
+
+        if (!NM_IS_DEVICE_ETHERNET(device))
+            continue;
+
+        hwaddr_dev = nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(device));
+        if (!hwaddr_dev)
+            continue;
+
+        s = nmcs_utils_hwaddr_normalize(hwaddr_dev, -1);
+        if (s && nm_streq(s, hwaddr))
+            return device;
+    }
+
+    return NULL;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainLoop *                  main_loop;
+    NMCSProviderGetConfigResult *result;
+} GetConfigData;
+
+static void
+_get_config_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    GetConfigData *                              data                                = user_data;
+    nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL;
+    gs_free_error GError *error                                                      = NULL;
+
+    result = nmcs_provider_get_config_finish(NMCS_PROVIDER(source), res, &error);
+
+    if (!result) {
+        if (!nm_utils_error_is_cancelled(error))
+            _LOGI("failure to get meta data: %s", error->message);
+    } else
+        _LOGD("meta data received");
+
+    data->result = g_steal_pointer(&result);
+    g_main_loop_quit(data->main_loop);
+}
+
+static NMCSProviderGetConfigResult *
+_get_config(GCancellable *sigterm_cancellable, NMCSProvider *provider, NMClient *nmc)
+{
+    nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
+    GetConfigData                      data      = {
+        .main_loop = main_loop,
+    };
+    gs_strfreev char **hwaddrs = NULL;
+
+    hwaddrs = _nmc_get_hwaddrs(nmc);
+
+    nmcs_provider_get_config(provider,
+                             TRUE,
+                             (const char *const *) hwaddrs,
+                             sigterm_cancellable,
+                             _get_config_cb,
+                             &data);
+
+    g_main_loop_run(main_loop);
+
+    return data.result;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_nmc_skip_connection(NMConnection *connection)
+{
+    NMSettingUser *s_user;
+    const char *   v;
+
+    s_user = NM_SETTING_USER(nm_connection_get_setting(connection, NM_TYPE_SETTING_USER));
+    if (!s_user)
+        return FALSE;
+
+#define USER_TAG_SKIP "org.freedesktop.nm-cloud-setup.skip"
+
+    nm_assert(nm_setting_user_check_key(USER_TAG_SKIP, NULL));
+
+    v = nm_setting_user_get_data(s_user, USER_TAG_SKIP);
+    return _nm_utils_ascii_str_to_bool(v, FALSE);
+}
+
+static gboolean
+_nmc_mangle_connection(NMDevice *                            device,
+                       NMConnection *                        connection,
+                       const NMCSProviderGetConfigResult *   result,
+                       const NMCSProviderGetConfigIfaceData *config_data,
+                       gboolean *                            out_skipped_single_addr,
+                       gboolean *                            out_changed)
+{
+    NMSettingIPConfig * s_ip;
+    NMActiveConnection *ac;
+    NMConnection *      remote_connection;
+    NMSettingIPConfig * remote_s_ip = NULL;
+    gsize               i;
+    gboolean            addrs_changed       = FALSE;
+    gboolean            rules_changed       = FALSE;
+    gboolean            routes_changed      = FALSE;
+    gs_unref_ptrarray GPtrArray *addrs_new  = NULL;
+    gs_unref_ptrarray GPtrArray *rules_new  = NULL;
+    gs_unref_ptrarray GPtrArray *routes_new = NULL;
+
+    NM_SET_OUT(out_skipped_single_addr, FALSE);
+    NM_SET_OUT(out_changed, FALSE);
+
+    if (!nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_WIRED_SETTING_NAME))
+        return FALSE;
+
+    s_ip = nm_connection_get_setting_ip4_config(connection);
+    if (!s_ip)
+        return FALSE;
+
+    if ((ac = nm_device_get_active_connection(device))
+        && (remote_connection = NM_CONNECTION(nm_active_connection_get_connection(ac))))
+        remote_s_ip = nm_connection_get_setting_ip4_config(remote_connection);
+
+    addrs_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref);
+    rules_new =
+        g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref);
+    routes_new = g_ptr_array_new_full(config_data->iproutes_len + !!config_data->ipv4s_len,
+                                      (GDestroyNotify) nm_ip_route_unref);
+
+    if (remote_s_ip) {
+        guint len;
+        guint j;
+
+        len = nm_setting_ip_config_get_num_addresses(remote_s_ip);
+        for (j = 0; j < len; j++) {
+            g_ptr_array_add(addrs_new,
+                            nm_ip_address_dup(nm_setting_ip_config_get_address(remote_s_ip, j)));
+        }
+
+        len = nm_setting_ip_config_get_num_routes(remote_s_ip);
+        for (j = 0; j < len; j++) {
+            g_ptr_array_add(routes_new,
+                            nm_ip_route_dup(nm_setting_ip_config_get_route(remote_s_ip, j)));
+        }
+
+        len = nm_setting_ip_config_get_num_routing_rules(remote_s_ip);
+        for (j = 0; j < len; j++) {
+            g_ptr_array_add(
+                rules_new,
+                nm_ip_routing_rule_ref(nm_setting_ip_config_get_routing_rule(remote_s_ip, j)));
+        }
+    }
+
+    if (result->num_valid_ifaces <= 1 && result->num_ipv4s <= 1) {
+        /* this setup only has one interface and one IPv4 address (or less).
+         * We don't need to configure policy routing in this case. */
+        NM_SET_OUT(out_skipped_single_addr, TRUE);
+    } else if (config_data->has_ipv4s && config_data->has_cidr) {
+        gs_unref_hashtable GHashTable *unique_subnets =
+            g_hash_table_new(nm_direct_hash, g_direct_equal);
+        NMIPAddress *    addr_entry;
+        NMIPRoute *      route_entry;
+        NMIPRoutingRule *rule_entry;
+        in_addr_t        gateway;
+        char             sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+        for (i = 0; i < config_data->ipv4s_len; i++) {
+            addr_entry = nm_ip_address_new_binary(AF_INET,
+                                                  &config_data->ipv4s_arr[i],
+                                                  config_data->cidr_prefix,
+                                                  NULL);
+            nm_assert(addr_entry);
+            g_ptr_array_add(addrs_new, addr_entry);
+        }
+
+        if (config_data->has_gateway && config_data->gateway) {
+            gateway = config_data->gateway;
+        } else {
+            gateway = nm_utils_ip4_address_clear_host_address(config_data->cidr_addr,
+                                                              config_data->cidr_prefix);
+            if (config_data->cidr_prefix < 32)
+                ((guint8 *) &gateway)[3] += 1;
+        }
+
+        for (i = 0; i < config_data->ipv4s_len; i++) {
+            in_addr_t a = config_data->ipv4s_arr[i];
+
+            a = nm_utils_ip4_address_clear_host_address(a, config_data->cidr_prefix);
+
+            G_STATIC_ASSERT_EXPR(sizeof(gsize) >= sizeof(in_addr_t));
+            if (g_hash_table_add(unique_subnets, GSIZE_TO_POINTER(a))) {
+                route_entry =
+                    nm_ip_route_new_binary(AF_INET, &a, config_data->cidr_prefix, NULL, 10, NULL);
+                nm_ip_route_set_attribute(route_entry,
+                                          NM_IP_ROUTE_ATTRIBUTE_TABLE,
+                                          g_variant_new_uint32(30200 + config_data->iface_idx));
+                g_ptr_array_add(routes_new, route_entry);
+            }
+
+            rule_entry = nm_ip_routing_rule_new(AF_INET);
+            nm_ip_routing_rule_set_priority(rule_entry, 30200 + config_data->iface_idx);
+            nm_ip_routing_rule_set_from(rule_entry,
+                                        _nm_utils_inet4_ntop(config_data->ipv4s_arr[i], sbuf),
+                                        32);
+            nm_ip_routing_rule_set_table(rule_entry, 30200 + config_data->iface_idx);
+            nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
+            g_ptr_array_add(rules_new, rule_entry);
+        }
+
+        rule_entry = nm_ip_routing_rule_new(AF_INET);
+        nm_ip_routing_rule_set_priority(rule_entry, 30350);
+        nm_ip_routing_rule_set_table(rule_entry, RT_TABLE_MAIN);
+        nm_ip_routing_rule_set_suppress_prefixlength(rule_entry, 0);
+        nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
+        g_ptr_array_add(rules_new, rule_entry);
+
+        route_entry = nm_ip_route_new_binary(AF_INET, &nm_ip_addr_zero, 0, &gateway, 10, NULL);
+        nm_ip_route_set_attribute(route_entry,
+                                  NM_IP_ROUTE_ATTRIBUTE_TABLE,
+                                  g_variant_new_uint32(30400 + config_data->iface_idx));
+        g_ptr_array_add(routes_new, route_entry);
+
+        for (i = 0; i < config_data->ipv4s_len; i++) {
+            rule_entry = nm_ip_routing_rule_new(AF_INET);
+            nm_ip_routing_rule_set_priority(rule_entry, 30400 + config_data->iface_idx);
+            nm_ip_routing_rule_set_from(rule_entry,
+                                        _nm_utils_inet4_ntop(config_data->ipv4s_arr[i], sbuf),
+                                        32);
+            nm_ip_routing_rule_set_table(rule_entry, 30400 + config_data->iface_idx);
+            nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
+            g_ptr_array_add(rules_new, rule_entry);
+        }
+    }
+
+    for (i = 0; i < config_data->iproutes_len; ++i)
+        g_ptr_array_add(routes_new, config_data->iproutes_arr[i]);
+
+    addrs_changed = nmcs_setting_ip_replace_ipv4_addresses(s_ip,
+                                                           (NMIPAddress **) addrs_new->pdata,
+                                                           addrs_new->len);
+
+    routes_changed = nmcs_setting_ip_replace_ipv4_routes(s_ip,
+                                                         (NMIPRoute **) routes_new->pdata,
+                                                         routes_new->len);
+
+    rules_changed = nmcs_setting_ip_replace_ipv4_rules(s_ip,
+                                                       (NMIPRoutingRule **) rules_new->pdata,
+                                                       rules_new->len);
+
+    NM_SET_OUT(out_changed, addrs_changed || routes_changed || rules_changed);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_config_one(GCancellable *                     sigterm_cancellable,
+            NMClient *                         nmc,
+            const NMCSProviderGetConfigResult *result,
+            guint                              idx)
+{
+    const NMCSProviderGetConfigIfaceData *config_data = result->iface_datas_arr[idx];
+    const char *                          hwaddr      = config_data->hwaddr;
+    gs_unref_object NMDevice *device                  = NULL;
+    gs_unref_object NMConnection *applied_connection  = NULL;
+    guint64                       applied_version_id;
+    gs_free_error GError *error = NULL;
+    gboolean              changed;
+    gboolean              skipped_single_addr;
+    gboolean              version_id_changed;
+    guint                 try_count;
+    gboolean              any_changes = FALSE;
+
+    g_main_context_iteration(NULL, FALSE);
+
+    if (g_cancellable_is_cancelled(sigterm_cancellable))
+        return FALSE;
+
+    device = nm_g_object_ref(_nmc_get_device_by_hwaddr(nmc, hwaddr));
+    if (!device) {
+        _LOGD("config device %s: skip because device not found", hwaddr);
+        return FALSE;
+    }
+
+    if (!nmcs_provider_get_config_iface_data_is_valid(config_data)) {
+        _LOGD("config device %s: skip because meta data not successfully fetched", hwaddr);
+        return FALSE;
+    }
+
+    if (config_data->iface_idx >= 100) {
+        /* since we use the iface_idx to select a table number, the range is limited from
+         * 0 to 99. Note that the providers are required to provide increasing numbers,
+         * so this means we bail out after the first 100 devices.  */
+        _LOGD("config device %s: skip because number of supported interfaces reached", hwaddr);
+        return FALSE;
+    }
+
+    _LOGD("config device %s: configuring \"%s\" (%s)...",
+          hwaddr,
+          nm_device_get_iface(device) ?: "/unknown/",
+          nm_object_get_path(NM_OBJECT(device)));
+
+    try_count = 0;
+
+try_again:
+
+    applied_connection = nmcs_device_get_applied_connection(device,
+                                                            sigterm_cancellable,
+                                                            &applied_version_id,
+                                                            &error);
+    if (!applied_connection) {
+        if (!nm_utils_error_is_cancelled(error))
+            _LOGD("config device %s: device has no applied connection (%s). Skip",
+                  hwaddr,
+                  error->message);
+        return any_changes;
+    }
+
+    if (_nmc_skip_connection(applied_connection)) {
+        _LOGD("config device %s: skip applied connection due to user data %s",
+              hwaddr,
+              USER_TAG_SKIP);
+        return any_changes;
+    }
+
+    if (!_nmc_mangle_connection(device,
+                                applied_connection,
+                                result,
+                                config_data,
+                                &skipped_single_addr,
+                                &changed)) {
+        _LOGD("config device %s: device has no suitable applied connection. Skip", hwaddr);
+        return any_changes;
+    }
+
+    if (!changed) {
+        if (skipped_single_addr) {
+            _LOGD("config device %s: device needs no update to applied connection \"%s\" (%s) "
+                  "because there are not multiple IP addresses. Skip",
+                  hwaddr,
+                  nm_connection_get_id(applied_connection),
+                  nm_connection_get_uuid(applied_connection));
+        } else {
+            _LOGD(
+                "config device %s: device needs no update to applied connection \"%s\" (%s). Skip",
+                hwaddr,
+                nm_connection_get_id(applied_connection),
+                nm_connection_get_uuid(applied_connection));
+        }
+        return any_changes;
+    }
+
+    _LOGD("config device %s: reapply connection \"%s\" (%s)",
+          hwaddr,
+          nm_connection_get_id(applied_connection),
+          nm_connection_get_uuid(applied_connection));
+
+    /* we are about to call Reapply(). Even if that fails, it counts as if we changed something. */
+    any_changes = TRUE;
+
+    if (!nmcs_device_reapply(device,
+                             sigterm_cancellable,
+                             applied_connection,
+                             applied_version_id,
+                             &version_id_changed,
+                             &error)) {
+        if (version_id_changed && try_count < 5) {
+            _LOGD("config device %s: applied connection changed in the meantime. Retry...", hwaddr);
+            g_clear_object(&applied_connection);
+            g_clear_error(&error);
+            try_count++;
+            goto try_again;
+        }
+
+        if (!nm_utils_error_is_cancelled(error)) {
+            _LOGD("config device %s: failure to reapply connection \"%s\" (%s): %s",
+                  hwaddr,
+                  nm_connection_get_id(applied_connection),
+                  nm_connection_get_uuid(applied_connection),
+                  error->message);
+        }
+        return any_changes;
+    }
+
+    _LOGD("config device %s: connection \"%s\" (%s) reapplied",
+          hwaddr,
+          nm_connection_get_id(applied_connection),
+          nm_connection_get_uuid(applied_connection));
+
+    return any_changes;
+}
+
+static gboolean
+_config_all(GCancellable *                     sigterm_cancellable,
+            NMClient *                         nmc,
+            const NMCSProviderGetConfigResult *result)
+{
+    gboolean any_changes = FALSE;
+    guint    i;
+
+    for (i = 0; i < result->n_iface_datas; i++) {
+        if (_config_one(sigterm_cancellable, nmc, result, i))
+            any_changes = TRUE;
+    }
+
+    return any_changes;
+}
+
+/*****************************************************************************/
+
+static gboolean
+sigterm_handler(gpointer user_data)
+{
+    GCancellable *sigterm_cancellable = user_data;
+
+    if (!g_cancellable_is_cancelled(sigterm_cancellable)) {
+        _LOGD("SIGTERM received");
+        g_cancellable_cancel(user_data);
+    } else
+        _LOGD("SIGTERM received (again)");
+    return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+int
+main(int argc, const char *const *argv)
+{
+    gs_unref_object GCancellable *    sigterm_cancellable                            = NULL;
+    nm_auto_destroy_and_unref_gsource GSource *sigterm_source                        = NULL;
+    gs_unref_object NMCSProvider *provider                                           = NULL;
+    gs_unref_object NMClient *                   nmc                                 = NULL;
+    nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL;
+    gs_free_error GError *error                                                      = NULL;
+
+    _nm_logging_enabled_init(g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_LOG")));
+
+    _LOGD("nm-cloud-setup %s starting...", NM_DIST_VERSION);
+
+    if (argc != 1) {
+        g_printerr("%s: no command line arguments supported\n", argv[0]);
+        return EXIT_FAILURE;
+    }
+
+    sigterm_cancellable = g_cancellable_new();
+
+    sigterm_source = nm_g_source_attach(nm_g_unix_signal_source_new(SIGTERM,
+                                                                    G_PRIORITY_DEFAULT,
+                                                                    sigterm_handler,
+                                                                    sigterm_cancellable,
+                                                                    NULL),
+                                        NULL);
+
+    provider = _provider_detect(sigterm_cancellable);
+    if (!provider)
+        goto done;
+
+    nmc_client_new_waitsync(sigterm_cancellable,
+                            &nmc,
+                            &error,
+                            NM_CLIENT_INSTANCE_FLAGS,
+                            (guint) NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS,
+                            NULL);
+
+    nmcs_wait_for_objects_register(nmc);
+    nmcs_wait_for_objects_register(nm_client_get_context_busy_watcher(nmc));
+
+    if (error) {
+        if (!nm_utils_error_is_cancelled(error))
+            _LOGI("failure to talk to NetworkManager: %s", error->message);
+        goto done;
+    }
+
+    if (!nm_client_get_nm_running(nmc)) {
+        _LOGI("NetworkManager is not running");
+        goto done;
+    }
+
+    result = _get_config(sigterm_cancellable, provider, nmc);
+    if (!result)
+        goto done;
+
+    if (_config_all(sigterm_cancellable, nmc, result))
+        _LOGI("some changes were applied for provider %s", nmcs_provider_get_name(provider));
+    else
+        _LOGD("no changes were applied for provider %s", nmcs_provider_get_name(provider));
+
+done:
+    nm_clear_pointer(&result, nmcs_provider_get_config_result_free);
+    g_clear_object(&nmc);
+    g_clear_object(&provider);
+
+    if (!nmcs_wait_for_objects_iterate_until_done(NULL, 2000)) {
+        _LOGE("shutdown: timeout waiting to application to quit. This is a bug");
+        nm_assert_not_reached();
+    }
+
+    nm_clear_g_source_inst(&sigterm_source);
+    g_clear_object(&sigterm_cancellable);
+
+    return 0;
+}
diff --git a/src/nm-cloud-setup/meson.build b/src/nm-cloud-setup/meson.build
new file mode 100644
index 00000000..ea4ad113
--- /dev/null
+++ b/src/nm-cloud-setup/meson.build
@@ -0,0 +1,67 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+if install_systemdunitdir
+
+  nm_cloud_setup_service = configure_file(
+    input: 'nm-cloud-setup.service.in',
+    output: '@BASENAME@',
+    install_dir: systemd_systemdsystemunitdir,
+    configuration: data_conf,
+  )
+
+  install_data(
+    'nm-cloud-setup.timer',
+    install_dir: systemd_systemdsystemunitdir,
+  )
+
+  install_data(
+    '90-nm-cloud-setup.sh',
+    install_dir: join_paths(nm_pkglibdir, 'dispatcher.d', 'no-wait.d'),
+  )
+
+endif
+
+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-aliyun.c',
+    'nmcs-provider.c',
+  ),
+  dependencies: [
+    libnm_dep,
+    glib_dep,
+    libcurl_dep,
+  ],
+)
+
+executable(
+  'nm-cloud-setup',
+  files(
+    'main.c',
+  ),
+  dependencies: [
+    libnm_dep,
+    glib_dep,
+    libcurl_dep,
+  ],
+  link_with: [
+    libnm_cloud_setup_core,
+    libnmc_base,
+    libnm_client_aux_extern,
+    libnm_core_aux_extern,
+    libnm_core_aux_intern,
+    libnm_log_null,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+  link_args: ldflags_linker_script_binary,
+  link_depends: linker_script_binary,
+  install: true,
+  install_dir: nm_libexecdir,
+)
diff --git a/src/nm-cloud-setup/nm-cloud-setup-utils.c b/src/nm-cloud-setup/nm-cloud-setup-utils.c
new file mode 100644
index 00000000..ed36b4dd
--- /dev/null
+++ b/src/nm-cloud-setup/nm-cloud-setup-utils.c
@@ -0,0 +1,916 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-cloud-setup-utils.h"
+
+#include <linux/if_ether.h>
+#include <linux/if_infiniband.h>
+
+#include "libnm-glib-aux/nm-time-utils.h"
+#include "libnm-glib-aux/nm-logging-base.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+
+/*****************************************************************************/
+
+volatile NMLogLevel _nm_logging_configured_level = LOGL_TRACE;
+
+void
+_nm_logging_enabled_init(const char *level_str)
+{
+    NMLogLevel level;
+
+    if (!_nm_log_parse_level(level_str, &level))
+        level = LOGL_WARN;
+    else if (level == _LOGL_KEEP)
+        level = LOGL_WARN;
+
+    _nm_logging_configured_level = level;
+}
+
+void
+_nm_log_impl_cs(NMLogLevel level, const char *fmt, ...)
+{
+    gs_free char *msg = NULL;
+    va_list       ap;
+    const char *  level_str;
+    gint64        ts;
+
+    va_start(ap, fmt);
+    msg = g_strdup_vprintf(fmt, ap);
+    va_end(ap);
+
+    switch (level) {
+    case LOGL_TRACE:
+        level_str = "<trace>";
+        break;
+    case LOGL_DEBUG:
+        level_str = "<debug>";
+        break;
+    case LOGL_INFO:
+        level_str = "<info> ";
+        break;
+    case LOGL_WARN:
+        level_str = "<warn> ";
+        break;
+    default:
+        nm_assert(level == LOGL_ERR);
+        level_str = "<error>";
+        break;
+    }
+
+    ts = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
+
+    g_print("[%" G_GINT64_FORMAT ".%05" G_GINT64_FORMAT "] %s %s\n",
+            ts / NM_UTILS_NSEC_PER_SEC,
+            (ts / (NM_UTILS_NSEC_PER_SEC / 10000)) % 10000,
+            level_str,
+            msg);
+}
+
+void
+_nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
+                                          gint64                 offset_sec,
+                                          gboolean               is_boottime)
+{}
+
+/*****************************************************************************/
+
+G_LOCK_DEFINE_STATIC(_wait_for_objects_lock);
+static GSList *_wait_for_objects_list;
+static GSList *_wait_for_objects_iterate_loops;
+
+static void
+_wait_for_objects_maybe_quit_mainloops_with_lock(void)
+{
+    GSList *iter;
+
+    if (!_wait_for_objects_list) {
+        for (iter = _wait_for_objects_iterate_loops; iter; iter = iter->next)
+            g_main_loop_quit(iter->data);
+    }
+}
+
+static void
+_wait_for_objects_weak_cb(gpointer data, GObject *where_the_object_was)
+{
+    G_LOCK(_wait_for_objects_lock);
+    nm_assert(g_slist_find(_wait_for_objects_list, where_the_object_was));
+    _wait_for_objects_list = g_slist_remove(_wait_for_objects_list, where_the_object_was);
+    _wait_for_objects_maybe_quit_mainloops_with_lock();
+    G_UNLOCK(_wait_for_objects_lock);
+}
+
+/**
+ * nmcs_wait_for_objects_register:
+ * @target: a #GObject to wait for.
+ *
+ * Registers @target as a pointer to wait during shutdown. Using
+ * nmcs_wait_for_objects_iterate_until_done() we keep waiting until
+ * @target gets destroyed, which means that it gets completely unreferenced.
+ */
+gpointer
+nmcs_wait_for_objects_register(gpointer target)
+{
+    g_return_val_if_fail(G_IS_OBJECT(target), NULL);
+
+    G_LOCK(_wait_for_objects_lock);
+    _wait_for_objects_list = g_slist_prepend(_wait_for_objects_list, target);
+    G_UNLOCK(_wait_for_objects_lock);
+
+    g_object_weak_ref(target, _wait_for_objects_weak_cb, NULL);
+    return target;
+}
+
+typedef struct {
+    GMainLoop *loop;
+    gboolean   got_timeout;
+} WaitForObjectsData;
+
+static gboolean
+_wait_for_objects_iterate_until_done_timeout_cb(gpointer user_data)
+{
+    WaitForObjectsData *data = user_data;
+
+    data->got_timeout = TRUE;
+    g_main_loop_quit(data->loop);
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_wait_for_objects_iterate_until_done_idle_cb(gpointer user_data)
+{
+    /* This avoids a race where:
+     *
+     *   - we check whether there are objects to wait for.
+     *   - the last object to wait for gets removed (issuing g_main_loop_quit()).
+     *   - we run the mainloop (and missed our signal).
+     *
+     * It's really a missing feature of GMainLoop where the "is-running" flag is always set to
+     * TRUE by g_main_loop_run(). That means, you cannot catch a g_main_loop_quit() in a race
+     * free way while not iterating the loop.
+     *
+     * Avoid this, by checking once again after we start running the mainloop.
+     */
+
+    G_LOCK(_wait_for_objects_lock);
+    _wait_for_objects_maybe_quit_mainloops_with_lock();
+    G_UNLOCK(_wait_for_objects_lock);
+    return G_SOURCE_REMOVE;
+}
+
+/**
+ * nmcs_wait_for_objects_iterate_until_done:
+ * @context: the #GMainContext to iterate.
+ * @timeout_msec: timeout or -1 for no timeout.
+ *
+ * Iterates the provided @context until all objects that we wait for
+ * are destroyed.
+ *
+ * The purpose of this is to cleanup all objects that we have on exit. That
+ * is especially because objects have asynchronous operations pending that
+ * should be cancelled and properly completed during exit.
+ *
+ * Returns: %FALSE on timeout or %TRUE if all objects destroyed before timeout.
+ */
+gboolean
+nmcs_wait_for_objects_iterate_until_done(GMainContext *context, int timeout_msec)
+{
+    nm_auto_unref_gmainloop GMainLoop *loop                   = g_main_loop_new(context, FALSE);
+    nm_auto_destroy_and_unref_gsource GSource *timeout_source = NULL;
+    WaitForObjectsData                         data;
+    gboolean                                   has_more_objects;
+
+    G_LOCK(_wait_for_objects_lock);
+    if (!_wait_for_objects_list) {
+        G_UNLOCK(_wait_for_objects_lock);
+        return TRUE;
+    }
+    _wait_for_objects_iterate_loops = g_slist_prepend(_wait_for_objects_iterate_loops, loop);
+    G_UNLOCK(_wait_for_objects_lock);
+
+    data = (WaitForObjectsData){
+        .loop        = loop,
+        .got_timeout = FALSE,
+    };
+
+    if (timeout_msec >= 0) {
+        timeout_source = nm_g_source_attach(
+            nm_g_timeout_source_new(timeout_msec,
+                                    G_PRIORITY_DEFAULT,
+                                    _wait_for_objects_iterate_until_done_timeout_cb,
+                                    &data,
+                                    NULL),
+            context);
+    }
+
+    has_more_objects = TRUE;
+    while (has_more_objects && !data.got_timeout) {
+        nm_auto_destroy_and_unref_gsource GSource *idle_source = NULL;
+
+        idle_source =
+            nm_g_source_attach(nm_g_idle_source_new(G_PRIORITY_DEFAULT,
+                                                    _wait_for_objects_iterate_until_done_idle_cb,
+                                                    &data,
+                                                    NULL),
+                               context);
+
+        g_main_loop_run(loop);
+
+        G_LOCK(_wait_for_objects_lock);
+        has_more_objects = (!!_wait_for_objects_list);
+        if (data.got_timeout || !has_more_objects)
+            _wait_for_objects_iterate_loops = g_slist_remove(_wait_for_objects_iterate_loops, loop);
+        G_UNLOCK(_wait_for_objects_lock);
+    }
+
+    return !data.got_timeout;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GTask *                     task;
+    GSource *                   source_timeout;
+    GSource *                   source_next_poll;
+    GMainContext *              context;
+    GCancellable *              internal_cancellable;
+    NMCSUtilsPollProbeStartFcn  probe_start_fcn;
+    NMCSUtilsPollProbeFinishFcn probe_finish_fcn;
+    gpointer                    probe_user_data;
+    gulong                      cancellable_id;
+    gint64                      last_poll_start_ms;
+    int                         sleep_timeout_ms;
+    int                         ratelimit_timeout_ms;
+    bool                        completed : 1;
+} PollTaskData;
+
+static void
+_poll_task_data_free(gpointer data)
+{
+    PollTaskData *poll_task_data = data;
+
+    nm_assert(G_IS_TASK(poll_task_data->task));
+    nm_assert(!poll_task_data->source_next_poll);
+    nm_assert(!poll_task_data->source_timeout);
+    nm_assert(poll_task_data->cancellable_id == 0);
+
+    g_main_context_unref(poll_task_data->context);
+
+    nm_g_slice_free(poll_task_data);
+}
+
+static void
+_poll_return(PollTaskData *poll_task_data, GError *error_take)
+{
+    nm_clear_g_source_inst(&poll_task_data->source_next_poll);
+    nm_clear_g_source_inst(&poll_task_data->source_timeout);
+    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(poll_task_data->task),
+                                      &poll_task_data->cancellable_id);
+
+    nm_clear_g_cancellable(&poll_task_data->internal_cancellable);
+
+    if (error_take)
+        g_task_return_error(poll_task_data->task, g_steal_pointer(&error_take));
+    else
+        g_task_return_boolean(poll_task_data->task, TRUE);
+
+    g_object_unref(poll_task_data->task);
+}
+
+static gboolean _poll_start_cb(gpointer user_data);
+
+static void
+_poll_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    PollTaskData *             poll_task_data = user_data;
+    _nm_unused gs_unref_object GTask *task =
+        poll_task_data->task; /* balance ref from _poll_start_cb() */
+    gs_free_error GError *error = NULL;
+    gint64                now_ms;
+    gint64                wait_ms;
+    gboolean              is_finished;
+
+    is_finished =
+        poll_task_data->probe_finish_fcn(source, result, poll_task_data->probe_user_data, &error);
+
+    if (nm_utils_error_is_cancelled(error)) {
+        /* we already handle this differently. Nothing to do. */
+        return;
+    }
+
+    if (error || is_finished) {
+        _poll_return(poll_task_data, g_steal_pointer(&error));
+        return;
+    }
+
+    now_ms = nm_utils_get_monotonic_timestamp_msec();
+    if (poll_task_data->ratelimit_timeout_ms > 0)
+        wait_ms =
+            (poll_task_data->last_poll_start_ms + poll_task_data->ratelimit_timeout_ms) - now_ms;
+    else
+        wait_ms = 0;
+    if (poll_task_data->sleep_timeout_ms > 0)
+        wait_ms = MAX(wait_ms, poll_task_data->sleep_timeout_ms);
+
+    poll_task_data->source_next_poll =
+        nm_g_source_attach(nm_g_timeout_source_new(MAX(1, wait_ms),
+                                                   G_PRIORITY_DEFAULT,
+                                                   _poll_start_cb,
+                                                   poll_task_data,
+                                                   NULL),
+                           poll_task_data->context);
+}
+
+static gboolean
+_poll_start_cb(gpointer user_data)
+{
+    PollTaskData *poll_task_data = user_data;
+
+    nm_clear_g_source_inst(&poll_task_data->source_next_poll);
+
+    poll_task_data->last_poll_start_ms = nm_utils_get_monotonic_timestamp_msec();
+
+    g_object_ref(poll_task_data->task); /* balanced by _poll_done_cb() */
+
+    poll_task_data->probe_start_fcn(poll_task_data->internal_cancellable,
+                                    poll_task_data->probe_user_data,
+                                    _poll_done_cb,
+                                    poll_task_data);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_poll_timeout_cb(gpointer user_data)
+{
+    PollTaskData *poll_task_data = user_data;
+
+    _poll_return(poll_task_data, nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "timeout expired"));
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_poll_cancelled_cb(GObject *object, gpointer user_data)
+{
+    PollTaskData *poll_task_data = user_data;
+    GError *      error          = NULL;
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(poll_task_data->task),
+                              &poll_task_data->cancellable_id);
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    _poll_return(poll_task_data, error);
+}
+
+/**
+ * nmcs_utils_poll:
+ * @poll_timeout_ms: if >= 0, then this is the overall timeout for how long we poll.
+ *   When this timeout expires, the request completes with failure (and error set).
+ * @ratelimit_timeout_ms: if > 0, we ratelimit the starts from one prope_start_fcn
+ *   call to the next.
+ * @sleep_timeout_ms: if > 0, then we wait after a probe finished this timeout
+ *   before the next. Together with @ratelimit_timeout_ms this determines how
+ *   frequently we probe.
+ * @probe_start_fcn: used to start a (asynchronous) probe. A probe must be completed
+ *   by calling the provided callback. While a probe is in progress, we will not
+ *   start another. This function is already invoked the first time synchronously,
+ *   during nmcs_utils_poll().
+ * @probe_finish_fcn: will be called from the callback of @probe_start_fcn. If the
+ *   function returns %TRUE (polling done) or an error, polling stops. Otherwise,
+ *   another poll will be started.
+ * @probe_user_data: user_data for the probe functions.
+ * @cancellable: cancellable for polling.
+ * @callback: when polling completes.
+ * @user_data: for @callback.
+ *
+ * This uses the current g_main_context_get_thread_default() for scheduling
+ * actions.
+ */
+void
+nmcs_utils_poll(int                         poll_timeout_ms,
+                int                         ratelimit_timeout_ms,
+                int                         sleep_timeout_ms,
+                NMCSUtilsPollProbeStartFcn  probe_start_fcn,
+                NMCSUtilsPollProbeFinishFcn probe_finish_fcn,
+                gpointer                    probe_user_data,
+                GCancellable *              cancellable,
+                GAsyncReadyCallback         callback,
+                gpointer                    user_data)
+{
+    PollTaskData *poll_task_data;
+
+    poll_task_data  = g_slice_new(PollTaskData);
+    *poll_task_data = (PollTaskData){
+        .task             = nm_g_task_new(NULL, cancellable, nmcs_utils_poll, callback, user_data),
+        .probe_start_fcn  = probe_start_fcn,
+        .probe_finish_fcn = probe_finish_fcn,
+        .probe_user_data  = probe_user_data,
+        .completed        = FALSE,
+        .context          = g_main_context_ref_thread_default(),
+        .sleep_timeout_ms = sleep_timeout_ms,
+        .ratelimit_timeout_ms = ratelimit_timeout_ms,
+        .internal_cancellable = g_cancellable_new(),
+    };
+
+    nmcs_wait_for_objects_register(poll_task_data->task);
+
+    g_task_set_task_data(poll_task_data->task, poll_task_data, _poll_task_data_free);
+
+    if (poll_timeout_ms >= 0) {
+        poll_task_data->source_timeout =
+            nm_g_source_attach(nm_g_timeout_source_new(poll_timeout_ms,
+                                                       G_PRIORITY_DEFAULT,
+                                                       _poll_timeout_cb,
+                                                       poll_task_data,
+                                                       NULL),
+                               poll_task_data->context);
+    }
+
+    poll_task_data->source_next_poll = nm_g_source_attach(
+        nm_g_idle_source_new(G_PRIORITY_DEFAULT, _poll_start_cb, poll_task_data, NULL),
+        poll_task_data->context);
+
+    if (cancellable) {
+        gulong signal_id;
+
+        signal_id = g_cancellable_connect(cancellable,
+                                          G_CALLBACK(_poll_cancelled_cb),
+                                          poll_task_data,
+                                          NULL);
+        if (signal_id == 0) {
+            /* the request is already cancelled. Return. */
+            return;
+        }
+        poll_task_data->cancellable_id = signal_id;
+    }
+}
+
+/**
+ * nmcs_utils_poll_finish:
+ * @result: the GAsyncResult from the GAsyncReadyCallback callback.
+ * @probe_user_data: the user data provided to nmcs_utils_poll().
+ * @error: the failure code.
+ *
+ * Returns: %TRUE if the polling completed with success. In that case,
+ *   the error won't be set.
+ *   If the request was cancelled, this is indicated by @error and
+ *   %FALSE will be returned.
+ *   If the probe returned a failure, this returns %FALSE and the error
+ *   provided by @probe_finish_fcn.
+ *   If the request times out, this returns %FALSE with error set.
+ *   Error is always set if (and only if) the function returns %FALSE.
+ */
+gboolean
+nmcs_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data, GError **error)
+{
+    GTask *       task;
+    PollTaskData *poll_task_data;
+
+    g_return_val_if_fail(nm_g_task_is_valid(result, NULL, nmcs_utils_poll), FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    task = G_TASK(result);
+
+    if (probe_user_data) {
+        poll_task_data = g_task_get_task_data(task);
+        NM_SET_OUT(probe_user_data, poll_task_data->probe_user_data);
+    }
+
+    return g_task_propagate_boolean(task, error);
+}
+
+/*****************************************************************************/
+
+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 {
+        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;
+        }
+    }
+
+    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(hw,
+                                  FALSE,
+                                  FALSE,
+                                  FALSE,
+                                  ":-",
+                                  sizeof(buf),
+                                  buf,
+                                  sizeof(buf),
+                                  NULL))
+        return NULL;
+
+    return nm_utils_hwaddr_ntoa(buf, sizeof(buf));
+}
+
+/*****************************************************************************/
+
+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)
+{
+    const char *mem_data;
+    gsize       mem_size;
+
+    g_return_val_if_fail(mem, NULL);
+    g_return_val_if_fail(needle, NULL);
+
+    mem_data = g_bytes_get_data(mem, &mem_size);
+    return memmem(mem_data, mem_size, needle, strlen(needle));
+}
+
+const char *
+nmcs_utils_parse_get_full_line(GBytes *mem, const char *needle)
+{
+    const char *mem_data;
+    gsize       mem_size;
+    gsize       c;
+    gsize       l;
+
+    const char *line;
+
+    line = nmcs_utils_parse_memmem(mem, needle);
+    if (!line)
+        return NULL;
+
+    mem_data = g_bytes_get_data(mem, &mem_size);
+
+    if (line != mem_data && line[-1] != '\n') {
+        /* the line must be preceeded either by the begin of the data or
+         * by a newline. */
+        return NULL;
+    }
+
+    c = mem_size - (line - mem_data);
+    l = strlen(needle);
+
+    if (c != l && line[l] != '\n') {
+        /* the end of the needle must be either a newline or the end of the buffer. */
+        return NULL;
+    }
+
+    return line;
+}
+
+/*****************************************************************************/
+
+char *
+nmcs_utils_uri_build_concat_v(const char *base, const char **components, gsize n_components)
+{
+    NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
+
+    nm_assert(base);
+    nm_assert(base[0]);
+    nm_assert(!NM_STR_HAS_SUFFIX(base, "/"));
+
+    nm_str_buf_append(&strbuf, base);
+
+    if (n_components > 0 && components[0] && components[0][0] == '/') {
+        /* the first component starts with a slash. We allow that, and don't add a duplicate
+         * slash. Otherwise, we add a separator after base.
+         *
+         * We only do that for the first component. */
+    } else
+        nm_str_buf_append_c(&strbuf, '/');
+
+    while (n_components > 0) {
+        if (!components[0]) {
+            /* we allow NULL, to indicate nothing to append */
+        } else
+            nm_str_buf_append(&strbuf, components[0]);
+        components++;
+        n_components--;
+    }
+
+    return nm_str_buf_finalize(&strbuf, NULL);
+}
+
+const char *
+nmcs_utils_uri_complete_interned(const char *uri)
+{
+    gs_free char *s = NULL;
+
+    if (nm_str_is_empty(uri))
+        return NULL;
+    if (NM_STR_HAS_PREFIX(uri, "http://") || NM_STR_HAS_PREFIX(uri, "https://") || strchr(uri, '/'))
+        return g_intern_string(uri);
+
+    s = g_strconcat("http://", uri, NULL);
+    return g_intern_string(s);
+}
+
+/*****************************************************************************/
+
+gboolean
+nmcs_setting_ip_replace_ipv4_addresses(NMSettingIPConfig *s_ip,
+                                       NMIPAddress **     entries_arr,
+                                       guint              entries_len)
+{
+    gboolean any_changes = FALSE;
+    guint    i_next;
+    guint    num;
+    guint    i;
+
+    num = nm_setting_ip_config_get_num_addresses(s_ip);
+
+    i_next = 0;
+
+    for (i = 0; i < entries_len; i++) {
+        NMIPAddress *entry = entries_arr[i];
+
+        if (!any_changes) {
+            if (i_next < num) {
+                if (nm_ip_address_cmp_full(entry,
+                                           nm_setting_ip_config_get_address(s_ip, i_next),
+                                           NM_IP_ADDRESS_CMP_FLAGS_WITH_ATTRS)
+                    == 0) {
+                    i_next++;
+                    continue;
+                }
+            }
+            while (i_next < num)
+                nm_setting_ip_config_remove_address(s_ip, --num);
+            any_changes = TRUE;
+        }
+
+        if (!nm_setting_ip_config_add_address(s_ip, entry))
+            continue;
+
+        i_next++;
+    }
+    if (!any_changes) {
+        while (i_next < num) {
+            nm_setting_ip_config_remove_address(s_ip, --num);
+            any_changes = TRUE;
+        }
+    }
+
+    return any_changes;
+}
+
+gboolean
+nmcs_setting_ip_replace_ipv4_routes(NMSettingIPConfig *s_ip,
+                                    NMIPRoute **       entries_arr,
+                                    guint              entries_len)
+{
+    gboolean any_changes = FALSE;
+    guint    i_next;
+    guint    num;
+    guint    i;
+
+    num = nm_setting_ip_config_get_num_routes(s_ip);
+
+    i_next = 0;
+
+    for (i = 0; i < entries_len; i++) {
+        NMIPRoute *entry = entries_arr[i];
+
+        if (!any_changes) {
+            if (i_next < num) {
+                if (nm_ip_route_equal_full(entry,
+                                           nm_setting_ip_config_get_route(s_ip, i_next),
+                                           NM_IP_ROUTE_EQUAL_CMP_FLAGS_WITH_ATTRS)) {
+                    i_next++;
+                    continue;
+                }
+            }
+            while (i_next < num)
+                nm_setting_ip_config_remove_route(s_ip, --num);
+            any_changes = TRUE;
+        }
+
+        if (!nm_setting_ip_config_add_route(s_ip, entry))
+            continue;
+
+        i_next++;
+    }
+    if (!any_changes) {
+        while (i_next < num) {
+            nm_setting_ip_config_remove_route(s_ip, --num);
+            any_changes = TRUE;
+        }
+    }
+
+    return any_changes;
+}
+
+gboolean
+nmcs_setting_ip_replace_ipv4_rules(NMSettingIPConfig *s_ip,
+                                   NMIPRoutingRule ** entries_arr,
+                                   guint              entries_len)
+{
+    gboolean any_changes = FALSE;
+    guint    i_next;
+    guint    num;
+    guint    i;
+
+    num = nm_setting_ip_config_get_num_routing_rules(s_ip);
+
+    i_next = 0;
+
+    for (i = 0; i < entries_len; i++) {
+        NMIPRoutingRule *entry = entries_arr[i];
+
+        if (!any_changes) {
+            if (i_next < num) {
+                if (nm_ip_routing_rule_cmp(entry,
+                                           nm_setting_ip_config_get_routing_rule(s_ip, i_next))
+                    == 0) {
+                    i_next++;
+                    continue;
+                }
+            }
+            while (i_next < num)
+                nm_setting_ip_config_remove_routing_rule(s_ip, --num);
+            any_changes = TRUE;
+        }
+
+        nm_setting_ip_config_add_routing_rule(s_ip, entry);
+        i_next++;
+    }
+    if (!any_changes) {
+        while (i_next < num) {
+            nm_setting_ip_config_remove_routing_rule(s_ip, --num);
+            any_changes = TRUE;
+        }
+    }
+
+    return any_changes;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainLoop *   main_loop;
+    NMConnection *connection;
+    GError *      error;
+    guint64       version_id;
+} DeviceGetAppliedConnectionData;
+
+static void
+_nmcs_device_get_applied_connection_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    DeviceGetAppliedConnectionData *data = user_data;
+
+    data->connection = nm_device_get_applied_connection_finish(NM_DEVICE(source),
+                                                               result,
+                                                               &data->version_id,
+                                                               &data->error);
+    g_main_loop_quit(data->main_loop);
+}
+
+NMConnection *
+nmcs_device_get_applied_connection(NMDevice *    device,
+                                   GCancellable *cancellable,
+                                   guint64 *     version_id,
+                                   GError **     error)
+{
+    nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
+    DeviceGetAppliedConnectionData     data      = {
+        .main_loop = main_loop,
+    };
+
+    nm_device_get_applied_connection_async(device,
+                                           0,
+                                           cancellable,
+                                           _nmcs_device_get_applied_connection_cb,
+                                           &data);
+
+    g_main_loop_run(main_loop);
+
+    if (data.error)
+        g_propagate_error(error, data.error);
+    NM_SET_OUT(version_id, data.version_id);
+    return data.connection;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainLoop *main_loop;
+    GError *   error;
+} DeviceReapplyData;
+
+static void
+_nmcs_device_reapply_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    DeviceReapplyData *data = user_data;
+
+    nm_device_reapply_finish(NM_DEVICE(source), result, &data->error);
+    g_main_loop_quit(data->main_loop);
+}
+
+gboolean
+nmcs_device_reapply(NMDevice *    device,
+                    GCancellable *sigterm_cancellable,
+                    NMConnection *connection,
+                    guint64       version_id,
+                    gboolean *    out_version_id_changed,
+                    GError **     error)
+{
+    nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
+    DeviceReapplyData                  data      = {
+        .main_loop = main_loop,
+    };
+
+    nm_device_reapply_async(device,
+                            connection,
+                            version_id,
+                            0,
+                            sigterm_cancellable,
+                            _nmcs_device_reapply_cb,
+                            &data);
+
+    g_main_loop_run(main_loop);
+
+    if (data.error) {
+        NM_SET_OUT(
+            out_version_id_changed,
+            g_error_matches(data.error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_VERSION_ID_MISMATCH));
+        g_propagate_error(error, data.error);
+        return FALSE;
+    }
+
+    NM_SET_OUT(out_version_id_changed, FALSE);
+    return TRUE;
+}
diff --git a/src/nm-cloud-setup/nm-cloud-setup-utils.h b/src/nm-cloud-setup/nm-cloud-setup-utils.h
new file mode 100644
index 00000000..3ca7298f
--- /dev/null
+++ b/src/nm-cloud-setup/nm-cloud-setup-utils.h
@@ -0,0 +1,154 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_CLOUD_SETUP_UTILS_H__
+#define __NM_CLOUD_SETUP_UTILS_H__
+
+#include "libnm-glib-aux/nm-logging-fwd.h"
+
+/*****************************************************************************/
+
+/* mark names for  variables that can be used as configuration. Search
+ * for NMCS_ENV_VARIABLE() to find all honored environment variables. */
+#define NMCS_ENV_VARIABLE(var) "" var ""
+
+/*****************************************************************************/
+
+extern volatile NMLogLevel _nm_logging_configured_level;
+
+static inline gboolean
+nm_logging_enabled(NMLogLevel level)
+{
+    return level >= _nm_logging_configured_level;
+}
+
+void _nm_logging_enabled_init(const char *level_str);
+
+void _nm_log_impl_cs(NMLogLevel level, const char *fmt, ...) _nm_printf(2, 3);
+
+#define _nm_log(level, ...) _nm_log_impl_cs((level), __VA_ARGS__);
+
+#define _NMLOG(level, ...)                 \
+    G_STMT_START                           \
+    {                                      \
+        const NMLogLevel _level = (level); \
+                                           \
+        if (nm_logging_enabled(_level)) {  \
+            _nm_log(_level, __VA_ARGS__);  \
+        }                                  \
+    }                                      \
+    G_STMT_END
+
+/*****************************************************************************/
+
+#ifndef NM_DIST_VERSION
+    #define NM_DIST_VERSION VERSION
+#endif
+
+/*****************************************************************************/
+
+gpointer nmcs_wait_for_objects_register(gpointer target);
+
+gboolean nmcs_wait_for_objects_iterate_until_done(GMainContext *context, int timeout_msec);
+
+/*****************************************************************************/
+
+typedef void (*NMCSUtilsPollProbeStartFcn)(GCancellable *      cancellable,
+                                           gpointer            probe_user_data,
+                                           GAsyncReadyCallback callback,
+                                           gpointer            user_data);
+
+typedef gboolean (*NMCSUtilsPollProbeFinishFcn)(GObject *     source,
+                                                GAsyncResult *result,
+                                                gpointer      probe_user_data,
+                                                GError **     error);
+
+void nmcs_utils_poll(int                         poll_timeout_ms,
+                     int                         ratelimit_timeout_ms,
+                     int                         sleep_timeout_ms,
+                     NMCSUtilsPollProbeStartFcn  probe_start_fcn,
+                     NMCSUtilsPollProbeFinishFcn probe_finish_fcn,
+                     gpointer                    probe_user_data,
+                     GCancellable *              cancellable,
+                     GAsyncReadyCallback         callback,
+                     gpointer                    user_data);
+
+gboolean nmcs_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data, GError **error);
+
+/*****************************************************************************/
+
+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);
+
+const char *nmcs_utils_parse_get_full_line(GBytes *mem, const char *needle);
+
+/*****************************************************************************/
+
+char *nmcs_utils_uri_build_concat_v(const char *base, const char **components, gsize n_components);
+
+#define nmcs_utils_uri_build_concat(base, ...) \
+    nmcs_utils_uri_build_concat_v(base, ((const char *[]){__VA_ARGS__}), NM_NARG(__VA_ARGS__))
+
+const char *nmcs_utils_uri_complete_interned(const char *uri);
+
+/*****************************************************************************/
+
+gboolean nmcs_setting_ip_replace_ipv4_addresses(NMSettingIPConfig *s_ip,
+                                                NMIPAddress **     entries_arr,
+                                                guint              entries_len);
+
+gboolean nmcs_setting_ip_replace_ipv4_routes(NMSettingIPConfig *s_ip,
+                                             NMIPRoute **       entries_arr,
+                                             guint              entries_len);
+
+gboolean nmcs_setting_ip_replace_ipv4_rules(NMSettingIPConfig *s_ip,
+                                            NMIPRoutingRule ** entries_arr,
+                                            guint              entries_len);
+
+/*****************************************************************************/
+
+NMConnection *nmcs_device_get_applied_connection(NMDevice *    device,
+                                                 GCancellable *cancellable,
+                                                 guint64 *     version_id,
+                                                 GError **     error);
+
+gboolean nmcs_device_reapply(NMDevice *    device,
+                             GCancellable *sigterm_cancellable,
+                             NMConnection *connection,
+                             guint64       version_id,
+                             gboolean *    out_version_id_changed,
+                             GError **     error);
+
+#endif /* __NM_CLOUD_SETUP_UTILS_H__ */
diff --git a/src/nm-cloud-setup/nm-cloud-setup.service.in b/src/nm-cloud-setup/nm-cloud-setup.service.in
new file mode 100644
index 00000000..f4b0e263
--- /dev/null
+++ b/src/nm-cloud-setup/nm-cloud-setup.service.in
@@ -0,0 +1,43 @@
+[Unit]
+Description=Automatically configure NetworkManager in cloud
+Documentation=man:nm-cloud-setup(8)
+After=NetworkManager.service
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/nm-cloud-setup
+
+#Environment=NM_CLOUD_SETUP_LOG=TRACE
+
+# 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
+#Environment=NM_CLOUD_SETUP_ALIYUN=yes
+
+CapabilityBoundingSet=
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+PrivateDevices=yes
+PrivateTmp=yes
+ProtectControlGroups=yes
+ProtectHome=yes
+ProtectHostname=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectSystem=strict
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+SystemCallFilter=@system-service
+
+[Install]
+WantedBy=NetworkManager.service
diff --git a/src/nm-cloud-setup/nm-cloud-setup.timer b/src/nm-cloud-setup/nm-cloud-setup.timer
new file mode 100644
index 00000000..fd1722a6
--- /dev/null
+++ b/src/nm-cloud-setup/nm-cloud-setup.timer
@@ -0,0 +1,9 @@
+[Unit]
+Description=Periodically run nm-cloud-setup
+
+[Timer]
+OnBootSec=5min
+OnUnitActiveSec=5min
+
+[Install]
+WantedBy=timers.target
diff --git a/src/nm-cloud-setup/nm-http-client.c b/src/nm-cloud-setup/nm-http-client.c
new file mode 100644
index 00000000..618ab1be
--- /dev/null
+++ b/src/nm-cloud-setup/nm-http-client.c
@@ -0,0 +1,801 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-http-client.h"
+
+#include <curl/curl.h>
+
+#include "nm-cloud-setup-utils.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+
+#define NM_CURL_DEBUG 0
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainContext *context;
+    CURLM *       mhandle;
+    GSource *     mhandle_source_timeout;
+    GHashTable *  source_sockets_hashtable;
+} NMHttpClientPrivate;
+
+struct _NMHttpClient {
+    GObject             parent;
+    NMHttpClientPrivate _priv;
+};
+
+struct _NMHttpClientClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMHttpClient, nm_http_client, G_TYPE_OBJECT);
+
+#define NM_HTTP_CLIENT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMHttpClient, NM_IS_HTTP_CLIENT)
+
+/*****************************************************************************/
+
+#define _NMLOG2(level, edata, ...)                               \
+    G_STMT_START                                                 \
+    {                                                            \
+        EHandleData *_edata = (edata);                           \
+                                                                 \
+        _NMLOG(level,                                            \
+               "http-request[" NM_HASH_OBFUSCATE_PTR_FMT         \
+               ", \"%s\"]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+               NM_HASH_OBFUSCATE_PTR(_edata),                    \
+               (_edata)->url _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                            \
+    G_STMT_END
+
+/*****************************************************************************/
+
+G_LOCK_DEFINE_STATIC(_my_curl_initalized_lock);
+static bool _my_curl_initialized = FALSE;
+
+__attribute__((destructor)) static void
+_my_curl_global_cleanup(void)
+{
+    G_LOCK(_my_curl_initalized_lock);
+    if (_my_curl_initialized) {
+        _my_curl_initialized = FALSE;
+        curl_global_cleanup();
+    }
+    G_UNLOCK(_my_curl_initalized_lock);
+}
+
+static void
+nm_http_client_curl_global_init(void)
+{
+    G_LOCK(_my_curl_initalized_lock);
+    if (!_my_curl_initialized) {
+        _my_curl_initialized = TRUE;
+        if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
+            /* Even if this fails, we are partly initialized. WTF. */
+            _LOGE("curl: curl_global_init() failed!");
+        }
+    }
+    G_UNLOCK(_my_curl_initalized_lock);
+}
+
+/*****************************************************************************/
+
+GMainContext *
+nm_http_client_get_main_context(NMHttpClient *self)
+{
+    g_return_val_if_fail(NM_IS_HTTP_CLIENT(self), NULL);
+
+    return NM_HTTP_CLIENT_GET_PRIVATE(self)->context;
+}
+
+/*****************************************************************************/
+
+static GSource *
+_source_attach(NMHttpClient *self, GSource *source)
+{
+    return nm_g_source_attach(source, NM_HTTP_CLIENT_GET_PRIVATE(self)->context);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    long    response_code;
+    GBytes *response_data;
+} GetResult;
+
+static void
+_get_result_free(gpointer data)
+{
+    GetResult *get_result = data;
+
+    g_bytes_unref(get_result->response_data);
+    nm_g_slice_free(get_result);
+}
+
+typedef struct {
+    GTask *            task;
+    GSource *          timeout_source;
+    CURLcode           ehandle_result;
+    CURL *             ehandle;
+    char *             url;
+    NMStrBuf           recv_data;
+    struct curl_slist *headers;
+    gssize             max_data;
+    gulong             cancellable_id;
+} EHandleData;
+
+static void
+_ehandle_free_ehandle(EHandleData *edata)
+{
+    if (edata->ehandle) {
+        NMHttpClient *       self = g_task_get_source_object(edata->task);
+        NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+        curl_multi_remove_handle(priv->mhandle, edata->ehandle);
+        curl_easy_cleanup(g_steal_pointer(&edata->ehandle));
+    }
+}
+
+static void
+_ehandle_free(EHandleData *edata)
+{
+    nm_assert(!edata->ehandle);
+    nm_assert(!edata->timeout_source);
+
+    g_object_unref(edata->task);
+
+    nm_str_buf_destroy(&edata->recv_data);
+    if (edata->headers)
+        curl_slist_free_all(edata->headers);
+    g_free(edata->url);
+    nm_g_slice_free(edata);
+}
+
+static void
+_ehandle_complete(EHandleData *edata, GError *error_take)
+{
+    GetResult *   get_result;
+    gs_free char *str_tmp_1     = NULL;
+    long          response_code = -1;
+
+    nm_clear_pointer(&edata->timeout_source, nm_g_source_destroy_and_unref);
+
+    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(edata->task), &edata->cancellable_id);
+
+    if (error_take) {
+        if (nm_utils_error_is_cancelled(error_take))
+            _LOG2T(edata, "cancelled");
+        else
+            _LOG2D(edata, "failed with %s", error_take->message);
+    } else if (edata->ehandle_result != CURLE_OK) {
+        _LOG2D(edata, "failed with curl error \"%s\"", curl_easy_strerror(edata->ehandle_result));
+        nm_utils_error_set(&error_take,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failed with curl error \"%s\"",
+                           curl_easy_strerror(edata->ehandle_result));
+    }
+
+    if (error_take) {
+        _ehandle_free_ehandle(edata);
+        g_task_return_error(edata->task, error_take);
+        _ehandle_free(edata);
+        return;
+    }
+
+    if (curl_easy_getinfo(edata->ehandle, CURLINFO_RESPONSE_CODE, &response_code) != CURLE_OK)
+        _LOG2E(edata, "failed to get response code from curl easy handle");
+
+    _LOG2D(edata,
+           "success getting %" G_GSIZE_FORMAT " bytes (response code %ld)",
+           edata->recv_data.len,
+           response_code);
+
+    _LOG2T(edata,
+           "received %" G_GSIZE_FORMAT " bytes: [[%s]]",
+           edata->recv_data.len,
+           nm_utils_buf_utf8safe_escape(nm_str_buf_get_str(&edata->recv_data),
+                                        edata->recv_data.len,
+                                        NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                        &str_tmp_1));
+
+    _ehandle_free_ehandle(edata);
+
+    get_result  = g_slice_new(GetResult);
+    *get_result = (GetResult){
+        .response_code = response_code,
+        /* This ensures that response_data is always NUL terminated. This is an important guarantee
+         * that NMHttpClient makes. */
+        .response_data = nm_str_buf_finalize_to_gbytes(&edata->recv_data),
+    };
+
+    g_task_return_pointer(edata->task, get_result, _get_result_free);
+
+    _ehandle_free(edata);
+}
+
+/*****************************************************************************/
+
+static size_t
+_get_writefunction_cb(char *ptr, size_t size, size_t nmemb, void *user_data)
+{
+    EHandleData *edata = user_data;
+    gsize        nconsume;
+
+    /* size should always be 1, but still. Multiply them to be sure. */
+    nmemb *= size;
+
+    if (edata->max_data >= 0) {
+        nm_assert(edata->recv_data.len <= edata->max_data);
+        nconsume = (((gsize) edata->max_data) - edata->recv_data.len);
+        if (nconsume > nmemb)
+            nconsume = nmemb;
+    } else
+        nconsume = nmemb;
+
+    nm_str_buf_append_len(&edata->recv_data, ptr, nconsume);
+    return nconsume;
+}
+
+static gboolean
+_get_timeout_cb(gpointer user_data)
+{
+    _ehandle_complete(
+        user_data,
+        g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "HTTP request timed out"));
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_get_cancelled_cb(GObject *object, gpointer user_data)
+{
+    EHandleData *edata = user_data;
+    GError *     error = NULL;
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(edata->task), &edata->cancellable_id);
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    _ehandle_complete(edata, error);
+}
+
+void
+nm_http_client_get(NMHttpClient *      self,
+                   const char *        url,
+                   int                 timeout_msec,
+                   gssize              max_data,
+                   const char *const * http_headers,
+                   GCancellable *      cancellable,
+                   GAsyncReadyCallback callback,
+                   gpointer            user_data)
+{
+    NMHttpClientPrivate *priv;
+    EHandleData *        edata;
+    guint                i;
+
+    g_return_if_fail(NM_IS_HTTP_CLIENT(self));
+    g_return_if_fail(url);
+    g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable));
+    g_return_if_fail(timeout_msec >= 0);
+    g_return_if_fail(max_data >= -1);
+
+    priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    edata  = g_slice_new(EHandleData);
+    *edata = (EHandleData){
+        .task      = nm_g_task_new(self, cancellable, nm_http_client_get, callback, user_data),
+        .recv_data = NM_STR_BUF_INIT(0, FALSE),
+        .max_data  = max_data,
+        .url       = g_strdup(url),
+        .headers   = NULL,
+    };
+
+    nmcs_wait_for_objects_register(edata->task);
+
+    _LOG2D(edata, "start get ...");
+
+    edata->ehandle = curl_easy_init();
+    if (!edata->ehandle) {
+        _ehandle_complete(edata,
+                          g_error_new_literal(NM_UTILS_ERROR,
+                                              NM_UTILS_ERROR_UNKNOWN,
+                                              "HTTP request failed to create curl handle"));
+        return;
+    }
+
+    curl_easy_setopt(edata->ehandle, CURLOPT_URL, url);
+
+    curl_easy_setopt(edata->ehandle, CURLOPT_WRITEFUNCTION, _get_writefunction_cb);
+    curl_easy_setopt(edata->ehandle, CURLOPT_WRITEDATA, edata);
+    curl_easy_setopt(edata->ehandle, CURLOPT_PRIVATE, edata);
+
+    if (http_headers) {
+        for (i = 0; http_headers[i]; ++i) {
+            struct curl_slist *tmp;
+
+            tmp = curl_slist_append(edata->headers, http_headers[i]);
+            if (!tmp) {
+                curl_slist_free_all(tmp);
+                _LOGE("curl: curl_slist_append() failed adding %s", http_headers[i]);
+                continue;
+            }
+            edata->headers = tmp;
+        }
+
+        curl_easy_setopt(edata->ehandle, CURLOPT_HTTPHEADER, edata->headers);
+    }
+
+    if (timeout_msec > 0) {
+        edata->timeout_source = _source_attach(self,
+                                               nm_g_timeout_source_new(timeout_msec,
+                                                                       G_PRIORITY_DEFAULT,
+                                                                       _get_timeout_cb,
+                                                                       edata,
+                                                                       NULL));
+    }
+
+    curl_multi_add_handle(priv->mhandle, edata->ehandle);
+
+    if (cancellable) {
+        gulong signal_id;
+
+        signal_id = g_cancellable_connect(cancellable, G_CALLBACK(_get_cancelled_cb), edata, NULL);
+        if (signal_id == 0) {
+            /* the request is already cancelled. Return. */
+            return;
+        }
+        edata->cancellable_id = signal_id;
+    }
+}
+
+/**
+ * nm_http_client_get_finish:
+ * @self: the #NMHttpClient instance
+ * @result: the #GAsyncResult which to complete.
+ * @out_response_code: (allow-none) (out): the HTTP response code or -1 on other error.
+ * @out_response_data: (allow-none) (transfer full): the HTTP response data, if any.
+ *   The GBytes buffer is guaranteed to have a trailing NUL character *after* the
+ *   returned buffer size. That means, you can always trust that the buffer is NUL terminated
+ *   and that there is one additional hidden byte after the data.
+ *   Also, the returned buffer is allocated just for you. While GBytes is immutable, you are
+ *   allowed to modify the buffer as it's not used by anybody else.
+ * @error: the error
+ *
+ * Returns: %TRUE on success or %FALSE with an error code.
+ */
+gboolean
+nm_http_client_get_finish(NMHttpClient *self,
+                          GAsyncResult *result,
+                          long *        out_response_code,
+                          GBytes **     out_response_data,
+                          GError **     error)
+{
+    GetResult *get_result;
+
+    g_return_val_if_fail(NM_IS_HTTP_CLIENT(self), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nm_http_client_get), FALSE);
+
+    get_result = g_task_propagate_pointer(G_TASK(result), error);
+
+    nm_assert(!error || (!!get_result) == (!*error));
+
+    if (!get_result) {
+        NM_SET_OUT(out_response_code, -1);
+        NM_SET_OUT(out_response_data, NULL);
+        return FALSE;
+    }
+
+    NM_SET_OUT(out_response_code, get_result->response_code);
+
+    /* response_data is binary, but is also guaranteed to be NUL terminated! */
+    NM_SET_OUT(out_response_data, g_steal_pointer(&get_result->response_data));
+
+    _get_result_free(get_result);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GTask *                     task;
+    char *                      uri;
+    const char *const *         http_headers;
+    NMHttpClientPollGetCheckFcn check_fcn;
+    gpointer                    check_user_data;
+    GBytes *                    response_data;
+    gsize                       request_max_data;
+    long                        response_code;
+    int                         request_timeout_ms;
+} PollGetData;
+
+static void
+_poll_get_data_free(gpointer data)
+{
+    PollGetData *poll_get_data = data;
+
+    g_free(poll_get_data->uri);
+
+    nm_clear_pointer(&poll_get_data->response_data, g_bytes_unref);
+    g_strfreev((char **) poll_get_data->http_headers);
+
+    nm_g_slice_free(poll_get_data);
+}
+
+static void
+_poll_get_probe_start_fcn(GCancellable *      cancellable,
+                          gpointer            probe_user_data,
+                          GAsyncReadyCallback callback,
+                          gpointer            user_data)
+{
+    PollGetData *poll_get_data = probe_user_data;
+
+    /* balanced by _poll_get_probe_finish_fcn() */
+    g_object_ref(poll_get_data->task);
+
+    nm_http_client_get(g_task_get_source_object(poll_get_data->task),
+                       poll_get_data->uri,
+                       poll_get_data->request_timeout_ms,
+                       poll_get_data->request_max_data,
+                       poll_get_data->http_headers,
+                       cancellable,
+                       callback,
+                       user_data);
+}
+
+static gboolean
+_poll_get_probe_finish_fcn(GObject *     source,
+                           GAsyncResult *result,
+                           gpointer      probe_user_data,
+                           GError **     error)
+{
+    PollGetData *              poll_get_data = probe_user_data;
+    _nm_unused gs_unref_object GTask *task =
+        poll_get_data->task; /* balance ref from _poll_get_probe_start_fcn() */
+    gboolean      success;
+    gs_free_error GError *local_error    = NULL;
+    gs_unref_bytes GBytes *response_data = NULL;
+    long                   response_code = -1;
+
+    success = nm_http_client_get_finish(g_task_get_source_object(poll_get_data->task),
+                                        result,
+                                        &response_code,
+                                        &response_data,
+                                        &local_error);
+
+    nm_assert((!!success) == (!local_error));
+
+    if (local_error) {
+        if (nm_utils_error_is_cancelled(local_error)) {
+            g_propagate_error(error, g_steal_pointer(&local_error));
+            return TRUE;
+        }
+        /* any other error. Continue polling. */
+        return FALSE;
+    }
+
+    if (poll_get_data->check_fcn) {
+        success = poll_get_data->check_fcn(response_code,
+                                           response_data,
+                                           poll_get_data->check_user_data,
+                                           &local_error);
+    } else
+        success = (response_code == 200);
+
+    if (local_error) {
+        g_propagate_error(error, g_steal_pointer(&local_error));
+        return TRUE;
+    }
+
+    if (!success) {
+        /* Not yet ready. Continue polling. */
+        return FALSE;
+    }
+
+    poll_get_data->response_code = response_code;
+    poll_get_data->response_data = g_steal_pointer(&response_data);
+    return TRUE;
+}
+
+static void
+_poll_get_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    PollGetData * poll_get_data = user_data;
+    gs_free_error GError *error = NULL;
+    gboolean              success;
+
+    success = nmcs_utils_poll_finish(result, NULL, &error);
+
+    nm_assert((!!success) == (!error));
+
+    if (error)
+        g_task_return_error(poll_get_data->task, g_steal_pointer(&error));
+    else
+        g_task_return_boolean(poll_get_data->task, TRUE);
+
+    g_object_unref(poll_get_data->task);
+}
+
+void
+nm_http_client_poll_get(NMHttpClient *              self,
+                        const char *                uri,
+                        int                         request_timeout_ms,
+                        gssize                      request_max_data,
+                        int                         poll_timeout_ms,
+                        int                         ratelimit_timeout_ms,
+                        const char *const *         http_headers,
+                        GCancellable *              cancellable,
+                        NMHttpClientPollGetCheckFcn check_fcn,
+                        gpointer                    check_user_data,
+                        GAsyncReadyCallback         callback,
+                        gpointer                    user_data)
+{
+    nm_auto_pop_gmaincontext GMainContext *context = NULL;
+    PollGetData *                          poll_get_data;
+
+    g_return_if_fail(NM_IS_HTTP_CLIENT(self));
+    g_return_if_fail(uri && uri[0]);
+    g_return_if_fail(request_timeout_ms >= -1);
+    g_return_if_fail(request_max_data >= -1);
+    g_return_if_fail(poll_timeout_ms >= -1);
+    g_return_if_fail(ratelimit_timeout_ms >= -1);
+    g_return_if_fail(!cancellable || G_CANCELLABLE(cancellable));
+
+    poll_get_data  = g_slice_new(PollGetData);
+    *poll_get_data = (PollGetData){
+        .task = nm_g_task_new(self, cancellable, nm_http_client_poll_get, callback, user_data),
+        .uri  = g_strdup(uri),
+        .request_timeout_ms = request_timeout_ms,
+        .request_max_data   = request_max_data,
+        .check_fcn          = check_fcn,
+        .check_user_data    = check_user_data,
+        .response_code      = -1,
+        .http_headers       = NM_CAST_STRV_CC(g_strdupv((char **) http_headers)),
+    };
+
+    nmcs_wait_for_objects_register(poll_get_data->task);
+
+    g_task_set_task_data(poll_get_data->task, poll_get_data, _poll_get_data_free);
+
+    context =
+        nm_g_main_context_push_thread_default_if_necessary(nm_http_client_get_main_context(self));
+
+    nmcs_utils_poll(poll_timeout_ms,
+                    ratelimit_timeout_ms,
+                    0,
+                    _poll_get_probe_start_fcn,
+                    _poll_get_probe_finish_fcn,
+                    poll_get_data,
+                    cancellable,
+                    _poll_get_done_cb,
+                    poll_get_data);
+}
+
+gboolean
+nm_http_client_poll_get_finish(NMHttpClient *self,
+                               GAsyncResult *result,
+                               long *        out_response_code,
+                               GBytes **     out_response_data,
+                               GError **     error)
+{
+    PollGetData * poll_get_data;
+    GTask *       task;
+    gboolean      success;
+    gs_free_error GError *local_error = NULL;
+
+    g_return_val_if_fail(NM_HTTP_CLIENT(self), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nm_http_client_poll_get), FALSE);
+
+    task = G_TASK(result);
+
+    success = g_task_propagate_boolean(task, &local_error);
+
+    nm_assert((!!success) == (!local_error));
+
+    if (local_error) {
+        g_propagate_error(error, g_steal_pointer(&local_error));
+        NM_SET_OUT(out_response_code, -1);
+        NM_SET_OUT(out_response_data, NULL);
+        return FALSE;
+    }
+
+    poll_get_data = g_task_get_task_data(task);
+
+    NM_SET_OUT(out_response_code, poll_get_data->response_code);
+    NM_SET_OUT(out_response_data, g_steal_pointer(&poll_get_data->response_data));
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_mhandle_action(NMHttpClient *self, int sockfd, int ev_bitmask)
+{
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+    EHandleData *        edata;
+    CURLMsg *            msg;
+    CURLcode             eret;
+    int                  m_left;
+    CURLMcode            ret;
+    int                  running_handles;
+
+    ret = curl_multi_socket_action(priv->mhandle, sockfd, ev_bitmask, &running_handles);
+    if (ret != CURLM_OK) {
+        _LOGE("curl: curl_multi_socket_action() failed: (%d) %s", ret, curl_multi_strerror(ret));
+        /* really unexpected. Not clear how to handle this. */
+    }
+
+    while ((msg = curl_multi_info_read(priv->mhandle, &m_left))) {
+        if (msg->msg != CURLMSG_DONE)
+            continue;
+
+        eret = curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **) &edata);
+
+        nm_assert(eret == CURLE_OK);
+        nm_assert(edata);
+
+        edata->ehandle_result = msg->data.result;
+        _ehandle_complete(edata, NULL);
+    }
+}
+
+static gboolean
+_mhandle_socket_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    int ev_bitmask = 0;
+
+    if (condition & G_IO_IN)
+        ev_bitmask |= CURL_CSELECT_IN;
+    if (condition & G_IO_OUT)
+        ev_bitmask |= CURL_CSELECT_OUT;
+    if (condition & G_IO_ERR)
+        ev_bitmask |= CURL_CSELECT_ERR;
+
+    _mhandle_action(user_data, fd, ev_bitmask);
+    return G_SOURCE_CONTINUE;
+}
+
+static int
+_mhandle_socketfunction_cb(CURL *        e_handle,
+                           curl_socket_t fd,
+                           int           what,
+                           void *        user_data,
+                           void *        socketp)
+{
+    GSource *            source_socket;
+    NMHttpClient *       self = user_data;
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    (void) _NM_ENSURE_TYPE(int, fd);
+
+    g_hash_table_remove(priv->source_sockets_hashtable, GINT_TO_POINTER(fd));
+
+    if (what != CURL_POLL_REMOVE) {
+        GIOCondition condition = 0;
+
+        if (what == CURL_POLL_IN)
+            condition = G_IO_IN;
+        else if (what == CURL_POLL_OUT)
+            condition = G_IO_OUT;
+        else if (what == CURL_POLL_INOUT)
+            condition = G_IO_IN | G_IO_OUT;
+        else
+            condition = 0;
+
+        if (condition) {
+            source_socket = nm_g_unix_fd_source_new(fd,
+                                                    condition,
+                                                    G_PRIORITY_DEFAULT,
+                                                    _mhandle_socket_cb,
+                                                    self,
+                                                    NULL);
+            g_source_attach(source_socket, priv->context);
+
+            g_hash_table_insert(priv->source_sockets_hashtable, GINT_TO_POINTER(fd), source_socket);
+        }
+    }
+
+    return CURLM_OK;
+}
+
+static gboolean
+_mhandle_timeout_cb(gpointer user_data)
+{
+    _mhandle_action(user_data, CURL_SOCKET_TIMEOUT, 0);
+    return G_SOURCE_REMOVE;
+}
+
+static int
+_mhandle_timerfunction_cb(CURLM *multi, long timeout_msec, void *user_data)
+{
+    NMHttpClient *       self = user_data;
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->mhandle_source_timeout, nm_g_source_destroy_and_unref);
+    if (timeout_msec >= 0) {
+        priv->mhandle_source_timeout =
+            _source_attach(self,
+                           nm_g_timeout_source_new(NM_MIN(timeout_msec, G_MAXINT),
+                                                   G_PRIORITY_DEFAULT,
+                                                   _mhandle_timeout_cb,
+                                                   self,
+                                                   NULL));
+    }
+    return 0;
+}
+
+/*****************************************************************************/
+
+static void
+nm_http_client_init(NMHttpClient *self)
+{
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    priv->source_sockets_hashtable =
+        g_hash_table_new_full(nm_direct_hash,
+                              NULL,
+                              NULL,
+                              (GDestroyNotify) nm_g_source_destroy_and_unref);
+}
+
+static void
+constructed(GObject *object)
+{
+    NMHttpClient *       self = NM_HTTP_CLIENT(object);
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    priv->context = g_main_context_ref_thread_default();
+
+    priv->mhandle = curl_multi_init();
+    if (!priv->mhandle)
+        _LOGE("curl: failed to create multi-handle");
+    else {
+        curl_multi_setopt(priv->mhandle, CURLMOPT_SOCKETFUNCTION, _mhandle_socketfunction_cb);
+        curl_multi_setopt(priv->mhandle, CURLMOPT_SOCKETDATA, self);
+        curl_multi_setopt(priv->mhandle, CURLMOPT_TIMERFUNCTION, _mhandle_timerfunction_cb);
+        curl_multi_setopt(priv->mhandle, CURLMOPT_TIMERDATA, self);
+    }
+
+    G_OBJECT_CLASS(nm_http_client_parent_class)->constructed(object);
+}
+
+NMHttpClient *
+nm_http_client_new(void)
+{
+    return g_object_new(NM_TYPE_HTTP_CLIENT, NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMHttpClient *       self = NM_HTTP_CLIENT(object);
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->mhandle, curl_multi_cleanup);
+    nm_clear_pointer(&priv->source_sockets_hashtable, g_hash_table_unref);
+
+    nm_clear_g_source_inst(&priv->mhandle_source_timeout);
+
+    G_OBJECT_CLASS(nm_http_client_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMHttpClient *       self = NM_HTTP_CLIENT(object);
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_http_client_parent_class)->finalize(object);
+
+    g_main_context_unref(priv->context);
+
+    curl_global_cleanup();
+}
+
+static void
+nm_http_client_class_init(NMHttpClientClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->constructed = constructed;
+    object_class->dispose     = dispose;
+    object_class->finalize    = finalize;
+
+    nm_http_client_curl_global_init();
+}
diff --git a/src/nm-cloud-setup/nm-http-client.h b/src/nm-cloud-setup/nm-http-client.h
new file mode 100644
index 00000000..862b7338
--- /dev/null
+++ b/src/nm-cloud-setup/nm-http-client.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_HTTP_CLIENT_C__
+#define __NM_HTTP_CLIENT_C__
+
+/*****************************************************************************/
+
+typedef struct _NMHttpClient      NMHttpClient;
+typedef struct _NMHttpClientClass NMHttpClientClass;
+
+#define NM_TYPE_HTTP_CLIENT (nm_http_client_get_type())
+#define NM_HTTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_HTTP_CLIENT, NMHttpClient))
+#define NM_HTTP_CLIENT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_HTTP_CLIENT, NMHttpClientClass))
+#define NM_IS_HTTP_CLIENT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_HTTP_CLIENT))
+#define NM_IS_HTTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_HTTP_CLIENT))
+#define NM_HTTP_CLIENT_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_HTTP_CLIENT, NMHttpClientClass))
+
+GType nm_http_client_get_type(void);
+
+NMHttpClient *nm_http_client_new(void);
+
+/*****************************************************************************/
+
+GMainContext *nm_http_client_get_main_context(NMHttpClient *self);
+
+/*****************************************************************************/
+
+void nm_http_client_get(NMHttpClient *      self,
+                        const char *        uri,
+                        int                 timeout_msec,
+                        gssize              max_data,
+                        const char *const * http_headers,
+                        GCancellable *      cancellable,
+                        GAsyncReadyCallback callback,
+                        gpointer            user_data);
+
+gboolean nm_http_client_get_finish(NMHttpClient *self,
+                                   GAsyncResult *result,
+                                   long *        out_response_code,
+                                   GBytes **     out_response_data,
+                                   GError **     error);
+
+typedef gboolean (*NMHttpClientPollGetCheckFcn)(long     response_code,
+                                                GBytes * response_data,
+                                                gpointer check_user_data,
+                                                GError **error);
+
+void nm_http_client_poll_get(NMHttpClient *              self,
+                             const char *                uri,
+                             int                         request_timeout_ms,
+                             gssize                      request_max_data,
+                             int                         poll_timeout_ms,
+                             int                         ratelimit_timeout_ms,
+                             const char *const *         http_headers,
+                             GCancellable *              cancellable,
+                             NMHttpClientPollGetCheckFcn check_fcn,
+                             gpointer                    check_user_data,
+                             GAsyncReadyCallback         callback,
+                             gpointer                    user_data);
+
+gboolean nm_http_client_poll_get_finish(NMHttpClient *self,
+                                        GAsyncResult *result,
+                                        long *        out_response_code,
+                                        GBytes **     out_response_data,
+                                        GError **     error);
+
+/*****************************************************************************/
+
+#endif /* __NM_HTTP_CLIENT_C__ */
diff --git a/src/nm-cloud-setup/nmcs-provider-aliyun.c b/src/nm-cloud-setup/nmcs-provider-aliyun.c
new file mode 100644
index 00000000..126980fd
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-aliyun.c
@@ -0,0 +1,507 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmcs-provider-aliyun.h"
+
+#include <arpa/inet.h>
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+#define HTTP_TIMEOUT_MS 3000
+
+#define NM_ALIYUN_HOST              "100.100.100.200"
+#define NM_ALIYUN_BASE              "http://" NM_ALIYUN_HOST
+#define NM_ALIYUN_API_VERSION       "2016-01-01"
+#define NM_ALIYUN_METADATA_URL_BASE /* $NM_ALIYUN_BASE/$NM_ALIYUN_API_VERSION */ \
+    "/meta-data/network/interfaces/macs/"
+
+static const char *
+_aliyun_base(void)
+{
+    static const char *base_cached = NULL;
+    const char *       base;
+
+again:
+    base = g_atomic_pointer_get(&base_cached);
+    if (G_UNLIKELY(!base)) {
+        /* The base URI can be set via environment variable.
+         * This is mainly for testing, it's not usually supposed to be configured.
+         * Consider this private API! */
+        base = g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_ALIYUN_HOST"));
+
+        if (!g_atomic_pointer_compare_and_exchange(&base_cached, NULL, base))
+            goto again;
+    }
+    base = nmcs_utils_uri_complete_interned(base) ?: ("" NM_ALIYUN_BASE);
+    return base;
+}
+
+#define _aliyun_uri_concat(...) nmcs_utils_uri_build_concat(_aliyun_base(), __VA_ARGS__)
+#define _aliyun_uri_interfaces(...) \
+    _aliyun_uri_concat(NM_ALIYUN_API_VERSION, NM_ALIYUN_METADATA_URL_BASE, ##__VA_ARGS__)
+
+/*****************************************************************************/
+
+struct _NMCSProviderAliyun {
+    NMCSProvider parent;
+};
+
+struct _NMCSProviderAliyunClass {
+    NMCSProviderClass parent;
+};
+
+G_DEFINE_TYPE(NMCSProviderAliyun, nmcs_provider_aliyun, NMCS_TYPE_PROVIDER);
+
+/*****************************************************************************/
+
+static void
+filter_chars(char *str, const char *chars)
+{
+    gsize i;
+    gsize j;
+
+    for (i = 0, j = 0; str[i]; i++) {
+        if (!strchr(chars, str[i]))
+            str[j++] = str[i];
+    }
+    str[j] = '\0';
+}
+
+static void
+_detect_get_meta_data_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask *task     = user_data;
+    gs_free_error GError *get_error = NULL;
+    gs_free_error GError *error     = NULL;
+
+    nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error);
+
+    if (nm_utils_error_is_cancelled(get_error)) {
+        g_task_return_error(task, g_steal_pointer(&get_error));
+        return;
+    }
+
+    if (get_error) {
+        nm_utils_error_set(&error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failure to get ALIYUN metadata: %s",
+                           get_error->message);
+        g_task_return_error(task, g_steal_pointer(&error));
+        return;
+    }
+
+    g_task_return_boolean(task, TRUE);
+}
+
+static void
+detect(NMCSProvider *provider, GTask *task)
+{
+    NMHttpClient *http_client;
+    gs_free char *uri = NULL;
+
+    http_client = nmcs_provider_get_http_client(provider);
+
+    nm_http_client_poll_get(http_client,
+                            (uri = _aliyun_uri_concat(NM_ALIYUN_API_VERSION "/meta-data/")),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            7000,
+                            1000,
+                            NULL,
+                            g_task_get_cancellable(task),
+                            NULL,
+                            NULL,
+                            _detect_get_meta_data_done_cb,
+                            task);
+}
+
+/*****************************************************************************/
+
+typedef enum {
+    GET_CONFIG_FETCH_DONE_TYPE_SUBNET_VPC_CIDR_BLOCK,
+    GET_CONFIG_FETCH_DONE_TYPE_PRIVATE_IPV4S,
+    GET_CONFIG_FETCH_DONE_TYPE_NETMASK,
+    GET_CONFIG_FETCH_DONE_TYPE_GATEWAY,
+} GetConfigFetchDoneType;
+
+static void
+_get_config_fetch_done_cb(NMHttpClient *         http_client,
+                          GAsyncResult *         result,
+                          gpointer               user_data,
+                          GetConfigFetchDoneType fetch_type)
+{
+    NMCSProviderGetConfigTaskData *get_config_data;
+    gs_unref_bytes GBytes *response       = NULL;
+    gs_free_error GError *          error = NULL;
+    NMCSProviderGetConfigIfaceData *config_iface_data;
+    in_addr_t                       tmp_addr;
+    int                             tmp_prefix;
+    in_addr_t                       netmask_bin;
+    in_addr_t                       gateway_bin;
+    gs_free const char **           s_addrs = NULL;
+    gsize                           i;
+    gsize                           len;
+
+    nm_utils_user_data_unpack(user_data, &get_config_data, &config_iface_data);
+
+    nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error);
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    if (error)
+        goto out;
+
+    switch (fetch_type) {
+    case GET_CONFIG_FETCH_DONE_TYPE_PRIVATE_IPV4S:
+
+        s_addrs = nm_utils_strsplit_set_full(g_bytes_get_data(response, NULL),
+                                             ",",
+                                             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++) {
+                filter_chars((char *) s_addrs[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;
+                }
+            }
+        }
+        break;
+
+    case GET_CONFIG_FETCH_DONE_TYPE_SUBNET_VPC_CIDR_BLOCK:
+
+        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_addr = tmp_addr;
+        }
+        break;
+
+    case GET_CONFIG_FETCH_DONE_TYPE_NETMASK:
+
+        if (nm_utils_parse_inaddr_bin(AF_INET,
+                                      g_bytes_get_data(response, NULL),
+                                      NULL,
+                                      &netmask_bin)) {
+            config_iface_data->cidr_prefix = nm_utils_ip4_netmask_to_prefix(netmask_bin);
+        };
+        break;
+
+    case GET_CONFIG_FETCH_DONE_TYPE_GATEWAY:
+
+        if (nm_utils_parse_inaddr_bin(AF_INET,
+                                      g_bytes_get_data(response, NULL),
+                                      NULL,
+                                      &gateway_bin)) {
+            config_iface_data->has_gateway = TRUE;
+            config_iface_data->gateway     = gateway_bin;
+        };
+        break;
+    }
+
+out:
+    get_config_data->n_pending--;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
+}
+
+static void
+_get_config_fetch_done_cb_vpc_cidr_block(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_DONE_TYPE_SUBNET_VPC_CIDR_BLOCK);
+}
+
+static void
+_get_config_fetch_done_cb_private_ipv4s(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_DONE_TYPE_PRIVATE_IPV4S);
+}
+
+static void
+_get_config_fetch_done_cb_netmask(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_DONE_TYPE_NETMASK);
+}
+
+static void
+_get_config_fetch_done_cb_gateway(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_DONE_TYPE_GATEWAY);
+}
+
+typedef struct {
+    gssize iface_idx;
+    char   path[0];
+} GetConfigMetadataMac;
+
+static void
+_get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    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_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &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) {
+        _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;
+    }
+
+    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);
+    while (g_hash_table_iter_next(&h_iter, (gpointer *) &v_hwaddr, (gpointer *) &v_mac_data)) {
+        NMCSProviderGetConfigIfaceData *config_iface_data;
+        gs_free char *                  uri1 = NULL;
+        gs_free char *                  uri2 = NULL;
+        gs_free char *                  uri3 = NULL;
+        gs_free char *                  uri4 = NULL;
+
+        config_iface_data = g_hash_table_lookup(get_config_data->result_dict, v_hwaddr);
+
+        if (!config_iface_data) {
+            if (!get_config_data->any) {
+                _LOGD("get-config: skip fetching meta data for %s (%s)",
+                      v_hwaddr,
+                      v_mac_data->path);
+                continue;
+            }
+
+            config_iface_data =
+                nmcs_provider_get_config_iface_data_create(get_config_data->result_dict,
+                                                           FALSE,
+                                                           v_hwaddr);
+        }
+
+        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)",
+              config_iface_data->iface_idx,
+              config_iface_data->hwaddr,
+              v_mac_data->path);
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(
+            http_client,
+            (uri1 = _aliyun_uri_interfaces(v_mac_data->path,
+                                           NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/",
+                                           "vpc-cidr-block")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NULL,
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_vpc_cidr_block,
+            nm_utils_user_data_pack(get_config_data, config_iface_data));
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(
+            http_client,
+            (uri2 = _aliyun_uri_interfaces(v_mac_data->path,
+                                           NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/",
+                                           "private-ipv4s")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NULL,
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_private_ipv4s,
+            nm_utils_user_data_pack(get_config_data, config_iface_data));
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(
+            http_client,
+            (uri3 = _aliyun_uri_interfaces(v_mac_data->path,
+                                           NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/",
+                                           "netmask")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NULL,
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_netmask,
+            nm_utils_user_data_pack(get_config_data, config_iface_data));
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(
+            http_client,
+            (uri4 = _aliyun_uri_interfaces(v_mac_data->path,
+                                           NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/",
+                                           "gateway")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NULL,
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_gateway,
+            nm_utils_user_data_pack(get_config_data, config_iface_data));
+    }
+
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, NULL);
+}
+
+static gboolean
+_get_config_metadata_ready_check(long     response_code,
+                                 GBytes * response,
+                                 gpointer check_user_data,
+                                 GError **error)
+{
+    NMCSProviderGetConfigTaskData *get_config_data = check_user_data;
+    gs_unref_hashtable GHashTable *response_parsed = NULL;
+    const guint8 *                 r_data;
+    const char *                   cur_line;
+    gsize                          r_len;
+    gsize                          cur_line_len;
+    GHashTableIter                 h_iter;
+    gboolean                       has_all;
+    const char *                   c_hwaddr;
+    gssize                         iface_idx_counter = 0;
+
+    if (response_code != 200 || !response) {
+        /* we wait longer. */
+        return FALSE;
+    }
+
+    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);
+
+    while (nm_utils_parse_next_line((const char **) &r_data, &r_len, &cur_line, &cur_line_len)) {
+        GetConfigMetadataMac *mac_data;
+        char *                hwaddr;
+
+        if (cur_line_len == 0)
+            continue;
+
+        /* 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';
+
+        hwaddr = nmcs_utils_hwaddr_normalize(
+            cur_line,
+            cur_line[cur_line_len - 1u] == '/' ? (gssize) (cur_line_len - 1u) : -1);
+        if (!hwaddr)
+            continue;
+
+        if (!response_parsed)
+            response_parsed = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
+
+        mac_data            = g_malloc(sizeof(GetConfigMetadataMac) + 1u + cur_line_len);
+        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, 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;
+            break;
+        }
+    }
+
+    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;
+
+    /* 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
+     * around from the start...
+     */
+    nm_http_client_poll_get(nmcs_provider_get_http_client(provider),
+                            (uri = _aliyun_uri_interfaces()),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            15000,
+                            1000,
+                            NULL,
+                            get_config_data->intern_cancellable,
+                            _get_config_metadata_ready_check,
+                            get_config_data,
+                            _get_config_metadata_ready_cb,
+                            get_config_data);
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_aliyun_init(NMCSProviderAliyun *self)
+{}
+
+static void
+nmcs_provider_aliyun_class_init(NMCSProviderAliyunClass *klass)
+{
+    NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass);
+
+    provider_class->_name                 = "aliyun";
+    provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_ALIYUN");
+    provider_class->detect                = detect;
+    provider_class->get_config            = get_config;
+}
diff --git a/src/nm-cloud-setup/nmcs-provider-aliyun.h b/src/nm-cloud-setup/nmcs-provider-aliyun.h
new file mode 100644
index 00000000..6e733a11
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-aliyun.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMCS_PROVIDER_ALIYUN_H__
+#define __NMCS_PROVIDER_ALIYUN_H__
+
+#include "nmcs-provider.h"
+
+/*****************************************************************************/
+
+typedef struct _NMCSProviderAliyun      NMCSProviderAliyun;
+typedef struct _NMCSProviderAliyunClass NMCSProviderAliyunClass;
+
+#define NMCS_TYPE_PROVIDER_ALIYUN (nmcs_provider_aliyun_get_type())
+#define NMCS_PROVIDER_ALIYUN(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_ALIYUN, NMCSProviderAliyun))
+#define NMCS_PROVIDER_ALIYUN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_ALIYUN, NMCSProviderAliyunClass))
+#define NMCS_IS_PROVIDER_ALIYUN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_ALIYUN))
+#define NMCS_IS_PROVIDER_ALIYUN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_ALIYUN))
+#define NMCS_PROVIDER_ALIYUN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_ALIYUN, NMCSProviderAliyunClass))
+
+GType nmcs_provider_aliyun_get_type(void);
+
+/*****************************************************************************/
+
+#endif /* __NMCS_PROVIDER_ALIYUN_H__ */
diff --git a/src/nm-cloud-setup/nmcs-provider-azure.c b/src/nm-cloud-setup/nmcs-provider-azure.c
new file mode 100644
index 00000000..b3f0c68b
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-azure.c
@@ -0,0 +1,565 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmcs-provider-azure.h"
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+#define HTTP_TIMEOUT_MS 3000
+
+#define NM_AZURE_METADATA_HEADER   "Metadata:true"
+#define NM_AZURE_HOST              "169.254.169.254"
+#define NM_AZURE_BASE              "http://" NM_AZURE_HOST
+#define NM_AZURE_API_VERSION       "?format=text&api-version=2017-04-02"
+#define NM_AZURE_METADATA_URL_BASE /* $NM_AZURE_BASE/$NM_AZURE_API_VERSION */ \
+    "/metadata/instance/network/interface/"
+
+#define _azure_uri_concat(...) \
+    nmcs_utils_uri_build_concat(NM_AZURE_BASE, __VA_ARGS__, NM_AZURE_API_VERSION)
+#define _azure_uri_interfaces(...) _azure_uri_concat(NM_AZURE_METADATA_URL_BASE, ##__VA_ARGS__)
+
+/*****************************************************************************/
+
+struct _NMCSProviderAzure {
+    NMCSProvider parent;
+};
+
+struct _NMCSProviderAzureClass {
+    NMCSProviderClass parent;
+};
+
+G_DEFINE_TYPE(NMCSProviderAzure, nmcs_provider_azure, NMCS_TYPE_PROVIDER);
+
+/*****************************************************************************/
+
+static void
+_detect_get_meta_data_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask *task     = user_data;
+    gs_free_error GError *get_error = NULL;
+    gs_free_error GError *error     = NULL;
+
+    nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error);
+
+    if (nm_utils_error_is_cancelled(get_error)) {
+        g_task_return_error(task, g_steal_pointer(&get_error));
+        return;
+    }
+
+    if (get_error) {
+        nm_utils_error_set(&error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failure to get Azure metadata: %s",
+                           get_error->message);
+        g_task_return_error(task, g_steal_pointer(&error));
+        return;
+    }
+
+    g_task_return_boolean(task, TRUE);
+}
+
+static void
+detect(NMCSProvider *provider, GTask *task)
+{
+    NMHttpClient *http_client;
+    gs_free char *uri = NULL;
+
+    http_client = nmcs_provider_get_http_client(provider);
+
+    nm_http_client_poll_get(http_client,
+                            (uri = _azure_uri_concat("/metadata/instance")),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            7000,
+                            1000,
+                            NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+                            g_task_get_cancellable(task),
+                            NULL,
+                            NULL,
+                            _detect_get_meta_data_done_cb,
+                            task);
+}
+
+/*****************************************************************************/
+
+typedef enum {
+    GET_CONFIG_FETCH_TYPE_IPV4_IPADDRESS_X_PRIVATEIPADDRESS,
+    GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_ADDRESS,
+    GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_PREFIX,
+} GetConfigFetchType;
+
+typedef struct {
+    NMCSProviderGetConfigTaskData * get_config_data;
+    NMCSProviderGetConfigIfaceData *iface_get_config;
+    gssize                          intern_iface_idx;
+    gssize                          extern_iface_idx;
+    guint                           n_iface_data_pending;
+} AzureIfaceData;
+
+static void
+_azure_iface_data_destroy(AzureIfaceData *iface_data)
+{
+    nm_g_slice_free(iface_data);
+}
+
+static void
+_get_config_fetch_done_cb(NMHttpClient *     http_client,
+                          GAsyncResult *     result,
+                          AzureIfaceData *   iface_data,
+                          GetConfigFetchType fetch_type)
+{
+    NMCSProviderGetConfigTaskData * get_config_data;
+    NMCSProviderGetConfigIfaceData *iface_get_config;
+    gs_unref_bytes GBytes *response = NULL;
+    gs_free_error GError *error     = NULL;
+    const char *          resp_str  = NULL;
+    gsize                 resp_len;
+    char                  tmp_addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    in_addr_t             tmp_addr;
+    int                   tmp_prefix = -1;
+
+    nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error);
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    get_config_data  = iface_data->get_config_data;
+    iface_get_config = iface_data->iface_get_config;
+
+    if (error)
+        goto out_done;
+
+    resp_str = g_bytes_get_data(response, &resp_len);
+    nm_assert(resp_str[resp_len] == '\0');
+
+    switch (fetch_type) {
+    case GET_CONFIG_FETCH_TYPE_IPV4_IPADDRESS_X_PRIVATEIPADDRESS:
+
+        if (!nmcs_utils_ipaddr_normalize_bin(AF_INET, resp_str, resp_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 "]: received address %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++;
+        break;
+
+    case GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_ADDRESS:
+
+        if (!nmcs_utils_ipaddr_normalize_bin(AF_INET, resp_str, resp_len, NULL, &tmp_addr)) {
+            error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "ip is not a subnet address");
+            goto out_done;
+        }
+        _LOGD("interface[%" G_GSSIZE_FORMAT "]: received subnet address %s",
+              iface_data->intern_iface_idx,
+              _nm_utils_inet4_ntop(tmp_addr, tmp_addr_str));
+        iface_get_config->cidr_addr = tmp_addr;
+        break;
+
+    case GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_PREFIX:
+
+        tmp_prefix = _nm_utils_ascii_str_to_int64_bin(resp_str, resp_len, 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 "]: received subnet prefix %d",
+              iface_data->intern_iface_idx,
+              tmp_prefix);
+        iface_get_config->cidr_prefix = tmp_prefix;
+        break;
+    }
+
+out_done:
+    if (!error) {
+        --iface_data->n_iface_data_pending;
+        if (iface_data->n_iface_data_pending > 0)
+            return;
+
+        /* we surely have cidr_addr and cidr_prefix, otherwise
+         * we would have errored out above. */
+        iface_get_config->has_cidr = TRUE;
+    }
+
+    --get_config_data->n_pending;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
+}
+
+static void
+_get_config_fetch_done_cb_ipv4_ipaddress_x_privateipaddress(GObject *     source,
+                                                            GAsyncResult *result,
+                                                            gpointer      user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_TYPE_IPV4_IPADDRESS_X_PRIVATEIPADDRESS);
+}
+
+static void
+_get_config_fetch_done_cb_ipv4_subnet_0_address(GObject *     source,
+                                                GAsyncResult *result,
+                                                gpointer      user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_ADDRESS);
+}
+
+static void
+_get_config_fetch_done_cb_ipv4_subnet_0_prefix(GObject *     source,
+                                               GAsyncResult *result,
+                                               gpointer      user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source),
+                              result,
+                              user_data,
+                              GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_PREFIX);
+}
+
+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;
+    NMCSProviderGetConfigTaskData *get_config_data;
+    const char *                   line;
+    gsize                          line_len;
+    char                           iface_idx_str[30];
+
+    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 out_error;
+
+    response_str = g_bytes_get_data(response, &response_len);
+    /* NMHttpClient guarantees that there is a trailing NUL after the data. */
+    nm_assert(response_str[response_len] == 0);
+
+    nm_assert(!iface_data->iface_get_config->ipv4s_arr);
+    nm_assert(!iface_data->iface_get_config->has_ipv4s);
+    nm_assert(!iface_data->iface_get_config->has_cidr);
+
+    nm_sprintf_buf(iface_idx_str, "%" G_GSSIZE_FORMAT, iface_data->intern_iface_idx);
+
+    while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) {
+        gint64        ips_prefix_idx;
+        gs_free char *uri = NULL;
+        char          buf[100];
+
+        if (line_len == 0)
+            continue;
+
+        /* 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] == '/')
+            ((char *) line)[--line_len] = '\0';
+
+        ips_prefix_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXINT64, -1);
+
+        if (ips_prefix_idx < 0)
+            continue;
+
+        iface_data->n_iface_data_pending++;
+
+        nm_http_client_poll_get(
+            NM_HTTP_CLIENT(source),
+            (uri = _azure_uri_interfaces(iface_idx_str,
+                                         "/ipv4/ipAddress/",
+                                         nm_sprintf_buf(buf, "%" G_GINT64_FORMAT, ips_prefix_idx),
+                                         "/privateIpAddress")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_ipv4_ipaddress_x_privateipaddress,
+            iface_data);
+    }
+
+    iface_data->iface_get_config->ipv4s_len = 0;
+    iface_data->iface_get_config->ipv4s_arr = g_new(in_addr_t, iface_data->n_iface_data_pending);
+
+    {
+        gs_free char *uri = NULL;
+
+        iface_data->n_iface_data_pending++;
+        nm_http_client_poll_get(
+            NM_HTTP_CLIENT(source),
+            (uri = _azure_uri_interfaces(iface_idx_str, "/ipv4/subnet/0/address/")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_ipv4_subnet_0_address,
+            iface_data);
+
+        nm_clear_g_free(&uri);
+
+        iface_data->n_iface_data_pending++;
+        nm_http_client_poll_get(
+            NM_HTTP_CLIENT(source),
+            (uri = _azure_uri_interfaces(iface_idx_str, "/ipv4/subnet/0/prefix/")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_ipv4_subnet_0_prefix,
+            iface_data);
+    }
+    return;
+
+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];
+
+    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 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(get_config_data->result_dict, v_hwaddr);
+
+    if (!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_create(get_config_data->result_dict,
+                                                       FALSE,
+                                                       v_hwaddr);
+    } 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->iface_get_config->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->intern_iface_idx,
+          iface_data->iface_get_config->hwaddr);
+
+    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)),
+                            HTTP_TIMEOUT_MS,
+                            512 * 1024,
+                            10000,
+                            1000,
+                            NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+                            get_config_data->intern_cancellable,
+                            NULL,
+                            NULL,
+                            _get_config_ips_prefix_list_cb,
+                            iface_data);
+    return;
+
+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;
+    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) {
+        _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
+        return;
+    }
+
+    response_str = g_bytes_get_data(response, &response_len);
+    /* NMHttpClient guarantees that there is a trailing NUL after the data. */
+    nm_assert(response_str[response_len] == 0);
+
+    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          intern_iface_idx;
+
+        if (line_len == 0)
+            continue;
+
+        /* 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] == '/')
+            ((char *) line)[--line_len] = '\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,
+            .intern_iface_idx     = intern_iface_idx,
+            .extern_iface_idx     = extern_iface_idx_cnt++,
+            .n_iface_data_pending = 0,
+        };
+        g_ptr_array_add(ifaces_arr, iface_data);
+    }
+
+    _LOGD("found azure 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 Azure interfaces found"));
+        return;
+    }
+
+    for (i = 0; i < ifaces_arr->len; ++i) {
+        AzureIfaceData *    iface_data = ifaces_arr->pdata[i];
+        gs_free const char *uri        = NULL;
+        char                buf[100];
+
+        _LOGD("azure interface[%" G_GSSIZE_FORMAT "]: retrieving configuration",
+              iface_data->intern_iface_idx);
+
+        nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/macAddress", iface_data->intern_iface_idx);
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(NM_HTTP_CLIENT(source),
+                                (uri = _azure_uri_interfaces(buf)),
+                                HTTP_TIMEOUT_MS,
+                                512 * 1024,
+                                10000,
+                                1000,
+                                NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+                                get_config_data->intern_cancellable,
+                                NULL,
+                                NULL,
+                                _get_config_iface_cb,
+                                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;
+
+    nm_http_client_poll_get(nmcs_provider_get_http_client(provider),
+                            (uri = _azure_uri_interfaces()),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            15000,
+                            1000,
+                            NM_MAKE_STRV(NM_AZURE_METADATA_HEADER),
+                            get_config_data->intern_cancellable,
+                            NULL,
+                            NULL,
+                            _get_net_ifaces_list_cb,
+                            get_config_data);
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_azure_init(NMCSProviderAzure *self)
+{}
+
+static void
+nmcs_provider_azure_class_init(NMCSProviderAzureClass *klass)
+{
+    NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass);
+
+    provider_class->_name                 = "azure";
+    provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_AZURE");
+    provider_class->detect                = detect;
+    provider_class->get_config            = get_config;
+}
diff --git a/src/nm-cloud-setup/nmcs-provider-azure.h b/src/nm-cloud-setup/nmcs-provider-azure.h
new file mode 100644
index 00000000..be78768f
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-azure.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMCS_PROVIDER_AZURE_H__
+#define __NMCS_PROVIDER_AZURE_H__
+
+#include "nmcs-provider.h"
+
+/*****************************************************************************/
+
+typedef struct _NMCSProviderAzure      NMCSProviderAzure;
+typedef struct _NMCSProviderAzureClass NMCSProviderAzureClass;
+
+#define NMCS_TYPE_PROVIDER_AZURE (nmcs_provider_azure_get_type())
+#define NMCS_PROVIDER_AZURE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_AZURE, NMCSProviderAzure))
+#define NMCS_PROVIDER_AZURE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_AZURE, NMCSProviderAzureClass))
+#define NMCS_IS_PROVIDER_AZURE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_AZURE))
+#define NMCS_IS_PROVIDER_AZURE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_AZURE))
+#define NMCS_PROVIDER_AZURE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_AZURE, NMCSProviderAzureClass))
+
+GType nmcs_provider_azure_get_type(void);
+
+/*****************************************************************************/
+
+#endif /* __NMCS_PROVIDER_AZURE_H__ */
diff --git a/src/nm-cloud-setup/nmcs-provider-ec2.c b/src/nm-cloud-setup/nmcs-provider-ec2.c
new file mode 100644
index 00000000..9fe62518
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-ec2.c
@@ -0,0 +1,408 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmcs-provider-ec2.h"
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+#define HTTP_TIMEOUT_MS 3000
+
+#define NM_EC2_HOST              "169.254.169.254"
+#define NM_EC2_BASE              "http://" NM_EC2_HOST
+#define NM_EC2_API_VERSION       "2018-09-24"
+#define NM_EC2_METADATA_URL_BASE /* $NM_EC2_BASE/$NM_EC2_API_VERSION */ \
+    "/meta-data/network/interfaces/macs/"
+
+static const char *
+_ec2_base(void)
+{
+    static const char *base_cached = NULL;
+    const char *       base;
+
+again:
+    base = g_atomic_pointer_get(&base_cached);
+    if (G_UNLIKELY(!base)) {
+        /* The base URI can be set via environment variable.
+         * This is mainly for testing, it's not usually supposed to be configured.
+         * Consider this private API! */
+        base = g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_EC2_HOST"));
+        base = nmcs_utils_uri_complete_interned(base) ?: ("" NM_EC2_BASE);
+
+        if (!g_atomic_pointer_compare_and_exchange(&base_cached, NULL, base))
+            goto again;
+    }
+
+    return base;
+}
+
+#define _ec2_uri_concat(...) nmcs_utils_uri_build_concat(_ec2_base(), __VA_ARGS__)
+#define _ec2_uri_interfaces(...) \
+    _ec2_uri_concat(NM_EC2_API_VERSION, NM_EC2_METADATA_URL_BASE, ##__VA_ARGS__)
+
+/*****************************************************************************/
+
+struct _NMCSProviderEC2 {
+    NMCSProvider parent;
+};
+
+struct _NMCSProviderEC2Class {
+    NMCSProviderClass parent;
+};
+
+G_DEFINE_TYPE(NMCSProviderEC2, nmcs_provider_ec2, NMCS_TYPE_PROVIDER);
+
+/*****************************************************************************/
+
+static gboolean
+_detect_get_meta_data_check_cb(long     response_code,
+                               GBytes * response,
+                               gpointer check_user_data,
+                               GError **error)
+{
+    return response_code == 200 && nmcs_utils_parse_get_full_line(response, "ami-id");
+}
+
+static void
+_detect_get_meta_data_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask *task     = user_data;
+    gs_free_error GError *get_error = NULL;
+    gs_free_error GError *error     = NULL;
+
+    nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error);
+
+    if (nm_utils_error_is_cancelled(get_error)) {
+        g_task_return_error(task, g_steal_pointer(&get_error));
+        return;
+    }
+
+    if (get_error) {
+        nm_utils_error_set(&error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failure to get EC2 metadata: %s",
+                           get_error->message);
+        g_task_return_error(task, g_steal_pointer(&error));
+        return;
+    }
+
+    g_task_return_boolean(task, TRUE);
+}
+
+static void
+detect(NMCSProvider *provider, GTask *task)
+{
+    NMHttpClient *http_client;
+    gs_free char *uri = NULL;
+
+    http_client = nmcs_provider_get_http_client(provider);
+
+    nm_http_client_poll_get(http_client,
+                            (uri = _ec2_uri_concat("latest/meta-data/")),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            7000,
+                            1000,
+                            NULL,
+                            g_task_get_cancellable(task),
+                            _detect_get_meta_data_check_cb,
+                            NULL,
+                            _detect_get_meta_data_done_cb,
+                            task);
+}
+
+/*****************************************************************************/
+
+static void
+_get_config_fetch_done_cb(NMHttpClient *http_client,
+                          GAsyncResult *result,
+                          gpointer      user_data,
+                          gboolean      is_local_ipv4)
+{
+    NMCSProviderGetConfigTaskData *get_config_data;
+    gs_unref_bytes GBytes *response       = NULL;
+    gs_free_error GError *          error = NULL;
+    NMCSProviderGetConfigIfaceData *config_iface_data;
+    in_addr_t                       tmp_addr;
+    int                             tmp_prefix;
+
+    nm_utils_user_data_unpack(user_data, &get_config_data, &config_iface_data);
+
+    nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error);
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    if (error)
+        goto out;
+
+    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, 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, 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;
+        }
+    }
+
+out:
+    get_config_data->n_pending--;
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
+}
+
+static void
+_get_config_fetch_done_cb_subnet_ipv4_cidr_block(GObject *     source,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), result, user_data, FALSE);
+}
+
+static void
+_get_config_fetch_done_cb_local_ipv4s(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), result, user_data, TRUE);
+}
+
+typedef struct {
+    gssize iface_idx;
+    char   path[0];
+} GetConfigMetadataMac;
+
+static void
+_get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    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_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &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) {
+        _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;
+    }
+
+    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);
+    while (g_hash_table_iter_next(&h_iter, (gpointer *) &v_hwaddr, (gpointer *) &v_mac_data)) {
+        NMCSProviderGetConfigIfaceData *config_iface_data;
+        gs_free char *                  uri1 = NULL;
+        gs_free char *                  uri2 = NULL;
+
+        config_iface_data = g_hash_table_lookup(get_config_data->result_dict, v_hwaddr);
+
+        if (!config_iface_data) {
+            if (!get_config_data->any) {
+                _LOGD("get-config: skip fetching meta data for %s (%s)",
+                      v_hwaddr,
+                      v_mac_data->path);
+                continue;
+            }
+            config_iface_data =
+                nmcs_provider_get_config_iface_data_create(get_config_data->result_dict,
+                                                           FALSE,
+                                                           v_hwaddr);
+        }
+
+        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)",
+              config_iface_data->iface_idx,
+              config_iface_data->hwaddr,
+              v_mac_data->path);
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(
+            http_client,
+            (uri1 = _ec2_uri_interfaces(v_mac_data->path,
+                                        NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/",
+                                        "subnet-ipv4-cidr-block")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NULL,
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_subnet_ipv4_cidr_block,
+            nm_utils_user_data_pack(get_config_data, config_iface_data));
+
+        get_config_data->n_pending++;
+        nm_http_client_poll_get(
+            http_client,
+            (uri2 = _ec2_uri_interfaces(v_mac_data->path,
+                                        NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/",
+                                        "local-ipv4s")),
+            HTTP_TIMEOUT_MS,
+            512 * 1024,
+            10000,
+            1000,
+            NULL,
+            get_config_data->intern_cancellable,
+            NULL,
+            NULL,
+            _get_config_fetch_done_cb_local_ipv4s,
+            nm_utils_user_data_pack(get_config_data, config_iface_data));
+    }
+
+    _nmcs_provider_get_config_task_maybe_return(get_config_data, NULL);
+}
+
+static gboolean
+_get_config_metadata_ready_check(long     response_code,
+                                 GBytes * response,
+                                 gpointer check_user_data,
+                                 GError **error)
+{
+    NMCSProviderGetConfigTaskData *get_config_data = check_user_data;
+    gs_unref_hashtable GHashTable *response_parsed = NULL;
+    const guint8 *                 r_data;
+    const char *                   cur_line;
+    gsize                          r_len;
+    gsize                          cur_line_len;
+    GHashTableIter                 h_iter;
+    gboolean                       has_all;
+    const char *                   c_hwaddr;
+    gssize                         iface_idx_counter = 0;
+
+    if (response_code != 200 || !response) {
+        /* we wait longer. */
+        return FALSE;
+    }
+
+    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);
+
+    while (nm_utils_parse_next_line((const char **) &r_data, &r_len, &cur_line, &cur_line_len)) {
+        GetConfigMetadataMac *mac_data;
+        char *                hwaddr;
+
+        if (cur_line_len == 0)
+            continue;
+
+        /* 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';
+
+        hwaddr = nmcs_utils_hwaddr_normalize(
+            cur_line,
+            cur_line[cur_line_len - 1u] == '/' ? (gssize) (cur_line_len - 1u) : -1);
+        if (!hwaddr)
+            continue;
+
+        if (!response_parsed)
+            response_parsed = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
+
+        mac_data            = g_malloc(sizeof(GetConfigMetadataMac) + 1u + cur_line_len);
+        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, 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;
+            break;
+        }
+    }
+
+    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;
+
+    /* 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
+     * around from the start...
+     */
+    nm_http_client_poll_get(nmcs_provider_get_http_client(provider),
+                            (uri = _ec2_uri_interfaces()),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            15000,
+                            1000,
+                            NULL,
+                            get_config_data->intern_cancellable,
+                            _get_config_metadata_ready_check,
+                            get_config_data,
+                            _get_config_metadata_ready_cb,
+                            get_config_data);
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_ec2_init(NMCSProviderEC2 *self)
+{}
+
+static void
+nmcs_provider_ec2_class_init(NMCSProviderEC2Class *klass)
+{
+    NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass);
+
+    provider_class->_name                 = "ec2";
+    provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_EC2");
+    provider_class->detect                = detect;
+    provider_class->get_config            = get_config;
+}
diff --git a/src/nm-cloud-setup/nmcs-provider-ec2.h b/src/nm-cloud-setup/nmcs-provider-ec2.h
new file mode 100644
index 00000000..8f8b6400
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-ec2.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMCS_PROVIDER_EC2_H__
+#define __NMCS_PROVIDER_EC2_H__
+
+#include "nmcs-provider.h"
+
+/*****************************************************************************/
+
+typedef struct _NMCSProviderEC2      NMCSProviderEC2;
+typedef struct _NMCSProviderEC2Class NMCSProviderEC2Class;
+
+#define NMCS_TYPE_PROVIDER_EC2 (nmcs_provider_ec2_get_type())
+#define NMCS_PROVIDER_EC2(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_EC2, NMCSProviderEC2))
+#define NMCS_PROVIDER_EC2_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_EC2, NMCSProviderEC2Class))
+#define NMCS_IS_PROVIDER_EC2(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_EC2))
+#define NMCS_IS_PROVIDER_EC2_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_EC2))
+#define NMCS_PROVIDER_EC2_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_EC2, NMCSProviderEC2Class))
+
+GType nmcs_provider_ec2_get_type(void);
+
+/*****************************************************************************/
+
+#endif /* __NMCS_PROVIDER_EC2_H__ */
diff --git a/src/nm-cloud-setup/nmcs-provider-gcp.c b/src/nm-cloud-setup/nmcs-provider-gcp.c
new file mode 100644
index 00000000..60425ad9
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-gcp.c
@@ -0,0 +1,462 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmcs-provider-gcp.h"
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+#define HTTP_TIMEOUT_MS      3000
+#define HTTP_REQ_MAX_DATA    512 * 1024
+#define HTTP_POLL_TIMEOUT_MS 10000
+#define HTTP_RATE_LIMIT_MS   1000
+
+#define NM_GCP_HOST              "metadata.google.internal"
+#define NM_GCP_BASE              "http://" NM_GCP_HOST
+#define NM_GCP_API_VERSION       "/v1"
+#define NM_GCP_METADATA_URL_BASE NM_GCP_BASE "/computeMetadata" NM_GCP_API_VERSION "/instance"
+#define NM_GCP_METADATA_URL_NET  "/network-interfaces/"
+
+#define NM_GCP_METADATA_HEADER "Metadata-Flavor: Google"
+
+#define _gcp_uri_concat(...)     nmcs_utils_uri_build_concat(NM_GCP_METADATA_URL_BASE, __VA_ARGS__)
+#define _gcp_uri_interfaces(...) _gcp_uri_concat(NM_GCP_METADATA_URL_NET, ##__VA_ARGS__)
+
+/*****************************************************************************/
+
+struct _NMCSProviderGCP {
+    NMCSProvider parent;
+};
+
+struct _NMCSProviderGCPClass {
+    NMCSProviderClass parent;
+};
+
+G_DEFINE_TYPE(NMCSProviderGCP, nmcs_provider_gcp, NMCS_TYPE_PROVIDER);
+
+/*****************************************************************************/
+
+static void
+_detect_get_meta_data_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask *task     = user_data;
+    gs_free_error GError *get_error = NULL;
+    gs_free_error GError *error     = NULL;
+
+    nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error);
+
+    if (nm_utils_error_is_cancelled(get_error)) {
+        g_task_return_error(task, g_steal_pointer(&get_error));
+        return;
+    }
+
+    if (get_error) {
+        nm_utils_error_set(&error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failure to get GCP metadata: %s",
+                           get_error->message);
+        g_task_return_error(task, g_steal_pointer(&error));
+        return;
+    }
+
+    g_task_return_boolean(task, TRUE);
+}
+
+static void
+detect(NMCSProvider *provider, GTask *task)
+{
+    NMHttpClient *http_client;
+    gs_free char *uri = NULL;
+
+    http_client = nmcs_provider_get_http_client(provider);
+
+    nm_http_client_poll_get(http_client,
+                            (uri = _gcp_uri_concat("id")),
+                            HTTP_TIMEOUT_MS,
+                            256 * 1024,
+                            7000,
+                            1000,
+                            NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
+                            g_task_get_cancellable(task),
+                            NULL,
+                            NULL,
+                            _detect_get_meta_data_done_cb,
+                            task);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NMCSProviderGetConfigTaskData * get_config_data;
+    NMCSProviderGetConfigIfaceData *iface_get_config;
+    gssize                          intern_iface_idx;
+    gssize                          extern_iface_idx;
+    guint                           n_fips_pending;
+} GCPIfaceData;
+
+static void
+_gcp_iface_data_destroy(GCPIfaceData *iface_data)
+{
+    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;
+    gs_free char *        ipaddr      = NULL;
+    NMIPRoute **          routes_arr;
+    NMIPRoute *           route_new;
+
+    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 out_done;
+
+    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 out_done;
+    }
+
+    _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: adding forwarded-ip %s",
+          iface_data->intern_iface_idx,
+          ipaddr);
+
+    iface_get_config = iface_data->iface_get_config;
+    routes_arr       = iface_get_config->iproutes_arr;
+
+    route_new = nm_ip_route_new(AF_INET, ipaddr, 32, NULL, 100, &error);
+    if (error)
+        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;
+
+out_done:
+    if (!error) {
+        --iface_data->n_fips_pending;
+        if (iface_data->n_fips_pending > 0)
+            return;
+    }
+
+    --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;
+    const char *          line;
+    gsize                 line_len;
+    guint                 i;
+
+    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 out_error;
+
+    response_str = g_bytes_get_data(response, &response_len);
+    /* NMHttpClient guarantees that there is a trailing NUL after the data. */
+    nm_assert(response_str[response_len] == 0);
+
+    uri_arr = g_ptr_array_new_with_free_func(g_free);
+    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 an it has an
+         * extra NUL character after the buffer. */
+        ((char *) line)[line_len] = '\0';
+
+        fip_index = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXINT64, -1);
+        if (fip_index < 0)
+            continue;
+
+        g_ptr_array_add(uri_arr,
+                        g_strdup_printf("%" G_GSSIZE_FORMAT "/forwarded-ips/%" G_GINT64_FORMAT,
+                                        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->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 out_error;
+    }
+
+    iface_data->iface_get_config->iproutes_arr = g_new(NMIPRoute *, iface_data->n_fips_pending);
+
+    for (i = 0; i < uri_arr->len; ++i) {
+        const char *        str = uri_arr->pdata[i];
+        gs_free const char *uri = NULL;
+
+        nm_http_client_poll_get(NM_HTTP_CLIENT(source),
+                                (uri = _gcp_uri_interfaces(str)),
+                                HTTP_TIMEOUT_MS,
+                                HTTP_REQ_MAX_DATA,
+                                HTTP_POLL_TIMEOUT_MS,
+                                HTTP_RATE_LIMIT_MS,
+                                NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
+                                get_config_data->intern_cancellable,
+                                NULL,
+                                NULL,
+                                _get_config_fip_cb,
+                                iface_data);
+    }
+    return;
+
+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 char *                 v_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 out_done;
+
+    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,
+                                   "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(get_config_data->result_dict, v_hwaddr);
+
+    if (!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_create(get_config_data->result_dict,
+                                                       FALSE,
+                                                       v_hwaddr);
+        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,
+                  iface_data->iface_get_config->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;
+    }
+
+    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 " : "",
+          iface_data->iface_get_config->hwaddr);
+
+    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)),
+                            HTTP_TIMEOUT_MS,
+                            HTTP_REQ_MAX_DATA,
+                            HTTP_POLL_TIMEOUT_MS,
+                            HTTP_RATE_LIMIT_MS,
+                            NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
+                            get_config_data->intern_cancellable,
+                            NULL,
+                            NULL,
+                            _get_config_ips_list_cb,
+                            iface_data);
+    return;
+
+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)
+{
+    gs_unref_ptrarray GPtrArray *ifaces_arr = NULL;
+    gs_unref_bytes GBytes *response         = NULL;
+    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) {
+        _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
+        return;
+    }
+
+    response_str = g_bytes_get_data(response, &response_len);
+    /* NMHttpClient guarantees that there is a trailing NUL after the data. */
+    nm_assert(response_str[response_len] == 0);
+
+    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        intern_iface_idx;
+
+        if (line_len == 0)
+            continue;
+
+        /* 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';
+
+        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,
+            .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);
+    }
+
+    _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->intern_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,
+                                HTTP_REQ_MAX_DATA,
+                                HTTP_POLL_TIMEOUT_MS,
+                                HTTP_RATE_LIMIT_MS,
+                                NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
+                                get_config_data->intern_cancellable,
+                                NULL,
+                                NULL,
+                                _get_config_iface_cb,
+                                data);
+    }
+
+    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;
+
+    nm_http_client_poll_get(nmcs_provider_get_http_client(provider),
+                            (uri = _gcp_uri_interfaces()),
+                            HTTP_TIMEOUT_MS,
+                            HTTP_REQ_MAX_DATA,
+                            HTTP_POLL_TIMEOUT_MS,
+                            HTTP_RATE_LIMIT_MS,
+                            NM_MAKE_STRV(NM_GCP_METADATA_HEADER),
+                            get_config_data->intern_cancellable,
+                            NULL,
+                            NULL,
+                            _get_net_ifaces_list_cb,
+                            get_config_data);
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_gcp_init(NMCSProviderGCP *self)
+{}
+
+static void
+nmcs_provider_gcp_class_init(NMCSProviderGCPClass *klass)
+{
+    NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass);
+
+    provider_class->_name                 = "GCP";
+    provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_GCP");
+    provider_class->detect                = detect;
+    provider_class->get_config            = get_config;
+}
diff --git a/src/nm-cloud-setup/nmcs-provider-gcp.h b/src/nm-cloud-setup/nmcs-provider-gcp.h
new file mode 100644
index 00000000..b0329935
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider-gcp.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMCS_PROVIDER_GCP_H__
+#define __NMCS_PROVIDER_GCP_H__
+
+#include "nmcs-provider.h"
+
+/*****************************************************************************/
+
+typedef struct _NMCSProviderGCP      NMCSProviderGCP;
+typedef struct _NMCSProviderGCPClass NMCSProviderGCPClass;
+
+#define NMCS_TYPE_PROVIDER_GCP (nmcs_provider_gcp_get_type())
+#define NMCS_PROVIDER_GCP(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_GCP, NMCSProviderGCP))
+#define NMCS_PROVIDER_GCP_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_GCP, NMCSProviderGCPClass))
+#define NMCS_IS_PROVIDER_GCP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_GCP))
+#define NMCS_IS_PROVIDER_GCP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_GCP))
+#define NMCS_PROVIDER_GCP_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_GCP, NMCSProviderGCPClass))
+
+GType nmcs_provider_gcp_get_type(void);
+
+/*****************************************************************************/
+
+#endif /* __NMCS_PROVIDER_GCP_H__ */
diff --git a/src/nm-cloud-setup/nmcs-provider.c b/src/nm-cloud-setup/nmcs-provider.c
new file mode 100644
index 00000000..138e78d4
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider.c
@@ -0,0 +1,380 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmcs-provider.h"
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_HTTP_CLIENT, );
+
+typedef struct _NMCSProviderPrivate {
+    NMHttpClient *http_client;
+} NMCSProviderPrivate;
+
+G_DEFINE_TYPE(NMCSProvider, nmcs_provider, G_TYPE_OBJECT);
+
+#define NMCS_PROVIDER_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMCSProvider, NMCS_IS_PROVIDER)
+
+/*****************************************************************************/
+
+const char *
+nmcs_provider_get_name(NMCSProvider *self)
+{
+    NMCSProviderClass *klass;
+
+    g_return_val_if_fail(NMCS_IS_PROVIDER(self), NULL);
+
+    klass = NMCS_PROVIDER_GET_CLASS(self);
+    nm_assert(klass->_name);
+    return klass->_name;
+}
+
+/*****************************************************************************/
+
+NMHttpClient *
+nmcs_provider_get_http_client(NMCSProvider *self)
+{
+    g_return_val_if_fail(NMCS_IS_PROVIDER(self), NULL);
+
+    return NMCS_PROVIDER_GET_PRIVATE(self)->http_client;
+}
+
+GMainContext *
+nmcs_provider_get_main_context(NMCSProvider *self)
+{
+    g_return_val_if_fail(NMCS_IS_PROVIDER(self), NULL);
+
+    return nm_http_client_get_main_context(NMCS_PROVIDER_GET_PRIVATE(self)->http_client);
+}
+/*****************************************************************************/
+
+static int
+_result_new_sort_iface_data(gconstpointer pa, gconstpointer pb)
+{
+    const NMCSProviderGetConfigIfaceData *a = *((const NMCSProviderGetConfigIfaceData *const *) pa);
+    const NMCSProviderGetConfigIfaceData *b = *((const NMCSProviderGetConfigIfaceData *const *) pb);
+
+    /* negative iface_idx are sorted to the end. */
+    NM_CMP_DIRECT((a->iface_idx < 0), (b->iface_idx < 0));
+
+    NM_CMP_FIELD(a, b, iface_idx);
+    return 0;
+}
+
+static NMCSProviderGetConfigResult *
+nmcs_provider_get_config_result_new(GHashTable *iface_datas)
+{
+    const NMCSProviderGetConfigIfaceData *iface_data;
+    NMCSProviderGetConfigResult *         result;
+    GHashTableIter                        h_iter;
+    guint                                 num_valid_ifaces = 0;
+    guint                                 num_ipv4s        = 0;
+    GPtrArray *                           ptrarr;
+    guint                                 n_iface_datas;
+
+    n_iface_datas = g_hash_table_size(iface_datas);
+
+    ptrarr = g_ptr_array_sized_new(n_iface_datas + 1u);
+
+    g_hash_table_iter_init(&h_iter, iface_datas);
+    while (g_hash_table_iter_next(&h_iter, NULL, (gpointer *) &iface_data)) {
+        if (nmcs_provider_get_config_iface_data_is_valid(iface_data)) {
+            num_valid_ifaces++;
+            num_ipv4s += iface_data->ipv4s_len;
+        }
+        g_ptr_array_add(ptrarr, (gpointer) iface_data);
+    }
+
+    g_ptr_array_sort(ptrarr, _result_new_sort_iface_data);
+
+    nm_assert(n_iface_datas == ptrarr->len);
+
+    g_ptr_array_add(ptrarr, NULL);
+
+    result  = g_new(NMCSProviderGetConfigResult, 1);
+    *result = (NMCSProviderGetConfigResult){
+        .iface_datas   = g_hash_table_ref(iface_datas),
+        .n_iface_datas = n_iface_datas,
+        .iface_datas_arr =
+            (const NMCSProviderGetConfigIfaceData **) g_ptr_array_free(ptrarr, FALSE),
+        .num_valid_ifaces = num_valid_ifaces,
+        .num_ipv4s        = num_ipv4s,
+    };
+
+#if NM_MORE_ASSERTS > 5
+    {
+        gsize iface_idx_expected = 0;
+        guint i;
+
+        for (i = 0; i < result->n_iface_datas; i++) {
+            if (result->iface_datas_arr[i]->iface_idx < 0) {
+                nm_assert(result->iface_datas_arr[i]->iface_idx == -1);
+                iface_idx_expected = -1;
+                continue;
+            }
+            nm_assert(result->iface_datas_arr[i]->iface_idx == iface_idx_expected);
+            iface_idx_expected++;
+        }
+    }
+#endif
+
+    return result;
+}
+
+void
+nmcs_provider_get_config_result_free(NMCSProviderGetConfigResult *result)
+{
+    if (result) {
+        nm_g_hash_table_unref(result->iface_datas);
+        g_free((gpointer) result->iface_datas_arr);
+        g_free(result);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nmcs_provider_detect(NMCSProvider *      self,
+                     GCancellable *      cancellable,
+                     GAsyncReadyCallback callback,
+                     gpointer            user_data)
+{
+    gs_unref_object GTask *task = NULL;
+    const char *           env;
+
+    g_return_if_fail(NMCS_IS_PROVIDER(self));
+    g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable));
+
+    task = nm_g_task_new(self, cancellable, nmcs_provider_detect, callback, user_data);
+
+    nmcs_wait_for_objects_register(task);
+
+    env = g_getenv(NMCS_PROVIDER_GET_CLASS(self)->_env_provider_enabled);
+    if (!_nm_utils_ascii_str_to_bool(env, FALSE)) {
+        g_task_return_error(task,
+                            nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "provider is disabled"));
+        return;
+    }
+
+    NMCS_PROVIDER_GET_CLASS(self)->detect(self, g_steal_pointer(&task));
+}
+
+gboolean
+nmcs_provider_detect_finish(NMCSProvider *self, GAsyncResult *result, GError **error)
+{
+    g_return_val_if_fail(NMCS_IS_PROVIDER(self), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nmcs_provider_detect), FALSE);
+
+    return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+/*****************************************************************************/
+
+NMCSProviderGetConfigIfaceData *
+nmcs_provider_get_config_iface_data_create(GHashTable *iface_datas,
+                                           gboolean    was_requested,
+                                           const char *hwaddr)
+{
+    NMCSProviderGetConfigIfaceData *iface_data;
+
+    nm_assert(hwaddr);
+
+    iface_data  = g_slice_new(NMCSProviderGetConfigIfaceData);
+    *iface_data = (NMCSProviderGetConfigIfaceData){
+        .hwaddr        = g_strdup(hwaddr),
+        .iface_idx     = -1,
+        .was_requested = was_requested,
+    };
+
+    /* the has does not own the key (iface_datta->hwaddr), the lifetime of the
+     * key is associated with the iface_data instance. */
+    g_hash_table_replace(iface_datas, (char *) iface_data->hwaddr, iface_data);
+
+    return iface_data;
+}
+
+static void
+_iface_data_free(gpointer data)
+{
+    NMCSProviderGetConfigIfaceData *iface_data = data;
+
+    g_free(iface_data->ipv4s_arr);
+    g_free(iface_data->iproutes_arr);
+    g_free((char *) iface_data->hwaddr);
+
+    nm_g_slice_free(iface_data);
+}
+
+static void
+_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data, GError *error_take)
+{
+    gs_free_error GError *error = error_take;
+
+    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,
+                              nmcs_provider_get_config_result_new(get_config_data->result_dict),
+                              (GDestroyNotify) nmcs_provider_get_config_result_free);
+    }
+
+    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,
+                         GCancellable *      cancellable,
+                         GAsyncReadyCallback callback,
+                         gpointer            user_data)
+{
+    NMCSProviderGetConfigTaskData *get_config_data;
+
+    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),
+        .any  = any,
+        .result_dict = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, _iface_data_free),
+    };
+
+    nmcs_wait_for_objects_register(get_config_data->task);
+
+    for (; hwaddrs && hwaddrs[0]; hwaddrs++)
+        nmcs_provider_get_config_iface_data_create(get_config_data->result_dict, TRUE, hwaddrs[0]);
+
+    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);
+}
+
+NMCSProviderGetConfigResult *
+nmcs_provider_get_config_finish(NMCSProvider *self, GAsyncResult *result, GError **error)
+{
+    g_return_val_if_fail(NMCS_IS_PROVIDER(self), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nmcs_provider_get_config), FALSE);
+
+    return g_task_propagate_pointer(G_TASK(result), error);
+}
+
+/*****************************************************************************/
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMCSProviderPrivate *priv = NMCS_PROVIDER_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_HTTP_CLIENT:
+        priv->http_client = g_value_dup_object(value);
+        g_return_if_fail(NM_IS_HTTP_CLIENT(priv->http_client));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_init(NMCSProvider *self)
+{
+    NMCSProviderPrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NMCS_TYPE_PROVIDER, NMCSProviderPrivate);
+
+    self->_priv = priv;
+}
+
+static void
+dispose(GObject *object)
+{
+    NMCSProvider *       self = NMCS_PROVIDER(object);
+    NMCSProviderPrivate *priv = NMCS_PROVIDER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->http_client);
+
+    G_OBJECT_CLASS(nmcs_provider_parent_class)->dispose(object);
+}
+
+static void
+nmcs_provider_class_init(NMCSProviderClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMCSProviderPrivate));
+
+    object_class->set_property = set_property;
+    object_class->dispose      = dispose;
+
+    obj_properties[PROP_HTTP_CLIENT] =
+        g_param_spec_object(NMCS_PROVIDER_HTTP_CLIENT,
+                            "",
+                            "",
+                            NM_TYPE_HTTP_CLIENT,
+                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/nm-cloud-setup/nmcs-provider.h b/src/nm-cloud-setup/nmcs-provider.h
new file mode 100644
index 00000000..36632059
--- /dev/null
+++ b/src/nm-cloud-setup/nmcs-provider.h
@@ -0,0 +1,170 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMCS_PROVIDER_H__
+#define __NMCS_PROVIDER_H__
+
+/*****************************************************************************/
+
+#include "nm-http-client.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    /* And it's exactly the same pointer that is also the key for the iface_datas
+     * dictionary. */
+    const char *hwaddr;
+
+    in_addr_t *ipv4s_arr;
+    gsize      ipv4s_len;
+
+    /* 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;
+    in_addr_t gateway;
+    guint8    cidr_prefix;
+    bool      has_ipv4s : 1;
+    bool      has_cidr : 1;
+    bool      has_gateway : 1;
+
+    NMIPRoute **iproutes_arr;
+    gsize       iproutes_len;
+
+    /* TRUE, if the configuration was requested via hwaddrs argument to
+     * nmcs_provider_get_config(). */
+    bool was_requested : 1;
+
+} NMCSProviderGetConfigIfaceData;
+
+static inline gboolean
+nmcs_provider_get_config_iface_data_is_valid(const NMCSProviderGetConfigIfaceData *config_data)
+{
+    return config_data && config_data->iface_idx >= 0
+           && ((config_data->has_ipv4s && config_data->has_cidr) || config_data->iproutes_len);
+}
+
+NMCSProviderGetConfigIfaceData *nmcs_provider_get_config_iface_data_create(GHashTable *iface_datas,
+                                                                           gboolean was_requested,
+                                                                           const char *hwaddr);
+
+/*****************************************************************************/
+
+typedef struct {
+    /* A dictionary of (const char *) -> (NMCSProviderGetConfigIfaceData *).
+     * This is the per-interface result of get_config().
+     *
+     * The key is the same pointer as NMCSProviderGetConfigIfaceData's hwaddr. */
+    GHashTable *iface_datas;
+
+    /* The number of iface_datas that are nmcs_provider_get_config_iface_data_is_valid(). */
+    guint num_valid_ifaces;
+
+    /* the number of IPv4 addresses over all valid iface_datas. */
+    guint num_ipv4s;
+
+    guint n_iface_datas;
+
+    /* The sorted value of @iface_datas, sorted by iface_idx.
+     *
+     * Not found entries (iface_idx == -1) are sorted at the end. */
+    const NMCSProviderGetConfigIfaceData *const *iface_datas_arr;
+
+} NMCSProviderGetConfigResult;
+
+void nmcs_provider_get_config_result_free(NMCSProviderGetConfigResult *result);
+
+NM_AUTO_DEFINE_FCN0(NMCSProviderGetConfigResult *,
+                    _nm_auto_free_nmcs_provider_get_config_result,
+                    nmcs_provider_get_config_result_free);
+#define nm_auto_free_nmcs_provider_get_config_result \
+    nm_auto(_nm_auto_free_nmcs_provider_get_config_result)
+
+/*****************************************************************************/
+
+typedef struct {
+    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_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())
+#define NMCS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER, NMCSProvider))
+#define NMCS_PROVIDER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER, NMCSProviderClass))
+#define NMCS_IS_PROVIDER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER))
+#define NMCS_IS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER))
+#define NMCS_PROVIDER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER, NMCSProviderClass))
+
+#define NMCS_PROVIDER_HTTP_CLIENT "http-client"
+
+struct _NMCSProviderPrivate;
+
+typedef struct {
+    GObject                      parent;
+    struct _NMCSProviderPrivate *_priv;
+} NMCSProvider;
+
+typedef struct {
+    GObjectClass parent;
+    const char * _name;
+    const char * _env_provider_enabled;
+
+    void (*detect)(NMCSProvider *self, GTask *task);
+
+    void (*get_config)(NMCSProvider *self, NMCSProviderGetConfigTaskData *get_config_data);
+
+} NMCSProviderClass;
+
+GType nmcs_provider_get_type(void);
+
+/*****************************************************************************/
+
+const char *nmcs_provider_get_name(NMCSProvider *provider);
+
+NMHttpClient *nmcs_provider_get_http_client(NMCSProvider *provider);
+GMainContext *nmcs_provider_get_main_context(NMCSProvider *provider);
+
+/*****************************************************************************/
+
+void nmcs_provider_detect(NMCSProvider *      provider,
+                          GCancellable *      cancellable,
+                          GAsyncReadyCallback callback,
+                          gpointer            user_data);
+
+gboolean nmcs_provider_detect_finish(NMCSProvider *provider, GAsyncResult *result, GError **error);
+
+/*****************************************************************************/
+
+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,
+                              GCancellable *      cancellable,
+                              GAsyncReadyCallback callback,
+                              gpointer            user_data);
+
+NMCSProviderGetConfigResult *
+nmcs_provider_get_config_finish(NMCSProvider *provider, GAsyncResult *result, GError **error);
+
+#endif /* __NMCS_PROVIDER_H__ */
diff --git a/src/nm-cloud-setup/tests/meson.build b/src/nm-cloud-setup/tests/meson.build
new file mode 100644
index 00000000..c6881a57
--- /dev/null
+++ b/src/nm-cloud-setup/tests/meson.build
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+exe = executable(
+  'test-cloud-setup-general',
+  'test-cloud-setup-general.c',
+  dependencies: [
+    libnm_dep,
+    glib_dep,
+    libcurl_dep,
+  ],
+  link_with: [
+    libnm_cloud_setup_core,
+    libnmc_base,
+    libnm_log_null,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+)
+
+test(
+  'src/nm-cloud-setup/tests/test-cloud-setup-general',
+  test_script,
+  args: test_args + [exe.full_path()],
+)
diff --git a/src/nm-cloud-setup/tests/test-cloud-setup-general.c b/src/nm-cloud-setup/tests/test-cloud-setup-general.c
new file mode 100644
index 00000000..2039ce34
--- /dev/null
+++ b/src/nm-cloud-setup/tests/test-cloud-setup-general.c
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-cloud-setup/nm-cloud-setup-utils.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+
+#include "libnm-glib-aux/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();
+}