/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2008 - 2012 Red Hat, Inc. */ #include "libnm/nm-default-client.h" #include #include #include #include #include #include #include #include #include #include #include "nm-libnm-core-aux/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; guint err; 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)) { err = WEXITSTATUS(status); if (err == 0) script->result = DISPATCH_RESULT_SUCCESS; else { script->error = g_strdup_printf("Script '%s' exited with error status %d.", script->script, err); } } else if (WIFSTOPPED(status)) { script->error = g_strdup_printf("Script '%s' stopped unexpectedly with signal %d.", script->script, WSTOPSIG(status)); } else if (WIFSIGNALED(status)) { script->error = g_strdup_printf("Script '%s' died with signal %d", script->script, WTERMSIG(status)); } else { script->error = g_strdup_printf("Script '%s' died from an unknown cause", script->script); } 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; }