Add script to block Tor exit nodes

Change-Id: Ib4c216a9bd9c9d2f91a0fd8e5b7d420a5838ad0a
diff --git a/scripts/block_tor_exits.sh b/scripts/block_tor_exits.sh
new file mode 100755
index 0000000..b561f53
--- /dev/null
+++ b/scripts/block_tor_exits.sh
@@ -0,0 +1,186 @@
+#!/bin/bash
+#
+# Block Tor Exit Nodes Script
+# Fetches the current list of Tor exit nodes and blocks them via iptables
+# Intended to be run regularly via cron
+#
+# Usage: block_tor_exits.sh [-v|--verbose] [-d|--debug] [-n|--dry-run]
+#
+# Example crontab entry (run every 6 hours):
+# 0 */6 * * * /path/to/block_tor_exits.sh >> /var/log/block_tor_exits.log 2>&1
+
+# Configuration
+CHAIN_NAME="TOR_BLOCKLIST"
+TOR_EXIT_LIST_URL="https://check.torproject.org/torbulkexitlist"
+TEMP_FILE="/tmp/tor_exit_nodes.txt"
+
+# Options
+VERBOSE=false
+DEBUG=false
+DRY_RUN=false
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        -v|--verbose) VERBOSE=true; shift ;;
+        -d|--debug)   DEBUG=true; VERBOSE=true; shift ;;
+        -n|--dry-run) DRY_RUN=true; VERBOSE=true; shift ;;
+        *) echo "Unknown option: $1"; exit 1 ;;
+    esac
+done
+
+# Logging functions
+log() {
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
+}
+
+log_verbose() {
+    if $VERBOSE; then
+        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [VERBOSE] $1"
+    fi
+}
+
+log_debug() {
+    if $DEBUG; then
+        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] $1"
+    fi
+}
+
+# Check if running as root
+if [[ $EUID -ne 0 ]]; then
+    log "ERROR: This script must be run as root"
+    exit 1
+fi
+
+log "Starting Tor exit node blocklist update..."
+log_verbose "Chain name: ${CHAIN_NAME}"
+log_verbose "Source URL: ${TOR_EXIT_LIST_URL}"
+log_verbose "Temp file: ${TEMP_FILE}"
+$DRY_RUN && log "*** DRY RUN MODE - no iptables changes will be made ***"
+
+# Download the Tor exit node list
+log "Fetching Tor exit node list..."
+log_debug "Running: curl -s --max-time 60 -o ${TEMP_FILE} ${TOR_EXIT_LIST_URL}"
+
+if ! curl -s --max-time 60 -o "${TEMP_FILE}" "${TOR_EXIT_LIST_URL}"; then
+    log "ERROR: Failed to download Tor exit node list"
+    exit 1
+fi
+
+# Show file info
+FILE_SIZE=$(stat -c%s "${TEMP_FILE}" 2>/dev/null || echo "unknown")
+log_verbose "Downloaded file size: ${FILE_SIZE} bytes"
+
+# Debug: show first few lines
+if $DEBUG; then
+    log_debug "First 5 lines of downloaded file:"
+    head -5 "${TEMP_FILE}" | while read -r line; do
+        log_debug "  > ${line}"
+    done
+fi
+
+# Verify we got valid data (should contain IP addresses)
+VALID_IP_COUNT=$(grep -cE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' "${TEMP_FILE}" || echo "0")
+log_verbose "Valid IP addresses in file: ${VALID_IP_COUNT}"
+
+if [[ "${VALID_IP_COUNT}" -eq 0 ]]; then
+    log "ERROR: Downloaded file doesn't contain valid IP addresses"
+    if $DEBUG; then
+        log_debug "File contents:"
+        cat "${TEMP_FILE}"
+    fi
+    rm -f "${TEMP_FILE}"
+    exit 1
+fi
+
+TOTAL_LINES=$(wc -l < "${TEMP_FILE}")
+log "Downloaded file: ${TOTAL_LINES} lines, ${VALID_IP_COUNT} valid IPs"
+
+# Create the chain if it doesn't exist
+if ! iptables -L "${CHAIN_NAME}" -n >/dev/null 2>&1; then
+    log "Creating new iptables chain: ${CHAIN_NAME}"
+    if ! $DRY_RUN; then
+        iptables -N "${CHAIN_NAME}"
+    fi
+    # Add jump rule to INPUT chain if not already present
+    if ! iptables -C INPUT -j "${CHAIN_NAME}" 2>/dev/null; then
+        log_verbose "Adding jump rule from INPUT to ${CHAIN_NAME}"
+        if ! $DRY_RUN; then
+            iptables -I INPUT -j "${CHAIN_NAME}"
+        fi
+    fi
+else
+    log_verbose "Chain ${CHAIN_NAME} already exists"
+    EXISTING_RULES=$(iptables -L "${CHAIN_NAME}" -n | tail -n +3 | wc -l)
+    log_verbose "Existing rules in chain: ${EXISTING_RULES}"
+fi
+
+# Flush existing rules in the chain
+log "Flushing existing rules in ${CHAIN_NAME}..."
+if ! $DRY_RUN; then
+    iptables -F "${CHAIN_NAME}"
+fi
+
+# Add new rules
+log "Adding blocking rules..."
+ADDED=0
+SKIPPED=0
+ERRORS=0
+
+while IFS= read -r ip; do
+    # Skip empty lines and comments
+    if [[ -z "$ip" || "$ip" =~ ^# ]]; then
+        log_debug "Skipping line: '${ip}'"
+        continue
+    fi
+    
+    # Trim whitespace (including carriage returns from Windows-style files)
+    ip=$(echo "$ip" | tr -d '\r' | xargs)
+    
+    # Validate IP format
+    if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+        if ! $DRY_RUN; then
+            if iptables -A "${CHAIN_NAME}" -s "$ip" -j DROP 2>/dev/null; then
+                ADDED=$((ADDED + 1))
+            else
+                log_verbose "Failed to add rule for IP: ${ip}"
+                ERRORS=$((ERRORS + 1))
+            fi
+        else
+            ADDED=$((ADDED + 1))
+        fi
+        
+        # Progress indicator every 500 IPs
+        if $VERBOSE && (( ADDED % 500 == 0 )); then
+            log_verbose "Progress: ${ADDED} rules added..."
+        fi
+    else
+        log_debug "Invalid IP format: '${ip}'"
+        SKIPPED=$((SKIPPED + 1))
+    fi
+done < "${TEMP_FILE}"
+
+log "Results: Added ${ADDED} rules, skipped ${SKIPPED} invalid, ${ERRORS} errors"
+
+# Verify the rules were added
+if ! $DRY_RUN; then
+    FINAL_COUNT=$(iptables -L "${CHAIN_NAME}" -n | tail -n +3 | wc -l)
+    log "Verification: ${FINAL_COUNT} rules now in ${CHAIN_NAME} chain"
+    
+    if [[ ${FINAL_COUNT} -ne ${ADDED} ]]; then
+        log "WARNING: Expected ${ADDED} rules but found ${FINAL_COUNT}"
+    fi
+fi
+
+# Clean up
+rm -f "${TEMP_FILE}"
+log_verbose "Cleaned up temp file"
+
+# Optional: Save iptables rules (uncomment based on your distro)
+# For Debian/Ubuntu:
+# iptables-save > /etc/iptables/rules.v4
+
+# For RHEL/CentOS:
+# service iptables save
+
+log "Tor exit node blocking updated successfully"