/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <arpa/inet.h>
#include <endian.h>
#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nf_nat.h>
#include <linux/netfilter_ipv4.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>

#include "sd-netlink.h"

#include "alloc-util.h"
#include "firewall-util.h"
#include "firewall-util-private.h"
#include "in-addr-util.h"
#include "macro.h"
#include "socket-util.h"
#include "time-util.h"

#define NFT_SYSTEMD_DNAT_MAP_NAME "map_port_ipport"
#define NFT_SYSTEMD_TABLE_NAME   "io.systemd.nat"
#define NFT_SYSTEMD_MASQ_SET_NAME "masq_saddr"

#define NFNL_DEFAULT_TIMEOUT_USECS (1ULL * USEC_PER_SEC)

#define UDP_DPORT_OFFSET 2

static int nfnl_netlink_sendv(sd_netlink *nfnl,
                              sd_netlink_message *messages[],
                              size_t msgcount) {
        _cleanup_free_ uint32_t *serial = NULL;
        size_t i;
        int r;

        assert(msgcount > 0);

        r = sd_netlink_sendv(nfnl, messages, msgcount, &serial);
        if (r < 0)
                return r;

        r = 0;
        for (i = 1; i < msgcount - 1; i++) {
                int tmp;

                /* If message is an error, this returns embedded errno */
                tmp = sd_netlink_read(nfnl, serial[i], NFNL_DEFAULT_TIMEOUT_USECS, NULL);
                if (tmp < 0 && r == 0)
                        r = tmp;
        }

        return r;
}

static int nfnl_add_open_expr_container(sd_netlink_message *m, const char *name) {
        int r;

        r = sd_netlink_message_open_array(m, NFTA_LIST_ELEM);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_string(m, NFTA_EXPR_NAME, name);
        if (r < 0)
                return r;

        return sd_netlink_message_open_container_union(m, NFTA_EXPR_DATA, name);
}

static int nfnl_add_expr_fib(sd_netlink_message *m, uint32_t nft_fib_flags,
                             enum nft_fib_result result,
                             enum nft_registers dreg) {
        int r;

        r = nfnl_add_open_expr_container(m, "fib");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_FIB_FLAGS, htobe32(nft_fib_flags));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_FIB_RESULT, htobe32(result));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_FIB_DREG, htobe32(dreg));
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;

        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_meta(sd_netlink_message *m, enum nft_meta_keys key,
                              enum nft_registers dreg) {
        int r;

        r = nfnl_add_open_expr_container(m, "meta");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_META_KEY, htobe32(key));
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_META_DREG, htobe32(dreg));
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;

        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_payload(sd_netlink_message *m, enum nft_payload_bases pb,
                                 uint32_t offset, uint32_t len, enum nft_registers dreg) {
        int r;

        r = nfnl_add_open_expr_container(m, "payload");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_PAYLOAD_DREG, htobe32(dreg));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_PAYLOAD_BASE, htobe32(pb));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_PAYLOAD_OFFSET, htobe32(offset));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_PAYLOAD_LEN, htobe32(len));
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;
        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_lookup_set_data(sd_netlink_message *m, const char *set_name,
                                         enum nft_registers sreg) {
        int r;

        r = nfnl_add_open_expr_container(m, "lookup");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_string(m, NFTA_LOOKUP_SET, set_name);
        if (r < 0)
                return r;

        return sd_netlink_message_append_u32(m, NFTA_LOOKUP_SREG, htobe32(sreg));
}

