/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2004 - 2016 Red Hat, Inc.
 * Copyright (C) 2005 - 2008 Novell, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-firewall-utils.h"

#include "libnm-core-aux-intern/nm-libnm-core-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"

/*****************************************************************************/

static const struct {
    const char *name;
    const char *path;
} FirewallBackends[] = {
    [NM_FIREWALL_BACKEND_NONE - 1] =
        {
            .name = "none",
        },
    [NM_FIREWALL_BACKEND_NFTABLES - 1] =
        {
            .name = "nftables",
            .path = NFT_PATH,
        },
    [NM_FIREWALL_BACKEND_IPTABLES - 1] =
        {
            .name = "iptables",
            .path = IPTABLES_PATH,
        },
};

/*****************************************************************************/

static const char *
_nft_ifname_valid(const char *str)
{
    gsize i;

    /* `nft -f -` takes certain strings, like "device $IFNAME", but
     * those strings are from a limited character set. Check that
     * @str is valid according to those rules.
     *
     * src/scanner.l:
     *   digit   [0-9]
     *   letter  [a-zA-Z]
     *   string  ({letter}|[_.])({letter}|{digit}|[/\-_\.])*
     **/

    if (!str || !str[0])
        return NULL;

    for (i = 0; str[i]; i++) {
        switch (str[i]) {
        case 'a' ... 'z':
        case 'A' ... 'Z':
        case '_':
        case '.':
            continue;
        case '0' ... '9':
        case '/':
        case '-':
            if (i == 0)
                return NULL;
            continue;
        default:
            return NULL;
        }
    }
    if (i >= NM_IFNAMSIZ)
        return NULL;

    return str;
}

static const char *
_strbuf_set_sanitized(NMStrBuf *strbuf, const char *prefix, const char *str_to_sanitize)
{
    nm_str_buf_reset(strbuf);

    if (prefix)
        nm_str_buf_append(strbuf, prefix);

    for (; str_to_sanitize[0] != '\0'; str_to_sanitize++) {
        const char ch = str_to_sanitize[0];

        if (g_ascii_isalpha(ch) || g_ascii_isdigit(ch)) {
            nm_str_buf_append_c(strbuf, ch);
            continue;
        }
        nm_str_buf_append_c(strbuf, '_');
        nm_str_buf_append_c_hex(strbuf, ch, FALSE);
    }

    return nm_str_buf_get_str(strbuf);
}

/*****************************************************************************/

#define _SHARE_IPTABLES_SUBNET_TO_STR_LEN (INET_ADDRSTRLEN + 1 + 2 + 1)

static const char *
_share_iptables_subnet_to_str(char      buf[static _SHARE_IPTABLES_SUBNET_TO_STR_LEN],
                              in_addr_t addr,
                              guint8    plen)
{
    char      buf_addr[INET_ADDRSTRLEN];
    in_addr_t netmask;
    int       l;

    netmask = nm_ip4_addr_netmask_from_prefix(plen);

    l = g_snprintf(buf,
                   _SHARE_IPTABLES_SUBNET_TO_STR_LEN,
                   "%s/%u",
                   nm_inet4_ntop(addr & netmask, buf_addr),
                   plen);
    nm_assert(l < _SHARE_IPTABLES_SUBNET_TO_STR_LEN);
    return buf;
}

static char *
_iptables_get_name(gboolean is_iptables_chain, const char *prefix, const char *ip_iface)
{
    NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_40, FALSE);
    gsize    ip_iface_len;

    nm_assert(prefix);
    nm_assert(ip_iface);

    /* This function is used to generate iptables chain names and comments.
     * Chain names must be shorter than 29 chars. Comments don't have this
     * limitation.
     *
     * Below we sanitize the ip_iface. If it's all benign, we use
     * - either "-$IP_IFACE" (at most 16 chars)
     * - otherwise, we base64 encode the name as "$(base64 $IP_IFACE)", at
     *   most 20 chars.
     *
     * Since for benign names we already add a '-', prefix probably should not
     * contain a '-'. The '-' is necessary to distinguish between base64 encoding
     * an plain name.
     *
     * That means, for chain names the prefix must be at most 8 chars long. */
    nm_assert(!is_iptables_chain || (strlen(prefix) <= 8));

    nm_str_buf_append(&strbuf, prefix);

    ip_iface_len = strlen(ip_iface);
    G_STATIC_ASSERT_EXPR(NM_IFNAMSIZ == 16);
    if (ip_iface_len >= NM_IFNAMSIZ) {
        nm_assert_not_reached();
        ip_iface_len = NM_IFNAMSIZ - 1;
    }

    if (NM_STRCHAR_ALL(ip_iface,
                       ch,
                       (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')
                           || (ch >= 'A' && ch <= 'Z') || NM_IN_SET(ch, '.', '_', '-', '+'))) {
        nm_str_buf_append_c(&strbuf, '-');
        nm_str_buf_append(&strbuf, ip_iface);
    } else {
        gs_free char *s = NULL;

        s = g_base64_encode((const guchar *) ip_iface, ip_iface_len);
        nm_str_buf_append(&strbuf, s);
    }

    return nm_str_buf_finalize(&strbuf, NULL);
}

