use super::{FirewallArguments, FirewallPolicy};
use crate::split_tunnel;
use ipnetwork::IpNetwork;
use nftnl::{
    Batch, Chain, FinalizedBatch, ProtoFamily, Rule, Table,
    expr::{self, IcmpCode, Payload, RejectionType, Verdict},
    nft_expr, table,
};
use std::{
    env,
    ffi::CStr,
    fs, io,
    net::{IpAddr, Ipv4Addr},
    sync::LazyLock,
};
use talpid_cgroup::v2::CGroup2;
use talpid_tunnel::TunnelMetadata;
use talpid_types::net::{
    ALLOWED_LAN_MULTICAST_NETS, ALLOWED_LAN_NETS, AllowedEndpoint, AllowedTunnelTraffic, Endpoint,
    TransportProtocol,
};

/// Priority for rules that tag split tunneling packets. Equals NF_IP_PRI_MANGLE.
const MANGLE_CHAIN_PRIORITY: i32 = libc::NF_IP_PRI_MANGLE;
const PREROUTING_CHAIN_PRIORITY: i32 = libc::NF_IP_PRI_CONNTRACK + 1;
const PROC_SYS_NET_IPV4_CONF_SRC_VALID_MARK: &str = "/proc/sys/net/ipv4/conf/all/src_valid_mark";
const PROC_SYS_NET_IPV4_CONF_ARP_IGNORE: &str = "/proc/sys/net/ipv4/conf/all/arp_ignore";