static int nfnl_add_expr_lookup_set(sd_netlink_message *m, const char *set_name,
                                    enum nft_registers sreg) {
        int r;

        r = nfnl_add_expr_lookup_set_data(m, set_name, sreg);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;
        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_lookup_map(sd_netlink_message *m, const char *set_name,
                                    enum nft_registers sreg, enum nft_registers dreg) {
        int r;

        r = nfnl_add_expr_lookup_set_data(m, set_name, sreg);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_LOOKUP_DREG, htobe32(dreg));
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;

        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_data(sd_netlink_message *m, int attr, const void *data, uint32_t dlen) {
        int r;

        r = sd_netlink_message_open_container(m, attr);
        if (r < 0)
                return r;
        r = sd_netlink_message_append_data(m, NFTA_DATA_VALUE, data, dlen);
        if (r < 0)
                return r;

        return sd_netlink_message_close_container(m); /* attr */
}

static int nfnl_add_expr_cmp_data(sd_netlink_message *m, const void *data, uint32_t dlen) {
        return nfnl_add_expr_data(m, NFTA_CMP_DATA, data, dlen);
}

static int nfnl_add_expr_cmp(sd_netlink_message *m, enum nft_cmp_ops cmp_op,
                             enum nft_registers sreg, const void *data, uint32_t dlen) {
        int r;

        r = nfnl_add_open_expr_container(m, "cmp");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_CMP_OP, htobe32(cmp_op));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_CMP_SREG, htobe32(sreg));
        if (r < 0)
                return r;

        r = nfnl_add_expr_cmp_data(m, data, dlen);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;
        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_bitwise(sd_netlink_message *m,
                                 enum nft_registers sreg,
                                 enum nft_registers dreg,
                                 const void *and,
                                 const void *xor, uint32_t len) {
        int r;

        r = nfnl_add_open_expr_container(m, "bitwise");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_BITWISE_SREG, htobe32(sreg));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_BITWISE_DREG, htobe32(dreg));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_BITWISE_LEN, htobe32(len));
        if (r < 0)
                return r;

        r = nfnl_add_expr_data(m, NFTA_BITWISE_MASK, and, len);
        if (r < 0)
                return r;

        r = nfnl_add_expr_data(m, NFTA_BITWISE_XOR, xor, len);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_EXPR_DATA */
        if (r < 0)
                return r;
        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int nfnl_add_expr_dnat(sd_netlink_message *m,
                              int family,
                              enum nft_registers areg,
                              enum nft_registers preg) {
        int r;

        r = nfnl_add_open_expr_container(m, "nat");
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_NAT_TYPE, htobe32(NFT_NAT_DNAT));
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_NAT_FAMILY, htobe32(family));
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_NAT_REG_ADDR_MIN, htobe32(areg));
        if (r < 0)
                return r;
        r = sd_netlink_message_append_u32(m, NFTA_NAT_REG_PROTO_MIN, htobe32(preg));
        if (r < 0)
                return r;
        r = sd_netlink_message_close_container(m);
        if (r < 0)
                return r;

        return sd_netlink_message_close_container(m);
}

static int nfnl_add_expr_masq(sd_netlink_message *m) {
        int r;

        r = sd_netlink_message_open_array(m, NFTA_LIST_ELEM);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_string(m, NFTA_EXPR_NAME, "masq");
        if (r < 0)
                return r;

        return sd_netlink_message_close_container(m); /* NFTA_LIST_ELEM */
}

static int sd_nfnl_message_new_masq_rule(sd_netlink *nfnl, sd_netlink_message **ret, int family,
                                         const char *chain) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        /* -t nat -A POSTROUTING -p protocol -s source/pflen -o out_interface -d destination/pflen -j MASQUERADE */

        r = sd_nfnl_nft_message_new_rule(nfnl, &m, family, NFT_SYSTEMD_TABLE_NAME, chain);
        if (r < 0)
                return r;

        r = sd_netlink_message_open_container(m, NFTA_RULE_EXPRESSIONS);
        if (r < 0)
                return r;

        /* 1st statement: ip saddr @masq_saddr. Place iph->saddr in reg1, resp. ipv6 in reg1..reg4. */
        if (family == AF_INET)
                r = nfnl_add_expr_payload(m, NFT_PAYLOAD_NETWORK_HEADER, offsetof(struct iphdr, saddr),
                                          sizeof(uint32_t), NFT_REG32_01);
        else
                r = nfnl_add_expr_payload(m, NFT_PAYLOAD_NETWORK_HEADER, offsetof(struct ip6_hdr, ip6_src.s6_addr),
                                          sizeof(struct in6_addr), NFT_REG32_01);
        if (r < 0)
                return r;

        /* 1st statement: use reg1 content to make lookup in @masq_saddr set. */
        r = nfnl_add_expr_lookup_set(m, NFT_SYSTEMD_MASQ_SET_NAME, NFT_REG32_01);
        if (r < 0)
                return r;

        /* 2nd statement: masq.  Only executed by kernel if the previous lookup was successful. */
        r = nfnl_add_expr_masq(m);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_RULE_EXPRESSIONS */
        if (r < 0)
                return r;
        *ret = TAKE_PTR(m);
        return 0;
}