/*****************************************************************************/

static gboolean
_iptables_call_v(const char *const *argv)
{
    gs_free_error GError *error    = NULL;
    gs_free char         *argv_str = NULL;
    int                   status;

    nm_log_dbg(LOGD_FIREWALL, "iptables: %s", (argv_str = g_strjoinv(" ", (char **) argv)));

    if (!g_spawn_sync("/",
                      (char **) argv,
                      (char **) NM_PTRARRAY_EMPTY(const char *),
                      G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL,
                      NULL,
                      NULL,
                      NULL,
                      NULL,
                      &status,
                      &error)) {
        nm_log_warn(LOGD_FIREWALL,
                    "iptables: error executing command %s: %s",
                    argv[0],
                    error->message);
        return FALSE;
    }

    if (!g_spawn_check_exit_status(status, &error)) {
        nm_log_warn(LOGD_FIREWALL, "iptables: command %s failed: %s", argv[0], error->message);
        return FALSE;
    }

    return TRUE;
}

#define _ipxtables_call(family, ...)                                                   \
    _iptables_call_v(                                                                  \
        NM_MAKE_STRV((family == AF_INET ? "" IPTABLES_PATH "" : "" IP6TABLES_PATH ""), \
                     "--wait",                                                         \
                     "2",                                                              \
                     __VA_ARGS__))

static gboolean
_share_iptables_chain_op(const char *table, const char *chain, const char *op)
{
    return _ipxtables_call(AF_INET, "--table", table, op, chain);
}

static gboolean
_share_iptables_chain_delete(const char *table, const char *chain)
{
    _share_iptables_chain_op(table, chain, "--flush");
    return _share_iptables_chain_op(table, chain, "--delete-chain");
}

static gboolean
_share_iptables_chain_add(const char *table, const char *chain)
{
    if (_share_iptables_chain_op(table, chain, "--new-chain"))
        return TRUE;

    _share_iptables_chain_delete(table, chain);
    return _share_iptables_chain_op(table, chain, "--new-chain");
}

static void
_share_iptables_set_masquerade_sync(gboolean up, const char *ip_iface, in_addr_t addr, guint8 plen)
{
    char          str_subnet[_SHARE_IPTABLES_SUBNET_TO_STR_LEN];
    gs_free char *comment_name = NULL;

    comment_name = _iptables_get_name(FALSE, "nm-shared", ip_iface);

    _share_iptables_subnet_to_str(str_subnet, addr, plen);
    _ipxtables_call(AF_INET,
                    "--table",
                    "nat",
                    up ? "--insert" : "--delete",
                    "POSTROUTING",
                    "--source",
                    str_subnet,
                    "!",
                    "--destination",
                    str_subnet,
                    "--jump",
                    "MASQUERADE",
                    "-m",
                    "comment",
                    "--comment",
                    comment_name);
}

