#!/bin/bash
# vim: cindent:shiftwidth=4:tabstop=4:smarttab:textwidth=100

set -o posix
set -o errexit
set -o pipefail
set -o nounset
#set -o xtrace

#CHANGELOG
#0.6.0	create an initial version
#0.6.1	add Xen profile
#0.6.2	add nsX profile
#0.6.3	add detection of vsftpd
#0.6.4	add detection and configuration of DRBD
#0.6.5	add detection of PostgreSQL/pgbouncer
#0.6.6	add BackupPC profile
#0.6.7	add Roundcube profile
#0.6.8	add detection of activated certbot
#0.6.9	enhance drbd.conf parsing capability
#0.6.10	add detection of Icinga2 livestatus
#0.6.11	add detection of FRRouting BGP daemon
#0.6.12	add detection of syslog-ng collector
#0.6.13	add detection of elastiflow
#0.6.14	add detection of Apache2 GUI on monitor
#0.6.15	add detection of TACACS+ daemon
#0.6.16	add SOE4.2 prov profile
#0.6.17	add detection of DHCPv6
#0.6.18	link time service ACLs to DHCP
#0.6.18	add detection of Asterisk daemon
#0.6.19	detect FRR service via frr.conf
#0.7.0	add detection of services on master
#0.7.1	exclude SNMP traps from logging
#0.8.0	output rules to stdout only
#0.8.1	suppress exception logs for DHCPv4 broadcast requests
#0.8.2	add UDP transport for NTPsec profile
#0.8.3	update hostname of entropy servers
#0.8.4	enable DoT and DoH ports for bind9
#0.8.5	refine logic for BIND 9 authoritative mode selection
#0.8.6	enable rules for vsftpd including PASV mode port range
#0.9.0	adjust output packet DSCP for known application types
#0.9.1	simplify rsync daemon status detection
#0.9.2	skip DSCP classification on SOE4.2
#0.9.3	log to nfnetlink_log on SOE5
#0.9.4	adjust how provX nginx ACL is loaded (using prov-allow.conf)
#0.9.5	remove static DSCP classification for SSH
#0.9.6	add detection of caddy (ACME: HTTP challenge)
#0.9.7	update applications and ports for RPKI (SOE5)
#0.9.8	authorize forwarding based on IPForward setting (SOE5)
#0.9.9	synchronize NFT BGP parser to peer-group layout
#0.9.10 add captive portal (fence) functionality including dnsmasq config write
#1.0.0	integrate configuration writes into script
#1.0.1	fix acl_nginx() unbound variable error
#1.0.2	add nginx for portal http and https
#1.0.3	added provision for ipv6 addresses in captive_portal block
#1.0.4	fix `nftables` service check for `sysvinit` (SOE 4.3 and below)

CONF="/etc/nftables.conf"

warn() {
	echo "[$(date --rfc-3339=seconds)]: $*" >&2
}

debug() {
	if [[ $mode_debug -gt 0 ]]; then
		echo "[$(date --rfc-3339=seconds)]: $*" >&2
	fi
}

# https://stackoverflow.com/questions/1203583/how-do-i-rename-a-bash-function
function copy_function() {
	test -n "$(declare -f "$1")" || return
	eval "${_/$1/$2}"
}

source '/etc/os-release'

HOSTNAME_SHORT=${HOSTNAME%%.*}
SOE_PROFILE=${HOSTNAME_SHORT%%[0-9]*}

declare -r HOSTS_ALLOW='/etc/hosts.allow'
HOSTS_ALLOW_COMBINED=$(mktemp)
readonly HOSTS_ALLOW_COMBINED

declare -r HOSTS_ALLOW_HEADER='
ALL:	[fe80::]/10'

NFT_RULESET=$(mktemp)
readonly NFT_RULESET

