## -*- mode: shell-script; -*-
##
## To be able to make changes to the part of configuration created
## from this configlet you need to copy this file to the directory
## fwbuilder/configlets/linux24/ in your home directory and modify it.
## Double "##" comments are removed during processing but single "#"
## comments are be retained and appear in the generated script. Empty
## lines are removed as well.
##
## Configlets support simple macro language with these constructs:
## {{$var}} is variable expansion
## {{if var}} is conditional operator.


## this function checks if ipset actually can work on the system note
## that we check if it is present separately in check_utilities
## configlet By this time, it is assumed the utility is installed and
## is available, but we still need to check if it works properly
## because it also depends on the kernel module.
##
## ipset -V  checks the version of ipset utility and kernel module and
## is a good way to check if the utility can communicate with the module.
## Unfortunately "ipset -V" returns 0 return code even in the case of
## an error. Will use "ipset --list" which fails when it can't talk to
## the module and then use ipset -V to get diagnostics.

{{if using_ipset}}

check_module_ipset() {
    "$IPSET" --list > /dev/null 2>&1 || {
        echo "Detected an error with ipset utility :"
        "$IPSET" -V
        exit 1
    }
}

## reloads ipset from the data file. The file must have one address
## per line.  The difficulty with ipset is that no set type accepts a
## mix of individual ip addresses and CIDR blocks. Set type iphash
## takes only ip addresses and type nethash takes only CIDR blocks
## with netmask between 1 and 31 bits (no 32 bits). Using a setlist
## set with two sub-sets, one for addresses and another for subnets.
##
## The third argument is the address family of the ruleset this set
## belongs to, "-4" or "-6".  A set holds one family, so a table used by
## both rulesets gets one set per family, and each takes only the lines of
## the file that belong to it: ipset refuses an IPv6 address in an inet
## set with "resolving to IPv4 address failed" and vice versa.  The test
## is on the first field, because a line may carry a trailing comment.
##
reload_address_table() {
    addrtbl_name=$1
    data_file=$2
    af=${3:--4}

    test -z "$addrtbl_name" || test -z "$data_file" && {
        echo "Usage: reload_address_table address_table_object_name file_name [-4|-6]"
        exit 1
    }

##  Called as a command of its own the file has been checked by nobody:
##  check_run_time_address_table_files runs in the start branch alone.  The
##  nftables loader has asked since it was written.
    test -r "$data_file" || {
        echo "Can not find file $data_file referenced by address table object" \
             "$addrtbl_name"
        return 1
    }

##  Every step below is a step the address table needs, and the function
##  used to answer with the status of the last one - an "ipset -X" of a
##  temporary set, which succeeds whatever went wrong before it.
    status=0

    if [ "$af" = "-6" ]; then
        set_family="family inet6"
        addr_filter="\$1 ~ /:/"
    else
        set_family="family inet"
        addr_filter="\$1 !~ /:/"
    fi

    "$IPSET" -X tmp_fwb_set:ip -q
    "$IPSET" -X tmp_fwb_set:net -q

    DATAFILE_SIZE=$(wc -l < "$data_file")
    echo "Processing $DATAFILE_SIZE items in file: $data_file"

##  A hash set takes IPSET_DEFAULT_MAXELEM elements, which is 65536, and
##  answers every address after that with -IPSET_ERR_HASH_FULL, "Hash is
##  full, cannot add more elements" (ipset
##  kernel/include/linux/netfilter/ipset/ip_set_hash.h and
##  ip_set_hash_gen.h).  A block list longer than that therefore blocked
##  its first 65536 addresses and none of the rest, while the activation
##  reported success.  So the set is made large enough for the file it is
##  filled from, with room to grow through add_to_address_table; maxelem is
##  a ceiling and costs no memory of its own.  The swap below carries the
##  size over to the set the rules match against, whichever release created
##  that one.
    set_maxelem=65536
    while test "$set_maxelem" -le "$DATAFILE_SIZE"; do
        set_maxelem=$((set_maxelem * 2))
    done

    # shellcheck disable=SC2086
    "$IPSET" -N tmp_fwb_set:ip  iphash  $set_family maxelem $set_maxelem || status=1
    # shellcheck disable=SC2086
    "$IPSET" -N tmp_fwb_set:net nethash $set_family maxelem $set_maxelem || status=1

##  "-exist" is what makes the count mean "ipset could not parse this".
##  Without it an address listed twice - the ordinary shape of a block list
##  stitched together from several feeds - answers "Element cannot be added
##  to the set: it's already added" and would be reported as refused, and
##  adding an address that is already blocked would answer non-zero where
##  the nftables set takes it silently.
##
##  ipset refuses a line it cannot parse, and the loop went on without
##  counting: a malformed address, a prefix length out of range and
##  0.0.0.0/0, which hash:net has no room for, were all skipped while the
##  rule naming the table was installed either way and the command reported
##  success.  The count is kept inside the group the pipeline ends in,
##  which is the one place a variable set there survives, and printed on
##  its standard output.  Unlike nftables, where one bad line costs the
##  whole element list, the addresses that parsed are still installed.
    refused=$(grep -Ev '^#|^;|^\s*$' "$data_file" | awk "$addr_filter" | {
        n=0
        while read -r L ; do
            # shellcheck disable=SC2086
            set -- $L
            addr=$1
            if echo "$addr" | grep -q "/"
            then
                "$IPSET" -A -exist tmp_fwb_set:net "$addr" >/dev/null || n=$((n + 1))
            else
                "$IPSET" -A -exist tmp_fwb_set:ip "$addr" >/dev/null || n=$((n + 1))
            fi
        done
        echo "$n"
    })
    test "$refused" -eq 0 || {
        echo "$refused of the addresses in $data_file were refused by ipset and" \
             "are not in address table $addrtbl_name"
        status=1
    }

    # shellcheck disable=SC2086
    "$IPSET" --list "${addrtbl_name}:ip" >/dev/null || "$IPSET" -N "${addrtbl_name}:ip" iphash $set_family
    # shellcheck disable=SC2086
    "$IPSET" --list "${addrtbl_name}:net" >/dev/null || "$IPSET" -N "${addrtbl_name}:net" nethash $set_family

    "$IPSET" -W "${addrtbl_name}:ip" tmp_fwb_set:ip || status=1
    "$IPSET" -W "${addrtbl_name}:net" tmp_fwb_set:net || status=1

    "$IPSET" --list "${addrtbl_name}" >/dev/null || {
        "$IPSET" -N "${addrtbl_name}" setlist
    }

    "$IPSET" --list "${addrtbl_name}" | grep -q "${addrtbl_name}:ip" || {
        "$IPSET" -A "${addrtbl_name}" "${addrtbl_name}:ip"
    }

    "$IPSET" --list "${addrtbl_name}" | grep -q "${addrtbl_name}:net" || {
        "$IPSET" -A "${addrtbl_name}" "${addrtbl_name}:net"
    }

    "$IPSET" -X tmp_fwb_set:ip
    "$IPSET" -X tmp_fwb_set:net

    return "$status"
}