static void
_share_iptables_set_shared_chains_add(const char *chain_input,
                                      const char *chain_forward,
                                      const char *ip_iface,
                                      in_addr_t   addr,
                                      guint       plen)
{
    const char *const input_params[][2] = {
        {
            "tcp",
            "67",
        },
        {
            "udp",
            "67",
        },
        {
            "tcp",
            "53",
        },
        {
            "udp",
            "53",
        },
    };
    char str_subnet[_SHARE_IPTABLES_SUBNET_TO_STR_LEN];
    int  i;

    _share_iptables_subnet_to_str(str_subnet, addr, plen);

    _share_iptables_chain_add("filter", chain_input);

    for (i = 0; i < (int) G_N_ELEMENTS(input_params); i++) {
        _ipxtables_call(AF_INET,
                        "--table",
                        "filter",
                        "--append",
                        chain_input,
                        "--protocol",
                        input_params[i][0],
                        "--destination-port",
                        input_params[i][1],
                        "--jump",
                        "ACCEPT");
    }

    _share_iptables_chain_add("filter", chain_forward);

    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    "--append",
                    chain_forward,
                    "--destination",
                    str_subnet,
                    "--out-interface",
                    ip_iface,
                    "--match",
                    "state",
                    "--state",
                    "ESTABLISHED,RELATED",
                    "--jump",
                    "ACCEPT");
    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    "--append",
                    chain_forward,
                    "--source",
                    str_subnet,
                    "--in-interface",
                    ip_iface,
                    "--jump",
                    "ACCEPT");
    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    "--append",
                    chain_forward,
                    "--in-interface",
                    ip_iface,
                    "--out-interface",
                    ip_iface,
                    "--jump",
                    "ACCEPT");
    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    "--append",
                    chain_forward,
                    "--out-interface",
                    ip_iface,
                    "--jump",
                    "REJECT");
    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    "--append",
                    chain_forward,
                    "--in-interface",
                    ip_iface,
                    "--jump",
                    "REJECT");
}

static void
_share_iptables_set_shared_chains_delete(const char *chain_input, const char *chain_forward)
{
    _share_iptables_chain_delete("filter", chain_input);
    _share_iptables_chain_delete("filter", chain_forward);
}

static void
_share_iptables_set_shared_sync(gboolean up, const char *ip_iface, in_addr_t addr, guint plen)
{
    gs_free char *comment_name  = NULL;
    gs_free char *chain_input   = NULL;
    gs_free char *chain_forward = NULL;

    comment_name  = _iptables_get_name(FALSE, "nm-shared", ip_iface);
    chain_input   = _iptables_get_name(TRUE, "nm-sh-in", ip_iface);
    chain_forward = _iptables_get_name(TRUE, "nm-sh-fw", ip_iface);

    if (up)
        _share_iptables_set_shared_chains_add(chain_input, chain_forward, ip_iface, addr, plen);

    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    up ? "--insert" : "--delete",
                    "INPUT",
                    "--in-interface",
                    ip_iface,
                    "--jump",
                    chain_input,
                    "-m",
                    "comment",
                    "--comment",
                    comment_name);

    _ipxtables_call(AF_INET,
                    "--table",
                    "filter",
                    up ? "--insert" : "--delete",
                    "FORWARD",
                    "--jump",
                    chain_forward,
                    "-m",
                    "comment",
                    "--comment",
                    comment_name);

    if (!up)
        _share_iptables_set_shared_chains_delete(chain_input, chain_forward);
}

/*****************************************************************************/

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, 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_FIREWALL,
                       "firewall: nft[%s]: communication failed: %s. Kill process",
                       call_data->identifier,
                       error->message);
        } else if (!call_data->timeout_source) {
            nm_log_dbg(LOGD_FIREWALL,
                       "firewall: nft[%s]: communication timed out. Kill process",
                       call_data->identifier);
            nm_clear_error(&error);
            nm_utils_error_set(&error, NM_UTILS_ERROR_UNKNOWN, "timeout communicating with nft");
        } else {
            nm_log_dbg(LOGD_FIREWALL,
                       "firewall: nft[%s]: communication cancelled. Kill process",
                       call_data->identifier);
        }

        {
            _nm_unused nm_auto_pop_gmaincontext GMainContext *main_context =
                nm_g_main_context_push_thread_default_if_necessary(NULL);

            nm_shutdown_wait_obj_register_object(call_data->subprocess, "nft-terminate");
            G_STATIC_ASSERT_EXPR(200 < NM_SHUTDOWN_TIMEOUT_ADDITIONAL_MSEC * 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_FIREWALL, "firewall: nft[%s]: command successful", call_data->identifier);
    } else {
        char          buf[NM_UTILS_GET_PROCESS_EXIT_STATUS_BUF_LEN];
        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);
        int           status;

        status = g_subprocess_get_status(call_data->subprocess);

        nm_utils_get_process_exit_status_desc_buf(status, buf, sizeof(buf));

        nm_log_warn(LOGD_FIREWALL,
                    "firewall: nft[%s]: command %s:%s%s%s%s%s%s%s",
                    call_data->identifier,
                    buf,
                    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),
                        "\")",
                        ""));

        nm_utils_error_set(&error, NM_UTILS_ERROR_COMMAND_FAILED, "nft command %s", buf);
    }

    _fw_nft_call_data_free(call_data, g_steal_pointer(&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_FIREWALL, "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_FIREWALL,
               "firewall: nft[%s]: cancel operation after timeout",
               call_data->identifier);

    nm_clear_g_cancellable(&call_data->intern_cancellable);
    return G_SOURCE_CONTINUE;
}

