/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2011 Red Hat, Inc. */ #include "libnm-client-aux-extern/nm-default-client.h" #include #include #include "nm-dispatcher/nm-dispatcher-utils.h" #include "libnm-core-aux-extern/nm-dispatcher-api.h" #include "libnm-glib-aux/nm-test-utils.h" #include "nm-dispatcher/nmdbus-dispatcher.h" #define TEST_DIR NM_BUILD_SRCDIR "/src/nm-dispatcher/tests" /*****************************************************************************/ static void _print_env(const char *const *denv, GHashTable *expected_env) { const char *const *iter; GHashTableIter k; const char *key; g_print("\n******* Generated environment:\n"); for (iter = denv; iter && *iter; iter++) g_print(" %s\n", *iter); g_print("\n******* Expected environment:\n"); g_hash_table_iter_init(&k, expected_env); while (g_hash_table_iter_next(&k, (gpointer) &key, NULL)) g_print(" %s\n", key); } static gboolean parse_main(GKeyFile *kf, const char *filename, GVariant **out_con_dict, GVariant **out_con_props, char **out_expected_iface, char **out_action, char **out_connectivity_state, char **out_vpn_ip_iface, GError **error) { nm_auto_clear_variant_builder GVariantBuilder props = {}; gs_free char *uuid = NULL; gs_free char *id = NULL; gs_unref_object NMConnection *connection = NULL; NMSettingConnection *s_con; const char *s; *out_expected_iface = g_key_file_get_string(kf, "main", "expected-iface", NULL); *out_connectivity_state = g_key_file_get_string(kf, "main", "connectivity-state", NULL); *out_vpn_ip_iface = g_key_file_get_string(kf, "main", "vpn-ip-iface", NULL); *out_action = g_key_file_get_string(kf, "main", "action", error); if (*out_action == NULL) return FALSE; uuid = g_key_file_get_string(kf, "main", "uuid", error); if (uuid == NULL) return FALSE; id = g_key_file_get_string(kf, "main", "id", error); if (id == NULL) return FALSE; connection = nm_simple_connection_new(); s_con = (NMSettingConnection *) nm_setting_connection_new(); g_object_set(s_con, NM_SETTING_CONNECTION_UUID, uuid, NM_SETTING_CONNECTION_ID, id, NULL); nm_connection_add_setting(connection, NM_SETTING(s_con)); *out_con_dict = nm_connection_to_dbus(connection, NM_CONNECTION_SERIALIZE_ALL); g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add( &props, "{sv}", NMD_CONNECTION_PROPS_PATH, g_variant_new_object_path("/org/freedesktop/NetworkManager/Connections/5")); /* Strip out the non-fixed portion of the filename */ s = filename; filename = NULL; while ((s = strstr(s, "/src/nm-dispatcher"))) { filename = s; s += 1; } g_assert(filename); g_assert(g_str_has_prefix(filename, "/src/nm-dispatcher")); g_variant_builder_add(&props, "{sv}", "filename", g_variant_new_string(filename)); if (g_key_file_get_boolean(kf, "main", "external", NULL)) { g_variant_builder_add(&props, "{sv}", "external", g_variant_new_boolean(TRUE)); } *out_con_props = g_variant_builder_end(&props); return TRUE; } static gboolean parse_device(GKeyFile *kf, GVariant **out_device_props, GError **error) { nm_auto_clear_variant_builder GVariantBuilder props = {}; gs_free char *tmp = NULL; int i; g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); i = g_key_file_get_integer(kf, "device", "state", error); if (i == 0) return FALSE; g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_STATE, g_variant_new_uint32(i)); i = g_key_file_get_integer(kf, "device", "type", error); if (i == 0) return FALSE; g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_TYPE, g_variant_new_uint32(i)); tmp = g_key_file_get_string(kf, "device", "interface", error); if (tmp == NULL) return FALSE; g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_INTERFACE, g_variant_new_string(tmp)); nm_clear_g_free(&tmp); tmp = g_key_file_get_string(kf, "device", "ip-interface", error); if (tmp == NULL) return FALSE; g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_IP_INTERFACE, g_variant_new_string(tmp)); nm_clear_g_free(&tmp); tmp = g_key_file_get_string(kf, "device", "path", error); if (tmp == NULL) return FALSE; g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_PATH, g_variant_new_object_path(tmp)); *out_device_props = g_variant_builder_end(&props); return TRUE; } static gboolean add_uint_array(GKeyFile *kf, GVariantBuilder *props, const char *section, const char *key, GError **error) { gs_free char *tmp = NULL; gs_free const char **split = NULL; gsize i; tmp = g_key_file_get_string(kf, section, key, NULL); if (tmp == NULL) return TRUE; split = nm_strsplit_set_with_empty(tmp, " "); if (split) { gs_unref_array GArray *items = NULL; items = g_array_sized_new(FALSE, TRUE, sizeof(guint32), NM_PTRARRAY_LEN(split)); for (i = 0; split[i]; i++) { const char *s; s = split[i]; g_strstrip((char *) s); if (s[0]) { guint32 addr; if (inet_pton(AF_INET, s, &addr) != 1) g_assert_not_reached(); g_array_append_val(items, addr); } } g_variant_builder_add(props, "{sv}", key, g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, items->data, items->len, sizeof(guint32))); } return TRUE; } static gboolean parse_proxy(GKeyFile *kf, GVariant **out_props, const char *section, GError **error) { nm_auto_clear_variant_builder GVariantBuilder props = {}; gs_free char *tmp = NULL; g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); tmp = g_key_file_get_string(kf, section, "pac-url", error); if (tmp == NULL) return FALSE; g_variant_builder_add(&props, "{sv}", "pac-url", g_variant_new_string(tmp)); nm_clear_g_free(&tmp); tmp = g_key_file_get_string(kf, section, "pac-script", error); if (tmp == NULL) return FALSE; g_variant_builder_add(&props, "{sv}", "pac-script", g_variant_new_string(tmp)); *out_props = g_variant_builder_end(&props); return TRUE; } static gboolean parse_ip4(GKeyFile *kf, GVariant **out_props, const char *section, GError **error) { nm_auto_clear_variant_builder GVariantBuilder props = {}; gs_free char *tmp = NULL; gs_free const char **split = NULL; const char **iter; g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); /* search domains */ /* Use char** for domains. (DBUS_TYPE_G_ARRAY_OF_STRING of NMIP4Config * becomes G_TYPE_STRV when sending the value over D-Bus) */ tmp = g_key_file_get_string(kf, section, "domains", error); if (tmp == NULL) return FALSE; split = nm_strsplit_set_with_empty(tmp, " "); if (split) { for (iter = split; *iter; iter++) g_strstrip((char *) *iter); g_variant_builder_add(&props, "{sv}", "domains", g_variant_new_strv((gpointer) split, -1)); } nm_clear_g_free(&split); if (!add_uint_array(kf, &props, "ip4", "nameservers", error)) return FALSE; if (!add_uint_array(kf, &props, "ip4", "wins-servers", error)) return FALSE; nm_clear_g_free(&tmp); tmp = g_key_file_get_string(kf, section, "addresses", error); if (tmp == NULL) return FALSE; split = nm_strsplit_set_with_empty(tmp, ","); if (split) { gs_unref_ptrarray GPtrArray *addresses = NULL; const char *gateway = NULL; addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref); for (iter = split; *iter; iter++) { const char *s = *iter; NMIPAddress *addr; const char *ip; const char *prefix; g_strstrip((char *) s); if (s[0] == '\0') continue; ip = *iter; prefix = strchr(ip, '/'); g_assert(prefix); ((char *) (prefix++))[0] = '\0'; if (addresses->len == 0) { gateway = strchr(prefix, ' '); g_assert(gateway); gateway++; } addr = nm_ip_address_new(AF_INET, ip, (guint) atoi(prefix), error); if (!addr) return FALSE; g_ptr_array_add(addresses, addr); } g_variant_builder_add(&props, "{sv}", "addresses", nm_utils_ip4_addresses_to_variant(addresses, gateway)); } nm_clear_g_free(&split); nm_clear_g_free(&tmp); tmp = g_key_file_get_string(kf, section, "routes", NULL); split = nm_strsplit_set_with_empty(tmp, ","); if (split) { gs_unref_ptrarray GPtrArray *routes = NULL; routes = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_route_unref); for (iter = split; *iter; iter++) { const char *s = *iter; NMIPRoute *route; const char *dest; const char *prefix; const char *next_hop; const char *metric; g_strstrip((char *) s); if (s[0] == '\0') continue; dest = s; prefix = strchr(dest, '/'); g_assert(prefix); ((char *) (prefix++))[0] = '\0'; next_hop = strchr(prefix, ' '); g_assert(next_hop); ((char *) (next_hop++))[0] = '\0'; metric = strchr(next_hop, ' '); g_assert(metric); ((char *) (metric++))[0] = '\0'; route = nm_ip_route_new(AF_INET, dest, _nm_utils_ascii_str_to_int64(prefix, 10, 0, 32, 255), next_hop, (guint) atoi(metric), error); if (!route) return FALSE; g_ptr_array_add(routes, route); } g_variant_builder_add(&props, "{sv}", "routes", nm_utils_ip4_routes_to_variant(routes)); } *out_props = g_variant_builder_end(&props); return TRUE; } static gboolean parse_dhcp(GKeyFile *kf, const char *group_name, GVariant **out_props, GError **error) { nm_auto_clear_variant_builder GVariantBuilder props = {}; gs_strfreev char **keys = NULL; char **iter; keys = g_key_file_get_keys(kf, group_name, NULL, error); if (!keys) return FALSE; g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); for (iter = keys; iter && *iter; iter++) { gs_free char *val = NULL; val = g_key_file_get_string(kf, group_name, *iter, error); if (!val) return FALSE; g_variant_builder_add(&props, "{sv}", *iter, g_variant_new_string(val)); } *out_props = g_variant_builder_end(&props); return TRUE; } static gboolean get_dispatcher_file(const char *file, GVariant **out_con_dict, GVariant **out_con_props, GVariant **out_device_props, GVariant **out_device_proxy_props, GVariant **out_device_ip4_props, GVariant **out_device_ip6_props, GVariant **out_device_dhcp4_props, GVariant **out_device_dhcp6_props, char **out_connectivity_state, char **out_vpn_ip_iface, GVariant **out_vpn_proxy_props, GVariant **out_vpn_ip4_props, GVariant **out_vpn_ip6_props, char **out_expected_iface, char **out_action, GHashTable **out_env, GError **error) { nm_auto_unref_keyfile GKeyFile *kf = NULL; gs_strfreev char **keys = NULL; char **iter; g_assert(!error || !*error); g_assert(out_con_dict && !*out_con_dict); g_assert(out_con_props && !*out_con_props); g_assert(out_device_props && !*out_device_props); g_assert(out_device_proxy_props && !*out_device_proxy_props); g_assert(out_device_ip4_props && !*out_device_ip4_props); g_assert(out_device_ip6_props && !*out_device_ip6_props); g_assert(out_device_dhcp4_props && !*out_device_dhcp4_props); g_assert(out_device_dhcp6_props && !*out_device_dhcp6_props); g_assert(out_connectivity_state && !*out_connectivity_state); g_assert(out_vpn_ip_iface && !*out_vpn_ip_iface); g_assert(out_vpn_proxy_props && !*out_vpn_proxy_props); g_assert(out_vpn_ip4_props && !*out_vpn_ip4_props); g_assert(out_vpn_ip6_props && !*out_vpn_ip6_props); g_assert(out_expected_iface && !*out_expected_iface); g_assert(out_action && !*out_action); g_assert(out_env && !*out_env); kf = g_key_file_new(); if (!g_key_file_load_from_file(kf, file, G_KEY_FILE_NONE, error)) return FALSE; if (!parse_main(kf, file, out_con_dict, out_con_props, out_expected_iface, out_action, out_connectivity_state, out_vpn_ip_iface, error)) return FALSE; if (!parse_device(kf, out_device_props, error)) return FALSE; if (g_key_file_has_group(kf, "proxy")) { if (!parse_proxy(kf, out_device_proxy_props, "proxy", error)) return FALSE; } if (g_key_file_has_group(kf, "ip4")) { if (!parse_ip4(kf, out_device_ip4_props, "ip4", error)) return FALSE; } if (g_key_file_has_group(kf, "dhcp4")) { if (!parse_dhcp(kf, "dhcp4", out_device_dhcp4_props, error)) return FALSE; } if (g_key_file_has_group(kf, "dhcp6")) { if (!parse_dhcp(kf, "dhcp6", out_device_dhcp6_props, error)) return FALSE; } g_assert(g_key_file_has_group(kf, "env")); keys = g_key_file_get_keys(kf, "env", NULL, error); *out_env = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); for (iter = keys; iter && *iter; iter++) { gs_free char *val = NULL; val = g_key_file_get_string(kf, "env", *iter, error); if (!val) return FALSE; g_hash_table_insert(*out_env, g_strdup_printf("%s=%s", *iter, val), GUINT_TO_POINTER(1)); } return TRUE; } /*****************************************************************************/ static void test_generic(const char *file, const char *override_vpn_ip_iface) { gs_unref_variant GVariant *con_dict = NULL; gs_unref_variant GVariant *con_props = NULL; gs_unref_variant GVariant *device_props = NULL; gs_unref_variant GVariant *device_proxy_props = NULL; gs_unref_variant GVariant *device_ip4_props = NULL; gs_unref_variant GVariant *device_ip6_props = NULL; gs_unref_variant GVariant *device_dhcp4_props = NULL; gs_unref_variant GVariant *device_dhcp6_props = NULL; gs_free char *connectivity_change = NULL; gs_free char *vpn_ip_iface = NULL; gs_unref_variant GVariant *vpn_proxy_props = NULL; gs_unref_variant GVariant *vpn_ip4_props = NULL; gs_unref_variant GVariant *vpn_ip6_props = NULL; gs_free char *expected_iface = NULL; gs_free char *action = NULL; gs_free char *out_iface = NULL; const char *error_message = NULL; gs_unref_hashtable GHashTable *expected_env = NULL; GError *error = NULL; gboolean success; gs_free char *filename = NULL; gs_strfreev char **denv = NULL; char **iter; filename = g_build_filename(TEST_DIR, file, NULL); success = get_dispatcher_file(filename, &con_dict, &con_props, &device_props, &device_proxy_props, &device_ip4_props, &device_ip6_props, &device_dhcp4_props, &device_dhcp6_props, &connectivity_change, &vpn_ip_iface, &vpn_proxy_props, &vpn_ip4_props, &vpn_ip6_props, &expected_iface, &action, &expected_env, &error); nmtst_assert_success(success, error); /* Get the environment from the dispatcher code */ denv = nm_dispatcher_utils_construct_envp(action, con_dict, con_props, device_props, device_proxy_props, device_ip4_props, device_ip6_props, device_dhcp4_props, device_dhcp6_props, connectivity_change, override_vpn_ip_iface ?: vpn_ip_iface, vpn_proxy_props, vpn_ip4_props, vpn_ip6_props, &out_iface, &error_message); g_assert((!denv && error_message) || (denv && !error_message)); if (error_message) { NM_PRAGMA_WARNING_DISABLE_DANGLING_POINTER g_error("FAILED: %s", error_message); NM_PRAGMA_WARNING_REENABLE } if (g_strv_length(denv) != g_hash_table_size(expected_env)) { _print_env(NM_CAST_STRV_CC(denv), expected_env); g_assert_cmpint(g_strv_length(denv), ==, g_hash_table_size(expected_env)); } /* Compare dispatcher generated env and expected env */ for (iter = denv; iter && *iter; iter++) { gpointer foo; const char *i_value = *iter; if (strstr(i_value, "PATH=") == i_value) { g_assert_cmpstr(&i_value[strlen("PATH=")], ==, g_getenv("PATH")); /* The path is constructed dynamically. Ignore the actual value. */ i_value = "PATH="; } foo = g_hash_table_lookup(expected_env, i_value); if (!foo) { NM_PRAGMA_WARNING_DISABLE_DANGLING_POINTER _print_env(NM_CAST_STRV_CC(denv), expected_env); g_error("Failed to find %s in environment", i_value); NM_PRAGMA_WARNING_REENABLE } } g_assert_cmpstr(expected_iface, ==, out_iface); } /*****************************************************************************/ static void test_up(void) { test_generic("dispatcher-up", NULL); } static void test_down(void) { test_generic("dispatcher-down", NULL); } static void test_vpn_up(void) { test_generic("dispatcher-vpn-up", NULL); } static void test_vpn_down(void) { test_generic("dispatcher-vpn-down", NULL); } static void test_external(void) { test_generic("dispatcher-external", NULL); } static void test_connectivity_changed(void) { /* These tests will check that the CONNECTIVITY_STATE environment * variable is only defined for known states, such as 'full'. */ test_generic("dispatcher-connectivity-unknown", NULL); test_generic("dispatcher-connectivity-full", NULL); } static void test_up_empty_vpn_iface(void) { /* Test that an empty VPN iface variable, like is passed through D-Bus * from NM, is ignored by the dispatcher environment construction code. */ test_generic("dispatcher-up", ""); } /*****************************************************************************/ static void test_gdbus_codegen(void) { gs_unref_object NMDBusDispatcher *dbus_dispatcher = NULL; dbus_dispatcher = nmdbus_dispatcher_skeleton_new(); g_assert(NMDBUS_IS_DISPATCHER_SKELETON(dbus_dispatcher)); } /*****************************************************************************/ NMTST_DEFINE(); int main(int argc, char **argv) { nmtst_init(&argc, &argv, TRUE); g_test_add_func("/dispatcher/up", test_up); g_test_add_func("/dispatcher/down", test_down); g_test_add_func("/dispatcher/vpn_up", test_vpn_up); g_test_add_func("/dispatcher/vpn_down", test_vpn_down); g_test_add_func("/dispatcher/external", test_external); g_test_add_func("/dispatcher/connectivity_changed", test_connectivity_changed); g_test_add_func("/dispatcher/up_empty_vpn_iface", test_up_empty_vpn_iface); g_test_add_func("/dispatcher/gdbus-codegen", test_gdbus_codegen); return g_test_run(); }