## An ipset holds one address family, so an address table used by both
## rulesets has a set per family and the IPv6 one carries a "_v6" suffix -
## the name its rules match against (normalize_set_name in
## platforms/iptables/_utils.py).  The three commands below take an address
## from the administrator, so the family is the address's own; without
## asking it an IPv6 address went to the set of the other family, which
## ipset answers with "resolving to IPv4 address failed", and the address
## was neither added, removed nor tested.  A name that already carries the
## suffix keeps it.
address_table_set_for() {
    case "$2" in
    *:*) echo "${1%_v6}_v6" ;;
    *) echo "$1" ;;
    esac
}

add_to_address_table() {
    addrtbl_name=$1
    data_file=$2
    address=$3

    test -z "$addrtbl_name" || test -z "$data_file" || test -z "$address" && {
        echo "Usage: add_to_address_table address_table_object_name file_name address"
        exit 1
    }

    echo "$address" >> "$data_file"

    set_name=$(address_table_set_for "$addrtbl_name" "$address")

    if echo "$address" | grep -q "/"
    then
        "$IPSET" -A -exist "${set_name}:net" "$address"
    else
        "$IPSET" -A -exist "${set_name}:ip" "$address"
    fi
}

remove_from_address_table() {
    addrtbl_name=$1
    data_file=$2
    address=$3

    test -z "$addrtbl_name" || test -z "$data_file" || test -z "$address" && {
        echo "Usage: remove_from_address_table address_table_object_name file_name address"
        exit 1
    }

## note that $address may contain "/"
    escaped_addr=$(echo "$address" | sed 's!/!\\/!')
    sed -i "/^ *$escaped_addr *\$/d" "$data_file"

    set_name=$(address_table_set_for "$addrtbl_name" "$address")

    if echo "$address" | grep -q "/"
    then
        "$IPSET" -D "${set_name}:net" "$address"
    else
        "$IPSET" -D "${set_name}:ip" "$address"
    fi
}

test_address_table() {
    addrtbl_name=$1
    address=$2

    test -z "$addrtbl_name" || test -z "$address" && {
        echo "Usage: test_address_table address_table_object_name address"
        exit 1
    }

    set_name=$(address_table_set_for "$addrtbl_name" "$address")

##  A plain address may be in the table as itself or covered by one of its
##  networks, and the two live in different sets: hash:ip holds the single
##  addresses, hash:net answers a longest-prefix lookup, and an address
##  inside 203.0.113.0/24 is found only by asking the second one.  Choosing
##  the set by the shape of the argument alone therefore answered "not in
##  set" for an address the firewall does block, and now that the command
##  answers with an exit code it would tell a monitoring check so.  The
##  nftables `nft get element` answers with the element that covers the
##  address and needs no such loop.
    case "$address" in
    */*) at_sets="${set_name}:net" ;;
    *) at_sets="${set_name}:ip ${set_name}:net" ;;
    esac

    for at_set in $at_sets; do
        if "$IPSET" -T "$at_set" "$address" > /dev/null 2>&1; then
            echo "$address is in address table $addrtbl_name"
            return 0
        fi
    done

    echo "$address is not in address table $addrtbl_name"
    return 1
}


load_run_time_address_table_files() {
    :
    {{$load_files_commands}}
}

{{endif}}

check_file() {
    test -r "$2" || {
        echo "Can not find file $2 referenced by address table object $1"
        exit 1
    }
}

## function to check if the data file is available. This is done
## regardless of whether we use module ipset or not.
## Since macro language does not support loops at this time, whole
## code for the body of this function is generated in
## OSConfigurator_linux24::printRunTimeAddressTablesCode()
check_run_time_address_table_files() {
    :
    {{$check_files_commands}}
}
