#!/bin/bash
# 自动分析 SSH 登录失败日志并封禁恶意 /24 段

LOG_FILE="/var/log/block_bad_subnets.log"
INSTALL_SCRIPT_PATH="/usr/local/bin/block_bad_subnets.sh"
SCRIPT_SOURCE_URL="${SCRIPT_SOURCE_URL:-https://dl.erdaye.shop/BAK/block_bad_subnets.sh}"
SCRIPT_SELF_PATH="$(realpath "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")"
EFFECTIVE_SCRIPT_PATH="$SCRIPT_SELF_PATH"
AUTO_MODE=false

log() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    echo "$msg"
    { mkdir -p "$(dirname "$LOG_FILE")" && echo "$msg" >> "$LOG_FILE"; } 2>/dev/null || true
}

ensure_script_installed() {
    local current="$SCRIPT_SELF_PATH"
    local target="$INSTALL_SCRIPT_PATH"
    mkdir -p "$(dirname "$target")"
    # 若已有本地版本且不是临时文件，直接复用
    if [[ -f "$current" && "$current" != /dev/fd/* && "$current" != /proc/* ]]; then
        if [[ "$current" != "$target" ]]; then
            if cp "$current" "$target" 2>/dev/null; then
                chmod +x "$target"
                log "已将脚本保存到 $target"
            fi
        else
            chmod +x "$target" 2>/dev/null
        fi
        EFFECTIVE_SCRIPT_PATH="$target"
        return 0
    fi

    # 运行自进程替换或临时路径，尝试使用原URL下载
    if [[ -n "$SCRIPT_SOURCE_URL" ]]; then
        local tmp_file="$target.tmp.$$"
        if curl -fsSL "$SCRIPT_SOURCE_URL" -o "$tmp_file"; then
            mv "$tmp_file" "$target"
            chmod +x "$target"
            EFFECTIVE_SCRIPT_PATH="$target"
            log "脚本运行自临时路径，已从 $SCRIPT_SOURCE_URL 下载副本"
            return 0
        else
            rm -f "$tmp_file"
            log "警告：无法从 $SCRIPT_SOURCE_URL 下载脚本副本"
        fi
    fi

    # 最后尝试将当前脚本内容拷贝一份
    if cat "$current" > "$target" 2>/dev/null; then
        chmod +x "$target"
        EFFECTIVE_SCRIPT_PATH="$target"
        log "已从临时路径复制脚本到 $target"
        return 0
    fi

    log "警告：无法保存脚本副本，cron 可能无法调用"
    EFFECTIVE_SCRIPT_PATH="$current"
    return 1
}

setup_cron() {
    ensure_script_installed
    local script_path="$INSTALL_SCRIPT_PATH"
    local quoted_script=$(printf '%q' "$script_path")
    local quoted_log=$(printf '%q' "$LOG_FILE")
    local cron_cmd="0 * * * * $quoted_script --auto >> $quoted_log 2>&1"
    if crontab -l 2>/dev/null | grep -F "$script_path --auto" | grep -v '^#' >/dev/null; then
        log "定时任务已存在"
    else
        (crontab -l 2>/dev/null; echo "$cron_cmd") | crontab -
        log "已设置每小时自动执行任务"
    fi
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --auto)
            AUTO_MODE=true
            shift
            ;;
        *)
            echo "未知参数: $1"
            echo "用法: $0 [--auto]"
            exit 1
            ;;
    esac
done

ensure_script_installed

# 1. 判断日志文件（包含历史/轮转日志）
shopt -s nullglob
LOG_FILES=()
declare -A seen_files=()
for candidate in /var/log/auth.log /var/log/auth.log* /var/log/secure /var/log/secure*; do
    [[ -f "$candidate" ]] || continue
    if [[ -z "${seen_files[$candidate]}" ]]; then
        LOG_FILES+=("$candidate")
        seen_files[$candidate]=1
    fi
done
shopt -u nullglob

if [[ ${#LOG_FILES[@]} -eq 0 ]]; then
    log "未找到 /var/log/auth.log* 或 /var/log/secure*"
    exit 1
fi

log "使用日志文件: ${LOG_FILES[*]}"

# 2. 提取失败 IP
FAILED_IPS=$(zgrep -h "Failed password" "${LOG_FILES[@]}" 2>/dev/null | awk '{print $(NF-3)}' | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}')

if [ -z "$FAILED_IPS" ]; then
    log "未发现失败登录记录"
    exit 0
fi

# 3. 转换成 /24 段并统计
SUBNETS=$(echo "$FAILED_IPS" | awk -F. '{print $1"."$2"."$3".0/24"}' | sort | uniq -c | sort -nr)

# 4. 过滤次数 > 10
BAD_SUBNETS=$(echo "$SUBNETS" | awk '$1>10 {print $1,$2}')

if [ -z "$BAD_SUBNETS" ]; then
    log "没有出现超过 10 次失败登录的 /24 段"
    exit 0
fi

echo "以下网段出现超过 10 次失败登录："
echo
i=1
declare -A SUBNET_MAP
while read -r count subnet; do
    echo "[$i] $subnet  (${count} 次失败)"
    SUBNET_MAP[$i]=$subnet
    i=$((i+1))
done <<< "$BAD_SUBNETS"
echo

# 5. 交互确认
EXCLUDE_LIST=()
if [[ "$AUTO_MODE" == true ]]; then
    log "自动模式：默认封禁所有满足条件的网段"
else
    read -p "请输入要【排除不封禁】的序号 (用空格分隔, 默认全封禁): " EXCLUDES
    EXCLUDE_LIST=($EXCLUDES)
fi

# 6. 执行封禁
echo
echo "准备封禁以下网段："
for idx in "${!SUBNET_MAP[@]}"; do
    skip=false
    for ex in "${EXCLUDE_LIST[@]}"; do
        if [ "$idx" == "$ex" ]; then
            skip=true
            break
        fi
    done
    if [ "$skip" = false ]; then
        echo "封禁: ${SUBNET_MAP[$idx]}"
        log "封禁网段: ${SUBNET_MAP[$idx]}"
        fail2ban-client set sshd banip "${SUBNET_MAP[$idx]}"
    else
        echo "跳过: ${SUBNET_MAP[$idx]}"
    fi
done

echo
echo "操作完成。"
log "本次封禁任务完成"

if [[ "$AUTO_MODE" == false ]]; then
    setup_cron
    echo "已将脚本保存到 $INSTALL_SCRIPT_PATH，并设置每小时自动执行任务"
fi