# this map is needed ONLY for services where
# the service name does NOT match the port name in /etc/services
declare -r -A APPLICATION_PORT=(
[ALL]='tcpmux'
[sshd]='ssh'
[in.tftpd]='tftp'
[ntpd]='ntp'
[ntpsec]='ntp'
[postfix]='smtp'
[mysqld]='mysql'
[rpki-http]='http'
[rpki-https]='https'
[snmpd]='snmp'
[rrdtool]='rrdcached'
[ser2net]='telnet'
[slapd]='ldap'
[bind9]='domain'
[bind9_DoT]='domain-s'
[bind9_DoH]='https'
[vsftpd]='ftp'
[backuppc]='https'
[roundcube]='https'
[roundcube80]='http'
[certbot]='http'
[caddy]='http'
[apache2_monitor]='https'
[tac_plus]='tacacs'
[dhcpd]='bootps'
[DHCPv6]='dhcpv6-server'
[stun1]='stun'
[stun2]='stun-secondary'
[nginx]='http'
[time4]='time'
[time6]='time'
[asterisk]='sip'
[clamav_dist]='https'
[debian_dist]='http'
[mirror_http]='http'
[mirror_https]='https'
[portal_http]='http'
[portal_https]='https'
)

declare -r -A APPLICATION_PRESENCE=(
[ALL]='/'
[sshd]='/usr/sbin/sshd'
[slapd]='/usr/sbin/slapd'
[influxdb]='/usr/bin/influxd'
[rpki-http]='/usr/bin/rpki-validator-3.sh'
[rpki-https]='/usr/bin/routinator'
[rpki-rtr]='/usr/bin/routinator'
[in.tftpd]='/usr/sbin/in.tftpd'
[rpcbind]='/sbin/rpcbind'
[mountd]='/usr/sbin/rpc.mountd'
[ntpsec]='/etc/ntpsec'
[ntpd]='/etc/ntp.conf'
[entropy]='/usr/local/sbin/entropyd'
[munin]='/usr/sbin/munin-node'
[postfix]='/etc/postfix/main.cf'
[mysqld]='/usr/sbin/mysqld'
[xen]='/usr/sbin/xl'
[snmpd]='/usr/sbin/snmpd'
[rrdtool]='/usr/bin/rrdcached'
[ser2net]='/etc/ser2net.conf'
[slapd]='/usr/sbin/slapd'
[bind9]='/usr/sbin/named'
[bind9_DoT]='/usr/sbin/named'
[bind9_DoH]='/usr/sbin/named'
[vsftpd]='/usr/sbin/vsftpd'
[drbd]='/etc/drbd.d'
[postgresql]='/usr/bin/pg_config'
[pgbouncer]='/usr/sbin/pgbouncer'
[backuppc]='/usr/share/backuppc/bin/BackupPC'
[roundcube]='/usr/share/roundcube'
[roundcube80]='/usr/share/roundcube'
[certbot]='/etc/letsencrypt/live/default'
[caddy]='/usr/bin/caddy'
[livestatus]='/etc/icinga2/features-enabled/livestatus.conf'
[bgp]='/etc/frr/frr.conf'
[syslog]='/etc/syslog-ng/conf.d/observium.conf'
[ipfix]='/etc/default/elastiflow'
[netflow]='/etc/default/elastiflow'
[sflow]='/etc/default/elastiflow'
[apache2_monitor]='/opt/observium/config.php'
[tac_plus]='/usr/sbin/tac_plus'
[dhcpd]='/etc/dhcp/subnet'
[DHCPv6]='/etc/dhcp6/subnet'
[stun1]='/usr/sbin/stund'
[stun2]='/usr/sbin/stund'
[nginx]='/usr/sbin/nginx'
[time4]='/etc/dhcp/subnet'
[time6]='/etc/dhcp6/subnet'
[asterisk]='/etc/asterisk/sip.conf'
[clamav_dist]='/srv/clamav'
[debian_dist]='/srv/reprepro'
[nats]='/usr/bin/nats-server'
[rsync]='/etc/rsyncd.conf'
[bcfg2]='/etc/bcfg2-server.conf'
[mirror_http]='/etc/mirror'
[mirror_https]='/etc/mirror'
[portal_http]='/etc/nginx/nginx.conf'
[portal_https]='/etc/nginx/nginx.conf'
[captiveportal]='/usr/local/etc/captive-portal.conf'
)

declare -r -A APPLICATION_UDP=(
[in.tftpd]=1
[entropy]=1
[ntpd]=1
[ntpsec]=1
[snmpd]=1
[bind9]=1
[syslog]=1
[ipfix]=1
[netflow]=1
[sflow]=1
[dhcpd]=1
[DHCPv6]=1
[stun1]=1
[stun2]=1
[time4]=1
[time6]=1
[asterisk]=1
)