void
nm_firewall_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;
    gs_free char                        *ss1 = NULL;

    call_data  = g_slice_new(FwNftCallData);
    *call_data = (FwNftCallData) {
        .task =
            nm_g_task_new(NULL, cancellable, nm_firewall_nft_call, callback, callback_user_data),
        .subprocess     = NULL,
        .timeout_source = NULL,
    };

    nm_log_trace(LOGD_FIREWALL,
                 "firewall: nft: call command: [ '%s' ]",
                 nm_utils_buf_utf8safe_escape_bytes(stdin_buf,
                                                    NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
                                                    &ss1));

    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_FIREWALL, "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_FIREWALL, "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_FIREWALL, "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_1500_MSEC * 2) / 3,
                                                   G_PRIORITY_DEFAULT,
                                                   _fw_nft_call_timeout_cb,
                                                   call_data,
                                                   NULL),
                           g_task_get_context(call_data->task));
}

gboolean
nm_firewall_nft_call_finish(GAsyncResult *result, GError **error)
{
    g_return_val_if_fail(nm_g_task_is_valid(result, NULL, nm_firewall_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 = nm_firewall_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,
    };

    nm_firewall_nft_call(stdin_buf, NULL, _fw_nft_call_sync_done, &data);

    g_main_loop_run(main_loop);
    return data.success;
}

/*****************************************************************************/

#define _append(p_strbuf, fmt, ...) nm_str_buf_append_printf((p_strbuf), "" fmt "\n", ##__VA_ARGS__)

static void
_fw_nft_append_cmd_table(NMStrBuf *strbuf, const char *family, const char *table_name, gboolean up)
{
    /* Either delete the table, or create/flush it. */
    _append(strbuf, "add table %s %s", family, table_name);
    _append(strbuf, "%s table %s %s", up ? "flush" : "delete", family, table_name);
}

static GBytes *
_fw_nft_set_shared_construct(gboolean up, 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_free char            *table_name = NULL;
    char                     str_subnet[_SHARE_IPTABLES_SUBNET_TO_STR_LEN];

    table_name = _iptables_get_name(FALSE, "nm-shared", ip_iface);

    _share_iptables_subnet_to_str(str_subnet, addr, plen);

    _fw_nft_append_cmd_table(&strbuf, "ip", table_name, up);

    if (up) {
        _append(&strbuf,
                "add chain ip %s nat_postrouting {"
                " type nat hook postrouting priority 100; policy accept; "
                "};",
                table_name);
        _append(&strbuf,
                "add rule ip %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 ip %s filter_input {"
         *         " type filter hook input priority 0; policy accept; "
         *         "};",
         *         table_name);
         * _append(&strbuf, "add rule ip %s filter_input tcp dport { 67, 53 } accept;", table_name);
         * _append(&strbuf, "add rule ip %s filter_input udp dport { 67, 53 } accept;", table_name);
         */

        _append(&strbuf,
                "add chain ip %s filter_forward {"
                " type filter hook forward priority 0; policy accept; "
                "};",
                table_name);
        _append(&strbuf,
                "add rule ip %s filter_forward ip daddr %s oifname \"%s\" "
                " ct state { established, related } accept;",
                table_name,
                str_subnet,
                ip_iface);
        _append(&strbuf,
                "add rule ip %s filter_forward ip saddr %s iifname \"%s\" accept;",
                table_name,
                str_subnet,
                ip_iface);
        _append(&strbuf,
                "add rule ip %s filter_forward iifname \"%s\" oifname \"%s\" accept;",
                table_name,
                ip_iface,
                ip_iface);
        _append(&strbuf,
                "add rule ip %s filter_forward iifname \"%s\" reject;",
                table_name,
                ip_iface);
        _append(&strbuf,
                "add rule ip %s filter_forward oifname \"%s\" reject;",
                table_name,
                ip_iface);
    }

    return nm_str_buf_finalize_to_gbytes(&strbuf);
}

static GBytes *
_fw_nft_wg_default_construct(const char        *ip_iface,
                             NMSettingIPConfig *ip_config,
                             int                fwmark,
                             gboolean           up)
{
    nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
    gs_free char            *table_name = NULL;
    const char              *family_str;

    table_name = _iptables_get_name(FALSE, "nm-wg", ip_iface);
    family_str = nm_setting_ip_config_get_addr_family(ip_config) == AF_INET ? "ip" : "ip6";

    _fw_nft_append_cmd_table(&strbuf, family_str, table_name, up);

    if (up) {
        guint n_addresses = nm_setting_ip_config_get_num_addresses(ip_config);

        if (n_addresses) {
            _append(&strbuf, "add chain %s %s preraw {", family_str, table_name);

            for (guint i = 0; i < n_addresses; i++) {
                NMIPAddress *addr = nm_setting_ip_config_get_address(ip_config, i);

                _append(&strbuf,
                        " iifname != \"%s\" "
                        " %s daddr %s "
                        " fib saddr type != local "
                        "drop;",
                        ip_iface,
                        family_str,
                        nm_ip_address_get_address(addr));
            }

            _append(&strbuf, "};");
        }

        _append(&strbuf,
                "add chain %s %s premangle {"
                " type filter hook prerouting priority mangle; policy accept; "
                " meta l4proto udp meta mark set ct mark; "
                "};",
                family_str,
                table_name);

        _append(&strbuf,
                "add chain %s %s postmangle {"
                " type filter hook postrouting priority mangle; policy accept; "
                " meta l4proto udp mark 0x%08x ct mark set meta mark; "
                "};",
                family_str,
                table_name,
                fwmark);
    }

    return nm_str_buf_finalize_to_gbytes(&strbuf);
}

static void
_fw_iptables_wg_configure(const char        *ip_iface,
                          NMSettingIPConfig *ip_config,
                          int                fwmark,
                          gboolean           up)
{
    gs_free char *comment_name = NULL;
    char          fwmark_str[11];
    int           family      = nm_setting_ip_config_get_addr_family(ip_config);
    guint         n_addresses = nm_setting_ip_config_get_num_addresses(ip_config);

    comment_name = _iptables_get_name(FALSE, "nm-wg", ip_iface);
    g_snprintf(fwmark_str, sizeof(fwmark_str), "%" G_GUINT32_FORMAT, fwmark);

    nm_assert(strlen(fwmark_str) > 0);

    for (guint i = 0; i < n_addresses; i++) {
        NMIPAddress *addr = nm_setting_ip_config_get_address(ip_config, i);

        _ipxtables_call(family,
                        "--table",
                        "raw",
                        up ? "--insert" : "--delete",
                        "PREROUTING",
                        "!",
                        "--in-interface",
                        ip_iface,
                        "--destination",
                        nm_ip_address_get_address(addr),
                        "--match",
                        "addrtype",
                        "!",
                        "--src-type",
                        "LOCAL",
                        "-j",
                        "DROP",
                        "-m",
                        "comment",
                        "--comment",
                        comment_name);
    }

    _ipxtables_call(family,
                    "--table",
                    "mangle",
                    up ? "--insert" : "--delete",
                    "POSTROUTING",
                    "--match",
                    "mark",
                    "--mark",
                    fwmark_str,
                    "--protocol",
                    "udp",
                    "--jump",
                    "CONNMARK",
                    "--save-mark",
                    "-m",
                    "comment",
                    "--comment",
                    comment_name);

    _ipxtables_call(family,
                    "--table",
                    "mangle",
                    up ? "--insert" : "--delete",
                    "PREROUTING",
                    "--protocol",
                    "udp",
                    "--jump",
                    "CONNMARK",
                    "--restore-mark",
                    "-m",
                    "comment",
                    "--comment",
                    comment_name);
}

/*****************************************************************************/

GBytes *
nm_firewall_nft_stdio_mlag(gboolean           up,
                           const char        *bond_ifname,
                           const char *const *bond_ifnames_down,
                           const char *const *active_members,
                           const char *const *previous_members,
                           gboolean           with_counters)
{
    nm_auto_str_buf NMStrBuf strbuf_table_name =
        NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_32, FALSE);
    nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
    const char              *table_name;
    gsize                    i;
    const char *const        s_counter = with_counters ? " counter" : "";

    if (NM_MORE_ASSERTS > 10 && active_members) {
        /* No duplicates. We make certain assumptions here, and we don't
         * want to check that there are no duplicates. The caller must take
         * care of this. */
        for (i = 0; active_members[i]; i++)
            nm_assert(!nm_strv_contains(&active_members[i + 1], -1, active_members[i]));
    }

    /* If an interface gets renamed, we need to update the nft tables. Since one nft
     * invocation is atomic, it is reasonable to drop the previous tables(s) at the
     * same time when creating the new one. */
    for (; bond_ifnames_down && bond_ifnames_down[0]; bond_ifnames_down++) {
        if (nm_streq(bond_ifname, bond_ifnames_down[0]))
            continue;
        table_name = _strbuf_set_sanitized(&strbuf_table_name, "nm-mlag-", bond_ifnames_down[0]);
        _fw_nft_append_cmd_table(&strbuf, "netdev", table_name, FALSE);
    }

    table_name = _strbuf_set_sanitized(&strbuf_table_name, "nm-mlag-", bond_ifname);

    _fw_nft_append_cmd_table(&strbuf, "netdev", table_name, up);

    if (up) {
        nm_auto_str_buf NMStrBuf strbuf_1 =
            NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
        const gsize n_active_members = NM_PTRARRAY_LEN(active_members);

        if (!_nft_ifname_valid(bond_ifname)) {
            /* We cannot meaningfully express this interface name. Ignore all chains
             * and only create an empty table. */
            goto out;
        }

        for (; previous_members && previous_members[0]; previous_members++) {
            const char *previous_member = previous_members[0];
            const char *chain_name;

            /* The caller already ensures that the previous member is not part of the new
             * active members. Avoid the overhead of checking, and assert against that. */
            nm_assert(!nm_strv_contains(active_members, n_active_members, previous_member));

            if (!_nft_ifname_valid(previous_member))
                continue;

            chain_name = _strbuf_set_sanitized(&strbuf_1, "rx-drop-bc-mc-", previous_member);

            /* We want atomically update our table, however, we don't want to delete
             * and recreate it, because then the sets get lost (which we don't want).
             *
             * Instead, we only "add && flush" the table, which removes all rules from
             * the chain. However, as our active-members change, we want to delete
             * the obsolete chains too.
             *
             * nft has no way to delete all chains in a table, we have to name
             * them one by one. So we keep track of active members that we had
             * in the past, and which are now no longer in use. For those previous
             * members we delete the chains (again, with the "add && delete" dance
             * to avoid failure deleting a non-existing chain (in case our tracking
             * is wrong or somebody else modified the table in the meantime).
             *
             * We need to track the previous members, because we don't want to first
             * ask nft which chains exist. Doing that would be cumbersome as we would
             * have to do one async program invocation and parse stdout. */
            _append(&strbuf,
                    "add chain netdev %s %s {"
                    " type filter hook ingress device %s priority filter; "
                    "}",
                    table_name,
                    chain_name,
                    previous_member);
            _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);

            chain_name =
                _strbuf_set_sanitized(&strbuf_1, "tx-redirect-igmp-reports-", previous_member);

            _append(&strbuf,
                    "add chain netdev %s %s {"
                    " type filter hook egress device %s priority filter + 1; "
                    "}",
                    table_name,
                    chain_name,
                    previous_member);
            _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
        }

        /* OVS SLB rule 1
         *
         * "Open vSwitch avoids packet duplication by accepting multicast and broadcast
         *  packets on only the active member, and dropping multicast and broadcast
         *  packets on all other members."
         *
         * primary is first member, we drop on all others */
        for (i = 0; i < n_active_members; i++) {
            const char *active_member = active_members[i];
            const char *chain_name;

            if (!_nft_ifname_valid(active_member))
                continue;

            chain_name = _strbuf_set_sanitized(&strbuf_1, "rx-drop-bc-mc-", active_member);

            _append(&strbuf,
                    "add chain netdev %s %s {"
                    " type filter hook ingress device %s priority filter; "
                    "}",
                    table_name,
                    chain_name,
                    active_member);

            if (i == 0) {
                _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
                continue;
            }

            _append(&strbuf,
                    "add rule netdev %s %s pkttype {"
                    " broadcast, multicast "
                    "}%s drop",
                    table_name,
                    chain_name,
                    s_counter);
        }

        /* OVS SLB rule 2
         *
         * "Open vSwitch deals with this case by dropping packets received on any SLB
         * bonded link that have a source MAC+VLAN that has been learned on any other
         * port."
         */
        _append(&strbuf,
                "add set netdev %s macset-tagged {"
                " typeof ether saddr . vlan id; flags dynamic,timeout; "
                "}",
                table_name);
        _append(&strbuf,
                "add set netdev %s macset-untagged {"
                " typeof ether saddr; flags dynamic,timeout; "
                "}",
                table_name);

        _append(&strbuf,
                "add chain netdev %s tx-snoop-source-mac {"
                " type filter hook egress device %s priority filter; "
                "}",
                table_name,
                bond_ifname);
        _append(&strbuf,
                "add rule netdev %s tx-snoop-source-mac set update ether saddr . vlan id "
                "timeout 5s @macset-tagged%s return"
                "", /* tagged */
                table_name,
                s_counter);
        _append(&strbuf,
                "add rule netdev %s tx-snoop-source-mac set update ether saddr timeout 5s "
                "@macset-untagged%s"
                "", /* untagged*/
                table_name,
                s_counter);

        _append(&strbuf,
                "add chain netdev %s rx-drop-looped-packets {"
                " type filter hook ingress device %s priority filter; "
                "}",
                table_name,
                bond_ifname);
        _append(
            &strbuf,
            "add rule netdev %s rx-drop-looped-packets ether saddr . vlan id @macset-tagged%s drop",
            table_name,
            s_counter);
        _append(&strbuf,
                "add rule netdev %s rx-drop-looped-packets ether type vlan%s return"
                "", /* avoid looking up tagged packets in untagged table */
                table_name,
                s_counter);
        _append(&strbuf,
                "add rule netdev %s rx-drop-looped-packets ether saddr @macset-untagged%s drop",
                table_name,
                s_counter);

        /* IGMP SNOOPING
         *
         * This redirects all IGMP reports to the primary member port. The TOR switches
         * may prune the multicast tree. If we let the bonding vlan+srcmac hash occur,
         * then the TOR may send the pruned multicast stream to a bond member port for
         * which the RX filters will drop (e.g. report out non-primary, stream in
         * non-primary). If it's known multicast then we must control the pruned tree by
         * only sending the IGMP reports on a port for which we will accept the traffic,
         * i.e. the primary member port.
         */
        for (i = 0; i < n_active_members; i++) {
            const char *active_member = active_members[i];
            const char *chain_name;

            if (!_nft_ifname_valid(active_member))
                continue;

            chain_name =
                _strbuf_set_sanitized(&strbuf_1, "tx-redirect-igmp-reports-", active_member);

            _append(&strbuf,
                    "add chain netdev %s %s {"
                    " type filter hook egress device %s priority filter + 1; "
                    "}",
                    table_name,
                    chain_name,
                    active_member);
            /* first is primary, we clean up in case it was previously a non-primary member */
            if (i == 0) {
                _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
                continue;
            }

            _append(&strbuf,
                    "add rule netdev %s %s igmp type {"
                    " membership-report-v1, membership-report-v2, membership-report-v3 "
                    "}%s fwd to %s",
                    table_name,
                    chain_name,
                    s_counter,
                    active_members[0]);
            _append(&strbuf,
                    "add rule netdev %s %s icmpv6 type {"
                    " mld-listener-report, mld2-listener-report "
                    "}%s fwd to %s",
                    table_name,
                    chain_name,
                    s_counter,
                    active_members[0]);
        }
    }