/// Results returned by functions interacting with Linux netfilter.
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can happen when interacting with Linux netfilter.
#[derive(thiserror::Error, Debug)]
pub enum Error {
    /// Unable to open netlink socket to netfilter.
    #[error("Unable to open netlink socket to netfilter")]
    NetlinkOpenError(#[source] io::Error),

    /// Unable to send netlink command to netfilter.
    #[error("Unable to send netlink command to netfilter")]
    NetlinkSendError(#[source] io::Error),

    /// Error while reading from netlink socket.
    #[error("Error while reading from netlink socket")]
    NetlinkRecvError(#[source] io::Error),

    /// Error while processing an incoming netlink message.
    #[error("Error while processing an incoming netlink message")]
    ProcessNetlinkError(#[source] io::Error),

    /// Failed to verify that our tables are set. Probably means that
    /// it's the host that does not support nftables properly.
    #[error("Failed to set firewall rules")]
    NetfilterTableNotSetError,

    /// Unable to translate network interface name into index.
    #[error("Unable to translate network interface name \"{0}\" into index")]
    LookupIfaceIndexError(String, #[source] crate::linux::IfaceIndexLookupError),
}

/// TODO(linus): This crate is not supposed to be Mullvad-aware. So at some point this should be
/// replaced by allowing the table name to be configured from the public API of this crate.
const TABLE_NAME: &CStr = c"mullvad";
const IN_CHAIN_NAME: &CStr = c"input";
const OUT_CHAIN_NAME: &CStr = c"output";
const FORWARD_CHAIN_NAME: &CStr = c"forward";
const PREROUTING_CHAIN_NAME: &CStr = c"prerouting";
const MANGLE_CHAIN_NAME: &CStr = c"mangle";
const NAT_CHAIN_NAME: &CStr = c"nat";

/// Allows controlling whether firewall rules should have packet counters or not from an env
/// variable. Useful for debugging the rules.
static ADD_COUNTERS: LazyLock<bool> = LazyLock::new(|| {
    env::var("TALPID_FIREWALL_DEBUG")
        .map(|v| v != "0")
        .unwrap_or(false)
});

static DONT_SET_SRC_VALID_MARK: LazyLock<bool> = LazyLock::new(|| {
    env::var("TALPID_FIREWALL_DONT_SET_SRC_VALID_MARK")
        .map(|v| v != "0")
        .unwrap_or(false)
});
static DONT_SET_ARP_IGNORE: LazyLock<bool> = LazyLock::new(|| {
    env::var("TALPID_FIREWALL_DONT_SET_ARP_IGNORE")
        .map(|v| v != "0")
        .unwrap_or(false)
});

#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum Direction {
    In,
    Out,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum End {
    Src,
    Dst,
}

/// The Linux implementation for the firewall and DNS.
pub struct Firewall {
    /// Firewall mark is used to mark traffic which should be able to bypass the tunnel
    fwmark: u32,
    /// The cgroup2 used for split tunneling.
    /// Traffic from processes in this cgroup2 should be allowed outside the tunnel.
    excluded_cgroup2: Option<CGroup2>,
    /// The net_cls id of the v1 cgroup used for split tunneling.
    /// This is used as a fallback to [`Self::excluded_cgroup2`] since old kernels don't support cgroups v2.
    net_cls: Option<u32>,
}

impl Firewall {
    /// Create a `Firewall` from a `FirewallArguments`.
    pub fn from_args(args: FirewallArguments) -> Result<Self> {
        Firewall::new(
            args.linux_ids.fwmark,
            args.linux_ids.excluded_cgroup2,
            args.linux_ids.net_cls,
        )
    }

    /// Create a `Firewall`.
    ///
    /// - `fwmark` is the metadata mark used by nft to allow some packets outside the tunnel.
    /// - `excluded_cgroup2` is the cgroup2 used by nft to apply `fwmark` on some packets.
    pub fn new(
        fwmark: u32,
        excluded_cgroup2: Option<CGroup2>,
        net_cls: Option<u32>,
    ) -> Result<Self> {
        if cfg!(not(feature = "cgroup2")) && excluded_cgroup2.is_some() {
            log::error!("cgroup2 support disabled, but excluded_cgroup2 was provided");
        }

        Ok(Firewall {
            fwmark,
            excluded_cgroup2,
            net_cls,
        })
    }

    /// Apply a [`FirewallPolicy`] by setting up [`TABLE_NAME`] nftable.
    pub fn apply_policy(&mut self, policy: FirewallPolicy) -> Result<()> {
        let table = Table::new(TABLE_NAME, ProtoFamily::Inet);
        let batch = PolicyBatch::new(&table).finalize(&policy, self)?;
        Self::send_and_process(&batch)?;
        Self::apply_kernel_config(&policy);
        self.verify_tables(&[TABLE_NAME])
    }

    /// Remove [`TABLE_NAME`] nftable.
    pub fn reset_policy(&mut self) -> Result<()> {
        let table = Table::new(TABLE_NAME, ProtoFamily::Inet);
        let mut batch = Batch::new();

        // Our batch will add and remove the table even though the goal is just to remove
        // it. This because only removing it throws a strange error if the
        // table does not exist.
        batch.add(&table, nftnl::MsgType::Add);
        batch.add(&table, nftnl::MsgType::Del);

        let batch = batch.finalize();

        log::debug!("Removing table and chain from netfilter");
        Self::send_and_process(&batch)?;

        Ok(())
    }

    fn apply_kernel_config(policy: &FirewallPolicy) {
        if *DONT_SET_SRC_VALID_MARK {
            log::debug!("Not setting src_valid_mark");
        } else if let FirewallPolicy::Connecting { .. } = policy
            && let Err(err) = set_src_valid_mark_sysctl()
        {
            log::error!("Failed to apply src_valid_mark: {}", err);
        }

        // When we have a tunnel with an IP configured, we configure the system
        // to not reply to arp requests for this tunnel IP *on other interfaces*.
        // By default, Linux responds to incoming arp requests for any IP configured on any
        // interface on the system. This makes it possible to via ARP-pinging figure out the
        // VPN in-tunnel IP by spamming ARP requests to any physical interface on the device.
        //
        // We never store the initial value and restore it. We deem the default value
        // to be too relaxed and don't see any reason why anyone would want a more relaxed
        // setting than the one we are setting here.
        if *DONT_SET_ARP_IGNORE {
            log::debug!("Not setting arp_ignore");
        } else if let FirewallPolicy::Connecting { .. } | FirewallPolicy::Connected { .. } = policy
            && let Err(err) = lock_down_arp_ignore_sysctl()
        {
            log::error!("Failed to apply arp_ignore: {}", err);
        }
    }

    /// Send a [`nftnl::FinalizedBatch`] to the kernel and process the result.
    pub fn send_and_process(batch: &FinalizedBatch) -> Result<()> {
        // Create a netlink socket to netfilter.
        let socket = mnl::Socket::new(mnl::Bus::Netfilter).map_err(Error::NetlinkOpenError)?;
        let portid = socket.portid();

        // Send all the bytes in the batch.
        socket.send_all(batch).map_err(Error::NetlinkSendError)?;

        // TODO: this buffer must be aligned to nlmsghdr
        let mut buffer = vec![0; nftnl::nft_nlmsg_maxsize() as usize];
        let mut expected_seqs = batch.sequence_numbers();

        // Process acknowledgment messages from netfilter.
        while !expected_seqs.is_empty() {
            for message in socket
                .recv(&mut buffer[..])
                .map_err(Error::NetlinkSendError)?
            {
                let message = message.map_err(Error::ProcessNetlinkError)?;
                let expected_seq = expected_seqs.next().expect("Unexpected ACK");
                // Validate sequence number and check for error messages
                mnl::cb_run(message, expected_seq, portid).map_err(Error::ProcessNetlinkError)?;
            }
        }
        Ok(())
    }

    fn verify_tables(&self, expected_tables: &[&CStr]) -> Result<()> {
        let socket = mnl::Socket::new(mnl::Bus::Netfilter).map_err(Error::NetlinkOpenError)?;
        let portid = socket.portid();
        let seq = 1;

        let get_tables_msg = table::get_tables_nlmsg(seq);
        socket
            .send(&get_tables_msg)
            .map_err(Error::NetlinkSendError)?;

        let mut table_set = std::collections::HashSet::new();
        let mut buffer = vec![0; nftnl::nft_nlmsg_maxsize() as usize];

        // Process acknowledgment messages from netfilter.
        for message in socket
            .recv(&mut buffer[..])
            .map_err(Error::NetlinkRecvError)?
        {
            let message = message.map_err(Error::ProcessNetlinkError)?;
            // Validate sequence number and check for error messages
            mnl::cb_run2(message, seq, portid, table::get_tables_cb, &mut table_set)
                .map_err(Error::ProcessNetlinkError)?;
        }

        for expected_table in expected_tables {
            if !table_set.contains(*expected_table) {
                log::error!(
                    "Expected '{}' netfilter table to be set, but it is not",
                    expected_table.to_string_lossy()
                );
                return Err(Error::NetfilterTableNotSetError);
            }
        }
        Ok(())
    }
}

struct PolicyBatch<'a> {
    batch: Batch,
    in_chain: Chain<'a>,
    out_chain: Chain<'a>,
    forward_chain: Chain<'a>,
    prerouting_chain: Chain<'a>,
    mangle_chain: Chain<'a>,
    nat_chain: Chain<'a>,
}

impl<'a> PolicyBatch<'a> {
    /// Bootstrap a new nftnl message batch object and add the initial messages creating the
    /// table and chains.
    pub fn new(table: &'a Table) -> Self {
        let mut batch = Batch::new();

        // Create the table if it does not exist and clear it otherwise.
        batch.add(table, nftnl::MsgType::Add);
        batch.add(table, nftnl::MsgType::Del);
        batch.add(table, nftnl::MsgType::Add);

        let mut prerouting_chain = Chain::new(PREROUTING_CHAIN_NAME, table);
        prerouting_chain.set_hook(nftnl::Hook::PreRouting, PREROUTING_CHAIN_PRIORITY);
        prerouting_chain.set_type(nftnl::ChainType::Filter);
        batch.add(&prerouting_chain, nftnl::MsgType::Add);

        let mut out_chain = Chain::new(OUT_CHAIN_NAME, table);
        out_chain.set_hook(nftnl::Hook::Out, 0);
        out_chain.set_policy(nftnl::Policy::Drop);
        batch.add(&out_chain, nftnl::MsgType::Add);

        let mut in_chain = Chain::new(IN_CHAIN_NAME, table);
        in_chain.set_hook(nftnl::Hook::In, 0);
        in_chain.set_policy(nftnl::Policy::Drop);
        batch.add(&in_chain, nftnl::MsgType::Add);

        let mut forward_chain = Chain::new(FORWARD_CHAIN_NAME, table);
        forward_chain.set_hook(nftnl::Hook::Forward, 0);
        forward_chain.set_policy(nftnl::Policy::Drop);
        batch.add(&forward_chain, nftnl::MsgType::Add);

        let mut mangle_chain = Chain::new(MANGLE_CHAIN_NAME, table);
        mangle_chain.set_hook(nftnl::Hook::Out, MANGLE_CHAIN_PRIORITY);
        mangle_chain.set_type(nftnl::ChainType::Route);
        mangle_chain.set_policy(nftnl::Policy::Accept);
        batch.add(&mangle_chain, nftnl::MsgType::Add);

        let mut nat_chain = Chain::new(NAT_CHAIN_NAME, table);
        nat_chain.set_hook(nftnl::Hook::PostRouting, libc::NF_IP_PRI_NAT_SRC);
        nat_chain.set_type(nftnl::ChainType::Nat);
        nat_chain.set_policy(nftnl::Policy::Accept);
        batch.add(&nat_chain, nftnl::MsgType::Add);

        PolicyBatch {
            batch,
            in_chain,
            out_chain,
            forward_chain,
            prerouting_chain,
            mangle_chain,
            nat_chain,
        }
    }

    /// Finalize the nftnl message batch by adding every firewall rule needed to satisfy the given
    /// policy.
    pub fn finalize(
        mut self,
        policy: &FirewallPolicy,
        firewall: &Firewall,
    ) -> Result<FinalizedBatch> {
        self.add_loopback_rules()?;
        // TODO: Investigate if these rules could/should be handled by PidManager instead.
        // It would allow for the firewall to be set up in a secure way even though split tunneling
        // does not work, which is okay. It would also allow us to de-duplicate some copy-paste
        // code which is present both in this module and in PidManager ..
        self.add_split_tunneling_rules(policy, firewall)?;
        self.add_dhcp_client_rules();
        self.add_ndp_rules();
        self.add_policy_specific_rules(policy, firewall.fwmark)?;

        Ok(self.batch.finalize())
    }

    /// Allow split-tunneled traffic outside the tunnel.
    fn add_split_tunneling_rules(
        &mut self,
        policy: &FirewallPolicy,
        firewall: &Firewall,
    ) -> Result<()> {
        if cfg!(feature = "cgroup2")
            && let Some(cgroup2) = &firewall.excluded_cgroup2
        {
            self.add_actual_split_tunneling_rules(policy, firewall.fwmark, |rule| {
                // 1. From Linux kernel documentation:
                // cgroup(2) is a mechanism to organize processes hierarchically ... cgroups form a tree structure and
                // every process in the system belongs to one and only one cgroup ... On creation, all processes are put
                // in the cgroup that the parent process belongs to at the time.
                //
                // 2. From `man nft` on "Socket expression":
                // .. You can also use it [socket expression] to match on the socket cgroupv2 at a given ancestor level,
                // e.g. if the socket belongs to cgroupv2 a/b, ancestor level 1 checks for a matching on cgroup a and
                // ancestor level 2 checks for a matching on cgroup b.
                //
                // 3. Since the current split-tunnel implementation spawn each split process into the same cgroup2, the
                // nftables rule does not have to look at any level of ancestry away *from* `cgroup`. This applies for
                // any sub-process that a split process may spawn, as per the kernel docs.
                //
                // Following from 1,2,3, `socket cgroupv2 level 1` should be fine here.
                rule.add_expr(&nft_expr!(socket cgroupv2 level 1));
                rule.add_expr(&nft_expr!(cmp == cgroup2.inode()));
            })?;
        } else if let Some(net_cls) = firewall.net_cls {
            self.add_actual_split_tunneling_rules(policy, firewall.fwmark, |rule| {
                // For cgroups v1, processes are assigned to a net_cls.                                                                                                      ║
                // This causes all packets sent by that process to be marked with the                                                                                        ║
                // cgroups classid (`net_cls`), which we can reference in nftables.
                rule.add_expr(&nft_expr!(meta cgroup));
                rule.add_expr(&nft_expr!(cmp == net_cls));
            })?;
        } else {
            log::warn!("no cgroups, skipping add_split_tunneling_rules");
        };

        Ok(())
    }

    /// Mark connections initated by processes matched by `add_selector_rules` with `fwmark`.
    fn add_actual_split_tunneling_rules(
        &mut self,
        policy: &FirewallPolicy,
        fwmark: u32,
        mut add_selector_rules: impl FnMut(&mut Rule<'_>),
    ) -> Result<()> {
        // Send select DNS requests in the tunnel
        if let FirewallPolicy::Connected {
            tunnel, dns_config, ..
        } = policy
        {
            for server in dns_config.tunnel_config() {
                let allow_rule = allow_tunnel_dns_rule(
                    &self.mangle_chain,
                    &tunnel.interface,
                    TransportProtocol::Udp,
                    *server,
                )?;
                self.batch.add(&allow_rule, nftnl::MsgType::Add);
                let allow_rule = allow_tunnel_dns_rule(
                    &self.mangle_chain,
                    &tunnel.interface,
                    TransportProtocol::Tcp,
                    *server,
                )?;
                self.batch.add(&allow_rule, nftnl::MsgType::Add);
            }
        }

        // Split tunneled processes have their PIDs added to a cgroup (v1 or v2).
        //
        // This rule matches packets sent by those processes.
        // Packet will have two new marks applied to it, the `split_tunnel::MARK`
        // as a connection tracking mark and the `fwmark` as packet metadata.
        let mut rule = Rule::new(&self.mangle_chain);
        // Add rules for matching packets from a cgroup.
        // This is the only implementation detail of the split tunneling rule that differs between cgroup v1 and v2.
        add_selector_rules(&mut rule);
        // Loads `split_tunnel::MARK` into first nftnl register
        rule.add_expr(&nft_expr!(immediate data split_tunnel::MARK));
        // Sets `split_tunnel::MARK` as connection tracker mark
        rule.add_expr(&nft_expr!(ct mark set));
        // Loads `fwmark` into first nftnl register
        rule.add_expr(&nft_expr!(immediate data fwmark));
        // Sets `fwmark` as metadata mark for packet
        rule.add_expr(&nft_expr!(meta mark set));
        if *ADD_COUNTERS {
            rule.add_expr(&nft_expr!(counter));
        }
        self.batch.add(&rule, nftnl::MsgType::Add);

        for chain in &[&self.in_chain, &self.out_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            rule.add_expr(&nft_expr!(ct mark));
            rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK));
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }

        // Block remaining marked outgoing in-tunnel traffic
        if let FirewallPolicy::Connected { tunnel, .. } = policy {
            let mut block_tunnel_rule = Rule::new(&self.nat_chain);
            check_iface(&mut block_tunnel_rule, Direction::Out, &tunnel.interface)?;
            block_tunnel_rule.add_expr(&nft_expr!(ct mark));
            block_tunnel_rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK));
            add_verdict(&mut block_tunnel_rule, &Verdict::Drop);
            self.batch.add(&block_tunnel_rule, nftnl::MsgType::Add);
        }

        // Fix source IP address in rerouted packets using masquerade.
        // Don't masquerade packets on the loopback device.
        let mut rule = Rule::new(&self.nat_chain);

        let iface_index = crate::linux::iface_index("lo")
            .map_err(|e| Error::LookupIfaceIndexError("lo".to_string(), e))?;
        rule.add_expr(&nft_expr!(meta oif));
        rule.add_expr(&nft_expr!(cmp != iface_index));

        rule.add_expr(&nft_expr!(ct mark));
        rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK));

