mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 13:03:56 +08:00
🌴 Sync 2026-03-05 23:51:42
This commit is contained in:
1 parent
97c049ed0a
commit
0acff03804
14253 files changed
+3893239
No files matched your search
@@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright (C) 2025-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=watchdog
|
||||
PKG_VERSION:=1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
TITLE:=Routing Security Watchdog for OpenWrt
|
||||
DEPENDS:=+curl +bash
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
Routing Security Watchdog for OpenWrt @sirpdboy <herboy2008@gmail.com>
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/watchdog
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(CP) ./files/watchdog.config $(1)/etc/config/watchdog
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/watchdog.init $(1)/etc/init.d/watchdog
|
||||
$(INSTALL_DIR) $(1)/usr/libexec
|
||||
$(INSTALL_BIN) $(CURDIR)/files/watchdog-call.libexec $(1)/usr/libexec/watchdog-call
|
||||
$(INSTALL_DIR) $(1)/usr/share/watchdog
|
||||
$(INSTALL_BIN) $(CURDIR)/files/watchdog.share $(1)/usr/share/watchdog/watchdog
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
|
||||
#
|
||||
|
||||
logfile="/tmp/watchdog/watchdog.log"
|
||||
dir="/tmp/watchdog" && mkdir -p "${dir}"
|
||||
lang=$(uci get luci.main.lang 2>/dev/null)
|
||||
if [ -z "$lang" ] || [[ "$lang" == "auto" ]]; then
|
||||
lang=$(echo "${LANG:-${LANGUAGE:-${LC_ALL:-${LC_MESSAGES:-zh_cn}}}}" | awk -F'[ .@]' '{print tolower($1)}' | sed 's/-/_/' 2>/dev/null)
|
||||
fi
|
||||
|
||||
translate() {
|
||||
# 处理特殊字符
|
||||
local lua_script=$(cat <<LUA
|
||||
require "luci.i18n".setlanguage("$lang")
|
||||
print(require "luci.i18n".translate([==[$1]==]))
|
||||
LUA
|
||||
)
|
||||
|
||||
lua -e "$lua_script"
|
||||
}
|
||||
|
||||
if [ "$1" == "clear_log" ]; then
|
||||
# 清空日志
|
||||
>"${logfile}"
|
||||
|
||||
elif [ "$1" == "child" ]; then
|
||||
shift
|
||||
command_name=$1
|
||||
shift
|
||||
"$command_name" "$@"
|
||||
fi
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
config watchdog 'config'
|
||||
option sleeptime '60'
|
||||
option debuglevel '1'
|
||||
option up_timeout '2'
|
||||
option down_timeout '10'
|
||||
option timeout_retry_count '2'
|
||||
option thread_num '3'
|
||||
list login_control 'web_logged'
|
||||
list login_control 'ssh_logged'
|
||||
list login_control 'web_login_failed'
|
||||
list login_control 'ssh_login_failed'
|
||||
option login_max_num '3'
|
||||
option enable '0'
|
||||
option login_web_black '1'
|
||||
option login_ip_black_timeout '86400'
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# Copyright (C) 2025-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
|
||||
#
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
config=watchdog
|
||||
dir="/tmp/$config/"
|
||||
|
||||
start_service() {
|
||||
clear_rule
|
||||
procd_open_instance
|
||||
enable_value=$(uci get $config.config.enable 2>/dev/null || echo "0")
|
||||
[ "$enable_value" -ne "0" ] && procd_set_param command /usr/share/$config/$config && echo "$config is starting now ..."
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
# 检测防火墙类型
|
||||
detect_firewall_type() {
|
||||
if command -v nft >/dev/null && [ -x /sbin/fw4 ]; then
|
||||
echo "nft"
|
||||
elif command -v iptables >/dev/null || command -v ip6tables >/dev/null; then
|
||||
echo "iptables"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
clear_rule(){
|
||||
|
||||
|
||||
local fw_type=$(detect_firewall_type)
|
||||
|
||||
if [ "$fw_type" = "nft" ]; then
|
||||
|
||||
local drop_handles=$(nft -a list ruleset 2>/dev/null | grep -E "watchdog Drop rule" | awk '{print $NF}')
|
||||
for handle in $drop_handles; do
|
||||
nft delete rule inet fw4 input handle $handle 2>/dev/null
|
||||
done
|
||||
nft delete rule inet fw4 watchdog_input ip saddr @watchdog_blacklist 2>/dev/null
|
||||
nft delete rule inet fw4 watchdog_input ip6 saddr @watchdog_blacklistv6 2>/dev/null
|
||||
nft delete rule inet fw4 watchdog_input ether saddr @watchdog_blacklistbridge 2>/dev/null
|
||||
nft delete chain inet fw4 watchdog_input 2>/dev/null
|
||||
nft delete set inet fw4 watchdog_blacklist 2>/dev/null
|
||||
nft delete set inet fw4 watchdog_blacklistv6 2>/dev/null
|
||||
nft delete set inet fw4 watchdog_blacklistbridge 2>/dev/null
|
||||
elif [ "$fw_type" = "iptables" ]; then
|
||||
iptables -D INPUT -m set --match-set watchdog_blacklist src -j DROP 2>/dev/null
|
||||
iptables -D INPUT -m set --match-set watchdog_range src -j DROP 2>/dev/null
|
||||
ip6tables -D INPUT -m set --match-set watchdog_blacklistv6 src -j DROP 2>/dev/null
|
||||
ipset destroy watchdog_blacklist 2>/dev/null
|
||||
ipset destroy watchdog_blacklistv6 2>/dev/null
|
||||
ipset destroy watchdog_range 2>/dev/null
|
||||
fi
|
||||
|
||||
}
|
||||
stop_service() {
|
||||
[ -f ${dir}child_pid ] && parent_pid=$(cat ${dir}child_pid)
|
||||
clear_rule
|
||||
[ -n "$parent_pid" ] && {
|
||||
child_pids=$(pgrep -P $parent_pid)
|
||||
echo "Terminating child processes of $config..."
|
||||
for child_pid in $child_pids; do
|
||||
kill $child_pid
|
||||
done
|
||||
}
|
||||
local pids=$(ps | grep "$config" | grep -v grep | grep -v $$ | awk '{print $1}')
|
||||
[ -n "$pids" ] && echo "$pids" | xargs kill 2>/dev/null
|
||||
echo "Terminating $config process..."
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger $config
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
|
||||
#
|
||||
|
||||
APP=watchdog
|
||||
base_dir="/usr/share/$APP"
|
||||
api_dir="$base_dir/api"
|
||||
|
||||
# 检测防火墙类型
|
||||
detect_firewall_type() {
|
||||
if command -v nft >/dev/null && [ -x /sbin/fw4 ]; then
|
||||
echo "nft"
|
||||
elif command -v iptables >/dev/null; then
|
||||
echo "iptables"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# 读取设置文件
|
||||
get_config() {
|
||||
while [[ "$*" != "" ]]; do
|
||||
[[ "$1" == "lang" ]] && {
|
||||
lang=$(uci get luci.main.lang 2>/dev/null)
|
||||
if [ -z "$lang" ] || [[ "$lang" == "auto" ]]; then
|
||||
lang=$(echo "${LANG:-${LANGUAGE:-${LC_ALL:-${LC_MESSAGES:-zh_cn}}}}" | awk -F'[ .@]' '{print tolower($1)}' | sed 's/-/_/' 2>/dev/null)
|
||||
fi
|
||||
} || {
|
||||
eval "${1}='$(uci get $APP.config.$1 2>/dev/null)'"
|
||||
}
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 初始化设置信息
|
||||
read_config() {
|
||||
get_config \
|
||||
"enable" "sleeptime" "lang" \
|
||||
"login_control" "login_max_num" \
|
||||
"login_web_black" "login_ip_black_timeout"
|
||||
|
||||
(echo "$login_control" | grep -q "web_logged") && web_logged="true"
|
||||
(echo "$login_control" | grep -q "ssh_logged") && ssh_logged="true"
|
||||
(echo "$login_control" | grep -q "web_login_failed") && web_login_failed="true"
|
||||
(echo "$login_control" | grep -q "ssh_login_failed") && ssh_login_failed="true"
|
||||
|
||||
(opkg list-installed | grep -w -q "^firewall4") && nftables_version="true"
|
||||
ip_blacklist_path="$api_dir/ip_blacklist"
|
||||
|
||||
ipv4_urllist=$(cat /usr/share/$APP/api/ipv4.list) 2>/dev/null
|
||||
ipv6_urllist=$(cat /usr/share/$APP/api/ipv6.list) 2>/dev/null
|
||||
[ -z "$sleeptime" ] && sleeptime="60"
|
||||
[ -z "$login_ip_black_timeout" ] && login_ip_black_timeout="86400"
|
||||
deltemp
|
||||
}
|
||||
|
||||
# 初始化
|
||||
init() {
|
||||
# 检测程序开关
|
||||
enable_test
|
||||
[ -f "$logfile" ] && local logrow=$(grep -c "" "$logfile") || local logrow="0"
|
||||
[ "$logrow" -ne 0 ] && echo "----------------------------------" >>${logfile}
|
||||
log "[INFO] $(translate "Start running")"
|
||||
if [ -f "/usr/share/$APP/errlog" ]; then
|
||||
cat /usr/share/$APP/errlog >${logfile}
|
||||
log "[ERROR] $(translate "Loaded logs from previous restart")"
|
||||
fi
|
||||
|
||||
# 文件清理
|
||||
rm -f "/usr/share/$APP/errlog" >/dev/null 2>&1
|
||||
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"
|
||||
set_ip_black
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 主程序
|
||||
main() {
|
||||
# 限制并发进程
|
||||
dir="/tmp/$APP"
|
||||
logfile="${dir}/$APP.log"
|
||||
mkdir -p "$(dirname "$logfile")"
|
||||
get_config "thread_num"
|
||||
[ -z "$thread_num" ] || [ "$thread_num" -eq "0" ] && thread_num=5
|
||||
[ "$1" ] && [ $1 == "t1" ] && thread_num=1
|
||||
max_thread_num="$thread_num"
|
||||
|
||||
FIFO_PATH="${dir}/fifo.$$"
|
||||
mkfifo "$FIFO_PATH"
|
||||
exec 5<>"$FIFO_PATH"
|
||||
rm "$FIFO_PATH" >/dev/null 2>&1
|
||||
|
||||
for i in $(seq 1 "$max_thread_num"); do
|
||||
echo >&5
|
||||
done
|
||||
unset i
|
||||
|
||||
# 定义锁文件
|
||||
lock_file="${dir}/$APP.lock"
|
||||
touch "$lock_file"
|
||||
|
||||
# 设置信号处理
|
||||
trap cleanup SIGINT SIGTERM EXIT
|
||||
MAIN_PID=$$
|
||||
PROCESS_TAG="{watchdog}_${MAIN_PID}"
|
||||
|
||||
silent_run read_config
|
||||
|
||||
# 载入在线设备
|
||||
init
|
||||
[ $? -eq 1 ] && log "[ERROR] $(translate "Failed to read settings, please check configuration.")" && exit
|
||||
if [ -n "$web_logged" ] || [ -n "$ssh_logged" ] || [ -n "$web_login_failed" ] || [ -n "$ssh_login_failed" ]; then
|
||||
# 声明关联数组
|
||||
declare -A web_login_counts
|
||||
declare -A ssh_login_counts
|
||||
declare -A web_failed_counts
|
||||
declare -A ssh_failed_counts
|
||||
fi
|
||||
|
||||
>"${dir}/send_enable.lock" && deltemp
|
||||
log "[INFO] $(translate "Initialization completed")"
|
||||
while [ "$enable" -eq "1" ]; do
|
||||
deltemp
|
||||
|
||||
|
||||
silent_run run_logins
|
||||
set_ip_black
|
||||
sleep $sleeptime
|
||||
done
|
||||
}
|
||||
|
||||
# 隐藏输出
|
||||
silent_run() {
|
||||
"$@" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
|
||||
# 计算字符串显示宽度
|
||||
length_str() {
|
||||
[ -z "$1" ] && return
|
||||
|
||||
local result
|
||||
{
|
||||
local str="$1"
|
||||
local length=0
|
||||
|
||||
while IFS= read -r -n1 char; do
|
||||
local char_width
|
||||
char_width=$(printf "%s" "$char" | awk '{
|
||||
if (match($0, /[一-龥]/)) print 2;
|
||||
else print 1;
|
||||
}')
|
||||
|
||||
length=$((length + char_width))
|
||||
done <<< "$str"
|
||||
|
||||
result="$length"
|
||||
} > /dev/null 2>&1
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# 字符串显示宽度处理
|
||||
cut_str() {
|
||||
[ -z "$1" ] && return
|
||||
[ -z "$2" ] && return
|
||||
local result
|
||||
# 调试模式不要输出信息
|
||||
{
|
||||
local str="$1"
|
||||
local max_width="$2"
|
||||
local current_width=0
|
||||
|
||||
# 遍历字符串的每个字符
|
||||
for ((i = 0; i < ${#str}; i++)); do
|
||||
local char="${str:$i:1}"
|
||||
local char_width=$(length_str "$char")
|
||||
|
||||
# 如果当前宽度加上当前字符的宽度超过最大宽度,则停止
|
||||
if [ $((current_width + char_width)) -gt "$max_width" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
result="${result}${char}"
|
||||
current_width=$((current_width + char_width))
|
||||
done
|
||||
|
||||
# 如果裁剪了字符串,则添加 ".."
|
||||
if [ "$current_width" -lt $(length_str "$str") ]; then
|
||||
result=$(echo "$result" | sed 's/ *$//')
|
||||
result="${result}.."
|
||||
fi
|
||||
} > /dev/null 2>&1
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# 翻译
|
||||
translate() {
|
||||
local template="$1"
|
||||
shift # 移出第一个参数,剩余参数作为变量
|
||||
|
||||
# 获取基础翻译
|
||||
|
||||
local lua_script=$(cat <<LUA
|
||||
require "luci.i18n".setlanguage("$lang")
|
||||
print(require "luci.i18n".translate([==[$template]==]))
|
||||
LUA
|
||||
)
|
||||
local translated=$(lua -e "$lua_script")
|
||||
|
||||
# 如果有额外参数则进行格式化
|
||||
if [ $# -gt 0 ]; then
|
||||
printf "$translated" "$@"
|
||||
else
|
||||
echo "$translated"
|
||||
fi
|
||||
}
|
||||
|
||||
# 随机数
|
||||
rand() {
|
||||
local min=$1
|
||||
local max=$(($2 - $min + 1))
|
||||
local num=$(date +%s%N)
|
||||
echo $(($num % $max + $min))
|
||||
}
|
||||
# 初始化日志
|
||||
log() {
|
||||
echo "$(date "+%Y-%m-%d %H:%M:%S") - $1" >> "$logfile"
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 文件锁
|
||||
LockFile() {
|
||||
local fd=200
|
||||
|
||||
if [ "$1" = "lock" ]; then
|
||||
eval "exec $fd>$lock_file"
|
||||
flock -n $fd
|
||||
if [ $? -ne 0 ]; then
|
||||
while ! flock -n $fd; do
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
elif [ "$1" = "unlock" ]; then
|
||||
eval "exec $fd>&-"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检测退出信号
|
||||
cleanup() {
|
||||
local pids=$(ps | grep -E "\{watchdog\}_${MAIN_PID}|\{watchdog-call\}" | grep -v grep | awk '{print $1}')
|
||||
[ -n "$pids" ] && echo "$pids" | xargs kill 2>/dev/null
|
||||
LockFile unlock
|
||||
$EXIT_FLAG && exit 0
|
||||
}
|
||||
|
||||
# 子进程调用
|
||||
run_with_tag() {
|
||||
[ -z "$1" ] && return
|
||||
local command_name=$1 # 第一个参数是命令名称
|
||||
shift # 移除第一个参数,剩下的参数传递给命令
|
||||
local command_path=$(readlink -f "$(which "$command_name")") # 检查命令路径
|
||||
|
||||
# 如果是 BusyBox 的 applet,调用 watchdog-call
|
||||
if [[ "$command_path" == *"busybox"* ]]; then
|
||||
/usr/libexec/watchdog-call child "$command_name" "$@"
|
||||
else
|
||||
bash -c 'exec -a "$0" "$@"' "${PROCESS_TAG} ${command_name}" "$command_name" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# 清理临时文件
|
||||
deltemp() {
|
||||
rm -f "${dir}/send_enable.lock" >/dev/null 2>&1
|
||||
[ -f "$logfile" ] && local logrow=$(grep -c "" "$logfile") || local logrow="0"
|
||||
[ "$logrow" -gt 500 ] && tail -n 300 "$logfile" >"${logfile}.tmp" && mv "${logfile}.tmp" "$logfile" && log "[DEBUG] $(translate "Log exceeded limit, keeping last 300 entries")"
|
||||
}
|
||||
|
||||
# ------------------------------------
|
||||
# 信息获取类
|
||||
|
||||
|
||||
# 查询 IP 归属地
|
||||
get_ip_attribution() {
|
||||
ip="$1"
|
||||
jq -e --arg ip "$ip" '.devices[] | select(.ip == $ip)' "$devices_json" >/dev/null 2>&1 && echo "本地局域网" && return
|
||||
local ip_attribution_urls=$(cat /usr/share/watchdog/api/ip_attribution.list)
|
||||
local sorted_attribution_urls=$(echo "$ip_attribution_urls" | awk 'BEGIN {srand()} {print rand() "\t" $0}' | sort -k1,1n | cut -f2-)
|
||||
local ip_attribution_url
|
||||
while IFS= read -r ip_attribution_url; do
|
||||
local login_ip_attribution=$(eval curl --connect-timeout 2 -m 2 -k -s "$ip_attribution_url" 2>/dev/null)
|
||||
[ -n "$login_ip_attribution" ] && echo "$login_ip_attribution" && break
|
||||
done <<<"$sorted_attribution_urls"
|
||||
}
|
||||
|
||||
|
||||
# 检测程序开关
|
||||
enable_test() {
|
||||
[ -z "$1" ] && local time_n=1
|
||||
for i in $(seq 1 $time_n); do
|
||||
get_config enable
|
||||
[ -z "$enable" ] || [ "$enable" -eq "0" ] && exit || sleep 1
|
||||
done
|
||||
unset i
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 初始化黑名单规则
|
||||
init_ip_black() {
|
||||
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] || return
|
||||
# 设置 IP 版本变量
|
||||
if [ $1 == "ipv4" ]; then
|
||||
ipset_name="watchdog_blacklist"
|
||||
ip_version="ip"
|
||||
elif [ $1 == "ipv6" ]; then
|
||||
ipset_name="watchdog_blacklistv6"
|
||||
ip_version="ip6"
|
||||
nat_table_cmd="family inet6"
|
||||
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 \"\!watchdog Drop rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 input $ip_version saddr @${ipset_name} counter drop comment \"\!watchdog 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
|
||||
}
|
||||
}
|
||||
|
||||
# 添加黑名单
|
||||
add_ip_black() {
|
||||
local login_ip=$1
|
||||
[ -z "$login_ip" ] && return 0
|
||||
# 检查 IP 版本
|
||||
unset ipset_name
|
||||
(echo "$login_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="watchdog_blacklist"
|
||||
(echo "$login_ip" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="watchdog_blacklistv6"
|
||||
[ -z "$ipset_name" ] && sed -i "/^$login_ip /d" "$ip_blacklist_path" && log "[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
|
||||
}
|
||||
}
|
||||
|
||||
# 移出黑名单
|
||||
del_ip_black() {
|
||||
[ -z "$1" ] && return
|
||||
sed -i "/^${1}/d" ${ip_blacklist_path}
|
||||
|
||||
# 检查 IP 版本
|
||||
unset ipset_name
|
||||
(echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="watchdog_blacklist"
|
||||
(echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="watchdog_blacklistv6"
|
||||
[ -z "$ipset_name" ] && return
|
||||
|
||||
[ -n "$nftables_version" ] && {
|
||||
nft delete element inet fw4 ${ipset_name} { $1 } >/dev/null 2>&1
|
||||
} || {
|
||||
ipset list ${ipset_name} >/dev/null 2>&1 && ipset -! del ${ipset_name} ${1}
|
||||
}
|
||||
}
|
||||
|
||||
# 设置防火墙列表
|
||||
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
|
||||
# 当 ip_blacklist 文件清除 IP 时,从集合中清除 IP
|
||||
[ -n "$nftables_version" ] && fw_info_blacklist=$(nft list set inet fw4 watchdog_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 watchdog_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
|
||||
[ -z "$nftables_version" ] && fw_info_blacklist=$(ipset list watchdog_blacklist | grep "timeout" 2>/dev/null)
|
||||
[ -z "$nftables_version" ] && fw_info_blacklistv6=$(ipset list watchdog_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}"
|
||||
|
||||
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}")
|
||||
[ -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}
|
||||
log "$(translate "[Ban information]Cancel the ban IP:%s" "$ip_black")"
|
||||
}
|
||||
done <<<"$combined_fw_info_blacklist"
|
||||
}
|
||||
|
||||
# 监听登录事件
|
||||
run_logins() {
|
||||
if [ -n "$web_logged" ] || [ -n "$ssh_logged" ] || [ -n "$web_login_failed" ] || [ -n "$ssh_login_failed" ]; then
|
||||
# 监听系统日志,-f 表示跟随实时日志,-p 表示日志级别为 notice
|
||||
run_with_tag logread -f -p notice | while IFS= read -r line; do
|
||||
[ -n "$web_logged" ] && {
|
||||
web_login_ip=$(echo "$line" | grep -i "accepted login" | awk '{print $NF}')
|
||||
[ -n "$web_login_ip" ] && process_login "$web_login_ip" $(echo "$line" | awk '{print $4}') web_login_counts
|
||||
}
|
||||
|
||||
[ -n "$ssh_logged" ] && {
|
||||
ssh_login_ip=$(echo "$line" | grep -i "Password auth succeeded\|Pubkey auth succeeded" | awk '{print $NF}' | sed -nr 's#^(.*):.[0-9]{1,5}#\1#gp' | sed -e 's/%.*//')
|
||||
[ -n "$ssh_login_ip" ] && process_login "$ssh_login_ip" $(echo "$line" | awk '{print $4}') ssh_login_counts
|
||||
}
|
||||
|
||||
[ -n "$web_login_failed" ] && {
|
||||
web_failed_ip=$(echo "$line" | grep -i "failed login" | awk '{print $NF}')
|
||||
[ -n "$web_failed_ip" ] && process_login "$web_failed_ip" $(echo "$line" | awk '{print $4}') web_failed_counts
|
||||
}
|
||||
|
||||
[ -n "$ssh_login_failed" ] && {
|
||||
# 匹配特定的 SSH 登录失败情况并提取 IP 地址和时间
|
||||
ssh_failed_ip=$(echo "$line" | grep -iE "Bad password attempt|Login attempt for nonexistent user|Max auth tries reached" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//')
|
||||
|
||||
# 如果未能提取到 IP,从日志标识符提取失败用户的 ID,并再次提取 IP
|
||||
if [ -z "$ssh_failed_ip" ]; then
|
||||
ssh_failed_num=$(echo "$line" | sed -n 's/.*authpriv\.warn dropbear\[\([0-9]\+\)\]: Login attempt for nonexistent user/\1/p')
|
||||
[ -n "$ssh_failed_num" ] && ssh_failed_ip=$(logread notice | grep "authpriv\.info dropbear\[${ssh_failed_num}\].*Child connection from" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//' | tail -n 1)
|
||||
fi
|
||||
|
||||
# 如果成功提取到 IP 地址,调用 process_login 处理
|
||||
[ -n "$ssh_failed_ip" ] && process_login "$ssh_failed_ip" $(echo "$line" | awk '{print $4}') ssh_failed_counts
|
||||
}
|
||||
|
||||
done
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# 处理登录事件
|
||||
# 参数:
|
||||
# $1: IP
|
||||
# $2: 日志时间 - 从日志中读取而不是使用当前时间,避免秒对应不上
|
||||
# $3: 数组名 - 记录 IP 和登录次数的关联数组名
|
||||
process_login() {
|
||||
local login_ip=$1
|
||||
local login_time=$2
|
||||
local -n login_counts=$3
|
||||
|
||||
# 如果数组中不存在此 IP,初始化为 0
|
||||
if [ -z "${login_counts["$login_ip"]}" ]; then
|
||||
login_counts["$login_ip"]=0
|
||||
fi
|
||||
# +1
|
||||
login_counts["$login_ip"]=$((login_counts["$login_ip"] + 1))
|
||||
local count=${login_counts["$login_ip"]}
|
||||
|
||||
# 封禁
|
||||
if [[ ("$3" == "web_failed_counts" || "$3" == "ssh_failed_counts") ]]; then
|
||||
if [[ $count -ge $login_max_num ]] ;then
|
||||
add_ip_black ${login_ip} && {
|
||||
unset login_counts["$login_ip"]
|
||||
login_send "$login_ip" "$login_time" "$3"
|
||||
log "$(translate "[Block Information]Add to blacklist IP: %s Attempts:%s Time:%s" "$login_ip" "$count" "$login_time" )"
|
||||
}
|
||||
else
|
||||
login_send "$login_ip" "$login_time" "$3"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# 正常登录
|
||||
if [[ "$3" == "web_login_counts" || "$3" == "ssh_login_counts" ]]; then
|
||||
del_ip_black ${login_ip} # 白名单已经优先于黑名单,但白名单集合有超时限制,防止下次修改代码忘记,上保险
|
||||
unset web_failed_counts["$login_ip"]
|
||||
unset ssh_failed_counts["$login_ip"]
|
||||
unset login_counts["$login_ip"]
|
||||
login_send "$login_ip" "$login_time" "$3"
|
||||
fi
|
||||
[ "${#login_counts[@]}" -gt "100" ] && login_counts=("${login_counts[@]: -100}")
|
||||
}
|
||||
|
||||
# 登录提醒
|
||||
login_send() {
|
||||
local login_ip=$1
|
||||
local login_time=$2
|
||||
local log_type=$3
|
||||
|
||||
local login_title
|
||||
local login_content
|
||||
|
||||
>"${dir}/send_enable.lock"
|
||||
|
||||
[ -z "$login_ip" ] && return
|
||||
|
||||
[[ "$log_type" == "web"* ]] && local log_type_short="Web" || local log_type_short="SSH"
|
||||
[ -f "$logfile" ] && login_log=$(grep -w "$login_ip" "$logfile" | grep -v "\[info\]" | tail -n 1)
|
||||
[ -n "$login_log" ] && log_timestamp=$(date -d "$(echo "$login_log" | awk '{print $1, $2}')" +%s) || log_timestamp=0
|
||||
|
||||
# 查询 IP 归属地
|
||||
local login_ip_attribution=$(get_ip_attribution "${login_ip}")
|
||||
# 登录方式
|
||||
if [[ "$log_type" == "web"* ]]; then
|
||||
# Web 登录、非法登录
|
||||
local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $13}' | tail -n 1)
|
||||
[ "$login_mode" = "/" ] && login_mode="$(translate "/ (Homepage login)")"
|
||||
elif [[ "$log_type" == "ssh_login"* ]]; then
|
||||
# SSH 登录
|
||||
local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $8}' | tail -n 1)
|
||||
else
|
||||
local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{for(i=8;i<NF;i++) if($i=="from") break; else printf $i " "}' | tail -n 1)
|
||||
fi
|
||||
if [[ "$log_type" == *"failed"* ]]; then
|
||||
local login_title=$(translate "%s frequent %s login attempts" "$login_ip" "$log_type_short")
|
||||
local login_content_info="${str_splitline}${str_title_start}$(translate "Block Information")${str_title_end}"
|
||||
log "$(translate "Device %s (%s) frequently attempted %s %s login" "$login_ip" "$login_ip_attribution" "$log_type_short" "$login_mode")"
|
||||
else
|
||||
local login_title=$(translate "%s logged into router via %s" "$login_ip" "$log_type_short")
|
||||
local login_content_info="${str_splitline}${str_title_start}$(translate "Login Information")${str_title_end}"
|
||||
log "$(translate "Device %s (%s) logged into router via %s %s" "$login_ip" "$login_ip_attribution" "$log_type_short" "$login_mode")"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
main "$@"
|
||||
Reference in new issue
Block a user