declare -r qq='"'

forward_policy='drop'
if [[ $(sysctl --values net.ipv6.conf.all.forwarding) -gt 0 ]]; then
	forward_policy='accept'
fi
if [[ -d '/etc/systemd/network' ]]; then
	if grep --quiet --extended-regexp --recursive '^IPForward=' '/etc/systemd/network'; then
		forward_policy='accept'
	fi
fi

declare -r NFT_HEADER="#!/usr/sbin/nft -f
flush ruleset
add table inet filter
add chain inet filter input { type filter hook input priority 0; policy drop; }
add chain inet filter forward { type filter hook forward priority 0; policy $forward_policy; }
add chain inet filter output { type filter hook output priority 0; policy accept; }
add rule inet filter input ct state invalid counter drop
add rule inet filter input ct state { established, related } counter accept
add rule inet filter input iif ${qq}lo${qq} accept
add rule inet filter input iif != ${qq}lo${qq} ip daddr 127.0.0.0/8 counter drop
add rule inet filter input iif != ${qq}lo${qq} ip6 daddr ::1 counter drop
add rule inet filter input ip protocol icmp counter accept
add rule inet filter input ip6 nexthdr ipv6-icmp counter accept
"

if [[ $VERSION_ID -ge 12 ]]; then
	# nfnetlink_log (ulogd2 -> systemd-journald)
	declare -r LOG_TYPE='log group 0'
else
	# kernel log (-> dmesg)
	declare -r LOG_TYPE='log'
fi

if [[ $forward_policy = 'drop' ]]; then
	forward_drop_log="add rule inet filter forward $LOG_TYPE"
else
	forward_drop_log=""
fi

declare -r NFT_FOOTER="
add rule inet filter input udp sport bootpc udp dport bootps ip saddr 0.0.0.0 ip daddr 255.255.255.255 counter drop comment ${qq}DHCPv4 broadcast${qq}
add rule inet filter input counter
add rule inet filter input $LOG_TYPE
add rule inet filter input reject

add rule inet filter forward counter
$forward_drop_log
add rule inet filter output counter
"

declare -r DSCP_CLASSIFY='
# https://en.wikipedia.org/wiki/Differentiated_services
add rule inet filter output tcp sport rsync counter ip  dscp set cs1
add rule inet filter output tcp sport rsync counter ip6 dscp set cs1
add rule inet filter output tcp sport domain counter ip  dscp set ef
add rule inet filter output tcp sport domain counter ip6 dscp set ef
add rule inet filter output udp sport domain counter ip  dscp set ef
add rule inet filter output udp sport domain counter ip6 dscp set ef
add rule inet filter output tcp sport munin counter ip  dscp set cs2
add rule inet filter output tcp sport munin counter ip6 dscp set cs2
'

template_acl_ALL() {
	echo 'ALL'
}

copy_function template_acl_ALL acl_ntpsec

acl_ntpd() {
	if [[ -d '/sys/bus/scsi' ]]; then
		# domB/dom0
		echo 'ALL'
	fi
	if [[ -s '/var/lib/dpkg/info/isc-dhcp-server.list' ]]; then
		# provX
		echo 'ALL'
	fi
}

acl_entropy() {
	if [[ $HOSTNAME =~ ^clock ]]; then
		echo 'ALL'
	fi
}

