firewall: implement masquerading for shared mode with nftables
Add support for nftables, as a second backend beside iptables (firewalld still missing). Like iptables, choose to call the `nft` tool. The alternative would be to use libnftables or talk netlink. It's ugly to blocking wait for a process to complete. We already do that for iptables, but we better should not because we should not treat other processes as trusted and not allow untrusted code to block NetworkManager. Fixing that would require a central manager that serializes all requests. Especially with firewalld support, this will be interesting again, because we don't want to synchronously talk D-Bus either. For now, `nft` is still called synchronously. However, the internal implementation uses an asynchronous function. That currently serves no purpose except supporting a timeout. Otherwise, the only reason why this is asynchronous is that I implemented this first, and I think in the future we want this code to be non-blocking. So, instead of dropping the asynchronous code, I wrap it in a synchronous function for now. The configured nft table is: table inet nm-shared-eth0 { chain nat_postrouting { type nat hook postrouting priority srcnat; policy accept; ip saddr 192.168.42.0/24 ip daddr != 192.168.42.0/24 masquerade } chain filter_forward { type filter hook forward priority filter; policy accept; ip daddr 192.168.42.0/24 oifname "eth0" ct state { established, related } accept ip saddr 192.168.42.0/24 iifname "eth0" accept iifname "eth0" oifname "eth0" accept iifname "eth0" reject oifname "eth0" reject } }
This commit is contained in:
@@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user