blob: b561f532755546e57edc8d5ed9338f41ae506d9d [file] [log] [blame]
#!/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"