static int sd_nfnl_message_new_dnat_rule_pre(sd_netlink *nfnl, sd_netlink_message **ret, int family,
                                             const char *chain) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        enum nft_registers proto_reg;
        uint32_t local = RTN_LOCAL;
        int r;

        /* -t nat -A PREROUTING -p protocol --dport local_port -i in_interface -s source/pflen
         * -d destination/pflen -j DNAT --to-destination remote_addr:remote_port */

        r = sd_nfnl_nft_message_new_rule(nfnl, &m, family, NFT_SYSTEMD_TABLE_NAME, chain);
        if (r < 0)
                return r;

        r = sd_netlink_message_open_container(m, NFTA_RULE_EXPRESSIONS);
        if (r < 0)
                return r;

        /* 1st statement: fib daddr type local */
        r = nfnl_add_expr_fib(m, NFTA_FIB_F_DADDR, NFT_FIB_RESULT_ADDRTYPE, NFT_REG32_01);
        if (r < 0)
                return r;

        /* 1st statement (cont.): compare RTN_LOCAL */
        r = nfnl_add_expr_cmp(m, NFT_CMP_EQ, NFT_REG32_01, &local, sizeof(local));
        if (r < 0)
                return r;

        /* 2nd statement: lookup local port in map, fetch address:dport to map to */
        r = nfnl_add_expr_meta(m, NFT_META_L4PROTO, NFT_REG32_01);
        if (r < 0)
                return r;

        r = nfnl_add_expr_payload(m, NFT_PAYLOAD_TRANSPORT_HEADER, UDP_DPORT_OFFSET,
                                  sizeof(uint16_t), NFT_REG32_02);
        if (r < 0)
                return r;

        /* 3rd statement: lookup 'l4proto . dport', e.g. 'tcp . 22' as key and
         * store address and port for the dnat mapping in REG1/REG2.
        */
        r = nfnl_add_expr_lookup_map(m, NFT_SYSTEMD_DNAT_MAP_NAME, NFT_REG32_01, NFT_REG32_01);
        if (r < 0)
                return r;

        proto_reg = family == AF_INET ? NFT_REG32_02 : NFT_REG32_05;
        r = nfnl_add_expr_dnat(m, family, NFT_REG32_01, proto_reg);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_RULE_EXPRESSIONS */
        if (r < 0)
                return r;
        *ret = TAKE_PTR(m);
        return 0;
}