acl_bind9() {
	local authoritative

	if [[ "${FUNCNAME[0]}" = 'acl_bind9_DoT' ]]; then
		authoritative=0
	elif [[ "${FUNCNAME[0]}" = 'acl_bind9_DoH' ]]; then
		authoritative=0
	elif [[ "$SOE_PROFILE" = 'ns' ]]; then
		authoritative=1
	elif [[ "$SOE_PROFILE" = 'prov' ]]; then
		authoritative=0
	else
		local global_authoritative_zones=$(named-checkconf -p -x | awk '
{
	if ($1=="zone") {
		zone=$2
		gsub("\"","",zone)
		switch (zone) {
			case ".":
				break
			case "use-application-dns.net":
				break
			case @/^(\S+[.])?(0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)[.]172[.]in-addr.arpa$/:
				break
			case @/^(\S+[.])?168[.]192[.]in-addr.arpa$/:
				break
			case @/^(\S+[.])?10[.]in-addr.arpa$/:
				break
			case @/^(\S+[.])?(18|19)[.]198[.]in-addr.arpa$/:
				break
			case @/^(\S+[.])?[cd][.]f[.]ip6.arpa$/:
				break
			case @/^\w+$/:
				break
			default:
				print zone
		}
	}
}')
		if [[ -n "$global_authoritative_zones" ]]; then
			authoritative=1
		else
			authoritative=0
		fi
	fi

	if [[ $authoritative -gt 0 ]]; then
		# allow authoritative queries from anywhere
		echo 'ALL'
	else
		# restrict recursive-only server to known netblocks
		named-checkconf -p -x | awk '
{
	if ($1=="acl")
		ACL_BLOCK=1
	else if ($1=="};")
		ACL_BLOCK=0
	else if (ACL_BLOCK) {
		acl=$1
		sub(";","",acl)
		if (substr(acl,1,1)!="\"")
			print acl
	}
}'
	fi
}
copy_function acl_bind9 acl_bind9_DoT
copy_function acl_bind9 acl_bind9_DoH

copy_function template_acl_ALL acl_roundcube
copy_function template_acl_ALL acl_roundcube80
copy_function template_acl_ALL acl_certbot
copy_function template_acl_ALL acl_caddy

copy_function template_acl_ALL acl_mirror_http
copy_function template_acl_ALL acl_mirror_https

copy_function template_acl_ALL acl_portal_http
copy_function template_acl_ALL acl_portal_https

acl_bgp() {
	awk '
{
	if ($1=="neighbor" && $3=="peer-group" && length($4))
		print $2
}' "${APPLICATION_PRESENCE[bgp]}"
}

copy_function template_acl_ALL acl_apache2_monitor

acl_dhcpd() {
	awk '
{
	if ($1" "$2=="option routers") {
		sub(";","",$3)
		print $3
	} else if ($1" "$3" "$5=="subnet netmask {") {
		print $2"/"$4
	}
}' "${APPLICATION_PRESENCE[dhcpd]}" |
while read addr_spec; do
	netmask $addr_spec
done
}
copy_function acl_dhcpd acl_time4

acl_DHCPv6() {
    if grep -q '^# ipv6-allocation:' "${APPLICATION_PRESENCE[DHCPv6]}"; then
        awk '
            {
                if ($2=="ipv6-allocation:") {
                    print $3
                }
            }
        ' "${APPLICATION_PRESENCE[DHCPv6]}"
    else
	    awk '
            {
            	if ($1=="subnet6") {
            		print $2
            	}
            }
        ' "${APPLICATION_PRESENCE[DHCPv6]}"
    fi
}
copy_function acl_DHCPv6 acl_time6

copy_function template_acl_ALL acl_stun1
copy_function template_acl_ALL acl_stun2

acl_nginx() {
	ACLFILE=""
    if [[ -f /etc/nginx/prov-allow.conf ]]; then
        # nginx on provX (via prov-allow.conf, non-RFC1918 entries)
        ACLFILE="/etc/nginx/prov-allow.conf"
    elif [[ -f /etc/nginx/sites-enabled/prov ]]; then
        # nginx on provX (default, RFC1918 only)
        ACLFILE="/etc/nginx/sites-enabled/prov"
    elif [[ -f /etc/nginx/sites-enabled/nomad ]]; then
        # nginx on nomadX (v4)
        ACLFILE="/etc/nginx/sites-enabled/nomad"
    elif [[ -f /etc/nginx/acl/wowza.conf ]]; then
        # nginx on flowcasterX
        ACLFILE="/etc/nginx/acl/wowza.conf"
    elif [[ -f /usr/local/etc/captive-portal.conf ]]; then
        # nginx on fenceX; handled manually later
        return
    fi

	if [[ -n "${ACLFILE}" ]]; then
		awk '
		{
			if ($1=="allow") {
				sub(";","",$2)
				print $2
			}
		}' "${ACLFILE}"
	fi
}

special_asterisk() {
	echo "add rule inet filter input tcp dport sip accept"
	echo "add rule inet filter input udp dport sip accept"
	echo "add rule inet filter input udp dport 10000-40000 accept"
}

special_ntpd() {
	if [[ ! -d '/sys/bus/scsi' ]]; then
		# domU
		echo 'add rule inet filter input ip6 daddr ff05::101 udp dport ntp accept'
		echo 'add rule inet filter input ip daddr 224.0.1.1 udp dport ntp accept'
	fi
}

special_vsftpd() {
	echo "add rule inet filter input tcp dport ftp accept"
	local pasv_min_port
	local pasv_max_port
	pasv_min_port=$(awk -F'[[:space:]]*=[[:space:]]*' '{if ($1=="pasv_min_port"){print $2}}' '/etc/vsftpd.conf')
	pasv_max_port=$(awk -F'[[:space:]]*=[[:space:]]*' '{if ($1=="pasv_max_port"){print $2}}' '/etc/vsftpd.conf')
	echo "add rule inet filter input tcp dport $pasv_min_port-$pasv_max_port accept"
}

special_drbd() {
	find /etc/drbd.d -type f -name '*.res' -print0 |
		xargs --null --no-run-if-empty awk '
# drbd.conf(5)
# address AF addr:port
#
# AF must be one of ipv4, ipv6, ssocks or sdp (for compatibility reasons sci is
# an alias for ssocks). It may be omited for IPv4 addresses. The actual IPv6
# address that follows the ipv6 keyword must be placed inside brackets: ipv6
# [fd01:2345:6789:abcd::1]:7800.
{
	if ($1=="address") {
		if ($2=="ipv6") {
			af="ip6"
			match($3,/[[]([[:xdigit:]:]+)[]]:([[:digit:]]+);/,address_and_port)
		} else if ($2=="ipv4") {
			af="ip"
			match($3,/([[:digit:].]+):([[:digit:]]+);/,address_and_port)
		} else if (!$3) {
			af="ip"
			match($2,/([[:digit:].]+):([[:digit:]]+);/,address_and_port)
		} else {
			print "unexpected syntax in drbd.conf: {" $0 "}" > "/dev/stderr"
			exit 1
		}
		print "add rule inet filter input " af " saddr " address_and_port[1] " tcp dport " address_and_port[2] " ct state new accept"
	}
}'
}

special_syslog() {
	# SNMP traps are not supported
	echo 'add rule inet filter input udp dport 162 counter drop'
}

acl_munin() {
	awk '{if ($1=="cidr_allow"){print $2}}' '/etc/munin/munin-node.conf'
}

special_captiveportal() {
    # This block performs generation of the dnsmasq configuration as well
	source /usr/local/etc/captive-portal.conf
	echo "add rule inet filter input tcp dport 53 ct state new accept"
	echo "add rule inet filter input udp dport 53 ct state new accept"
	echo "add rule inet filter input tcp dport 80 ct state new accept"
	echo "add rule inet filter input tcp dport 443 ct state new accept"
	echo "add rule inet filter forward ct state established,related accept"
	echo "add table ip nat"
	echo "add chain ip nat postrouting { type nat hook postrouting priority 100 ; }"
    for LAN_NET in ${CAPTIVEPORTAL_LAN_NETWORKS[@]}; do
    	echo "add rule ip nat postrouting oifname \"${CAPTIVEPORTAL_WAN_IFACE}\" ip saddr ${LAN_NET} masquerade"
    done
	for NET in ${CAPTIVEPORTAL_ALLOWED_NETWORKS[@]}; do
		if [[ "$NET" == *":"* ]]; then
			echo "add rule inet filter forward ip6 daddr ${NET} accept"
		else
			echo "add rule inet filter forward ip daddr ${NET} accept"
		fi
    done | sort
	cat <<EOF >/etc/dnsmasq.conf.new
interface=${CAPTIVEPORTAL_LAN_IFACE}
listen-address=${CAPTIVEPORTAL_LAN_ADDR}
no-dhcp-interface=${CAPTIVEPORTAL_LAN_ADDR}
bind-interfaces
bogus-nxdomain=::/128
no-resolv
log-queries
log-debug
address=/#/${CAPTIVEPORTAL_LAN_ADDR}
EOF
	for DOMAIN in ${CAPTIVEPORTAL_ALLOWED_DOMAINS[@]}; do
		ip_addresses=( $( dig @localhost-resolved +short A ${DOMAIN} | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' ) )
		for IP in ${ip_addresses[@]}; do
			echo "add rule inet filter forward ip daddr ${IP}/32 accept"
			echo "address=/${DOMAIN}/${IP}" >>/etc/dnsmasq.conf.new
		done
	done | sort
    for LAN_NET in ${CAPTIVEPORTAL_LAN_NETWORKS[@]}; do
    	echo "add rule inet filter forward ip saddr ${LAN_NET} reject"
    done
    if cmp --silent /etc/dnsmasq.conf.new /etc/dnsmasq.conf; then
        rm /etc/dnsmasq.conf.new
    else
		mv /etc/dnsmasq.conf.new /etc/dnsmasq.conf
		service dnsmasq restart
    fi
}

copy_function template_acl_ALL acl_postfix
copy_function template_acl_ALL acl_rsync
copy_function template_acl_ALL acl_debian_dist
copy_function template_acl_ALL acl_clamav_dist
copy_function template_acl_ALL acl_nats
copy_function template_acl_ALL acl_bcfg2

# --- #

usage() {
	echo 'prints SOE profile-specific nf_tables ruleset'
	echo "usage: $0 [--verbose|-v]"
	echo
	exit 1
}

mode_debug=0
ARGS=$(getopt --options 'd' --longoptions 'debug' -- "$@")
if [[ $? -gt 0 ]]; then
	usage
fi

eval set -- "$ARGS"
for i ; do
	case "$1" in
		-d|--debug) shift; mode_debug=1 ;;
		--) shift ; break ;;
		'') shift ;;
		*) echo "Internal error! _${1}_" ; exit 1 ;;
	esac
