1155 字
6 分钟
Using ipset on Linux to Efficiently Block Country IPs (e.g. China)

Preface#

I got a free server from Google Cloud, but recently I kept finding that it was quietly racking up charges — one or two dollars a month. It’s not much, but it conflicts with the whole point of getting something for free.

Because although I get 200G of free traffic per month, China is not covered by the free traffic allowance, so every time traffic comes from a Chinese IP, money gets deducted. So I set out to block Chinese IPs.

At first I came across many ufw scripts online, like this one:

#!/bin/bash for ip in $(cat cn.zone); do sudo ufw deny from $ip done

They stuff a bunch of for loops into ufw. When a network packet arrives, the kernel has to check this long rule list one by one from top to bottom. The time complexity is O(n), and when there are many rules, performance drops significantly.

ipset, on the other hand, is specifically designed for handling large numbers of IP addresses. It uses hash tables to store IP addresses, giving query time complexity close to O(1). So using ipset instead of ufw can significantly improve performance.

Initially I used ipset-persistent and netfilter-persistent for persistence, but these two tools save all rules, which can easily cause ipset rules to accumulate too much. So I switched to a service-based approach to bypass persistence.

Solution#

systemd Service#

/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
[Service]
Type=oneshot
# Absolute path to the script
ExecStart=/usr/local/bin/update_china_blocklist.sh
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/run
ProtectHome=true
NoNewPrivileges=true
SupplementaryGroups=systemd-resolve

systemd Timer#

/etc/systemd/system/china-ip-blocker.timer
[Unit]
Description=Run china-ip-blocker service on boot and daily
[Timer]
OnBootSec=2min
OnUnitActiveSec=12h
Unit=china-ip-blocker.service
[Install]
WantedBy=timers.target

Main script#

#!/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"
}
# Centralized error-handling function
die() {
log "FATAL: $1" >&2
exit 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 head tail; 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"
exit 0

Usage#

Create the systemd Service file#

Create china-ip-blocker.service in the /etc/systemd/system/ directory.

Create the systemd Timer file#

This file defines when and how often the above service is triggered.

In the /etc/systemd/system/ directory, create a china-ip-blocker.timer file with the same name as the service file (but with a different suffix).

Deploy and enable#

  1. Place the script: Put the script (e.g. update_china_blocklist.sh) in a suitable location, such as /usr/local/bin/, and make sure it has executable permissions.

    Terminal window
    sudo chmod +x /usr/local/bin/update_china_blocklist.sh
  2. Reload the systemd configuration: Let systemd know that you have created new unit files.

    Terminal window
    sudo systemctl daemon-reload
  3. Enable and start the timer:

    Terminal window
    sudo systemctl enable china-ip-blocker.timer
    sudo systemctl start china-ip-blocker.timer
    • enable makes the timer start automatically at boot.
    • start immediately activates the timer so it begins counting down.
    • Note: you only need to enable and start the .timer file; it will automatically manage the .service file.

Management and debugging#

  • Check the timer status:

    Terminal window
    systemctl status china-ip-blocker.timer

    The output shows NEXT (the next run time).

  • View the service run logs:

    Terminal window
    journalctl -u china-ip-blocker.service

    This shows all output and error messages from the script, which is more convenient than traditional log file management.

  • Manually trigger the task once:

    Terminal window
    sudo systemctl start china-ip-blocker.service

Verify it works#

Terminal window
root@nagasaki-soyo:/home/tokisaki# sudo ipset list chinablock
Name: chinablock
Type: hash:net
Revision: 7
Header: family inet hashsize 2048 maxelem 65536 bucketsize 12 initval 0xc19a419f
Size in memory: 233664
References: 1
Number of entries: 8711
Members:
58.240.0.0/15
103.3.100.0/22
...
Terminal window
root@nagasaki-soyo:/home/tokisaki# sudo iptables -L INPUT -n -v
Chain INPUT (policy DROP 619 packets, 41086 bytes)
pkts bytes target prot opt in out source destination
5345 916K DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set chinablock src
492K 168M ts-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
355K 138M ufw-before-logging-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
355K 138M ufw-before-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
627 41710 ufw-after-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
619 41086 ufw-after-logging-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
619 41086 ufw-reject-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
619 41086 ufw-track-input 0 -- * * 0.0.0.0/0 0.0.0.0/0
Using ipset on Linux to Efficiently Block Country IPs (e.g. China)
https://tski.uk/blog/en/use-ipset-ban-chinaip/
作者
Tokisaki Galaxy
发布于
2025-08-17
许可协议
CC BY