static int sd_nfnl_message_new_dnat_rule_out(sd_netlink *nfnl, sd_netlink_message **ret,
                                             int family, const char *chain) {
        static const uint32_t zero = 0, one = 1;

        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        enum nft_registers proto_reg;
        int r;

        r = sd_nfnl_nft_message_new_rule(nfnl, &m, family, NFT_SYSTEMD_TABLE_NAME, chain);
        if (r < 0)
                return r;

        r = sd_netlink_message_open_container(m, NFTA_RULE_EXPRESSIONS);
        if (r < 0)
                return r;

        /* 1st statement: exclude 127.0.0.1/8: ip daddr != 127.0.0.1/8, resp. avoid ::1 */
        if (family == AF_INET) {
                uint32_t lonet = htobe32(UINT32_C(0x7F000000)), lomask = htobe32(UINT32_C(0xff000000));

                r = nfnl_add_expr_payload(m, NFT_PAYLOAD_NETWORK_HEADER, offsetof(struct iphdr, daddr),
                                          sizeof(lonet), NFT_REG32_01);
                if (r < 0)
                        return r;
                /* 1st statement (cont.): bitops/prefix */
                r = nfnl_add_expr_bitwise(m, NFT_REG32_01, NFT_REG32_01, &lomask, &zero, sizeof(lomask));
                if (r < 0)
                        return r;

                /* 1st statement (cont.): compare reg1 with 127/8 */
                r = nfnl_add_expr_cmp(m, NFT_CMP_NEQ, NFT_REG32_01, &lonet, sizeof(lonet));
        } else {
                struct in6_addr loaddr = IN6ADDR_LOOPBACK_INIT;

                r = nfnl_add_expr_payload(m, NFT_PAYLOAD_NETWORK_HEADER, offsetof(struct ip6_hdr, ip6_dst.s6_addr),
                                          sizeof(loaddr), NFT_REG32_01);
                if (r < 0)
                        return r;

                r = nfnl_add_expr_cmp(m, NFT_CMP_NEQ, NFT_REG32_01, &loaddr, sizeof(loaddr));
        }
        if (r < 0)
                return r;

        /* 2nd statement: meta oif lo */
        r = nfnl_add_expr_meta(m, NFT_META_OIF, NFT_REG32_01);
        if (r < 0)
                return r;

        /* 2nd statement (cont.): compare to lo ifindex (1) */
        r = nfnl_add_expr_cmp(m, NFT_CMP_EQ, NFT_REG32_01, &one, sizeof(one));
        if (r < 0)
                return r;

        /* 3rd statement: meta l4proto . th dport dnat ip . port to map @map_port_ipport */
        r = nfnl_add_expr_meta(m, NFT_META_L4PROTO, NFT_REG32_01);
        if (r < 0)
                return r;

        /* 3rd statement (cont): store the port number in reg2 */
        r = nfnl_add_expr_payload(m, NFT_PAYLOAD_TRANSPORT_HEADER, UDP_DPORT_OFFSET,
                                  sizeof(uint16_t), NFT_REG32_02);
        if (r < 0)
                return r;

        /* 3rd statement (cont): use reg1 and reg2 and retrieve
         * the new destination ip and port number.
         *
         * reg1 and reg2 are clobbered and will then contain the new
         * address/port number.
         */
        r = nfnl_add_expr_lookup_map(m, NFT_SYSTEMD_DNAT_MAP_NAME, NFT_REG32_01, NFT_REG32_01);
        if (r < 0)
                return r;

        /* 4th statement: dnat connection to address/port retrieved by the
         * preceding expression. */
        proto_reg = family == AF_INET ? NFT_REG32_02 : NFT_REG32_05;
        r = nfnl_add_expr_dnat(m, family, NFT_REG32_01, proto_reg);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m); /* NFTA_RULE_EXPRESSIONS */
        if (r < 0)
                return r;
        *ret = TAKE_PTR(m);
        return 0;
}

static int nft_new_set(struct sd_netlink *nfnl,
                       sd_netlink_message **ret,
                       int family, const char *set_name,
                       uint32_t set_id,
                       uint32_t flags, uint32_t type, uint32_t klen) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        r = sd_nfnl_nft_message_new_set(nfnl, &m, family, NFT_SYSTEMD_TABLE_NAME, set_name, set_id, klen);
        if (r < 0)
                return r;

        if (flags != 0) {
                r = sd_netlink_message_append_u32(m, NFTA_SET_FLAGS, htobe32(flags));
                if (r < 0)
                        return r;
        }

        r = sd_netlink_message_append_u32(m, NFTA_SET_KEY_TYPE, htobe32(type));
        if (r < 0)
                return r;

        *ret = TAKE_PTR(m);
        return r;
}

static int nft_new_map(struct sd_netlink *nfnl,
                       sd_netlink_message **ret,
                       int family, const char *set_name, uint32_t set_id,
                       uint32_t flags, uint32_t type, uint32_t klen, uint32_t dtype, uint32_t dlen) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        r = nft_new_set(nfnl, &m, family, set_name, set_id, flags | NFT_SET_MAP, type, klen);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_SET_DATA_TYPE, htobe32(dtype));
        if (r < 0)
               return r;

        r = sd_netlink_message_append_u32(m, NFTA_SET_DATA_LEN, htobe32(dlen));
        if (r < 0)
                return r;
        *ret = TAKE_PTR(m);
        return 0;
}