done

# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=959989
declare -A PORT_NUMBER
PORT_NUMBER[NULL]=0
while read -r name number_protocol remainder; do
	if [[ -z "$name" ]]; then
		continue
	elif [[ ${name:0:1} = '#' ]]; then
		continue
	fi

	number=${number_protocol%%/*}
	protocol=${number_protocol##*/}
	PORT_NUMBER[$name]=$number
done < /etc/services
readonly PORT_NUMBER

echo "$HOSTS_ALLOW_HEADER" > $HOSTS_ALLOW_COMBINED
cat $HOSTS_ALLOW >> $HOSTS_ALLOW_COMBINED

for application in ${!APPLICATION_PRESENCE[@]}; do
	if [[ -e "${APPLICATION_PRESENCE[$application]}" ]]; then
		acl_helper="acl_$application"
		if type -t $acl_helper >/dev/null; then
			acl_helper_lines=$($acl_helper)
			declare -a acl_set=()
			for acl in $acl_helper_lines; do
				if [[ "$acl" = 'ALL' || "$acl" =~ [.] ]]; then
					acl_set+=($acl)
				elif [[ "$acl" =~ ^([[:xdigit:]:]+)/([[:digit:]]+)$ ]]; then
					acl_set+=("[${BASH_REMATCH[1]}]/${BASH_REMATCH[2]}")
				elif [[ "$acl" =~ ^[[:xdigit:]:]+$ ]]; then
					acl_set+=("[$acl]")
				else
					warn "application ACL export syntax error: {$acl}"
				fi
			done
			if [[ ${#acl_set[@]} -gt 0 ]]; then
				echo "$application: ${acl_set[@]}" >> $HOSTS_ALLOW_COMBINED
			fi
		fi
	fi
done

echo -n "Generating NFT ruleset... "

##cat $HOSTS_ALLOW_COMBINED;exit 1
echo "$NFT_HEADER" >> $NFT_RULESET

for application in ${!APPLICATION_PRESENCE[@]}; do
	if [[ -e "${APPLICATION_PRESENCE[$application]}" ]]; then
		special_helper="special_$application"
		if type -t $special_helper >/dev/null; then
			$special_helper >> $NFT_RULESET
		fi
	fi
done

declare -A generic_acl_cache
while read application netblocks; do
	if [[ -z "$application" ]]; then
		continue
	elif [[ ${application:0:1} = '#' ]]; then
		continue
	else
		application=${application%:}
	fi

	if [[ -n "${APPLICATION_PRESENCE[$application]+xxx}" ]]; then
		if [[ -e "${APPLICATION_PRESENCE[$application]}" ]]; then
			debug "$application found"
			:
		else
			debug "$application is not installed"
			continue
		fi
	else
		warn "unable to determine if $application is installed"
		continue
	fi

	if [[ -n "${PORT_NUMBER[$application]+xxx}" ]]; then
		port_name=$application
	elif [[ -n "${APPLICATION_PORT[$application]+xxx}" ]]; then
		port_name=${APPLICATION_PORT[$application]}
	else
		warn "unable to determine port name for $application"
		continue
	fi

	if [[ -n "${PORT_NUMBER[$port_name]+xxx}" ]]; then
		port_number=${PORT_NUMBER[$port_name]}
	else
		warn "unable to determine port number for $application/$port_name"
		continue
	fi

	if [[ -n "${APPLICATION_UDP[$application]+xxx}" ]]; then
		protocols='tcp udp'
	else
		protocols='tcp'
	fi

	##echo $application
	for netblock in $netblocks; do
		if [[ $netblock =~ ^[a-z.] ]]; then
			continue
		elif [[ $netblock = 'ALL' ]]; then
			address_family=''
		elif [[ ${netblock:0:1} = '[' ]]; then
				address_family='ip6'
				netblock=${netblock:1}
				netblock=${netblock/]/}
		else
				address_family='ip'
		fi

		if [[ "$application" = 'ALL' ]]; then
			debug "generic_acl_cache +$netblock"
			generic_acl_cache[$netblock]=1
		elif [[ -n "${generic_acl_cache[$netblock]+xxx}" ]]; then
			debug "ACL $netblock already exists as generic: no need to add $application specific version"
			continue #netblock
		fi

		##echo "	$address_family:$netblock"

		for protocol in $protocols; do
			echo -n 'add rule inet filter input' >> $NFT_RULESET

			if [[ -n "$address_family" ]]; then
				echo -n " $address_family saddr $netblock" >> $NFT_RULESET
			fi

			if [[ "$port_number" -ne 1 ]]; then
				echo -n " $protocol dport $port_number" >> $NFT_RULESET
			fi

			echo ' ct state new accept' >> $NFT_RULESET
		done
	done
done < $HOSTS_ALLOW_COMBINED
rm $HOSTS_ALLOW_COMBINED

echo -ne "$NFT_FOOTER" >> $NFT_RULESET
if [[ $VERSION_ID -ge 10 ]]; then
	echo -ne "$DSCP_CLASSIFY" >> $NFT_RULESET
fi

echo "done."

echo -n "Checking for changes to existing NFT ruleset... "

# If configs are identical, quit
if cmp --silent $CONF $NFT_RULESET; then
    rm $NFT_RULESET
    echo "none found."
    exit 0
fi
echo "done."

echo -n "Installing new NFT ruleset... "
# Correct ownership and move new configuration into place
chown --reference="$CONF" $NFT_RULESET
chmod --reference="$CONF" $NFT_RULESET
mv --force $NFT_RULESET "$CONF"
echo "done."

# Reload nftables if the service exists on the system
SERVICE_NAME="nftables"

# NOTE: check for SOE 4.3, does not take into account systemd for SOE 5.0 yet
if [ -f "/etc/init.d/$SERVICE_NAME" ]; then
	# A simple check for enabled status can be complex. We'll assume if the init script exists,
	# it's a good candidate for reloading. A better check would be to parse runlevel symlinks.
	# For simplicity, we'll just check if the init script exists.
	echo "Found init script for '$SERVICE_NAME'. Attempting to reload..."
	service "$SERVICE_NAME" reload
	if [ $? -eq 0 ]; then
		echo "Service '$SERVICE_NAME' reloaded successfully."
	else
		echo "Error: Failed to reload service '$SERVICE_NAME'."
	fi
else
	echo "Init script for service '$SERVICE_NAME' not found in /etc/init.d/. Skipping reload."
fi
