| Marc Kupietz | c6c873e | 2026-01-31 11:00:58 +0100 | [diff] [blame] | 1 | #!/bin/bash |
| 2 | # |
| 3 | # Block Tor Exit Nodes Script |
| 4 | # Fetches the current list of Tor exit nodes and blocks them via iptables |
| 5 | # Intended to be run regularly via cron |
| 6 | # |
| 7 | # Usage: block_tor_exits.sh [-v|--verbose] [-d|--debug] [-n|--dry-run] |
| 8 | # |
| 9 | # Example crontab entry (run every 6 hours): |
| 10 | # 0 */6 * * * /path/to/block_tor_exits.sh >> /var/log/block_tor_exits.log 2>&1 |
| 11 | |
| 12 | # Configuration |
| 13 | CHAIN_NAME="TOR_BLOCKLIST" |
| 14 | TOR_EXIT_LIST_URL="https://check.torproject.org/torbulkexitlist" |
| 15 | TEMP_FILE="/tmp/tor_exit_nodes.txt" |
| 16 | |
| 17 | # Options |
| 18 | VERBOSE=false |
| 19 | DEBUG=false |
| 20 | DRY_RUN=false |
| 21 | |
| 22 | # Parse command line arguments |
| 23 | while [[ $# -gt 0 ]]; do |
| 24 | case $1 in |
| 25 | -v|--verbose) VERBOSE=true; shift ;; |
| 26 | -d|--debug) DEBUG=true; VERBOSE=true; shift ;; |
| 27 | -n|--dry-run) DRY_RUN=true; VERBOSE=true; shift ;; |
| 28 | *) echo "Unknown option: $1"; exit 1 ;; |
| 29 | esac |
| 30 | done |
| 31 | |
| 32 | # Logging functions |
| 33 | log() { |
| 34 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" |
| 35 | } |
| 36 | |
| 37 | log_verbose() { |
| 38 | if $VERBOSE; then |
| 39 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] [VERBOSE] $1" |
| 40 | fi |
| 41 | } |
| 42 | |
| 43 | log_debug() { |
| 44 | if $DEBUG; then |
| 45 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] $1" |
| 46 | fi |
| 47 | } |
| 48 | |
| 49 | # Check if running as root |
| 50 | if [[ $EUID -ne 0 ]]; then |
| 51 | log "ERROR: This script must be run as root" |
| 52 | exit 1 |
| 53 | fi |
| 54 | |
| 55 | log "Starting Tor exit node blocklist update..." |
| 56 | log_verbose "Chain name: ${CHAIN_NAME}" |
| 57 | log_verbose "Source URL: ${TOR_EXIT_LIST_URL}" |
| 58 | log_verbose "Temp file: ${TEMP_FILE}" |
| 59 | $DRY_RUN && log "*** DRY RUN MODE - no iptables changes will be made ***" |
| 60 | |
| 61 | # Download the Tor exit node list |
| 62 | log "Fetching Tor exit node list..." |
| 63 | log_debug "Running: curl -s --max-time 60 -o ${TEMP_FILE} ${TOR_EXIT_LIST_URL}" |
| 64 | |
| 65 | if ! curl -s --max-time 60 -o "${TEMP_FILE}" "${TOR_EXIT_LIST_URL}"; then |
| 66 | log "ERROR: Failed to download Tor exit node list" |
| 67 | exit 1 |
| 68 | fi |
| 69 | |
| 70 | # Show file info |
| 71 | FILE_SIZE=$(stat -c%s "${TEMP_FILE}" 2>/dev/null || echo "unknown") |
| 72 | log_verbose "Downloaded file size: ${FILE_SIZE} bytes" |
| 73 | |
| 74 | # Debug: show first few lines |
| 75 | if $DEBUG; then |
| 76 | log_debug "First 5 lines of downloaded file:" |
| 77 | head -5 "${TEMP_FILE}" | while read -r line; do |
| 78 | log_debug " > ${line}" |
| 79 | done |
| 80 | fi |
| 81 | |
| 82 | # Verify we got valid data (should contain IP addresses) |
| 83 | VALID_IP_COUNT=$(grep -cE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' "${TEMP_FILE}" || echo "0") |
| 84 | log_verbose "Valid IP addresses in file: ${VALID_IP_COUNT}" |
| 85 | |
| 86 | if [[ "${VALID_IP_COUNT}" -eq 0 ]]; then |
| 87 | log "ERROR: Downloaded file doesn't contain valid IP addresses" |
| 88 | if $DEBUG; then |
| 89 | log_debug "File contents:" |
| 90 | cat "${TEMP_FILE}" |
| 91 | fi |
| 92 | rm -f "${TEMP_FILE}" |
| 93 | exit 1 |
| 94 | fi |
| 95 | |
| 96 | TOTAL_LINES=$(wc -l < "${TEMP_FILE}") |
| 97 | log "Downloaded file: ${TOTAL_LINES} lines, ${VALID_IP_COUNT} valid IPs" |
| 98 | |
| 99 | # Create the chain if it doesn't exist |
| 100 | if ! iptables -L "${CHAIN_NAME}" -n >/dev/null 2>&1; then |
| 101 | log "Creating new iptables chain: ${CHAIN_NAME}" |
| 102 | if ! $DRY_RUN; then |
| 103 | iptables -N "${CHAIN_NAME}" |
| 104 | fi |
| 105 | # Add jump rule to INPUT chain if not already present |
| 106 | if ! iptables -C INPUT -j "${CHAIN_NAME}" 2>/dev/null; then |
| 107 | log_verbose "Adding jump rule from INPUT to ${CHAIN_NAME}" |
| 108 | if ! $DRY_RUN; then |
| 109 | iptables -I INPUT -j "${CHAIN_NAME}" |
| 110 | fi |
| 111 | fi |
| 112 | else |
| 113 | log_verbose "Chain ${CHAIN_NAME} already exists" |
| 114 | EXISTING_RULES=$(iptables -L "${CHAIN_NAME}" -n | tail -n +3 | wc -l) |
| 115 | log_verbose "Existing rules in chain: ${EXISTING_RULES}" |
| 116 | fi |
| 117 | |
| 118 | # Flush existing rules in the chain |
| 119 | log "Flushing existing rules in ${CHAIN_NAME}..." |
| 120 | if ! $DRY_RUN; then |
| 121 | iptables -F "${CHAIN_NAME}" |
| 122 | fi |
| 123 | |
| 124 | # Add new rules |
| 125 | log "Adding blocking rules..." |
| 126 | ADDED=0 |
| 127 | SKIPPED=0 |
| 128 | ERRORS=0 |
| 129 | |
| 130 | while IFS= read -r ip; do |
| 131 | # Skip empty lines and comments |
| 132 | if [[ -z "$ip" || "$ip" =~ ^# ]]; then |
| 133 | log_debug "Skipping line: '${ip}'" |
| 134 | continue |
| 135 | fi |
| 136 | |
| 137 | # Trim whitespace (including carriage returns from Windows-style files) |
| 138 | ip=$(echo "$ip" | tr -d '\r' | xargs) |
| 139 | |
| 140 | # Validate IP format |
| 141 | if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then |
| 142 | if ! $DRY_RUN; then |
| 143 | if iptables -A "${CHAIN_NAME}" -s "$ip" -j DROP 2>/dev/null; then |
| 144 | ADDED=$((ADDED + 1)) |
| 145 | else |
| 146 | log_verbose "Failed to add rule for IP: ${ip}" |
| 147 | ERRORS=$((ERRORS + 1)) |
| 148 | fi |
| 149 | else |
| 150 | ADDED=$((ADDED + 1)) |
| 151 | fi |
| 152 | |
| 153 | # Progress indicator every 500 IPs |
| 154 | if $VERBOSE && (( ADDED % 500 == 0 )); then |
| 155 | log_verbose "Progress: ${ADDED} rules added..." |
| 156 | fi |
| 157 | else |
| 158 | log_debug "Invalid IP format: '${ip}'" |
| 159 | SKIPPED=$((SKIPPED + 1)) |
| 160 | fi |
| 161 | done < "${TEMP_FILE}" |
| 162 | |
| 163 | log "Results: Added ${ADDED} rules, skipped ${SKIPPED} invalid, ${ERRORS} errors" |
| 164 | |
| 165 | # Verify the rules were added |
| 166 | if ! $DRY_RUN; then |
| 167 | FINAL_COUNT=$(iptables -L "${CHAIN_NAME}" -n | tail -n +3 | wc -l) |
| 168 | log "Verification: ${FINAL_COUNT} rules now in ${CHAIN_NAME} chain" |
| 169 | |
| 170 | if [[ ${FINAL_COUNT} -ne ${ADDED} ]]; then |
| 171 | log "WARNING: Expected ${ADDED} rules but found ${FINAL_COUNT}" |
| 172 | fi |
| 173 | fi |
| 174 | |
| 175 | # Clean up |
| 176 | rm -f "${TEMP_FILE}" |
| 177 | log_verbose "Cleaned up temp file" |
| 178 | |
| 179 | # Optional: Save iptables rules (uncomment based on your distro) |
| 180 | # For Debian/Ubuntu: |
| 181 | # iptables-save > /etc/iptables/rules.v4 |
| 182 | |
| 183 | # For RHEL/CentOS: |
| 184 | # service iptables save |
| 185 | |
| 186 | log "Tor exit node blocking updated successfully" |