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"