static int nft_add_element(sd_netlink *nfnl, sd_netlink_message **ret,
                           int family, const char *set_name,
                           const void *key, uint32_t klen,
                           const void *data, uint32_t dlen) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        /*
         * Ideally there would be an API that provides:
         *
         * 1) an init function to add the main ruleset skeleton
         * 2) a function that populates the sets with all known address/port pairs to s/dnat for
         * 3) a function that can remove address/port pairs again.
         *
         * At this time, the existing API is used which is built on a
         * 'add/delete a rule' paradigm.
         *
         * This replicated here and each element gets added to the set
         * one-by-one.
         */
        r = sd_nfnl_nft_message_new_setelems_begin(nfnl, &m, family, NFT_SYSTEMD_TABLE_NAME, set_name);
        if (r < 0)
                return r;

        r = sd_nfnl_nft_message_add_setelem(m, 0, key, klen, data, dlen);
        if (r < 0)
                return r;

        /* could theoretically append more set elements to add here */
        r = sd_nfnl_nft_message_add_setelem_end(m);
        if (r < 0)
                return r;
        *ret = TAKE_PTR(m);
        return 0;
}

static int nft_del_element(sd_netlink *nfnl,
                           sd_netlink_message **ret, int family, const char *set_name,
                           const void *key, uint32_t klen,
                           const void *data, uint32_t dlen) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        r = sd_nfnl_nft_message_del_setelems_begin(nfnl, &m, family, NFT_SYSTEMD_TABLE_NAME, set_name);
        if (r < 0)
               return r;

        r = sd_nfnl_nft_message_add_setelem(m, 0, key, klen, data, dlen);
        if (r < 0)
               return r;

        r = sd_nfnl_nft_message_add_setelem_end(m);
        if (r < 0)
                return r;
        *ret = TAKE_PTR(m);
        return 0;
}

/* This is needed so 'nft' userspace tool can properly format the contents
 * of the set/map when someone uses 'nft' to inspect their content.
 *
 * The values cannot be changed, they are part of the nft tool type identifier ABI.
 */
#define TYPE_BITS 6

enum nft_key_types {
        TYPE_IPADDR = 7,
        TYPE_IP6ADDR = 8,
        TYPE_INET_PROTOCOL = 12,
        TYPE_INET_SERVICE = 13,
};

static uint32_t concat_types2(enum nft_key_types a, enum nft_key_types b) {
        uint32_t type = (uint32_t)a;

        type <<= TYPE_BITS;
        type |= (uint32_t)b;

        return type;
}

/* enough space to hold netlink messages for table skeleton */
#define NFT_INIT_MSGS 16
static int fw_nftables_init_family(sd_netlink *nfnl, int family) {
        sd_netlink_message *batch[NFT_INIT_MSGS] = {};
        size_t msgcnt = 0, i, ip_type_size;
        uint32_t set_id = 0;
        int ip_type, r;

        assert(IN_SET(family, AF_INET, AF_INET6));

        r = sd_nfnl_message_batch_begin(nfnl, &batch[msgcnt]);
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        /* Set F_EXCL so table add fails if the table already exists. */
        r = sd_nfnl_nft_message_new_table(nfnl, &batch[msgcnt], family, NFT_SYSTEMD_TABLE_NAME);
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);

        r = sd_nfnl_nft_message_new_basechain(nfnl, &batch[msgcnt], family, NFT_SYSTEMD_TABLE_NAME,
                                              "prerouting", "nat",
                                              NF_INET_PRE_ROUTING, NF_IP_PRI_NAT_DST + 1);
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        r = sd_nfnl_nft_message_new_basechain(nfnl, &batch[msgcnt], family, NFT_SYSTEMD_TABLE_NAME,
                                              "output", "nat",
                                              NF_INET_LOCAL_OUT, NF_IP_PRI_NAT_DST + 1);
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        r = sd_nfnl_nft_message_new_basechain(nfnl, &batch[msgcnt], family, NFT_SYSTEMD_TABLE_NAME,
                                              "postrouting", "nat",
                                              NF_INET_POST_ROUTING, NF_IP_PRI_NAT_SRC + 1);
        if (r < 0)
                goto out_unref;

        if (family == AF_INET) {
                ip_type_size = sizeof(uint32_t);
                ip_type = TYPE_IPADDR;
        } else {
                assert(family == AF_INET6);
                ip_type_size = sizeof(struct in6_addr);
                ip_type = TYPE_IP6ADDR;
        }
        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        /* set to store ip address ranges we should masquerade for */
        r = nft_new_set(nfnl, &batch[msgcnt], family, NFT_SYSTEMD_MASQ_SET_NAME, ++set_id, NFT_SET_INTERVAL, ip_type, ip_type_size);
        if (r < 0)
                goto out_unref;

        /*
         * map to store ip address:port pair to dnat to.  elements in concatenation
         * are rounded up to 4 bytes.
         *
         * Example: ip protocol . tcp daddr is sizeof(uint32_t) + sizeof(uint32_t), not
         * sizeof(uint8_t) + sizeof(uint16_t).
         */
        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        r = nft_new_map(nfnl, &batch[msgcnt], family, NFT_SYSTEMD_DNAT_MAP_NAME, ++set_id, 0,
                        concat_types2(TYPE_INET_PROTOCOL, TYPE_INET_SERVICE), sizeof(uint32_t) * 2,
                        concat_types2(ip_type, TYPE_INET_SERVICE), ip_type_size + sizeof(uint32_t));
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        r = sd_nfnl_message_new_dnat_rule_pre(nfnl, &batch[msgcnt], family, "prerouting");
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        r = sd_nfnl_message_new_dnat_rule_out(nfnl, &batch[msgcnt], family, "output");
        if (r < 0)
                goto out_unref;

        msgcnt++;
        r = sd_nfnl_message_new_masq_rule(nfnl, &batch[msgcnt], family, "postrouting");
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt < NFT_INIT_MSGS);
        r = sd_nfnl_message_batch_end(nfnl, &batch[msgcnt]);
        if (r < 0)
                goto out_unref;

        msgcnt++;
        assert(msgcnt <= NFT_INIT_MSGS);
        r = nfnl_netlink_sendv(nfnl, batch, msgcnt);
        if (r == -EEXIST)
                r = 0;

