Files
shared-bans/scripts/shared-bans-apply.sh
T

111 lines
3.1 KiB
Bash

#!/bin/bash
# Shared ban list applier — pulls git repo, applies to ipset on this server.
# Runs every 5 min via systemd timer.
set -uo pipefail
REPO_DIR="/var/lib/shared-bans"
LOG="/var/log/shared-bans-apply.log"
IPSET_NAME="shared_bans"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; }
CONF="/etc/shared-bans.conf"
if [ -f "$CONF" ]; then
# shellcheck disable=SC1090
source "$CONF"
fi
REPO_URL="${SHARED_BANS_REPO_URL:-}"
if [ -z "$REPO_URL" ]; then
log "ERROR: SHARED_BANS_REPO_URL not set in $CONF"
exit 1
fi
mkdir -p "$(dirname "$REPO_DIR")"
log "=== apply tick ==="
# Clone or pull
if [ ! -d "$REPO_DIR/.git" ]; then
log "Cloning $REPO_URL"
if ! git clone --depth 1 "$REPO_URL" "$REPO_DIR" 2>>"$LOG"; then
log "ERROR: clone failed"
exit 1
fi
else
cd "$REPO_DIR" || exit 1
if ! git pull --ff-only 2>>"$LOG"; then
log "WARN: pull failed (using cached repo)"
fi
fi
cd "$REPO_DIR" || exit 1
# Build merged list (static + dynamic, deduped, sorted)
MERGED="$REPO_DIR/merged/bans.txt"
mkdir -p "$(dirname "$MERGED")"
{
cat "$REPO_DIR/static/bans.txt" 2>/dev/null
echo
cat "$REPO_DIR/dynamic/bans.txt" 2>/dev/null
} | grep -vE '^\s*(#|$)' | grep -E '^[0-9]' | awk '!seen[$0]++' | sort -V > "$MERGED"
COUNT=$(wc -l < "$MERGED")
log "Merged list: $COUNT unique entries"
if [ "$COUNT" -lt 50 ]; then
log "ERROR: suspiciously few entries ($COUNT), refusing to apply"
exit 1
fi
# Create main set if missing
if ! /sbin/ipset list "$IPSET_NAME" >/dev/null 2>&1; then
/sbin/ipset create "$IPSET_NAME" hash:net family inet hashsize 16384 maxelem 500000 2>>"$LOG"
log "Created ipset $IPSET_NAME"
fi
# Build new set with unique name (PID-based)
TMP_SET="${IPSET_NAME}_new_$$"
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
/sbin/ipset create "$TMP_SET" hash:net family inet hashsize 16384 maxelem 500000 2>>"$LOG"
# Build ipset restore-format file pointing at TMP_SET (NOT the live one)
TMP_RESTORE="$(mktemp)"
sed "s|^|add ${TMP_SET} |" "$MERGED" > "$TMP_RESTORE"
# Restore into temp set
if ! /sbin/ipset restore -file "$TMP_RESTORE" 2>>"$LOG"; then
log "ERROR: ipset restore failed (stderr above)"
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
rm -f "$TMP_RESTORE"
exit 1
fi
PRE_SWAP=$(/sbin/ipset list "$TMP_SET" 2>/dev/null | grep -cE '^[0-9]')
log "Restored $PRE_SWAP entries into $TMP_SET"
# Atomic swap so packets are never matched against an empty set
/sbin/ipset swap "$TMP_SET" "$IPSET_NAME" 2>>"$LOG" || {
log "ERROR: ipset swap failed"
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
rm -f "$TMP_RESTORE"
exit 1
}
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
rm -f "$TMP_RESTORE"
# Ensure iptables rule
if ! /sbin/iptables -C INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null; then
/sbin/iptables -I INPUT 1 -m set --match-set "$IPSET_NAME" src -j DROP 2>>"$LOG" && \
log "Inserted iptables rule for $IPSET_NAME" || \
log "WARN: failed to insert iptables rule"
fi
# Log rotation
if [ "$(wc -l < "$LOG")" -gt 5000 ]; then
tail -n 3000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
fi
APPLIED=$(/sbin/ipset list "$IPSET_NAME" 2>/dev/null | grep -cE '^[0-9]')
log "Applied: $APPLIED entries in $IPSET_NAME"
exit 0