// 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; gboolean success; success = 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; } if (!success) { nm_utils_error_set (&error, NM_UTILS_ERROR_UNKNOWN, "failure to detect EC2 metadata"); 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; GCancellable *cancellable; gulong cancelled_id; guint n_pending; } GetConfigIfaceData; static void _get_config_task_return (GetConfigIfaceData *iface_data, GError *error_take) { NMCSProviderGetConfigTaskData *get_config_data = iface_data->get_config_data; nm_clear_g_cancellable_disconnect (g_task_get_cancellable (get_config_data->task), &iface_data->cancelled_id); nm_clear_g_cancellable (&iface_data->cancellable); nm_g_slice_free (iface_data); if (error_take) { if (nm_utils_error_is_cancelled (error_take)) _LOGD ("get-config: cancelled"); else _LOGD ("get-config: failed: %s", error_take->message); g_task_return_error (get_config_data->task, error_take); } 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); } 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; NMCSProviderGetConfigTaskData *get_config_data; const char *hwaddr = NULL; gs_unref_bytes GBytes *response_data = NULL; gs_free_error GError *error = NULL; gboolean success; NMCSProviderGetConfigIfaceData *config_iface_data; nm_utils_user_data_unpack (user_data, &iface_data, &hwaddr); success = nm_http_client_poll_get_finish (http_client, result, NULL, &response_data, &error); if (nm_utils_error_is_cancelled (error)) return; get_config_data = iface_data->get_config_data; config_iface_data = g_hash_table_lookup (get_config_data->result_dict, hwaddr); if (success) { in_addr_t tmp_addr; int tmp_prefix; 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 (--iface_data->n_pending > 0) return; _get_config_task_return (iface_data, NULL); } 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; if (iface_data->cancelled_id == 0) return; nm_clear_g_signal_handler (g_task_get_cancellable (iface_data->get_config_data->task), &iface_data->cancelled_id); _get_config_task_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_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_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) { _get_config_task_return (iface_data, nm_utils_error_new_cancelled (FALSE, NULL)); return; } iface_data->cancelled_id = cancelled_id; } iface_data->cancellable = g_cancellable_new (); http_client = nmcs_provider_get_http_client (g_task_get_source_object (get_config_data->task)); g_hash_table_iter_init (&h_iter, response_parsed); 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)); } if (iface_data->n_pending == 0) _get_config_task_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; gsize r_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); while (r_len > 0) { const guint8 *p_eol; const char *p_start; gsize p_start_l; gsize p_start_l_2; char *hwaddr; GetConfigMetadataMac *mac_data; p_start = (const char *) r_data; p_eol = memchr (r_data, '\n', r_len); if (p_eol) { p_start_l = (p_eol - r_data); r_len -= p_start_l + 1; r_data = &p_eol[1]; } else { p_start_l = r_len; r_data = &r_data[r_len]; r_len = 0; } if (p_start_l == 0) continue; p_start_l_2 = p_start_l; if (p_start[p_start_l_2 - 1] == '/') { /* trim the trailing "/". */ p_start_l_2--; } hwaddr = nmcs_utils_hwaddr_normalize (p_start, p_start_l_2); 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) + 1 + p_start_l); mac_data->iface_idx = iface_idx_counter++; memcpy (mac_data->path, p_start, p_start_l); mac_data->path[p_start_l] = '\0'; 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; }