111 lines
3.1 KiB
Bash
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
|