out_unref:
        for (i = 0; i < msgcnt; i++)
                sd_netlink_message_unref(batch[i]);

        return r;
}

int fw_nftables_init(FirewallContext *ctx) {
        _cleanup_(sd_netlink_unrefp) sd_netlink *nfnl = NULL;
        int r;

        r = sd_nfnl_socket_open(&nfnl);
        if (r < 0)
                return r;

        r = fw_nftables_init_family(nfnl, AF_INET);
        if (r < 0)
                return r;

        if (socket_ipv6_is_supported()) {
                r = fw_nftables_init_family(nfnl, AF_INET6);
                if (r < 0)
                        log_debug_errno(r, "Failed to init ipv6 NAT: %m");
        }

        ctx->nfnl = TAKE_PTR(nfnl);
        return 0;
}

void fw_nftables_exit(FirewallContext *ctx) {
        ctx->nfnl = sd_netlink_unref(ctx->nfnl);
}

static int nft_message_add_setelem_iprange(sd_netlink_message *m,
                                           const union in_addr_union *source,
                                           unsigned int prefixlen) {
        uint32_t mask, start, end;
        unsigned int nplen;
        int r;

        assert(prefixlen <= 32);
        nplen = 32 - prefixlen;

        mask = (1U << nplen) - 1U;
        mask = htobe32(~mask);
        start = source->in.s_addr & mask;

        r = sd_nfnl_nft_message_add_setelem(m, 0, &start, sizeof(start), NULL, 0);
        if (r < 0)
                return r;

        r = sd_nfnl_nft_message_add_setelem_end(m);
        if (r < 0)
                return r;

        end = be32toh(start) + (1U << nplen);
        if (end < be32toh(start))
                end = 0U;
        end = htobe32(end);

        r = sd_nfnl_nft_message_add_setelem(m, 1, &end, sizeof(end), NULL, 0);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_SET_ELEM_FLAGS, htobe32(NFT_SET_ELEM_INTERVAL_END));
        if (r < 0)
                return r;

        r = sd_nfnl_nft_message_add_setelem_end(m);
        if (r < 0)
                return r;

        return 0;
}

