88551536e6
Amp-Thread-ID: https://ampcode.com/threads/T-019cc404-c628-759e-a50b-f5eeea35b91f Co-authored-by: Amp <amp@ampcode.com>
997 lines
34 KiB
Bash
Executable File
997 lines
34 KiB
Bash
Executable File
#!/bin/bash
|
|
################################################################################
|
|
# Script Name: ufw-blocklists.sh
|
|
# Version: 1.0
|
|
# Description: Per-feed UFW threat intelligence blocking with ipset
|
|
# Author: Phil Connor
|
|
# Contact: contact@mylinux.work
|
|
# Website: https://mylinux.work
|
|
# License: MIT
|
|
################################################################################
|
|
# Don't use 'set -e' - it causes silent failures when log file has permission issues
|
|
|
|
CONFIG_DIR="/etc/ufw-threats"
|
|
FEEDS_CONFIG="$CONFIG_DIR/feeds.conf"
|
|
CACHE_DIR="$CONFIG_DIR/cache"
|
|
LOG_FILE="/var/log/ufw-threats.log"
|
|
SSH_PORT="22"
|
|
ENABLE_AUTO_UPDATE=true
|
|
UPDATE_INTERVAL="daily"
|
|
ENABLE_IPV6=true
|
|
UFW_RULES_FILE="/etc/ufw/before.rules"
|
|
UFW_RULES_V6_FILE="/etc/ufw/before6.rules"
|
|
IPSET_PREFIX="ufw-feed"
|
|
WHITELIST_IPSET="ufw-whitelist"
|
|
WHITELIST_IPSET_V6="ufw-whitelist-v6"
|
|
MAX_BACKUPS=10
|
|
|
|
|
|
show_usage() {
|
|
cat <<EOF
|
|
Usage: $0 [OPTIONS] [COMMAND]
|
|
|
|
PER-FEED VERSION: Each threat feed gets its own ipset and iptables rule.
|
|
Provides detailed per-feed blocking statistics and metrics.
|
|
|
|
COMMANDS:
|
|
install Install and configure threat feed blocking
|
|
update Update all enabled feeds now (ipsets only, no UFW reload)
|
|
apply-rules Regenerate and apply UFW rules (use with caution!)
|
|
test-rules Test rule generation without applying
|
|
add-feed NAME URL Add a custom feed
|
|
remove-feed NAME Remove a feed
|
|
enable-feed NAME Enable a disabled feed
|
|
disable-feed NAME Disable a feed
|
|
list-feeds List all configured feeds
|
|
show-stats Show blocking statistics per feed
|
|
whitelist-add IP Add IP/CIDR to whitelist
|
|
whitelist-init Initialize whitelist with RFC1918/Docker networks
|
|
whitelist-list Show all whitelisted IPs
|
|
clean-cache Remove cache files for disabled feeds
|
|
|
|
OPTIONS:
|
|
-h, --help Show this help message
|
|
-s, --ssh-port PORT SSH port (default: 22)
|
|
--no-auto-update Disable automatic daily updates
|
|
--no-ipv6 Disable IPv6 support
|
|
--update-interval TIME Update interval: hourly, daily, weekly (default: daily)
|
|
|
|
EXAMPLES:
|
|
sudo $0 install
|
|
sudo $0 update # Safe - only updates ipsets
|
|
sudo $0 test-rules # Safe - validates without applying
|
|
sudo $0 apply-rules # DANGER - regenerates UFW config
|
|
sudo $0 show-stats
|
|
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
log_message() {
|
|
local msg
|
|
msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
|
echo "$msg"
|
|
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
# Iterate over enabled feeds in $FEEDS_CONFIG, calling the provided callback
|
|
# function with arguments: name url type description
|
|
# Usage: for_each_enabled_feed my_callback_function
|
|
for_each_enabled_feed() {
|
|
local callback="$1"
|
|
[ -f "$FEEDS_CONFIG" ] || return 0
|
|
|
|
local enabled name url type description
|
|
while IFS='|' read -r enabled name url type description; do
|
|
[[ "$enabled" =~ ^#.*$ ]] && continue
|
|
[[ -z "$enabled" ]] && continue
|
|
[ "$enabled" != "1" ] && continue
|
|
"$callback" "$name" "$url" "$type" "$description"
|
|
done < "$FEEDS_CONFIG"
|
|
}
|
|
|
|
# Iterate over ALL feeds (enabled + disabled), calling the provided callback
|
|
# function with arguments: enabled name url type description
|
|
for_each_feed() {
|
|
local callback="$1"
|
|
[ -f "$FEEDS_CONFIG" ] || return 0
|
|
|
|
local enabled name url type description
|
|
while IFS='|' read -r enabled name url type description; do
|
|
[[ "$enabled" =~ ^#.*$ ]] && continue
|
|
[[ -z "$enabled" ]] && continue
|
|
"$callback" "$enabled" "$name" "$url" "$type" "$description"
|
|
done < "$FEEDS_CONFIG"
|
|
}
|
|
|
|
parse_args() {
|
|
COMMAND=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help) show_usage ;;
|
|
-s|--ssh-port) SSH_PORT="$2"; shift 2 ;;
|
|
--no-auto-update) ENABLE_AUTO_UPDATE=false; shift ;;
|
|
--no-ipv6) ENABLE_IPV6=false; shift ;;
|
|
--update-interval) UPDATE_INTERVAL="$2"; shift 2 ;;
|
|
install|update|apply-rules|test-rules|list-feeds|show-stats|whitelist-init|whitelist-list|clean-cache) COMMAND="$1"; shift ;;
|
|
add-feed) COMMAND="add-feed"; FEED_NAME="$2"; FEED_URL="$3"; shift 3 ;;
|
|
remove-feed|enable-feed|disable-feed) COMMAND="$1"; FEED_NAME="$2"; shift 2 ;;
|
|
whitelist-add) COMMAND="whitelist-add"; WHITELIST_IP="$2"; shift 2 ;;
|
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
[ -z "$COMMAND" ] && COMMAND="install"
|
|
}
|
|
|
|
cleanup_old_backups() {
|
|
local max_keep=${MAX_BACKUPS:-10}
|
|
|
|
find "$(dirname "$UFW_RULES_FILE")" -maxdepth 1 -name "$(basename "$UFW_RULES_FILE").backup-*" -printf '%T@ %p\n' 2>/dev/null \
|
|
| sort -rn | tail -n +$((max_keep + 1)) | cut -d' ' -f2- | xargs -r rm -f 2>/dev/null || true
|
|
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
find "$(dirname "$UFW_RULES_V6_FILE")" -maxdepth 1 -name "$(basename "$UFW_RULES_V6_FILE").backup-*" -printf '%T@ %p\n' 2>/dev/null \
|
|
| sort -rn | tail -n +$((max_keep + 1)) | cut -d' ' -f2- | xargs -r rm -f 2>/dev/null || true
|
|
fi
|
|
|
|
rm -f "${UFW_RULES_FILE}.backup-"*.clean "${UFW_RULES_V6_FILE}.backup-"*.clean 2>/dev/null || true
|
|
}
|
|
|
|
check_requirements() {
|
|
local enable_ufw="${1:-true}"
|
|
|
|
[ "$EUID" -ne 0 ] && { echo "Please run as root"; exit 1; }
|
|
|
|
if ! command -v ufw >/dev/null 2>&1; then
|
|
apt-get update && apt-get install -y ufw ipset curl 2>/dev/null || \
|
|
dnf install -y ufw ipset curl 2>/dev/null || \
|
|
yum install -y ufw ipset curl 2>/dev/null
|
|
fi
|
|
|
|
command -v ipset >/dev/null 2>&1 || apt-get install -y ipset
|
|
command -v curl >/dev/null 2>&1 || { echo "ERROR: curl required"; exit 1; }
|
|
|
|
# CRITICAL: Ensure all ipsets referenced by before.rules exist BEFORE enabling UFW.
|
|
# If ipsets are missing (e.g., after reboot, failed persistence), UFW enable will fail
|
|
# with "Set ufw-feed-XXX doesn't exist" and block ALL traffic including DNS.
|
|
ensure_ipsets_exist
|
|
|
|
if [ "$enable_ufw" = true ]; then
|
|
ufw --force enable
|
|
fi
|
|
|
|
cleanup_old_backups
|
|
}
|
|
|
|
_ensure_feed_ipset() {
|
|
local name="$1"
|
|
|
|
ipset list "${IPSET_PREFIX}-${name}" >/dev/null 2>&1 || \
|
|
ipset create "${IPSET_PREFIX}-${name}" hash:net family inet hashsize 4096 maxelem 200000 2>/dev/null || true
|
|
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
ipset list "${IPSET_PREFIX}-${name}-v6" >/dev/null 2>&1 || \
|
|
ipset create "${IPSET_PREFIX}-${name}-v6" hash:net family inet6 hashsize 4096 maxelem 200000 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
ensure_ipsets_exist() {
|
|
if [ -f /etc/ipset.conf ]; then
|
|
ipset restore -f /etc/ipset.conf 2>/dev/null || true
|
|
fi
|
|
|
|
ipset list "$WHITELIST_IPSET" >/dev/null 2>&1 || \
|
|
ipset create "$WHITELIST_IPSET" hash:net family inet hashsize 1024 maxelem 10000 2>/dev/null || true
|
|
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
ipset list "$WHITELIST_IPSET_V6" >/dev/null 2>&1 || \
|
|
ipset create "$WHITELIST_IPSET_V6" hash:net family inet6 hashsize 1024 maxelem 10000 2>/dev/null || true
|
|
fi
|
|
|
|
for_each_enabled_feed _ensure_feed_ipset
|
|
}
|
|
|
|
validate_feed_name() {
|
|
local name="$1"
|
|
if [ -z "$name" ]; then
|
|
echo "ERROR: Feed name cannot be empty"; return 1
|
|
fi
|
|
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
echo "ERROR: Feed name '$name' contains invalid characters (only a-z, 0-9, _, - allowed)"; return 1
|
|
fi
|
|
if [ "${#name}" -gt 20 ]; then
|
|
echo "ERROR: Feed name '$name' too long (max 20 chars, ipset name limit)"; return 1
|
|
fi
|
|
}
|
|
|
|
create_directory_structure() {
|
|
mkdir -p "$CONFIG_DIR" "$CACHE_DIR"
|
|
touch "$LOG_FILE"
|
|
chmod 700 "$CONFIG_DIR"
|
|
chmod 600 "$LOG_FILE"
|
|
}
|
|
|
|
initialize_feeds_config() {
|
|
local has_feeds
|
|
has_feeds=$(grep -c '^[01]|' "$FEEDS_CONFIG" 2>/dev/null || echo 0)
|
|
|
|
if [ -f "$FEEDS_CONFIG" ] && [ "$has_feeds" -gt 0 ]; then
|
|
log_message "Feeds configuration already exists with $has_feeds feeds"
|
|
return
|
|
fi
|
|
|
|
log_message "Creating feeds configuration..."
|
|
|
|
[ -f "$FEEDS_CONFIG" ] && mv "$FEEDS_CONFIG" "${FEEDS_CONFIG}.old-$(date +%Y%m%d-%H%M%S)"
|
|
|
|
cat > "$FEEDS_CONFIG" <<'EOF'
|
|
# Threat Intelligence Feeds Configuration
|
|
# Format: ENABLED|NAME|URL|TYPE|DESCRIPTION
|
|
#
|
|
# ENABLED: 1 (enabled) or 0 (disabled)
|
|
# NAME: Unique feed identifier
|
|
# URL: Feed URL
|
|
# TYPE: Format type (plain, cidr, commented, custom)
|
|
# DESCRIPTION: Feed description
|
|
|
|
1|cinsarmy|http://cinsscore.com/list/ci-badguys.txt|plain|CINS Army Malicious IPs
|
|
1|firehol-level1|https://raw.githubusercontent.com/ktsaou/blocklist-ipsets/master/firehol_level1.netset|cidr|FireHOL Level 1 - Most aggressive attackers
|
|
1|firehol-level2|https://raw.githubusercontent.com/ktsaou/blocklist-ipsets/master/firehol_level2.netset|cidr|FireHOL Level 2 - Attacks in last 48h
|
|
0|firehol-level3|https://raw.githubusercontent.com/ktsaou/blocklist-ipsets/master/firehol_level3.netset|cidr|FireHOL Level 3 - Attacks in last 30d
|
|
1|ipsum-1|https://raw.githubusercontent.com/stamparm/ipsum/master/levels/1.txt|plain|IPsum Level 1 - Most dangerous
|
|
0|ipsum-2|https://raw.githubusercontent.com/stamparm/ipsum/master/levels/2.txt|plain|IPsum Level 2 - Dangerous
|
|
0|ipsum-3|https://raw.githubusercontent.com/stamparm/ipsum/master/levels/3.txt|plain|IPsum Level 3 - Suspicious
|
|
0|spamhaus-drop|https://www.spamhaus.org/drop/drop.txt|commented|Spamhaus DROP List
|
|
0|spamhaus-edrop|https://www.spamhaus.org/drop/edrop.txt|commented|Spamhaus EDROP List
|
|
1|spamhaus-dropv6|https://www.spamhaus.org/drop/dropv6.txt|commented|Spamhaus DROP V6 List
|
|
0|feodo-tracker|https://feodotracker.abuse.ch/downloads/ipblocklist.txt|commented|Feodo Tracker C2 IPs
|
|
0|sslbl-aggressive|https://sslbl.abuse.ch/blacklist/sslipblacklist_aggressive.txt|commented|SSL Blacklist Aggressive
|
|
0|sslbl-all|https://sslbl.abuse.ch/blacklist/sslipblacklist.txt|commented|SSL Blacklist All
|
|
1|blocklist-de|https://lists.blocklist.de/lists/all.txt|plain|Blocklist.de All Attacks
|
|
0|greensnow|https://blocklist.greensnow.co/greensnow.txt|plain|GreenSnow Blacklist
|
|
0|emergingthreats|https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt|plain|Emerging Threats IPs
|
|
0|bruteforce-ssh|https://lists.blocklist.de/lists/ssh.txt|plain|SSH Bruteforce Attempts
|
|
1|binarydefense|https://www.binarydefense.com/banlist.txt|plain|Binary Defense Blacklist
|
|
1|bruteforce-bl|https://danger.rulez.sk/projects/bruteforceblocker/blist.php|commented|BruteForce Blocker
|
|
0|dshield-top|https://www.dshield.org/block.txt|commented|DShield Top Attackers
|
|
1|dshield-fhol|https://iplists.firehol.org/files/dshield.netset|commented|Dshield FireHol top 20
|
|
0|tor-exit|https://check.torproject.org/torbulkexitlist|plain|TOR Exit Nodes (optional)
|
|
0|abuseipdb-1d|https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/main/abuseipdb-s100-1d.ipv4|commented|AbuseIPDB with confidence score 100 1 day
|
|
0|abuseipdb-3d|https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/main/abuseipdb-s100-3d.ipv4|commented|AbuseIPDB with confidence score 100 3 day
|
|
0|abuseipdb-7d|https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/main/abuseipdb-s100-7d.ipv4|commented|AbuseIPDB with confidence score 100 7 day
|
|
1|abuseipdb-14d|https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/main/abuseipdb-s100-14d.ipv4|commented|AbuseIPDB with confidence score 100 14 day
|
|
0|abuseipdb-30d|https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/main/abuseipdb-s100-30d.ipv4|commented|AbuseIPDB with confidence score 100 30 day
|
|
|
|
|
|
# Add custom feeds below this line
|
|
EOF
|
|
chmod 600 "$FEEDS_CONFIG"
|
|
}
|
|
|
|
_setup_feed_ipset() {
|
|
local name="$1"
|
|
|
|
if ! ipset list "${IPSET_PREFIX}-${name}" >/dev/null 2>&1; then
|
|
ipset create "${IPSET_PREFIX}-${name}" hash:net family inet hashsize 4096 maxelem 200000
|
|
log_message " Created ipset: ${IPSET_PREFIX}-${name}"
|
|
fi
|
|
|
|
if [ "$ENABLE_IPV6" = true ] && ! ipset list "${IPSET_PREFIX}-${name}-v6" >/dev/null 2>&1; then
|
|
ipset create "${IPSET_PREFIX}-${name}-v6" hash:net family inet6 hashsize 4096 maxelem 200000
|
|
log_message " Created ipset: ${IPSET_PREFIX}-${name}-v6"
|
|
fi
|
|
}
|
|
|
|
setup_ipsets() {
|
|
log_message "Setting up ipsets (per-feed mode)..."
|
|
|
|
if ! ipset list "$WHITELIST_IPSET" >/dev/null 2>&1; then
|
|
ipset create "$WHITELIST_IPSET" hash:net family inet hashsize 1024 maxelem 10000
|
|
ipset add "$WHITELIST_IPSET" 127.0.0.1
|
|
fi
|
|
|
|
if [ "$ENABLE_IPV6" = true ] && ! ipset list "$WHITELIST_IPSET_V6" >/dev/null 2>&1; then
|
|
ipset create "$WHITELIST_IPSET_V6" hash:net family inet6 hashsize 1024 maxelem 10000
|
|
ipset add "$WHITELIST_IPSET_V6" ::1
|
|
fi
|
|
|
|
for_each_enabled_feed _setup_feed_ipset
|
|
setup_ipset_persistence
|
|
}
|
|
|
|
setup_ipset_persistence() {
|
|
cat > /etc/systemd/system/ipset-persistent.service <<'EOF'
|
|
[Unit]
|
|
Description=ipset persistent configuration
|
|
Before=network-pre.target ufw.service
|
|
Wants=network-pre.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=-/sbin/ipset restore -f /etc/ipset.conf
|
|
ExecStop=/sbin/ipset save -f /etc/ipset.conf
|
|
StandardOutput=null
|
|
StandardError=null
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
ipset save > /etc/ipset.conf
|
|
systemctl enable ipset-persistent.service 2>/dev/null || true
|
|
}
|
|
|
|
download_feed() {
|
|
local url="$1" output="$2"
|
|
local http_code
|
|
http_code=$(curl -f -s -m 60 --connect-timeout 10 -L \
|
|
-A "ufw-threat-feeds-per-feed/1.0" \
|
|
-w "%{http_code}" -o "$output" "$url" 2>/dev/null) || true
|
|
|
|
if [ ! -s "$output" ]; then
|
|
log_message " Download failed for $url (HTTP $http_code, empty response)"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
parse_feed() {
|
|
local file="$1" type="$2" output_v4="$3" output_v6="$4"
|
|
|
|
: > "$output_v4"
|
|
: > "$output_v6"
|
|
|
|
case "$type" in
|
|
plain)
|
|
grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$' "$file" >> "$output_v4" 2>/dev/null || true
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
grep -E '^[0-9a-fA-F:]+(/[0-9]+)?$' "$file" | grep ':' >> "$output_v6" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
cidr)
|
|
grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?' "$file" \
|
|
| cut -d' ' -f1 | cut -d'#' -f1 | grep -v '^$' >> "$output_v4" 2>/dev/null || true
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
grep -E '^[0-9a-fA-F:]+(/[0-9]+)?' "$file" \
|
|
| grep ':' | cut -d' ' -f1 | cut -d'#' -f1 | grep -v '^$' >> "$output_v6" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
commented)
|
|
grep -v -E '^[#;]|^$' "$file" \
|
|
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?' >> "$output_v4" 2>/dev/null || true
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
grep -v -E '^[#;]|^$' "$file" \
|
|
| grep -oE '[0-9a-fA-F:]+(/[0-9]+)?' \
|
|
| grep -E '^[0-9a-fA-F]{1,4}:[0-9a-fA-F:]+' >> "$output_v6" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
_clean_stale_cache() {
|
|
local enabled_feeds="$1"
|
|
local cleaned=0
|
|
|
|
for cache_file in "$CACHE_DIR"/*.raw "$CACHE_DIR"/*-v4.parsed "$CACHE_DIR"/*-v6.parsed; do
|
|
[ -f "$cache_file" ] || continue
|
|
local bn feed_name
|
|
bn=$(basename "$cache_file")
|
|
feed_name="${bn%%.raw}"
|
|
feed_name="${feed_name%%-v4.parsed}"
|
|
feed_name="${feed_name%%-v6.parsed}"
|
|
|
|
if ! grep -q "^${feed_name}$" <<< "$enabled_feeds"; then
|
|
rm -f "$cache_file" && cleaned=$((cleaned + 1))
|
|
fi
|
|
done
|
|
|
|
[ "$cleaned" -gt 0 ] && log_message " Cleaned $cleaned stale cache files"
|
|
}
|
|
|
|
_load_ipset_v4() {
|
|
local name="$1" v4_file="$2"
|
|
{
|
|
echo "create ${IPSET_PREFIX}-${name}-tmp hash:net family inet hashsize 4096 maxelem 200000"
|
|
while IFS= read -r ip; do
|
|
[ -z "$ip" ] && continue
|
|
echo "add ${IPSET_PREFIX}-${name}-tmp $ip"
|
|
done < "$v4_file"
|
|
echo "swap ${IPSET_PREFIX}-${name} ${IPSET_PREFIX}-${name}-tmp"
|
|
echo "destroy ${IPSET_PREFIX}-${name}-tmp"
|
|
} | ipset restore 2>/dev/null
|
|
}
|
|
|
|
_load_ipset_v6() {
|
|
local name="$1" v6_file="$2"
|
|
{
|
|
echo "create ${IPSET_PREFIX}-${name}-v6-tmp hash:net family inet6 hashsize 4096 maxelem 200000"
|
|
while IFS= read -r ip; do
|
|
[ -z "$ip" ] && continue
|
|
echo "add ${IPSET_PREFIX}-${name}-v6-tmp $ip"
|
|
done < "$v6_file"
|
|
echo "swap ${IPSET_PREFIX}-${name}-v6 ${IPSET_PREFIX}-${name}-v6-tmp"
|
|
echo "destroy ${IPSET_PREFIX}-${name}-v6-tmp"
|
|
} | ipset restore 2>/dev/null
|
|
}
|
|
|
|
update_feeds() {
|
|
log_message "Starting per-feed update..."
|
|
|
|
if [ ! -f "$FEEDS_CONFIG" ]; then
|
|
echo "ERROR: Feeds config not found: $FEEDS_CONFIG"
|
|
echo "Run 'install' command first"
|
|
exit 1
|
|
fi
|
|
|
|
local enabled_count
|
|
enabled_count=$(grep -c '^1|' "$FEEDS_CONFIG" 2>/dev/null || echo 0)
|
|
if [ "$enabled_count" -eq 0 ]; then
|
|
echo "ERROR: No enabled feeds found in $FEEDS_CONFIG"
|
|
echo "Check the config file format"
|
|
exit 1
|
|
fi
|
|
|
|
log_message "Found $enabled_count enabled feeds"
|
|
|
|
local enabled_feeds
|
|
enabled_feeds=$(grep '^1|' "$FEEDS_CONFIG" 2>/dev/null | cut -d'|' -f2)
|
|
|
|
# NOTE: Do NOT destroy ipsets for disabled feeds here. The before.rules may still
|
|
# reference them (if apply-rules hasn't been re-run). Destroying in-use ipsets causes
|
|
# "Set doesn't exist" on next UFW reload, which blocks all traffic.
|
|
# Ipset cleanup happens safely in cmd_disable_feed/cmd_remove_feed after rules are regenerated.
|
|
_clean_stale_cache "$enabled_feeds"
|
|
|
|
local total_feeds=0
|
|
local failed_feeds=0
|
|
|
|
local enabled name url type description
|
|
while IFS='|' read -r enabled name url type description; do
|
|
[[ "$enabled" =~ ^#.*$ ]] && continue
|
|
[[ -z "$enabled" ]] && continue
|
|
[ "$enabled" != "1" ] && continue
|
|
|
|
total_feeds=$((total_feeds + 1))
|
|
log_message "Processing feed: $name"
|
|
|
|
local raw="$CACHE_DIR/${name}.raw"
|
|
local v4_file="$CACHE_DIR/${name}-v4.parsed"
|
|
local v6_file="$CACHE_DIR/${name}-v6.parsed"
|
|
|
|
if download_feed "$url" "$raw" && parse_feed "$raw" "$type" "$v4_file" "$v6_file"; then
|
|
local count_v4 count_v6
|
|
count_v4=$(wc -l < "$v4_file" 2>/dev/null || echo 0)
|
|
count_v6=0
|
|
[ "$ENABLE_IPV6" = true ] && count_v6=$(wc -l < "$v6_file" 2>/dev/null || echo 0)
|
|
|
|
[ "$count_v4" -gt 0 ] && _load_ipset_v4 "$name" "$v4_file"
|
|
[ "$ENABLE_IPV6" = true ] && [ "$count_v6" -gt 0 ] && _load_ipset_v6 "$name" "$v6_file"
|
|
|
|
log_message " $name: $count_v4 IPv4, $count_v6 IPv6"
|
|
else
|
|
log_message " FAILED: $name"
|
|
failed_feeds=$((failed_feeds + 1))
|
|
fi
|
|
done < "$FEEDS_CONFIG"
|
|
|
|
ipset save > /etc/ipset.conf
|
|
log_message "Updated $total_feeds feeds ($failed_feeds failed)"
|
|
}
|
|
|
|
# Build iptables rules block for IPv4 or IPv6
|
|
# Args: v4|v6 output_file
|
|
_build_rules_block() {
|
|
local family="$1" output="$2"
|
|
local chain_prefix whitelist_set set_suffix log_tag
|
|
|
|
if [ "$family" = "v4" ]; then
|
|
chain_prefix="ufw-before-input"
|
|
whitelist_set="$WHITELIST_IPSET"
|
|
set_suffix=""
|
|
log_tag="THREAT"
|
|
else
|
|
chain_prefix="ufw6-before-input"
|
|
whitelist_set="$WHITELIST_IPSET_V6"
|
|
set_suffix="-v6"
|
|
log_tag="THREAT-v6"
|
|
fi
|
|
|
|
cat > "$output" <<EOF
|
|
|
|
# UFW THREAT FEEDS - PER-FEED MODE - START
|
|
# Whitelist bypass
|
|
-A ${chain_prefix} -m set --match-set ${whitelist_set} src -j ACCEPT
|
|
|
|
EOF
|
|
|
|
local enabled name url type description
|
|
while IFS='|' read -r enabled name url type description; do
|
|
[[ "$enabled" =~ ^#.*$ ]] && continue
|
|
[[ -z "$enabled" ]] && continue
|
|
[ "$enabled" != "1" ] && continue
|
|
|
|
cat >> "$output" <<EOF
|
|
# $description
|
|
-A ${chain_prefix} -m set --match-set ${IPSET_PREFIX}-${name}${set_suffix} src -m limit --limit 5/min -j LOG --log-prefix "[${log_tag}:${name}] "
|
|
-A ${chain_prefix} -m set --match-set ${IPSET_PREFIX}-${name}${set_suffix} src -j DROP
|
|
EOF
|
|
done < "$FEEDS_CONFIG"
|
|
|
|
echo "# UFW THREAT FEEDS - PER-FEED MODE - END" >> "$output"
|
|
}
|
|
|
|
# Insert rules into a UFW template file and validate
|
|
# Args: template_file rules_file output_file
|
|
# Returns 0 on success, 1 on validation failure
|
|
_insert_and_validate_rules() {
|
|
local template="$1" rules_file="$2" output="$3"
|
|
local insert_line
|
|
|
|
insert_line=$(grep -n "^# End required lines" "$template" | cut -d: -f1)
|
|
if [ -z "$insert_line" ]; then
|
|
log_message "ERROR: Could not find '# End required lines' in $template"
|
|
return 1
|
|
fi
|
|
|
|
head -n "$insert_line" "$template" > "$output"
|
|
cat "$rules_file" >> "$output"
|
|
tail -n +"$((insert_line + 1))" "$template" >> "$output"
|
|
|
|
local filter_count
|
|
filter_count=$(grep -c '^\*filter' "$output" 2>/dev/null || echo 0)
|
|
if [ "$filter_count" -ne 1 ]; then
|
|
log_message "ERROR: Generated rules file has $filter_count *filter blocks (expected 1)"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
_verify_ipsets_callback() {
|
|
local name="$1"
|
|
|
|
if ! ipset list "${IPSET_PREFIX}-${name}" >/dev/null 2>&1; then
|
|
log_message "ERROR: Required ipset ${IPSET_PREFIX}-${name} is missing"
|
|
_MISSING_SETS=$((_MISSING_SETS + 1))
|
|
fi
|
|
if [ "$ENABLE_IPV6" = true ] && ! ipset list "${IPSET_PREFIX}-${name}-v6" >/dev/null 2>&1; then
|
|
log_message "ERROR: Required ipset ${IPSET_PREFIX}-${name}-v6 is missing"
|
|
_MISSING_SETS=$((_MISSING_SETS + 1))
|
|
fi
|
|
}
|
|
|
|
apply_ufw_rules() {
|
|
log_message "Applying UFW rules (per-feed)..."
|
|
|
|
if [ ! -f /usr/share/ufw/before.rules ]; then
|
|
log_message "ERROR: UFW default template /usr/share/ufw/before.rules not found"
|
|
return 1
|
|
fi
|
|
|
|
local tmpdir
|
|
tmpdir=$(mktemp -d)
|
|
trap 'rm -rf "$tmpdir"' RETURN
|
|
|
|
[ -f "$UFW_RULES_FILE" ] && cp "$UFW_RULES_FILE" "${UFW_RULES_FILE}.backup-$(date +%Y%m%d-%H%M%S)"
|
|
[ "$ENABLE_IPV6" = true ] && [ -f "$UFW_RULES_V6_FILE" ] && \
|
|
cp "$UFW_RULES_V6_FILE" "${UFW_RULES_V6_FILE}.backup-$(date +%Y%m%d-%H%M%S)"
|
|
|
|
cp /usr/share/ufw/before.rules "$UFW_RULES_FILE"
|
|
[ "$ENABLE_IPV6" = true ] && cp /usr/share/ufw/before6.rules "$UFW_RULES_V6_FILE"
|
|
|
|
log_message " Starting from clean UFW templates"
|
|
|
|
# Build and insert IPv4 rules
|
|
local v4_rules="$tmpdir/v4_rules"
|
|
local v4_output="$tmpdir/v4_output"
|
|
_build_rules_block "v4" "$v4_rules"
|
|
|
|
if ! _insert_and_validate_rules "$UFW_RULES_FILE" "$v4_rules" "$v4_output"; then
|
|
log_message " Aborting to prevent corruption."
|
|
return 1
|
|
fi
|
|
mv "$v4_output" "$UFW_RULES_FILE"
|
|
log_message " IPv4 rules generated and validated"
|
|
|
|
# Build and insert IPv6 rules
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
local v6_rules="$tmpdir/v6_rules"
|
|
local v6_output="$tmpdir/v6_output"
|
|
_build_rules_block "v6" "$v6_rules"
|
|
|
|
if _insert_and_validate_rules "$UFW_RULES_V6_FILE" "$v6_rules" "$v6_output"; then
|
|
mv "$v6_output" "$UFW_RULES_V6_FILE"
|
|
log_message " IPv6 rules generated and validated"
|
|
else
|
|
log_message " Aborting IPv6 rules. Keeping IPv4 only."
|
|
fi
|
|
fi
|
|
|
|
ufw limit "$SSH_PORT/tcp" 2>/dev/null || ufw allow "$SSH_PORT/tcp"
|
|
|
|
# CRITICAL: Ensure all ipsets exist BEFORE reloading UFW
|
|
log_message " Verifying ipsets exist..."
|
|
ensure_ipsets_exist
|
|
setup_ipsets
|
|
|
|
_MISSING_SETS=0
|
|
for_each_enabled_feed _verify_ipsets_callback
|
|
|
|
if [ "$_MISSING_SETS" -gt 0 ]; then
|
|
log_message "ERROR: $_MISSING_SETS required ipsets missing. Aborting UFW reload to prevent lockout."
|
|
return 1
|
|
fi
|
|
|
|
ipset save > /etc/ipset.conf
|
|
|
|
log_message " Reloading UFW..."
|
|
if ufw status | grep -q "Status: active"; then
|
|
ufw reload
|
|
else
|
|
ufw --force enable
|
|
fi
|
|
|
|
cleanup_old_backups
|
|
log_message "UFW rules applied and validated successfully"
|
|
}
|
|
|
|
setup_auto_update() {
|
|
[ "$ENABLE_AUTO_UPDATE" = false ] && return
|
|
|
|
local script_path
|
|
script_path=$(readlink -f "$0")
|
|
|
|
cat > /etc/systemd/system/ufw-threat-feeds-update.service <<EOF
|
|
[Unit]
|
|
Description=Update UFW threat feeds (per-feed)
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=$script_path update
|
|
ExecStartPost=/bin/sh -c 'ipset save > /etc/ipset.conf'
|
|
EOF
|
|
|
|
cat > /etc/systemd/system/ufw-threat-feeds-update.timer <<EOF
|
|
[Unit]
|
|
Description=Update UFW threat feeds $UPDATE_INTERVAL
|
|
|
|
[Timer]
|
|
Unit=ufw-threat-feeds-update.service
|
|
OnCalendar=$UPDATE_INTERVAL
|
|
Persistent=true
|
|
RandomizedDelaySec=1800
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now ufw-threat-feeds-update.timer
|
|
}
|
|
|
|
create_management_commands() {
|
|
cat > /usr/local/bin/ufw-whitelist <<'EOF'
|
|
#!/bin/bash
|
|
[ -z "$1" ] && { echo "Usage: ufw-whitelist <IP|CIDR>"; exit 1; }
|
|
if [[ "$1" == *:* ]]; then
|
|
ipset add ufw-whitelist-v6 "$1" && echo "Whitelisted IPv6: $1"
|
|
else
|
|
ipset add ufw-whitelist "$1" && echo "Whitelisted IPv4: $1"
|
|
fi
|
|
ipset save > /etc/ipset.conf
|
|
EOF
|
|
|
|
local script_path
|
|
script_path=$(readlink -f "$0")
|
|
cat > /usr/local/bin/ufw-threat-reload <<EOF
|
|
#!/bin/bash
|
|
$script_path apply-rules
|
|
EOF
|
|
|
|
chmod +x /usr/local/bin/ufw-{whitelist,threat-reload}
|
|
}
|
|
|
|
cmd_show_stats() {
|
|
echo "=========================================="
|
|
echo "Per-Feed Blocking Statistics"
|
|
echo "=========================================="
|
|
printf "%-25s %10s %10s %12s\n" "FEED" "IPv4 IPs" "IPv6 IPs" "BLOCKS (1h)"
|
|
echo "-------------------------------------------------------------------"
|
|
|
|
if [ ! -f "$FEEDS_CONFIG" ]; then
|
|
echo "ERROR: Config not found. Run 'install' first."
|
|
return 1
|
|
fi
|
|
|
|
local enabled name url type description
|
|
local v4_count v6_count blocks
|
|
while IFS='|' read -r enabled name url type description; do
|
|
[[ "$enabled" =~ ^#.*$ ]] && continue
|
|
[[ -z "$enabled" ]] && continue
|
|
[ "$enabled" != "1" ] && continue
|
|
|
|
v4_count=$(ipset list "${IPSET_PREFIX}-${name}" 2>/dev/null | grep -c '^[0-9]' 2>/dev/null)
|
|
v4_count=${v4_count:-0}
|
|
|
|
v6_count=0
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
v6_count=$(ipset list "${IPSET_PREFIX}-${name}-v6" 2>/dev/null | grep -c '^[0-9a-fA-F:]' 2>/dev/null)
|
|
v6_count=${v6_count:-0}
|
|
fi
|
|
|
|
blocks=$(journalctl --since "1 hour ago" 2>/dev/null | grep -c "\[THREAT:${name}\]" 2>/dev/null)
|
|
blocks=${blocks:-0}
|
|
|
|
printf "%-25s %10d %10d %12d\n" "$name" "$v4_count" "$v6_count" "$blocks"
|
|
done < "$FEEDS_CONFIG"
|
|
}
|
|
|
|
_list_feed_entry() {
|
|
local feed_enabled="$1" name="$2" url="$3" type="$4" description="$5"
|
|
local status="DISABLED"
|
|
[ "$feed_enabled" = "1" ] && status="ENABLED"
|
|
printf "%-10s %-25s %s\n" "$status" "$name" "$description"
|
|
}
|
|
|
|
cmd_list_feeds() {
|
|
printf "%-10s %-25s %s\n" "STATUS" "NAME" "DESCRIPTION"
|
|
echo "-------------------------------------------------------------------"
|
|
for_each_feed _list_feed_entry
|
|
}
|
|
|
|
cmd_add_feed() {
|
|
validate_feed_name "$FEED_NAME" || exit 1
|
|
grep -q "^[01]|${FEED_NAME}|" "$FEEDS_CONFIG" 2>/dev/null && { echo "Feed exists"; exit 1; }
|
|
echo "1|${FEED_NAME}|${FEED_URL}|plain|Custom: ${FEED_NAME}" >> "$FEEDS_CONFIG"
|
|
log_message "Added feed: $FEED_NAME"
|
|
}
|
|
|
|
cmd_remove_feed() {
|
|
validate_feed_name "$FEED_NAME" || exit 1
|
|
sed -i "/^[01]|${FEED_NAME}|/d" "$FEEDS_CONFIG"
|
|
log_message "Removed feed: $FEED_NAME"
|
|
|
|
log_message "Regenerating UFW rules..."
|
|
apply_ufw_rules || return 1
|
|
|
|
ipset destroy "${IPSET_PREFIX}-${FEED_NAME}" 2>/dev/null || true
|
|
ipset destroy "${IPSET_PREFIX}-${FEED_NAME}-v6" 2>/dev/null || true
|
|
}
|
|
|
|
cmd_enable_feed() {
|
|
validate_feed_name "$FEED_NAME" || exit 1
|
|
sed -i "s/^0|${FEED_NAME}|/1|${FEED_NAME}|/" "$FEEDS_CONFIG"
|
|
log_message "Enabled: $FEED_NAME"
|
|
|
|
log_message "Regenerating UFW rules..."
|
|
apply_ufw_rules
|
|
}
|
|
|
|
cmd_disable_feed() {
|
|
validate_feed_name "$FEED_NAME" || exit 1
|
|
sed -i "s/^1|${FEED_NAME}|/0|${FEED_NAME}|/" "$FEEDS_CONFIG"
|
|
log_message "Disabled: $FEED_NAME"
|
|
|
|
log_message "Regenerating UFW rules..."
|
|
apply_ufw_rules || return 1
|
|
|
|
ipset destroy "${IPSET_PREFIX}-${FEED_NAME}" 2>/dev/null || true
|
|
ipset destroy "${IPSET_PREFIX}-${FEED_NAME}-v6" 2>/dev/null || true
|
|
}
|
|
|
|
cmd_whitelist_add() {
|
|
[ -z "$WHITELIST_IP" ] && { echo "Usage: $0 whitelist-add <IP|CIDR>"; exit 1; }
|
|
|
|
if [[ "$WHITELIST_IP" == *:* ]]; then
|
|
if ipset add "$WHITELIST_IPSET_V6" "$WHITELIST_IP" 2>/dev/null; then
|
|
log_message "Added to IPv6 whitelist: $WHITELIST_IP"
|
|
else
|
|
echo "Failed to add $WHITELIST_IP"; exit 1
|
|
fi
|
|
else
|
|
if ipset add "$WHITELIST_IPSET" "$WHITELIST_IP" 2>/dev/null; then
|
|
log_message "Added to IPv4 whitelist: $WHITELIST_IP"
|
|
else
|
|
echo "Failed to add $WHITELIST_IP"; exit 1
|
|
fi
|
|
fi
|
|
|
|
ipset save > /etc/ipset.conf
|
|
}
|
|
|
|
cmd_whitelist_init() {
|
|
log_message "Initializing whitelist with private networks..."
|
|
|
|
local private_networks=(
|
|
"10.0.0.0/8"
|
|
"172.16.0.0/12"
|
|
"192.168.0.0/16"
|
|
"169.254.0.0/16"
|
|
"127.0.0.0/8"
|
|
)
|
|
|
|
local private_networks_v6=(
|
|
"fc00::/7"
|
|
"fe80::/10"
|
|
"::1"
|
|
)
|
|
|
|
echo "Adding IPv4 private networks to whitelist..."
|
|
for net in "${private_networks[@]}"; do
|
|
if ipset add "$WHITELIST_IPSET" "$net" 2>/dev/null; then
|
|
echo " + $net"
|
|
else
|
|
echo " - $net (already exists or error)"
|
|
fi
|
|
done
|
|
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
echo "Adding IPv6 private networks to whitelist..."
|
|
for net in "${private_networks_v6[@]}"; do
|
|
if ipset add "$WHITELIST_IPSET_V6" "$net" 2>/dev/null; then
|
|
echo " + $net"
|
|
else
|
|
echo " - $net (already exists or error)"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
ipset save > /etc/ipset.conf
|
|
log_message "Whitelist initialized with RFC1918/private networks"
|
|
}
|
|
|
|
cmd_whitelist_list() {
|
|
echo "=========================================="
|
|
echo "IPv4 Whitelist ($WHITELIST_IPSET)"
|
|
echo "=========================================="
|
|
ipset list "$WHITELIST_IPSET" 2>/dev/null | grep -E '^[0-9]' || echo "No entries"
|
|
|
|
if [ "$ENABLE_IPV6" = true ]; then
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "IPv6 Whitelist ($WHITELIST_IPSET_V6)"
|
|
echo "=========================================="
|
|
ipset list "$WHITELIST_IPSET_V6" 2>/dev/null | grep -E '^[0-9a-fA-F:]' || echo "No entries"
|
|
fi
|
|
}
|
|
|
|
cmd_clean_cache() {
|
|
log_message "Cleaning cache for disabled feeds..."
|
|
|
|
local removed=0
|
|
local kept=0
|
|
|
|
local enabled_feeds
|
|
enabled_feeds=$(grep '^1|' "$FEEDS_CONFIG" 2>/dev/null | cut -d'|' -f2)
|
|
|
|
for cache_file in "$CACHE_DIR"/*.raw "$CACHE_DIR"/*-v4.parsed "$CACHE_DIR"/*-v6.parsed; do
|
|
[ -f "$cache_file" ] || continue
|
|
|
|
local bn feed_name
|
|
bn=$(basename "$cache_file")
|
|
feed_name="${bn%%.raw}"
|
|
feed_name="${feed_name%%-v4.parsed}"
|
|
feed_name="${feed_name%%-v6.parsed}"
|
|
|
|
if ! grep -q "^${feed_name}$" <<< "$enabled_feeds"; then
|
|
rm -f "$cache_file"
|
|
removed=$((removed + 1))
|
|
else
|
|
kept=$((kept + 1))
|
|
fi
|
|
done
|
|
|
|
log_message "Removed $removed cache files, kept $kept active feeds"
|
|
}
|
|
|
|
cmd_test_rules() {
|
|
log_message "Testing UFW rule generation (dry-run mode)..."
|
|
|
|
if [ ! -f /usr/share/ufw/before.rules ]; then
|
|
echo "ERROR: UFW default template /usr/share/ufw/before.rules not found"
|
|
return 1
|
|
fi
|
|
|
|
local test_dir
|
|
test_dir=$(mktemp -d)
|
|
trap 'rm -rf "$test_dir"' RETURN
|
|
|
|
local test_v4="$test_dir/before.rules.test"
|
|
cp /usr/share/ufw/before.rules "$test_v4"
|
|
[ "$ENABLE_IPV6" = true ] && cp /usr/share/ufw/before6.rules "$test_dir/before6.rules.test"
|
|
|
|
local v4_rules="$test_dir/v4_rules"
|
|
local v4_output="$test_dir/v4_output"
|
|
|
|
_build_rules_block "v4" "$v4_rules"
|
|
|
|
local feed_count
|
|
feed_count=$(grep -c '^1|' "$FEEDS_CONFIG" 2>/dev/null || echo 0)
|
|
echo "Generated rules for $feed_count enabled feeds"
|
|
|
|
if ! _insert_and_validate_rules "$test_v4" "$v4_rules" "$v4_output"; then
|
|
echo "VALIDATION FAILED"
|
|
return 1
|
|
fi
|
|
|
|
echo "Validation passed: exactly 1 *filter block found"
|
|
|
|
local total_lines rule_lines
|
|
total_lines=$(wc -l < "$v4_output")
|
|
rule_lines=$(grep -c "^-A " "$v4_output" 2>/dev/null || echo 0)
|
|
|
|
echo "Generated $rule_lines iptables rules in $total_lines total lines"
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "Sample of generated rules:"
|
|
echo "=========================================="
|
|
grep "# UFW THREAT FEEDS" -A 10 "$v4_output" | head -15
|
|
echo "..."
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "Test passed - rules would be generated safely"
|
|
echo " To apply these rules, run: $0 apply-rules"
|
|
echo "=========================================="
|
|
}
|
|
|
|
cmd_install() {
|
|
log_message "Installing per-feed threat blocking..."
|
|
check_requirements
|
|
create_directory_structure
|
|
initialize_feeds_config
|
|
setup_ipsets
|
|
update_feeds
|
|
apply_ufw_rules
|
|
setup_auto_update
|
|
create_management_commands
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "Per-Feed Installation Complete"
|
|
echo "=========================================="
|
|
echo "Mode: Per-feed ipsets (detailed tracking)"
|
|
echo "Feeds: $(grep -c '^1|' "$FEEDS_CONFIG")"
|
|
echo "IPv6: $ENABLE_IPV6"
|
|
echo "Auto-update: $ENABLE_AUTO_UPDATE ($UPDATE_INTERVAL)"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " $0 show-stats # View per-feed statistics"
|
|
echo " $0 update # Update all feeds"
|
|
echo " ufw-whitelist IP # Whitelist an IP"
|
|
echo ""
|
|
echo "Logs: grep 'THREAT:' /var/log/syslog"
|
|
echo "=========================================="
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
case "$COMMAND" in
|
|
install) cmd_install ;;
|
|
update)
|
|
check_requirements false
|
|
create_directory_structure
|
|
update_feeds
|
|
;;
|
|
apply-rules)
|
|
check_requirements
|
|
apply_ufw_rules
|
|
;;
|
|
test-rules) cmd_test_rules ;;
|
|
list-feeds) cmd_list_feeds ;;
|
|
show-stats) cmd_show_stats ;;
|
|
add-feed) cmd_add_feed ;;
|
|
remove-feed) cmd_remove_feed ;;
|
|
enable-feed) cmd_enable_feed ;;
|
|
disable-feed) cmd_disable_feed ;;
|
|
whitelist-add) cmd_whitelist_add ;;
|
|
whitelist-init) cmd_whitelist_init ;;
|
|
whitelist-list) cmd_whitelist_list ;;
|
|
clean-cache) cmd_clean_cache ;;
|
|
*) show_usage ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|