diff --git a/src/core/nm-firewall-utils.c b/src/core/nm-firewall-utils.c index fdefbf5ea1..22d52031a0 100644 --- a/src/core/nm-firewall-utils.c +++ b/src/core/nm-firewall-utils.c @@ -9,9 +9,11 @@ #include "nm-firewall-utils.h" #include "libnm-glib-aux/nm-str-buf.h" +#include "libnm-glib-aux/nm-io-utils.h" #include "libnm-platform/nm-platform.h" #include "nm-config.h" +#include "NetworkManagerUtils.h" /*****************************************************************************/ @@ -349,6 +351,331 @@ _share_iptables_set_shared(gboolean add, const char *ip_iface, in_addr_t addr, g /*****************************************************************************/ +typedef struct { + GTask * task; + GSubprocess * subprocess; + GSource * timeout_source; + GCancellable *intern_cancellable; + char * identifier; + gulong cancellable_id; +} FwNftCallData; + +static void +_fw_nft_call_data_free(FwNftCallData *call_data, GError *error_take) +{ + nm_clear_g_signal_handler(g_task_get_cancellable(call_data->task), &call_data->cancellable_id); + nm_clear_g_cancellable(&call_data->intern_cancellable); + nm_clear_g_source_inst(&call_data->timeout_source); + + if (error_take) + g_task_return_error(call_data->task, g_steal_pointer(&error_take)); + else + g_task_return_boolean(call_data->task, TRUE); + + g_object_unref(call_data->task); + nm_g_object_unref(call_data->subprocess); + g_free(call_data->identifier); + + nm_g_slice_free(call_data); +} + +static void +_fw_nft_call_communicate_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + FwNftCallData *call_data = user_data; + gs_free_error GError *error = NULL; + gs_unref_bytes GBytes *stdout_buf = NULL; + gs_unref_bytes GBytes *stderr_buf = NULL; + + nm_assert(source == (gpointer) call_data->subprocess); + + if (!g_subprocess_communicate_finish(G_SUBPROCESS(source), + result, + &stdout_buf, + &stderr_buf, + &error)) { + /* on any error, the process might still be running. We need to abort it in + * the background... */ + if (nm_utils_error_is_cancelled(error)) { + nm_log_dbg(LOGD_SHARING, + "firewall: ntf[%s]: communication cancelled. Kill process", + call_data->identifier); + } else { + nm_log_dbg(LOGD_SHARING, + "firewall: nft[%s]: communication failed: %s. Kill process", + call_data->identifier, + error->message); + } + + { + _nm_unused nm_auto_pop_gmaincontext GMainContext *main_context = + nm_g_main_context_push_thread_default(NULL); + + nm_shutdown_wait_obj_register_object(call_data->subprocess, "nft-terminate"); + G_STATIC_ASSERT_EXPR(200 < NM_SHUTDOWN_TIMEOUT_MS_WATCHDOG * 2 / 3); + nm_g_subprocess_terminate_in_background(call_data->subprocess, 200); + } + } else if (g_subprocess_get_successful(call_data->subprocess)) { + nm_log_dbg(LOGD_SHARING, "firewall: nft[%s]: command successful", call_data->identifier); + } else { + gs_free char *ss_stdout = NULL; + gs_free char *ss_stderr = NULL; + gboolean print_stdout = (stdout_buf && g_bytes_get_size(stdout_buf) > 0); + gboolean print_stderr = (stderr_buf && g_bytes_get_size(stderr_buf) > 0); + + nm_log_warn(LOGD_SHARING, + "firewall: nft[%s]: command failed:%s%s%s%s%s%s%s", + call_data->identifier, + print_stdout || print_stderr ? "" : " unknown reason", + NM_PRINT_FMT_QUOTED( + print_stdout, + " (stdout: \"", + nm_utils_buf_utf8safe_escape_bytes(stdout_buf, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, + &ss_stdout), + "\")", + ""), + NM_PRINT_FMT_QUOTED( + print_stderr, + " (stderr: \"", + nm_utils_buf_utf8safe_escape_bytes(stderr_buf, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, + &ss_stderr), + "\")", + "")); + } + + _fw_nft_call_data_free(call_data, error); +} + +static void +_fw_nft_call_cancelled_cb(GCancellable *cancellable, gpointer user_data) +{ + FwNftCallData *call_data = user_data; + + if (call_data->cancellable_id == 0) + return; + + nm_log_dbg(LOGD_SHARING, "firewall: nft[%s]: operation cancelled", call_data->identifier); + + nm_clear_g_signal_handler(g_task_get_cancellable(call_data->task), &call_data->cancellable_id); + nm_clear_g_cancellable(&call_data->intern_cancellable); +} + +static gboolean +_fw_nft_call_timeout_cb(gpointer user_data) +{ + FwNftCallData *call_data = user_data; + + nm_clear_g_source_inst(&call_data->timeout_source); + nm_log_dbg(LOGD_SHARING, + "firewall: nft[%s]: cancel operation after timeout", + call_data->identifier); + + nm_clear_g_cancellable(&call_data->intern_cancellable); + return G_SOURCE_CONTINUE; +} + +static void +_fw_nft_call(GBytes * stdin_buf, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer callback_user_data) +{ + gs_unref_object GSubprocessLauncher *subprocess_launcher = NULL; + gs_free_error GError *error = NULL; + FwNftCallData * call_data; + + call_data = g_slice_new(FwNftCallData); + *call_data = (FwNftCallData){ + .task = nm_g_task_new(NULL, cancellable, _fw_nft_call, callback, callback_user_data), + .subprocess = NULL, + .timeout_source = NULL, + }; + + if (cancellable) { + call_data->cancellable_id = g_cancellable_connect(cancellable, + G_CALLBACK(_fw_nft_call_cancelled_cb), + call_data, + NULL); + if (call_data->cancellable_id == 0) { + nm_log_dbg(LOGD_SHARING, "firewall: nft: already cancelled"); + nm_utils_error_set_cancelled(&error, FALSE, NULL); + _fw_nft_call_data_free(call_data, g_steal_pointer(&error)); + return; + } + } + + subprocess_launcher = + g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE + | G_SUBPROCESS_FLAGS_STDERR_PIPE); + g_subprocess_launcher_set_environ(subprocess_launcher, NM_STRV_EMPTY()); + + call_data->subprocess = g_subprocess_launcher_spawnv(subprocess_launcher, + NM_MAKE_STRV(NFT_PATH, "-f", "-"), + &error); + + if (!call_data->subprocess) { + nm_log_dbg(LOGD_SHARING, "firewall: nft: spawning nft failed: %s", error->message); + _fw_nft_call_data_free(call_data, g_steal_pointer(&error)); + return; + } + + call_data->identifier = g_strdup(g_subprocess_get_identifier(call_data->subprocess)); + + nm_log_dbg(LOGD_SHARING, "firewall: nft[%s]: communicate with nft", call_data->identifier); + + nm_shutdown_wait_obj_register_object(call_data->task, "nft-call"); + + call_data->intern_cancellable = g_cancellable_new(), + + g_subprocess_communicate_async(call_data->subprocess, + stdin_buf, + call_data->intern_cancellable, + _fw_nft_call_communicate_cb, + call_data); + + call_data->timeout_source = + nm_g_source_attach(nm_g_timeout_source_new((NM_SHUTDOWN_TIMEOUT_MS * 2) / 3, + G_PRIORITY_DEFAULT, + _fw_nft_call_timeout_cb, + call_data, + NULL), + g_task_get_context(call_data->task)); +} + +static gboolean +_fw_nft_call_finish(GAsyncResult *result, GError **error) +{ + g_return_val_if_fail(nm_g_task_is_valid(result, NULL, _fw_nft_call), FALSE); + + return g_task_propagate_boolean(G_TASK(result), error); +} + +/*****************************************************************************/ + +typedef struct { + GMainLoop *loop; + GError ** error; + gboolean success; +} FwNftCallSyncData; + +static void +_fw_nft_call_sync_done(GObject *source, GAsyncResult *result, gpointer user_data) +{ + FwNftCallSyncData *data = user_data; + + data->success = _fw_nft_call_finish(result, data->error); + g_main_loop_quit(data->loop); +} + +static gboolean +_fw_nft_call_sync(GBytes *stdin_buf, GError **error) +{ + nm_auto_pop_and_unref_gmaincontext GMainContext *main_context = + nm_g_main_context_push_thread_default(g_main_context_new()); + nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(main_context, FALSE); + FwNftCallSyncData data = (FwNftCallSyncData){ + .loop = main_loop, + .error = error, + }; + + _fw_nft_call(stdin_buf, NULL, _fw_nft_call_sync_done, &data); + + g_main_loop_run(main_loop); + return data.success; +} + +/*****************************************************************************/ + +static void +_fw_nft_set(gboolean add, const char *ip_iface, in_addr_t addr, guint8 plen) +{ + nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE); + gs_unref_bytes GBytes *stdin_buf = NULL; + gs_free char * table_name = NULL; + gs_free char * ss1 = NULL; + char str_subnet[_SHARE_IPTABLES_SUBNET_TO_STR_LEN]; + + table_name = _share_iptables_get_name(FALSE, "nm-shared", ip_iface); + + _share_iptables_subnet_to_str(str_subnet, addr, plen); + +#define _append(p_strbuf, fmt, ...) nm_str_buf_append_printf((p_strbuf), "" fmt "\n", ##__VA_ARGS__) + + _append(&strbuf, "add table inet %s", table_name); + _append(&strbuf, "%s table inet %s", add ? "flush" : "delete", table_name); + + if (add) { + _append(&strbuf, + "add chain inet %s nat_postrouting {" + " type nat hook postrouting priority 100; policy accept; " + "};", + table_name); + _append(&strbuf, + "add rule inet %s nat_postrouting ip saddr %s ip daddr != %s masquerade;", + table_name, + str_subnet, + str_subnet); + + /* This filter_input chain serves no real purpose, because "accept" only stops + * evaluation of the current rule. It cannot fully accept the packet. Since + * this chain has no other rules, it is useless in this form. + */ + /* + _append(&strbuf, + "add chain inet %s filter_input {" + " type filter hook input priority 0; policy accept; " + "};", + table_name); + _append(&strbuf, "add rule inet %s filter_input tcp dport { 67, 53 } accept;", table_name); + _append(&strbuf, "add rule inet %s filter_input udp dport { 67, 53 } accept;", table_name); + */ + + _append(&strbuf, + "add chain inet %s filter_forward {" + " type filter hook forward priority 0; policy accept; " + "};", + table_name); + _append(&strbuf, + "add rule inet %s filter_forward ip daddr %s oifname \"%s\" " + " ct state { established, related } accept;", + table_name, + str_subnet, + ip_iface); + _append(&strbuf, + "add rule inet %s filter_forward ip saddr %s iifname \"%s\" accept;", + table_name, + str_subnet, + ip_iface); + _append(&strbuf, + "add rule inet %s filter_forward iifname \"%s\" oifname \"%s\" accept;", + table_name, + ip_iface, + ip_iface); + _append(&strbuf, + "add rule inet %s filter_forward iifname \"%s\" reject;", + table_name, + ip_iface); + _append(&strbuf, + "add rule inet %s filter_forward oifname \"%s\" reject;", + table_name, + ip_iface); + } + + nm_log_trace(LOGD_SHARING, + "firewall: nft command: [ %s ]", + nm_utils_str_utf8safe_escape(nm_str_buf_get_str(&strbuf), + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, + &ss1)); + + stdin_buf = g_bytes_new_static(nm_str_buf_get_str(&strbuf), strbuf.len); + + _fw_nft_call_sync(stdin_buf, NULL); +} + +/*****************************************************************************/ + struct _NMFirewallConfig { char * ip_iface; in_addr_t addr; @@ -386,8 +713,18 @@ nm_firewall_config_free(NMFirewallConfig *self) void nm_firewall_config_apply(NMFirewallConfig *self, gboolean shared) { - _share_iptables_set_masquerade(shared, self->ip_iface, self->addr, self->plen); - _share_iptables_set_shared(shared, self->ip_iface, self->addr, self->plen); + switch (nm_firewall_utils_get_backend()) { + case NM_FIREWALL_BACKEND_IPTABLES: + _share_iptables_set_masquerade(shared, self->ip_iface, self->addr, self->plen); + _share_iptables_set_shared(shared, self->ip_iface, self->addr, self->plen); + break; + case NM_FIREWALL_BACKEND_NFTABLES: + _fw_nft_set(shared, self->ip_iface, self->addr, self->plen); + break; + default: + nm_assert_not_reached(); + break; + } } /*****************************************************************************/ @@ -437,15 +774,6 @@ again: nm_assert(NM_IN_SET(b, NM_FIREWALL_BACKEND_IPTABLES, NM_FIREWALL_BACKEND_NFTABLES)); - if (b == NM_FIREWALL_BACKEND_NFTABLES) { - if (!detect) - nm_log_warn(LOGD_SHARING, - "firewall: backend \"nftables\" is not yet implemented. Fallback to " - "\"iptables\""); - nm_clear_g_free(&conf_value); - b = NM_FIREWALL_BACKEND_IPTABLES; - } - if (!g_atomic_int_compare_and_exchange(&backend, NM_FIREWALL_BACKEND_UNKNOWN, b)) goto again;