static int nft_message_add_setelem_ip6range(
                sd_netlink_message *m,
                const union in_addr_union *source,
                unsigned int prefixlen) {

        union in_addr_union start, end;
        int r;

        r = in_addr_prefix_range(AF_INET6, source, prefixlen, &start, &end);
        if (r < 0)
                return r;

        r = sd_nfnl_nft_message_add_setelem(m, 0, &start.in6, sizeof(start.in6), NULL, 0);
        if (r < 0)
                return r;

        r = sd_nfnl_nft_message_add_setelem_end(m);
        if (r < 0)
                return r;

        r = sd_nfnl_nft_message_add_setelem(m, 1, &end.in6, sizeof(end.in6), NULL, 0);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, NFTA_SET_ELEM_FLAGS, htobe32(NFT_SET_ELEM_INTERVAL_END));
        if (r < 0)
                return r;

        return sd_nfnl_nft_message_add_setelem_end(m);
}

#define NFT_MASQ_MSGS   3

static int fw_nftables_add_masquerade_internal(
                FirewallContext *ctx,
                bool add,
                int af,
                const union in_addr_union *source,
                unsigned int source_prefixlen) {

        sd_netlink_message *transaction[NFT_MASQ_MSGS] = {};
        size_t tsize;
        int r;

        if (!source || source_prefixlen == 0)
                return -EINVAL;

        if (af == AF_INET6 && source_prefixlen < 8)
                return -EINVAL;

        r = sd_nfnl_message_batch_begin(ctx->nfnl, &transaction[0]);
        if (r < 0)
                return r;
        tsize = 1;
        if (add)
                r = sd_nfnl_nft_message_new_setelems_begin(ctx->nfnl, &transaction[tsize], af, NFT_SYSTEMD_TABLE_NAME, NFT_SYSTEMD_MASQ_SET_NAME);
        else
                r = sd_nfnl_nft_message_del_setelems_begin(ctx->nfnl, &transaction[tsize], af, NFT_SYSTEMD_TABLE_NAME, NFT_SYSTEMD_MASQ_SET_NAME);
        if (r < 0)
                goto out_unref;

        if (af == AF_INET)
                 r = nft_message_add_setelem_iprange(transaction[tsize], source, source_prefixlen);
        else
                 r = nft_message_add_setelem_ip6range(transaction[tsize], source, source_prefixlen);
        if (r < 0)
                goto out_unref;

        ++tsize;
        assert(tsize < NFT_MASQ_MSGS);
        r = sd_nfnl_message_batch_end(ctx->nfnl, &transaction[tsize]);
        if (r < 0)
                return r;

        ++tsize;
        r = nfnl_netlink_sendv(ctx->nfnl, transaction, tsize);

out_unref:
        while (tsize > 0)
                sd_netlink_message_unref(transaction[--tsize]);
        return r < 0 ? r : 0;
}

int fw_nftables_add_masquerade(
                FirewallContext *ctx,
                bool add,
                int af,
                const union in_addr_union *source,
                unsigned int source_prefixlen) {

        int r;

        if (!socket_ipv6_is_supported() && af == AF_INET6)
                return -EOPNOTSUPP;

        r = fw_nftables_add_masquerade_internal(ctx, add, af, source, source_prefixlen);
        if (r != -ENOENT)
                return r;

        /* When someone runs 'nft flush ruleset' in the same net namespace this will also tear down the
         * systemd nat table.
         *
         * Unlike iptables -t nat -F (which will remove all rules added by the systemd iptables
         * backend, iptables has builtin chains that cannot be deleted -- the next add operation will
         * 'just work'.
         *
         * In the nftables case, everything gets removed. The next add operation will yield -ENOENT.
         *
         * If we see -ENOENT on add, replay the initial table setup. If that works, re-do the add
         * operation.
         *
         * Note that this doesn't protect against external sabotage such as a
         * 'while true; nft flush ruleset; done'. There is nothing that could be done about that short
         * of extending the kernel to allow tables to be owned by stystemd-networkd and making them
         * non-deleteable except by the 'owning process'. */

        r = fw_nftables_init_family(ctx->nfnl, af);
        if (r < 0)
                return r;

        return fw_nftables_add_masquerade_internal(ctx, add, af, source, source_prefixlen);
}

#define NFT_DNAT_MSGS   4