out:
    return nm_str_buf_finalize_to_gbytes(&strbuf);
}

/*****************************************************************************/

struct _NMFirewallConfig {
    char     *ip_iface;
    in_addr_t addr;
    guint8    plen;
};

NMFirewallConfig *
nm_firewall_config_new_shared(const char *ip_iface, in_addr_t addr, guint8 plen)
{
    NMFirewallConfig *self;

    nm_assert(ip_iface);
    nm_assert(addr != 0u);
    nm_assert(plen <= 32);

    self  = g_slice_new(NMFirewallConfig);
    *self = (NMFirewallConfig) {
        .ip_iface = g_strdup(ip_iface),
        .addr     = addr,
        .plen     = plen,
    };
    return self;
}

void
nm_firewall_config_free(NMFirewallConfig *self)
{
    if (!self)
        return;

    g_free(self->ip_iface);
    nm_g_slice_free(self);
}

/*****************************************************************************/
void
nm_firewall_config_set_wg_rule(const char        *ifname,
                               NMSettingIPConfig *ip_config,
                               int                fwmark,
                               gboolean           up)
{
    switch (nm_firewall_utils_get_backend()) {
    case NM_FIREWALL_BACKEND_NFTABLES:
    {
        gs_unref_bytes GBytes *stdin_buf = NULL;

        stdin_buf = _fw_nft_wg_default_construct(ifname, ip_config, fwmark, up);
        _fw_nft_call_sync(stdin_buf, NULL);
        break;
    }
    case NM_FIREWALL_BACKEND_IPTABLES:
        _fw_iptables_wg_configure(ifname, ip_config, fwmark, up);
        break;
    case NM_FIREWALL_BACKEND_NONE:
        break;
    default:
        nm_assert_not_reached();
        break;
    }
}