        rule.add_expr(&nft_expr!(masquerade));
        if *ADD_COUNTERS {
            rule.add_expr(&nft_expr!(counter));
        }
        self.batch.add(&rule, nftnl::MsgType::Add);

        // Route incoming traffic correctly to prevent strict rpf from rejecting packets
        // for excluded processes
        if let FirewallPolicy::Connected { tunnel, .. } = policy {
            let mut prerouting_rule = Rule::new(&self.prerouting_chain);
            check_not_iface(&mut prerouting_rule, Direction::In, &tunnel.interface)?;
            prerouting_rule.add_expr(&nft_expr!(ct mark));
            prerouting_rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK));
            prerouting_rule.add_expr(&nft_expr!(immediate data fwmark));
            prerouting_rule.add_expr(&nft_expr!(meta mark set));
            if *ADD_COUNTERS {
                prerouting_rule.add_expr(&nft_expr!(counter));
            }
            self.batch.add(&prerouting_rule, nftnl::MsgType::Add);
        }

        Ok(())
    }

    fn add_loopback_rules(&mut self) -> Result<()> {
        const LOOPBACK_IFACE_NAME: &str = "lo";
        self.batch.add(
            &allow_interface_rule(&self.out_chain, Direction::Out, LOOPBACK_IFACE_NAME)?,
            nftnl::MsgType::Add,
        );
        self.batch.add(
            &allow_interface_rule(&self.in_chain, Direction::In, LOOPBACK_IFACE_NAME)?,
            nftnl::MsgType::Add,
        );
        Ok(())
    }

    fn add_dhcp_client_rules(&mut self) {
        use self::TransportProtocol::Udp;
        // Outgoing DHCPv4 request
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut out_v4 = Rule::new(chain);
            check_port(&mut out_v4, Udp, End::Src, super::DHCPV4_CLIENT_PORT);
            check_ip(&mut out_v4, End::Dst, IpAddr::V4(Ipv4Addr::BROADCAST));
            check_port(&mut out_v4, Udp, End::Dst, super::DHCPV4_SERVER_PORT);
            add_verdict(&mut out_v4, &Verdict::Accept);
            self.batch.add(&out_v4, nftnl::MsgType::Add);
        }
        // Incoming DHCPv4 response
        for chain in &[&self.in_chain, &self.forward_chain] {
            let mut in_v4 = Rule::new(chain);
            check_port(&mut in_v4, Udp, End::Src, super::DHCPV4_SERVER_PORT);
            check_port(&mut in_v4, Udp, End::Dst, super::DHCPV4_CLIENT_PORT);
            add_verdict(&mut in_v4, &Verdict::Accept);
            self.batch.add(&in_v4, nftnl::MsgType::Add);
        }
        // Outgoing DHCPv6 request
        for chain in &[&self.out_chain, &self.forward_chain] {
            for dhcpv6_server in super::DHCPV6_SERVER_ADDRS {
                let mut out_v6 = Rule::new(chain);
                check_net(&mut out_v6, End::Src, super::IPV6_LINK_LOCAL);
                check_port(&mut out_v6, Udp, End::Src, super::DHCPV6_CLIENT_PORT);
                check_ip(&mut out_v6, End::Dst, dhcpv6_server);
                check_port(&mut out_v6, Udp, End::Dst, super::DHCPV6_SERVER_PORT);
                add_verdict(&mut out_v6, &Verdict::Accept);
                self.batch.add(&out_v6, nftnl::MsgType::Add);
            }
        }
        // Incoming DHCPv6 response
        for chain in &[&self.in_chain, &self.forward_chain] {
            let mut in_v6 = Rule::new(chain);
            check_net(&mut in_v6, End::Src, super::IPV6_LINK_LOCAL);
            check_port(&mut in_v6, Udp, End::Src, super::DHCPV6_SERVER_PORT);
            check_net(&mut in_v6, End::Dst, super::IPV6_LINK_LOCAL);
            check_port(&mut in_v6, Udp, End::Dst, super::DHCPV6_CLIENT_PORT);
            add_verdict(&mut in_v6, &Verdict::Accept);
            self.batch.add(&in_v6, nftnl::MsgType::Add);
        }
    }

    fn add_ndp_rules(&mut self) {
        // The non-0 constants used are icmpv6 transport header types corresponding
        // to the type of message that each block deals with.

        // Outgoing Router solicitation (part of NDP)
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_ip(&mut rule, End::Dst, super::ROUTER_SOLICITATION_OUT_DST_ADDR);
            check_icmpv6(&mut rule, 133, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        // Incoming Router advertisement (part of NDP)
        for chain in &[&self.in_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_net(&mut rule, End::Src, super::IPV6_LINK_LOCAL);
            check_icmpv6(&mut rule, 134, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        // Incoming Redirect (part of NDP)
        for chain in &[&self.in_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_net(&mut rule, End::Src, super::IPV6_LINK_LOCAL);
            check_icmpv6(&mut rule, 137, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        // Outgoing Neighbor solicitation (part of NDP)
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_net(&mut rule, End::Dst, super::SOLICITED_NODE_MULTICAST);
            check_icmpv6(&mut rule, 135, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_net(&mut rule, End::Dst, super::IPV6_LINK_LOCAL);
            check_icmpv6(&mut rule, 135, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        // Incoming Neighbor solicitation (part of NDP)
        for chain in &[&self.in_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_net(&mut rule, End::Src, super::IPV6_LINK_LOCAL);
            check_icmpv6(&mut rule, 135, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        // Outgoing Neighbor advertisement (part of NDP)
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_net(&mut rule, End::Dst, super::IPV6_LINK_LOCAL);
            check_icmpv6(&mut rule, 136, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        // Incoming Neighbor advertisement (part of NDP)
        for chain in &[&self.in_chain, &self.forward_chain] {
            let mut rule = Rule::new(chain);
            check_icmpv6(&mut rule, 136, 0);
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
    }

    fn add_policy_specific_rules(&mut self, policy: &FirewallPolicy, fwmark: u32) -> Result<()> {
        let allow_lan = match policy {
            FirewallPolicy::Connecting {
                peer_endpoints,
                tunnel,
                allow_lan,
                allowed_endpoint,
                allowed_tunnel_traffic,
            } => {
                for endpoint in peer_endpoints {
                    self.add_allow_tunnel_endpoint_rules(endpoint, fwmark);
                }
                self.add_allow_endpoint_rules(allowed_endpoint);

                // Important to block DNS after allow relay rule (so the relay can operate
                // over port 53) but before allow LAN (so DNS does not leak to the LAN)
                self.add_drop_dns_rule();

                if let Some(tunnel) = tunnel {
                    match allowed_tunnel_traffic {
                        AllowedTunnelTraffic::All => {
                            self.add_allow_tunnel_rules(&tunnel.interface)?;
                        }
                        AllowedTunnelTraffic::None => (),
                        AllowedTunnelTraffic::One(endpoint) => {
                            self.add_allow_in_tunnel_endpoint_rules(&tunnel.interface, endpoint)?;
                        }
                        AllowedTunnelTraffic::Two(endpoint1, endpoint2) => {
                            self.add_allow_in_tunnel_endpoint_rules(&tunnel.interface, endpoint1)?;
                            self.add_allow_in_tunnel_endpoint_rules(&tunnel.interface, endpoint2)?;
                        }
                    }
                    if *allow_lan {
                        self.add_block_cve_2019_14899(tunnel);
                    }
                }
                *allow_lan
            }
            FirewallPolicy::Connected {
                peer_endpoints,
                tunnel,
                allow_lan,
                dns_config,
            } => {
                for endpoint in peer_endpoints {
                    self.add_allow_tunnel_endpoint_rules(endpoint, fwmark);
                }

                for server in dns_config.tunnel_config() {
                    self.add_allow_tunnel_dns_rule(
                        &tunnel.interface,
                        TransportProtocol::Udp,
                        *server,
                    )?;
                    self.add_allow_tunnel_dns_rule(
                        &tunnel.interface,
                        TransportProtocol::Tcp,
                        *server,
                    )?;
                }
                for server in dns_config.non_tunnel_config() {
                    self.add_allow_local_dns_rule(
                        &tunnel.interface,
                        TransportProtocol::Udp,
                        *server,
                    )?;
                    self.add_allow_local_dns_rule(
                        &tunnel.interface,
                        TransportProtocol::Tcp,
                        *server,
                    )?;
                }

                // Important to block DNS *before* we allow the tunnel and allow LAN. So DNS
                // can't leak to the wrong IPs in the tunnel or on the LAN.
                self.add_drop_dns_rule();
                self.add_allow_tunnel_rules(&tunnel.interface)?;
                if *allow_lan {
                    self.add_block_cve_2019_14899(tunnel);
                }
                *allow_lan
            }
            FirewallPolicy::Blocked {
                allow_lan,
                allowed_endpoint,
            } => {
                if let Some(endpoint) = allowed_endpoint {
                    self.add_allow_endpoint_rules(endpoint);
                }

                // Important to drop DNS before allowing LAN (to stop DNS leaking to the LAN)
                self.add_drop_dns_rule();
                *allow_lan
            }
        };

        if allow_lan {
            self.add_allow_lan_rules();
        }

        // Reject any remaining outgoing traffic
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut reject_rule = Rule::new(chain);
            add_verdict(
                &mut reject_rule,
                &Verdict::Reject(RejectionType::Icmp(IcmpCode::PortUnreach)),
            );
            self.batch.add(&reject_rule, nftnl::MsgType::Add);
        }

        Ok(())
    }

    fn add_allow_tunnel_endpoint_rules(&mut self, endpoint: &AllowedEndpoint, fwmark: u32) {
        let mut prerouting_rule = Rule::new(&self.prerouting_chain);
        // Mark incoming traffic from endpoint with fwmark
        check_endpoint(&mut prerouting_rule, End::Src, &endpoint.endpoint);
        prerouting_rule.add_expr(&nft_expr!(immediate data fwmark));
        prerouting_rule.add_expr(&nft_expr!(meta mark set));

        if *ADD_COUNTERS {
            prerouting_rule.add_expr(&nft_expr!(counter));
        }

        self.batch.add(&prerouting_rule, nftnl::MsgType::Add);

        let mut in_rule = Rule::new(&self.in_chain);
        check_endpoint(&mut in_rule, End::Src, &endpoint.endpoint);

        // Allow all incoming traffic from established connections to the endpoint
        check_ct_established(&mut in_rule);
        add_verdict(&mut in_rule, &Verdict::Accept);

        self.batch.add(&in_rule, nftnl::MsgType::Add);

        // Allow any traffic to the endpoint which is marked with fwmark
        let mut out_rule = Rule::new(&self.out_chain);
        check_endpoint(&mut out_rule, End::Dst, &endpoint.endpoint);
        out_rule.add_expr(&nft_expr!(meta mark));
        out_rule.add_expr(&nft_expr!(cmp == fwmark));
        add_verdict(&mut out_rule, &Verdict::Accept);

        self.batch.add(&out_rule, nftnl::MsgType::Add);

        // Used for local custom bridge, allows some local socks5 proxy to send traffic to the
        // endpoint
        if endpoint.clients.allow_all() {
            let mut rule = Rule::new(&self.mangle_chain);
            check_endpoint(&mut rule, End::Dst, &endpoint.endpoint);
            rule.add_expr(&nft_expr!(immediate data split_tunnel::MARK));
            rule.add_expr(&nft_expr!(ct mark set));
            rule.add_expr(&nft_expr!(immediate data fwmark));
            rule.add_expr(&nft_expr!(meta mark set));
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
    }

    /// Adds firewall rules allow traffic to flow to the API. Allows the app to reach the API in
    /// blocked states.
    fn add_allow_endpoint_rules(&mut self, endpoint: &AllowedEndpoint) {
        let mut in_rule = Rule::new(&self.in_chain);
        // Allow incoming traffic from established connections to the endpoint
        check_endpoint(&mut in_rule, End::Src, &endpoint.endpoint);
        check_ct_established(&mut in_rule);
        if !endpoint.clients.allow_all() {
            in_rule.add_expr(&nft_expr!(meta skuid));
            in_rule.add_expr(&nft_expr!(cmp == super::ROOT_UID));
        }

        add_verdict(&mut in_rule, &Verdict::Accept);

        self.batch.add(&in_rule, nftnl::MsgType::Add);

        let mut out_rule = Rule::new(&self.out_chain);
        check_endpoint(&mut out_rule, End::Dst, &endpoint.endpoint);
        if !endpoint.clients.allow_all() {
            out_rule.add_expr(&nft_expr!(meta skuid));
            out_rule.add_expr(&nft_expr!(cmp == super::ROOT_UID));
        }
        add_verdict(&mut out_rule, &Verdict::Accept);

        self.batch.add(&out_rule, nftnl::MsgType::Add);
    }

    fn add_allow_tunnel_dns_rule(
        &mut self,
        interface: &str,
        protocol: TransportProtocol,
        host: IpAddr,
    ) -> Result<()> {
        for chain in &[&self.out_chain, &self.forward_chain] {
            let allow_rule = allow_tunnel_dns_rule(chain, interface, protocol, host)?;
            self.batch.add(&allow_rule, nftnl::MsgType::Add);
        }
        Ok(())
    }

    fn add_allow_local_dns_rule(
        &mut self,
        tunnel_interface: &str,
        protocol: TransportProtocol,
        host: IpAddr,
    ) -> Result<()> {
        let chains = [
            (&self.out_chain, Direction::Out),
            (&self.forward_chain, Direction::Out),
            (&self.in_chain, Direction::In),
            (&self.forward_chain, Direction::In),
        ];

        for (chain, direction) in &chains {
            let mut allow_rule = Rule::new(chain);
            let addr = match (host, direction) {
                (IpAddr::V4(_), Direction::Out) => nft_expr!(payload ipv4 daddr),
                (IpAddr::V6(_), Direction::Out) => nft_expr!(payload ipv6 daddr),
                (IpAddr::V4(_), Direction::In) => nft_expr!(payload ipv4 saddr),
                (IpAddr::V6(_), Direction::In) => nft_expr!(payload ipv6 saddr),
            };

            let port_dir = match direction {
                Direction::In => End::Src,
                Direction::Out => End::Dst,
            };

            check_not_iface(&mut allow_rule, *direction, tunnel_interface)?;
            check_port(&mut allow_rule, protocol, port_dir, 53);
            check_l3proto(&mut allow_rule, host);

            allow_rule.add_expr(&addr);
            allow_rule.add_expr(&nft_expr!(cmp == host));

            if let Direction::In = direction {
                check_ct_established(&mut allow_rule);
            }

            add_verdict(&mut allow_rule, &Verdict::Accept);

            self.batch.add(&allow_rule, nftnl::MsgType::Add);
        }

        Ok(())
    }

    /// Blocks all outgoing DNS (port 53) on both TCP and UDP
    fn add_drop_dns_rule(&mut self) {
        for chain in &[&self.out_chain, &self.forward_chain] {
            let mut block_udp_rule = Rule::new(chain);
            check_port(&mut block_udp_rule, TransportProtocol::Udp, End::Dst, 53);
            add_verdict(
                &mut block_udp_rule,
                &Verdict::Reject(RejectionType::Icmp(IcmpCode::PortUnreach)),
            );
            self.batch.add(&block_udp_rule, nftnl::MsgType::Add);

            let mut block_tcp_rule = Rule::new(chain);
            check_port(&mut block_tcp_rule, TransportProtocol::Tcp, End::Dst, 53);
            add_verdict(&mut block_tcp_rule, &Verdict::Reject(RejectionType::TcpRst));
            self.batch.add(&block_tcp_rule, nftnl::MsgType::Add);
        }
    }

    fn add_allow_in_tunnel_endpoint_rules(
        &mut self,
        tunnel_interface: &str,
        endpoint: &Endpoint,
    ) -> Result<()> {
        for (chain, dir, end) in [
            (&self.out_chain, Direction::Out, End::Dst),
            (&self.in_chain, Direction::In, End::Src),
        ] {
            let mut rule = Rule::new(chain);
            check_iface(&mut rule, dir, tunnel_interface)?;
            check_ip(&mut rule, end, endpoint.address.ip());
            check_port(&mut rule, endpoint.protocol, end, endpoint.address.port());
            add_verdict(&mut rule, &Verdict::Accept);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
        Ok(())
    }

    fn add_allow_tunnel_rules(&mut self, tunnel_interface: &str) -> Result<()> {
        self.batch.add(
            &allow_interface_rule(&self.out_chain, Direction::Out, tunnel_interface)?,
            nftnl::MsgType::Add,
        );
        self.batch.add(
            &allow_interface_rule(&self.forward_chain, Direction::Out, tunnel_interface)?,
            nftnl::MsgType::Add,
        );
        self.batch.add(
            &allow_interface_rule(&self.in_chain, Direction::In, tunnel_interface)?,
            nftnl::MsgType::Add,
        );

        // Forward packets coming from the tunnel interface only if they are from established
        // connections.
        let mut interface_rule = Rule::new(&self.forward_chain);
        check_iface(&mut interface_rule, Direction::In, tunnel_interface)?;
        check_ct_established(&mut interface_rule);
        add_verdict(&mut interface_rule, &Verdict::Accept);
        self.batch.add(&interface_rule, nftnl::MsgType::Add);

        Ok(())
    }

    /// Adds rules for stopping [CVE-2019-14899](https://seclists.org/oss-sec/2019/q4/122).
    /// An attacker on the same local network as the VPN connected device could figure out
    /// the tunnel IP the device used if the device was set to not filter reverse path (rp_filter.)
    /// These rules stops all packets coming in to the tunnel IP. As such, these rules must come
    /// after the rule allowing the tunnel, otherwise even the tunnel can't talk to that IP.
    fn add_block_cve_2019_14899(&mut self, tunnel: &TunnelMetadata) {
        for tunnel_ip in &tunnel.ips {
            let mut rule = Rule::new(&self.in_chain);
            check_ip(&mut rule, End::Dst, *tunnel_ip);
            add_verdict(&mut rule, &Verdict::Drop);
            self.batch.add(&rule, nftnl::MsgType::Add);
        }
    }

    fn add_allow_lan_rules(&mut self) {
        // Output and forward chains
        for chain in &[&self.out_chain, &self.forward_chain] {
            // LAN -> LAN
            for net in ALLOWED_LAN_NETS {
                let mut out_rule = Rule::new(chain);
                check_net(&mut out_rule, End::Dst, net);
                add_verdict(&mut out_rule, &Verdict::Accept);
                self.batch.add(&out_rule, nftnl::MsgType::Add);
            }

            // LAN -> Multicast
            for net in ALLOWED_LAN_MULTICAST_NETS {
                let mut rule = Rule::new(chain);
                check_net(&mut rule, End::Dst, net);
                add_verdict(&mut rule, &Verdict::Accept);
                self.batch.add(&rule, nftnl::MsgType::Add);
            }
        }

        // Input chain
        // LAN -> LAN
        for net in ALLOWED_LAN_NETS {
            let mut in_rule = Rule::new(&self.in_chain);
            check_net(&mut in_rule, End::Src, net);
            add_verdict(&mut in_rule, &Verdict::Accept);
            self.batch.add(&in_rule, nftnl::MsgType::Add);
        }
        self.add_dhcp_server_rules();
    }

    fn add_dhcp_server_rules(&mut self) {
        use TransportProtocol::Udp;
        // Outgoing DHCPv4 response
        {
            let mut out_v4 = Rule::new(&self.out_chain);
            check_port(&mut out_v4, Udp, End::Src, super::DHCPV4_SERVER_PORT);
            check_port(&mut out_v4, Udp, End::Dst, super::DHCPV4_CLIENT_PORT);
            add_verdict(&mut out_v4, &Verdict::Accept);
            self.batch.add(&out_v4, nftnl::MsgType::Add);
        }
        // Incoming DHCPv4 request
        {
            let mut in_v4 = Rule::new(&self.in_chain);
            check_port(&mut in_v4, Udp, End::Src, super::DHCPV4_CLIENT_PORT);
            check_endpoint(
                &mut in_v4,
                End::Dst,
                &Endpoint::new(Ipv4Addr::BROADCAST, super::DHCPV4_SERVER_PORT, Udp),
            );
            add_verdict(&mut in_v4, &Verdict::Accept);
            self.batch.add(&in_v4, nftnl::MsgType::Add);
        }
    }
}

fn allow_tunnel_dns_rule<'a>(
    chain: &'a Chain<'_>,
    iface: &str,
    protocol: TransportProtocol,
    host: IpAddr,
) -> Result<Rule<'a>> {
    let mut rule = Rule::new(chain);
    check_iface(&mut rule, Direction::Out, iface)?;
    check_port(&mut rule, protocol, End::Dst, 53);

    let daddr = match host {
        IpAddr::V4(_) => nft_expr!(payload ipv4 daddr),
        IpAddr::V6(_) => nft_expr!(payload ipv6 daddr),
    };
    if chain.get_table().get_family() == ProtoFamily::Inet {
        check_l3proto(&mut rule, host);
    }

    rule.add_expr(&daddr);
    rule.add_expr(&nft_expr!(cmp == host));
    add_verdict(&mut rule, &Verdict::Accept);

    Ok(rule)
}

fn allow_interface_rule<'a>(
    chain: &'a Chain<'_>,
    direction: Direction,
    iface: &str,
) -> Result<Rule<'a>> {
    let mut rule = Rule::new(chain);
    check_iface(&mut rule, direction, iface)?;
    add_verdict(&mut rule, &Verdict::Accept);

    Ok(rule)
}

fn check_iface(rule: &mut Rule<'_>, direction: Direction, iface: &str) -> Result<()> {
    let iface_index = crate::linux::iface_index(iface)
        .map_err(|e| Error::LookupIfaceIndexError(iface.to_owned(), e))?;
    rule.add_expr(&match direction {
        Direction::In => nft_expr!(meta iif),
        Direction::Out => nft_expr!(meta oif),
    });
    rule.add_expr(&nft_expr!(cmp == iface_index));
    Ok(())
}

fn check_not_iface(rule: &mut Rule<'_>, direction: Direction, iface: &str) -> Result<()> {
    let iface_index = crate::linux::iface_index(iface)
        .map_err(|e| Error::LookupIfaceIndexError(iface.to_owned(), e))?;
    rule.add_expr(&match direction {
        Direction::In => nft_expr!(meta iif),
        Direction::Out => nft_expr!(meta oif),
    });
    rule.add_expr(&nft_expr!(cmp != iface_index));
    Ok(())
}

fn check_net(rule: &mut Rule<'_>, end: End, net: impl Into<IpNetwork>) {
    let net = net.into();
    // Must check network layer protocol before loading network layer payload
    check_l3proto(rule, net.ip());

    rule.add_expr(&match (net, end) {
        (IpNetwork::V4(_), End::Src) => nft_expr!(payload ipv4 saddr),
        (IpNetwork::V4(_), End::Dst) => nft_expr!(payload ipv4 daddr),
        (IpNetwork::V6(_), End::Src) => nft_expr!(payload ipv6 saddr),
        (IpNetwork::V6(_), End::Dst) => nft_expr!(payload ipv6 daddr),
    });
    // Check that packet subnet is the same as `net`
    match net {
        IpNetwork::V4(_) => rule.add_expr(&nft_expr!(bitwise mask net.mask(), xor 0u32)),
        IpNetwork::V6(_) => rule.add_expr(&nft_expr!(bitwise mask net.mask(), xor &[0u16; 8][..])),
    };
    rule.add_expr(&nft_expr!(cmp == net.ip()));
}

fn check_icmpv6(rule: &mut Rule<'_>, r#type: u8, code: u8) {
    rule.add_expr(&nft_expr!(meta l4proto));
    rule.add_expr(&nft_expr!(cmp == libc::IPPROTO_ICMPV6 as u8));

    rule.add_expr(&Payload::Transport(
        nftnl::expr::TransportHeaderField::Icmpv6(nftnl::expr::Icmpv6HeaderField::Type),
    ));
    rule.add_expr(&nft_expr!(cmp == r#type));
    rule.add_expr(&nftnl::expr::Payload::Transport(
        nftnl::expr::TransportHeaderField::Icmpv6(nftnl::expr::Icmpv6HeaderField::Code),
    ));
    rule.add_expr(&nft_expr!(cmp == code));
}

fn check_endpoint(rule: &mut Rule<'_>, end: End, endpoint: &Endpoint) {
    check_ip(rule, end, endpoint.address.ip());
    check_port(rule, endpoint.protocol, end, endpoint.address.port());
}

fn check_ip(rule: &mut Rule<'_>, end: End, ip: impl Into<IpAddr>) {
    let ip = ip.into();
    // Must check network layer protocol before loading network layer payload
    check_l3proto(rule, ip);

    rule.add_expr(&match (ip, end) {
        (IpAddr::V4(..), End::Src) => nft_expr!(payload ipv4 saddr),
        (IpAddr::V4(..), End::Dst) => nft_expr!(payload ipv4 daddr),
        (IpAddr::V6(..), End::Src) => nft_expr!(payload ipv6 saddr),
        (IpAddr::V6(..), End::Dst) => nft_expr!(payload ipv6 daddr),
    });
    match ip {
        IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
        IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
    }
}

fn check_port(rule: &mut Rule<'_>, protocol: TransportProtocol, end: End, port: u16) {
    // Must check transport layer protocol before loading transport layer payload
    check_l4proto(rule, protocol);

    rule.add_expr(&match (protocol, end) {
        (TransportProtocol::Udp, End::Src) => nft_expr!(payload udp sport),
        (TransportProtocol::Udp, End::Dst) => nft_expr!(payload udp dport),
        (TransportProtocol::Tcp, End::Src) => nft_expr!(payload tcp sport),
        (TransportProtocol::Tcp, End::Dst) => nft_expr!(payload tcp dport),
    });
    rule.add_expr(&nft_expr!(cmp == port.to_be()));
}

fn check_l3proto(rule: &mut Rule<'_>, ip: IpAddr) {
    rule.add_expr(&nft_expr!(meta nfproto));
    rule.add_expr(&nft_expr!(cmp == l3proto(ip)));
}

fn l3proto(addr: IpAddr) -> u8 {
    match addr {
        IpAddr::V4(_) => libc::NFPROTO_IPV4 as u8,
        IpAddr::V6(_) => libc::NFPROTO_IPV6 as u8,
    }
}

fn check_l4proto(rule: &mut Rule<'_>, protocol: TransportProtocol) {
    rule.add_expr(&nft_expr!(meta l4proto));
    rule.add_expr(&nft_expr!(cmp == l4proto(protocol)));
}

fn l4proto(protocol: TransportProtocol) -> u8 {
    match protocol {
        TransportProtocol::Udp => libc::IPPROTO_UDP as u8,
        TransportProtocol::Tcp => libc::IPPROTO_TCP as u8,
    }
}

/// Add `ct state established` requirement to `rule`.
///
/// This matches packets which are a part of an already established connection.
fn check_ct_established(rule: &mut Rule<'_>) {
    // bitwise mask will bitwise-and the allowed_states and the ct state. It will then xor it
    // with 0 (which changes nothing). This means it works as a bitwise-and which checks that
    // the ESTABLISHED bit is set in the connection state.
    let established = nftnl::expr::ct::States::ESTABLISHED.bits();
    rule.add_expr(&nft_expr!(ct state));
    rule.add_expr(&nft_expr!(bitwise mask established, xor 0u32));
    rule.add_expr(&nft_expr!(cmp != 0u32));
}

fn add_verdict(rule: &mut Rule<'_>, verdict: &expr::Verdict) {
    if *ADD_COUNTERS {
        rule.add_expr(&nft_expr!(counter));
    }
    rule.add_expr(verdict);
}

fn set_src_valid_mark_sysctl() -> io::Result<()> {
    fs::write(PROC_SYS_NET_IPV4_CONF_SRC_VALID_MARK, b"1")
}

/// If the `net.ipv4.conf.all.arp_ignore` setting is below 2, sets it to 2.
///
/// 2 means: reply only if the target IP address is local address configured on the incoming
/// interface and both with the sender's IP address are part from same subnet on this interface.
fn lock_down_arp_ignore_sysctl() -> io::Result<()> {
    // Should be safe to treat the content as a string, since it should always be a number.
    let current_arp_ignore = fs::read_to_string(PROC_SYS_NET_IPV4_CONF_ARP_IGNORE)?;
    match current_arp_ignore.trim() {
        "0" | "1" => fs::write(PROC_SYS_NET_IPV4_CONF_ARP_IGNORE, b"2")?,
        "2" => (),
        _ => log::trace!("Not locking down arp_ignore since it is set to {current_arp_ignore}"),
    }
    Ok(())
}