static int fw_nftables_add_local_dnat_internal(
                FirewallContext *ctx,
                bool add,
                int af,
                int protocol,
                uint16_t local_port,
                const union in_addr_union *remote,
                uint16_t remote_port,
                const union in_addr_union *previous_remote) {

        sd_netlink_message *transaction[NFT_DNAT_MSGS] = {};
        static bool ipv6_supported = true;
        uint32_t data[5], key[2], dlen;
        size_t tsize;
        int r;

        assert(add || !previous_remote);

        if (!ipv6_supported && af == AF_INET6)
                return -EOPNOTSUPP;

        if (!IN_SET(protocol, IPPROTO_TCP, IPPROTO_UDP))
                return -EPROTONOSUPPORT;

        if (local_port <= 0)
                return -EINVAL;

        key[0] = protocol;
        key[1] = htobe16(local_port);

        if (!remote)
                return -EOPNOTSUPP;

        if (remote_port <= 0)
                return -EINVAL;

        if (af == AF_INET) {
                dlen = 8;
                data[1] = htobe16(remote_port);
        } else {
                assert(af == AF_INET6);
                dlen = sizeof(data);
                data[4] = htobe16(remote_port);
        }

        r = sd_nfnl_message_batch_begin(ctx->nfnl, &transaction[0]);
        if (r < 0)
                return r;

        tsize = 1;
        /* If a previous remote is set, remove its entry */
        if (add && previous_remote && !in_addr_equal(af, previous_remote, remote)) {
                if (af == AF_INET)
                        data[0] = previous_remote->in.s_addr;
                else
                        memcpy(data, &previous_remote->in6, sizeof(previous_remote->in6));

                r = nft_del_element(ctx->nfnl, &transaction[tsize], af, NFT_SYSTEMD_DNAT_MAP_NAME, key, sizeof(key), data, dlen);
                if (r < 0)
                        goto out_unref;

                tsize++;
        }

        if (af == AF_INET)
                data[0] = remote->in.s_addr;
        else
                memcpy(data, &remote->in6, sizeof(remote->in6));

        assert(tsize < NFT_DNAT_MSGS);
        if (add)
                r = nft_add_element(ctx->nfnl, &transaction[tsize], af, NFT_SYSTEMD_DNAT_MAP_NAME, key, sizeof(key), data, dlen);
        else
                r = nft_del_element(ctx->nfnl, &transaction[tsize], af, NFT_SYSTEMD_DNAT_MAP_NAME, key, sizeof(key), data, dlen);
        if (r < 0)
                goto out_unref;

        tsize++;
        assert(tsize < NFT_DNAT_MSGS);

        r = sd_nfnl_message_batch_end(ctx->nfnl, &transaction[tsize]);
        if (r < 0)
                goto out_unref;

        tsize++;
        assert(tsize <= NFT_DNAT_MSGS);

        r = nfnl_netlink_sendv(ctx->nfnl, transaction, tsize);
        if (r == -EOVERFLOW && af == AF_INET6) {
                /* The current implementation of DNAT in systemd requires kernel's
                 * fdb9c405e35bdc6e305b9b4e20ebc141ed14fc81 (v5.8), and the older kernel returns
                 * -EOVERFLOW. Let's treat the error as -EOPNOTSUPP. */
                log_debug_errno(r, "The current implementation of IPv6 DNAT in systemd requires kernel 5.8 or newer, ignoring: %m");
                ipv6_supported = false;
                r = -EOPNOTSUPP;
        }

out_unref:
        while (tsize > 0)
                sd_netlink_message_unref(transaction[--tsize]);

        return r < 0 ? r : 0;
}

int fw_nftables_add_local_dnat(
                FirewallContext *ctx,
                bool add,
                int af,
                int protocol,
                uint16_t local_port,
                const union in_addr_union *remote,
                uint16_t remote_port,
                const union in_addr_union *previous_remote) {

        int r;

        if (!socket_ipv6_is_supported() && af == AF_INET6)
                return -EOPNOTSUPP;

        r = fw_nftables_add_local_dnat_internal(ctx, add, af, protocol, local_port, remote, remote_port, previous_remote);
        if (r != -ENOENT)
                return r;

        /* See comment in fw_nftables_add_masquerade(). */
        r = fw_nftables_init_family(ctx->nfnl, af);
        if (r < 0)
                return r;

        /* table created anew; previous address already gone */
        return fw_nftables_add_local_dnat_internal(ctx, add, af, protocol, local_port, remote, remote_port, NULL);
}