void
nm_firewall_config_apply_sync(NMFirewallConfig *self, gboolean up)
{
    switch (nm_firewall_utils_get_backend()) {
    case NM_FIREWALL_BACKEND_IPTABLES:
        _share_iptables_set_masquerade_sync(up, self->ip_iface, self->addr, self->plen);
        _share_iptables_set_shared_sync(up, self->ip_iface, self->addr, self->plen);
        break;
    case NM_FIREWALL_BACKEND_NFTABLES:
    {
        gs_unref_bytes GBytes *stdin_buf = NULL;

        stdin_buf = _fw_nft_set_shared_construct(up, self->ip_iface, self->addr, self->plen);
        _fw_nft_call_sync(stdin_buf, NULL);
        break;
    }
    case NM_FIREWALL_BACKEND_NONE:
        break;
    default:
        nm_assert_not_reached();
        break;
    }
}

/*****************************************************************************/

static NMFirewallBackend
_firewall_backend_detect(void)
{
    if (g_file_test(NFT_PATH, G_FILE_TEST_IS_EXECUTABLE))
        return NM_FIREWALL_BACKEND_NFTABLES;
    if (g_file_test(IPTABLES_PATH, G_FILE_TEST_IS_EXECUTABLE))
        return NM_FIREWALL_BACKEND_IPTABLES;

    return NM_FIREWALL_BACKEND_NFTABLES;
}

