在 Linux 上使用 ipset 高效屏蔽特定国家 IP(中国等)

前言

从Google Cloud白嫖了个服务器,但是最近时不时总是发现偷偷跑钱,每个月一两块钱,虽然少但是与白嫖相悖。

因为虽然每个月有200G免费流量,但是中国不属于免费流量范围,所以每次访问中国IP都会扣钱。于是就想办法封中国IP。

一开始看到网上很多ufw的脚步,比如这种

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

用一堆for循环塞到ufw里面,当一个网络数据包到达时,内核需要从上到下逐一检查这个长长的规则列表。时间复杂度是O(n),当规则数量很多时,性能会显著下降。

而ipset是专门为处理大量IP地址而设计的,它使用哈希表来存储IP地址,查询时间复杂度接近O(1)。所以使用ipset代替ufw可以显著提高性能。

一开始是用ipset-persistent,netfilter-persistent进行持久化,但是这两个工具会保存所有规则,容易导致ipset规则积累过多。所以采用服务的方式绕开持久化。

解决方案

systemed Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# /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
# 脚本绝对路径
ExecStart=/usr/local/bin/update_china_blocklist.sh
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/run
ProtectHome=true
NoNewPrivileges=true
SupplementaryGroups=systemd-resolve

systemed Timer

1
2
3
4
5
6
7
8
9
# /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

主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/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

使用方式

创建 systemd Service 文件

/etc/systemd/system/ 目录下创建 china-ip-blocker.service

创建 systemd Timer 文件

这个文件定义了触发上述 service 的时间和频率。

/etc/systemd/system/ 目录下创建一个与 service 文件同名(但后缀不同)的 china-ip-blocker.timer 文件。

部署和启用

  1. 放置脚本:将脚本(例如 update_china_blocklist.sh)放置到一个合适的位置,比如 /usr/local/bin/,并确保它有可执行权限。

    1
    sudo chmod +x /usr/local/bin/update_china_blocklist.sh
  2. 重载 systemd 配置:让 systemd 知道你创建了新的单元文件。

    1
    sudo systemctl daemon-reload
  3. 启用并启动定时器

    1
    2
    sudo systemctl enable china-ip-blocker.timer
    sudo systemctl start china-ip-blocker.timer
    • enable 会让定时器开机自启。
    • start 会立即激活定时器,让它开始计时。
    • 注意:你只需要 enablestart .timer 文件,它会自动管理 .service 文件。

管理和调试

  • 查看定时器状态

    1
    systemctl status china-ip-blocker.timer

    输出会显示 NEXT(下一次运行时间)。

  • 查看服务运行日志

    1
    journalctl -u china-ip-blocker.service

    这会显示脚本的所有输出和错误信息,比传统的日志文件管理更方便。

  • 手动触发一次任务

    1
    sudo systemctl start china-ip-blocker.service

检查生效

1
2
3
4
5
6
7
8
9
10
11
12
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
...
1
2
3
4
5
6
7
8
9
10
11
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

在 Linux 上使用 ipset 高效屏蔽特定国家 IP(中国等)
https://tokisaki.top/blog/use-ipset-ban-chinaip/
作者
Tokisaki Galaxy
发布于
2025年8月17日
许可协议