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>
313 lines
9.1 KiB
Bash
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
|
|
}
|