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