#!/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