update 2026-10-10 04:29:04

This commit is contained in:
action committed 2026-10-10 04:29:04 +08:00
1 parent cc69bd0f95
commit 33877ad635
188 files changed
+938 -40394

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wechatpush
PKG_VERSION:=3.6.12
PKG_VERSION:=3.6.13
PKG_RELEASE:=
PKG_MAINTAINER:=tty228 <tty228@yeah.net>
+2
View File
@@ -16,6 +16,8 @@
- [x] 路由 Web、SSH 登录提示,自动拉黑、端口敲门
- [x] 无人值守任务
自动封禁启用后,黑名单默认拦截访问路由器的流量;可另外开启“阻止黑名单 IP 的转发流量”,拦截经过路由器转发的所有流量(包括访问端口映射的下级设备、局域网客户端访问互联网)。开启转发封禁且已安装 conntrack 时,封禁还会尝试清理该 IP 的连接跟踪记录,以断开已有的流量卸载连接;未安装时跳过该步骤,新连接仍按黑名单规则拦截。关闭该选项或关闭自动封禁后,转发拦截规则会在下一次检测循环中被移除;与访问路由器的拦截规则一样,停止插件服务不会立即移除已生效的规则,它们会在防火墙重载或重启后消失,并在插件运行时于下一次检测循环中恢复。fw4 定时封禁按最近记录的剩余时长恢复。黑名单时间设置为 `0` 时永久封禁,手动清除黑名单后解除。
通知服务支持列表:
| 推送应用 | 方式 | 接口说明 |
| :-------- | :----- | :----- |
+2
View File
@@ -17,6 +17,8 @@ This OpenWrt plugin delivers real-time router notifications to your mobile devic
- [x] Security alerts (failed web/SSH logins with auto blacklist)
- [x] Port knocking & automated tasks
Auto-banning blocks blacklisted IPs from accessing the router. Optionally block all forwarded traffic from blacklisted IPs, including access to port-forwarded downstream devices and Internet access by LAN clients. With forwarded-traffic blocking enabled and conntrack installed, banning also attempts to clear that IP's existing connection-tracking entries to interrupt offloaded flows; without conntrack, this step is skipped while new connections remain blocked by the blacklist rules. Turning the option off, or turning auto-ban off, removes the forwarded-traffic drop rules on the next detection cycle; like the router-access drop rules, they are not removed by stopping the service, disappear after a firewall reload or reboot, and are restored on the plugin's next detection cycle while it runs. fw4 timed bans are restored with their last recorded remaining duration. Set the blacklist duration to `0` for permanent bans; remove the IP from the blacklist to lift the ban.
Supported services:
| Push application | Method | description |
| :-------- | :----- | :----- |
@@ -574,7 +574,12 @@ return view.extend({
o.rmempty = false;
o.datatype = 'and(uinteger,min(0))';
o.depends('login_web_black', '1');
o.description = _('\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab.');
o.description = _('"0" means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the "Do Not Disturb" tab.');
o = s.taboption('ipset', form.Flag, 'block_blacklisted_forwarding', _('Block forwarded traffic from blacklisted IPs'));
o.default = '0';
o.depends('login_web_black', '1');
o.description = _('Includes port-forwarded access to downstream devices and LAN clients accessing the Internet.');
o = s.taboption('ipset', form.Flag, 'port_knocking_enable', _('Port knocking'));
o.default = '0';
+8 -2
View File
@@ -582,8 +582,14 @@ msgid "Blacklisting time (s)"
msgstr "拉黑时间(秒)"
#: applications/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/config.js:489
msgid "\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab."
msgstr "ipset 中, \"0\" 为永久拉黑,慎用。如不幸误操作,请更改设备 IP 进入 LUCI 界面清空规则。<br/>注:封禁白名单位于 “免打扰” 选项卡。"
msgid "\"0\" means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab."
msgstr "\"0\" 为永久拉黑,慎用。如不幸误操作,请更改设备 IP 进入 LUCI 界面清空规则。<br/>注:封禁白名单位于“免打扰”选项卡。"
msgid "Block forwarded traffic from blacklisted IPs"
msgstr "阻止黑名单 IP 的转发流量"
msgid "Includes port-forwarded access to downstream devices and LAN clients accessing the Internet."
msgstr "包括经端口映射访问下级设备,以及局域网客户端访问互联网的流量。"
#: applications/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/config.js:491
msgid "Port knocking"
@@ -48,7 +48,7 @@ read_config() {
"get_ipv4_mode" "ipv4_interface" "get_ipv6_mode" "ipv6_interface" \
"device_notification" "cpu_notification" "cpu_load_threshold" "temperature_threshold" \
"client_usage" "client_usage_max" "client_usage_disturb" "client_usage_whitelist" \
"login_notification" "login_max_num" "login_web_black" "login_ip_black_timeout" "login_ip_white_list" "port_knocking_enable" "login_ip_white_timeout" "login_port_white" "login_port_forward_list" \
"login_notification" "login_max_num" "login_web_black" "login_ip_black_timeout" "block_blacklisted_forwarding" "login_ip_white_list" "port_knocking_enable" "login_ip_white_timeout" "login_port_white" "login_port_forward_list" \
"crontab_regular_time" "crontab_interval_time" \
"do_not_disturb_mode" "do_not_disturb_starttime" "do_not_disturb_endtime" "up_down_push_whitelist" "up_down_push_blacklist" "up_down_push_interface" "mac_online_list" "mac_offline_list" "cpu_threshold_duration" "cpu_notification_delay" "login_disturb" "login_notification_delay" "login_log_enable" \
"up_timeout" "down_timeout" "timeout_retry_count" "always_check_ip_list" "only_timeout_push" "passive_mode" "thread_num" "defaultSortColumn" "soc_code" "server_host" "server_port" \
@@ -72,12 +72,14 @@ read_config() {
for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do
eval $(echo ${str_version:0:2}"_version")=$(apk list | grep -w ^${str_version} | grep 'installed' | awk '{print $1}' | awk -F ${str_version}"-" '{print $2}') 2>/dev/null
done
(apk list | grep -w -q "^firewall4" | grep 'installed') && nftables_version="true"
else
for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do
eval $(echo ${str_version:0:2}"_version")=$(opkg list-installed | grep -w ^${str_version} | awk '{print $3}') 2>/dev/null
done
(opkg list-installed | grep -w -q "^firewall4") && nftables_version="true"
fi
# firewall4 使用 nftables,避免回退到 iptables/ipset。
if command -v fw4 >/dev/null 2>&1; then
nftables_version="true"
fi
devices_json="${dir}/devices.json"
tmp_devices_json="${dir}/devices.json.tmp"
@@ -226,8 +228,8 @@ init() {
LockFile unlock
# 防火墙初始化
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv4"
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv6"
init_ip_black "ipv4"
init_ip_black "ipv6"
[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv4"
[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv6"
tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u)
@@ -407,7 +409,75 @@ main() {
#[ ! -f "${dir}/send_enable.lock" ] && login_send
# 因修改为实时推送,白名单设备并不会再更新时间,暂时性修复
# 修改为实时推送后,防火墙列表读取和设置需单独列出,移除黑名单无法实时,待改善
# 防火墙重载会清除运行时规则,需在下一次循环恢复。
# nft 模式下先建立白名单 accept 规则,黑名单 drop 规则才能定位到其后;
# iptables 模式使用 -I 插入链首,因此必须先建立黑名单,再建立白名单。
if [ "$port_knocking_enable" = "1" ] && [ -n "$nftables_version" ]; then
init_ip_white "ipv4"
init_ip_white "ipv6"
fi
if [ "$login_web_black" = "1" ]; then
init_ip_black "ipv4"
init_ip_black "ipv6"
fi
if [ "$port_knocking_enable" = "1" ] && [ -z "$nftables_version" ]; then
init_ip_white "ipv4"
init_ip_white "ipv6"
fi
set_ip_black
if [ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && command -v conntrack >/dev/null 2>&1; then
for pending_file in "${dir}"/pending_black_flow_*; do
[ -f "$pending_file" ] || continue
IFS= read -r pending_ip <"$pending_file"
pending_set="wechatpush_blacklist"
pending_family="ipv4"
if [[ "$pending_ip" == *:* ]]; then
pending_set="wechatpush_blacklistv6"
pending_family="ipv6"
fi
if [ "$pending_family" = "ipv4" ]; then
[[ "$pending_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || pending_ip=""
else
valid_ipv6_blacklist_ip "$pending_ip" || pending_ip=""
fi
if [ -z "$pending_ip" ]; then
rm -f "$pending_file"
continue
fi
if [ -n "$nftables_version" ]; then
# 集合列举失败需保留重试文件,只有成功列举后才能判定元素确实不存在;
# nft 会规范化 IPv4/IPv6 写法,按规范化结果比较。
pending_elements=$(nft -j list set inet fw4 "$pending_set" 2>/dev/null) || continue
pending_elements=$(jq -r '.nftables[] | .set?.elem[]? | if type == "object" then .elem.val else . end | strings' <<<"$pending_elements") || continue
pending_found=""
if [[ "$pending_ip" == *:* ]]; then
pending_wanted=$(normalize_ipv6_blacklist_ip "$pending_ip")
while IFS= read -r pending_element; do
[ -n "$pending_element" ] || continue
[ "$(normalize_ipv6_blacklist_ip "$pending_element")" = "$pending_wanted" ] && pending_found="1" && break
done <<<"$pending_elements"
else
pending_wanted=$(normalize_ipv4_blacklist_ip "$pending_ip") || pending_wanted=""
while IFS= read -r pending_element; do
[ -n "$pending_element" ] || continue
pending_normalized=$(normalize_ipv4_blacklist_ip "$pending_element") || continue
[ "$pending_normalized" = "$pending_wanted" ] && pending_found="1" && break
done <<<"$pending_elements"
fi
[ -n "$pending_found" ] || {
rm -f "$pending_file"
continue
}
else
ipset list "$pending_set" >/dev/null 2>&1 || continue
ipset test "$pending_set" "$pending_ip" >/dev/null 2>&1 || {
rm -f "$pending_file"
continue
}
fi
clear_ip_black_flows "$pending_ip"
done
fi
tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u)
while IFS= read -r tmp_ip; do
@@ -2244,6 +2314,7 @@ add_ip_white() {
# 初始化白名单
init_ip_white() {
[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] || return
local nat_table_cmd=""
# 设置 IP 版本变量
if [ $1 == "ipv4" ]; then
ipset_name="wechatpush_whitelist"
@@ -2256,8 +2327,8 @@ init_ip_white() {
if [ -n "$nftables_version" ]; then
! nft list set inet fw4 $ipset_name >/dev/null 2>&1 && nft add set inet fw4 $ipset_name { type ${1}_addr\; flags timeout\; timeout ${login_ip_white_timeout}s\; }
nft -- add chain inet fw4 wechatpush_dstnat { type nat hook prerouting priority -100 \; }
nft add chain inet fw4 wechatpush_srcnat { type nat hook postrouting priority 100 \; }
nft list chain inet fw4 wechatpush_dstnat >/dev/null 2>&1 || nft -- add chain inet fw4 wechatpush_dstnat { type nat hook prerouting priority -100 \; }
nft list chain inet fw4 wechatpush_srcnat >/dev/null 2>&1 || nft add chain inet fw4 wechatpush_srcnat { type nat hook postrouting priority 100 \; }
else
! ipset list $ipset_name >/dev/null 2>&1 && ipset create $ipset_name hash:ip timeout $login_ip_white_timeout $nat_table_cmd >/dev/null 2>&1
fi
@@ -2301,58 +2372,313 @@ init_ip_white() {
unset port_forward
}
# 写入连接跟踪清理重试文件,供后续检测循环重试。
# 登录监视子进程与主循环并发运行,必须原子落盘,避免主循环读到空文件后误删重试记录。
write_pending_black_flow() {
local key="${1//[:.]/_}" pending_file="${dir}/pending_black_flow_${1//[:.]/_}" pending_tmp="${dir}/.pending_black_flow_${key}.${BASHPID:-$$}"
printf '%s\n' "$1" >"$pending_tmp" && mv -f "$pending_tmp" "$pending_file"
}
# 新封禁无法匹配已走流表的连接,需要删除该来源的连接跟踪记录。
clear_ip_black_flows() {
command -v conntrack >/dev/null 2>&1 || return 0
local pending_file="${dir}/pending_black_flow_${1//[:.]/_}" original reply original_status reply_status
local -a family=()
[[ "$1" == *:* ]] && family=(-f ipv6)
conntrack ${family[@]} -D -s "$1" >/dev/null 2>&1
conntrack ${family[@]} -D --reply-src "$1" >/dev/null 2>&1
original=$(conntrack -L "${family[@]}" -s "$1" 2>/dev/null)
original_status=$?
reply=$(conntrack -L "${family[@]}" --reply-src "$1" 2>/dev/null)
reply_status=$?
if printf '%s\n%s\n' "$original" "$reply" | grep -Eq '\[(HW_)?OFFLOAD\]'; then
write_pending_black_flow "$1"
log_change "[WARN] Failed to clear offloaded connections for $1"
elif [ "$original_status" -eq 0 ] && [ "$reply_status" -eq 0 ]; then
rm -f "$pending_file"
else
write_pending_black_flow "$1"
fi
return 0
}
clear_ip_black_set_flows() {
command -v conntrack >/dev/null 2>&1 || return 0
local elements ip
if [ -n "$nftables_version" ]; then
elements=$(nft -j list set inet fw4 "$ipset_name") || return 1
elements=$(jq -r '.nftables[] | .set?.elem[]? | if type == "object" then .elem.val else . end | strings' <<<"$elements") || return 1
else
elements=$(ipset save "$ipset_name") || return 1
elements=$(awk -v set_name="$ipset_name" '$1 == "add" && $2 == set_name {print $3}' <<<"$elements")
fi
while IFS= read -r ip; do
[ -n "$ip" ] || continue
clear_ip_black_flows "$ip"
done <<<"$elements"
return 0
}
delete_nft_ip_black_forward_rule() {
local drop_rule_handle rules
while :; do
# 列举失败与“没有规则”需区分:返回非零由下一次检测循环重试清理。
rules=$(nft --handle list chain inet fw4 forward 2>/dev/null) || return 1
drop_rule_handle=$(awk -v set_name="@$ipset_name" 'index($0, set_name " ") && /comment "!wechatpush Forward Drop rule"/ {print $NF; exit}' <<<"$rules")
[ -n "$drop_rule_handle" ] || break
nft delete rule inet fw4 forward handle "$drop_rule_handle" >/dev/null 2>&1 || return 1
done
}
# 初始化黑名单规则
init_ip_black() {
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] || return
local nat_table_cmd=""
# 设置 IP 版本变量
if [ $1 == "ipv4" ]; then
if [ "$1" = "ipv4" ]; then
ipset_name="wechatpush_blacklist"
ip_version="ip"
elif [ $1 == "ipv6" ]; then
elif [ "$1" = "ipv6" ]; then
ipset_name="wechatpush_blacklistv6"
ip_version="ip6"
nat_table_cmd="family inet6"
fi
local pending_clear_marker="${dir}/pending_black_set_clear_${ipset_name}"
if [ "$login_web_black" != "1" ]; then
rm -f "$pending_clear_marker" "${dir}"/pending_black_flow_*
if [ -n "$nftables_version" ]; then
delete_nft_ip_black_forward_rule
else
${ip_version}tables -D FORWARD -m set --match-set "$ipset_name" src -j DROP >/dev/null 2>&1
fi
return
fi
[ -n "$nftables_version" ] && {
! nft list set inet fw4 ${ipset_name} >/dev/null 2>&1 && nft add set inet fw4 ${ipset_name} { type ${1}_addr\; flags timeout\; timeout ${login_ip_black_timeout}s\; }
! nft list ruleset | grep "$ip_version saddr @${ipset_name} counter .* comment \"\!wechatpush Drop rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 input $ip_version saddr @${ipset_name} counter drop comment \"\!wechatpush Drop rule\" >/dev/null 2>&1
} || {
ipset list $ipset_name >/dev/null 2>&1 || ipset create ${ipset_name} hash:ip timeout ${login_ip_black_timeout} ${nat_table_cmd} >/dev/null 2>&1
${ip_version}tables -C INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1
}
if [ -n "$nftables_version" ]; then
local set_exists="" set_has_timeout="" want_timeout="" set_options="type ${1}_addr;" white_rule_handle input_rule set_meta
set_meta=$(nft -j list set inet fw4 "$ipset_name" 2>/dev/null)
if [ -n "$set_meta" ]; then
set_exists="1"
# 纯文本输出只显示默认 timeout,flags 需要通过 JSON 读取。
[ -n "$(jq -r '[.nftables[] | select(.set) | .set.flags[]?] | index("timeout") // empty' <<<"$set_meta")" ] && set_has_timeout="1"
fi
[ "$login_ip_black_timeout" -gt 0 ] && want_timeout="1" && set_options="${set_options} flags timeout; timeout ${login_ip_black_timeout}s;"
white_rule_handle=$(nft --handle list chain inet fw4 input 2>/dev/null | awk -v rule="$ip_version saddr @${ipset_name/blacklist/whitelist} counter" 'index($0, rule) && /accept comment "!wechatpush Accept rule"/ {print $NF; exit}')
input_rule="insert rule inet fw4 input $ip_version saddr @$ipset_name counter drop comment \"!wechatpush Drop rule\""
[ -n "$white_rule_handle" ] && input_rule="add rule inet fw4 input position $white_rule_handle $ip_version saddr @$ipset_name counter drop comment \"!wechatpush Drop rule\""
# nft 原子事务:切换集合失败时保留原有封禁规则。
if [ -n "$set_exists" ] && [ "$set_has_timeout" != "$want_timeout" ]; then
local rule_deletions="" element_commands="" forward_rule="" chain handle ip elements
elements=$(nft -j list set inet fw4 "$ipset_name") || return 1
elements=$(jq -r '.nftables[] | .set?.elem[]? | if type == "object" then .elem.val else . end | strings' <<<"$elements") || return 1
while IFS= read -r ip; do
[ -z "$ip" ] && continue
if [[ "$ip" =~ ^[0-9a-fA-F:.]+$ ]]; then
element_commands="${element_commands}add element inet fw4 $ipset_name { $ip"
[ -n "$want_timeout" ] && element_commands="${element_commands} timeout ${login_ip_black_timeout}s"
element_commands="${element_commands} }"$'\n'
else
log_change "[ERROR] Invalid blacklist element in $ipset_name: $ip"
return 1
fi
done <<<"$elements"
for chain in input forward; do
for handle in $(nft --handle list chain inet fw4 "$chain" 2>/dev/null | awk -v set_name="@$ipset_name" 'index($0, set_name " ") && /comment "!wechatpush/ && /Drop rule/ {print $NF}'); do
rule_deletions="${rule_deletions}delete rule inet fw4 $chain handle $handle"$'\n'
done
done
[ "$block_blacklisted_forwarding" = "1" ] && forward_rule="insert rule inet fw4 forward $ip_version saddr @$ipset_name counter drop comment \"!wechatpush Forward Drop rule\""
if ! nft -f - >/dev/null 2>&1 <<EOF
${rule_deletions}delete set inet fw4 $ipset_name
add set inet fw4 $ipset_name { $set_options }
${element_commands}${input_rule}
$forward_rule
EOF
then
log_change "[ERROR] Failed to switch blacklist set $ipset_name; existing firewall rules retained"
return 1
fi
if [ -n "$forward_rule" ]; then
while IFS= read -r ip; do
[ -n "$ip" ] && clear_ip_black_flows "$ip"
done <<<"$elements"
fi
# 集合元素已逐个清理连接,集合级清理义务同步完成。
rm -f "$pending_clear_marker"
return 0
fi
if [ -z "$set_exists" ]; then
if [ -n "$want_timeout" ]; then
nft add set inet fw4 "$ipset_name" { type ${1}_addr\; flags timeout\; timeout ${login_ip_black_timeout}s\; } || return 1
else
nft add set inet fw4 "$ipset_name" { type ${1}_addr\; } || return 1
fi
fi
if ! nft list chain inet fw4 input | awk -v rule="$ip_version saddr @$ipset_name counter" 'index($0, rule) && / drop comment "!wechatpush Drop rule"/ {found=1} END {exit !found}'; then
if [ -n "$white_rule_handle" ]; then
nft add rule inet fw4 input position "$white_rule_handle" "$ip_version" saddr "@$ipset_name" counter drop comment '"!wechatpush Drop rule"' >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist input rule for $ipset_name"
return 1
}
else
nft insert rule inet fw4 input "$ip_version" saddr "@$ipset_name" counter drop comment '"!wechatpush Drop rule"' >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist input rule for $ipset_name"
return 1
}
fi
fi
if [ "$block_blacklisted_forwarding" = "1" ]; then
if ! nft list chain inet fw4 forward | awk -v rule="$ip_version saddr @$ipset_name counter" 'index($0, rule) && / drop comment "!wechatpush Forward Drop rule"/ {found=1} END {exit !found}'; then
nft insert rule inet fw4 forward "$ip_version" saddr "@$ipset_name" counter drop comment '"!wechatpush Forward Drop rule"' >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist forward rule for $ipset_name"
return 1
}
touch "$pending_clear_marker"
fi
else
delete_nft_ip_black_forward_rule
fi
else
ipset list $ipset_name >/dev/null 2>&1 || ipset create $ipset_name hash:ip timeout $login_ip_black_timeout $nat_table_cmd >/dev/null 2>&1
${ip_version}tables -C INPUT -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1
if [ "$block_blacklisted_forwarding" = "1" ]; then
if ! ${ip_version}tables -C FORWARD -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1; then
${ip_version}tables -I FORWARD -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist forward rule for $ipset_name"
return 1
}
touch "$pending_clear_marker"
fi
else
${ip_version}tables -D FORWARD -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1
fi
fi
if [ "$block_blacklisted_forwarding" = "1" ] && [ -f "$pending_clear_marker" ]; then
clear_ip_black_set_flows || return 1
rm -f "$pending_clear_marker"
elif [ "$block_blacklisted_forwarding" != "1" ]; then
rm -f "$pending_clear_marker" "${dir}"/pending_black_flow_*
fi
}
# nft 输出可能以 :: 开头或结尾;只接受单个压缩段及合法的十六进制分组。
valid_ipv6_blacklist_ip() {
local ip="$1" left right group count=0
if [[ "$ip" == *::* ]]; then
left=${ip%%::*}
right=${ip#*::}
[[ "$right" != *::* ]] || return 1
[[ -z "$left" || "$left" =~ ^([[:xdigit:]]{1,4}:)*[[:xdigit:]]{1,4}$ ]] || return 1
[[ -z "$right" || "$right" =~ ^([[:xdigit:]]{1,4}:)*[[:xdigit:]]{1,4}$ ]] || return 1
for group in ${ip//:/ }; do ((count+=1)); done
((count < 8))
else
[[ "$ip" =~ ^([[:xdigit:]]{1,4}:){7}[[:xdigit:]]{1,4}$ ]]
fi
}
# 按 nft 对点分 IPv4 文本的解析规则转为集合输出格式。
# 带前导零的分组按八进制解析;不带前导零的分组按十进制解析。
normalize_ipv4_blacklist_ip() {
local ip="$1" octet value normalized=""
local -a octets=()
IFS=. read -r -a octets <<<"$ip"
[ "${#octets[@]}" -eq 4 ] || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1
if [[ "$octet" == 0* ]]; then
[[ "$octet" =~ ^0[0-7]{0,2}$ ]] || return 1
value=$((8#$octet))
else
value=$((10#$octet))
fi
((value <= 255)) || return 1
normalized+="$value."
done
printf '%s\n' "${normalized%.}"
}
normalize_ipv6_blacklist_ip() {
local ip="$1" left right group count=0 missing normalized=""
if [[ "$ip" == *::* ]]; then
left=${ip%%::*}
right=${ip#*::}
else
left="$ip"
right=""
fi
for group in ${left//:/ }; do ((count+=1)); done
for group in ${right//:/ }; do ((count+=1)); done
missing=$((8 - count))
for group in ${left//:/ }; do printf -v group '%04x' "0x$group"; normalized+="${group}:"; done
for ((count=0; count<missing; count++)); do normalized+="0000:"; done
for group in ${right//:/ }; do printf -v group '%04x' "0x$group"; normalized+="${group}:"; done
printf '%s\n' "${normalized%:}"
}
# 添加黑名单
add_ip_black() {
local login_ip=$1
local login_ip=$1 element_timeout="${login_ip_black_timeout}s"
[ -z "$login_ip" ] && return 0
echo "$login_ip_white_list" | grep -w -q "$login_ip" && return 1
# 检查 IP 版本
unset ipset_name
(echo "$login_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist"
(echo "$login_ip" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6"
if [ -n "$nftables_version" ] && [[ "$login_ip" == *:* ]]; then
valid_ipv6_blacklist_ip "$login_ip" && ipset_name="wechatpush_blacklistv6" || unset ipset_name
fi
[ -z "$ipset_name" ] && sed -i "/^$login_ip /d" "$ip_blacklist_path" && log_change "[WARN] $(translate "Failed to add to blacklist, invalid IP format: %s (removed from list)" "$login_ip")" && return 1
! cat "$ip_blacklist_path" | grep -q -w -i $login_ip && echo "$login_ip timeout $login_ip_black_timeout" >>"$ip_blacklist_path"
[ -n "$nftables_version" ] && {
nft list set inet fw4 ${ipset_name} | grep -qw "${login_ip}" && return 1 # IP 已存在
nft add element inet fw4 $ipset_name { $login_ip expires ${login_ip_black_timeout}s } >/dev/null 2>&1
} || {
ipset -exist add $ipset_name $login_ip timeout ${login_ip_black_timeout} >/dev/null 2>&1
}
if [ -n "$nftables_version" ]; then
nft get element inet fw4 "$ipset_name" { "$login_ip" } >/dev/null 2>&1 && return 1 # 按地址值判断,兼容 IPv6 的非规范写法
# 先记录清理义务,若在加入集合与清理连接之间中断,下次循环仍会重试。
[ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && write_pending_black_flow "$login_ip"
[[ "$2" =~ ^([0-9]+(ms|[smhdw]))+$ ]] && element_timeout="$2"
[ -z "$element_timeout" ] && element_timeout="${login_ip_black_timeout}s"
if [ "$login_ip_black_timeout" -eq 0 ]; then
nft add element inet fw4 "$ipset_name" { "$login_ip" } >/dev/null 2>&1 || return 1
else
nft add element inet fw4 "$ipset_name" { "$login_ip" timeout "$element_timeout" } >/dev/null 2>&1 || return 1
fi
# 新封禁可能覆盖磁盘中已到期的旧记录;写回当前封禁,再由集合同步剩余时间。
if [ "$login_ip_black_timeout" -gt 0 ]; then
remove_ip_black_file_entry "$login_ip" || return 1
echo "$login_ip expires $element_timeout" >>"$ip_blacklist_path"
fi
else
ipset test "$ipset_name" "$login_ip" >/dev/null 2>&1 && return 1
[ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && write_pending_black_flow "$login_ip"
ipset -exist add "$ipset_name" "$login_ip" timeout "$login_ip_black_timeout" >/dev/null 2>&1 || return 1
fi
[ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && clear_ip_black_flows "$login_ip"
return 0
}
remove_ip_black_file_entry() {
local escaped_ip=${1//./\\.}
sed -i -e "/^[[:space:]]*${escaped_ip}[[:space:]]/d" -e "/^[[:space:]]*${escaped_ip}$/d" "$ip_blacklist_path"
}
# 移出黑名单
del_ip_black() {
[ -z "$1" ] && return
sed -i "/^${1}/d" ${ip_blacklist_path}
remove_ip_black_file_entry "$1" || return 1
rm -f "${dir}/pending_black_flow_${1//[:.]/_}"
# 检查 IP 版本
unset ipset_name
(echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist"
(echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6"
[ -n "$nftables_version" ] && [[ "$1" == *:* ]] && valid_ipv6_blacklist_ip "$1" && ipset_name="wechatpush_blacklistv6"
if [ -n "$nftables_version" ] && [[ "$1" == *:* ]]; then
canonical_ip=$(nft get element inet fw4 "$ipset_name" { "$1" } 2>/dev/null | awk -F'elements = [{][[:space:]]*' 'NF > 1 {split($2, fields, /[[:space:],}]/); print fields[1]; exit}')
[ -n "$canonical_ip" ] && [ "$canonical_ip" != "$1" ] && remove_ip_black_file_entry "$canonical_ip"
fi
[ -z "$ipset_name" ] && return
[ -n "$nftables_version" ] && {
@@ -2368,23 +2694,104 @@ set_ip_black() {
[ $(tail -n1 "${ip_blacklist_path}" | wc -l) -eq "0" ] && echo -e >>${ip_blacklist_path}
# 从 ip_blacklist 文件逐行添加黑名单,add_ip_black() 处验证是否重复,此处不在验证
for ip_black in $(cat ${ip_blacklist_path} | awk '{print $1}'); do
add_ip_black "$ip_black"
done
local saved_remaining
while IFS= read -r ip_black_info; do
ip_black=$(awk '{print $1}' <<<"$ip_black_info")
[ -z "$ip_black" ] && continue
saved_remaining=""
if [ -n "$nftables_version" ] && [[ "$ip_black_info" == *" expires "* ]]; then
saved_remaining=${ip_black_info##* expires }
saved_remaining=${saved_remaining//[[:space:]]/}
fi
if [[ "$saved_remaining" == 0s || "$saved_remaining" == 0ms ]]; then
if [[ "$ip_black" == *:* ]]; then
ipset_name="wechatpush_blacklistv6"
else
ipset_name="wechatpush_blacklist"
fi
if ! nft get element inet fw4 "$ipset_name" { "$ip_black" } >/dev/null 2>&1; then
del_ip_black "$ip_black"
rm -f "${dir}/pending_black_flow_${ip_black//[:.]/_}"
fi
continue
fi
add_ip_black "$ip_black" "$saved_remaining"
done <<<"$(cat "$ip_blacklist_path")"
# 当 ip_blacklist 文件清除 IP 时,从集合中清除 IP
[ -n "$nftables_version" ] && fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
[ -n "$nftables_version" ] && fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
if [ -n "$nftables_version" ] && [ "$login_ip_black_timeout" -eq 0 ]; then
fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | sed -e 's/.*{//' -e 's/}//' | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | sed -e 's/.*{//' -e 's/}//' | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
elif [ -n "$nftables_version" ]; then
fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
fi
[ -z "$nftables_version" ] && fw_info_blacklist=$(ipset list wechatpush_blacklist | grep "timeout" 2>/dev/null)
[ -z "$nftables_version" ] && fw_info_blacklistv6=$(ipset list wechatpush_blacklistv6 | grep "timeout" 2>/dev/null)
[ -n "$fw_info_blacklist" ] && [ -n "$fw_info_blacklistv6" ] && combined_fw_info_blacklist="${fw_info_blacklist}\n${fw_info_blacklistv6}"
[ -z "$fw_info_blacklist" ] && combined_fw_info_blacklist="${fw_info_blacklistv6}" || combined_fw_info_blacklist="${fw_info_blacklist}"
combined_fw_info_blacklist="$fw_info_blacklist"
[ -n "$fw_info_blacklistv6" ] && combined_fw_info_blacklist="${combined_fw_info_blacklist}${combined_fw_info_blacklist:+$'\n'}${fw_info_blacklistv6}"
# nft 按地址值查询并返回规范写法,避免文件中的 IPv6 缩写差异导致误删集合元素。
local configured_ips="" configured_ip configured_set canonical_ip escaped_ip octet
if [ -n "$nftables_version" ]; then
for configured_ip in $(awk '{print $1}' "$ip_blacklist_path"); do
[[ "$configured_ip" =~ ^[0-9a-fA-F:.]+$ ]] || continue
case "$configured_ip" in
*:*) valid_ipv6_blacklist_ip "$configured_ip" || continue; configured_set="wechatpush_blacklistv6" ;;
*)
[[ "$configured_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || continue
for octet in ${configured_ip//./ }; do
((10#$octet <= 255)) || continue 2
done
configured_set="wechatpush_blacklist"
;;
esac
if [[ "$configured_ip" == *:* ]]; then
configured_canonical=$(normalize_ipv6_blacklist_ip "$configured_ip")
whitelist_match=""
for whitelist_ip in $login_ip_white_list; do
valid_ipv6_blacklist_ip "$whitelist_ip" || continue
[ "$configured_canonical" = "$(normalize_ipv6_blacklist_ip "$whitelist_ip")" ] && whitelist_match="1" && break
done
[ "$whitelist_match" = "1" ] && continue
elif echo "$login_ip_white_list" | grep -w -q "$configured_ip"; then
continue
fi
# get element 即使加 -j 仍输出集合文本,按实际输出提取规范化地址。
canonical_ip=$(nft get element inet fw4 "$configured_set" { "$configured_ip" } 2>/dev/null) || return 1
canonical_ip=$(awk -F'elements = [{][[:space:]]*' 'NF > 1 {split($2, fields, /[[:space:],}]/); print fields[1]; exit}' <<<"$canonical_ip")
[[ "$canonical_ip" =~ ^[0-9a-fA-F:.]+$ ]] || return 1
configured_ips="${configured_ips}${canonical_ip}"$'\n'
if [ "$configured_ip" != "$canonical_ip" ]; then
escaped_ip=${configured_ip//./\\.}
sed -i -e "s/^[[:space:]]*${escaped_ip}$/${canonical_ip}/" -e "s/^[[:space:]]*${escaped_ip} /${canonical_ip} /" "$ip_blacklist_path" || return 1
fi
done
fi
while IFS= read -r ip_black_info; do
ip_black=$(echo "$ip_black_info" | grep -Eo "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")
[ -z "$ip_black" ] && ip_black=$(echo "$ip_black_info" | grep -Eo "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
if [ -n "$nftables_version" ]; then
ip_black=$(awk '{print $1}' <<<"$ip_black_info")
[ -z "$ip_black" ] && continue
else
ip_black=$(echo "$ip_black_info" | grep -Eo "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")
[ -z "$ip_black" ] && ip_black=$(echo "$ip_black_info" | grep -Eo "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
fi
[ -z "$ip_black" ] && continue
cat ${ip_blacklist_path} | grep -q -w -i ${ip_black} && sed -i "/^${ip_black}/d" ${ip_blacklist_path} && echo ${ip_black_info} >>${ip_blacklist_path} || del_ip_black ${ip_black}
if [ -n "$nftables_version" ]; then
if grep -Fqx -- "$ip_black" <<<"$configured_ips"; then
remove_ip_black_file_entry "$ip_black" || return 1
# 按集合中的剩余时长写回,忽略 nft 为单个元素输出的 timeout 字段。
if [ "$login_ip_black_timeout" -gt 0 ]; then
echo "$ip_black expires ${ip_black_info##* expires }" >>"$ip_blacklist_path"
else
echo "$ip_black" >>"$ip_blacklist_path"
fi
else
del_ip_black "$ip_black"
fi
else
cat ${ip_blacklist_path} | grep -q -w -i ${ip_black} && sed -i "/^${ip_black}/d" ${ip_blacklist_path} && echo ${ip_black_info} >>${ip_blacklist_path} || del_ip_black ${ip_black}
fi
done <<<"$combined_fw_info_blacklist"
}