# /etc/systemd/system/china-ip-blocker.service [Unit] Description=Update and apply China IP blocklist using ipset After=network-online.target Wants=network-online.target
#!/bin/bash # ================================================================= # High-Performance, Low-Resource IP Blocklist Update Script # # Designed for execution via systemd timer or cron. # This script is non-persistent and relies on being run on boot # and periodically to maintain firewall rules. # =================================================================
set -o errexit # Exit immediately if a command exits with a non-zero status. set -o nounset # Exit immediately if it tries to use an undeclared variable. set -o pipefail # The return value of a pipeline is the status of the last command to exit with a non-zero status.
# --- Configuration --- readonly IPSET_NAME="chinablock" readonly IP_LIST_URL="https://www.ipdeny.com/ipblocks/data/countries/cn.zone" readonly TMP_FILE="/dev/shm/${IPSET_NAME}.zone" readonly LOCK_FILE="/var/run/${IPSET_NAME}.lock" readonly MIN_IP_COUNT=100 # Minimum expected number of IP ranges
# Efficient logging function using bash printf built-in (>= bash 4.2) log() { # %-1s prints a single space. Using it to get printf to evaluate the format string. printf'%(%Y-%m-%d %H:%M:%S)T - %s\n' -1 "$1" }
main() { # --- Prerequisite Checks --- if [[ ${EUID:-$(id -u)} -ne 0 ]]; then die "This script must be run as root." fi
for cmd in ipset wget iptables flock awk headtail; do if ! command -v "$cmd" &>/dev/null; then die "Required command '$cmd' is not found." fi done
# Ensure temporary file is cleaned up on any exit trap'rm -f "$TMP_FILE"' EXIT
log"Starting IP blocklist update for set '$IPSET_NAME'..."
# --- Download IP List --- log"Downloading IP list from $IP_LIST_URL..." if ! wget -q --timeout=60 --tries=3 -O "$TMP_FILE""$IP_LIST_URL"; then die "Download failed from $IP_LIST_URL." fi
# --- Create and Load Temporary IPSet --- local temp_ipset_name="${IPSET_NAME}_temp" ipset create "$temp_ipset_name"hash:net -exist ipset flush "$temp_ipset_name"
log"Validating list and preparing for bulk load..." # Use a single awk pass to validate and generate restore data. # It exits with an error code if the line count is too low. # The last line of its output is the total count. local awk_output awk_output=$(awk -v set_name="$temp_ipset_name" \ '{ print "add " set_name " " $1 } END { if (NR < '$MIN_IP_COUNT') exit 1; print NR }'"$TMP_FILE") \ || die "IP list validation failed (expected >$MIN_IP_COUNT lines, found $(wc -l < "$TMP_FILE" | awk '{print $1}'))."
# Pipe all but the last line (the count) to ipset restore for high-speed loading. echo"$awk_output" | head -n -1 | ipset restore || die "ipset restore command failed."
local final_count final_count=$(echo"$awk_output" | tail -n 1)
log"Loaded $final_count IP blocks into temporary set '$temp_ipset_name'."
# --- Atomically Activate the New IPSet --- ipset create "$IPSET_NAME"hash:net -exist ipset swap "$temp_ipset_name""$IPSET_NAME" ipset destroy "$temp_ipset_name" log"Successfully updated and activated ipset '$IPSET_NAME'."
# --- Ensure IPTables Rule Exists --- # This check is crucial because the rule is lost on reboot. if ! iptables -C INPUT -m set --match-set "$IPSET_NAME" src -j DROP &>/dev/null; then log"iptables rule not found. Inserting it at the top of the INPUT chain..." # -I INPUT 1 ensures it's one of the first rules evaluated, which is critical for performance. iptables -I INPUT 1 -m set --match-set "$IPSET_NAME" src -j DROP log"iptables rule for '$IPSET_NAME' added." else log"iptables rule for '$IPSET_NAME' already exists." fi
log"Update completed successfully." }
# --- Execution Wrapper --- # Use flock for robust concurrency control. The lock is held on file descriptor 200. ( flock -n 200 || die "Script is already running. Another instance holds the lock." main ) 200>"$LOCK_FILE"