From 6de29285e533f4fec22a219013f3687edb6b7399 Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Sat, 13 Dec 2025 13:48:57 +0100 Subject: New upstream version 1.54.3 --- NEWS | 11 + contrib/fedora/rpm/NetworkManager.spec | 1 + meson.build | 2 +- src/core/devices/nm-device-ethernet.c | 11 +- src/core/devices/nm-device-macsec.c | 11 +- src/core/devices/nm-device-private.h | 2 + src/core/devices/nm-device-utils.c | 3 +- src/core/devices/nm-device.c | 127 +++- src/core/devices/wifi/nm-device-wifi.c | 4 +- src/core/nm-core-utils.c | 221 ++++++- src/core/nm-core-utils.h | 19 +- src/core/supplicant/nm-supplicant-config.c | 170 +++-- src/core/supplicant/nm-supplicant-config.h | 4 +- src/core/supplicant/nm-supplicant-interface.c | 191 +++++- src/core/supplicant/tests/test-supplicant-config.c | 4 +- src/libnm-client-impl/libnm.ver | 6 + src/libnm-client-impl/tests/meson.build | 16 +- .../tests/test-copy-cert-as-user.c | 32 + src/libnm-client-impl/tests/test-gir.py | 2 + src/libnm-core-impl/nm-setting-8021x.c | 97 ++- src/libnm-core-impl/nm-setting-connection.c | 41 ++ src/libnm-core-impl/nm-setting-private.h | 17 +- src/libnm-core-impl/nm-setting.c | 29 + src/libnm-core-impl/nm-utils.c | 255 ++++++++ src/libnm-core-impl/nm-vpn-plugin-info.c | 23 + src/libnm-core-intern/nm-core-internal.h | 7 + src/libnm-core-public/nm-utils.h | 3 + src/libnm-core-public/nm-version-macros.h.in | 9 +- src/libnm-core-public/nm-version.h | 6 + src/libnm-core-public/nm-vpn-plugin-info.h | 2 + src/libnm-std-aux/nm-std-utils.c | 114 +++- src/libnm-std-aux/nm-std-utils.h | 4 + src/meson.build | 3 +- src/nm-daemon-helper/README.md | 11 - src/nm-daemon-helper/meson.build | 15 - src/nm-daemon-helper/nm-daemon-helper.c | 155 ----- src/nm-helpers/README.md | 51 ++ src/nm-helpers/meson.build | 76 +++ src/nm-helpers/nm-daemon-helper.c | 188 ++++++ src/nm-helpers/nm-libnm-helper.c | 45 ++ src/nm-helpers/nm-priv-helper.c | 695 +++++++++++++++++++++ src/nm-helpers/nm-priv-helper.conf | 13 + .../org.freedesktop.nm_priv_helper.service.in | 5 + src/nm-priv-helper/README.md | 24 - src/nm-priv-helper/meson.build | 36 -- src/nm-priv-helper/nm-priv-helper.c | 695 --------------------- src/nm-priv-helper/nm-priv-helper.conf | 13 - .../org.freedesktop.nm_priv_helper.service.in | 5 - 48 files changed, 2390 insertions(+), 1084 deletions(-) create mode 100644 src/libnm-client-impl/tests/test-copy-cert-as-user.c delete mode 100644 src/nm-daemon-helper/README.md delete mode 100644 src/nm-daemon-helper/meson.build delete mode 100644 src/nm-daemon-helper/nm-daemon-helper.c create mode 100644 src/nm-helpers/README.md create mode 100644 src/nm-helpers/meson.build create mode 100644 src/nm-helpers/nm-daemon-helper.c create mode 100644 src/nm-helpers/nm-libnm-helper.c create mode 100644 src/nm-helpers/nm-priv-helper.c create mode 100644 src/nm-helpers/nm-priv-helper.conf create mode 100644 src/nm-helpers/org.freedesktop.nm_priv_helper.service.in delete mode 100644 src/nm-priv-helper/README.md delete mode 100644 src/nm-priv-helper/meson.build delete mode 100644 src/nm-priv-helper/nm-priv-helper.c delete mode 100644 src/nm-priv-helper/nm-priv-helper.conf delete mode 100644 src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in diff --git a/NEWS b/NEWS index cb32853c..339ff253 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,14 @@ +=============================================== +NetworkManager-1.54.3 +Overview of changes since NetworkManager-1.54.2 +=============================================== + +* For private connections (the ones that specify a user in the + "connection.permissions" property), verify that the user can access + the 802.1X certificates and keys set in the connection. +* Introduce a libnm function that can be used by VPN plugins to check + user permissions on certificate and keys. + =============================================== NetworkManager-1.54.2 Overview of changes since NetworkManager-1.54.1 diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index 004cbbb3..29bf8050 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -896,6 +896,7 @@ fi %{_libexecdir}/nm-dispatcher %{_libexecdir}/nm-initrd-generator %{_libexecdir}/nm-daemon-helper +%{_libexecdir}/nm-libnm-helper %{_libexecdir}/nm-priv-helper %dir %{_libdir}/%{name} %dir %{nmplugindir} diff --git a/meson.build b/meson.build index 298cbb85..7998b0d3 100644 --- a/meson.build +++ b/meson.build @@ -5,7 +5,7 @@ project( # NOTE: When incrementing version also add corresponding # NM_VERSION_x_y_z macros in # "src/libnm-core-public/nm-version-macros.h.in" - version: '1.54.2', + version: '1.54.3', license: 'GPL2+', default_options: [ 'buildtype=debugoptimized', diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c index 4034fdaa..db1245b3 100644 --- a/src/core/devices/nm-device-ethernet.c +++ b/src/core/devices/nm-device-ethernet.c @@ -630,10 +630,17 @@ build_supplicant_config(NMDeviceEthernet *self, GError **error) mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), nm_device_get_ifindex(NM_DEVICE(self))); - config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE); + config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); security = nm_connection_get_setting_802_1x(connection); - if (!nm_supplicant_config_add_setting_8021x(config, security, con_uuid, mtu, TRUE, error)) { + if (!nm_supplicant_config_add_setting_8021x(config, + security, + con_uuid, + mtu, + TRUE, + nm_device_get_private_files(NM_DEVICE(self)), + error)) { g_prefix_error(error, "802-1x-setting: "); g_clear_object(&config); } diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c index 2ff1eeb3..1659ea05 100644 --- a/src/core/devices/nm-device-macsec.c +++ b/src/core/devices/nm-device-macsec.c @@ -201,7 +201,8 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error) mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), nm_device_get_ifindex(NM_DEVICE(self))); - config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE); + config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC); @@ -227,7 +228,13 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error) if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_EAP) { s_8021x = nm_connection_get_setting_802_1x(connection); - if (!nm_supplicant_config_add_setting_8021x(config, s_8021x, con_uuid, mtu, TRUE, error)) { + if (!nm_supplicant_config_add_setting_8021x(config, + s_8021x, + con_uuid, + mtu, + TRUE, + nm_device_get_private_files(NM_DEVICE(self)), + error)) { g_prefix_error(error, "802-1x-setting: "); return NULL; } diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h index 2f73a01b..2568d9f1 100644 --- a/src/core/devices/nm-device-private.h +++ b/src/core/devices/nm-device-private.h @@ -179,4 +179,6 @@ void nm_device_auth_request(NMDevice *self, void nm_device_link_properties_set(NMDevice *self, gboolean reapply); +GHashTable *nm_device_get_private_files(NMDevice *self); + #endif /* NM_DEVICE_PRIVATE_H */ diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c index a78499fb..3e86e2e8 100644 --- a/src/core/devices/nm-device-utils.c +++ b/src/core/devices/nm-device-utils.c @@ -237,7 +237,7 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data gs_free_error GError *error = NULL; gs_free char *output = NULL; - output = nm_utils_spawn_helper_finish(result, &error); + output = nm_utils_spawn_helper_finish_string(result, &error); if (nm_utils_error_is_cancelled(error)) return; @@ -276,6 +276,7 @@ resolve_addr_spawn_helper(ResolveAddrInfo *info, ResolveAddrService services) nm_inet_ntop(info->addr_family, &info->address, addr_str); _LOG2D(info, "start lookup via nm-daemon-helper using services: %s", str); nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str, str), + FALSE, g_task_get_cancellable(info->task), resolve_addr_helper_cb, info); diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 6bba982a..b0a3f780 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -335,6 +335,12 @@ typedef struct { int addr_family; } HostnameResolver; +typedef enum { + PRIVATE_FILES_STATE_UNKNOWN = 0, + PRIVATE_FILES_STATE_READING, + PRIVATE_FILES_STATE_DONE, +} PrivateFilesState; + /*****************************************************************************/ enum { @@ -771,6 +777,13 @@ typedef struct _NMDevicePrivate { guint64 rx_bytes; } stats; + struct { + GHashTable *table; + GCancellable *cancellable; + char *user; + PrivateFilesState state; + } private_files; + bool mtu_force_set_done : 1; bool needs_ip6_subnet : 1; @@ -10807,6 +10820,49 @@ tc_commit(NMDevice *self) return TRUE; } +static void +read_private_files_cb(GObject *source_object, GAsyncResult *result, gpointer data) +{ + gs_unref_hashtable GHashTable *table = NULL; + gs_free_error GError *error = NULL; + NMDevice *self; + NMDevicePrivate *priv; + + table = nm_utils_read_private_files_finish(result, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + self = NM_DEVICE(data); + priv = NM_DEVICE_GET_PRIVATE(self); + + if (error) { + NMConnection *connection = nm_device_get_applied_connection(self); + + _LOGW(LOGD_DEVICE, + "could not read files for private connection %s owned by user '%s': %s", + connection ? nm_connection_get_uuid(connection) : NULL, + priv->private_files.user, + error->message); + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + + _LOGD(LOGD_DEVICE, "private files successfully read"); + + priv->private_files.state = PRIVATE_FILES_STATE_DONE; + priv->private_files.table = g_steal_pointer(&table); + g_clear_pointer(&priv->private_files.user, g_free); + g_clear_object(&priv->private_files.cancellable); + + nm_device_activate_schedule_stage2_device_config(self, FALSE); +} + +GHashTable * +nm_device_get_private_files(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->private_files.table; +} + /* * activate_stage2_device_config * @@ -10819,6 +10875,7 @@ activate_stage2_device_config(NMDevice *self) { NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); NMDeviceClass *klass = NM_DEVICE_GET_CLASS(self); + NMConnection *applied; NMActStageReturn ret; NMSettingWired *s_wired; gboolean no_firmware = FALSE; @@ -10827,6 +10884,68 @@ activate_stage2_device_config(NMDevice *self) nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE); + applied = nm_device_get_applied_connection(self); + + /* If the connection is private (owned by a specific user), we need to + * verify that the user has permission to access any files specified in + * the connection, such as certificates and keys. We do that by calling + * nm_utils_read_private_files() and saving the file contents in a hash + * table that can be accessed later during the activation. It is important + * to never access the files again to avoid TOCTOU bugs. + */ + switch (priv->private_files.state) { + case PRIVATE_FILES_STATE_UNKNOWN: + { + gs_free const char **paths = NULL; + NMSettingConnection *s_con; + const char *user; + + s_con = nm_connection_get_setting_connection(applied); + nm_assert(s_con); + user = _nm_setting_connection_get_first_permissions_user(s_con); + + priv->private_files.user = g_strdup(user); + if (!priv->private_files.user) { + priv->private_files.state = PRIVATE_FILES_STATE_DONE; + break; + } + + paths = nm_utils_get_connection_private_files_paths(applied); + if (!paths) { + priv->private_files.state = PRIVATE_FILES_STATE_DONE; + break; + } + + if (_nm_setting_connection_get_num_permissions_users(s_con) > 1) { + _LOGW(LOGD_DEVICE, + "private connections with multiple users are not allowed to reference " + "certificates and keys on the filesystem. Specify only one user in the " + "connection.permissions property."); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + + priv->private_files.state = PRIVATE_FILES_STATE_READING; + priv->private_files.cancellable = g_cancellable_new(); + + _LOGD(LOGD_DEVICE, "reading private files"); + nm_utils_read_private_files(paths, + priv->private_files.user, + priv->private_files.cancellable, + read_private_files_cb, + self); + return; + } + case PRIVATE_FILES_STATE_READING: + /* wait */ + return; + case PRIVATE_FILES_STATE_DONE: + /* proceed */ + break; + } + if (!nm_device_managed_type_is_external(self)) { _ethtool_state_set(self); nm_device_link_properties_set(self, FALSE); @@ -10843,7 +10962,7 @@ activate_stage2_device_config(NMDevice *self) priv->tc_committed = TRUE; } - nm_routing_rules_sync(nm_device_get_applied_connection(self), + nm_routing_rules_sync(applied, NM_TERNARY_TRUE, klass->get_extra_rules, self, @@ -17154,6 +17273,12 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu if (klass->deactivate) klass->deactivate(self); + /* Clean up private files */ + nm_clear_g_cancellable(&priv->private_files.cancellable); + g_clear_pointer(&priv->private_files.table, g_hash_table_unref); + g_clear_pointer(&priv->private_files.user, g_free); + priv->private_files.state = PRIVATE_FILES_STATE_UNKNOWN; + ifindex = nm_device_get_ip_ifindex(self); if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) { diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c index b890b110..148caa11 100644 --- a/src/core/devices/wifi/nm-device-wifi.c +++ b/src/core/devices/wifi/nm-device-wifi.c @@ -2946,7 +2946,8 @@ build_supplicant_config(NMDeviceWifi *self, s_wireless = nm_connection_get_setting_wireless(connection); g_return_val_if_fail(s_wireless != NULL, NULL); - config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface)); + config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface), + nm_utils_get_connection_first_permissions_user(connection)); /* Warn if AP mode may not be supported */ if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP) @@ -3022,6 +3023,7 @@ build_supplicant_config(NMDeviceWifi *self, mtu, pmf, fils, + nm_device_get_private_files(NM_DEVICE(self)), error)) { g_prefix_error(error, "802-11-wireless-security: "); goto error; diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c index d3c2eb21..4a7f760d 100644 --- a/src/core/nm-core-utils.c +++ b/src/core/nm-core-utils.c @@ -5011,6 +5011,7 @@ typedef struct { int child_stdin; int child_stdout; int child_stderr; + gboolean binary_output; GSource *input_source; GSource *output_source; GSource *error_source; @@ -5090,9 +5091,17 @@ helper_complete(HelperInfo *info, GError *error) } nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id); - g_task_return_pointer(info->task, - nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1), - g_free); + + if (info->binary_output) { + g_task_return_pointer( + info->task, + g_bytes_new(nm_str_buf_get_str_unsafe(&info->in_buffer), info->in_buffer.len), + (GDestroyNotify) (g_bytes_unref)); + } else { + g_task_return_pointer(info->task, + nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1), + g_free); + } helper_info_free(info); } @@ -5235,6 +5244,7 @@ helper_cancelled(GObject *object, gpointer user_data) void nm_utils_spawn_helper(const char *const *args, + gboolean binary_output, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer cb_data) @@ -5250,9 +5260,14 @@ nm_utils_spawn_helper(const char *const *args, info = g_new(HelperInfo, 1); *info = (HelperInfo) { - .task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data), + .task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data), + .binary_output = binary_output, }; + /* Store if the caller requested binary output so that we can check later + * that the right result function is called. */ + g_task_set_task_data(info->task, GINT_TO_POINTER(binary_output), NULL); + if (!g_spawn_async_with_pipes("/", (char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"), (char **) NM_MAKE_STRV(), @@ -5363,11 +5378,25 @@ nm_utils_spawn_helper(const char *const *args, } char * -nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error) +nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error) +{ + GTask *task = G_TASK(result); + + nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper)); + /* Check binary_output */ + nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == FALSE); + + return g_task_propagate_pointer(task, error); +} + +GBytes * +nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error) { GTask *task = G_TASK(result); nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper)); + /* Check binary_output */ + nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == TRUE); return g_task_propagate_pointer(task, error); } @@ -5474,3 +5503,185 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened) *shortened = g_steal_pointer(&s); return TRUE; } + +const char * +nm_utils_get_connection_first_permissions_user(NMConnection *connection) +{ + NMSettingConnection *s_con; + + s_con = nm_connection_get_setting_connection(connection); + nm_assert(s_con); + + return _nm_setting_connection_get_first_permissions_user(s_con); +} + +/*****************************************************************************/ + +const char ** +nm_utils_get_connection_private_files_paths(NMConnection *connection) +{ + GPtrArray *files; + gs_free NMSetting **settings = NULL; + guint num_settings; + guint i; + + files = g_ptr_array_new(); + settings = nm_connection_get_settings(connection, &num_settings); + for (i = 0; i < num_settings; i++) { + _nm_setting_get_private_files(settings[i], files); + } + g_ptr_array_add(files, NULL); + + return (const char **) g_ptr_array_free(files, files->len == 1); +} + +typedef struct _ReadInfo ReadInfo; + +typedef struct { + char *path; + ReadInfo *read_info; +} FileInfo; + +struct _ReadInfo { + GTask *task; + GHashTable *table; + GPtrArray *file_infos; /* of FileInfo */ + GError *first_error; + guint num_pending; +}; + +static void +read_file_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + FileInfo *file_info = user_data; + ReadInfo *read_info = file_info->read_info; + gs_unref_bytes GBytes *output = NULL; + gs_free_error GError *error = NULL; + + output = nm_utils_spawn_helper_finish_binary(result, &error); + + nm_assert(read_info->num_pending > 0); + read_info->num_pending--; + + if (nm_utils_error_is_cancelled(error)) { + /* nop */ + } else if (error) { + nm_log_dbg(LOGD_CORE, + "read-private-files: failed to read file '%s': %s", + file_info->path, + error->message); + if (!read_info->first_error) { + /* @error just says "helper process exited with status X". + * Return a more human-friendly one. */ + read_info->first_error = g_error_new(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "error reading file '%s'", + file_info->path); + } + } else { + nm_log_dbg(LOGD_SUPPLICANT, + "read-private-files: successfully read file '%s'", + file_info->path); + + /* Store the file contents in the hash table */ + if (!read_info->table) { + read_info->table = g_hash_table_new_full(nm_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_bytes_unref); + } + g_hash_table_insert(read_info->table, + g_steal_pointer(&file_info->path), + g_steal_pointer(&output)); + } + + g_clear_pointer(&file_info->path, g_free); + + /* If all operations are completed, return */ + if (read_info->num_pending == 0) { + if (read_info->first_error) { + g_task_return_error(read_info->task, g_steal_pointer(&read_info->first_error)); + } else { + g_task_return_pointer(read_info->task, + g_steal_pointer(&read_info->table), + (GDestroyNotify) g_hash_table_unref); + } + + if (read_info->table) + g_hash_table_unref(read_info->table); + if (read_info->file_infos) + g_ptr_array_unref(read_info->file_infos); + + g_object_unref(read_info->task); + g_free(read_info); + } +} + +/** + * nm_utils_read_private_files: + * @paths: array of file paths to be read + * @user: name of the user to impersonate when reading the files + * @cancellable: cancellable to cancel the operation + * @callback: callback to invoke on completion + * @cb_data: data for @callback + * + * Reads the given list of files @paths on behalf of user @user. Invokes + * @callback asynchronously on completion. The callback must use + * nm_utils_read_private_files_finish() to obtain the result. + */ +void +nm_utils_read_private_files(const char *const *paths, + const char *user, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer cb_data) +{ + ReadInfo *read_info; + FileInfo *file_info; + guint i; + + g_return_if_fail(paths && paths[0]); + g_return_if_fail(cancellable); + g_return_if_fail(callback); + g_return_if_fail(cb_data); + + read_info = g_new(ReadInfo, 1); + *read_info = (ReadInfo) { + .task = nm_g_task_new(NULL, cancellable, nm_utils_read_private_files, callback, cb_data), + .file_infos = g_ptr_array_new_with_free_func(g_free), + }; + + for (i = 0; paths[i]; i++) { + file_info = g_new(FileInfo, 1); + *file_info = (FileInfo) { + .path = g_strdup(paths[i]), + .read_info = read_info, + }; + g_ptr_array_add(read_info->file_infos, file_info); + read_info->num_pending++; + + nm_utils_spawn_helper(NM_MAKE_STRV("read-file-as-user", user, paths[i]), + TRUE, + cancellable, + read_file_helper_cb, + file_info); + } +} + +/** + * nm_utils_read_private_files_finish: + * @result: the GAsyncResult + * @error: on return, the error + * + * Returns the files read by nm_utils_read_private_files(). The return value + * is a hash table {char * -> GBytes *}. Free it with g_hash_table_unref(). + */ +GHashTable * +nm_utils_read_private_files_finish(GAsyncResult *result, GError **error) +{ + GTask *task = G_TASK(result); + + nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_read_private_files)); + + return g_task_propagate_pointer(task, error); +} diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h index fdfed5f6..224018c6 100644 --- a/src/core/nm-core-utils.h +++ b/src/core/nm-core-utils.h @@ -478,11 +478,13 @@ guint8 nm_wifi_utils_level_to_quality(int val); /*****************************************************************************/ void nm_utils_spawn_helper(const char *const *args, + gboolean binary_output, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer cb_data); -char *nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error); +char *nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error); +GBytes *nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error); /*****************************************************************************/ @@ -490,4 +492,19 @@ uid_t nm_utils_get_nm_uid(void); gid_t nm_utils_get_nm_gid(void); +/*****************************************************************************/ + +const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection); + +/*****************************************************************************/ + +const char **nm_utils_get_connection_private_files_paths(NMConnection *connection); + +void nm_utils_read_private_files(const char *const *paths, + const char *user, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer cb_data); +GHashTable *nm_utils_read_private_files_finish(GAsyncResult *result, GError **error); + #endif /* __NM_CORE_UTILS_H__ */ diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c index 9e066395..56cc832b 100644 --- a/src/core/supplicant/nm-supplicant-config.c +++ b/src/core/supplicant/nm-supplicant-config.c @@ -30,6 +30,7 @@ typedef struct { typedef struct { GHashTable *config; GHashTable *blobs; + char *private_user; NMSupplCapMask capabilities; guint32 ap_scan; bool fast_required : 1; @@ -60,7 +61,7 @@ _get_capability(NMSupplicantConfigPrivate *priv, NMSupplCapType type) } NMSupplicantConfig * -nm_supplicant_config_new(NMSupplCapMask capabilities) +nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user) { NMSupplicantConfigPrivate *priv; NMSupplicantConfig *self; @@ -69,6 +70,7 @@ nm_supplicant_config_new(NMSupplCapMask capabilities) priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self); priv->capabilities = capabilities; + priv->private_user = g_strdup(private_user); return self; } @@ -258,19 +260,19 @@ static gboolean nm_supplicant_config_add_blob_for_connection(NMSupplicantConfig *self, GBytes *field, const char *name, - const char *con_uid, + const char *con_uuid, GError **error) { if (field && g_bytes_get_size(field)) { - gs_free char *uid = NULL; + gs_free char *blob_id = NULL; char *p; - uid = g_strdup_printf("%s-%s", con_uid, name); - for (p = uid; *p; p++) { + blob_id = g_strdup_printf("%s-%s", con_uuid, name); + for (p = blob_id; *p; p++) { if (*p == '/') *p = '-'; } - if (!nm_supplicant_config_add_blob(self, name, field, uid, error)) + if (!nm_supplicant_config_add_blob(self, name, field, blob_id, error)) return FALSE; } return TRUE; @@ -283,6 +285,7 @@ nm_supplicant_config_finalize(GObject *object) g_hash_table_destroy(priv->config); nm_clear_pointer(&priv->blobs, g_hash_table_destroy); + nm_clear_pointer(&priv->private_user, g_free); G_OBJECT_CLASS(nm_supplicant_config_parent_class)->finalize(object); } @@ -930,6 +933,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig guint32 mtu, NMSettingWirelessSecurityPmf pmf, NMSettingWirelessSecurityFils fils, + GHashTable *files, GError **error) { NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self); @@ -1284,6 +1288,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig con_uuid, mtu, FALSE, + files, error)) return FALSE; } @@ -1365,6 +1370,7 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, const char *con_uuid, guint32 mtu, gboolean wired, + GHashTable *files, GError **error) { NMSupplicantConfigPrivate *priv; @@ -1594,24 +1600,21 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, } /* CA certificate */ + path = NULL; + bytes = NULL; if (ca_cert_override) { - if (!add_string_val(self, ca_cert_override, "ca_cert", FALSE, NULL, error)) - return FALSE; + /* This is a build-time-configured system-wide file path, no need to pass + * it as a blob */ + path = ca_cert_override; } else { switch (nm_setting_802_1x_get_ca_cert_scheme(setting)) { case NM_SETTING_802_1X_CK_SCHEME_BLOB: bytes = nm_setting_802_1x_get_ca_cert_blob(setting); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "ca_cert", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_ca_cert_path(setting); - if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: if (!add_pkcs11_uri_with_pin(self, @@ -1627,26 +1630,32 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, break; } } + if (bytes) { + if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert", con_uuid, error)) + return FALSE; + } else if (path) { + /* Private connections cannot use paths other than the system CA store */ + g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE); + if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error)) + return FALSE; + } /* Phase 2 CA certificate */ + path = NULL; + bytes = NULL; if (ca_cert_override) { - if (!add_string_val(self, ca_cert_override, "ca_cert2", FALSE, NULL, error)) - return FALSE; + /* This is a build-time-configured system-wide file path, no need to pass + * it as a blob */ + path = ca_cert_override; } else { switch (nm_setting_802_1x_get_phase2_ca_cert_scheme(setting)) { case NM_SETTING_802_1X_CK_SCHEME_BLOB: bytes = nm_setting_802_1x_get_phase2_ca_cert_blob(setting); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "ca_cert2", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_phase2_ca_cert_path(setting); - if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: if (!add_pkcs11_uri_with_pin( @@ -1663,6 +1672,15 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, break; } } + if (bytes) { + if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert2", con_uuid, error)) + return FALSE; + } else if (path) { + /* Private connections cannot use paths other than the system CA store */ + g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE); + if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error)) + return FALSE; + } /* Subject match */ value = nm_setting_802_1x_get_subject_match(setting); @@ -1714,21 +1732,17 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, /* Private key */ added = FALSE; + path = NULL; + bytes = NULL; switch (nm_setting_802_1x_get_private_key_scheme(setting)) { case NM_SETTING_802_1X_CK_SCHEME_BLOB: bytes = nm_setting_802_1x_get_private_key_blob(setting); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "private_key", - con_uuid, - error)) - return FALSE; added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_private_key_path(setting); - if (!add_string_val(self, path, "private_key", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: @@ -1745,6 +1759,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, default: break; } + if (bytes) { + if (!nm_supplicant_config_add_blob_for_connection(self, + bytes, + "private_key", + con_uuid, + error)) + return FALSE; + } else if (path) { + /* Private connections cannot use paths */ + g_return_val_if_fail(!priv->private_user, FALSE); + if (!add_string_val(self, path, "private_key", FALSE, NULL, error)) + return FALSE; + } if (added) { NMSetting8021xCKFormat format; @@ -1768,20 +1795,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, /* Only add the client cert if the private key is not PKCS#12, as * wpa_supplicant configuration directs us to do. */ + path = NULL; + bytes = NULL; switch (nm_setting_802_1x_get_client_cert_scheme(setting)) { case NM_SETTING_802_1X_CK_SCHEME_BLOB: bytes = nm_setting_802_1x_get_client_cert_blob(setting); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "client_cert", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_client_cert_path(setting); - if (!add_string_val(self, path, "client_cert", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: if (!add_pkcs11_uri_with_pin( @@ -1797,26 +1820,35 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, default: break; } + if (bytes) { + if (!nm_supplicant_config_add_blob_for_connection(self, + bytes, + "client_cert", + con_uuid, + error)) + return FALSE; + } else if (path) { + /* Private connections cannot use paths */ + g_return_val_if_fail(!priv->private_user, FALSE); + if (!add_string_val(self, path, "client_cert", FALSE, NULL, error)) + return FALSE; + } } } /* Phase 2 private key */ added = FALSE; + path = NULL; + bytes = NULL; switch (nm_setting_802_1x_get_phase2_private_key_scheme(setting)) { case NM_SETTING_802_1X_CK_SCHEME_BLOB: bytes = nm_setting_802_1x_get_phase2_private_key_blob(setting); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "private_key2", - con_uuid, - error)) - return FALSE; added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_phase2_private_key_path(setting); - if (!add_string_val(self, path, "private_key2", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: @@ -1834,6 +1866,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, default: break; } + if (bytes) { + if (!nm_supplicant_config_add_blob_for_connection(self, + bytes, + "private_key2", + con_uuid, + error)) + return FALSE; + } else if (path) { + /* Private connections cannot use paths */ + g_return_val_if_fail(!priv->private_user, FALSE); + if (!add_string_val(self, path, "private_key2", FALSE, NULL, error)) + return FALSE; + } if (added) { NMSetting8021xCKFormat format; @@ -1857,20 +1902,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, /* Only add the client cert if the private key is not PKCS#12, as * wpa_supplicant configuration directs us to do. */ + path = NULL; + bytes = NULL; switch (nm_setting_802_1x_get_phase2_client_cert_scheme(setting)) { case NM_SETTING_802_1X_CK_SCHEME_BLOB: bytes = nm_setting_802_1x_get_phase2_client_cert_blob(setting); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "client_cert2", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_phase2_client_cert_path(setting); - if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: if (!add_pkcs11_uri_with_pin( @@ -1886,6 +1927,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, default: break; } + if (bytes) { + if (!nm_supplicant_config_add_blob_for_connection(self, + bytes, + "client_cert2", + con_uuid, + error)) + return FALSE; + } else if (path) { + /* Private connections cannot use paths */ + g_return_val_if_fail(!priv->private_user, FALSE); + if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error)) + return FALSE; + } } } diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h index c52b756e..96460b86 100644 --- a/src/core/supplicant/nm-supplicant-config.h +++ b/src/core/supplicant/nm-supplicant-config.h @@ -29,7 +29,7 @@ typedef struct _NMSupplicantConfigClass NMSupplicantConfigClass; GType nm_supplicant_config_get_type(void); -NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities); +NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user); guint32 nm_supplicant_config_get_ap_scan(NMSupplicantConfig *self); @@ -57,6 +57,7 @@ gboolean nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig guint32 mtu, NMSettingWirelessSecurityPmf pmf, NMSettingWirelessSecurityFils fils, + GHashTable *files, GError **error); gboolean nm_supplicant_config_add_no_security(NMSupplicantConfig *self, GError **error); @@ -66,6 +67,7 @@ gboolean nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, const char *con_uuid, guint32 mtu, gboolean wired, + GHashTable *files, GError **error); gboolean nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self, diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c index 514b7c0d..ef72447a 100644 --- a/src/core/supplicant/nm-supplicant-interface.c +++ b/src/core/supplicant/nm-supplicant-interface.c @@ -46,6 +46,7 @@ typedef struct { gpointer user_data; guint fail_on_idle_id; guint blobs_left; + guint remove_blobs_left; guint calls_left; struct _AddNetworkData *add_network_data; } AssocData; @@ -2264,6 +2265,7 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data) return; } + nm_assert(priv->assoc_data->blobs_left > 0); priv->assoc_data->blobs_left--; _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob added (%u left)", NM_HASH_OBFUSCATE_PTR(priv->assoc_data), @@ -2272,6 +2274,148 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data) assoc_call_select_network(self); } +static void +assoc_add_blobs(NMSupplicantInterface *self) +{ + NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self); + GHashTable *blobs; + GHashTableIter iter; + const char *blob_name; + GBytes *blob_data; + + blobs = nm_supplicant_config_get_blobs(priv->assoc_data->cfg); + priv->assoc_data->blobs_left = nm_g_hash_table_size(blobs); + + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to add %u blobs", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + priv->assoc_data->blobs_left); + + if (priv->assoc_data->blobs_left == 0) { + assoc_call_select_network(self); + return; + } + + g_hash_table_iter_init(&iter, blobs); + while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) { + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: adding blob '%s'", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + blob_name); + _dbus_connection_call( + self, + NM_WPAS_DBUS_IFACE_INTERFACE, + "AddBlob", + g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)), + G_VARIANT_TYPE("()"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MSEC, + priv->assoc_data->cancellable, + assoc_add_blob_cb, + self); + } +} + +static void +assoc_remove_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMSupplicantInterface *self; + NMSupplicantInterfacePrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *res = NULL; + + res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + self = NM_SUPPLICANT_INTERFACE(user_data); + priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self); + + /* We don't consider a failure fatal. The new association might be able + * to proceed even with the existing blobs, if they don't conflict with new + * ones. */ + + nm_assert(priv->assoc_data->remove_blobs_left > 0); + priv->assoc_data->remove_blobs_left--; + + if (error) { + g_dbus_error_strip_remote_error(error); + _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to delete blob: %s", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + error->message); + } else { + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob removed (%u left)", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + priv->assoc_data->remove_blobs_left); + } + + if (priv->assoc_data->remove_blobs_left == 0) + assoc_add_blobs(self); +} + +static void +assoc_get_blobs_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMSupplicantInterface *self; + NMSupplicantInterfacePrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *res = NULL; + gs_unref_variant GVariant *value = NULL; + GVariantIter iter; + const char *blob_name; + GVariant *blob_data; + + res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + self = NM_SUPPLICANT_INTERFACE(user_data); + priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self); + + if (error) { + _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: %s", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + error->message); + assoc_add_blobs(self); + return; + } + + g_variant_get(res, "(v)", &value); + + /* While the "Blobs" property is documented as type "as", it is actually "a{say}" */ + if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE("a{say}"))) { + _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: wrong return type %s", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + value ? g_variant_get_type_string(value) : "NULL"); + assoc_add_blobs(self); + return; + } + + g_variant_iter_init(&iter, value); + priv->assoc_data->remove_blobs_left = g_variant_iter_n_children(&iter); + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to delete %u blobs", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + priv->assoc_data->remove_blobs_left); + + if (priv->assoc_data->remove_blobs_left == 0) { + assoc_add_blobs(self); + } else { + while (g_variant_iter_loop(&iter, "{&s@ay}", &blob_name, &blob_data)) { + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: removing blob '%s'", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + blob_name); + _dbus_connection_call(self, + NM_WPAS_DBUS_IFACE_INTERFACE, + "RemoveBlob", + g_variant_new("(s)", blob_name), + G_VARIANT_TYPE("()"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MSEC, + priv->assoc_data->cancellable, + assoc_remove_blob_cb, + self); + } + } +} + static void assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data) { @@ -2279,12 +2423,8 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data) AssocData *assoc_data; NMSupplicantInterface *self; NMSupplicantInterfacePrivate *priv; - gs_unref_variant GVariant *res = NULL; - gs_free_error GError *error = NULL; - GHashTable *blobs; - GHashTableIter iter; - const char *blob_name; - GBytes *blob_data; + gs_unref_variant GVariant *res = NULL; + gs_free_error GError *error = NULL; nm_auto_ref_string NMRefString *name_owner = NULL; nm_auto_ref_string NMRefString *object_path = NULL; @@ -2336,34 +2476,21 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data) nm_assert(!priv->net_path); g_variant_get(res, "(o)", &priv->net_path); - /* Send blobs first; otherwise jump to selecting the network */ - blobs = nm_supplicant_config_get_blobs(priv->assoc_data->cfg); - priv->assoc_data->blobs_left = blobs ? g_hash_table_size(blobs) : 0u; - - _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s) (%u blobs left)", + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s)", NM_HASH_OBFUSCATE_PTR(priv->assoc_data), - priv->net_path, - priv->assoc_data->blobs_left); - - if (priv->assoc_data->blobs_left == 0) { - assoc_call_select_network(self); - return; - } + priv->net_path); - g_hash_table_iter_init(&iter, blobs); - while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) { - _dbus_connection_call( - self, - NM_WPAS_DBUS_IFACE_INTERFACE, - "AddBlob", - g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)), - G_VARIANT_TYPE("()"), - G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MSEC, - priv->assoc_data->cancellable, - assoc_add_blob_cb, - self); - } + /* Delete any existing blobs before adding new ones */ + _dbus_connection_call(self, + DBUS_INTERFACE_PROPERTIES, + "Get", + g_variant_new("(ss)", NM_WPAS_DBUS_IFACE_INTERFACE, "Blobs"), + G_VARIANT_TYPE("(v)"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MSEC, + priv->assoc_data->cancellable, + assoc_get_blobs_cb, + self); } static void diff --git a/src/core/supplicant/tests/test-supplicant-config.c b/src/core/supplicant/tests/test-supplicant-config.c index 1ca5b26e..416fe005 100644 --- a/src/core/supplicant/tests/test-supplicant-config.c +++ b/src/core/supplicant/tests/test-supplicant-config.c @@ -98,7 +98,8 @@ build_supplicant_config(NMConnection *connection, NMSetting8021x *s_8021x; gboolean success; - config = nm_supplicant_config_new(capabilities); + config = nm_supplicant_config_new(capabilities, + nm_utils_get_connection_first_permissions_user(connection)); s_wifi = nm_connection_get_setting_wireless(connection); g_assert(s_wifi); @@ -120,6 +121,7 @@ build_supplicant_config(NMConnection *connection, mtu, pmf, fils, + NULL, &error); } else { success = nm_supplicant_config_add_no_security(config, &error); diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index 040d6829..bd66e61d 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2083,3 +2083,9 @@ global: nm_setting_hsr_get_protocol_version; nm_setting_hsr_protocol_version_get_type; } libnm_1_54_0; + +libnm_1_54_3 { +global: + nm_utils_copy_cert_as_user; + nm_vpn_plugin_info_supports_safe_private_file_access; +} libnm_1_54_2; diff --git a/src/libnm-client-impl/tests/meson.build b/src/libnm-client-impl/tests/meson.build index 42e9883e..500504db 100644 --- a/src/libnm-client-impl/tests/meson.build +++ b/src/libnm-client-impl/tests/meson.build @@ -5,6 +5,7 @@ test_units = [ 'test-nm-client', 'test-remote-settings-client', 'test-secret-agent', + 'test-copy-cert-as-user' ] foreach test_unit: test_units @@ -37,12 +38,15 @@ foreach test_unit: test_units ], ) - test( - 'src/libnm-client-impl/tests/' + test_unit, - test_script, - timeout: 90, - args: test_args + [exe.full_path()], - ) + # test-copy-cert-as-user is a manual test, don't run it automatically + if test_unit != 'test-copy-cert-as-user' + test( + 'src/libnm-client-impl/tests/' + test_unit, + test_script, + timeout: 90, + args: test_args + [exe.full_path()], + ) + endif endforeach if enable_introspection diff --git a/src/libnm-client-impl/tests/test-copy-cert-as-user.c b/src/libnm-client-impl/tests/test-copy-cert-as-user.c new file mode 100644 index 00000000..b2ef9de6 --- /dev/null +++ b/src/libnm-client-impl/tests/test-copy-cert-as-user.c @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * This is a program to manually test the + * nm_utils_copy_cert_as_user() libnm function. + */ + +#include "libnm-client-impl/nm-default-libnm.h" + +#include "nm-utils.h" + +int +main(int argc, char **argv) +{ + gs_free_error GError *error = NULL; + gs_free char *filename = NULL; + + if (argc != 3) { + g_printerr("Usage: %s \n", argv[0]); + return 1; + } + + filename = nm_utils_copy_cert_as_user(argv[1], argv[2], &error); + if (!filename) { + g_printerr("Error: %s\n", error->message); + return 1; + } + + g_print("%s\n", filename); + + return 0; +} diff --git a/src/libnm-client-impl/tests/test-gir.py b/src/libnm-client-impl/tests/test-gir.py index 554080fa..81518e37 100755 --- a/src/libnm-client-impl/tests/test-gir.py +++ b/src/libnm-client-impl/tests/test-gir.py @@ -93,6 +93,8 @@ def syms_from_ver(verfile): # hardcode it. c_syms["nm_ethtool_optname_is_feature"] = "1.20" c_syms["nm_setting_bond_port_get_prio"] = "1.44" + c_syms["nm_utils_copy_cert_as_user"] = "1.56" + c_syms["nm_vpn_plugin_info_supports_safe_private_file_access"] = "1.56" return c_syms diff --git a/src/libnm-core-impl/nm-setting-8021x.c b/src/libnm-core-impl/nm-setting-8021x.c index 4ea60729..f9333803 100644 --- a/src/libnm-core-impl/nm-setting-8021x.c +++ b/src/libnm-core-impl/nm-setting-8021x.c @@ -3133,6 +3133,86 @@ need_secrets(NMSetting *setting, gboolean check_rerequest) /*****************************************************************************/ +static void +get_private_files(NMSetting *setting, GPtrArray *files) +{ + const struct { + const char *property; + NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *); + const char *(*get_path_func)(NMSetting8021x *); + } cert_props[] = { + {NM_SETTING_802_1X_CA_CERT, + nm_setting_802_1x_get_ca_cert_scheme, + nm_setting_802_1x_get_ca_cert_path}, + {NM_SETTING_802_1X_CLIENT_CERT, + nm_setting_802_1x_get_client_cert_scheme, + nm_setting_802_1x_get_client_cert_path}, + {NM_SETTING_802_1X_PRIVATE_KEY, + nm_setting_802_1x_get_private_key_scheme, + nm_setting_802_1x_get_private_key_path}, + {NM_SETTING_802_1X_PHASE2_CA_CERT, + nm_setting_802_1x_get_phase2_ca_cert_scheme, + nm_setting_802_1x_get_phase2_ca_cert_path}, + {NM_SETTING_802_1X_PHASE2_CLIENT_CERT, + nm_setting_802_1x_get_phase2_client_cert_scheme, + nm_setting_802_1x_get_phase2_client_cert_path}, + {NM_SETTING_802_1X_PHASE2_PRIVATE_KEY, + nm_setting_802_1x_get_phase2_private_key_scheme, + nm_setting_802_1x_get_phase2_private_key_path}, + }; + NMSetting8021x *s_8021x = NM_SETTING_802_1X(setting); + const char *path; + guint i; + + if (NM_MORE_ASSERT_ONCE(5)) { + GObjectClass *klass; + gs_free GParamSpec **properties = NULL; + guint n_properties; + gboolean found; + guint j; + + /* Check that all the properties in the setting with flag CERT_KEY_FILE + * are listed in the table, and vice versa. */ + + klass = G_OBJECT_GET_CLASS(setting); + + properties = g_object_class_list_properties(klass, &n_properties); + for (i = 0; i < n_properties; i++) { + if (!(properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE)) + continue; + + found = FALSE; + for (j = 0; j < G_N_ELEMENTS(cert_props); j++) { + if (nm_streq0(properties[i]->name, cert_props[j].property)) { + found = TRUE; + break; + } + } + + nm_assert(found); + } + + for (i = 0; i < G_N_ELEMENTS(cert_props); i++) { + GParamSpec *prop; + + prop = g_object_class_find_property(klass, cert_props[i].property); + nm_assert(prop); + nm_assert(prop->flags & NM_SETTING_PARAM_CERT_KEY_FILE); + } + } + + for (i = 0; i < G_N_ELEMENTS(cert_props); i++) { + if (cert_props[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) { + path = cert_props[i].get_path_func(s_8021x); + if (path) { + g_ptr_array_add(files, (gpointer) path); + } + } + } +} + +/*****************************************************************************/ + static void get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { @@ -3223,8 +3303,9 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) object_class->set_property = set_property; object_class->finalize = finalize; - setting_class->verify = verify; - setting_class->need_secrets = need_secrets; + setting_class->verify = verify; + setting_class->need_secrets = need_secrets; + setting_class->get_private_files = get_private_files; /** * NMSetting8021x:eap: @@ -3359,7 +3440,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_CA_CERT, PROP_CA_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, ca_cert); @@ -3556,7 +3637,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_CLIENT_CERT, PROP_CLIENT_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, client_cert); @@ -3803,7 +3884,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PHASE2_CA_CERT, PROP_PHASE2_CA_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, phase2_ca_cert); @@ -4006,7 +4087,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PHASE2_CLIENT_CERT, PROP_PHASE2_CLIENT_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, phase2_client_cert); @@ -4175,7 +4256,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PRIVATE_KEY, PROP_PRIVATE_KEY, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, private_key); @@ -4276,7 +4357,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PHASE2_PRIVATE_KEY, PROP_PHASE2_PRIVATE_KEY, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, phase2_private_key); diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c index 4829dcdc..ec3a1fed 100644 --- a/src/libnm-core-impl/nm-setting-connection.c +++ b/src/libnm-core-impl/nm-setting-connection.c @@ -431,6 +431,47 @@ nm_setting_connection_permissions_user_allowed_by_uid(NMSettingConnection *setti return _permissions_user_allowed(setting, NULL, uid); } +guint +_nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting) +{ + NMSettingConnectionPrivate *priv; + guint i; + guint count = 0; + + nm_assert(NM_IS_SETTING_CONNECTION(setting)); + priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting); + + for (i = 0; priv->permissions && i < priv->permissions->len; i++) { + const Permission *permission = &nm_g_array_index(priv->permissions, Permission, i); + + if (permission->ptype == PERM_TYPE_USER) { + count++; + } + } + + return count; +} + +const char * +_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting) +{ + NMSettingConnectionPrivate *priv; + guint i; + + nm_assert(NM_IS_SETTING_CONNECTION(setting)); + priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting); + + for (i = 0; priv->permissions && i < priv->permissions->len; i++) { + const Permission *permission = &nm_g_array_index(priv->permissions, Permission, i); + + if (permission->ptype == PERM_TYPE_USER) { + return permission->item; + } + } + + return NULL; +} + /** * nm_setting_connection_add_permission: * @setting: the #NMSettingConnection diff --git a/src/libnm-core-impl/nm-setting-private.h b/src/libnm-core-impl/nm-setting-private.h index 8ee770f4..61f96789 100644 --- a/src/libnm-core-impl/nm-setting-private.h +++ b/src/libnm-core-impl/nm-setting-private.h @@ -154,6 +154,11 @@ struct _NMSettingClass { guint /* NMSettingParseFlags */ parse_flags, GError **error); + /* returns a list of certificate/key files referenced in the connection. + * When the connection is private, we need to verify that the owner of + * the connection has access to them. */ + void (*get_private_files)(NMSetting *setting, GPtrArray *files); + const struct _NMMetaSettingInfo *setting_info; }; @@ -334,6 +339,11 @@ struct _NMRange { */ #define NM_SETTING_PARAM_TO_DBUS_IGNORE_FLAGS (1 << (7 + G_PARAM_USER_SHIFT)) +/* The property can refer to a certificate or key stored on disk. As such, + * special care is needed when accessing the file for private connections. + */ +#define NM_SETTING_PARAM_CERT_KEY_FILE (1 << (8 + G_PARAM_USER_SHIFT)) + extern const NMSettInfoPropertType nm_sett_info_propert_type_setting_name; extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_interface_name; extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_i; @@ -859,9 +869,10 @@ _nm_properties_override(GArray *properties_override, const NMSettInfoProperty *p { \ GParamSpec *_param_spec; \ \ - G_STATIC_ASSERT(!NM_FLAGS_ANY((param_flags), \ - ~(NM_SETTING_PARAM_SECRET | NM_SETTING_PARAM_INFERRABLE \ - | NM_SETTING_PARAM_FUZZY_IGNORE))); \ + G_STATIC_ASSERT( \ + !NM_FLAGS_ANY((param_flags), \ + ~(NM_SETTING_PARAM_SECRET | NM_SETTING_PARAM_INFERRABLE \ + | NM_SETTING_PARAM_FUZZY_IGNORE | NM_SETTING_PARAM_CERT_KEY_FILE))); \ \ _param_spec = g_param_spec_boxed("" prop_name "", \ "", \ diff --git a/src/libnm-core-impl/nm-setting.c b/src/libnm-core-impl/nm-setting.c index 98424c76..295eabe7 100644 --- a/src/libnm-core-impl/nm-setting.c +++ b/src/libnm-core-impl/nm-setting.c @@ -2262,6 +2262,34 @@ init_from_dbus(NMSetting *setting, return TRUE; } +static void +get_private_files(NMSetting *setting, GPtrArray *files) +{ + if (NM_MORE_ASSERTS) { + GParamSpec **properties; + guint n_properties; + int i; + + properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(setting), &n_properties); + for (i = 0; i < n_properties; i++) { + if (properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE) { + /* Certificates and keys needs special handling, see setting 802.1X */ + nm_assert_not_reached(); + } + } + g_free(properties); + } +} + +void +_nm_setting_get_private_files(NMSetting *setting, GPtrArray *files) +{ + g_return_if_fail(NM_IS_SETTING(setting)); + g_return_if_fail(files); + + NM_SETTING_GET_CLASS(setting)->get_private_files(setting, files); +} + /** * nm_setting_get_dbus_property_type: * @setting: an #NMSetting @@ -4672,6 +4700,7 @@ nm_setting_class_init(NMSettingClass *setting_class) setting_class->enumerate_values = enumerate_values; setting_class->aggregate = aggregate; setting_class->init_from_dbus = init_from_dbus; + setting_class->get_private_files = get_private_files; /** * NMSetting:name: diff --git a/src/libnm-core-impl/nm-utils.c b/src/libnm-core-impl/nm-utils.c index 6d5df98c..9a78e947 100644 --- a/src/libnm-core-impl/nm-utils.c +++ b/src/libnm-core-impl/nm-utils.c @@ -17,6 +17,7 @@ #include #include +#include "libnm-glib-aux/nm-io-utils.h" #include "libnm-glib-aux/nm-uuid.h" #include "libnm-glib-aux/nm-json-aux.h" #include "libnm-glib-aux/nm-str-buf.h" @@ -6195,3 +6196,257 @@ nm_utils_ensure_gtypes(void) for (meta_type = 0; meta_type < _NM_META_SETTING_TYPE_NUM; meta_type++) nm_meta_setting_infos[meta_type].get_setting_gtype(); } + +/*****************************************************************************/ + +typedef struct { + GPid pid; + GSource *child_watch_source; + GMainLoop *loop; + GError *error; + + int child_stdout; + int child_stderr; + + GSource *output_source; + GSource *error_source; + + NMStrBuf output_buffer; + NMStrBuf error_buffer; +} HelperInfo; + +static void +helper_complete(HelperInfo *info, GError *error_take) +{ + if (error_take) { + if (!info->error) + info->error = error_take; + else + g_error_free(error_take); + } + + if (info->output_source || info->error_source || info->pid != -1) { + /* Wait that the pipe is closed and process has terminated */ + return; + } + + if (info->error && info->error_buffer.len > 0) { + /* Prefer the message from stderr as it's more informative */ + g_error_free(info->error); + info->error = g_error_new(NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_FAILED, + "%s", + nm_str_buf_get_str(&info->error_buffer)); + } + + g_main_loop_quit(info->loop); +} + +static gboolean +helper_have_err_data(int fd, GIOCondition condition, gpointer user_data) +{ + HelperInfo *info = user_data; + gssize n_read; + GError *error = NULL; + + n_read = nm_utils_fd_read(fd, &info->error_buffer); + + if (n_read > 0) + return G_SOURCE_CONTINUE; + + nm_clear_g_source_inst(&info->error_source); + nm_clear_fd(&info->child_stderr); + + if (n_read < 0) { + error = g_error_new(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "read from process returned %d (%s)", + (int) -n_read, + nm_strerror_native((int) -n_read)); + } + + helper_complete(info, error); + return G_SOURCE_CONTINUE; +} + +static gboolean +helper_have_data(int fd, GIOCondition condition, gpointer user_data) +{ + HelperInfo *info = user_data; + gssize n_read; + GError *error = NULL; + + n_read = nm_utils_fd_read(fd, &info->output_buffer); + + if (n_read > 0) + return G_SOURCE_CONTINUE; + + nm_clear_g_source_inst(&info->output_source); + nm_clear_fd(&info->child_stdout); + + if (n_read < 0) { + error = g_error_new(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "read from process returned %d (%s)", + (int) -n_read, + nm_strerror_native((int) -n_read)); + } + + helper_complete(info, error); + return G_SOURCE_CONTINUE; +} + +static void +helper_child_terminated(GPid pid, int status, gpointer user_data) +{ + HelperInfo *info = user_data; + gs_free char *status_desc = NULL; + GError *error = NULL; + + info->pid = -1; + nm_clear_g_source_inst(&info->child_watch_source); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + if (!status_desc) + status_desc = nm_utils_get_process_exit_status_desc(status); + error = + g_error_new(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "helper process %s", status_desc); + } + + helper_complete(info, error); +} + +#define RUN_CERT_DIR NMRUNDIR "/cert" + +/** + * nm_utils_copy_cert_as_user: + * @filename: the file name of the certificate or key to copy + * @user: the user to impersonate when reading the file + * @error: (nullable): return location for a #GError, or %NULL + * + * Reads @filename on behalf of user @user and writes the + * content to a new file in /run/NetworkManager/cert/. + * The new file has permission 600 and is owned by root. + * + * This function is useful for VPN plugins that run as root and need + * to verify that the user owning the connection (the one listed in the + * connection.permissions property) can access the file. + * + * Returns: (transfer full): the name of the new temporary file. Or %NULL + * if an error occurred, including when the given user can't access the + * file. + * + * Since: 1.56, 1.54.3 + */ +char * +nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error) +{ + gs_unref_bytes GBytes *bytes = NULL; + char dst_path[] = RUN_CERT_DIR "/XXXXXX"; + HelperInfo info = { + .child_stdout = -1, + .child_stderr = -1, + }; + GMainContext *context; + int fd = -1; + + g_return_val_if_fail(filename, NULL); + g_return_val_if_fail(user, NULL); + g_return_val_if_fail(!error || !*error, NULL); + + if (geteuid() != 0) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("This function needs to be called by root")); + return NULL; + } + + if (!g_spawn_async_with_pipes( + "/", + (char **) + NM_MAKE_STRV(LIBEXECDIR "/nm-libnm-helper", "read-file-as-user", filename, user), + (char **) NM_MAKE_STRV(), + G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, + NULL, + &info.pid, + NULL, + &info.child_stdout, + &info.child_stderr, + error)) { + return NULL; + } + + context = g_main_context_new(); + info.loop = g_main_loop_new(context, FALSE); + + /* Watch process */ + info.child_watch_source = nm_g_child_watch_source_new(info.pid, + G_PRIORITY_DEFAULT, + helper_child_terminated, + &info, + NULL); + g_source_attach(info.child_watch_source, context); + + /* Watch stdout */ + info.output_buffer = NM_STR_BUF_INIT(0, FALSE); + info.output_source = nm_g_unix_fd_source_new(info.child_stdout, + G_IO_IN | G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + helper_have_data, + &info, + NULL); + g_source_attach(info.output_source, context); + + /* Watch stderr */ + info.error_buffer = NM_STR_BUF_INIT(0, FALSE); + info.error_source = nm_g_unix_fd_source_new(info.child_stderr, + G_IO_IN | G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + helper_have_err_data, + &info, + NULL); + g_source_attach(info.error_source, context); + + /* Wait termination */ + g_main_loop_run(info.loop); + g_clear_pointer(&info.loop, g_main_loop_unref); + g_clear_pointer(&context, g_main_context_unref); + + if (info.error) { + nm_str_buf_destroy(&info.output_buffer); + nm_str_buf_destroy(&info.error_buffer); + g_propagate_error(error, g_steal_pointer(&info.error)); + return NULL; + } + + /* Write the data to a new file */ + + bytes = g_bytes_new(nm_str_buf_get_str_unsafe(&info.output_buffer), info.output_buffer.len); + nm_str_buf_destroy(&info.output_buffer); + nm_str_buf_destroy(&info.error_buffer); + + mkdir(RUN_CERT_DIR, 0600); + fd = mkstemp(dst_path); + if (fd < 0) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Failure creating the temporary file")); + return NULL; + } + nm_close(fd); + + if (!nm_utils_file_set_contents(dst_path, + g_bytes_get_data(bytes, NULL), + g_bytes_get_size(bytes), + 0600, + NULL, + NULL, + error)) { + return NULL; + } + + return g_strdup(dst_path); +} diff --git a/src/libnm-core-impl/nm-vpn-plugin-info.c b/src/libnm-core-impl/nm-vpn-plugin-info.c index 223d8ab3..04b0fb16 100644 --- a/src/libnm-core-impl/nm-vpn-plugin-info.c +++ b/src/libnm-core-impl/nm-vpn-plugin-info.c @@ -913,6 +913,29 @@ nm_vpn_plugin_info_supports_multiple(NMVpnPluginInfo *self) return _nm_utils_ascii_str_to_bool(s, FALSE); } +/** + * nm_vpn_plugin_info_supports_safe_private_file_access: + * @self: plugin info instance + * + * Returns: %TRUE if the service supports reading files (certificates, keys) of + * private connections in a safe way (i.e. checking user permissions), or + if the service doesn't need to read any file from disk. + * + * Since: 1.56, 1.54.3 + */ +gboolean +nm_vpn_plugin_info_supports_safe_private_file_access(NMVpnPluginInfo *self) +{ + const char *s; + + g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), FALSE); + + s = nm_vpn_plugin_info_lookup_property(self, + NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION, + "supports-safe-private-file-access"); + return _nm_utils_ascii_str_to_bool(s, FALSE); +} + /** * nm_vpn_plugin_info_get_aliases: * @self: plugin info instance diff --git a/src/libnm-core-intern/nm-core-internal.h b/src/libnm-core-intern/nm-core-internal.h index 8b23dd44..b8df4d2e 100644 --- a/src/libnm-core-intern/nm-core-internal.h +++ b/src/libnm-core-intern/nm-core-internal.h @@ -1187,4 +1187,11 @@ gboolean nm_connection_need_secrets_for_rerequest(NMConnection *connection); const GPtrArray *_nm_setting_ovs_port_get_trunks_arr(NMSettingOvsPort *self); +/*****************************************************************************/ + +guint _nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting); +const char *_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting); + +void _nm_setting_get_private_files(NMSetting *setting, GPtrArray *files); + #endif diff --git a/src/libnm-core-public/nm-utils.h b/src/libnm-core-public/nm-utils.h index 35ef580d..dd7977a1 100644 --- a/src/libnm-core-public/nm-utils.h +++ b/src/libnm-core-public/nm-utils.h @@ -261,6 +261,9 @@ nm_utils_base64secret_decode(const char *base64_key, gsize required_key_len, gui NM_AVAILABLE_IN_1_42 void nm_utils_ensure_gtypes(void); +NM_AVAILABLE_IN_1_54_3 +char *nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error); + G_END_DECLS #endif /* __NM_UTILS_H__ */ diff --git a/src/libnm-core-public/nm-version-macros.h.in b/src/libnm-core-public/nm-version-macros.h.in index 4bdf6716..9771944d 100644 --- a/src/libnm-core-public/nm-version-macros.h.in +++ b/src/libnm-core-public/nm-version-macros.h.in @@ -79,6 +79,7 @@ #define NM_VERSION_1_52 (NM_ENCODE_VERSION(1, 52, 0)) #define NM_VERSION_1_54 (NM_ENCODE_VERSION(1, 54, 0)) #define NM_VERSION_1_54_2 (NM_ENCODE_VERSION(1, 54, 2)) +#define NM_VERSION_1_54_3 (NM_ENCODE_VERSION(1, 54, 3)) /* For releases, NM_API_VERSION is equal to NM_VERSION. * @@ -87,10 +88,10 @@ * version, you are already using the future API, even if * it is not yet released. Hence, the currently used API * version is the future one. */ -#define NM_API_VERSION \ - (((NM_MINOR_VERSION % 2) == 1) \ - ? NM_ENCODE_VERSION (NM_MAJOR_VERSION, NM_MINOR_VERSION + 1, 0 ) \ - : NM_ENCODE_VERSION (NM_MAJOR_VERSION, NM_MINOR_VERSION , ((NM_MICRO_VERSION + 1) / 2) * 2)) +#define NM_API_VERSION \ + (((NM_MINOR_VERSION % 2) == 1) \ + ? NM_ENCODE_VERSION(NM_MAJOR_VERSION, NM_MINOR_VERSION + 1, 0) \ + : NM_ENCODE_VERSION(NM_MAJOR_VERSION, NM_MINOR_VERSION, NM_MICRO_VERSION + 1)) /* deprecated. */ #define NM_VERSION_CUR_STABLE NM_API_VERSION diff --git a/src/libnm-core-public/nm-version.h b/src/libnm-core-public/nm-version.h index f8f49df2..26ede654 100644 --- a/src/libnm-core-public/nm-version.h +++ b/src/libnm-core-public/nm-version.h @@ -445,6 +445,12 @@ #define NM_AVAILABLE_IN_1_54_2 #endif +#if NM_VERSION_MAX_ALLOWED < NM_VERSION_1_54_3 +#define NM_AVAILABLE_IN_1_54_3 G_UNAVAILABLE(1, 54.3) +#else +#define NM_AVAILABLE_IN_1_54_3 +#endif + /* * Synchronous API for calling D-Bus in libnm is deprecated. See * https://networkmanager.dev/docs/libnm/latest/usage.html#sync-api diff --git a/src/libnm-core-public/nm-vpn-plugin-info.h b/src/libnm-core-public/nm-vpn-plugin-info.h index c045daa0..a6396862 100644 --- a/src/libnm-core-public/nm-vpn-plugin-info.h +++ b/src/libnm-core-public/nm-vpn-plugin-info.h @@ -64,6 +64,8 @@ NM_AVAILABLE_IN_1_4 gboolean nm_vpn_plugin_info_supports_hints(NMVpnPluginInfo *self); NM_AVAILABLE_IN_1_42 gboolean nm_vpn_plugin_info_supports_multiple(NMVpnPluginInfo *self); +NM_AVAILABLE_IN_1_54_3 +gboolean nm_vpn_plugin_info_supports_safe_private_file_access(NMVpnPluginInfo *self); NM_AVAILABLE_IN_1_4 const char *const *nm_vpn_plugin_info_get_aliases(NMVpnPluginInfo *self); NM_AVAILABLE_IN_1_2 diff --git a/src/libnm-std-aux/nm-std-utils.c b/src/libnm-std-aux/nm-std-utils.c index 8bc109f2..ef0e445d 100644 --- a/src/libnm-std-aux/nm-std-utils.c +++ b/src/libnm-std-aux/nm-std-utils.c @@ -4,10 +4,14 @@ #include "nm-std-utils.h" -#include #include +#include +#include #include #include +#include +#include +#include /*****************************************************************************/ @@ -95,6 +99,114 @@ out_huge: /*****************************************************************************/ +bool +nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len) +{ + struct passwd *pwentry; + int errsv; + char error[1024]; + + errno = 0; + pwentry = getpwnam(user); + if (!pwentry) { + errsv = errno; + if (errsv == 0) { + snprintf(errbuf, errbuf_len, "user not found"); + } else { + snprintf(errbuf, + errbuf_len, + "error getting user entry: %d (%s)\n", + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + } + return false; + } + + if (setgid(pwentry->pw_gid) != 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "failed to change group to %u: %d (%s)\n", + pwentry->pw_gid, + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + return false; + } + + if (initgroups(user, pwentry->pw_gid) != 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "failed to reset supplementary group list to %u: %d (%s)\n", + pwentry->pw_gid, + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + return false; + } + + if (setuid(pwentry->pw_uid) != 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "failed to change user to %u: %d (%s)\n", + pwentry->pw_uid, + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + return false; + } + + return true; +} + +/*****************************************************************************/ + +bool +nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len) +{ + nm_auto_close int fd = -1; + char buffer[4096]; + char error[1024]; + ssize_t bytes_read; + int errsv; + + fd = open(filename, O_RDONLY); + if (fd == -1) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "error opening the file: %d (%s)", + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + return false; + } + + while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) { + if (fwrite(buffer, 1, bytes_read, stdout) != (size_t) bytes_read) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "error writing to stdout: %d (%s)", + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + return false; + } + } + + if (bytes_read < 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "error reading the file: %d (%s)", + errsv, + _nm_strerror_r(errsv, error, sizeof(error))); + return false; + } + + return true; +} + +/*****************************************************************************/ + /** * _nm_strerror_r: * @errsv: the errno passed to strerror_r() diff --git a/src/libnm-std-aux/nm-std-utils.h b/src/libnm-std-aux/nm-std-utils.h index 015444c9..04c61c30 100644 --- a/src/libnm-std-aux/nm-std-utils.h +++ b/src/libnm-std-aux/nm-std-utils.h @@ -37,4 +37,8 @@ size_t nm_utils_get_next_realloc_size(bool true_realloc, size_t requested); const char *_nm_strerror_r(int errsv, char *buf, size_t buf_size); +bool nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_size); + +bool nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len); + #endif /* __NM_STD_UTILS_H__ */ diff --git a/src/meson.build b/src/meson.build index 1a031982..0cf93bfd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -103,8 +103,7 @@ if enable_nmtui endif subdir('nmcli') subdir('nm-dispatcher') -subdir('nm-priv-helper') -subdir('nm-daemon-helper') +subdir('nm-helpers') subdir('nm-online') if enable_nmtui subdir('nmtui') diff --git a/src/nm-daemon-helper/README.md b/src/nm-daemon-helper/README.md deleted file mode 100644 index 695f5335..00000000 --- a/src/nm-daemon-helper/README.md +++ /dev/null @@ -1,11 +0,0 @@ -nm-daemon-helper -================ - -A internal helper application that is spawned by NetworkManager -to perform certain actions. - -Currently all it does is doing a reverse DNS lookup, which -cannot be done by NetworkManager because the operation requires -to reconfigure the libc resolver (which is a process-wide operation). - -This is not directly useful to the user. diff --git a/src/nm-daemon-helper/meson.build b/src/nm-daemon-helper/meson.build deleted file mode 100644 index da0d6571..00000000 --- a/src/nm-daemon-helper/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -executable( - 'nm-daemon-helper', - 'nm-daemon-helper.c', - include_directories : [ - src_inc, - top_inc, - ], - link_with: [ - libnm_std_aux, - ], - link_args: ldflags_linker_script_binary, - link_depends: linker_script_binary, - install: true, - install_dir: nm_libexecdir, -) diff --git a/src/nm-daemon-helper/nm-daemon-helper.c b/src/nm-daemon-helper/nm-daemon-helper.c deleted file mode 100644 index 32be93a4..00000000 --- a/src/nm-daemon-helper/nm-daemon-helper.c +++ /dev/null @@ -1,155 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ - -/* Copyright (C) 2021 Red Hat, Inc. */ - -#include "libnm-std-aux/nm-default-std.h" - -#include -#include -#include -#include -#if defined(__GLIBC__) -#include -#endif -#include - -enum { - RETURN_SUCCESS = 0, - RETURN_INVALID_CMD = 1, - RETURN_INVALID_ARGS = 2, - RETURN_ERROR = 3, -}; - -static char * -read_arg(void) -{ - nm_auto_free char *arg = NULL; - size_t len = 0; - - if (getdelim(&arg, &len, '\0', stdin) < 0) - return NULL; - - return nm_steal_pointer(&arg); -} - -static int -more_args(void) -{ - nm_auto_free char *arg = NULL; - - arg = read_arg(); - - return !!arg; -} - -static int -cmd_version(void) -{ - if (more_args()) - return RETURN_INVALID_ARGS; - - printf("1"); - return RETURN_SUCCESS; -} - -static int -cmd_resolve_address(void) -{ - nm_auto_free char *address = NULL; - nm_auto_free char *services = NULL; - union { - struct sockaddr_in in; - struct sockaddr_in6 in6; - } sockaddr; - socklen_t sockaddr_size; - char name[NI_MAXHOST]; - char *saveptr = NULL; - char *service; - char *str; - int ret; - - address = read_arg(); - if (!address) - return RETURN_INVALID_ARGS; - - services = read_arg(); - if (!services) { - /* Called by an old NM version which doesn't support the 'services' - * argument. Use both services. */ - services = strdup("dns,files"); - } - - memset(&sockaddr, 0, sizeof(sockaddr)); - - if (inet_pton(AF_INET, address, &sockaddr.in.sin_addr) == 1) { - sockaddr.in.sin_family = AF_INET; - sockaddr_size = sizeof(struct sockaddr_in); - } else if (inet_pton(AF_INET6, address, &sockaddr.in6.sin6_addr) == 1) { - sockaddr.in6.sin6_family = AF_INET6; - sockaddr_size = sizeof(struct sockaddr_in6); - } else - return RETURN_INVALID_ARGS; - - for (str = services; (service = strtok_r(str, ",", &saveptr)); str = NULL) { - if (!NM_IN_STRSET(service, "dns", "files")) { - fprintf(stderr, "Unsupported resolver service '%s'\n", service); - continue; - } - -#if defined(__GLIBC__) - __nss_configure_lookup("hosts", service); -#endif - - ret = getnameinfo((struct sockaddr *) &sockaddr, - sockaddr_size, - name, - sizeof(name), - NULL, - 0, - NI_NAMEREQD); - - if (ret == 0) { - printf("%s", name); - return RETURN_SUCCESS; - } else if (ret == EAI_SYSTEM) { - char buf[1024]; - int errsv = errno; - - fprintf(stderr, - "getnameinfo() via service '%s' failed: %d (%s), system error: %d (%s)\n", - service, - ret, - gai_strerror(ret), - errsv, - _nm_strerror_r(errsv, buf, sizeof(buf))); - } else { - fprintf(stderr, - "getnameinfo() via service '%s' failed: %d (%s)\n", - service, - ret, - gai_strerror(ret)); - } -#if !defined(__GLIBC__) - break; -#endif - } - - return RETURN_ERROR; -} - -int -main(int argc, char **argv) -{ - nm_auto_free char *cmd = NULL; - - cmd = read_arg(); - if (!cmd) - return RETURN_INVALID_CMD; - - if (nm_streq(cmd, "version")) - return cmd_version(); - if (nm_streq(cmd, "resolve-address")) - return cmd_resolve_address(); - - return RETURN_INVALID_CMD; -} diff --git a/src/nm-helpers/README.md b/src/nm-helpers/README.md new file mode 100644 index 00000000..66a94292 --- /dev/null +++ b/src/nm-helpers/README.md @@ -0,0 +1,51 @@ +nm-helpers +========== + +This directory contains stand-alone helper programs used by various +components. + +nm-daemon-helper +---------------- + +A internal helper application that is spawned by NetworkManager to +perform certain actions which can't be done in the daemon. + +Currently it's used to do a reverse DNS lookup after reconfiguring the +libc resolver (which is a process-wide operation), and to read files +on behalf of unprivileged users (which requires a seteuid that affects +all the threads of the process). + +This is not directly useful to the user. + +nm-libnm-helper +--------------- + +A internal helper application that is spawned by libnm to perform +certain actions without impacting the calling process. + +This is not directly useful to the user. + +nm-priv-helper +-------------- + +This is a D-Bus activatable, exit-on-idle service, which +provides an internal API to NetworkManager daemon. + +This has no purpose for the user, it is an implementation detail +of the daemon. + +The purpose is that `nm-priv-helper` can execute certain +privileged operations which NetworkManager process is not +allowed to. We want to sandbox NetworkManager as much as +possible, and nm-priv-helper provides a controlled way to +perform some very specific operations. + +As such, nm-priv-helper should still be sandboxed too to only +being able to execute the operations that are necessary for +NetworkManager. + +nm-priv-helper will reject all D-Bus requests that are not +originating from the current name owner of +"org.freedesktop.NetworkManager". That is, it is supposed to +only reply to NetworkManager daemon and as such is not useful to +the user directly. diff --git a/src/nm-helpers/meson.build b/src/nm-helpers/meson.build new file mode 100644 index 00000000..7c148079 --- /dev/null +++ b/src/nm-helpers/meson.build @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# nm-daemon-helper + +executable( + 'nm-daemon-helper', + 'nm-daemon-helper.c', + include_directories : [ + src_inc, + top_inc, + ], + link_with: [ + libnm_std_aux, + ], + link_args: ldflags_linker_script_binary, + link_depends: linker_script_binary, + install: true, + install_dir: nm_libexecdir, +) + +# nm-libnm-helper + +executable( + 'nm-libnm-helper', + ['nm-libnm-helper.c'], + include_directories : [ + src_inc, + top_inc, + ], + dependencies: [ + glib_dep, + ], + link_with: [ + libnm_glib_aux, + libnm_std_aux, + ], + install: true, + install_dir: nm_libexecdir, +) + +# nm-priv-helper + +configure_file( + input: 'org.freedesktop.nm_priv_helper.service.in', + output: '@BASENAME@', + install_dir: dbus_system_bus_services_dir, + configuration: data_conf, +) + +install_data( + 'nm-priv-helper.conf', + install_dir: dbus_conf_dir, +) + +executable( + 'nm-priv-helper', + 'nm-priv-helper.c', + include_directories : [ + src_inc, + top_inc, + ], + dependencies: [ + glib_dep, + ], + link_with: [ + libnm_base, + 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-helpers/nm-daemon-helper.c b/src/nm-helpers/nm-daemon-helper.c new file mode 100644 index 00000000..759993a4 --- /dev/null +++ b/src/nm-helpers/nm-daemon-helper.c @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* Copyright (C) 2021 Red Hat, Inc. */ + +#include "libnm-std-aux/nm-default-std.h" + +#include +#include +#include +#include +#if defined(__GLIBC__) +#include +#endif +#include + +enum { + RETURN_SUCCESS = 0, + RETURN_INVALID_CMD = 1, + RETURN_INVALID_ARGS = 2, + RETURN_ERROR = 3, +}; + +static char * +read_arg(void) +{ + nm_auto_free char *arg = NULL; + size_t len = 0; + + if (getdelim(&arg, &len, '\0', stdin) < 0) + return NULL; + + return nm_steal_pointer(&arg); +} + +static int +more_args(void) +{ + nm_auto_free char *arg = NULL; + + arg = read_arg(); + + return !!arg; +} + +static int +cmd_version(void) +{ + if (more_args()) + return RETURN_INVALID_ARGS; + + printf("1"); + return RETURN_SUCCESS; +} + +static int +cmd_resolve_address(void) +{ + nm_auto_free char *address = NULL; + nm_auto_free char *services = NULL; + union { + struct sockaddr_in in; + struct sockaddr_in6 in6; + } sockaddr; + socklen_t sockaddr_size; + char name[NI_MAXHOST]; + char *saveptr = NULL; + char *service; + char *str; + int ret; + + address = read_arg(); + if (!address) + return RETURN_INVALID_ARGS; + + services = read_arg(); + if (!services) { + /* Called by an old NM version which doesn't support the 'services' + * argument. Use both services. */ + services = strdup("dns,files"); + } + + memset(&sockaddr, 0, sizeof(sockaddr)); + + if (inet_pton(AF_INET, address, &sockaddr.in.sin_addr) == 1) { + sockaddr.in.sin_family = AF_INET; + sockaddr_size = sizeof(struct sockaddr_in); + } else if (inet_pton(AF_INET6, address, &sockaddr.in6.sin6_addr) == 1) { + sockaddr.in6.sin6_family = AF_INET6; + sockaddr_size = sizeof(struct sockaddr_in6); + } else + return RETURN_INVALID_ARGS; + + for (str = services; (service = strtok_r(str, ",", &saveptr)); str = NULL) { + if (!NM_IN_STRSET(service, "dns", "files")) { + fprintf(stderr, "Unsupported resolver service '%s'\n", service); + continue; + } + +#if defined(__GLIBC__) + __nss_configure_lookup("hosts", service); +#endif + + ret = getnameinfo((struct sockaddr *) &sockaddr, + sockaddr_size, + name, + sizeof(name), + NULL, + 0, + NI_NAMEREQD); + + if (ret == 0) { + printf("%s", name); + return RETURN_SUCCESS; + } else if (ret == EAI_SYSTEM) { + char buf[1024]; + int errsv = errno; + + fprintf(stderr, + "getnameinfo() via service '%s' failed: %d (%s), system error: %d (%s)\n", + service, + ret, + gai_strerror(ret), + errsv, + _nm_strerror_r(errsv, buf, sizeof(buf))); + } else { + fprintf(stderr, + "getnameinfo() via service '%s' failed: %d (%s)\n", + service, + ret, + gai_strerror(ret)); + } +#if !defined(__GLIBC__) + break; +#endif + } + + return RETURN_ERROR; +} + +static int +cmd_read_file_as_user(void) +{ + nm_auto_free char *user = NULL; + nm_auto_free char *filename = NULL; + char error[1024]; + + user = read_arg(); + if (!user) + return RETURN_INVALID_ARGS; + + filename = read_arg(); + if (!filename) + return RETURN_INVALID_ARGS; + + if (more_args()) + return RETURN_INVALID_ARGS; + + if (!nm_utils_set_effective_user(user, error, sizeof(error))) { + fprintf(stderr, "Failed to set effective user '%s': %s", user, error); + return RETURN_ERROR; + } + + if (!nm_utils_read_file_to_stdout(filename, error, sizeof(error))) { + fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error); + return RETURN_ERROR; + } + + return RETURN_SUCCESS; +} + +int +main(int argc, char **argv) +{ + nm_auto_free char *cmd = NULL; + + cmd = read_arg(); + if (!cmd) + return RETURN_INVALID_CMD; + + if (nm_streq(cmd, "version")) + return cmd_version(); + if (nm_streq(cmd, "resolve-address")) + return cmd_resolve_address(); + if (nm_streq(cmd, "read-file-as-user")) + return cmd_read_file_as_user(); + + return RETURN_INVALID_CMD; +} diff --git a/src/nm-helpers/nm-libnm-helper.c b/src/nm-helpers/nm-libnm-helper.c new file mode 100644 index 00000000..bd0ba67d --- /dev/null +++ b/src/nm-helpers/nm-libnm-helper.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-std-aux/nm-default-std.h" + +#include + +enum { + RETURN_SUCCESS = 0, + RETURN_INVALID_CMD = 1, + RETURN_INVALID_ARGS = 2, + RETURN_ERROR = 3, +}; + +static int +read_file_as_user(const char *filename, const char *user) +{ + char error[1024]; + + if (!nm_utils_set_effective_user(user, error, sizeof(error))) { + fprintf(stderr, "Failed to set effective user '%s': %s", user, error); + return RETURN_ERROR; + } + + if (!nm_utils_read_file_to_stdout(filename, error, sizeof(error))) { + fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error); + return RETURN_ERROR; + } + + return RETURN_SUCCESS; +} + +int +main(int argc, char **argv) +{ + if (argc <= 1) + return RETURN_INVALID_CMD; + + if (nm_streq(argv[1], "read-file-as-user")) { + if (argc != 4) + return RETURN_INVALID_ARGS; + return read_file_as_user(argv[2], argv[3]); + } + + return RETURN_INVALID_CMD; +} \ No newline at end of file diff --git a/src/nm-helpers/nm-priv-helper.c b/src/nm-helpers/nm-priv-helper.c new file mode 100644 index 00000000..2fe9bcf5 --- /dev/null +++ b/src/nm-helpers/nm-priv-helper.c @@ -0,0 +1,695 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-glib-aux/nm-default-glib-i18n-prog.h" + +#include + +#include "c-list/src/c-list.h" +#include "libnm-base/nm-priv-helper-utils.h" +#include "libnm-glib-aux/nm-dbus-aux.h" +#include "libnm-glib-aux/nm-io-utils.h" +#include "libnm-glib-aux/nm-logging-base.h" +#include "libnm-glib-aux/nm-time-utils.h" + +/* nm-priv-helper doesn't link with libnm-core nor libnm-base, but these + * headers can be used independently. */ +#include "libnm-core-public/nm-dbus-interface.h" + +/*****************************************************************************/ + +#define IDLE_TIMEOUT_MSEC 2000 +#define IDLE_TIMEOUT_INFINITY G_MAXINT32 + +/*****************************************************************************/ + +/* Serves only the purpose to mark environment variables that are honored by + * the application. You can search for this macro, and find what options are supported. */ +#define _ENV(var) ("" var "") + +/*****************************************************************************/ + +typedef struct _GlobalData GlobalData; + +typedef struct { + CList pending_jobs_lst; + GlobalData *gl; +} PendingJobData; + +struct _GlobalData { + GDBusConnection *dbus_connection; + GCancellable *quit_cancellable; + + GSource *source_sigterm; + + CList pending_jobs_lst_head; + + GSource *source_idle_timeout; + + char *name_owner; + + gint64 start_timestamp_msec; + + guint name_owner_changed_id; + guint service_regist_id; + + guint32 timeout_msec; + + bool name_owner_initialized; + + /* This is controlled by $NM_PRIV_HELPER_NO_AUTH_FOR_TESTING. It disables authentication + * of the request, so it is ONLY for testing. */ + bool no_auth_for_testing; + + bool name_requested; + bool reject_new_requests; + + bool shutdown_quitting; + bool shutdown_timeout; +}; + +/*****************************************************************************/ + +static void _pending_job_register_object(GlobalData *gl, GObject *obj); + +/*****************************************************************************/ + +#define _nm_log(level, ...) _nm_log_simple_printf((level), __VA_ARGS__) + +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + if (_nm_logging_enabled(_level)) { \ + _nm_log(_level, __VA_ARGS__); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static void +_handle_ping(GlobalData *gl, GDBusMethodInvocation *invocation, const char *arg) +{ + gs_free char *msg = NULL; + gint64 running_msec; + + running_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME) - gl->start_timestamp_msec; + + msg = g_strdup_printf("pid=%lu, unique-name=%s, nm-name-owner=%s, since=%" G_GINT64_FORMAT + ".%03d%s, pong=%s", + (unsigned long) getpid(), + g_dbus_connection_get_unique_name(gl->dbus_connection), + gl->name_owner ?: "(none)", + (gint64) (running_msec / 1000), + (int) (running_msec % 1000), + gl->no_auth_for_testing ? ", no-auth-for-testing" : "", + arg); + g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", msg)); +} + +static void +_handle_get_fd(GlobalData *gl, GDBusMethodInvocation *invocation, guint32 fd_type) +{ + nm_auto_close int fd = -1; + gs_unref_object GUnixFDList *fd_list = NULL; + gs_free_error GError *error = NULL; + + if (fd_type != (NMPrivHelperGetFDType) fd_type) + fd_type = NM_PRIV_HELPER_GET_FD_TYPE_NONE; + + fd = nm_priv_helper_utils_open_fd(fd_type, &error); + if (fd < 0) { + g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error)); + return; + } + + fd_list = g_unix_fd_list_new_from_array(&fd, 1); + nm_steal_fd(&fd); + + g_dbus_method_invocation_return_value_with_unix_fd_list(invocation, NULL, fd_list); +} + +/*****************************************************************************/ + +static gboolean +_signal_callback_term(gpointer user_data) +{ + GlobalData *gl = user_data; + + _LOGD("sigterm received (%s)", + c_list_is_empty(&gl->pending_jobs_lst_head) ? "quit mainloop" : "cancel operations"); + + gl->shutdown_quitting = TRUE; + g_cancellable_cancel(gl->quit_cancellable); + return G_SOURCE_CONTINUE; +} + +/*****************************************************************************/ + +static GDBusConnection * +_bus_get(GCancellable *cancellable, int *out_exit_code) +{ + gs_free_error GError *error = NULL; + gs_unref_object GDBusConnection *dbus_connection = NULL; + + dbus_connection = nm_g_bus_get_blocking(cancellable, &error); + + if (!dbus_connection) { + gboolean was_cancelled = nm_utils_error_is_cancelled(error); + + NM_SET_OUT(out_exit_code, was_cancelled ? EXIT_SUCCESS : EXIT_FAILURE); + if (!was_cancelled) + _LOGE("dbus: failure to get D-Bus connection: %s", error->message); + return NULL; + } + + /* On bus-disconnect, GDBus will raise(SIGTERM), which we handle like a + * regular request to quit. */ + g_dbus_connection_set_exit_on_close(dbus_connection, TRUE); + + _LOGD("dbus: unique name: %s", g_dbus_connection_get_unique_name(dbus_connection)); + + return g_steal_pointer(&dbus_connection); +} + +/*****************************************************************************/ + +static void +_name_owner_changed_cb(GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GlobalData *gl = user_data; + const char *new_owner; + + if (!gl->name_owner_initialized) + return; + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) + return; + + g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); + new_owner = nm_str_not_empty(new_owner); + + _LOGD("%s name-owner changed: %s -> %s", + NM_DBUS_SERVICE, + gl->name_owner ?: "(null)", + new_owner ?: "(null)"); + + nm_strdup_reset(&gl->name_owner, new_owner); +} + +typedef struct { + GlobalData *gl; + char **p_name_owner; + gboolean is_cancelled; +} BusFindNMNameOwnerData; + +static void +_bus_find_nm_nameowner_cb(const char *name_owner, GError *error, gpointer user_data) +{ + BusFindNMNameOwnerData *data = user_data; + + *data->p_name_owner = nm_strdup_not_empty(name_owner); + data->is_cancelled = nm_utils_error_is_cancelled(error); + data->gl->name_owner_initialized = TRUE; +} + +static gboolean +_bus_find_nm_nameowner(GlobalData *gl) +{ + BusFindNMNameOwnerData data; + guint name_owner_changed_id; + gs_free char *name_owner = NULL; + + name_owner_changed_id = + nm_dbus_connection_signal_subscribe_name_owner_changed(gl->dbus_connection, + NM_DBUS_SERVICE, + _name_owner_changed_cb, + gl, + NULL); + + data = (BusFindNMNameOwnerData) { + .gl = gl, + .is_cancelled = FALSE, + .p_name_owner = &name_owner, + }; + nm_dbus_connection_call_get_name_owner(gl->dbus_connection, + NM_DBUS_SERVICE, + 10000, + gl->quit_cancellable, + _bus_find_nm_nameowner_cb, + &data); + while (!gl->name_owner_initialized) + g_main_context_iteration(NULL, TRUE); + + if (data.is_cancelled) { + g_dbus_connection_signal_unsubscribe(gl->dbus_connection, name_owner_changed_id); + return FALSE; + } + + gl->name_owner_changed_id = name_owner_changed_id; + gl->name_owner = g_steal_pointer(&name_owner); + return TRUE; +} + +/*****************************************************************************/ + +static void +_bus_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) +{ + GlobalData *gl = user_data; + const char *arg_s; + guint32 arg_u; + + nm_assert(nm_streq(object_path, NM_PRIV_HELPER_DBUS_OBJECT_PATH)); + nm_assert(nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME)); + + if (!gl->no_auth_for_testing && !nm_streq0(sender, gl->name_owner)) { + _LOGT("dbus: request sender=%s, %s%s, ACCESS DENIED", + sender, + method_name, + g_variant_get_type_string(parameters)); + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Access denied"); + return; + } + + if (gl->reject_new_requests) { + /* after the name was released, we must not accept new requests. This new + * request was probably targeted against the unique-name. But we already + * gave up the well-known name. If we'd accept new request now, they would + * keep the service running indefinitely (and thus preventing the service + * to restart and serve the well-known name. */ + _LOGT("dbus: request sender=%s, %s%s, SERVER SHUTTING DOWN", + sender, + method_name, + g_variant_get_type_string(parameters)); + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_NO_SERVER, + "Server is exiting"); + return; + } + + _LOGT("dbus: request sender=%s, %s%s", + sender, + method_name, + g_variant_get_type_string(parameters)); + + if (!nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME)) + goto out_unknown_method; + + if (nm_streq(method_name, "GetFD")) { + g_variant_get(parameters, "(u)", &arg_u); + _handle_get_fd(gl, invocation, arg_u); + return; + } + if (nm_streq(method_name, "Ping")) { + g_variant_get(parameters, "(&s)", &arg_s); + _handle_ping(gl, invocation, arg_s); + return; + } + +out_unknown_method: + 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_PRIV_HELPER_DBUS_IFACE_NAME, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_GDBUS_METHOD_INFO( + "Ping", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), ), + NM_DEFINE_GDBUS_METHOD_INFO("GetFD", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("fd_type", "u"), ), ), ), ); + +static gboolean +_bus_register_service(GlobalData *gl) +{ + static const GDBusInterfaceVTable interface_vtable = { + .method_call = _bus_method_call, + }; + gs_free_error GError *error = NULL; + NMDBusConnectionCallBlockingData data = { + .result = NULL, + }; + gs_unref_variant GVariant *ret = NULL; + guint32 ret_val; + + gl->service_regist_id = + g_dbus_connection_register_object(gl->dbus_connection, + NM_PRIV_HELPER_DBUS_OBJECT_PATH, + interface_info, + NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable), + gl, + NULL, + &error); + if (gl->service_regist_id == 0) { + _LOGE("dbus: error registering object %s: %s", + NM_PRIV_HELPER_DBUS_OBJECT_PATH, + error->message); + return FALSE; + } + + _LOGD("dbus: object %s registered", NM_PRIV_HELPER_DBUS_OBJECT_PATH); + + /* regardless whether the request is successful, after we start calling + * RequestName, we remember that we need to ReleaseName it. */ + gl->name_requested = TRUE; + + nm_dbus_connection_call_request_name(gl->dbus_connection, + NM_PRIV_HELPER_DBUS_BUS_NAME, + DBUS_NAME_FLAG_ALLOW_REPLACEMENT + | DBUS_NAME_FLAG_REPLACE_EXISTING, + 10000, + gl->quit_cancellable, + nm_dbus_connection_call_blocking_callback, + &data); + + /* Note that with D-Bus activation, the first request will already hit us before RequestName + * completes. So when we start iterating the main context, the first request may already come + * in. */ + + ret = nm_dbus_connection_call_blocking(&data, &error); + + if (nm_utils_error_is_cancelled(error)) + return FALSE; + + if (error) { + _LOGE("d-bus: failed to request name %s: %s", NM_PRIV_HELPER_DBUS_BUS_NAME, error->message); + return FALSE; + } + + g_variant_get(ret, "(u)", &ret_val); + + if (ret_val != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + _LOGW("dbus: request name for %s failed to take name (response %u)", + NM_PRIV_HELPER_DBUS_BUS_NAME, + ret_val); + return FALSE; + } + + _LOGD("dbus: request name for %s succeeded", NM_PRIV_HELPER_DBUS_BUS_NAME); + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +_idle_timeout_cb(gpointer user_data) +{ + GlobalData *gl = user_data; + + _LOGT("idle-timeout: expired"); + nm_clear_g_source_inst(&gl->source_idle_timeout); + gl->shutdown_timeout = TRUE; + return G_SOURCE_CONTINUE; +} + +static void +_idle_timeout_restart(GlobalData *gl) +{ + nm_clear_g_source_inst(&gl->source_idle_timeout); + + if (gl->shutdown_quitting) + return; + + if (!c_list_is_empty(&gl->pending_jobs_lst_head)) + return; + + if (gl->timeout_msec == IDLE_TIMEOUT_INFINITY) + return; + + nm_assert(gl->timeout_msec < G_MAXINT32); + G_STATIC_ASSERT_EXPR(G_MAXINT32 < G_MAXUINT); + + _LOGT("idle-timeout: start (%u msec)", gl->timeout_msec); + gl->source_idle_timeout = nm_g_timeout_add_source(gl->timeout_msec, _idle_timeout_cb, gl); +} + +/*****************************************************************************/ + +static gboolean +_pending_job_register_object_release_on_idle_cb(gpointer data) +{ + PendingJobData *idle_data = data; + GlobalData *gl = idle_data->gl; + + c_list_unlink_stale(&idle_data->pending_jobs_lst); + nm_g_slice_free(idle_data); + + _idle_timeout_restart(gl); + return G_SOURCE_REMOVE; +} + +static void +_pending_job_register_object_weak_cb(gpointer data, GObject *where_the_object_was) +{ + /* The object might be destroyed on another thread. We need + * to sync with the main GMainContext by scheduling an idle action + * there. */ + nm_g_idle_add(_pending_job_register_object_release_on_idle_cb, data); +} + +static void +_pending_job_register_object(GlobalData *gl, GObject *obj) +{ + PendingJobData *idle_data; + + /* if we just hit the timeout, we can ignore it. */ + gl->shutdown_timeout = FALSE; + + if (nm_clear_g_source_inst(&gl->source_idle_timeout)) + _LOGT("idle-timeout: suspend timeout for pending request"); + + idle_data = g_slice_new(PendingJobData); + + idle_data->gl = gl; + c_list_link_tail(&gl->pending_jobs_lst_head, &idle_data->pending_jobs_lst); + + g_object_weak_ref(obj, _pending_job_register_object_weak_cb, idle_data); +} + +/*****************************************************************************/ + +static void +_bus_release_name_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + _nm_unused gs_unref_object GObject *keep_alive_object = NULL; + GlobalData *gl; + + nm_utils_user_data_unpack(user_data, &gl, &keep_alive_object); + + gl->reject_new_requests = TRUE; + g_main_context_wakeup(NULL); +} + +static gboolean +_bus_release_name(GlobalData *gl) +{ + gs_unref_object GObject *keep_alive_object = NULL; + int r; + + /* We already requested a name. To exit-on-idle without race, we need to dance. + * See https://lists.freedesktop.org/archives/dbus/2015-May/016671.html . */ + + if (!gl->name_requested) + return FALSE; + + gl->name_requested = FALSE; + gl->shutdown_quitting = TRUE; + + _LOGT("shutdown: release-name"); + + keep_alive_object = g_object_new(G_TYPE_OBJECT, NULL); + + /* we use the _pending_job_register_object() mechanism to make the loop busy during + * shutdown. */ + _pending_job_register_object(gl, keep_alive_object); + + r = nm_sd_notify("STOPPING=1"); + if (r < 0) + _LOGW("shutdown: sd_notifiy(STOPPING=1) failed: %s", nm_strerror_native(-r)); + else + _LOGT("shutdown: sd_notifiy(STOPPING=1) succeeded"); + + g_dbus_connection_call(gl->dbus_connection, + DBUS_SERVICE_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS, + "ReleaseName", + g_variant_new("(s)", NM_PRIV_HELPER_DBUS_BUS_NAME), + G_VARIANT_TYPE("(u)"), + G_DBUS_CALL_FLAGS_NONE, + 10000, + NULL, + _bus_release_name_cb, + nm_utils_user_data_pack(gl, g_steal_pointer(&keep_alive_object))); + return TRUE; +} + +/*****************************************************************************/ + +static void +_initial_setup(GlobalData *gl) +{ + gl->no_auth_for_testing = + _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_NO_AUTH_FOR_TESTING")), + 0, + 0, + 1, + 0); + gl->timeout_msec = + _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_IDLE_TIMEOUT_MSEC")), + 0, + 0, + G_MAXINT32, + IDLE_TIMEOUT_MSEC); + + gl->quit_cancellable = g_cancellable_new(); + + signal(SIGPIPE, SIG_IGN); + gl->source_sigterm = nm_g_unix_signal_add_source(SIGTERM, _signal_callback_term, gl); +} + +/*****************************************************************************/ + +int +main(int argc, char **argv) +{ + GlobalData _gl = { + .quit_cancellable = NULL, + .pending_jobs_lst_head = C_LIST_INIT(_gl.pending_jobs_lst_head), + }; + GlobalData *const gl = &_gl; + int exit_code; + int r = 0; + + _nm_logging_enabled_init(g_getenv(_ENV("NM_PRIV_HELPER_LOG"))); + + gl->start_timestamp_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME); + + _LOGD("starting nm-priv-helper (%s)", NM_DIST_VERSION); + + _initial_setup(gl); + + if (gl->no_auth_for_testing) { + _LOGW("WARNING: running in debug mode without authentication " + "(NM_PRIV_HELPER_NO_AUTH_FOR_TESTING). "); + } + + if (gl->timeout_msec != IDLE_TIMEOUT_INFINITY) + _LOGT("idle-timeout: %u msec", gl->timeout_msec); + else + _LOGT("idle-timeout: disabled"); + + gl->dbus_connection = _bus_get(gl->quit_cancellable, &r); + if (!gl->dbus_connection) { + exit_code = r; + goto done; + } + + if (!_bus_find_nm_nameowner(gl)) { + /* abort due to cancellation. That is success. */ + exit_code = EXIT_SUCCESS; + goto done; + } + _LOGD("%s name-owner: %s", NM_DBUS_SERVICE, gl->name_owner ?: "(null)"); + + _idle_timeout_restart(gl); + + exit_code = EXIT_SUCCESS; + + if (!_bus_register_service(gl)) { + /* We failed to RequestName, but due to D-Bus activation we + * might have a pending request still (on the unique name). + * Process it below. + * + * Let's fake a shutdown signal, and still process the request below. */ + if (!g_cancellable_is_cancelled(gl->quit_cancellable)) + exit_code = EXIT_FAILURE; + gl->shutdown_quitting = TRUE; + + if (gl->name_requested) { + /* We requested a name, but something went wrong. Below we will release + * the name right away. */ + } else { + /* In case we didn't even went as far to request the name. New requests + * can only come via the unique name, and as we are shutting down, they + * are rejected. */ + gl->reject_new_requests = TRUE; + } + } + + while (TRUE) { + if (gl->shutdown_quitting) + _bus_release_name(gl); + + if (!c_list_is_empty(&gl->pending_jobs_lst_head)) { + /* we must first reply to all requests. No matter what. */ + } else if (gl->shutdown_quitting || gl->shutdown_timeout) { + /* we either hit the idle timeout or received SIGTERM. Note that + * if we received an idle-timeout and the very moment afterwards + * a new request, then _bus_method_call() will clear gl->shutdown_timeout + * (via _pending_job_register_object()). */ + if (!_bus_release_name(gl)) + break; + } + + g_main_context_iteration(NULL, TRUE); + } + +done: + _LOGD("shutdown: cleanup"); + + gl->shutdown_quitting = TRUE; + g_cancellable_cancel(gl->quit_cancellable); + + nm_assert(c_list_is_empty(&gl->pending_jobs_lst_head)); + + if (gl->service_regist_id != 0) { + g_dbus_connection_unregister_object(gl->dbus_connection, + nm_steal_int(&gl->service_regist_id)); + } + if (gl->name_owner_changed_id != 0) { + g_dbus_connection_signal_unsubscribe(gl->dbus_connection, + nm_steal_int(&gl->name_owner_changed_id)); + } + + if (gl->dbus_connection) { + g_dbus_connection_flush_sync(gl->dbus_connection, NULL, NULL); + g_clear_object(&gl->dbus_connection); + } + + nm_g_main_context_iterate_ready(NULL); + + nm_clear_g_free(&gl->name_owner); + + nm_clear_g_source_inst(&gl->source_sigterm); + nm_clear_g_source_inst(&gl->source_idle_timeout); + g_clear_object(&gl->quit_cancellable); + + _LOGD("exit (%d)", exit_code); + return exit_code; +} diff --git a/src/nm-helpers/nm-priv-helper.conf b/src/nm-helpers/nm-priv-helper.conf new file mode 100644 index 00000000..c0d04617 --- /dev/null +++ b/src/nm-helpers/nm-priv-helper.conf @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in new file mode 100644 index 00000000..9e86a635 --- /dev/null +++ b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.freedesktop.nm_priv_helper +Exec=@libexecdir@/nm-priv-helper +User=root +SystemdService=nm-priv-helper.service diff --git a/src/nm-priv-helper/README.md b/src/nm-priv-helper/README.md deleted file mode 100644 index 576da7a7..00000000 --- a/src/nm-priv-helper/README.md +++ /dev/null @@ -1,24 +0,0 @@ -nm-priv-helper -============== - -This is a D-Bus activatable, exit-on-idle service, which -provides an internal API to NetworkManager daemon. - -This has no purpose for the user, it is an implementation detail -of the daemon. - -The purpose is that `nm-priv-helper` can execute certain -privileged operations which NetworkManager process is not -allowed to. We want to sandbox NetworkManager as much as -possible, and nm-priv-helper provides a controlled way to -perform some very specific operations. - -As such, nm-priv-helper should still be sandboxed too to only -being able to execute the operations that are necessary for -NetworkManager. - -nm-priv-helper will reject all D-Bus requests that are not -originating from the current name owner of -"org.freedesktop.NetworkManager". That is, it is supposed to -only reply to NetworkManager daemon and as such is not useful to -the user directly. diff --git a/src/nm-priv-helper/meson.build b/src/nm-priv-helper/meson.build deleted file mode 100644 index 6141e0e2..00000000 --- a/src/nm-priv-helper/meson.build +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -configure_file( - input: 'org.freedesktop.nm_priv_helper.service.in', - output: '@BASENAME@', - install_dir: dbus_system_bus_services_dir, - configuration: data_conf, -) - -install_data( - 'nm-priv-helper.conf', - install_dir: dbus_conf_dir, -) - -executable( - 'nm-priv-helper', - 'nm-priv-helper.c', - include_directories : [ - src_inc, - top_inc, - ], - dependencies: [ - glib_dep, - ], - link_with: [ - libnm_base, - 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-priv-helper/nm-priv-helper.c b/src/nm-priv-helper/nm-priv-helper.c deleted file mode 100644 index 2fe9bcf5..00000000 --- a/src/nm-priv-helper/nm-priv-helper.c +++ /dev/null @@ -1,695 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ - -#include "libnm-glib-aux/nm-default-glib-i18n-prog.h" - -#include - -#include "c-list/src/c-list.h" -#include "libnm-base/nm-priv-helper-utils.h" -#include "libnm-glib-aux/nm-dbus-aux.h" -#include "libnm-glib-aux/nm-io-utils.h" -#include "libnm-glib-aux/nm-logging-base.h" -#include "libnm-glib-aux/nm-time-utils.h" - -/* nm-priv-helper doesn't link with libnm-core nor libnm-base, but these - * headers can be used independently. */ -#include "libnm-core-public/nm-dbus-interface.h" - -/*****************************************************************************/ - -#define IDLE_TIMEOUT_MSEC 2000 -#define IDLE_TIMEOUT_INFINITY G_MAXINT32 - -/*****************************************************************************/ - -/* Serves only the purpose to mark environment variables that are honored by - * the application. You can search for this macro, and find what options are supported. */ -#define _ENV(var) ("" var "") - -/*****************************************************************************/ - -typedef struct _GlobalData GlobalData; - -typedef struct { - CList pending_jobs_lst; - GlobalData *gl; -} PendingJobData; - -struct _GlobalData { - GDBusConnection *dbus_connection; - GCancellable *quit_cancellable; - - GSource *source_sigterm; - - CList pending_jobs_lst_head; - - GSource *source_idle_timeout; - - char *name_owner; - - gint64 start_timestamp_msec; - - guint name_owner_changed_id; - guint service_regist_id; - - guint32 timeout_msec; - - bool name_owner_initialized; - - /* This is controlled by $NM_PRIV_HELPER_NO_AUTH_FOR_TESTING. It disables authentication - * of the request, so it is ONLY for testing. */ - bool no_auth_for_testing; - - bool name_requested; - bool reject_new_requests; - - bool shutdown_quitting; - bool shutdown_timeout; -}; - -/*****************************************************************************/ - -static void _pending_job_register_object(GlobalData *gl, GObject *obj); - -/*****************************************************************************/ - -#define _nm_log(level, ...) _nm_log_simple_printf((level), __VA_ARGS__) - -#define _NMLOG(level, ...) \ - G_STMT_START \ - { \ - const NMLogLevel _level = (level); \ - \ - if (_nm_logging_enabled(_level)) { \ - _nm_log(_level, __VA_ARGS__); \ - } \ - } \ - G_STMT_END - -/*****************************************************************************/ - -static void -_handle_ping(GlobalData *gl, GDBusMethodInvocation *invocation, const char *arg) -{ - gs_free char *msg = NULL; - gint64 running_msec; - - running_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME) - gl->start_timestamp_msec; - - msg = g_strdup_printf("pid=%lu, unique-name=%s, nm-name-owner=%s, since=%" G_GINT64_FORMAT - ".%03d%s, pong=%s", - (unsigned long) getpid(), - g_dbus_connection_get_unique_name(gl->dbus_connection), - gl->name_owner ?: "(none)", - (gint64) (running_msec / 1000), - (int) (running_msec % 1000), - gl->no_auth_for_testing ? ", no-auth-for-testing" : "", - arg); - g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", msg)); -} - -static void -_handle_get_fd(GlobalData *gl, GDBusMethodInvocation *invocation, guint32 fd_type) -{ - nm_auto_close int fd = -1; - gs_unref_object GUnixFDList *fd_list = NULL; - gs_free_error GError *error = NULL; - - if (fd_type != (NMPrivHelperGetFDType) fd_type) - fd_type = NM_PRIV_HELPER_GET_FD_TYPE_NONE; - - fd = nm_priv_helper_utils_open_fd(fd_type, &error); - if (fd < 0) { - g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error)); - return; - } - - fd_list = g_unix_fd_list_new_from_array(&fd, 1); - nm_steal_fd(&fd); - - g_dbus_method_invocation_return_value_with_unix_fd_list(invocation, NULL, fd_list); -} - -/*****************************************************************************/ - -static gboolean -_signal_callback_term(gpointer user_data) -{ - GlobalData *gl = user_data; - - _LOGD("sigterm received (%s)", - c_list_is_empty(&gl->pending_jobs_lst_head) ? "quit mainloop" : "cancel operations"); - - gl->shutdown_quitting = TRUE; - g_cancellable_cancel(gl->quit_cancellable); - return G_SOURCE_CONTINUE; -} - -/*****************************************************************************/ - -static GDBusConnection * -_bus_get(GCancellable *cancellable, int *out_exit_code) -{ - gs_free_error GError *error = NULL; - gs_unref_object GDBusConnection *dbus_connection = NULL; - - dbus_connection = nm_g_bus_get_blocking(cancellable, &error); - - if (!dbus_connection) { - gboolean was_cancelled = nm_utils_error_is_cancelled(error); - - NM_SET_OUT(out_exit_code, was_cancelled ? EXIT_SUCCESS : EXIT_FAILURE); - if (!was_cancelled) - _LOGE("dbus: failure to get D-Bus connection: %s", error->message); - return NULL; - } - - /* On bus-disconnect, GDBus will raise(SIGTERM), which we handle like a - * regular request to quit. */ - g_dbus_connection_set_exit_on_close(dbus_connection, TRUE); - - _LOGD("dbus: unique name: %s", g_dbus_connection_get_unique_name(dbus_connection)); - - return g_steal_pointer(&dbus_connection); -} - -/*****************************************************************************/ - -static void -_name_owner_changed_cb(GDBusConnection *connection, - const char *sender_name, - const char *object_path, - const char *interface_name, - const char *signal_name, - GVariant *parameters, - gpointer user_data) -{ - GlobalData *gl = user_data; - const char *new_owner; - - if (!gl->name_owner_initialized) - return; - - if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) - return; - - g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); - new_owner = nm_str_not_empty(new_owner); - - _LOGD("%s name-owner changed: %s -> %s", - NM_DBUS_SERVICE, - gl->name_owner ?: "(null)", - new_owner ?: "(null)"); - - nm_strdup_reset(&gl->name_owner, new_owner); -} - -typedef struct { - GlobalData *gl; - char **p_name_owner; - gboolean is_cancelled; -} BusFindNMNameOwnerData; - -static void -_bus_find_nm_nameowner_cb(const char *name_owner, GError *error, gpointer user_data) -{ - BusFindNMNameOwnerData *data = user_data; - - *data->p_name_owner = nm_strdup_not_empty(name_owner); - data->is_cancelled = nm_utils_error_is_cancelled(error); - data->gl->name_owner_initialized = TRUE; -} - -static gboolean -_bus_find_nm_nameowner(GlobalData *gl) -{ - BusFindNMNameOwnerData data; - guint name_owner_changed_id; - gs_free char *name_owner = NULL; - - name_owner_changed_id = - nm_dbus_connection_signal_subscribe_name_owner_changed(gl->dbus_connection, - NM_DBUS_SERVICE, - _name_owner_changed_cb, - gl, - NULL); - - data = (BusFindNMNameOwnerData) { - .gl = gl, - .is_cancelled = FALSE, - .p_name_owner = &name_owner, - }; - nm_dbus_connection_call_get_name_owner(gl->dbus_connection, - NM_DBUS_SERVICE, - 10000, - gl->quit_cancellable, - _bus_find_nm_nameowner_cb, - &data); - while (!gl->name_owner_initialized) - g_main_context_iteration(NULL, TRUE); - - if (data.is_cancelled) { - g_dbus_connection_signal_unsubscribe(gl->dbus_connection, name_owner_changed_id); - return FALSE; - } - - gl->name_owner_changed_id = name_owner_changed_id; - gl->name_owner = g_steal_pointer(&name_owner); - return TRUE; -} - -/*****************************************************************************/ - -static void -_bus_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) -{ - GlobalData *gl = user_data; - const char *arg_s; - guint32 arg_u; - - nm_assert(nm_streq(object_path, NM_PRIV_HELPER_DBUS_OBJECT_PATH)); - nm_assert(nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME)); - - if (!gl->no_auth_for_testing && !nm_streq0(sender, gl->name_owner)) { - _LOGT("dbus: request sender=%s, %s%s, ACCESS DENIED", - sender, - method_name, - g_variant_get_type_string(parameters)); - g_dbus_method_invocation_return_error(invocation, - G_DBUS_ERROR, - G_DBUS_ERROR_ACCESS_DENIED, - "Access denied"); - return; - } - - if (gl->reject_new_requests) { - /* after the name was released, we must not accept new requests. This new - * request was probably targeted against the unique-name. But we already - * gave up the well-known name. If we'd accept new request now, they would - * keep the service running indefinitely (and thus preventing the service - * to restart and serve the well-known name. */ - _LOGT("dbus: request sender=%s, %s%s, SERVER SHUTTING DOWN", - sender, - method_name, - g_variant_get_type_string(parameters)); - g_dbus_method_invocation_return_error(invocation, - G_DBUS_ERROR, - G_DBUS_ERROR_NO_SERVER, - "Server is exiting"); - return; - } - - _LOGT("dbus: request sender=%s, %s%s", - sender, - method_name, - g_variant_get_type_string(parameters)); - - if (!nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME)) - goto out_unknown_method; - - if (nm_streq(method_name, "GetFD")) { - g_variant_get(parameters, "(u)", &arg_u); - _handle_get_fd(gl, invocation, arg_u); - return; - } - if (nm_streq(method_name, "Ping")) { - g_variant_get(parameters, "(&s)", &arg_s); - _handle_ping(gl, invocation, arg_s); - return; - } - -out_unknown_method: - 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_PRIV_HELPER_DBUS_IFACE_NAME, - .methods = NM_DEFINE_GDBUS_METHOD_INFOS( - NM_DEFINE_GDBUS_METHOD_INFO( - "Ping", - .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), - .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), ), - NM_DEFINE_GDBUS_METHOD_INFO("GetFD", - .in_args = NM_DEFINE_GDBUS_ARG_INFOS( - NM_DEFINE_GDBUS_ARG_INFO("fd_type", "u"), ), ), ), ); - -static gboolean -_bus_register_service(GlobalData *gl) -{ - static const GDBusInterfaceVTable interface_vtable = { - .method_call = _bus_method_call, - }; - gs_free_error GError *error = NULL; - NMDBusConnectionCallBlockingData data = { - .result = NULL, - }; - gs_unref_variant GVariant *ret = NULL; - guint32 ret_val; - - gl->service_regist_id = - g_dbus_connection_register_object(gl->dbus_connection, - NM_PRIV_HELPER_DBUS_OBJECT_PATH, - interface_info, - NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable), - gl, - NULL, - &error); - if (gl->service_regist_id == 0) { - _LOGE("dbus: error registering object %s: %s", - NM_PRIV_HELPER_DBUS_OBJECT_PATH, - error->message); - return FALSE; - } - - _LOGD("dbus: object %s registered", NM_PRIV_HELPER_DBUS_OBJECT_PATH); - - /* regardless whether the request is successful, after we start calling - * RequestName, we remember that we need to ReleaseName it. */ - gl->name_requested = TRUE; - - nm_dbus_connection_call_request_name(gl->dbus_connection, - NM_PRIV_HELPER_DBUS_BUS_NAME, - DBUS_NAME_FLAG_ALLOW_REPLACEMENT - | DBUS_NAME_FLAG_REPLACE_EXISTING, - 10000, - gl->quit_cancellable, - nm_dbus_connection_call_blocking_callback, - &data); - - /* Note that with D-Bus activation, the first request will already hit us before RequestName - * completes. So when we start iterating the main context, the first request may already come - * in. */ - - ret = nm_dbus_connection_call_blocking(&data, &error); - - if (nm_utils_error_is_cancelled(error)) - return FALSE; - - if (error) { - _LOGE("d-bus: failed to request name %s: %s", NM_PRIV_HELPER_DBUS_BUS_NAME, error->message); - return FALSE; - } - - g_variant_get(ret, "(u)", &ret_val); - - if (ret_val != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { - _LOGW("dbus: request name for %s failed to take name (response %u)", - NM_PRIV_HELPER_DBUS_BUS_NAME, - ret_val); - return FALSE; - } - - _LOGD("dbus: request name for %s succeeded", NM_PRIV_HELPER_DBUS_BUS_NAME); - return TRUE; -} - -/*****************************************************************************/ - -static gboolean -_idle_timeout_cb(gpointer user_data) -{ - GlobalData *gl = user_data; - - _LOGT("idle-timeout: expired"); - nm_clear_g_source_inst(&gl->source_idle_timeout); - gl->shutdown_timeout = TRUE; - return G_SOURCE_CONTINUE; -} - -static void -_idle_timeout_restart(GlobalData *gl) -{ - nm_clear_g_source_inst(&gl->source_idle_timeout); - - if (gl->shutdown_quitting) - return; - - if (!c_list_is_empty(&gl->pending_jobs_lst_head)) - return; - - if (gl->timeout_msec == IDLE_TIMEOUT_INFINITY) - return; - - nm_assert(gl->timeout_msec < G_MAXINT32); - G_STATIC_ASSERT_EXPR(G_MAXINT32 < G_MAXUINT); - - _LOGT("idle-timeout: start (%u msec)", gl->timeout_msec); - gl->source_idle_timeout = nm_g_timeout_add_source(gl->timeout_msec, _idle_timeout_cb, gl); -} - -/*****************************************************************************/ - -static gboolean -_pending_job_register_object_release_on_idle_cb(gpointer data) -{ - PendingJobData *idle_data = data; - GlobalData *gl = idle_data->gl; - - c_list_unlink_stale(&idle_data->pending_jobs_lst); - nm_g_slice_free(idle_data); - - _idle_timeout_restart(gl); - return G_SOURCE_REMOVE; -} - -static void -_pending_job_register_object_weak_cb(gpointer data, GObject *where_the_object_was) -{ - /* The object might be destroyed on another thread. We need - * to sync with the main GMainContext by scheduling an idle action - * there. */ - nm_g_idle_add(_pending_job_register_object_release_on_idle_cb, data); -} - -static void -_pending_job_register_object(GlobalData *gl, GObject *obj) -{ - PendingJobData *idle_data; - - /* if we just hit the timeout, we can ignore it. */ - gl->shutdown_timeout = FALSE; - - if (nm_clear_g_source_inst(&gl->source_idle_timeout)) - _LOGT("idle-timeout: suspend timeout for pending request"); - - idle_data = g_slice_new(PendingJobData); - - idle_data->gl = gl; - c_list_link_tail(&gl->pending_jobs_lst_head, &idle_data->pending_jobs_lst); - - g_object_weak_ref(obj, _pending_job_register_object_weak_cb, idle_data); -} - -/*****************************************************************************/ - -static void -_bus_release_name_cb(GObject *source, GAsyncResult *result, gpointer user_data) -{ - _nm_unused gs_unref_object GObject *keep_alive_object = NULL; - GlobalData *gl; - - nm_utils_user_data_unpack(user_data, &gl, &keep_alive_object); - - gl->reject_new_requests = TRUE; - g_main_context_wakeup(NULL); -} - -static gboolean -_bus_release_name(GlobalData *gl) -{ - gs_unref_object GObject *keep_alive_object = NULL; - int r; - - /* We already requested a name. To exit-on-idle without race, we need to dance. - * See https://lists.freedesktop.org/archives/dbus/2015-May/016671.html . */ - - if (!gl->name_requested) - return FALSE; - - gl->name_requested = FALSE; - gl->shutdown_quitting = TRUE; - - _LOGT("shutdown: release-name"); - - keep_alive_object = g_object_new(G_TYPE_OBJECT, NULL); - - /* we use the _pending_job_register_object() mechanism to make the loop busy during - * shutdown. */ - _pending_job_register_object(gl, keep_alive_object); - - r = nm_sd_notify("STOPPING=1"); - if (r < 0) - _LOGW("shutdown: sd_notifiy(STOPPING=1) failed: %s", nm_strerror_native(-r)); - else - _LOGT("shutdown: sd_notifiy(STOPPING=1) succeeded"); - - g_dbus_connection_call(gl->dbus_connection, - DBUS_SERVICE_DBUS, - DBUS_PATH_DBUS, - DBUS_INTERFACE_DBUS, - "ReleaseName", - g_variant_new("(s)", NM_PRIV_HELPER_DBUS_BUS_NAME), - G_VARIANT_TYPE("(u)"), - G_DBUS_CALL_FLAGS_NONE, - 10000, - NULL, - _bus_release_name_cb, - nm_utils_user_data_pack(gl, g_steal_pointer(&keep_alive_object))); - return TRUE; -} - -/*****************************************************************************/ - -static void -_initial_setup(GlobalData *gl) -{ - gl->no_auth_for_testing = - _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_NO_AUTH_FOR_TESTING")), - 0, - 0, - 1, - 0); - gl->timeout_msec = - _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_IDLE_TIMEOUT_MSEC")), - 0, - 0, - G_MAXINT32, - IDLE_TIMEOUT_MSEC); - - gl->quit_cancellable = g_cancellable_new(); - - signal(SIGPIPE, SIG_IGN); - gl->source_sigterm = nm_g_unix_signal_add_source(SIGTERM, _signal_callback_term, gl); -} - -/*****************************************************************************/ - -int -main(int argc, char **argv) -{ - GlobalData _gl = { - .quit_cancellable = NULL, - .pending_jobs_lst_head = C_LIST_INIT(_gl.pending_jobs_lst_head), - }; - GlobalData *const gl = &_gl; - int exit_code; - int r = 0; - - _nm_logging_enabled_init(g_getenv(_ENV("NM_PRIV_HELPER_LOG"))); - - gl->start_timestamp_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME); - - _LOGD("starting nm-priv-helper (%s)", NM_DIST_VERSION); - - _initial_setup(gl); - - if (gl->no_auth_for_testing) { - _LOGW("WARNING: running in debug mode without authentication " - "(NM_PRIV_HELPER_NO_AUTH_FOR_TESTING). "); - } - - if (gl->timeout_msec != IDLE_TIMEOUT_INFINITY) - _LOGT("idle-timeout: %u msec", gl->timeout_msec); - else - _LOGT("idle-timeout: disabled"); - - gl->dbus_connection = _bus_get(gl->quit_cancellable, &r); - if (!gl->dbus_connection) { - exit_code = r; - goto done; - } - - if (!_bus_find_nm_nameowner(gl)) { - /* abort due to cancellation. That is success. */ - exit_code = EXIT_SUCCESS; - goto done; - } - _LOGD("%s name-owner: %s", NM_DBUS_SERVICE, gl->name_owner ?: "(null)"); - - _idle_timeout_restart(gl); - - exit_code = EXIT_SUCCESS; - - if (!_bus_register_service(gl)) { - /* We failed to RequestName, but due to D-Bus activation we - * might have a pending request still (on the unique name). - * Process it below. - * - * Let's fake a shutdown signal, and still process the request below. */ - if (!g_cancellable_is_cancelled(gl->quit_cancellable)) - exit_code = EXIT_FAILURE; - gl->shutdown_quitting = TRUE; - - if (gl->name_requested) { - /* We requested a name, but something went wrong. Below we will release - * the name right away. */ - } else { - /* In case we didn't even went as far to request the name. New requests - * can only come via the unique name, and as we are shutting down, they - * are rejected. */ - gl->reject_new_requests = TRUE; - } - } - - while (TRUE) { - if (gl->shutdown_quitting) - _bus_release_name(gl); - - if (!c_list_is_empty(&gl->pending_jobs_lst_head)) { - /* we must first reply to all requests. No matter what. */ - } else if (gl->shutdown_quitting || gl->shutdown_timeout) { - /* we either hit the idle timeout or received SIGTERM. Note that - * if we received an idle-timeout and the very moment afterwards - * a new request, then _bus_method_call() will clear gl->shutdown_timeout - * (via _pending_job_register_object()). */ - if (!_bus_release_name(gl)) - break; - } - - g_main_context_iteration(NULL, TRUE); - } - -done: - _LOGD("shutdown: cleanup"); - - gl->shutdown_quitting = TRUE; - g_cancellable_cancel(gl->quit_cancellable); - - nm_assert(c_list_is_empty(&gl->pending_jobs_lst_head)); - - if (gl->service_regist_id != 0) { - g_dbus_connection_unregister_object(gl->dbus_connection, - nm_steal_int(&gl->service_regist_id)); - } - if (gl->name_owner_changed_id != 0) { - g_dbus_connection_signal_unsubscribe(gl->dbus_connection, - nm_steal_int(&gl->name_owner_changed_id)); - } - - if (gl->dbus_connection) { - g_dbus_connection_flush_sync(gl->dbus_connection, NULL, NULL); - g_clear_object(&gl->dbus_connection); - } - - nm_g_main_context_iterate_ready(NULL); - - nm_clear_g_free(&gl->name_owner); - - nm_clear_g_source_inst(&gl->source_sigterm); - nm_clear_g_source_inst(&gl->source_idle_timeout); - g_clear_object(&gl->quit_cancellable); - - _LOGD("exit (%d)", exit_code); - return exit_code; -} diff --git a/src/nm-priv-helper/nm-priv-helper.conf b/src/nm-priv-helper/nm-priv-helper.conf deleted file mode 100644 index c0d04617..00000000 --- a/src/nm-priv-helper/nm-priv-helper.conf +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in b/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in deleted file mode 100644 index 9e86a635..00000000 --- a/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in +++ /dev/null @@ -1,5 +0,0 @@ -[D-BUS Service] -Name=org.freedesktop.nm_priv_helper -Exec=@libexecdir@/nm-priv-helper -User=root -SystemdService=nm-priv-helper.service -- cgit 1.3.0-6-gf8a5