/* SPDX-License-Identifier: GPL-2.0+ */ /* * Copyright (C) 2011 - 2015 Red Hat, Inc. */ #include "nm-default.h" #include "nm-firewall-manager.h" #include "nm-glib-aux/nm-dbus-aux.h" #include "c-list/src/c-list.h" #include "NetworkManagerUtils.h" #include "nm-dbus-manager.h" #define FIREWALL_DBUS_SERVICE "org.fedoraproject.FirewallD1" #define FIREWALL_DBUS_PATH "/org/fedoraproject/FirewallD1" #define FIREWALL_DBUS_INTERFACE_ZONE "org.fedoraproject.FirewallD1.zone" /*****************************************************************************/ enum { STATE_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = {0}; typedef struct { GDBusConnection *dbus_connection; GCancellable *get_name_owner_cancellable; CList pending_calls; guint name_owner_changed_id; bool dbus_inited : 1; bool running : 1; } NMFirewallManagerPrivate; struct _NMFirewallManager { GObject parent; NMFirewallManagerPrivate _priv; }; struct _NMFirewallManagerClass { GObjectClass parent; }; G_DEFINE_TYPE(NMFirewallManager, nm_firewall_manager, G_TYPE_OBJECT) #define NM_FIREWALL_MANAGER_GET_PRIVATE(self) \ _NM_GET_PRIVATE(self, NMFirewallManager, NM_IS_FIREWALL_MANAGER) /*****************************************************************************/ NM_DEFINE_SINGLETON_GETTER(NMFirewallManager, nm_firewall_manager_get, NM_TYPE_FIREWALL_MANAGER); /*****************************************************************************/ typedef enum { OPS_TYPE_ADD = 1, OPS_TYPE_CHANGE, OPS_TYPE_REMOVE, } OpsType; struct _NMFirewallManagerCallId { CList lst; NMFirewallManager *self; char *iface; NMFirewallManagerAddRemoveCallback callback; gpointer user_data; union { struct { GCancellable *cancellable; GVariant * arg; } dbus; struct { guint id; } idle; }; OpsType ops_type; bool is_idle : 1; }; /*****************************************************************************/ static const char * _ops_type_to_string(OpsType ops_type) { switch (ops_type) { case OPS_TYPE_ADD: return "add"; case OPS_TYPE_REMOVE: return "remove"; case OPS_TYPE_CHANGE: return "change"; } nm_assert_not_reached(); return NULL; } #define _NMLOG_DOMAIN LOGD_FIREWALL #define _NMLOG_PREFIX_NAME "firewall" #define _NMLOG(level, call_id, ...) \ G_STMT_START \ { \ if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) { \ NMFirewallManagerCallId *_call_id = (call_id); \ char _prefix_name[30]; \ char _prefix_info[100]; \ \ _nm_log((level), \ (_NMLOG_DOMAIN), \ 0, \ NULL, \ NULL, \ "%s: %s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ (self) != singleton_instance ? ({ \ g_snprintf(_prefix_name, \ sizeof(_prefix_name), \ "%s[" NM_HASH_OBFUSCATE_PTR_FMT "]", \ ""_NMLOG_PREFIX_NAME, \ NM_HASH_OBFUSCATE_PTR(self)); \ _prefix_name; \ }) \ : _NMLOG_PREFIX_NAME, \ _call_id ? ({ \ g_snprintf(_prefix_info, \ sizeof(_prefix_info), \ "[" NM_HASH_OBFUSCATE_PTR_FMT ",%s%s:%s%s%s]: ", \ NM_HASH_OBFUSCATE_PTR(_call_id), \ _ops_type_to_string(_call_id->ops_type), \ _call_id->is_idle ? "*" : "", \ NM_PRINT_FMT_QUOTE_STRING(_call_id->iface)); \ _prefix_info; \ }) \ : "" _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ } \ } \ G_STMT_END /*****************************************************************************/ static gboolean _get_running(NMFirewallManagerPrivate *priv) { /* when starting, we need to asynchronously check whether there is * a name owner. During that time we optimistically assume that the * service is indeed running. That is the time when we queue the * requests, and they will be started once the get-name-owner call * returns. */ return priv->running || (priv->dbus_connection && !priv->dbus_inited); } gboolean nm_firewall_manager_get_running(NMFirewallManager *self) { g_return_val_if_fail(NM_IS_FIREWALL_MANAGER(self), FALSE); return _get_running(NM_FIREWALL_MANAGER_GET_PRIVATE(self)); } /*****************************************************************************/ static NMFirewallManagerCallId * _cb_info_create(NMFirewallManager * self, OpsType ops_type, const char * iface, const char * zone, NMFirewallManagerAddRemoveCallback callback, gpointer user_data) { NMFirewallManagerPrivate *priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); NMFirewallManagerCallId * call_id; call_id = g_slice_new0(NMFirewallManagerCallId); call_id->self = g_object_ref(self); call_id->ops_type = ops_type; call_id->iface = g_strdup(iface); call_id->callback = callback; call_id->user_data = user_data; if (_get_running(priv)) { call_id->is_idle = FALSE; call_id->dbus.arg = g_variant_new("(ss)", zone ?: "", iface); } else call_id->is_idle = TRUE; c_list_link_tail(&priv->pending_calls, &call_id->lst); return call_id; } static void _cb_info_complete(NMFirewallManagerCallId *call_id, GError *error) { c_list_unlink(&call_id->lst); if (call_id->callback) call_id->callback(call_id->self, call_id, error, call_id->user_data); if (call_id->is_idle) nm_clear_g_source(&call_id->idle.id); else { nm_g_variant_unref(call_id->dbus.arg); nm_clear_g_cancellable(&call_id->dbus.cancellable); } g_free(call_id->iface); g_object_unref(call_id->self); nm_g_slice_free(call_id); } static gboolean _handle_idle_cb(gpointer user_data) { NMFirewallManager * self; NMFirewallManagerCallId *call_id = user_data; nm_assert(call_id); nm_assert(NM_IS_FIREWALL_MANAGER(call_id->self)); nm_assert(call_id->is_idle); nm_assert(c_list_contains(&NM_FIREWALL_MANAGER_GET_PRIVATE(call_id->self)->pending_calls, &call_id->lst)); self = call_id->self; _LOGD(call_id, "complete: fake success"); call_id->idle.id = 0; _cb_info_complete(call_id, NULL); return G_SOURCE_REMOVE; } static gboolean _handle_idle_start(NMFirewallManager *self, NMFirewallManagerCallId *call_id) { if (!call_id->callback) { /* if the user did not provide a callback and firewalld is not running, * there is no point in scheduling an idle-request to fake success. Just * return right away. */ _LOGD(call_id, "complete: drop request simulating success"); _cb_info_complete(call_id, NULL); return FALSE; } call_id->idle.id = g_idle_add(_handle_idle_cb, call_id); return TRUE; } static void _handle_dbus_cb(GObject *source, GAsyncResult *result, gpointer user_data) { NMFirewallManager * self; NMFirewallManagerCallId *call_id; gs_free_error GError *error = NULL; gs_unref_variant GVariant *ret = NULL; ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error); if (!ret && nm_utils_error_is_cancelled(error)) return; call_id = user_data; nm_assert(call_id); nm_assert(NM_IS_FIREWALL_MANAGER(call_id->self)); nm_assert(!call_id->is_idle); nm_assert(c_list_contains(&NM_FIREWALL_MANAGER_GET_PRIVATE(call_id->self)->pending_calls, &call_id->lst)); self = call_id->self; if (error) { const char *non_error = NULL; g_dbus_error_strip_remote_error(error); switch (call_id->ops_type) { case OPS_TYPE_ADD: case OPS_TYPE_CHANGE: non_error = "ZONE_ALREADY_SET"; break; case OPS_TYPE_REMOVE: non_error = "UNKNOWN_INTERFACE"; break; } if (error->message && non_error && g_str_has_prefix(error->message, non_error) && NM_IN_SET(error->message[strlen(non_error)], '\0', ':')) { _LOGD(call_id, "complete: request failed with a non-error (%s)", error->message); /* The operation failed with an error reason that we don't want * to propagate. Instead, signal success. */ g_clear_error(&error); } else _LOGW(call_id, "complete: request failed (%s)", error->message); } else _LOGD(call_id, "complete: success"); g_clear_object(&call_id->dbus.cancellable); _cb_info_complete(call_id, error); } static void _handle_dbus_start(NMFirewallManager *self, NMFirewallManagerCallId *call_id) { NMFirewallManagerPrivate *priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); const char * dbus_method = NULL; GVariant * arg; nm_assert(call_id); nm_assert(priv->running); nm_assert(!call_id->is_idle); nm_assert(c_list_contains(&priv->pending_calls, &call_id->lst)); switch (call_id->ops_type) { case OPS_TYPE_ADD: dbus_method = "addInterface"; break; case OPS_TYPE_CHANGE: dbus_method = "changeZone"; break; case OPS_TYPE_REMOVE: dbus_method = "removeInterface"; break; } nm_assert(dbus_method); arg = g_steal_pointer(&call_id->dbus.arg); nm_assert(arg && g_variant_is_floating(arg)); nm_assert(!call_id->dbus.cancellable); call_id->dbus.cancellable = g_cancellable_new(); g_dbus_connection_call(priv->dbus_connection, FIREWALL_DBUS_SERVICE, FIREWALL_DBUS_PATH, FIREWALL_DBUS_INTERFACE_ZONE, dbus_method, arg, NULL, G_DBUS_CALL_FLAGS_NONE, 10000, call_id->dbus.cancellable, _handle_dbus_cb, call_id); } static NMFirewallManagerCallId * _start_request(NMFirewallManager * self, OpsType ops_type, const char * iface, const char * zone, NMFirewallManagerAddRemoveCallback callback, gpointer user_data) { NMFirewallManagerPrivate *priv; NMFirewallManagerCallId * call_id; g_return_val_if_fail(NM_IS_FIREWALL_MANAGER(self), NULL); g_return_val_if_fail(iface && *iface, NULL); priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); call_id = _cb_info_create(self, ops_type, iface, zone, callback, user_data); _LOGD(call_id, "firewall zone %s %s:%s%s%s%s", _ops_type_to_string(call_id->ops_type), iface, NM_PRINT_FMT_QUOTED(zone, "\"", zone, "\"", "default"), call_id->is_idle ? " (not running, simulate success)" : (!priv->running ? " (waiting to initialize)" : "")); if (!call_id->is_idle) { if (priv->running) _handle_dbus_start(self, call_id); if (!call_id->callback) { /* if the user did not provide a callback, the call_id is useless. * Especially, the user cannot use the call-id to cancel the request, * because he cannot know whether the request is still pending. * * Hence, returning %NULL doesn't mean that the request could not be started * (this function never fails and always starts a request). */ return NULL; } } else { if (!_handle_idle_start(self, call_id)) { /* if the user did not provide a callback and firewalld is not running, * there is no point in scheduling an idle-request to fake success. Just * return right away. */ return NULL; } } return call_id; } NMFirewallManagerCallId * nm_firewall_manager_add_or_change_zone(NMFirewallManager *self, const char * iface, const char * zone, gboolean add, /* TRUE == add, FALSE == change */ NMFirewallManagerAddRemoveCallback callback, gpointer user_data) { return _start_request(self, add ? OPS_TYPE_ADD : OPS_TYPE_CHANGE, iface, zone, callback, user_data); } NMFirewallManagerCallId * nm_firewall_manager_remove_from_zone(NMFirewallManager * self, const char * iface, const char * zone, NMFirewallManagerAddRemoveCallback callback, gpointer user_data) { return _start_request(self, OPS_TYPE_REMOVE, iface, zone, callback, user_data); } void nm_firewall_manager_cancel_call(NMFirewallManagerCallId *call_id) { NMFirewallManager * self; NMFirewallManagerPrivate *priv; gs_free_error GError *error = NULL; g_return_if_fail(call_id); g_return_if_fail(NM_IS_FIREWALL_MANAGER(call_id->self)); g_return_if_fail(!c_list_is_empty(&call_id->lst)); self = call_id->self; priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); nm_assert(c_list_contains(&priv->pending_calls, &call_id->lst)); nm_utils_error_set_cancelled(&error, FALSE, "NMFirewallManager"); _LOGD(call_id, "complete: cancel (%s)", error->message); _cb_info_complete(call_id, error); } /*****************************************************************************/ static void name_owner_changed(NMFirewallManager *self, const char *owner) { _nm_unused gs_unref_object NMFirewallManager *self_keep_alive = g_object_ref(self); NMFirewallManagerPrivate * priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); gboolean was_running; gboolean now_running; gboolean just_initied; owner = nm_str_not_empty(owner); if (!owner) _LOGT(NULL, "D-Bus name for firewalld has no owner (firewall stopped)"); else _LOGT(NULL, "D-Bus name for firewalld has owner %s (firewall started)", owner); was_running = _get_running(priv); just_initied = !priv->dbus_inited; priv->dbus_inited = TRUE; priv->running = !!owner; now_running = _get_running(priv); if (just_initied) { NMFirewallManagerCallId *call_id_safe; NMFirewallManagerCallId *call_id; /* We kick of the requests that we have pending. Note that this is * entirely asynchronous and also we don't invoke any callbacks for * the user. * Even _handle_idle_start() just schedules an idle handler. That is, * because we don't want to callback to the user before emitting the * DISCONNECTED signal below. Also, emitting callbacks means the user * can call back to modify the list of pending-calls and we'd have * to handle reentrancy. */ c_list_for_each_entry_safe (call_id, call_id_safe, &priv->pending_calls, lst) { nm_assert(!call_id->is_idle); nm_assert(call_id->dbus.arg); if (priv->running) { _LOGD(call_id, "initalizing: make D-Bus call"); _handle_dbus_start(self, call_id); } else { /* we don't want to invoke callbacks to the user right away. That is because * the user might schedule/cancel more calls, which messes up the order. * * Instead, convert the pending calls to idle requests... */ nm_clear_pointer(&call_id->dbus.arg, g_variant_unref); call_id->is_idle = TRUE; _LOGD(call_id, "initializing: fake success on idle"); _handle_idle_start(self, call_id); } } } if (was_running != now_running) g_signal_emit(self, signals[STATE_CHANGED], 0, FALSE); } 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) { NMFirewallManager *self = user_data; const char * new_owner; if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) return; g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); name_owner_changed(self, new_owner); } static void get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data) { NMFirewallManager * self; NMFirewallManagerPrivate *priv; if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; self = user_data; priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); g_clear_object(&priv->get_name_owner_cancellable); name_owner_changed(self, name_owner); } /*****************************************************************************/ static void nm_firewall_manager_init(NMFirewallManager *self) { NMFirewallManagerPrivate *priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); c_list_init(&priv->pending_calls); priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET); if (!priv->dbus_connection) { _LOGD(NULL, "no D-Bus connection"); return; } priv->name_owner_changed_id = nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection, FIREWALL_DBUS_SERVICE, name_owner_changed_cb, self, NULL); priv->get_name_owner_cancellable = g_cancellable_new(); nm_dbus_connection_call_get_name_owner(priv->dbus_connection, FIREWALL_DBUS_SERVICE, -1, priv->get_name_owner_cancellable, get_name_owner_cb, self); } static void dispose(GObject *object) { NMFirewallManager * self = NM_FIREWALL_MANAGER(object); NMFirewallManagerPrivate *priv = NM_FIREWALL_MANAGER_GET_PRIVATE(self); /* as every pending operation takes a reference to the manager, * we don't expect pending operations at this point. */ nm_assert(c_list_is_empty(&priv->pending_calls)); nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id); nm_clear_g_cancellable(&priv->get_name_owner_cancellable); G_OBJECT_CLASS(nm_firewall_manager_parent_class)->dispose(object); g_clear_object(&priv->dbus_connection); } static void nm_firewall_manager_class_init(NMFirewallManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = dispose; signals[STATE_CHANGED] = g_signal_new(NM_FIREWALL_MANAGER_STATE_CHANGED, G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, G_TYPE_BOOLEAN /* initialized_now */); }