#!/bin/sh
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2007-2022  IPFire Team  <info@ipfire.org>                     #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

. /etc/sysconfig/rc
. ${rc_functions}
. /etc/init.d/networking/functions.network

eval $(/usr/local/bin/readhash /var/ipfire/suricata/settings)

IPS_REPEAT_MARK="0x80000000"
IPS_REPEAT_MASK="0x80000000"

# The IPS requested that this connection is being bypassed
IPS_BYPASS_REQUESTED_MARK="0x40000000"
IPS_BYPASS_REQUESTED_MASK="0x40000000"

# Marks a connection to be bypassed
IPS_BYPASS_MARK="0x20000000"
IPS_BYPASS_MASK="0x20000000"

# Set if we request to scan this packet
IPS_SCAN_MARK="0x10000000"
IPS_SCAN_MASK="0x10000000"

# Set if a packet has been whitelisted
IPS_WHITELISTED_MARK="0x08000000"
IPS_WHITELISTED_MASK="0x08000000"

# Supported network zones
NETWORK_ZONES=( "RED" "GREEN" "ORANGE" "BLUE" "IPSEC" "WG" "OVPN" )

# Optional options for the Netfilter queue.
NFQ_OPTS=(
	"--queue-bypass"
)

# Function to flush the firewall chains.
flush_fw_chain() {
	iptables -w -t mangle -F IPS
	iptables -w -t mangle -F IPS_CLEAR
	iptables -w -t mangle -F IPS_SCAN_IN
	iptables -w -t mangle -F IPS_SCAN_OUT
}

# Function to create the firewall rules to pass the traffic to suricata.
generate_fw_rules() {
	# Assign NFQ_OPTS
	local NFQ_OPTIONS=( "${NFQ_OPTS[@]}" )

	local cpu_count="$(getconf _NPROCESSORS_ONLN)"

	# Check if there are multiple cpu cores available.
	if [ "$cpu_count" -gt "1" ]; then
		# Balance beetween all queues
		NFQ_OPTIONS+=(
			"--queue-balance" "0:$(($cpu_count-1))"
			"--queue-cpu-fanout"
		)
	else
		# Send all packets to queue 0
		NFQ_OPTIONS+=(
			"--queue-num" "0"
		)
	fi

	# Flush the firewall chains.
	flush_fw_chain

	# Don't process packets where the IPS has requested to bypass the stream
	iptables -w -t mangle -A IPS \
		-m comment --comment "BYPASSED" \
		-m mark --mark "$(( IPS_BYPASS_MARK ))/$(( IPS_BYPASS_MASK ))" -j RETURN

	# If suricata decided to bypass a stream, we will store the mark in the connection tracking table
	iptables -w -t mangle -A IPS \
		-m mark --mark "$(( IPS_BYPASS_REQUESTED_MARK ))/$(( IPS_BYPASS_REQUESTED_MASK ))" \
		-j CONNMARK --set-mark "$(( IPS_BYPASS_MARK ))/$(( IPS_BYPASS_MASK ))"

	# Don't process packets that have already been seen by the IPS
	for chain in IPS IPS_SCAN_IN IPS_SCAN_OUT; do
		iptables -w -t mangle -A "${chain}" \
			-m mark --mark "$(( IPS_REPEAT_MARK ))/$(( IPS_REPEAT_MASK ))" -j RETURN
	done

	local zone
	local status
	local intf

	# Mark packets for all zones that we want to scan
	for zone in "${NETWORK_ZONES[@]}"; do
		status="ENABLE_IDS_${zone}"

		if [ "${!status}" = "on" ]; then
			# Handle IPsec packets
			case "${zone}" in
				RED)
					# If IPsec is not enabled, skip everything that is IPsec traffic
					if [ "${ENABLE_IDS_IPSEC}" != "on" ]; then
						for intf in $(network_get_intfs "${zone}"); do
							iptables -w -t mangle -A IPS_SCAN_IN \
								-i "${intf}" -m policy --pol ipsec --dir in -j RETURN
							iptables -w -t mangle -A IPS_SCAN_OUT \
								-o "${intf}" -m policy --pol ipsec --dir out -j RETURN
						done
					fi
					;;

				IPSEC)
					iptables -w -t mangle -A IPS_SCAN_IN \
						-m policy --pol ipsec --dir in -j MARK --set-mark "$(( IPS_SCAN_MARK ))/$(( IPS_SCAN_MASK ))"
					iptables -w -t mangle -A IPS_SCAN_OUT \
						-m policy --pol ipsec --dir out -j MARK --set-mark "$(( IPS_SCAN_MARK ))/$(( IPS_SCAN_MASK ))"
					;;
			esac

			# Add interfaces
			for intf in $(network_get_intfs "${zone}"); do
				iptables -w -t mangle -A IPS_SCAN_IN \
					-i "${intf}" -j MARK --set-mark "$(( IPS_SCAN_MARK ))/$(( IPS_SCAN_MASK ))"
				iptables -w -t mangle -A IPS_SCAN_OUT \
					-o "${intf}" -j MARK --set-mark "$(( IPS_SCAN_MARK ))/$(( IPS_SCAN_MASK ))"
			done
		fi
	done

	# Don't keep processing packets we don't want to scan
	iptables -w -t mangle -A IPS -m mark ! --mark "$(( IPS_SCAN_MARK ))/$(( IPS_SCAN_MASK ))" -j RETURN

	# Never send any whitelisted packets to the IPS
	if [ -r "/var/ipfire/suricata/ignored" ]; then
		local id network remark enabled rest

		while IFS=',' read -r id network remark enabled rest; do
			# Skip disabled entries
			[ "${enabled}" = "enabled" ] || continue

			iptables -w -t mangle -A IPS -s "${network}" -j MARK --set-mark "$(( IPS_WHITELISTED_MARK ))/$(( IPS_WHITELISTED_MASK ))"
			iptables -w -t mangle -A IPS -d "${network}" -j MARK --set-mark "$(( IPS_WHITELISTED_MARK ))/$(( IPS_WHITELISTED_MASK ))"
		done < "/var/ipfire/suricata/ignored"
	fi

	# Count and skip the whitelisted packets
	iptables -w -t mangle -A IPS \
		-m comment --comment "WHITELISTED" \
		-m mark --mark "$(( IPS_WHITELISTED_MARK ))/$(( IPS_WHITELISTED_MASK ))" -j RETURN

	# Send packets to suricata
	iptables -w -t mangle -A IPS -m comment --comment "SCANNED" -j NFQUEUE "${NFQ_OPTIONS[@]}"

	# Clear all bits again after packets have been sent to the IPS
	# This is required so that encapsulated packets can't inherit any set bits here and won't be scanned.
	iptables -w -t mangle -A IPS_CLEAR \
			-j MARK --set-mark "0/$(( IPS_BYPASS_MASK | IPS_BYPASS_REQUESTED_MASK | IPS_REPEAT_MASK | IPS_SCAN_MASK ))"

	return 0
}

