#!/bin/sh /etc/rc.common
# SPDX-License-Identifier: MIT
# CrowdSec Firewall Bouncer - nftables integration for SecuBox OpenWrt
# Copyright (C) 2021-2022 Gerald Kerma
# Copyright (C) 2024-2025 CyberMind.fr (SecuBox adaptation)

USE_PROCD=1

START=99
STOP=10

NAME=crowdsec-firewall-bouncer
PROG=/usr/bin/cs-firewall-bouncer
VARCONFIGDIR=/var/etc/crowdsec/bouncers
VARCONFIG=/var/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml

CONFIGURATION=crowdsec

TABLE="crowdsec"
TABLE6="crowdsec6"

service_triggers() {
	procd_add_reload_trigger crowdsec-firewall-bouncer
	procd_add_config_trigger "config.change" "crowdsec" /etc/init.d/crowdsec-firewall-bouncer reload
	# Restart bouncer when firewall reloads to re-apply nftables rules
	procd_add_reload_trigger firewall
}

init_yaml() {
	local section="$1"

	local set_only
	local hook_priority
	local update_frequency
	local log_level
	local api_url
	local api_key
	local ipv6
	local deny_action
	local deny_log
	local log_prefix
	local log_max_size
	local log_max_backups
	local log_max_age
	local ipv4
	local chain_name
	local chain6_name
	local retry_initial_connect

	config_get hook_priority $section priority "4"
	config_get update_frequency $section update_frequency '10s'
	config_get log_level $section log_level 'info'
	config_get api_url $section api_url "http://127.0.0.1:8180"
	config_get api_key $section api_key "API_KEY"
	config_get_bool ipv6 $section ipv6 '1'
	config_get deny_action $section deny_action "drop"
	config_get_bool deny_log $section deny_log '0'
	config_get log_prefix $section log_prefix "crowdsec: "
	config_get log_max_size $section log_max_size '100'
	config_get log_max_backups $section log_max_backups '3'
	config_get log_max_age $section log_max_age '30'
	config_get_bool ipv4 $section ipv4 '1'
	config_get chain_name $section chain_name "crowdsec-chain"
	config_get chain6_name $section chain6_name "crowdsec6-chain"
	config_get_bool retry_initial_connect $section retry_initial_connect '1'

	# Create tmp dir & permissions if needed
	if [ ! -d "${VARCONFIGDIR}" ]; then
		mkdir -m 0755 -p "${VARCONFIGDIR}"
	fi

	cat > $VARCONFIG <<-EOM
	mode: nftables
	pid_dir: /var/run/
	update_frequency: $update_frequency
	daemonize: true
	log_mode: file
	log_dir: /var/log/
	log_level: $log_level
	log_compression: true
	log_max_size: $log_max_size
	log_max_backups: $log_max_backups
	log_max_age: $log_max_age
	api_url: $api_url
	api_key: $api_key
	retry_initial_connect: bool($retry_initial_connect)
	insecure_skip_verify: true
	disable_ipv6: boolnot($ipv6)
	deny_action: $deny_action
	deny_log: bool($deny_log)
	supported_decisions_type:
	  - ban
	deny_log_prefix: "$log_prefix"
	blacklists_ipv4: crowdsec-blacklists
	blacklists_ipv6: crowdsec6-blacklists
	ipset_type: nethash
	iptables_chains:
	  - INPUT
	nftables:
	  ipv4:
	    enabled: bool($ipv4)
	    set-only: false
	    table: $TABLE
	    chain: $chain_name
	    priority: $hook_priority
	  ipv6:
	    enabled: bool($ipv6)
	    set-only: false
	    table: $TABLE6
	    chain: $chain6_name
	    priority: $hook_priority
	nftables_hooks:
	  - input
	  - forward
	pf:
	  anchor_name: ""
	prometheus:
	  enabled: false
	  listen_addr: 127.0.0.1
	  listen_port: 60601
	EOM

	# Replace bool placeholders with actual values
	sed -i "s/bool(1)/true/g" $VARCONFIG
	sed -i "s/bool(0)/false/g" $VARCONFIG
	sed -i "s/boolnot(1)/false/g" $VARCONFIG
	sed -i "s/boolnot(0)/true/g" $VARCONFIG
	sed -i "s,^\(\s*api_url\s*:\s*\).*\$,\1$api_url," $VARCONFIG
	sed -i "s,^\(\s*api_key\s*:\s*\).*\$,\1$api_key," $VARCONFIG
}

_add_interface_to_list() {
	if [ -z "$interface_list" ]; then
		interface_list="$1"
	else
		interface_list="$interface_list $1"
	fi
}

