diff options
Diffstat (limited to 'src/nm-dispatcher')
| -rw-r--r-- | src/nm-dispatcher/meson.build | 51 | ||||
| -rw-r--r-- | src/nm-dispatcher/nm-dispatcher-utils.c | 581 | ||||
| -rw-r--r-- | src/nm-dispatcher/nm-dispatcher-utils.h | 26 | ||||
| -rw-r--r-- | src/nm-dispatcher/nm-dispatcher.c | 1091 | ||||
| -rw-r--r-- | src/nm-dispatcher/nm-dispatcher.conf | 13 | ||||
| -rw-r--r-- | src/nm-dispatcher/nm-dispatcher.xml | 46 | ||||
| -rw-r--r-- | src/nm-dispatcher/org.freedesktop.nm_dispatcher.service.in | 6 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-connectivity-full | 17 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-connectivity-unknown | 16 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-down | 23 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-external | 40 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-up | 66 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-vpn-down | 65 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/dispatcher-vpn-up | 65 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/meson.build | 27 | ||||
| -rw-r--r-- | src/nm-dispatcher/tests/test-dispatcher-envp.c | 645 |
16 files changed, 2778 insertions, 0 deletions
diff --git a/src/nm-dispatcher/meson.build b/src/nm-dispatcher/meson.build new file mode 100644 index 00000000..eb3ea777 --- /dev/null +++ b/src/nm-dispatcher/meson.build @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +dispatcher_inc = include_directories('.') + +configure_file( + input: 'org.freedesktop.nm_dispatcher.service.in', + output: '@BASENAME@', + install_dir: dbus_system_bus_services_dir, + configuration: data_conf, +) + +install_data( + 'nm-dispatcher.conf', + install_dir: dbus_conf_dir, +) + +dispatcher_nmdbus_dispatcher_sources = gnome.gdbus_codegen( + 'nmdbus-dispatcher', + 'nm-dispatcher.xml', + interface_prefix: 'org.freedesktop', + namespace: 'NMDBus', +) + +libnm_dispatcher_core = static_library( + 'nm-dispatcher-core', + sources: 'nm-dispatcher-utils.c', + dependencies: [ + libnm_dep, + ], +) + +executable( + 'nm-dispatcher', + 'nm-dispatcher.c', + dependencies: [ + libnm_dep, + glib_dep, + ], + link_with: [ + libnm_core_aux_extern, + libnm_dispatcher_core, + 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-dispatcher/nm-dispatcher-utils.c b/src/nm-dispatcher/nm-dispatcher-utils.c new file mode 100644 index 00000000..f754a3fa --- /dev/null +++ b/src/nm-dispatcher/nm-dispatcher-utils.c @@ -0,0 +1,581 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2008 - 2011 Red Hat, Inc. + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-dispatcher-utils.h" + +#include "nm-dbus-interface.h" +#include "nm-connection.h" +#include "nm-setting-ip4-config.h" +#include "nm-setting-ip6-config.h" +#include "nm-setting-connection.h" + +#include "libnm-core-aux-extern/nm-dispatcher-api.h" +#include "nm-utils.h" + +/*****************************************************************************/ + +static gboolean +_is_valid_key(const char *line, gssize len) +{ + gsize i, l; + char ch; + + if (!line) + return FALSE; + + if (len < 0) + len = strlen(line); + + if (len == 0) + return FALSE; + + ch = line[0]; + if (!(ch >= 'A' && ch <= 'Z') && !NM_IN_SET(ch, '_')) + return FALSE; + + l = (gsize) len; + + for (i = 1; i < l; i++) { + ch = line[i]; + + if (!(ch >= 'A' && ch <= 'Z') && !(ch >= '0' && ch <= '9') && !NM_IN_SET(ch, '_')) + return FALSE; + } + + return TRUE; +} + +static gboolean +_is_valid_line(const char *line) +{ + const char *d; + + if (!line) + return FALSE; + + d = strchr(line, '='); + if (!d || d == line) + return FALSE; + + return _is_valid_key(line, d - line); +} + +static char * +_sanitize_var_name(const char *key) +{ + char *sanitized; + + nm_assert(key); + + if (!key[0]) + return NULL; + + sanitized = g_ascii_strup(key, -1); + if (!NM_STRCHAR_ALL(sanitized, + ch, + (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') + || NM_IN_SET(ch, '_'))) { + g_free(sanitized); + return NULL; + } + + nm_assert(_is_valid_key(sanitized, -1)); + return sanitized; +} + +static void +_items_add_str_take(GPtrArray *items, char *line) +{ + nm_assert(items); + nm_assert(_is_valid_line(line)); + + g_ptr_array_add(items, line); +} + +static void +_items_add_str(GPtrArray *items, const char *line) +{ + _items_add_str_take(items, g_strdup(line)); +} + +static void +_items_add_key(GPtrArray *items, const char *prefix, const char *key, const char *value) +{ + nm_assert(items); + nm_assert(_is_valid_key(key, -1)); + nm_assert(value); + + _items_add_str_take(items, g_strconcat(prefix ?: "", key, "=", value, NULL)); +} + +static void +_items_add_key0(GPtrArray *items, const char *prefix, const char *key, const char *value) +{ + nm_assert(items); + nm_assert(_is_valid_key(key, -1)); + + if (!value) { + /* for convenience, allow NULL values to indicate to skip the line. */ + return; + } + + _items_add_str_take(items, g_strconcat(prefix ?: "", key, "=", value, NULL)); +} + +G_GNUC_PRINTF(2, 3) +static void +_items_add_printf(GPtrArray *items, const char *fmt, ...) +{ + va_list ap; + char * line; + + nm_assert(items); + nm_assert(fmt); + + va_start(ap, fmt); + line = g_strdup_vprintf(fmt, ap); + va_end(ap); + _items_add_str_take(items, line); +} + +static void +_items_add_strv(GPtrArray *items, const char *prefix, const char *key, const char *const *values) +{ + gboolean has; + guint i; + GString *str; + + nm_assert(items); + nm_assert(_is_valid_key(key, -1)); + + if (!values || !values[0]) { + /* Only add an item if the list of @values is not empty */ + return; + } + + str = g_string_new(NULL); + + if (prefix) + g_string_append(str, prefix); + g_string_append(str, key); + g_string_append_c(str, '='); + + has = FALSE; + for (i = 0; values[i]; i++) { + if (!values[i][0]) + continue; + if (has) + g_string_append_c(str, ' '); + else + has = TRUE; + g_string_append(str, values[i]); + } + + _items_add_str_take(items, g_string_free(str, FALSE)); +} + +/*****************************************************************************/ + +static void +construct_proxy_items(GPtrArray *items, GVariant *proxy_config, const char *prefix) +{ + GVariant *variant; + + nm_assert(items); + + if (!proxy_config) + return; + + variant = g_variant_lookup_value(proxy_config, "pac-url", G_VARIANT_TYPE_STRING); + if (variant) { + _items_add_key(items, prefix, "PROXY_PAC_URL", g_variant_get_string(variant, NULL)); + g_variant_unref(variant); + } + + variant = g_variant_lookup_value(proxy_config, "pac-script", G_VARIANT_TYPE_STRING); + if (variant) { + _items_add_key(items, prefix, "PROXY_PAC_SCRIPT", g_variant_get_string(variant, NULL)); + g_variant_unref(variant); + } +} + +static void +construct_ip_items(GPtrArray *items, int addr_family, GVariant *ip_config, const char *prefix) +{ + GVariant *val; + guint i; + guint nroutes = 0; + char four_or_six; + + if (!ip_config) + return; + + if (!prefix) + prefix = ""; + + four_or_six = nm_utils_addr_family_to_char(addr_family); + + val = g_variant_lookup_value(ip_config, + "addresses", + addr_family == AF_INET ? G_VARIANT_TYPE("aau") + : G_VARIANT_TYPE("a(ayuay)")); + if (val) { + gs_unref_ptrarray GPtrArray *addresses = NULL; + gs_free char * gateway_free = NULL; + const char * gateway; + + if (addr_family == AF_INET) + addresses = nm_utils_ip4_addresses_from_variant(val, &gateway_free); + else + addresses = nm_utils_ip6_addresses_from_variant(val, &gateway_free); + + gateway = gateway_free ?: "0.0.0.0"; + + if (addresses && addresses->len) { + for (i = 0; i < addresses->len; i++) { + NMIPAddress *addr = addresses->pdata[i]; + + _items_add_printf(items, + "%sIP%c_ADDRESS_%d=%s/%d %s", + prefix, + four_or_six, + i, + nm_ip_address_get_address(addr), + nm_ip_address_get_prefix(addr), + gateway); + } + + _items_add_printf(items, + "%sIP%c_NUM_ADDRESSES=%u", + prefix, + four_or_six, + addresses->len); + } + + _items_add_key(items, + prefix, + addr_family == AF_INET ? "IP4_GATEWAY" : "IP6_GATEWAY", + gateway); + + g_variant_unref(val); + } + + val = g_variant_lookup_value(ip_config, + "nameservers", + addr_family == AF_INET ? G_VARIANT_TYPE("au") + : G_VARIANT_TYPE("aay")); + if (val) { + gs_strfreev char **v = NULL; + + if (addr_family == AF_INET) + v = nm_utils_ip4_dns_from_variant(val); + else + v = nm_utils_ip6_dns_from_variant(val); + _items_add_strv(items, + prefix, + addr_family == AF_INET ? "IP4_NAMESERVERS" : "IP6_NAMESERVERS", + NM_CAST_STRV_CC(v)); + g_variant_unref(val); + } + + val = g_variant_lookup_value(ip_config, "domains", G_VARIANT_TYPE_STRING_ARRAY); + if (val) { + gs_free const char **v = NULL; + + v = g_variant_get_strv(val, NULL); + _items_add_strv(items, prefix, addr_family == AF_INET ? "IP4_DOMAINS" : "IP6_DOMAINS", v); + g_variant_unref(val); + } + + if (addr_family == AF_INET) { + val = g_variant_lookup_value(ip_config, "wins-servers", G_VARIANT_TYPE("au")); + if (val) { + gs_strfreev char **v = NULL; + + v = nm_utils_ip4_dns_from_variant(val); + _items_add_strv(items, prefix, "IP4_WINS_SERVERS", NM_CAST_STRV_CC(v)); + g_variant_unref(val); + } + } + + val = g_variant_lookup_value(ip_config, + "routes", + addr_family == AF_INET ? G_VARIANT_TYPE("aau") + : G_VARIANT_TYPE("a(ayuayu)")); + if (val) { + gs_unref_ptrarray GPtrArray *routes = NULL; + + if (addr_family == AF_INET) + routes = nm_utils_ip4_routes_from_variant(val); + else + routes = nm_utils_ip6_routes_from_variant(val); + + if (routes && routes->len > 0) { + const char *const DEFAULT_GW = addr_family == AF_INET ? "0.0.0.0" : "::"; + + nroutes = routes->len; + + for (i = 0; i < routes->len; i++) { + NMIPRoute *route = routes->pdata[i]; + + _items_add_printf(items, + "%sIP%c_ROUTE_%u=%s/%d %s %u", + prefix, + four_or_six, + i, + nm_ip_route_get_dest(route), + nm_ip_route_get_prefix(route), + nm_ip_route_get_next_hop(route) ?: DEFAULT_GW, + (guint) NM_MAX((gint64) 0, nm_ip_route_get_metric(route))); + } + } + + g_variant_unref(val); + } + if (nroutes > 0 || addr_family == AF_INET) { + /* we also set IP4_NUM_ROUTES=0, but don't do so for addresses and IPv6 routes. + * Historic reasons. */ + _items_add_printf(items, "%sIP%c_NUM_ROUTES=%u", prefix, four_or_six, nroutes); + } +} + +static void +construct_device_dhcp_items(GPtrArray *items, int addr_family, GVariant *dhcp_config) +{ + GVariantIter iter; + const char * key; + GVariant * val; + char four_or_six; + gboolean found_unknown_245 = FALSE; + gs_unref_variant GVariant *private_245_val = NULL; + + if (!dhcp_config) + return; + + if (!g_variant_is_of_type(dhcp_config, G_VARIANT_TYPE_VARDICT)) + return; + + four_or_six = nm_utils_addr_family_to_char(addr_family); + + g_variant_iter_init(&iter, dhcp_config); + while (g_variant_iter_next(&iter, "{&sv}", &key, &val)) { + if (g_variant_is_of_type(val, G_VARIANT_TYPE_STRING)) { + gs_free char *ucased = NULL; + + ucased = _sanitize_var_name(key); + if (ucased) { + _items_add_printf(items, + "DHCP%c_%s=%s", + four_or_six, + ucased, + g_variant_get_string(val, NULL)); + + /* MS Azure sends the server endpoint in the dhcp private + * option 245. cloud-init searches the Azure server endpoint + * value looking for the standard dhclient label used for + * that option, which is "unknown_245". + * The 11-dhclient script shipped with Fedora and RHEL dhcp + * package converts our dispatcher environment vars to the + * dhclient ones (new_<some_option>) and calls dhclient hook + * scripts. + * Let's make cloud-init happy and let's duplicate the dhcp + * option 245 with the legacy name of the default dhclient + * label also when using the internal client. + * Note however that the dhclient plugin will have unknown_ + * labels represented as ascii string when possible, falling + * back to hex string otherwise. + * private_ labels instead are always in hex string format. + * This shouldn't affect the MS Azure server endpoint value, + * as it usually belongs to the 240.0.0.0/4 network and so + * is always represented as an hex string. Moreover, cloudinit + * code checks just for an hex value in unknown_245. + */ + if (addr_family == AF_INET) { + if (nm_streq(key, "private_245")) + private_245_val = g_variant_ref(val); + else if (nm_streq(key, "unknown_245")) + found_unknown_245 = true; + } + } + } + g_variant_unref(val); + } + + if (private_245_val != NULL && !found_unknown_245) { + _items_add_printf(items, + "DHCP4_UNKNOWN_245=%s", + g_variant_get_string(private_245_val, NULL)); + } +} + +/*****************************************************************************/ + +char ** +nm_dispatcher_utils_construct_envp(const char * action, + GVariant * connection_dict, + GVariant * connection_props, + GVariant * device_props, + GVariant * device_proxy_props, + GVariant * device_ip4_props, + GVariant * device_ip6_props, + GVariant * device_dhcp4_props, + GVariant * device_dhcp6_props, + const char * connectivity_state, + const char * vpn_ip_iface, + GVariant * vpn_proxy_props, + GVariant * vpn_ip4_props, + GVariant * vpn_ip6_props, + char ** out_iface, + const char **out_error_message) +{ + const char * iface = NULL; + const char * ip_iface = NULL; + const char * uuid = NULL; + const char * id = NULL; + const char * path = NULL; + const char * filename = NULL; + gboolean external; + NMDeviceState dev_state = NM_DEVICE_STATE_UNKNOWN; + GVariant * variant; + gs_unref_ptrarray GPtrArray *items = NULL; + const char * error_message_backup; + + if (!out_error_message) + out_error_message = &error_message_backup; + + g_return_val_if_fail(action != NULL, NULL); + g_return_val_if_fail(out_iface != NULL, NULL); + g_return_val_if_fail(*out_iface == NULL, NULL); + + items = g_ptr_array_new_with_free_func(g_free); + + /* Hostname and connectivity changes don't require a device nor contain a connection */ + if (NM_IN_STRSET(action, NMD_ACTION_HOSTNAME, NMD_ACTION_CONNECTIVITY_CHANGE)) + goto done; + + /* Connection properties */ + if (g_variant_lookup(connection_props, NMD_CONNECTION_PROPS_PATH, "&o", &path)) + _items_add_key(items, NULL, "CONNECTION_DBUS_PATH", path); + + if (g_variant_lookup(connection_props, NMD_CONNECTION_PROPS_EXTERNAL, "b", &external) + && external) + _items_add_str(items, "CONNECTION_EXTERNAL=1"); + + if (g_variant_lookup(connection_props, NMD_CONNECTION_PROPS_FILENAME, "&s", &filename)) + _items_add_key(items, NULL, "CONNECTION_FILENAME", filename); + + /* Canonicalize the VPN interface name; "" is used when passing it through + * D-Bus so make sure that's fixed up here. + */ + if (vpn_ip_iface && !vpn_ip_iface[0]) + vpn_ip_iface = NULL; + + if (!g_variant_lookup(device_props, NMD_DEVICE_PROPS_INTERFACE, "&s", &iface)) { + *out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_INTERFACE "!"; + return NULL; + } + if (!*iface) + iface = NULL; + + variant = g_variant_lookup_value(device_props, NMD_DEVICE_PROPS_IP_INTERFACE, NULL); + if (variant) { + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) { + *out_error_message = "Invalid value " NMD_DEVICE_PROPS_IP_INTERFACE "!"; + return NULL; + } + g_variant_unref(variant); + (void) g_variant_lookup(device_props, NMD_DEVICE_PROPS_IP_INTERFACE, "&s", &ip_iface); + } + + if (!g_variant_lookup(device_props, NMD_DEVICE_PROPS_TYPE, "u", NULL)) { + *out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_TYPE "!"; + return NULL; + } + + variant = g_variant_lookup_value(device_props, NMD_DEVICE_PROPS_STATE, G_VARIANT_TYPE_UINT32); + if (!variant) { + *out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_STATE "!"; + return NULL; + } + dev_state = g_variant_get_uint32(variant); + g_variant_unref(variant); + + if (!g_variant_lookup(device_props, NMD_DEVICE_PROPS_PATH, "o", NULL)) { + *out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_PATH "!"; + return NULL; + } + + { + gs_unref_variant GVariant *con_setting = NULL; + + con_setting = g_variant_lookup_value(connection_dict, + NM_SETTING_CONNECTION_SETTING_NAME, + NM_VARIANT_TYPE_SETTING); + if (!con_setting) { + *out_error_message = "Failed to read connection setting"; + return NULL; + } + + if (!g_variant_lookup(con_setting, NM_SETTING_CONNECTION_UUID, "&s", &uuid)) { + *out_error_message = "Connection hash did not contain the UUID"; + return NULL; + } + + if (!g_variant_lookup(con_setting, NM_SETTING_CONNECTION_ID, "&s", &id)) { + *out_error_message = "Connection hash did not contain the ID"; + return NULL; + } + + _items_add_key0(items, NULL, "CONNECTION_UUID", uuid); + _items_add_key0(items, NULL, "CONNECTION_ID", id); + _items_add_key0(items, NULL, "DEVICE_IFACE", iface); + _items_add_key0(items, NULL, "DEVICE_IP_IFACE", ip_iface); + } + + /* Device items aren't valid if the device isn't activated */ + if (iface && dev_state == NM_DEVICE_STATE_ACTIVATED) { + construct_proxy_items(items, device_proxy_props, NULL); + construct_ip_items(items, AF_INET, device_ip4_props, NULL); + construct_ip_items(items, AF_INET6, device_ip6_props, NULL); + construct_device_dhcp_items(items, AF_INET, device_dhcp4_props); + construct_device_dhcp_items(items, AF_INET6, device_dhcp6_props); + } + + if (vpn_ip_iface) { + _items_add_key(items, NULL, "VPN_IP_IFACE", vpn_ip_iface); + construct_proxy_items(items, vpn_proxy_props, "VPN_"); + construct_ip_items(items, AF_INET, vpn_ip4_props, "VPN_"); + construct_ip_items(items, AF_INET6, vpn_ip6_props, "VPN_"); + } + + /* Backwards compat: 'iface' is set in this order: + * 1) VPN interface name + * 2) Device IP interface name + * 3) Device interface anme + */ + if (vpn_ip_iface) + *out_iface = g_strdup(vpn_ip_iface); + else if (ip_iface) + *out_iface = g_strdup(ip_iface); + else + *out_iface = g_strdup(iface); + +done: + /* The connectivity_state value will only be meaningful for 'connectivity-change' events + * (otherwise it will be "UNKNOWN"), so we only set the environment variable in those cases. + */ + if (!NM_IN_STRSET(connectivity_state, NULL, "UNKNOWN")) + _items_add_key(items, NULL, "CONNECTIVITY_STATE", connectivity_state); + + _items_add_key0(items, NULL, "PATH", g_getenv("PATH")); + + _items_add_key(items, NULL, "NM_DISPATCHER_ACTION", action); + + *out_error_message = NULL; + g_ptr_array_add(items, NULL); + return (char **) g_ptr_array_free(g_steal_pointer(&items), FALSE); +} diff --git a/src/nm-dispatcher/nm-dispatcher-utils.h b/src/nm-dispatcher/nm-dispatcher-utils.h new file mode 100644 index 00000000..cb40d146 --- /dev/null +++ b/src/nm-dispatcher/nm-dispatcher-utils.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2008 - 2011 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DISPATCHER_UTILS_H__ +#define __NETWORKMANAGER_DISPATCHER_UTILS_H__ + +char **nm_dispatcher_utils_construct_envp(const char * action, + GVariant * connection_dict, + GVariant * connection_props, + GVariant * device_props, + GVariant * device_proxy_props, + GVariant * device_ip4_props, + GVariant * device_ip6_props, + GVariant * device_dhcp4_props, + GVariant * device_dhcp6_props, + const char * connectivity_state, + const char * vpn_ip_iface, + GVariant * vpn_proxy_props, + GVariant * vpn_ip4_props, + GVariant * vpn_ip6_props, + char ** out_iface, + const char **out_error_message); + +#endif /* __NETWORKMANAGER_DISPATCHER_UTILS_H__ */ diff --git a/src/nm-dispatcher/nm-dispatcher.c b/src/nm-dispatcher/nm-dispatcher.c new file mode 100644 index 00000000..5df32959 --- /dev/null +++ b/src/nm-dispatcher/nm-dispatcher.c @@ -0,0 +1,1091 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2008 - 2012 Red Hat, Inc. + */ + +#define G_LOG_DOMAIN "nm-dispatcher" + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include <syslog.h> +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <sys/types.h> +#include <signal.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <arpa/inet.h> +#include <glib-unix.h> + +#include "libnm-core-aux-extern/nm-dispatcher-api.h" +#include "nm-dispatcher-utils.h" + +/*****************************************************************************/ + +typedef struct Request Request; + +static struct { + GDBusConnection *dbus_connection; + GMainLoop * loop; + gboolean debug; + gboolean persist; + guint quit_id; + guint request_id_counter; + gboolean ever_acquired_name; + bool exit_with_failure; + + Request *current_request; + GQueue * requests_waiting; + int num_requests_pending; +} gl; + +typedef struct { + Request *request; + + char * script; + GPid pid; + DispatchResult result; + char * error; + gboolean wait; + gboolean dispatched; + guint watch_id; + guint timeout_id; +} ScriptInfo; + +struct Request { + guint request_id; + + GDBusMethodInvocation *context; + char * action; + char * iface; + char ** envp; + gboolean debug; + + GPtrArray *scripts; /* list of ScriptInfo */ + guint idx; + int num_scripts_done; + int num_scripts_nowait; +}; + +/*****************************************************************************/ + +#define __LOG_print(print_cmd, ...) \ + G_STMT_START \ + { \ + if (FALSE) { \ + /* g_message() alone does not warn about invalid format. Add a dummy printf() statement to + * get a compiler warning about wrong format. */ \ + printf(__VA_ARGS__); \ + } \ + print_cmd(__VA_ARGS__); \ + } \ + G_STMT_END + +#define __LOG_print_R(print_cmd, _request, ...) \ + G_STMT_START \ + { \ + __LOG_print(print_cmd, \ + "req:%u '%s'%s%s%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + (_request)->request_id, \ + (_request)->action, \ + (_request)->iface ? " [" : "", \ + (_request)->iface ?: "", \ + (_request)->iface ? "]" : "" _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + G_STMT_END + +#define __LOG_print_S(print_cmd, _request, _script, ...) \ + G_STMT_START \ + { \ + __LOG_print_R(print_cmd, \ + (_request), \ + "%s%s%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + (_script) ? ", \"" : "", \ + (_script) ? (_script)->script : "", \ + (_script) ? "\"" : "" _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + G_STMT_END + +#define _LOG_X_(enabled_cmd, print_cmd, ...) \ + G_STMT_START \ + { \ + if (enabled_cmd) \ + __LOG_print(print_cmd, __VA_ARGS__); \ + } \ + G_STMT_END + +#define _LOG_R_(enabled_cmd, x_request, print_cmd, ...) \ + G_STMT_START \ + { \ + const Request *const _request = (x_request); \ + \ + nm_assert(_request); \ + if (enabled_cmd) \ + __LOG_print_R(print_cmd, _request, ": "__VA_ARGS__); \ + } \ + G_STMT_END + +#define _LOG_S_(enabled_cmd, x_script, print_cmd, ...) \ + G_STMT_START \ + { \ + const ScriptInfo *const _script = (x_script); \ + const Request *const _request = _script ? _script->request : NULL; \ + \ + nm_assert(_script &&_request); \ + if (enabled_cmd) \ + __LOG_print_S(print_cmd, _request, _script, ": "__VA_ARGS__); \ + } \ + G_STMT_END + +#define _LOG_X_D_enabled() (gl.debug) +#define _LOG_X_T_enabled() _LOG_X_D_enabled() + +#define _LOG_R_D_enabled(request) (_NM_ENSURE_TYPE_CONST(Request *, request)->debug) +#define _LOG_R_T_enabled(request) _LOG_R_D_enabled(request) + +#define _LOG_X_T(...) _LOG_X_(_LOG_X_T_enabled(), g_debug, __VA_ARGS__) +#define _LOG_X_D(...) _LOG_X_(_LOG_X_D_enabled(), g_info, __VA_ARGS__) +#define _LOG_X_I(...) _LOG_X_(TRUE, g_message, __VA_ARGS__) +#define _LOG_X_W(...) _LOG_X_(TRUE, g_warning, __VA_ARGS__) + +#define _LOG_R_T(request, ...) _LOG_R_(_LOG_R_T_enabled(_request), request, g_debug, __VA_ARGS__) +#define _LOG_R_D(request, ...) _LOG_R_(_LOG_R_D_enabled(_request), request, g_info, __VA_ARGS__) +#define _LOG_R_W(request, ...) _LOG_R_(TRUE, request, g_warning, __VA_ARGS__) + +#define _LOG_S_T(script, ...) _LOG_S_(_LOG_R_T_enabled(_request), script, g_debug, __VA_ARGS__) +#define _LOG_S_D(script, ...) _LOG_S_(_LOG_R_D_enabled(_request), script, g_info, __VA_ARGS__) +#define _LOG_S_W(script, ...) _LOG_S_(TRUE, script, g_warning, __VA_ARGS__) + +/*****************************************************************************/ + +static gboolean dispatch_one_script(Request *request); + +/*****************************************************************************/ + +static void +script_info_free(gpointer ptr) +{ + ScriptInfo *info = ptr; + + g_free(info->script); + g_free(info->error); + g_slice_free(ScriptInfo, info); +} + +static void +request_free(Request *request) +{ + g_assert_cmpuint(request->num_scripts_done, ==, request->scripts->len); + g_assert_cmpuint(request->num_scripts_nowait, ==, 0); + + g_free(request->action); + g_free(request->iface); + g_strfreev(request->envp); + g_ptr_array_free(request->scripts, TRUE); + + g_slice_free(Request, request); +} + +static gboolean +quit_timeout_cb(gpointer user_data) +{ + gl.quit_id = 0; + g_main_loop_quit(gl.loop); + return G_SOURCE_REMOVE; +} + +static void +quit_timeout_reschedule(void) +{ + if (!gl.persist) { + nm_clear_g_source(&gl.quit_id); + gl.quit_id = g_timeout_add_seconds(10, quit_timeout_cb, NULL); + } +} + +/** + * next_request: + * + * @request: (allow-none): the request to set as next. If %NULL, dequeue the next + * waiting request. Otherwise, try to set the given request. + * + * Sets the currently active request (@current_request). The current request + * is a request that has at least on "wait" script, because requests that only + * consist of "no-wait" scripts are handled right away and not enqueued to + * @requests_waiting nor set as @current_request. + * + * Returns: %TRUE, if there was currently not request in process and it set + * a new request as current. + */ +static gboolean +next_request(Request *request) +{ + if (request) { + if (gl.current_request) { + g_queue_push_tail(gl.requests_waiting, request); + return FALSE; + } + } else { + /* when calling next_request() without explicit @request, we always + * forcefully clear @current_request. That one is certainly + * handled already. */ + gl.current_request = NULL; + + request = g_queue_pop_head(gl.requests_waiting); + if (!request) + return FALSE; + } + + _LOG_R_D(request, "start running ordered scripts..."); + + gl.current_request = request; + + return TRUE; +} + +/** + * complete_request: + * @request: the request + * + * Checks if all the scripts for the request have terminated and in such case + * it sends the D-Bus response and releases the request resources. + * + * It also decreases @num_requests_pending and possibly does quit_timeout_reschedule(). + */ +static void +complete_request(Request *request) +{ + GVariantBuilder results; + GVariant * ret; + guint i; + + nm_assert(request); + + /* Are there still pending scripts? Then do nothing (for now). */ + if (request->num_scripts_done < request->scripts->len) + return; + + g_variant_builder_init(&results, G_VARIANT_TYPE("a(sus)")); + for (i = 0; i < request->scripts->len; i++) { + ScriptInfo *script = g_ptr_array_index(request->scripts, i); + + g_variant_builder_add(&results, + "(sus)", + script->script, + script->result, + script->error ?: ""); + } + + ret = g_variant_new("(a(sus))", &results); + g_dbus_method_invocation_return_value(request->context, ret); + + _LOG_R_T(request, "completed (%u scripts)", request->scripts->len); + + if (gl.current_request == request) + gl.current_request = NULL; + + request_free(request); + + g_assert_cmpuint(gl.num_requests_pending, >, 0); + if (--gl.num_requests_pending <= 0) { + nm_assert(!gl.current_request && !g_queue_peek_head(gl.requests_waiting)); + quit_timeout_reschedule(); + } +} + +static void +complete_script(ScriptInfo *script) +{ + Request *request; + gboolean wait = script->wait; + + request = script->request; + + if (wait) { + /* for "wait" scripts, try to schedule the next blocking script. + * If that is successful, return (as we must wait for its completion). */ + if (dispatch_one_script(request)) + return; + } + + nm_assert(!wait || gl.current_request == request); + + /* Try to complete the request. @request will be possibly free'd, + * making @script and @request a dangling pointer. */ + complete_request(request); + + if (!wait) { + /* this was a "no-wait" script. We either completed the request, + * or there is nothing to do. Especially, there is no need to + * queue the next_request() -- because no-wait scripts don't block + * requests. However, if this was the last "no-wait" script and + * there are "wait" scripts ready to run, launch them. + */ + if (gl.current_request == request && gl.current_request->num_scripts_nowait == 0) { + if (dispatch_one_script(gl.current_request)) + return; + + complete_request(gl.current_request); + } else + return; + } else { + /* if the script is a "wait" script, we already tried above to + * dispatch the next script. As we didn't do that, it means we + * just completed the last script of @request and we can continue + * with the next request... + * + * Also, it cannot be that there is another request currently being + * processed because only requests with "wait" scripts can become + * @current_request. As there can only be one "wait" script running + * at any time, it means complete_request() above completed @request. */ + nm_assert(!gl.current_request); + } + + while (next_request(NULL)) { + request = gl.current_request; + + if (dispatch_one_script(request)) + return; + + /* Try to complete the request. It will be either completed + * now, or when all pending "no-wait" scripts return. */ + complete_request(request); + + /* We can immediately start next_request(), because our current + * @request has obviously no more "wait" scripts either. + * Repeat... */ + } +} + +static void +script_watch_cb(GPid pid, int status, gpointer user_data) +{ + ScriptInfo * script = user_data; + gs_free char *status_desc = NULL; + + g_assert(pid == script->pid); + + script->watch_id = 0; + nm_clear_g_source(&script->timeout_id); + script->request->num_scripts_done++; + if (!script->wait) + script->request->num_scripts_nowait--; + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + script->result = DISPATCH_RESULT_SUCCESS; + } else { + status_desc = nm_utils_get_process_exit_status_desc(status); + script->error = g_strdup_printf("Script '%s' %s.", script->script, status_desc); + } + + if (script->result == DISPATCH_RESULT_SUCCESS) { + _LOG_S_T(script, "complete"); + } else { + script->result = DISPATCH_RESULT_FAILED; + _LOG_S_W(script, "complete: failed with %s", script->error); + } + + g_spawn_close_pid(script->pid); + + complete_script(script); +} + +static gboolean +script_timeout_cb(gpointer user_data) +{ + ScriptInfo *script = user_data; + + script->timeout_id = 0; + nm_clear_g_source(&script->watch_id); + script->request->num_scripts_done++; + if (!script->wait) + script->request->num_scripts_nowait--; + + _LOG_S_W(script, "complete: timeout (kill script)"); + + kill(script->pid, SIGKILL); +again: + if (waitpid(script->pid, NULL, 0) == -1) { + if (errno == EINTR) + goto again; + } + + script->error = g_strdup_printf("Script '%s' timed out.", script->script); + script->result = DISPATCH_RESULT_TIMEOUT; + + g_spawn_close_pid(script->pid); + + complete_script(script); + + return FALSE; +} + +static gboolean +check_permissions(struct stat *s, const char **out_error_msg) +{ + g_return_val_if_fail(s != NULL, FALSE); + g_return_val_if_fail(out_error_msg != NULL, FALSE); + g_return_val_if_fail(*out_error_msg == NULL, FALSE); + + /* Only accept files owned by root */ + if (s->st_uid != 0) { + *out_error_msg = "not owned by root."; + return FALSE; + } + + /* Only accept files not writable by group or other, and not SUID */ + if (s->st_mode & (S_IWGRP | S_IWOTH | S_ISUID)) { + *out_error_msg = "writable by group or other, or set-UID."; + return FALSE; + } + + /* Only accept files executable by the owner */ + if (!(s->st_mode & S_IXUSR)) { + *out_error_msg = "not executable by owner."; + return FALSE; + } + + return TRUE; +} + +static gboolean +check_filename(const char *file_name) +{ + static const char *bad_suffixes[] = { + "~", + ".rpmsave", + ".rpmorig", + ".rpmnew", + ".swp", + }; + char *tmp; + guint i; + + /* File must not be a backup file, package management file, or start with '.' */ + + if (file_name[0] == '.') + return FALSE; + for (i = 0; i < G_N_ELEMENTS(bad_suffixes); i++) { + if (g_str_has_suffix(file_name, bad_suffixes[i])) + return FALSE; + } + tmp = g_strrstr(file_name, ".dpkg-"); + if (tmp && !strchr(&tmp[1], '.')) + return FALSE; + return TRUE; +} + +#define SCRIPT_TIMEOUT 600 /* 10 minutes */ + +static gboolean +script_dispatch(ScriptInfo *script) +{ + gs_free_error GError *error = NULL; + char * argv[4]; + Request * request = script->request; + + if (script->dispatched) + return FALSE; + + script->dispatched = TRUE; + + /* Only for "hostname" action we coerce the interface name to "none". We don't + * do so for "connectivity-check" action. */ + + argv[0] = script->script; + argv[1] = request->iface ?: (nm_streq(request->action, NMD_ACTION_HOSTNAME) ? "none" : ""); + argv[2] = request->action; + argv[3] = NULL; + + _LOG_S_T(script, "run script%s", script->wait ? "" : " (no-wait)"); + + if (!g_spawn_async("/", + argv, + request->envp, + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, + NULL, + &script->pid, + &error)) { + _LOG_S_W(script, "complete: failed to execute script: %s", error->message); + script->result = DISPATCH_RESULT_EXEC_FAILED; + script->error = g_strdup(error->message); + request->num_scripts_done++; + return FALSE; + } + + script->watch_id = g_child_watch_add(script->pid, (GChildWatchFunc) script_watch_cb, script); + script->timeout_id = g_timeout_add_seconds(SCRIPT_TIMEOUT, script_timeout_cb, script); + if (!script->wait) + request->num_scripts_nowait++; + return TRUE; +} + +static gboolean +dispatch_one_script(Request *request) +{ + if (request->num_scripts_nowait > 0) + return TRUE; + + while (request->idx < request->scripts->len) { + ScriptInfo *script; + + script = g_ptr_array_index(request->scripts, request->idx++); + if (script_dispatch(script)) + return TRUE; + } + return FALSE; +} + +static int +_compare_basenames(gconstpointer a, gconstpointer b) +{ + const char *basename_a = strrchr(a, '/'); + const char *basename_b = strrchr(b, '/'); + int ret; + + nm_assert(basename_a); + nm_assert(basename_b); + + ret = strcmp(++basename_a, ++basename_b); + if (ret) + return ret; + + nm_assert_not_reached(); + return 0; +} + +static void +_find_scripts(Request *request, GHashTable *scripts, const char *base, const char *subdir) +{ + const char * filename; + gs_free char *dirname = NULL; + GError * error = NULL; + GDir * dir; + + dirname = g_build_filename(base, "dispatcher.d", subdir, NULL); + + if (!(dir = g_dir_open(dirname, 0, &error))) { + if (!g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { + _LOG_R_W(request, + "find-scripts: Failed to open dispatcher directory '%s': %s", + dirname, + error->message); + } + g_error_free(error); + return; + } + + while ((filename = g_dir_read_name(dir))) { + if (!check_filename(filename)) + continue; + + g_hash_table_insert(scripts, g_strdup(filename), g_build_filename(dirname, filename, NULL)); + } + + g_dir_close(dir); +} + +static GSList * +find_scripts(Request *request) +{ + gs_unref_hashtable GHashTable *scripts = NULL; + GSList * script_list = NULL; + GHashTableIter iter; + const char * subdir; + char * path; + char * filename; + + if (NM_IN_STRSET(request->action, NMD_ACTION_PRE_UP, NMD_ACTION_VPN_PRE_UP)) + subdir = "pre-up.d"; + else if (NM_IN_STRSET(request->action, NMD_ACTION_PRE_DOWN, NMD_ACTION_VPN_PRE_DOWN)) + subdir = "pre-down.d"; + else + subdir = NULL; + + scripts = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free); + + _find_scripts(request, scripts, NMLIBDIR, subdir); + _find_scripts(request, scripts, NMCONFDIR, subdir); + + g_hash_table_iter_init(&iter, scripts); + while (g_hash_table_iter_next(&iter, (gpointer *) &filename, (gpointer *) &path)) { + gs_free char *link_target = NULL; + const char * err_msg = NULL; + struct stat st; + int err; + + link_target = g_file_read_link(path, NULL); + if (nm_streq0(link_target, "/dev/null")) + continue; + + err = stat(path, &st); + if (err) + _LOG_R_W(request, "find-scripts: Failed to stat '%s': %d", path, err); + else if (!S_ISREG(st.st_mode) || st.st_size == 0) { + /* silently skip. */ + } else if (!check_permissions(&st, &err_msg)) + _LOG_R_W(request, "find-scripts: Cannot execute '%s': %s", path, err_msg); + else { + /* success */ + script_list = g_slist_prepend(script_list, g_strdup(path)); + continue; + } + } + + return g_slist_sort(script_list, _compare_basenames); +} + +static gboolean +script_must_wait(const char *path) +{ + gs_free char *link = NULL; + + link = g_file_read_link(path, NULL); + if (link) { + gs_free char * dir = NULL; + nm_auto_free char *real = NULL; + + if (!g_path_is_absolute(link)) { + char *tmp; + + dir = g_path_get_dirname(path); + tmp = g_build_path("/", dir, link, NULL); + g_free(link); + g_free(dir); + link = tmp; + } + + dir = g_path_get_dirname(link); + real = realpath(dir, NULL); + if (NM_STR_HAS_SUFFIX(real, "/no-wait.d")) + return FALSE; + } + + return TRUE; +} + +static void +_method_call_action(GDBusMethodInvocation *invocation, GVariant *parameters) +{ + const char * action; + gs_unref_variant GVariant *connection = NULL; + gs_unref_variant GVariant *connection_properties = NULL; + gs_unref_variant GVariant *device_properties = NULL; + gs_unref_variant GVariant *device_proxy_properties = NULL; + gs_unref_variant GVariant *device_ip4_config = NULL; + gs_unref_variant GVariant *device_ip6_config = NULL; + gs_unref_variant GVariant *device_dhcp4_config = NULL; + gs_unref_variant GVariant *device_dhcp6_config = NULL; + const char * connectivity_state; + const char * vpn_ip_iface; + gs_unref_variant GVariant *vpn_proxy_properties = NULL; + gs_unref_variant GVariant *vpn_ip4_config = NULL; + gs_unref_variant GVariant *vpn_ip6_config = NULL; + gboolean debug; + GSList * sorted_scripts = NULL; + GSList * iter; + Request * request; + char ** p; + guint i, num_nowait = 0; + const char * error_message = NULL; + + g_variant_get(parameters, + "(" + "&s" /* action */ + "@a{sa{sv}}" /* connection */ + "@a{sv}" /* connection_properties */ + "@a{sv}" /* device_properties */ + "@a{sv}" /* device_proxy_properties */ + "@a{sv}" /* device_ip4_config */ + "@a{sv}" /* device_ip6_config */ + "@a{sv}" /* device_dhcp4_config */ + "@a{sv}" /* device_dhcp6_config */ + "&s" /* connectivity_state */ + "&s" /* vpn_ip_iface */ + "@a{sv}" /* vpn_proxy_properties */ + "@a{sv}" /* vpn_ip4_config */ + "@a{sv}" /* vpn_ip6_config */ + "b" /* debug */ + ")", + &action, + &connection, + &connection_properties, + &device_properties, + &device_proxy_properties, + &device_ip4_config, + &device_ip6_config, + &device_dhcp4_config, + &device_dhcp6_config, + &connectivity_state, + &vpn_ip_iface, + &vpn_proxy_properties, + &vpn_ip4_config, + &vpn_ip6_config, + &debug); + + request = g_slice_new0(Request); + request->request_id = ++gl.request_id_counter; + request->debug = debug || gl.debug; + request->context = invocation; + request->action = g_strdup(action); + + request->envp = nm_dispatcher_utils_construct_envp(action, + connection, + connection_properties, + device_properties, + device_proxy_properties, + device_ip4_config, + device_ip6_config, + device_dhcp4_config, + device_dhcp6_config, + connectivity_state, + vpn_ip_iface, + vpn_proxy_properties, + vpn_ip4_config, + vpn_ip6_config, + &request->iface, + &error_message); + + request->scripts = g_ptr_array_new_full(5, script_info_free); + + sorted_scripts = find_scripts(request); + for (iter = sorted_scripts; iter; iter = g_slist_next(iter)) { + ScriptInfo *s; + + s = g_slice_new0(ScriptInfo); + s->request = request; + s->script = iter->data; + s->wait = script_must_wait(s->script); + g_ptr_array_add(request->scripts, s); + } + g_slist_free(sorted_scripts); + + _LOG_R_D(request, "new request (%u scripts)", request->scripts->len); + if (_LOG_R_T_enabled(request) && request->envp) { + for (p = request->envp; *p; p++) + _LOG_R_T(request, "environment: %s", *p); + } + + if (error_message || request->scripts->len == 0) { + GVariant *results; + + if (error_message) + _LOG_R_W(request, "completed: invalid request: %s", error_message); + else + _LOG_R_D(request, "completed: no scripts"); + + results = g_variant_new_array(G_VARIANT_TYPE("(sus)"), NULL, 0); + g_dbus_method_invocation_return_value(invocation, g_variant_new("(@a(sus))", results)); + request->num_scripts_done = request->scripts->len; + request_free(request); + return; + } + + nm_clear_g_source(&gl.quit_id); + + gl.num_requests_pending++; + + for (i = 0; i < request->scripts->len; i++) { + ScriptInfo *s = g_ptr_array_index(request->scripts, i); + + if (!s->wait) { + script_dispatch(s); + num_nowait++; + } + } + + if (num_nowait < request->scripts->len) { + /* The request has at least one wait script. + * Try next_request() to schedule the request for + * execution. This either enqueues the request or + * sets it as gl.current_request. */ + if (next_request(request)) { + /* @request is now @current_request. Go ahead and + * schedule the first wait script. */ + if (!dispatch_one_script(request)) { + /* If that fails, we might be already finished with the + * request. Try complete_request(). */ + complete_request(request); + + if (next_request(NULL)) { + /* As @request was successfully scheduled as next_request(), there is no + * other request in queue that can be scheduled afterwards. Assert against + * that, but call next_request() to clear current_request. */ + g_assert_not_reached(); + } + } + } + } else { + /* The request contains only no-wait scripts. Try to complete + * the request right away (we might have failed to schedule any + * of the scripts). It will be either completed now, or later + * when the pending scripts return. + * We don't enqueue it to gl.requests_waiting. + * There is no need to handle next_request(), because @request is + * not the current request anyway and does not interfere with requests + * that have any "wait" scripts. */ + complete_request(request); + } +} + +static void +on_name_acquired(GDBusConnection *connection, const char *name, gpointer user_data) +{ + gl.ever_acquired_name = TRUE; +} + +static void +on_name_lost(GDBusConnection *connection, const char *name, gpointer user_data) +{ + if (!connection) { + if (!gl.ever_acquired_name) { + _LOG_X_W("Could not get the system bus. Make sure the message bus daemon is running!"); + gl.exit_with_failure = TRUE; + } else { + _LOG_X_I("System bus stopped. Exiting"); + } + } else if (!gl.ever_acquired_name) { + _LOG_X_W("Could not acquire the " NM_DISPATCHER_DBUS_SERVICE " service."); + gl.exit_with_failure = TRUE; + } else + _LOG_X_I("Lost the " NM_DISPATCHER_DBUS_SERVICE " name. Exiting"); + + g_main_loop_quit(gl.loop); +} + +static void +_method_call(GDBusConnection * connection, + const char * sender, + const char * object_path, + const char * interface_name, + const char * method_name, + GVariant * parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + if (nm_streq(interface_name, NM_DISPATCHER_DBUS_INTERFACE)) { + if (nm_streq(method_name, "Action")) { + _method_call_action(invocation, parameters); + return; + } + } + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method %s", + method_name); +} + +static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO( + NM_DISPATCHER_DBUS_INTERFACE, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_GDBUS_METHOD_INFO( + "Action", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("action", "s"), + NM_DEFINE_GDBUS_ARG_INFO("connection", "a{sa{sv}}"), + NM_DEFINE_GDBUS_ARG_INFO("connection_properties", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("device_properties", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("device_proxy_properties", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("device_ip4_config", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("device_ip6_config", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("device_dhcp4_config", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("device_dhcp6_config", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("connectivity_state", "s"), + NM_DEFINE_GDBUS_ARG_INFO("vpn_ip_iface", "s"), + NM_DEFINE_GDBUS_ARG_INFO("vpn_proxy_properties", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("vpn_ip4_config", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("vpn_ip6_config", "a{sv}"), + NM_DEFINE_GDBUS_ARG_INFO("debug", "b"), ), + .out_args = + NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("results", "a(sus)"), ), ), ), ); + +static const GDBusInterfaceVTable interface_vtable = { + .method_call = _method_call, +}; + +/*****************************************************************************/ + +static void +log_handler(const char *log_domain, GLogLevelFlags log_level, const char *message, gpointer ignored) +{ + int syslog_priority; + + switch (log_level) { + case G_LOG_LEVEL_ERROR: + syslog_priority = LOG_CRIT; + break; + case G_LOG_LEVEL_CRITICAL: + syslog_priority = LOG_ERR; + break; + case G_LOG_LEVEL_WARNING: + syslog_priority = LOG_WARNING; + break; + case G_LOG_LEVEL_MESSAGE: + syslog_priority = LOG_NOTICE; + break; + case G_LOG_LEVEL_DEBUG: + syslog_priority = LOG_DEBUG; + break; + case G_LOG_LEVEL_INFO: + default: + syslog_priority = LOG_INFO; + break; + } + + syslog(syslog_priority, "%s", message); +} + +static void +logging_setup(void) +{ + openlog(G_LOG_DOMAIN, LOG_CONS, LOG_DAEMON); + g_log_set_handler(G_LOG_DOMAIN, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION, + log_handler, + NULL); +} + +static void +logging_shutdown(void) +{ + closelog(); +} + +static gboolean +signal_handler(gpointer user_data) +{ + int signo = GPOINTER_TO_INT(user_data); + + _LOG_X_I("Caught signal %d, shutting down...", signo); + g_main_loop_quit(gl.loop); + + return G_SOURCE_CONTINUE; +} + +static gboolean +parse_command_line(int *p_argc, char ***p_argv, GError **error) +{ + GOptionContext *opt_ctx; + GOptionEntry entries[] = { + {"debug", 0, 0, G_OPTION_ARG_NONE, &gl.debug, "Output to console rather than syslog", NULL}, + {"persist", 0, 0, G_OPTION_ARG_NONE, &gl.persist, "Don't quit after a short timeout", NULL}, + {NULL}}; + gboolean success; + + opt_ctx = g_option_context_new(NULL); + g_option_context_set_summary(opt_ctx, "Executes scripts upon actions by NetworkManager."); + g_option_context_add_main_entries(opt_ctx, entries, NULL); + + success = g_option_context_parse(opt_ctx, p_argc, p_argv, error); + + g_option_context_free(opt_ctx); + + return success; +} + +int +main(int argc, char **argv) +{ + gs_free_error GError *error = NULL; + guint signal_id_term = 0; + guint signal_id_int = 0; + guint dbus_regist_id = 0; + guint dbus_own_name_id = 0; + + if (!parse_command_line(&argc, &argv, &error)) { + _LOG_X_W("Error parsing command line arguments: %s", error->message); + gl.exit_with_failure = TRUE; + goto done; + } + + signal_id_term = g_unix_signal_add(SIGTERM, signal_handler, GINT_TO_POINTER(SIGTERM)); + signal_id_int = g_unix_signal_add(SIGINT, signal_handler, GINT_TO_POINTER(SIGINT)); + + if (gl.debug) { + if (!g_getenv("G_MESSAGES_DEBUG")) { + /* we log our regular messages using g_debug() and g_info(). + * When we redirect glib logging to syslog, there is no problem. + * But in "debug" mode, glib will no print these messages unless + * we set G_MESSAGES_DEBUG. */ + g_setenv("G_MESSAGES_DEBUG", "all", TRUE); + } + } else + logging_setup(); + + gl.loop = g_main_loop_new(NULL, FALSE); + + gl.dbus_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + if (!gl.dbus_connection) { + _LOG_X_W("Could not get the system bus (%s). Make sure the message bus daemon is running!", + error->message); + gl.exit_with_failure = TRUE; + goto done; + } + + gl.requests_waiting = g_queue_new(); + + dbus_regist_id = + g_dbus_connection_register_object(gl.dbus_connection, + NM_DISPATCHER_DBUS_PATH, + interface_info, + NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable), + NULL, + NULL, + &error); + if (dbus_regist_id == 0) { + _LOG_X_W("Could not export Dispatcher D-Bus interface: %s", error->message); + gl.exit_with_failure = 1; + goto done; + } + + dbus_own_name_id = g_bus_own_name_on_connection(gl.dbus_connection, + NM_DISPATCHER_DBUS_SERVICE, + G_BUS_NAME_OWNER_FLAGS_NONE, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + quit_timeout_reschedule(); + + g_main_loop_run(gl.loop); + +done: + + if (gl.num_requests_pending > 0) { + /* this only happens when we quit due to SIGTERM (not due to the idle timer). + * + * Log a warning about pending scripts. + * + * Maybe we should notify NetworkManager that these scripts are left in an unknown state. + * But this is either a bug of a dispatcher script (not terminating in time). + * + * FIXME(shutdown): Also, currently NetworkManager behaves wrongly on shutdown. + * Note that systemd would not terminate NetworkManager-dispatcher before NetworkManager. + * It's NetworkManager's responsibility to keep running long enough so that all requests + * can complete (with a watchdog timer, and a warning that user provided scripts hang). */ + _LOG_X_W("exiting but there are still %u requests pending", gl.num_requests_pending); + } + + if (dbus_own_name_id != 0) + g_bus_unown_name(nm_steal_int(&dbus_own_name_id)); + + if (dbus_regist_id != 0) + g_dbus_connection_unregister_object(gl.dbus_connection, nm_steal_int(&dbus_regist_id)); + + nm_clear_pointer(&gl.requests_waiting, g_queue_free); + + nm_clear_g_source(&signal_id_term); + nm_clear_g_source(&signal_id_int); + nm_clear_g_source(&gl.quit_id); + nm_clear_pointer(&gl.loop, g_main_loop_unref); + g_clear_object(&gl.dbus_connection); + + if (!gl.debug) + logging_shutdown(); + + return gl.exit_with_failure ? 1 : 0; +} diff --git a/src/nm-dispatcher/nm-dispatcher.conf b/src/nm-dispatcher/nm-dispatcher.conf new file mode 100644 index 00000000..d6f31135 --- /dev/null +++ b/src/nm-dispatcher/nm-dispatcher.conf @@ -0,0 +1,13 @@ +<!DOCTYPE busconfig PUBLIC + "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <policy user="root"> + <allow own="org.freedesktop.nm_dispatcher"/> + <allow send_destination="org.freedesktop.nm_dispatcher"/> + </policy> + <policy context="default"> + <deny own="org.freedesktop.nm_dispatcher"/> + <deny send_destination="org.freedesktop.nm_dispatcher"/> + </policy> +</busconfig> diff --git a/src/nm-dispatcher/nm-dispatcher.xml b/src/nm-dispatcher/nm-dispatcher.xml new file mode 100644 index 00000000..0d9d28e2 --- /dev/null +++ b/src/nm-dispatcher/nm-dispatcher.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<node name="/"> + <interface name="org.freedesktop.nm_dispatcher"> + <annotation name="org.gtk.GDBus.C.Name" value="Dispatcher"/> + + <!-- + Action: + @action: The action being performed. + @connection: The connection for which this action was triggered. + @connection_properties: Properties of the connection, including service and path. + @device_properties: Properties of the device, including type, path, interface, and state. + @device_proxy_properties: Properties of the device's proxy configuration. + @device_ip4_config: Properties of the device's IPv4 configuration. + @device_ip6_config: Properties of the device's IPv6 configuration. + @device_dhcp4_config: Properties of the device's DHCPv4 configuration. + @device_dhcp6_config: Properties of the device's DHCPv6 configuration. + @connectivity_state: Current connectivity state: unknown, none, limited, portal or full. + @vpn_ip_iface: VPN interface name. + @vpn_proxy_properties: Properties of the VPN's proxy configuration. + @vpn_ip4_config: Properties of the VPN's IPv4 configuration. + @vpn_ip6_config: Properties of the VPN's IPv6 configuration. + @debug: Whether to log debug output. + @results: Results of dispatching operations. Each element of the returned array is a struct containing the path of an executed script (s), the result of running that script (u), and a description of the result (s). + + INTERNAL; not public API. Perform an action. + --> + <method name="Action"> + <arg name="action" type="s" direction="in"/> + <arg name="connection" type="a{sa{sv}}" direction="in"/> + <arg name="connection_properties" type="a{sv}" direction="in"/> + <arg name="device_properties" type="a{sv}" direction="in"/> + <arg name="device_proxy_properties" type="a{sv}" direction="in"/> + <arg name="device_ip4_config" type="a{sv}" direction="in"/> + <arg name="device_ip6_config" type="a{sv}" direction="in"/> + <arg name="device_dhcp4_config" type="a{sv}" direction="in"/> + <arg name="device_dhcp6_config" type="a{sv}" direction="in"/> + <arg name="connectivity_state" type="s" direction="in"/> + <arg name="vpn_ip_iface" type="s" direction="in"/> + <arg name="vpn_proxy_properties" type="a{sv}" direction="in"/> + <arg name="vpn_ip4_config" type="a{sv}" direction="in"/> + <arg name="vpn_ip6_config" type="a{sv}" direction="in"/> + <arg name="debug" type="b" direction="in"/> + <arg name="results" type="a(sus)" direction="out"/> + </method> + </interface> +</node> diff --git a/src/nm-dispatcher/org.freedesktop.nm_dispatcher.service.in b/src/nm-dispatcher/org.freedesktop.nm_dispatcher.service.in new file mode 100644 index 00000000..ff037cca --- /dev/null +++ b/src/nm-dispatcher/org.freedesktop.nm_dispatcher.service.in @@ -0,0 +1,6 @@ +[D-BUS Service] +Name=org.freedesktop.nm_dispatcher +Exec=@libexecdir@/nm-dispatcher +User=root +SystemdService=dbus-org.freedesktop.nm-dispatcher.service + diff --git a/src/nm-dispatcher/tests/dispatcher-connectivity-full b/src/nm-dispatcher/tests/dispatcher-connectivity-full new file mode 100644 index 00000000..937e79d7 --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-connectivity-full @@ -0,0 +1,17 @@ +[main] +action=connectivity-change +uuid=3fd2a33a-d81b-423f-ae99-e6baba742311 +id=Random Connection +connectivity-state=FULL + +[device] +state=30 +ip-interface=wlan0 +type=2 +interface=wlan0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[env] +PATH= +NM_DISPATCHER_ACTION=connectivity-change +CONNECTIVITY_STATE=FULL diff --git a/src/nm-dispatcher/tests/dispatcher-connectivity-unknown b/src/nm-dispatcher/tests/dispatcher-connectivity-unknown new file mode 100644 index 00000000..9aa48e4a --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-connectivity-unknown @@ -0,0 +1,16 @@ +[main] +action=connectivity-change +uuid=3fd2a33a-d81b-423f-ae99-e6baba742311 +id=Random Connection +connectivity-state=UNKNOWN + +[device] +state=30 +ip-interface=wlan0 +type=2 +interface=wlan0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[env] +PATH= +NM_DISPATCHER_ACTION=connectivity-change diff --git a/src/nm-dispatcher/tests/dispatcher-down b/src/nm-dispatcher/tests/dispatcher-down new file mode 100644 index 00000000..bd7460ae --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-down @@ -0,0 +1,23 @@ +[main] +action=down +expected-iface=wlan0 +uuid=3fd2a33a-d81b-423f-ae99-e6baba742311 +id=Random Connection + +[device] +state=30 +ip-interface=wlan0 +type=2 +interface=wlan0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[env] +PATH= +NM_DISPATCHER_ACTION=down +CONNECTION_UUID=3fd2a33a-d81b-423f-ae99-e6baba742311 +CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5 +CONNECTION_ID=Random Connection +CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-down +DEVICE_IFACE=wlan0 +DEVICE_IP_IFACE=wlan0 + diff --git a/src/nm-dispatcher/tests/dispatcher-external b/src/nm-dispatcher/tests/dispatcher-external new file mode 100644 index 00000000..382ff706 --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-external @@ -0,0 +1,40 @@ +[main] +action=up +expected-iface=virbr0 +uuid=92bbc2fb-7304-46be-8ebb-6093dbe19a6a +id=virbr0 +external=1 + +[device] +state=100 +ip-interface=virbr0 +type=13 +interface=virbr0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[proxy] +pac-url=http://networkmanager.com/proxy.pac +pac-script="function FindProxyForURL (url, host) {}" + +[ip4] +addresses=192.168.122.1/24 0.0.0.0 +domains= +gateway=0.0.0.0 + +[env] +PATH= +NM_DISPATCHER_ACTION=up +CONNECTION_UUID=92bbc2fb-7304-46be-8ebb-6093dbe19a6a +CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5 +CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-external +CONNECTION_ID=virbr0 +CONNECTION_EXTERNAL=1 +DEVICE_IFACE=virbr0 +DEVICE_IP_IFACE=virbr0 +PROXY_PAC_URL=http://networkmanager.com/proxy.pac +PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}" +IP4_NUM_ADDRESSES=1 +IP4_ADDRESS_0=192.168.122.1/24 0.0.0.0 +IP4_GATEWAY=0.0.0.0 +IP4_NUM_ROUTES=0 + diff --git a/src/nm-dispatcher/tests/dispatcher-up b/src/nm-dispatcher/tests/dispatcher-up new file mode 100644 index 00000000..cd6c0fad --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-up @@ -0,0 +1,66 @@ +[main] +action=up +expected-iface=wlan0 +uuid=3fd2a33a-d81b-423f-ae99-e6baba742311 +id=Random Connection + +[device] +state=100 +ip-interface=wlan0 +type=2 +interface=wlan0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[dhcp4] +netbios_name_servers=0.0.0.0 +domain_name_servers=68.87.77.134 68.87.72.134 192.168.1.1 +dhcp_lease_time=86400 +network_number=192.168.1.0 +domain_name=hsd1.mn.comcast.net. +ip_address=192.168.1.119 +dhcp_message_type=5 +dhcp_server_identifier=192.168.1.1 +routers=192.168.1.1 +broadcast_address=192.168.1.255 +subnet_mask=255.255.255.0 +expiry=1304300446 + +[proxy] +pac-url=http://networkmanager.com/proxy.pac +pac-script="function FindProxyForURL (url, host) {}" + +[ip4] +addresses=192.168.1.119/24 192.168.1.1 +nameservers=68.87.77.134 68.87.72.134 192.168.1.1 +domains=hsd1.mn.comcast.net. + +[env] +PATH= +NM_DISPATCHER_ACTION=up +CONNECTION_UUID=3fd2a33a-d81b-423f-ae99-e6baba742311 +CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5 +CONNECTION_ID=Random Connection +CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-up +DEVICE_IFACE=wlan0 +DEVICE_IP_IFACE=wlan0 +PROXY_PAC_URL=http://networkmanager.com/proxy.pac +PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}" +IP4_ADDRESS_0=192.168.1.119/24 192.168.1.1 +IP4_NUM_ADDRESSES=1 +IP4_NAMESERVERS=68.87.77.134 68.87.72.134 192.168.1.1 +IP4_GATEWAY=192.168.1.1 +IP4_DOMAINS=hsd1.mn.comcast.net. +IP4_NUM_ROUTES=0 +DHCP4_NETBIOS_NAME_SERVERS=0.0.0.0 +DHCP4_DOMAIN_NAME_SERVERS=68.87.77.134 68.87.72.134 192.168.1.1 +DHCP4_DHCP_LEASE_TIME=86400 +DHCP4_NETWORK_NUMBER=192.168.1.0 +DHCP4_DOMAIN_NAME=hsd1.mn.comcast.net. +DHCP4_IP_ADDRESS=192.168.1.119 +DHCP4_DHCP_MESSAGE_TYPE=5 +DHCP4_DHCP_SERVER_IDENTIFIER=192.168.1.1 +DHCP4_ROUTERS=192.168.1.1 +DHCP4_BROADCAST_ADDRESS=192.168.1.255 +DHCP4_SUBNET_MASK=255.255.255.0 +DHCP4_EXPIRY=1304300446 + diff --git a/src/nm-dispatcher/tests/dispatcher-vpn-down b/src/nm-dispatcher/tests/dispatcher-vpn-down new file mode 100644 index 00000000..e99c6142 --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-vpn-down @@ -0,0 +1,65 @@ +[main] +action=vpn-down +expected-iface=tun0 +uuid=355653c0-34d3-4777-ad25-f9a498b7ef8e +id=Random Connection + +[device] +state=100 +ip-interface=tun0 +type=2 +interface=wlan0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[dhcp4] +netbios_name_servers=0.0.0.0 +domain_name_servers=68.87.77.134 68.87.72.134 192.168.1.1 +dhcp_lease_time=86400 +network_number=192.168.1.0 +domain_name=hsd1.mn.comcast.net. +ip_address=192.168.1.119 +dhcp_message_type=5 +dhcp_server_identifier=192.168.1.1 +routers=192.168.1.1 +broadcast_address=192.168.1.255 +subnet_mask=255.255.255.0 +expiry=1304349405 + +[proxy] +pac-url=http://networkmanager.com/proxy.pac +pac-script="function FindProxyForURL (url, host) {}" + +[ip4] +addresses=192.168.1.119/24 192.168.1.1 +nameservers=68.87.77.134 68.87.72.134 192.168.1.1 +domains=hsd1.mn.comcast.net. + +[env] +PATH= +NM_DISPATCHER_ACTION=vpn-down +CONNECTION_UUID=355653c0-34d3-4777-ad25-f9a498b7ef8e +CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5 +CONNECTION_ID=Random Connection +CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-vpn-down +DEVICE_IFACE=wlan0 +DEVICE_IP_IFACE=tun0 +PROXY_PAC_URL=http://networkmanager.com/proxy.pac +PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}" +IP4_ADDRESS_0=192.168.1.119/24 192.168.1.1 +IP4_NUM_ADDRESSES=1 +IP4_NAMESERVERS=68.87.77.134 68.87.72.134 192.168.1.1 +IP4_GATEWAY=192.168.1.1 +IP4_DOMAINS=hsd1.mn.comcast.net. +IP4_NUM_ROUTES=0 +DHCP4_NETBIOS_NAME_SERVERS=0.0.0.0 +DHCP4_DOMAIN_NAME_SERVERS=68.87.77.134 68.87.72.134 192.168.1.1 +DHCP4_DHCP_LEASE_TIME=86400 +DHCP4_NETWORK_NUMBER=192.168.1.0 +DHCP4_DOMAIN_NAME=hsd1.mn.comcast.net. +DHCP4_IP_ADDRESS=192.168.1.119 +DHCP4_DHCP_MESSAGE_TYPE=5 +DHCP4_DHCP_SERVER_IDENTIFIER=192.168.1.1 +DHCP4_ROUTERS=192.168.1.1 +DHCP4_BROADCAST_ADDRESS=192.168.1.255 +DHCP4_SUBNET_MASK=255.255.255.0 +DHCP4_EXPIRY=1304349405 diff --git a/src/nm-dispatcher/tests/dispatcher-vpn-up b/src/nm-dispatcher/tests/dispatcher-vpn-up new file mode 100644 index 00000000..a2414790 --- /dev/null +++ b/src/nm-dispatcher/tests/dispatcher-vpn-up @@ -0,0 +1,65 @@ +[main] +action=vpn-up +expected-iface=tun0 +uuid=355653c0-34d3-4777-ad25-f9a498b7ef8e +id=Random Connection + +[device] +state=100 +ip-interface=tun0 +type=2 +interface=wlan0 +path=/org/freedesktop/NetworkManager/Devices/0 + +[dhcp4] +netbios_name_servers=0.0.0.0 +domain_name_servers=68.87.77.134 68.87.72.134 192.168.1.1 +dhcp_lease_time=86400 +network_number=192.168.1.0 +domain_name=hsd1.mn.comcast.net. +ip_address=192.168.1.119 +dhcp_message_type=5 +dhcp_server_identifier=192.168.1.1 +routers=192.168.1.1 +broadcast_address=192.168.1.255 +subnet_mask=255.255.255.0 +expiry=1304349405 + +[proxy] +pac-url=http://networkmanager.com/proxy.pac +pac-script="function FindProxyForURL (url, host) {}" + +[ip4] +addresses=192.168.1.119/24 192.168.1.1 +nameservers=68.87.77.134 68.87.72.134 192.168.1.1 +domains=hsd1.mn.comcast.net. + +[env] +PATH= +NM_DISPATCHER_ACTION=vpn-up +CONNECTION_UUID=355653c0-34d3-4777-ad25-f9a498b7ef8e +CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5 +CONNECTION_ID=Random Connection +CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-vpn-up +DEVICE_IFACE=wlan0 +DEVICE_IP_IFACE=tun0 +PROXY_PAC_URL=http://networkmanager.com/proxy.pac +PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}" +IP4_ADDRESS_0=192.168.1.119/24 192.168.1.1 +IP4_NUM_ADDRESSES=1 +IP4_NAMESERVERS=68.87.77.134 68.87.72.134 192.168.1.1 +IP4_GATEWAY=192.168.1.1 +IP4_DOMAINS=hsd1.mn.comcast.net. +IP4_NUM_ROUTES=0 +DHCP4_NETBIOS_NAME_SERVERS=0.0.0.0 +DHCP4_DOMAIN_NAME_SERVERS=68.87.77.134 68.87.72.134 192.168.1.1 +DHCP4_DHCP_LEASE_TIME=86400 +DHCP4_NETWORK_NUMBER=192.168.1.0 +DHCP4_DOMAIN_NAME=hsd1.mn.comcast.net. +DHCP4_IP_ADDRESS=192.168.1.119 +DHCP4_DHCP_MESSAGE_TYPE=5 +DHCP4_DHCP_SERVER_IDENTIFIER=192.168.1.1 +DHCP4_ROUTERS=192.168.1.1 +DHCP4_BROADCAST_ADDRESS=192.168.1.255 +DHCP4_SUBNET_MASK=255.255.255.0 +DHCP4_EXPIRY=1304349405 diff --git a/src/nm-dispatcher/tests/meson.build b/src/nm-dispatcher/tests/meson.build new file mode 100644 index 00000000..deaa8819 --- /dev/null +++ b/src/nm-dispatcher/tests/meson.build @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +exe = executable( + 'test-dispatcher-envp', + [ + 'test-dispatcher-envp.c', + dispatcher_nmdbus_dispatcher_sources, + ], + dependencies: [ + libnm_dep, + glib_dep, + ], + c_args: introspection_extra_cflags, + link_with: [ + libnm_dispatcher_core, + libnm_log_null, + libnm_glib_aux, + libnm_std_aux, + libc_siphash, + ], +) + +test( + 'src/nm-dispatcher/tests/test-dispatcher-envp', + test_script, + args: test_args + [exe.full_path()], +) diff --git a/src/nm-dispatcher/tests/test-dispatcher-envp.c b/src/nm-dispatcher/tests/test-dispatcher-envp.c new file mode 100644 index 00000000..297cfbbd --- /dev/null +++ b/src/nm-dispatcher/tests/test-dispatcher-envp.c @@ -0,0 +1,645 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include <arpa/inet.h> +#include <stdlib.h> + +#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_utils_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_utils_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_utils_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_utils_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) + g_error("FAILED: %s", error_message); + + 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) { + _print_env(NM_CAST_STRV_CC(denv), expected_env); + g_error("Failed to find %s in environment", i_value); + } + } + + 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(); +} |