case "$1" in
	start)
		# Get amount of CPU cores
		cpu_count="$(getconf _NPROCESSORS_ONLN)"

		# Numer of NFQUES.
		NFQUEUES="-q 0"

		if [ $cpu_count -gt "1" ]; then
			NFQUEUES+=":$(($cpu_count-1))"
		fi

		# Check if suricata should be started
		if [ "$ENABLE_IDS" == "on" ]; then
			# Start the reporter
			boot_mesg "Starting Intrusion Prevention Reporter..."
			loadproc -f -p /var/run/suricata/reporter.pid -b /usr/bin/suricata-reporter \
				--config="/var/ipfire/suricata/reporter.conf"

			# Start suricata
			boot_mesg "Starting Intrusion Prevention System..."
			loadproc -b /usr/bin/suricata-watcher -c /etc/suricata/suricata.yaml $NFQUEUES

			# Flush the firewall chain
			flush_fw_chain

			# Generate firewall rules
			generate_fw_rules
		fi
		;;

	stop)
		boot_mesg "Stopping Intrusion Prevention System..."
		killproc -p /var/run/suricata.pid /usr/bin/suricata

		boot_mesg "Stopping Intrusion Prevention Reporter..."
		killproc -p /var/run/suricata/reporter.pid /usr/bin/suricata-reporter

		# Flush firewall chain.
		flush_fw_chain

		# Don't report returncode of rm if suricata was not started
		exit 0
		;;

	status)
		PIDFILE="/var/run/suricata.pid" statusproc /usr/bin/suricata
		PIDFILE="/var/run/suricata/reporter.pid" statusproc /usr/bin/suricata-reporter
		;;

	restart)
		$0 stop
		$0 start
		;;

	reload)
		# Send SIGUSR2 to the suricata process to perform a reload
		# of the ruleset.
		kill -USR2 $(pidof suricata)

		# Reload the reporter
		PIDFILE="/var/run/suricata/reporter.pid" \
			reloadproc /usr/bin/suricata-reporter

		# Flush the firewall chain.
		flush_fw_chain

		# Generate firewall rules.
		generate_fw_rules
		;;

	*)
		echo "Usage: $0 {start|stop|restart|reload|status}"
		exit 1
		;;
esac
