secubox-openwrt/package/secubox/secubox-app-cs-firewall-bouncer/files/crowdsec-firewall-bouncer.initd
CyberMind-FR f72ea0da32 fix(cs-firewall-bouncer): Add missing DROP rules for blacklisted IPs
The init script created nftables sets and chains but never added the
actual DROP rules to block traffic from blacklisted IPs. This caused
the bouncer to populate sets correctly but traffic was never blocked.

Added DROP rules for:
- IPv4 input chain (crowdsec-blacklists)
- IPv4 forward chain (crowdsec-blacklists)
- IPv6 input chain (crowdsec6-blacklists)
- IPv6 forward chain (crowdsec6-blacklists)

Each rule respects the deny_log and deny_action configuration options.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:27:46 +01:00

313 lines
9.1 KiB
Bash

#!/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:8080"
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
}