NMFirewallBackend
nm_firewall_utils_get_backend(void)
{
    static int backend = NM_FIREWALL_BACKEND_UNKNOWN;
    int        b;

again:
    b = g_atomic_int_get(&backend);
    if (b == NM_FIREWALL_BACKEND_UNKNOWN) {
        gs_free char *conf_value = NULL;
        gboolean      detect;
        int           i;

        conf_value =
            nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG,
                                     NM_CONFIG_KEYFILE_GROUP_MAIN,
                                     NM_CONFIG_KEYFILE_KEY_MAIN_FIREWALL_BACKEND,
                                     NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);

        if (conf_value) {
            for (i = 0; i < (int) G_N_ELEMENTS(FirewallBackends); i++) {
                if (!g_ascii_strcasecmp(conf_value, FirewallBackends[i].name)) {
                    b = (i + 1);
                    break;
                }
            }
        }

        detect = (b == NM_FIREWALL_BACKEND_UNKNOWN);
        if (detect)
            b = _firewall_backend_detect();

        nm_assert(NM_IN_SET(b,
                            NM_FIREWALL_BACKEND_NONE,
                            NM_FIREWALL_BACKEND_IPTABLES,
                            NM_FIREWALL_BACKEND_NFTABLES));

        if (!g_atomic_int_compare_and_exchange(&backend, NM_FIREWALL_BACKEND_UNKNOWN, b))
            goto again;

        nm_log_dbg(LOGD_FIREWALL,
                   "firewall: use %s backend%s%s%s%s%s%s%s",
                   FirewallBackends[b - 1].name,
                   NM_PRINT_FMT_QUOTED(FirewallBackends[b - 1].path,
                                       " (",
                                       FirewallBackends[b - 1].path,
                                       ")",
                                       ""),
                   detect ? " (detected)" : "",
                   NM_PRINT_FMT_QUOTED(detect && conf_value,
                                       " (invalid setting \"",
                                       conf_value,
                                       "\")",
                                       ""));
    }

    nm_assert(NM_IN_SET(b,
                        NM_FIREWALL_BACKEND_NONE,
                        NM_FIREWALL_BACKEND_IPTABLES,
                        NM_FIREWALL_BACKEND_NFTABLES));
    return b;
}
