/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2008 Novell, Inc. * Copyright (C) 2008 - 2018 Red Hat, Inc. */ #include "src/core/nm-default-daemon.h" #include "nms-keyfile-plugin.h" #include #include #include #include #include "libnm-std-aux/c-list-util.h" #include "libnm-glib-aux/nm-c-list.h" #include "libnm-glib-aux/nm-uuid.h" #include "libnm-glib-aux/nm-io-utils.h" #include "nm-connection.h" #include "nm-setting.h" #include "nm-setting-connection.h" #include "nm-utils.h" #include "nm-config.h" #include "libnm-core-intern/nm-core-internal.h" #include "libnm-core-intern/nm-keyfile-internal.h" #include "settings/nm-settings-plugin.h" #include "settings/nm-settings-storage.h" #include "settings/nm-settings-utils.h" #include "nms-keyfile-storage.h" #include "nms-keyfile-writer.h" #include "nms-keyfile-reader.h" #include "nms-keyfile-utils.h" /*****************************************************************************/ typedef struct { NMConfig *config; /* there can/could be multiple read-only directories. For example, one * could set dirname_libs to * - /usr/lib/NetworkManager/profiles/ * - /etc/NetworkManager/system-connections * and leave dirname_etc unset. In this case, there would be multiple * read-only directories. * * Directories that come later have higher priority and shadow profiles * from earlier directories. * * Currently, this is only an array with zero or one elements. It could be * easily extended to support multiple read-only directories. */ char *dirname_libs[2]; char *dirname_etc; char *dirname_run; NMSettUtilStorages storages; } NMSKeyfilePluginPrivate; struct _NMSKeyfilePlugin { NMSettingsPlugin parent; NMSKeyfilePluginPrivate _priv; }; struct _NMSKeyfilePluginClass { NMSettingsPluginClass parent; }; G_DEFINE_TYPE(NMSKeyfilePlugin, nms_keyfile_plugin, NM_TYPE_SETTINGS_PLUGIN) #define NMS_KEYFILE_PLUGIN_GET_PRIVATE(self) \ _NM_GET_PRIVATE(self, NMSKeyfilePlugin, NMS_IS_KEYFILE_PLUGIN, NMSettingsPlugin) /*****************************************************************************/ #define _NMLOG_PREFIX_NAME "keyfile" #define _NMLOG_DOMAIN LOGD_SETTINGS #define _NMLOG(level, ...) \ nm_log((level), \ _NMLOG_DOMAIN, \ NULL, \ NULL, \ "%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ _NMLOG_PREFIX_NAME ": " _NM_UTILS_MACRO_REST(__VA_ARGS__)) /*****************************************************************************/ static const char * _extra_flags_to_string(char *str, gsize str_len, gboolean is_nm_generated, gboolean is_volatile, gboolean is_external) { const char *str0 = str; if (!is_nm_generated && !is_volatile && !is_external) nm_strbuf_append_str(&str, &str_len, ""); else { char ch = '('; nm_strbuf_append_c(&str, &str_len, ' '); if (is_nm_generated) { nm_strbuf_append_c(&str, &str_len, ch); nm_strbuf_append_str(&str, &str_len, "nm-generated"); ch = ','; } if (is_volatile) { nm_strbuf_append_c(&str, &str_len, ch); nm_strbuf_append_str(&str, &str_len, "volatile"); ch = ','; } if (is_external) { nm_strbuf_append_c(&str, &str_len, ch); nm_strbuf_append_str(&str, &str_len, "external"); ch = ','; } nm_strbuf_append_c(&str, &str_len, ')'); } return str0; } static gboolean _ignore_filename(NMSKeyfileStorageType storage_type, const char *filename) { /* for backward-compatibility, we don't require an extension for * files under "/etc/...". */ return nm_keyfile_utils_ignore_filename(filename, (storage_type != NMS_KEYFILE_STORAGE_TYPE_ETC)); } static const char * _get_plugin_dir(NMSKeyfilePluginPrivate *priv) { /* the plugin dir is only needed to generate connection.uuid value via * nm_keyfile_read_ensure_uuid(). This is either the configured /etc * directory, of the compile-time default (in case the /etc directory * is disabled). */ return priv->dirname_etc ?: NM_KEYFILE_PATH_NAME_ETC_DEFAULT; } static gboolean _path_detect_storage_type(const char *full_filename, const char *const *dirname_libs, const char *dirname_etc, const char *dirname_run, NMSKeyfileStorageType *out_storage_type, const char **out_dirname, const char **out_filename, gboolean *out_is_nmmeta_file, gboolean *out_failed_due_to_invalid_filename) { NMSKeyfileStorageType storage_type; const char *filename = NULL; const char *dirname = NULL; guint i; gboolean is_nmmeta_file = FALSE; NM_SET_OUT(out_failed_due_to_invalid_filename, FALSE); if (full_filename[0] != '/') return FALSE; if (dirname_run && (filename = nm_utils_file_is_in_path(full_filename, dirname_run))) { storage_type = NMS_KEYFILE_STORAGE_TYPE_RUN; dirname = dirname_run; } else if (dirname_etc && (filename = nm_utils_file_is_in_path(full_filename, dirname_etc))) { storage_type = NMS_KEYFILE_STORAGE_TYPE_ETC; dirname = dirname_etc; } else { for (i = 0; dirname_libs && dirname_libs[i]; i++) { if ((filename = nm_utils_file_is_in_path(full_filename, dirname_libs[i]))) { storage_type = NMS_KEYFILE_STORAGE_TYPE_LIB(i); dirname = dirname_libs[i]; break; } } if (!dirname) return FALSE; } if (_ignore_filename(storage_type, filename)) { /* we accept nmmeta files, but only in /etc and /run directories. */ if (!NM_IN_SET(storage_type, NMS_KEYFILE_STORAGE_TYPE_RUN, NMS_KEYFILE_STORAGE_TYPE_ETC) || !nms_keyfile_nmmeta_check_filename(filename, NULL)) { NM_SET_OUT(out_failed_due_to_invalid_filename, TRUE); return FALSE; } is_nmmeta_file = TRUE; } NM_SET_OUT(out_storage_type, storage_type); NM_SET_OUT(out_dirname, dirname); NM_SET_OUT(out_filename, filename); NM_SET_OUT(out_is_nmmeta_file, is_nmmeta_file); return TRUE; } /*****************************************************************************/ static NMConnection * _read_from_file(const char *full_filename, const char *plugin_dir, struct stat *out_stat, NMTernary *out_is_nm_generated, NMTernary *out_is_volatile, NMTernary *out_is_external, char **out_shadowed_storage, NMTernary *out_shadowed_owned, GError **error) { NMConnection *connection; nm_assert(full_filename && full_filename[0] == '/'); connection = nms_keyfile_reader_from_file(full_filename, plugin_dir, out_stat, out_is_nm_generated, out_is_volatile, out_is_external, out_shadowed_storage, out_shadowed_owned, error); nm_assert(!connection || (_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS)); nm_assert(!connection || nm_uuid_is_normalized(nm_connection_get_uuid(connection))); return connection; } /*****************************************************************************/ static void _nm_assert_storage(gpointer plugin /* NMSKeyfilePlugin */, gpointer storage /* NMSKeyfileStorage */, gboolean tracked) { #if NM_MORE_ASSERTS NMSettUtilStorageByUuidHead *sbuh; const char *uuid; nm_assert(!plugin || NMS_IS_KEYFILE_PLUGIN(plugin)); nm_assert(NMS_IS_KEYFILE_STORAGE(storage)); nm_assert(!plugin || plugin == nm_settings_storage_get_plugin(storage)); nm_assert(({ const char *f = nms_keyfile_storage_get_filename(storage); f &&f[0] == '/'; })); uuid = nms_keyfile_storage_get_uuid(storage); nm_assert(nm_uuid_is_normalized(uuid)); nm_assert(((NMSKeyfileStorage *) storage)->is_meta_data || !(((NMSKeyfileStorage *) storage)->u.conn_data.connection) || (NM_IS_CONNECTION((((NMSKeyfileStorage *) storage)->u.conn_data.connection)) && nm_streq0(uuid, nm_connection_get_uuid( (((NMSKeyfileStorage *) storage)->u.conn_data.connection))))); nm_assert( !tracked || !plugin || c_list_contains(&NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin)->storages._storage_lst_head, &NMS_KEYFILE_STORAGE(storage)->parent._storage_lst)); nm_assert(!tracked || !plugin || storage == g_hash_table_lookup( NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin)->storages.idx_by_filename, nms_keyfile_storage_get_filename(storage))); if (tracked && plugin) { sbuh = g_hash_table_lookup(NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin)->storages.idx_by_uuid, &uuid); nm_assert(sbuh); nm_assert(c_list_contains(&sbuh->_storage_by_uuid_lst_head, &((NMSKeyfileStorage *) storage)->parent._storage_by_uuid_lst)); } #endif } /*****************************************************************************/ static NMSKeyfileStorage * _load_file(NMSKeyfilePlugin *self, const char *dirname, const char *filename, NMSKeyfileStorageType storage_type, GError **error) { NMSKeyfilePluginPrivate *priv; gs_unref_object NMConnection *connection = NULL; NMTernary is_nm_generated_opt; NMTernary is_volatile_opt; NMTernary is_external_opt; NMTernary shadowed_owned_opt; gs_free char *shadowed_storage = NULL; gs_free_error GError *local = NULL; gs_free char *full_filename = NULL; struct stat st; if (_ignore_filename(storage_type, filename)) { gs_free char *nmmeta = NULL; gs_free char *loaded_path = NULL; gs_free char *shadowed_storage_filename = NULL; if (!nms_keyfile_nmmeta_check_filename(filename, NULL)) { if (error) nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "skip due to invalid filename"); else _LOGT("load: \"%s/%s\": skip file due to invalid filename", dirname, filename); return NULL; } if (!nms_keyfile_nmmeta_read(dirname, filename, &full_filename, &nmmeta, &loaded_path, &shadowed_storage_filename, NULL)) { if (error) nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "skip unreadable nmmeta file"); else _LOGT("load: \"%s/%s\": skip unreadable nmmeta file", dirname, filename); return NULL; } nm_assert(loaded_path); if (!NM_IN_SET(storage_type, NMS_KEYFILE_STORAGE_TYPE_RUN, NMS_KEYFILE_STORAGE_TYPE_ETC)) { if (error) nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "skip nmmeta file from read-only directory"); else _LOGT("load: \"%s/%s\": skip nmmeta file from read-only directory", dirname, filename); return NULL; } if (!nm_streq(loaded_path, NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL)) { if (error) nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "skip nmmeta file not symlinking %s", NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL); else _LOGT("load: \"%s/%s\": skip nmmeta file not symlinking to %s", dirname, filename, NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL); return NULL; } return nms_keyfile_storage_new_tombstone(self, nmmeta, full_filename, storage_type, shadowed_storage_filename); } full_filename = g_build_filename(dirname, filename, NULL); priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); connection = _read_from_file(full_filename, _get_plugin_dir(priv), &st, &is_nm_generated_opt, &is_volatile_opt, &is_external_opt, &shadowed_storage, &shadowed_owned_opt, &local); if (!connection) { if (error) g_propagate_error(error, g_steal_pointer(&local)); else _LOGW("load: \"%s\": failed to load connection: %s", full_filename, local->message); return NULL; } return nms_keyfile_storage_new_connection(self, g_steal_pointer(&connection), full_filename, storage_type, is_nm_generated_opt, is_volatile_opt, is_external_opt, shadowed_storage, shadowed_owned_opt, &st.st_mtim); } static NMSKeyfileStorage * _load_file_from_path(NMSKeyfilePlugin *self, const char *full_filename, NMSKeyfileStorageType storage_type, GError **error) { gs_free char *f_dirname_free = NULL; const char *f_filename; const char *f_dirname; nm_assert(full_filename && full_filename[0] == '/'); f_filename = strrchr(full_filename, '/'); f_dirname = nm_strndup_a(300, full_filename, f_filename - full_filename, &f_dirname_free); f_filename++; return _load_file(self, f_dirname, f_filename, storage_type, error); } static void _load_dir(NMSKeyfilePlugin *self, NMSKeyfileStorageType storage_type, const char *dirname, NMSettUtilStorages *storages) { const char *filename; GDir *dir; gs_unref_hashtable GHashTable *dupl_filenames = NULL; dir = g_dir_open(dirname, 0, NULL); if (!dir) return; dupl_filenames = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free); while ((filename = g_dir_read_name(dir))) { gs_unref_object NMSKeyfileStorage *storage = NULL; filename = g_strdup(filename); if (!g_hash_table_add(dupl_filenames, (char *) filename)) continue; storage = _load_file(self, dirname, filename, storage_type, NULL); if (!storage) continue; nm_sett_util_storages_add_take(storages, g_steal_pointer(&storage)); } g_dir_close(dir); #if NM_MORE_ASSERTS { NMSKeyfileStorage *storage; c_list_for_each_entry (storage, &storages->_storage_lst_head, parent._storage_lst) nm_assert(NMS_IS_KEYFILE_STORAGE(storage)); } #endif } /*****************************************************************************/ static void _storages_consolidate(NMSKeyfilePlugin *self, NMSettUtilStorages *storages_new, gboolean replace_all, GHashTable *storages_replaced, NMSettingsPluginConnectionLoadCallback callback, gpointer user_data) { NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); CList lst_conn_info_deleted = C_LIST_INIT(lst_conn_info_deleted); gs_unref_ptrarray GPtrArray *storages_modified = NULL; CList storages_deleted; NMSKeyfileStorage *storage_safe; NMSKeyfileStorage *storage_new; NMSKeyfileStorage *storage_old; NMSKeyfileStorage *storage; guint i; storages_modified = g_ptr_array_new_with_free_func(g_object_unref); c_list_init(&storages_deleted); c_list_for_each_entry (storage_old, &priv->storages._storage_lst_head, parent._storage_lst) storage_old->is_dirty = TRUE; c_list_for_each_entry_safe (storage_new, storage_safe, &storages_new->_storage_lst_head, parent._storage_lst) { storage_old = nm_sett_util_storages_lookup_by_filename(&priv->storages, nms_keyfile_storage_get_filename(storage_new)); nm_sett_util_storages_steal(storages_new, storage_new); if (!storage_old || !nm_streq(nms_keyfile_storage_get_uuid(storage_new), nms_keyfile_storage_get_uuid(storage_old))) { if (storage_old) { nm_sett_util_storages_steal(&priv->storages, storage_old); c_list_link_tail(&storages_deleted, &storage_old->parent._storage_by_uuid_lst); } storage_new->is_dirty = FALSE; nm_sett_util_storages_add_take(&priv->storages, storage_new); g_ptr_array_add(storages_modified, g_object_ref(storage_new)); continue; } storage_old->is_dirty = FALSE; nms_keyfile_storage_copy_content(storage_old, storage_new); nms_keyfile_storage_destroy(storage_new); g_ptr_array_add(storages_modified, g_object_ref(storage_old)); } c_list_for_each_entry_safe (storage_old, storage_safe, &priv->storages._storage_lst_head, parent._storage_lst) { if (!storage_old->is_dirty) continue; if (replace_all || (storages_replaced && g_hash_table_contains(storages_replaced, storage_old))) { nm_sett_util_storages_steal(&priv->storages, storage_old); c_list_link_tail(&storages_deleted, &storage_old->parent._storage_by_uuid_lst); } } /* raise events. */ for (i = 0; i < storages_modified->len; i++) { storage = storages_modified->pdata[i]; storage->is_dirty = TRUE; } for (i = 0; i < storages_modified->len; i++) { gs_unref_object NMConnection *connection = NULL; storage = storages_modified->pdata[i]; if (!storage->is_dirty) { /* the entry is no longer is_dirty. In the meantime we already emitted * another signal for it. */ continue; } storage->is_dirty = FALSE; if (c_list_is_empty(&storage->parent._storage_lst)) { /* hm? The profile was deleted in the meantime? That is only possible * if the signal handler called again into the plugin. In any case, the event * was already emitted. Skip. */ continue; } nm_assert( storage == nm_sett_util_storages_lookup_by_filename(&priv->storages, nms_keyfile_storage_get_filename(storage))); connection = nms_keyfile_storage_steal_connection(storage); callback(NM_SETTINGS_PLUGIN(self), NM_SETTINGS_STORAGE(storage), connection, user_data); } while ((storage = c_list_first_entry(&storages_deleted, NMSKeyfileStorage, parent._storage_by_uuid_lst))) { c_list_unlink(&storage->parent._storage_by_uuid_lst); callback(NM_SETTINGS_PLUGIN(self), NM_SETTINGS_STORAGE(storage), NULL, user_data); nms_keyfile_storage_destroy(storage); } } static void reload_connections(NMSettingsPlugin *plugin, NMSettingsPluginConnectionLoadCallback callback, gpointer user_data) { NMSKeyfilePlugin *self = NMS_KEYFILE_PLUGIN(plugin); NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); nm_auto_clear_sett_util_storages NMSettUtilStorages storages_new = NM_SETT_UTIL_STORAGES_INIT(storages_new, nms_keyfile_storage_destroy); int i; _load_dir(self, NMS_KEYFILE_STORAGE_TYPE_RUN, priv->dirname_run, &storages_new); if (priv->dirname_etc) _load_dir(self, NMS_KEYFILE_STORAGE_TYPE_ETC, priv->dirname_etc, &storages_new); for (i = 0; priv->dirname_libs[i]; i++) _load_dir(self, NMS_KEYFILE_STORAGE_TYPE_LIB(i), priv->dirname_libs[i], &storages_new); _storages_consolidate(self, &storages_new, TRUE, NULL, callback, user_data); } static void load_connections(NMSettingsPlugin *plugin, NMSettingsPluginConnectionLoadEntry *entries, gsize n_entries, NMSettingsPluginConnectionLoadCallback callback, gpointer user_data) { NMSKeyfilePlugin *self = NMS_KEYFILE_PLUGIN(plugin); NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); nm_auto_clear_sett_util_storages NMSettUtilStorages storages_new = NM_SETT_UTIL_STORAGES_INIT(storages_new, nms_keyfile_storage_destroy); gs_unref_hashtable GHashTable *dupl_filenames = NULL; gs_unref_hashtable GHashTable *storages_replaced = NULL; gs_unref_hashtable GHashTable *loaded_uuids = NULL; const char *loaded_uuid; GHashTableIter h_iter; gsize i; if (n_entries == 0) return; dupl_filenames = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL); loaded_uuids = g_hash_table_new(nm_str_hash, g_str_equal); storages_replaced = g_hash_table_new_full(nm_direct_hash, NULL, g_object_unref, NULL); for (i = 0; i < n_entries; i++) { NMSettingsPluginConnectionLoadEntry *const entry = &entries[i]; NMSKeyfileStorageType storage_type; gs_free_error GError *local = NULL; const char *f_filename; const char *f_dirname; const char *full_filename; gs_free char *full_filename_keep = NULL; gboolean is_nmmeta_file; NMSettingsPluginConnectionLoadEntry *dupl_content_entry; gboolean failed_due_to_invalid_filename; gs_unref_object NMSKeyfileStorage *storage = NULL; if (entry->handled) continue; if (!_path_detect_storage_type(entry->filename, (const char *const *) priv->dirname_libs, priv->dirname_etc, priv->dirname_run, &storage_type, &f_dirname, &f_filename, &is_nmmeta_file, &failed_due_to_invalid_filename)) { if (failed_due_to_invalid_filename) { entry->handled = TRUE; nm_utils_error_set(&entry->error, NM_UTILS_ERROR_UNKNOWN, "filename is not valid for a keyfile"); } continue; } full_filename_keep = g_build_filename(f_dirname, f_filename, NULL); if ((dupl_content_entry = g_hash_table_lookup(dupl_filenames, full_filename_keep))) { /* we already visited this file. */ entry->handled = dupl_content_entry->handled; if (dupl_content_entry->error) { g_set_error_literal(&entry->error, dupl_content_entry->error->domain, dupl_content_entry->error->code, dupl_content_entry->error->message); } continue; } entry->handled = TRUE; full_filename = full_filename_keep; if (!g_hash_table_insert(dupl_filenames, g_steal_pointer(&full_filename_keep), entry)) nm_assert_not_reached(); storage = _load_file(self, f_dirname, f_filename, storage_type, &local); if (!storage) { if (nm_utils_file_stat(full_filename, NULL) == -ENOENT) { NMSKeyfileStorage *storage2; /* the file does not exist. We take that as indication to unload the file * that was previously loaded... */ storage2 = nm_sett_util_storages_lookup_by_filename(&priv->storages, full_filename); if (storage2) g_hash_table_add(storages_replaced, g_object_ref(storage2)); continue; } g_propagate_error(&entry->error, g_steal_pointer(&local)); continue; } g_hash_table_add(loaded_uuids, (char *) nms_keyfile_storage_get_uuid(storage)); nm_sett_util_storages_add_take(&storages_new, g_steal_pointer(&storage)); } /* now we visit all UUIDs that are about to change... */ g_hash_table_iter_init(&h_iter, loaded_uuids); while (g_hash_table_iter_next(&h_iter, (gpointer *) &loaded_uuid, NULL)) { NMSKeyfileStorage *storage; NMSettUtilStorageByUuidHead *sbuh; sbuh = nm_sett_util_storages_lookup_by_uuid(&priv->storages, loaded_uuid); if (!sbuh) continue; c_list_for_each_entry (storage, &sbuh->_storage_by_uuid_lst_head, parent._storage_by_uuid_lst) { const char *full_filename = nms_keyfile_storage_get_filename(storage); gs_unref_object NMSKeyfileStorage *storage_new = NULL; gs_free_error GError *local = NULL; if (g_hash_table_contains(dupl_filenames, full_filename)) { /* already re-loaded. */ continue; } /* @storage has a UUID that was just loaded from disk, but we have an entry in cache. * Reload that file too despite not being told to do so. The reason is to get * the latest file timestamp so that we get the priorities right. */ storage_new = _load_file_from_path(self, full_filename, storage->storage_type, &local); if (storage_new && !nm_streq(loaded_uuid, nms_keyfile_storage_get_uuid(storage_new))) { /* the file now references a different UUID. We are not told to reload * that file, so this means the existing storage (with the previous * filename and UUID tuple) is no longer valid. */ g_clear_object(&storage_new); } g_hash_table_add(storages_replaced, g_object_ref(storage)); if (storage_new) nm_sett_util_storages_add_take(&storages_new, g_steal_pointer(&storage_new)); } } nm_clear_pointer(&loaded_uuids, g_hash_table_destroy); nm_clear_pointer(&dupl_filenames, g_hash_table_destroy); _storages_consolidate(self, &storages_new, FALSE, storages_replaced, callback, user_data); } gboolean nms_keyfile_plugin_add_connection(NMSKeyfilePlugin *self, NMConnection *connection, gboolean in_memory, gboolean is_nm_generated, gboolean is_volatile, gboolean is_external, const char *shadowed_storage, gboolean shadowed_owned, NMSettingsStorage **out_storage, NMConnection **out_connection, GError **error) { NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); gs_unref_object NMConnection *reread = NULL; gs_free char *full_filename = NULL; NMSKeyfileStorageType storage_type; gs_unref_object NMSKeyfileStorage *storage = NULL; GError *local = NULL; const char *uuid; gboolean reread_same; struct timespec mtime; char strbuf[100]; nm_assert(NM_IS_CONNECTION(connection)); nm_assert(out_storage && !*out_storage); nm_assert(out_connection && !*out_connection); nm_assert(in_memory || (!is_nm_generated && !is_volatile && !is_external && !shadowed_storage && !shadowed_owned)); uuid = nm_connection_get_uuid(connection); /* Note that even if the caller requests persistent storage, we may switch to in-memory, if * no /etc directory is configured. */ storage_type = !in_memory && priv->dirname_etc ? NMS_KEYFILE_STORAGE_TYPE_ETC : NMS_KEYFILE_STORAGE_TYPE_RUN; if (!nms_keyfile_writer_connection( connection, is_nm_generated, is_volatile, is_external, shadowed_storage, shadowed_owned, storage_type == NMS_KEYFILE_STORAGE_TYPE_ETC ? priv->dirname_etc : priv->dirname_run, _get_plugin_dir(priv), NULL, FALSE, FALSE, nm_sett_util_allow_filename_cb, NM_SETT_UTIL_ALLOW_FILENAME_DATA(&priv->storages, NULL), &full_filename, &reread, &reread_same, &local)) { _LOGT("commit: %s (%s) failed to add: %s", nm_connection_get_uuid(connection), nm_connection_get_id(connection), local->message); g_propagate_error(error, local); return FALSE; } if (!reread || reread_same) nm_g_object_ref_set(&reread, connection); nm_assert(_nm_connection_verify(reread, NULL) == NM_SETTING_VERIFY_SUCCESS); nm_assert(nm_streq0(nm_connection_get_uuid(connection), nm_connection_get_uuid(reread))); nm_assert(full_filename && full_filename[0] == '/'); nm_assert(!nm_sett_util_storages_lookup_by_filename(&priv->storages, full_filename)); _LOGT("commit: %s (%s) added as \"%s\"%s%s%s%s", uuid, nm_connection_get_id(connection), full_filename, _extra_flags_to_string(strbuf, sizeof(strbuf), is_nm_generated, is_volatile, is_external), NM_PRINT_FMT_QUOTED(shadowed_storage, " (shadows \"", shadowed_storage, shadowed_owned ? "\", owned)" : "\")", "")); storage = nms_keyfile_storage_new_connection(self, g_steal_pointer(&reread), full_filename, storage_type, is_nm_generated ? NM_TERNARY_TRUE : NM_TERNARY_FALSE, is_volatile ? NM_TERNARY_TRUE : NM_TERNARY_FALSE, is_external ? NM_TERNARY_TRUE : NM_TERNARY_FALSE, shadowed_storage, shadowed_owned ? NM_TERNARY_TRUE : NM_TERNARY_FALSE, nm_sett_util_stat_mtime(full_filename, FALSE, &mtime)); nm_sett_util_storages_add_take(&priv->storages, g_object_ref(storage)); *out_connection = nms_keyfile_storage_steal_connection(storage); *out_storage = NM_SETTINGS_STORAGE(g_steal_pointer(&storage)); return TRUE; } static gboolean add_connection(NMSettingsPlugin *plugin, NMConnection *connection, NMSettingsStorage **out_storage, NMConnection **out_connection, GError **error) { return nms_keyfile_plugin_add_connection(NMS_KEYFILE_PLUGIN(plugin), connection, FALSE, FALSE, FALSE, FALSE, NULL, FALSE, out_storage, out_connection, error); } gboolean nms_keyfile_plugin_update_connection(NMSKeyfilePlugin *self, NMSettingsStorage *storage_x, NMConnection *connection, gboolean is_nm_generated, gboolean is_volatile, gboolean is_external, const char *shadowed_storage, gboolean shadowed_owned, gboolean force_rename, NMSettingsStorage **out_storage, NMConnection **out_connection, GError **error) { NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); NMSKeyfileStorage *storage = NMS_KEYFILE_STORAGE(storage_x); gs_unref_object NMConnection *connection_clone = NULL; gs_unref_object NMConnection *reread = NULL; gs_free char *full_filename = NULL; gs_free_error GError *local = NULL; struct timespec mtime; const char *previous_filename; gboolean reread_same; const char *uuid; char strbuf[100]; _nm_assert_storage(self, storage, TRUE); nm_assert(NM_IS_CONNECTION(connection)); nm_assert(_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS); nm_assert(nm_streq(nms_keyfile_storage_get_uuid(storage), nm_connection_get_uuid(connection))); nm_assert(!error || !*error); nm_assert(NM_IN_SET(storage->storage_type, NMS_KEYFILE_STORAGE_TYPE_ETC, NMS_KEYFILE_STORAGE_TYPE_RUN)); nm_assert(!storage->is_meta_data); nm_assert(storage->storage_type == NMS_KEYFILE_STORAGE_TYPE_RUN || (!is_nm_generated && !is_volatile && !is_external && !shadowed_storage && !shadowed_owned)); nm_assert(!shadowed_owned || shadowed_storage); nm_assert(priv->dirname_etc || storage->storage_type != NMS_KEYFILE_STORAGE_TYPE_ETC); previous_filename = nms_keyfile_storage_get_filename(storage); uuid = nms_keyfile_storage_get_uuid(storage); if (!nms_keyfile_writer_connection( connection, is_nm_generated, is_volatile, is_external, shadowed_storage, shadowed_owned, storage->storage_type == NMS_KEYFILE_STORAGE_TYPE_ETC ? priv->dirname_etc : priv->dirname_run, _get_plugin_dir(priv), previous_filename, FALSE, FALSE, nm_sett_util_allow_filename_cb, NM_SETT_UTIL_ALLOW_FILENAME_DATA(&priv->storages, previous_filename), &full_filename, &reread, &reread_same, &local)) { _LOGW("commit: failure to write %s (%s) to \"%s\": %s", uuid, nm_connection_get_id(connection_clone), previous_filename, local->message); g_propagate_error(error, g_steal_pointer(&local)); return FALSE; } nm_assert(full_filename && nm_streq(full_filename, previous_filename)); if (!reread || reread_same) nm_g_object_ref_set(&reread, connection); nm_assert(_nm_connection_verify(reread, NULL) == NM_SETTING_VERIFY_SUCCESS); nm_assert(nm_streq(nm_connection_get_uuid(reread), uuid)); _LOGT("commit: \"%s\": profile %s (%s) written%s%s%s%s", full_filename, uuid, nm_connection_get_id(connection), _extra_flags_to_string(strbuf, sizeof(strbuf), is_nm_generated, is_volatile, is_external), NM_PRINT_FMT_QUOTED(shadowed_storage, shadowed_owned ? " (owns \"" : " (shadows \"", shadowed_storage, "\")", "")); storage->u.conn_data.is_nm_generated = is_nm_generated; storage->u.conn_data.is_volatile = is_volatile; storage->u.conn_data.is_external = is_external; storage->u.conn_data.stat_mtime = *nm_sett_util_stat_mtime(full_filename, FALSE, &mtime); storage->u.conn_data.shadowed_owned = shadowed_owned; *out_storage = g_object_ref(NM_SETTINGS_STORAGE(storage)); *out_connection = g_steal_pointer(&reread); return TRUE; } static gboolean update_connection(NMSettingsPlugin *plugin, NMSettingsStorage *storage, NMConnection *connection, NMSettingsStorage **out_storage, NMConnection **out_connection, GError **error) { return nms_keyfile_plugin_update_connection(NMS_KEYFILE_PLUGIN(plugin), storage, connection, FALSE, FALSE, FALSE, NULL, FALSE, FALSE, out_storage, out_connection, error); } static gboolean delete_connection(NMSettingsPlugin *plugin, NMSettingsStorage *storage_x, GError **error) { NMSKeyfilePlugin *self = NMS_KEYFILE_PLUGIN(plugin); NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); gs_unref_object NMSKeyfileStorage *storage = g_object_ref(NMS_KEYFILE_STORAGE(storage_x)); const char *remove_from_disk_errmsg = NULL; const char *operation_message; const char *previous_filename; const char *uuid; gboolean success = TRUE; _nm_assert_storage(self, storage, TRUE); nm_assert(!error || !*error); previous_filename = nms_keyfile_storage_get_filename(storage); uuid = nms_keyfile_storage_get_uuid(storage); if (!NM_IN_SET(storage->storage_type, NMS_KEYFILE_STORAGE_TYPE_ETC, NMS_KEYFILE_STORAGE_TYPE_RUN)) { nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "profile in read-only storage cannot be deleted"); success = FALSE; operation_message = "dropped readonly file from memory"; } else if (unlink(previous_filename) != 0) { int errsv; errsv = errno; if (errsv != ENOENT) { remove_from_disk_errmsg = nm_strerror_native(errsv); operation_message = "failed to delete from disk"; success = FALSE; nm_utils_error_set_errno(error, errsv, "failure to delete \"%s\": %s", previous_filename); } else operation_message = "does not exist on disk"; } else operation_message = "deleted from disk"; _LOGT("commit: deleted \"%s\", %s %s (%s%s%s%s)", previous_filename, storage->is_meta_data ? "meta-data" : "profile", uuid, operation_message, NM_PRINT_FMT_QUOTED(remove_from_disk_errmsg, ": ", remove_from_disk_errmsg, "", "")); if (success) { nm_sett_util_storages_steal(&priv->storages, storage); nms_keyfile_storage_destroy(storage); } return success; } /** * nms_keyfile_plugin_set_nmmeta_tombstone: * @self: the #NMSKeyfilePlugin instance * @simulate: if %TRUE, don't do anything on the filename but just pretend * that the loaded UUID file gets tracked/untracked. In this mode, the function * cannot fail (except on hard-failure, see below). * The idea is that you first try without simulate to write to disk. * If that fails, you might still want to forcefully pretend (in-memory * only) that this uuid is marked as tombstone (or not), as desired. * So you repeate the call with @simulate %TRUE. * @uuid: the UUID for which to write/delete the nmmeta file * @in_memory: the storage type, either /etc or /run. Note that if @self * has no /etc directory configured, this results in a hard failure. * @set: if %TRUE, write the symlink to point to /dev/null. If %FALSE, * delete the nmmeta file (if it exists). * @shadowed_storage: a tombstone can also shadow an existing storage. * In combination with @set and @in_memory, this is allowed to store * the shadowed storage filename. * @out_storage: (transfer full) (allow-none): the storage element that changes, or * NULL if nothing changed. Note that the file on disk is already as * we want to write it, then this still counts as a change. No change only * means if we try to delete a storage (@set %FALSE) that did not * exist previously. * @out_hard_failure: (allow-none): on failure, indicate that this is a hard failure. * * The function writes or deletes nmmeta files to/from filesystem. In this case, * the nmmeta files can only be symlinks to /dev/null (to indicate tombstones). * * A hard failure can only happen if @self has no /etc directory configured * and @in_memory is FALSE. In such case even @simulate call fails (which * otherwise would always succeed). * Also, if you get a hard-failure (with @simulate %FALSE) there is no point * in retrying with @simulate %TRUE (contrary to all other cases!). * * Returns: %TRUE on success. */ gboolean nms_keyfile_plugin_set_nmmeta_tombstone(NMSKeyfilePlugin *self, gboolean simulate, const char *uuid, gboolean in_memory, gboolean set, const char *shadowed_storage, NMSettingsStorage **out_storage, gboolean *out_hard_failure) { NMSKeyfilePluginPrivate *priv; gboolean hard_failure = FALSE; NMSKeyfileStorage *storage; gs_unref_object NMSKeyfileStorage *storage_result = NULL; gboolean nmmeta_errno; gs_free char *nmmeta_filename = NULL; NMSKeyfileStorageType storage_type; const char *loaded_path; const char *dirname; nm_assert(NMS_IS_KEYFILE_PLUGIN(self)); nm_assert(nm_uuid_is_normalized(uuid)); nm_assert(!out_storage || !*out_storage); nm_assert(!shadowed_storage || (set && in_memory)); priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); loaded_path = set ? NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL : NULL; if (in_memory) { storage_type = NMS_KEYFILE_STORAGE_TYPE_RUN; dirname = priv->dirname_run; } else { if (!priv->dirname_etc) { _LOGT("commit: cannot %s%s nmmeta file for %s as there is no /etc directory", simulate ? "simulate " : "", loaded_path ? "write" : "delete", uuid); nmmeta_errno = 0; hard_failure = TRUE; goto out; } storage_type = NMS_KEYFILE_STORAGE_TYPE_ETC; dirname = priv->dirname_etc; } if (simulate) { nmmeta_errno = 0; nmmeta_filename = nms_keyfile_nmmeta_filename(dirname, uuid, FALSE); } else { nmmeta_errno = nms_keyfile_nmmeta_write(dirname, uuid, loaded_path, FALSE, shadowed_storage, &nmmeta_filename); } _LOGT("commit: %s nmmeta file \"%s\"%s%s%s%s%s%s %s%s%s%s", loaded_path ? "writing" : "deleting", nmmeta_filename, NM_PRINT_FMT_QUOTED(loaded_path, " (pointing to \"", loaded_path, "\")", ""), NM_PRINT_FMT_QUOTED(shadowed_storage, " (shadows \"", shadowed_storage, "\")", ""), simulate ? "simulated" : (nmmeta_errno < 0 ? "failed" : "succeeded"), NM_PRINT_FMT_QUOTED(nmmeta_errno < 0, " (", nm_strerror_native(nm_errno_native(nmmeta_errno)), ")", "")); if (nmmeta_errno < 0) goto out; storage = nm_sett_util_storages_lookup_by_filename(&priv->storages, nmmeta_filename); nm_assert(!storage || (storage->is_meta_data && storage->storage_type == storage_type && nm_streq(nms_keyfile_storage_get_uuid(storage), uuid))); if (loaded_path) { if (!storage) { storage = nms_keyfile_storage_new_tombstone(self, uuid, nmmeta_filename, storage_type, shadowed_storage); nm_sett_util_storages_add_take(&priv->storages, storage); } else { g_free(storage->u.meta_data.shadowed_storage); storage->u.meta_data.shadowed_storage = g_strdup(shadowed_storage); } storage_result = g_object_ref(storage); } else { if (storage) storage_result = nm_sett_util_storages_steal(&priv->storages, storage); } out: nm_assert(nmmeta_errno <= 0); nm_assert(nmmeta_errno < 0 || !hard_failure); nm_assert(nmmeta_errno == 0 || !storage_result); NM_SET_OUT(out_hard_failure, hard_failure); NM_SET_OUT(out_storage, (NMSettingsStorage *) g_steal_pointer(&storage_result)); return nmmeta_errno >= 0; } /*****************************************************************************/ static void config_changed_cb(NMConfig *config, NMConfigData *config_data, NMConfigChangeFlags changes, NMConfigData *old_data, NMSKeyfilePlugin *self) { gs_free char *old_value = NULL; gs_free char *new_value = NULL; old_value = nm_config_data_get_value(old_data, NM_CONFIG_KEYFILE_GROUP_KEYFILE, NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES, NM_CONFIG_GET_VALUE_TYPE_SPEC); new_value = nm_config_data_get_value(config_data, NM_CONFIG_KEYFILE_GROUP_KEYFILE, NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES, NM_CONFIG_GET_VALUE_TYPE_SPEC); if (!nm_streq0(old_value, new_value)) _nm_settings_plugin_emit_signal_unmanaged_specs_changed(NM_SETTINGS_PLUGIN(self)); } static GSList * get_unmanaged_specs(NMSettingsPlugin *config) { NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(config); gs_free char *value = NULL; value = nm_config_data_get_value(nm_config_get_data(priv->config), NM_CONFIG_KEYFILE_GROUP_KEYFILE, NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES, NM_CONFIG_GET_VALUE_TYPE_SPEC); return nm_match_spec_split(value); } /*****************************************************************************/ static void nms_keyfile_plugin_init(NMSKeyfilePlugin *plugin) { NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin); priv->config = g_object_ref(nm_config_get()); priv->storages = (NMSettUtilStorages) NM_SETT_UTIL_STORAGES_INIT(priv->storages, nms_keyfile_storage_destroy); /* dirname_libs are a set of read-only directories with lower priority than /etc or /run. * There is nothing complicated about having multiple of such directories, so dirname_libs * is a list (which currently only has at most one directory). */ priv->dirname_libs[0] = nm_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_LIB)); priv->dirname_libs[1] = NULL; priv->dirname_run = nm_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_RUN)); priv->dirname_etc = nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG, NM_CONFIG_KEYFILE_GROUP_KEYFILE, NM_CONFIG_KEYFILE_KEY_KEYFILE_PATH, NM_CONFIG_GET_VALUE_STRIP); if (priv->dirname_etc && priv->dirname_etc[0] == '\0') { /* special case: configure an empty keyfile path so that NM has no writable keyfile * directory. In this case, NM will only honor dirname_libs and dirname_run, meaning * it cannot persist profile to non-volatile memory. */ nm_clear_g_free(&priv->dirname_etc); } else if (!priv->dirname_etc || priv->dirname_etc[0] != '/') { /* either invalid path or unspecified. Use the default. */ g_free(priv->dirname_etc); priv->dirname_etc = nm_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_ETC_DEFAULT)); } else nm_path_simplify(priv->dirname_etc); /* no duplicates */ if (NM_IN_STRSET(priv->dirname_libs[0], priv->dirname_etc, priv->dirname_run)) nm_clear_g_free(&priv->dirname_libs[0]); if (NM_IN_STRSET(priv->dirname_etc, priv->dirname_run)) nm_clear_g_free(&priv->dirname_etc); nm_assert(!priv->dirname_libs[0] || priv->dirname_libs[0][0] == '/'); nm_assert(!priv->dirname_etc || priv->dirname_etc[0] == '/'); nm_assert(priv->dirname_run && priv->dirname_run[0] == '/'); } static void constructed(GObject *object) { NMSKeyfilePlugin *self = NMS_KEYFILE_PLUGIN(object); NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); G_OBJECT_CLASS(nms_keyfile_plugin_parent_class)->constructed(object); if (nm_config_data_has_value(nm_config_get_data_orig(priv->config), NM_CONFIG_KEYFILE_GROUP_KEYFILE, NM_CONFIG_KEYFILE_KEY_KEYFILE_HOSTNAME, NM_CONFIG_GET_VALUE_RAW)) _LOGW("'hostname' option is deprecated and has no effect"); if (nm_config_data_has_value(nm_config_get_data_orig(priv->config), NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_MONITOR_CONNECTION_FILES, NM_CONFIG_GET_VALUE_RAW)) _LOGW("'monitor-connection-files' option is deprecated and has no effect"); g_signal_connect(G_OBJECT(priv->config), NM_CONFIG_SIGNAL_CONFIG_CHANGED, G_CALLBACK(config_changed_cb), self); } NMSKeyfilePlugin * nms_keyfile_plugin_new(void) { return g_object_new(NMS_TYPE_KEYFILE_PLUGIN, NULL); } static void dispose(GObject *object) { NMSKeyfilePlugin *self = NMS_KEYFILE_PLUGIN(object); NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self); if (priv->config) g_signal_handlers_disconnect_by_func(priv->config, config_changed_cb, object); nm_sett_util_storages_clear(&priv->storages); nm_clear_g_free(&priv->dirname_libs[0]); nm_clear_g_free(&priv->dirname_etc); nm_clear_g_free(&priv->dirname_run); g_clear_object(&priv->config); G_OBJECT_CLASS(nms_keyfile_plugin_parent_class)->dispose(object); } static void nms_keyfile_plugin_class_init(NMSKeyfilePluginClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); NMSettingsPluginClass *plugin_class = NM_SETTINGS_PLUGIN_CLASS(klass); object_class->constructed = constructed; object_class->dispose = dispose; plugin_class->plugin_name = "keyfile"; plugin_class->get_unmanaged_specs = get_unmanaged_specs; plugin_class->reload_connections = reload_connections; plugin_class->load_connections = load_connections; plugin_class->add_connection = add_connection; plugin_class->update_connection = update_connection; plugin_class->delete_connection = delete_connection; }