/* SPDX-License-Identifier: LGPL-2.1+ */ #include "nm-default.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")); if (base && base[0] && !strchr(base, '/')) { if (NM_STR_HAS_PREFIX(base, "http://") || NM_STR_HAS_PREFIX(base, "https://")) base = g_intern_string(base); else { gs_free char *s = NULL; s = g_strconcat("http://", base, NULL); base = g_intern_string(s); } } if (!base) base = NM_EC2_BASE; nm_assert(!NM_STR_HAS_SUFFIX(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_data, gpointer check_user_data, GError **error) { return response_code == 200 && nmcs_utils_parse_get_full_line(response_data, "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); } /*****************************************************************************/ typedef struct { NMCSProviderGetConfigTaskData *get_config_data; GError * error; GCancellable * cancellable; gulong cancelled_id; guint n_pending; } GetConfigIfaceData; static void _get_config_task_maybe_return(GetConfigIfaceData *iface_data, GError *error_take) { NMCSProviderGetConfigTaskData *get_config_data = iface_data->get_config_data; if (error_take) { if (!iface_data->error) iface_data->error = error_take; else if (!nm_utils_error_is_cancelled(iface_data->error) && nm_utils_error_is_cancelled(error_take)) { nm_clear_error(&iface_data->error); iface_data->error = error_take; } else g_error_free(error_take); nm_clear_g_cancellable(&iface_data->cancellable); } if (iface_data->n_pending > 0) return; nm_clear_g_cancellable_disconnect(g_task_get_cancellable(get_config_data->task), &iface_data->cancelled_id); nm_clear_g_cancellable(&iface_data->cancellable); if (iface_data->error) { if (nm_utils_error_is_cancelled(iface_data->error)) _LOGD("get-config: cancelled"); else _LOGD("get-config: failed: %s", iface_data->error->message); g_task_return_error(get_config_data->task, g_steal_pointer(&iface_data->error)); } else { _LOGD("get-config: success"); g_task_return_pointer(get_config_data->task, g_hash_table_ref(get_config_data->result_dict), (GDestroyNotify) g_hash_table_unref); } nm_g_slice_free(iface_data); g_object_unref(get_config_data->task); } static void _get_config_fetch_done_cb(NMHttpClient *http_client, GAsyncResult *result, gpointer user_data, gboolean is_local_ipv4) { GetConfigIfaceData *iface_data; const char * hwaddr = NULL; gs_unref_bytes GBytes *response_data = NULL; gs_free_error GError *error = NULL; nm_utils_user_data_unpack(user_data, &iface_data, &hwaddr); nm_http_client_poll_get_finish(http_client, result, NULL, &response_data, &error); if (!error) { NMCSProviderGetConfigIfaceData *config_iface_data; in_addr_t tmp_addr; int tmp_prefix; config_iface_data = g_hash_table_lookup(iface_data->get_config_data->result_dict, hwaddr); if (is_local_ipv4) { gs_free const char **s_addrs = NULL; gsize i, len; s_addrs = nm_utils_strsplit_set_full(g_bytes_get_data(response_data, NULL), "\n", NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP); len = NM_PTRARRAY_LEN(s_addrs); nm_assert(!config_iface_data->has_ipv4s); nm_assert(!config_iface_data->ipv4s_arr); config_iface_data->has_ipv4s = TRUE; config_iface_data->ipv4s_len = 0; if (len > 0) { config_iface_data->ipv4s_arr = g_new(in_addr_t, len); for (i = 0; i < len; i++) { if (nm_utils_parse_inaddr_bin(AF_INET, s_addrs[i], NULL, &tmp_addr)) config_iface_data->ipv4s_arr[config_iface_data->ipv4s_len++] = tmp_addr; } } } else { if (nm_utils_parse_inaddr_prefix_bin(AF_INET, g_bytes_get_data(response_data, NULL), NULL, &tmp_addr, &tmp_prefix)) { nm_assert(!config_iface_data->has_cidr); config_iface_data->has_cidr = TRUE; config_iface_data->cidr_prefix = tmp_prefix; config_iface_data->cidr_addr = tmp_addr; } } } /* If nm_utils_error_is_cancelled(error), then our internal iface_data->cancellable * was cancelled, because the overall request failed. From point of view of the * caller, this does not mean that a cancellation happened. It also means, our * request overall is already about to fail. */ nm_assert(!nm_utils_error_is_cancelled(error) || iface_data->error); iface_data->n_pending--; _get_config_task_maybe_return(iface_data, nm_utils_error_is_cancelled(error) ? NULL : 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); } static void _get_config_fetch_cancelled_cb(GObject *object, gpointer user_data) { GetConfigIfaceData *iface_data = user_data; nm_clear_g_signal_handler(g_task_get_cancellable(iface_data->get_config_data->task), &iface_data->cancelled_id); _get_config_task_maybe_return(iface_data, nm_utils_error_new_cancelled(FALSE, NULL)); } typedef struct { NMCSProviderGetConfigTaskData *get_config_data; GHashTable * response_parsed; } GetConfigMetadataData; typedef struct { gssize iface_idx; char path[0]; } GetConfigMetadataMac; static void _get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer user_data) { GetConfigMetadataData * metadata_data = user_data; GetConfigIfaceData * iface_data; NMCSProviderGetConfigTaskData *get_config_data = metadata_data->get_config_data; gs_unref_hashtable GHashTable *response_parsed = g_steal_pointer(&metadata_data->response_parsed); gs_free_error GError *error = NULL; GCancellable * cancellable; GetConfigMetadataMac *v_mac_data; const char * v_hwaddr; GHashTableIter h_iter; NMHttpClient * http_client; nm_g_slice_free(metadata_data); nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &error); iface_data = g_slice_new(GetConfigIfaceData); *iface_data = (GetConfigIfaceData){ .get_config_data = get_config_data, .n_pending = 0, }; if (nm_utils_error_is_cancelled(error)) { _get_config_task_maybe_return(iface_data, g_steal_pointer(&error)); return; } /* We ignore errors. Only if we got no response at all, it's a problem. * Otherwise, we proceed with whatever we could fetch. */ if (!response_parsed) { _get_config_task_maybe_return( iface_data, nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "meta data for interfaces not found")); return; } cancellable = g_task_get_cancellable(get_config_data->task); if (cancellable) { gulong cancelled_id; cancelled_id = g_cancellable_connect(cancellable, G_CALLBACK(_get_config_fetch_cancelled_cb), iface_data, NULL); if (cancelled_id == 0) { /* the callback was already invoked synchronously and the task already returned. */ return; } iface_data->cancelled_id = cancelled_id; } iface_data->cancellable = g_cancellable_new(); http_client = nmcs_provider_get_http_client(g_task_get_source_object(get_config_data->task)); g_hash_table_iter_init(&h_iter, response_parsed); 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; const char * hwaddr; if (!g_hash_table_lookup_extended(get_config_data->result_dict, v_hwaddr, (gpointer *) &hwaddr, (gpointer *) &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_new(FALSE); g_hash_table_insert(get_config_data->result_dict, (char *) (hwaddr = g_strdup(v_hwaddr)), config_iface_data); } 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, hwaddr, v_mac_data->path); iface_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, iface_data->cancellable, NULL, NULL, _get_config_fetch_done_cb_subnet_ipv4_cidr_block, nm_utils_user_data_pack(iface_data, hwaddr)); iface_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, iface_data->cancellable, NULL, NULL, _get_config_fetch_done_cb_local_ipv4s, nm_utils_user_data_pack(iface_data, hwaddr)); } _get_config_task_maybe_return(iface_data, NULL); } static gboolean _get_config_metadata_ready_check(long response_code, GBytes * response_data, gpointer check_user_data, GError **error) { GetConfigMetadataData *metadata_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_data) { /* we wait longer. */ return FALSE; } r_data = g_bytes_get_data(response_data, &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_data 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); g_hash_table_insert(response_parsed, hwaddr, mac_data); } has_all = TRUE; g_hash_table_iter_init(&h_iter, metadata_data->get_config_data->result_dict); 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(&metadata_data->response_parsed, g_hash_table_unref); metadata_data->response_parsed = g_steal_pointer(&response_parsed); return has_all; } static void get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data) { gs_free char * uri = NULL; GetConfigMetadataData *metadata_data; metadata_data = g_slice_new(GetConfigMetadataData); *metadata_data = (GetConfigMetadataData){ .get_config_data = get_config_data, }; /* 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, g_task_get_cancellable(get_config_data->task), _get_config_metadata_ready_check, metadata_data, _get_config_metadata_ready_cb, metadata_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; }