blob: b561f532755546e57edc8d5ed9338f41ae506d9d [file] [log] [blame]
Marc Kupietzc6c873e2026-01-31 11:00:58 +01001#!/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
13CHAIN_NAME="TOR_BLOCKLIST"
14TOR_EXIT_LIST_URL="https://check.torproject.org/torbulkexitlist"
15TEMP_FILE="/tmp/tor_exit_nodes.txt"
16
17# Options
18VERBOSE=false
19DEBUG=false
20DRY_RUN=false
21
22# Parse command line arguments
23while [[ $# -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
30done
31
32# Logging functions
33log() {
34 echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
35}
36
37log_verbose() {
38 if $VERBOSE; then
39 echo "[$(date '+%Y-%m-%d %H:%M:%S')] [VERBOSE] $1"
40 fi
41}
42
43log_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
50if [[ $EUID -ne 0 ]]; then
51 log "ERROR: This script must be run as root"
52 exit 1
53fi
54
55log "Starting Tor exit node blocklist update..."
56log_verbose "Chain name: ${CHAIN_NAME}"
57log_verbose "Source URL: ${TOR_EXIT_LIST_URL}"
58log_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
62log "Fetching Tor exit node list..."
63log_debug "Running: curl -s --max-time 60 -o ${TEMP_FILE} ${TOR_EXIT_LIST_URL}"
64
65if ! 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
68fi
69
70# Show file info
71FILE_SIZE=$(stat -c%s "${TEMP_FILE}" 2>/dev/null || echo "unknown")
72log_verbose "Downloaded file size: ${FILE_SIZE} bytes"
73
74# Debug: show first few lines
75if $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
80fi
81
82# Verify we got valid data (should contain IP addresses)
83VALID_IP_COUNT=$(grep -cE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' "${TEMP_FILE}" || echo "0")
84log_verbose "Valid IP addresses in file: ${VALID_IP_COUNT}"
85
86if [[ "${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
94fi
95
96TOTAL_LINES=$(wc -l < "${TEMP_FILE}")
97log "Downloaded file: ${TOTAL_LINES} lines, ${VALID_IP_COUNT} valid IPs"
98
99# Create the chain if it doesn't exist
100if ! 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
112else
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}"
116fi
117
118# Flush existing rules in the chain
119log "Flushing existing rules in ${CHAIN_NAME}..."
120if ! $DRY_RUN; then
121 iptables -F "${CHAIN_NAME}"
122fi
123
124# Add new rules
125log "Adding blocking rules..."
126ADDED=0
127SKIPPED=0
128ERRORS=0
129
130while 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
161done < "${TEMP_FILE}"
162
163log "Results: Added ${ADDED} rules, skipped ${SKIPPED} invalid, ${ERRORS} errors"
164
165# Verify the rules were added
166if ! $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
173fi
174
175# Clean up
176rm -f "${TEMP_FILE}"
177log_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
186log "Tor exit node blocking updated successfully"