init_nftables() {
	local section="$1"

	local hook_priority
	local deny_action
	local deny_log
	local log_prefix
	local ipv4
	local ipv6
	local filter_input
	local filter_forward
	local chain_name
	local chain6_name
	local interface
	local log_term=""

	config_get hook_priority $section priority "4"
	config_get deny_action $section deny_action "drop"
	config_get_bool deny_log $section deny_log '0'
	config_get log_prefix $section log_prefix "crowdsec: "
	config_get_bool ipv4 $section ipv4 '1'
	config_get_bool ipv6 $section ipv6 '1'
	config_get_bool filter_input $section filter_input '1'
	config_get_bool filter_forward $section filter_forward '1'
	config_get chain_name $section chain_name "crowdsec-chain"
	config_get chain6_name $section chain6_name "crowdsec6-chain"

	# Read interface list properly (UCI list or single value)
	local interface_list=""
	config_list_foreach "$section" interface _add_interface_to_list
	if [ -z "$interface_list" ]; then
		# Fallback: try single value
		config_get interface_list $section interface ''
	fi
	# Default interfaces for SecuBox (eth1=WAN on x86, br-wan=WAN bridge, br-lan=LAN)
	interface="${interface_list:-eth1, br-lan, br-wan}"

	if [ "$deny_log" -eq "1" ]; then
		log_term="log prefix \"${log_prefix}\""
	fi

	# Handle multiple interfaces (space-separated to comma-separated)
	interface="${interface// /, }"

	# Clean up existing tables (kernel 3.18+ supports delete without flush)
	nft delete table ip crowdsec 2>/dev/null
	nft delete table ip6 crowdsec6 2>/dev/null

	# Setup IPv4 nftables
	if [ "$ipv4" -eq "1" ]; then
		nft add table ip crowdsec
		nft add set ip crowdsec crowdsec-blacklists '{ type ipv4_addr; flags timeout; }'

		if [ "$filter_input" -eq "1" ]; then
			nft add chain ip "$TABLE" $chain_name-input "{ type filter hook input priority $hook_priority; policy accept; }"
			nft add rule ip "$TABLE" $chain_name-input ct state established,related accept
			nft add rule ip "$TABLE" $chain_name-input iifname != \{ $interface \} accept
			# Drop traffic from blacklisted IPs
			if [ "$deny_log" -eq "1" ]; then
				nft add rule ip "$TABLE" $chain_name-input ip saddr @crowdsec-blacklists $log_term $deny_action
			else
				nft add rule ip "$TABLE" $chain_name-input ip saddr @crowdsec-blacklists $deny_action
			fi
		fi

		if [ "$filter_forward" -eq "1" ]; then
			nft add chain ip "$TABLE" $chain_name-forward "{ type filter hook forward priority $hook_priority; policy accept; }"
			nft add rule ip "$TABLE" $chain_name-forward ct state established,related accept
			nft add rule ip "$TABLE" $chain_name-forward iifname != \{ $interface \} accept
			# Drop traffic from blacklisted IPs
			if [ "$deny_log" -eq "1" ]; then
				nft add rule ip "$TABLE" $chain_name-forward ip saddr @crowdsec-blacklists $log_term $deny_action
			else
				nft add rule ip "$TABLE" $chain_name-forward ip saddr @crowdsec-blacklists $deny_action
			fi
		fi
	fi

	# Setup IPv6 nftables
	if [ "$ipv6" -eq "1" ]; then
		nft add table ip6 crowdsec6
		nft add set ip6 crowdsec6 crowdsec6-blacklists '{ type ipv6_addr; flags timeout; }'

		if [ "$filter_input" -eq "1" ]; then
			nft add chain ip6 "$TABLE6" $chain6_name-input "{ type filter hook input priority $hook_priority; policy accept; }"
			nft add rule ip6 "$TABLE6" $chain6_name-input ct state established,related accept
			nft add rule ip6 "$TABLE6" $chain6_name-input iifname != \{ $interface \} accept
			# Drop traffic from blacklisted IPs
			if [ "$deny_log" -eq "1" ]; then
				nft add rule ip6 "$TABLE6" $chain6_name-input ip6 saddr @crowdsec6-blacklists $log_term $deny_action
			else
				nft add rule ip6 "$TABLE6" $chain6_name-input ip6 saddr @crowdsec6-blacklists $deny_action
			fi
		fi

		if [ "$filter_forward" -eq "1" ]; then
			nft add chain ip6 "$TABLE6" $chain6_name-forward "{ type filter hook forward priority $hook_priority; policy accept; }"
			nft add rule ip6 "$TABLE6" $chain6_name-forward ct state established,related accept
			nft add rule ip6 "$TABLE6" $chain6_name-forward iifname != \{ $interface \} accept
			# Drop traffic from blacklisted IPs
			if [ "$deny_log" -eq "1" ]; then
				nft add rule ip6 "$TABLE6" $chain6_name-forward ip6 saddr @crowdsec6-blacklists $log_term $deny_action
			else
				nft add rule ip6 "$TABLE6" $chain6_name-forward ip6 saddr @crowdsec6-blacklists $deny_action
			fi
		fi
	fi
}

run_bouncer() {
	local section="$1"

	local enabled
	config_get_bool enabled $section enabled 0

	if [ "$enabled" -eq "1" ]; then
		init_yaml "$section"
		init_nftables "$section"

		procd_open_instance
		procd_set_param command "$PROG" -c "$VARCONFIG"
		procd_set_param stdout 1
		procd_set_param stderr 1
		procd_set_param nice 10

		# Note: ujail disabled - bouncer needs direct nftables access
		# to add/remove IPs from sets which requires CAP_NET_ADMIN
		# if [ -x "/sbin/ujail" ]; then
		# 	procd_add_jail cs-bouncer log
		# 	procd_add_jail_mount $VARCONFIG
		# 	procd_add_jail_mount_rw /var/log/
		# 	procd_set_param no_new_privs 1
		# fi

		procd_close_instance
	fi
}

wait_for_firewall() {
	# Wait for fw4/nftables to be ready (max 30 seconds)
	local i=0
	while [ $i -lt 30 ]; do
		if nft list tables >/dev/null 2>&1; then
			return 0
		fi
		sleep 1
		i=$((i + 1))
	done
	logger -t crowdsec-bouncer "Warning: nftables not ready after 30s, starting anyway"
	return 1
}

start_service() {
	# Wait for firewall/nftables to be ready
	wait_for_firewall

	config_load "${CONFIGURATION}"
	config_foreach run_bouncer bouncer
}

service_stopped() {
	# Clean up config file
	rm -f $VARCONFIG

	# Remove nftables tables
	nft delete table ip crowdsec 2>/dev/null
	nft delete table ip6 crowdsec6 2>/dev/null
}

reload_service() {
	stop
	start
}
