CrowdSec: - Change LAPI default port from 8080 to 8180 (avoid Docker conflict) - Update bouncer config, init script, and RPCD dashboard - Fix port detection hex value (1FF4 for 8180) Streamlit: - Complete rewrite with folder-based app structure - Multi-instance support (multiple apps on different ports) - Gitea integration (clone, pull, setup commands) - Auto-install requirements.txt with hash-based caching HexoJS: - Multi-instance support with folder structure - Multiple blog instances on different ports HAProxy: - Auto-generate fallback backends (luci, apps, default_luci) - Add --server letsencrypt to ACME commands 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: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
|
|
}
|