diff --git a/Limiting_Shut_down.sh b/Limiting_Shut_down.sh index bda347a..20cc015 100644 --- a/Limiting_Shut_down.sh +++ b/Limiting_Shut_down.sh @@ -1,32 +1,83 @@ #!/bin/bash -# 获取总的接收和发送流量 -output=$(awk 'BEGIN { rx_total = 0; tx_total = 0 } - NR > 2 { rx_total += $2; tx_total += $10 } - END { - printf("%.0f Bytes %.0f Bytes", rx_total, tx_total); - }' /proc/net/dev) - -# 获取接收和发送的流量数据 -rx=$(echo "$output" | awk '{print $1}') -tx=$(echo "$output" | awk '{print $3}') - -# 显示当前流量使用情况 -echo "当前接收流量: $rx" -echo "当前发送流量: $tx" +PATH=/usr/sbin:/usr/bin:/sbin:/bin +# 流量阈值(单位 GB),由 kejilion.sh 菜单写入 threshold_gb=110 -# 将GB转换为字节 -threshold=$((threshold_gb * 1024 * 1024 * 1024)) - -# 检查是否达到流量阈值 -if (( $rx > $threshold || $tx > $threshold )); then - echo "流量达到${threshold},正在关闭服务器..." - # 在此处执行关闭服务器的命令,例如: - shutdown -h now - # 或者 - # systemctl poweroff +if [ ! -r /proc/net/dev ]; then + echo "无法读取 /proc/net/dev,跳过本次流量检查。" >&2 + exit 1 +fi + +# 统计外部网卡的累计收发流量(排除回环和常见容器/隧道接口) +output=$(awk 'BEGIN { rx_total = 0; tx_total = 0 } + NR > 2 { + idx = index($0, ":"); + if (idx == 0) next; + iface = substr($0, 1, idx - 1); + gsub(/[ \t]/, "", iface); + if (iface == "lo" || iface ~ /^(docker|veth|br-|virbr|tun|tap|wg|tailscale|zt)/) next; + split(substr($0, idx + 1), f, " "); + rx_total += f[1]; + tx_total += f[9]; + } + END { + printf("%.0f %.0f", rx_total, tx_total); + }' /proc/net/dev) + +# 获取接收和发送的流量数据(单位:字节) +rx=$(echo "$output" | awk '{print $1}') +tx=$(echo "$output" | awk '{print $2}') + +# 取值异常时直接退出,避免把空值带进整数比较导致误判。 +# 必须分开校验:拼接后 rx 为空、tx 非空的情况会被误判为合法。 +case "$rx" in + ''|*[!0-9]*) + echo "流量统计结果异常,跳过本次检查。" >&2 + exit 1 + ;; +esac +case "$tx" in + ''|*[!0-9]*) + echo "流量统计结果异常,跳过本次检查。" >&2 + exit 1 + ;; +esac + +# 显示当前流量使用情况 +awk -v rx="$rx" -v tx="$tx" 'BEGIN { + printf("当前接收流量: %.2f GB\n当前发送流量: %.2f GB\n", rx / 1073741824, tx / 1073741824); +}' + +# 配置可能被手动修改,执行关机前再次校验阈值 +case "$threshold_gb" in + ''|*[!0-9]*) + echo "流量阈值配置不合法: ${threshold_gb}" >&2 + exit 1 + ;; +esac +# 用整数比较而非匹配字面量 0,否则 00/000 会漏网并让阈值变成 0(每分钟关机) +if [ "$threshold_gb" -le 0 ]; then + echo "流量阈值必须大于 0: ${threshold_gb}" >&2 + exit 1 +fi + +# 任一方向达到阈值即触发。主流 VPS(Vultr/DO/Linode/Hetzner)只计出站, +# 按单向最大值判断更贴近实际配额,也与旧版行为保持一致。 +if awk -v rx="$rx" -v tx="$tx" -v threshold_gb="$threshold_gb" 'BEGIN { + threshold = threshold_gb * 1073741824; + exit !(rx >= threshold || tx >= threshold); +}'; then + echo "单向流量已达到 ${threshold_gb}GB,正在关闭服务器..." + if command -v systemctl >/dev/null 2>&1 && systemctl poweroff; then + exit 0 + fi + if command -v shutdown >/dev/null 2>&1 && shutdown -h now; then + exit 0 + fi + echo "提交关机请求失败,未找到可用命令或命令执行失败。" >&2 + exit 1 else - echo "当前流量未达到${threshold},继续监视..." + echo "当前单向流量未达到 ${threshold_gb}GB,继续监视..." fi diff --git a/README.md b/README.md index 7f34954..ac43548 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,43 @@ # 科技lion一键脚本工具 ## 介绍 -科技Lion 的 Shell 脚本工具是一款面向 Linux VPS 的交互式管理工具箱,聚焦系统信息与维护、Docker 基础管理、网络测试与调优、SSH 安全防护、Telegram 监控通知、tmux 后台工作区和 VPS 集群批量运维。无论您是初学者还是经验丰富的用户,都可以通过菜单快速执行常见服务器管理任务。 +科技Lion 的 Shell 脚本工具是一款面向 Debian / Ubuntu VPS 的交互式管理工具箱,聚焦系统信息与维护、网络测试与调优、SSH 安全防护、流量限额控制和 VPS 集群批量运维。无论您是初学者还是经验丰富的用户,都可以通过菜单快速执行常见服务器管理任务。 [视频介绍](https://www.youtube.com/watch?v=0o7oH3Dit70&t=211s) *** -### 科技lion一键脚本工具 的支持列表: ->Debian ->Ubuntu ->Cent OS +### 科技lion一键脚本工具的支持列表 + +> Debian +> Ubuntu + +其他 Linux 发行版不在支持范围内,脚本启动时会拒绝运行。 + *** ## 使用方法 ### Debian / Ubuntu 安装下载工具 ```bash -apt update -y && apt install -y curl -``` -### CentOS 安装下载工具 -```bash -apt update -y && apt install -y curl +apt update -y && apt install -y curl ``` *** ### 一键脚本 ```bash -curl -sS -O https://kejilion.pro/kejilion.sh && chmod +x kejilion.sh && ./kejilion.sh +curl -sS -O https://gitea.tohub.top/Share/kejilion/raw/branch/main/kejilion.sh && chmod +x kejilion.sh && ./kejilion.sh ``` -or +*** +### 下载源配置 + +脚本内通过 GitHub Raw 下载的资源默认走官方地址,脚本自身更新走本仓库 `main` 分支,均可用环境变量覆盖。部分测试工具使用其项目提供的独立域名,不受 `GITHUB_RAW_URL` 控制: + ```bash -curl -sS -O https://raw.githubusercontent.com/kejilion/sh/main/kejilion.sh && chmod +x kejilion.sh && ./kejilion.sh +# 自定义第三方脚本下载源(例如自建 GitHub 反代) +GITHUB_RAW_URL="https://your-mirror.example.com" ./kejilion.sh + +# 自定义脚本自身的更新源 +UPDATE_RAW_URL="https://gitea.tohub.top/Share/kejilion/raw/branch/main" ./kejilion.sh ``` *** ### 觉得脚本还可以USTD TRC20打赏 ![Snipaste_2024-01-17_18-01-52](https://github.com/kejilion/sh/assets/131984541/98cf2762-1bfb-4c33-af10-af0eda29fc20) - diff --git a/app.md b/app.md index 1362f67..ab44f52 100644 --- a/app.md +++ b/app.md @@ -1,4 +1,4 @@ -本项目是一套面向 **Linux VPS 的交互式服务器管理脚本工具箱**,核心程序是 `kejilion.sh`。当前功能聚焦于系统管理、网络测试与调优、安全防护、流量限额控制和 VPS 集群运维。 +本项目是一套面向 **Debian / Ubuntu VPS 的交互式服务器管理脚本工具箱**,核心程序是 `kejilion.sh`。当前功能聚焦于系统管理、网络测试与调优、安全防护、流量限额控制和 VPS 集群运维。 ## 1. VPS 系统管理 @@ -12,8 +12,12 @@ - 查看端口占用 - 管理 `/etc/hosts` - 管理 Crontab 定时任务 +- 切换系统更新源(阿里云 / 官方 / 备份初始源 / 还原初始源) +- 安装 Python 指定版本(基于 pyenv) - 重启服务器 +> 说明:一键重装系统(dd)功能已移除。 + ## 2. 网络与 VPS 测试 - WARP 管理 @@ -23,7 +27,7 @@ - 三网回程路由测试 - MTR 线路测试 - NextTrace 路由测试 -- SuperSpeed 和多功能测速 +- SuperSpeed 和 i-abc 多功能测速 - YABS 性能测试 - Geekbench 5 CPU 测试 - Bench 综合测试 @@ -46,7 +50,7 @@ - 实时查看 Fail2Ban 日志 - 安装或卸载 SSH 防护 -相关配置文件:`sshd.local` +相关配置文件:`/path/to/fail2ban/config/fail2ban/jail.d/linux-ssh.conf` ### 账户安全 @@ -58,7 +62,7 @@ - 设置每月流量额度 - 定期统计接收和发送流量 -- 达到阈值后自动关机 +- 接收或发送任意一个方向达到阈值后自动关机(多数 VPS 只计出站,按单向判断更贴近实际配额) - 每月重新开始统计 - 支持启用和停用 @@ -82,4 +86,10 @@ > **VPS 系统管理 + 网络测试与调优 + SSH 安全防护 + 流量限额控制 + 集群运维。** -部分功能会在运行时从 GitHub 或第三方站点下载脚本和配置,因此实际可用性取决于外部资源和目标 Linux 发行版。 \ No newline at end of file +部分功能会在运行时从 GitHub 或第三方站点下载脚本和配置,因此实际可用性取决于外部资源以及目标 Debian / Ubuntu 版本。 + +## 下载源说明 + +- 通过 GitHub Raw 下载的第三方脚本默认走官方地址,可通过环境变量 `GITHUB_RAW_URL` 覆盖(例如自建反代);使用项目独立域名的测试工具不受该变量控制。 +- 脚本自身的更新源由环境变量 `UPDATE_RAW_URL` 控制,默认指向本仓库的 `main` 分支。 +- 已移除按地区自动切换国内镜像的逻辑(原镜像域名已失效)。 diff --git a/kejilion.sh b/kejilion.sh index 21eed2e..2e666da 100644 --- a/kejilion.sh +++ b/kejilion.sh @@ -1,33 +1,234 @@ #!/bin/bash -sh_v="2.6.7" +sh_v="2.7.0" huang='\033[33m' bai='\033[0m' lv='\033[0;32m' -lan='\033[0;34m' hong='\033[31m' kjlan='\033[96m' hui='\e[37m' -if [ -z "${GITHUB_RAW_URL:-}" ]; then - country=$(curl -fsS --max-time 3 https://ipinfo.io/country 2>/dev/null) - case "$country" in - CN) - GITHUB_RAW_URL="https://raw.gitmirror.com" - ;; - "") - GITHUB_RAW_URL="https://raw.githubusercontent.com" - echo "无法检测服务器地区,使用默认 GitHub Raw 地址。" >&2 - ;; - *) - GITHUB_RAW_URL="https://raw.githubusercontent.com" - ;; - esac -fi +# 统一使用 GitHub 官方 Raw 地址,可通过环境变量 GITHUB_RAW_URL 覆盖 +GITHUB_RAW_URL="${GITHUB_RAW_URL:-https://raw.githubusercontent.com}" GITHUB_RAW_URL="${GITHUB_RAW_URL%/}" -cp ./kejilion.sh /usr/local/bin/k > /dev/null 2>&1 +# 本脚本自身的更新源 +UPDATE_RAW_URL="${UPDATE_RAW_URL:-https://gitea.tohub.top/Share/kejilion/raw/branch/main}" +UPDATE_RAW_URL="${UPDATE_RAW_URL%/}" + +XANMOD_SOURCE_FILE=/etc/apt/sources.list.d/kejilion-xanmod.list + +if [ ! -r /etc/os-release ]; then + echo "无法识别操作系统,仅支持 Debian 和 Ubuntu。" >&2 + exit 1 +fi + +OS_ID=$(. /etc/os-release && printf '%s' "$ID") +case "$OS_ID" in + debian|ubuntu) ;; + *) + echo "当前系统不受支持,仅支持 Debian 和 Ubuntu。" >&2 + exit 1 + ;; +esac + +# 记录脚本自身的绝对路径,供重启自身和安装快捷命令使用 +SCRIPT_PATH=$(readlink -f "$0" 2>/dev/null) +[ -f "$SCRIPT_PATH" ] || SCRIPT_PATH="$0" +if [ ! -f "$SCRIPT_PATH" ]; then + echo "无法定位脚本自身路径,请先将脚本保存到本地再运行。" >&2 + exit 1 +fi + +cp "$SCRIPT_PATH" /usr/local/bin/k > /dev/null 2>&1 + +# 统一的错误提示 +err_msg() { + echo -e "${hong}$*${bai}" >&2 +} + +# 新建账户用:与 adduser 默认 NAME_REGEX 一致,只允许小写 +valid_username() { + [ "${#1}" -le 32 ] && [[ "$1" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]] +} + +# 仅校验字符集:useradd 允许含大写字母的用户名,操作已有账户时沿用新建账户 +# 的严格规则会让这类账户无法通过菜单管理。白名单仍要保留,避免把任意字符串 +# 写进 sudoers 或拼进文件路径。 +valid_username_chars() { + [ -n "$1" ] && [ "${#1}" -le 32 ] && [[ "$1" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*[$]?$ ]] +} + +# 授予权限等场景用:额外要求账户真实存在。比对 id -un 而非只看 id 的退出码, +# 是为了挡掉直接传 UID 数字的情况(id 0 会成功并解析成 root)。 +valid_existing_username() { + valid_username_chars "$1" && [ "$(id -un "$1" 2>/dev/null)" = "$1" ] +} + +valid_hostname() { + local hostname="$1" label + local hostname_labels=() + [ -n "$hostname" ] && [ "${#hostname}" -le 253 ] || return 1 + [[ "$hostname" != .* && "$hostname" != *. && "$hostname" != *..* ]] || return 1 + IFS=. read -r -a hostname_labels <<< "$hostname" + for label in "${hostname_labels[@]}"; do + [ -n "$label" ] && [ "${#label}" -le 63 ] && + [[ "$label" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]] || return 1 + done +} + +grant_sudo() { + local username="$1" sudoers_file="/etc/sudoers.d/kejilion-$1" tmp_sudoers + if ! valid_existing_username "$username"; then + err_msg "用户不存在或用户名格式不合法: $username" + return 1 + fi + if ! command -v visudo &>/dev/null; then + install sudo || return 1 + fi + mkdir -p /etc/sudoers.d || return 1 + tmp_sudoers=$(mktemp /etc/sudoers.d/.kejilion.XXXXXX) || return 1 + if ! printf '%s ALL=(ALL:ALL) ALL\n' "$username" > "$tmp_sudoers" || + ! chmod 440 "$tmp_sudoers" || + ! visudo -cf "$tmp_sudoers" >/dev/null; then + rm -f "$tmp_sudoers" + err_msg "sudoers 配置校验失败,未授予 sudo 权限。" + return 1 + fi + if ! mv -f "$tmp_sudoers" "$sudoers_file"; then + rm -f "$tmp_sudoers" + err_msg "写入 $sudoers_file 失败。" + return 1 + fi +} + +revoke_sudo() { + local username="$1" sudoers_file="/etc/sudoers.d/kejilion-$1" tmp_sudoers legacy_rule + # 这里只校验字符集,不要求账户仍然存在:账户已被删除、只剩 sudoers + # 残留规则时,仍应允许清理。 + if ! valid_username_chars "$username"; then + err_msg "用户名格式不合法: $username" + return 1 + fi + if ! command -v visudo &>/dev/null; then + err_msg "未找到 visudo,无法安全修改 sudoers。" + return 1 + fi + + # 兼容旧版本直接追加到 /etc/sudoers 的授权;没有旧规则时不重写主配置。 + legacy_rule="$username ALL=(ALL:ALL) ALL" + if grep -Fqx -- "$legacy_rule" /etc/sudoers; then + tmp_sudoers=$(mktemp /etc/.sudoers.kejilion.XXXXXX) || return 1 + awk -v rule="$legacy_rule" '$0 != rule' /etc/sudoers > "$tmp_sudoers" || { + rm -f "$tmp_sudoers" + return 1 + } + if ! chmod --reference=/etc/sudoers "$tmp_sudoers" 2>/dev/null || + ! chown --reference=/etc/sudoers "$tmp_sudoers" 2>/dev/null || + ! visudo -cf "$tmp_sudoers" >/dev/null || + ! mv -f "$tmp_sudoers" /etc/sudoers; then + rm -f "$tmp_sudoers" + err_msg "sudoers 配置校验或写入失败,未修改主配置。" + return 1 + fi + fi + rm -f "$sudoers_file" || { + err_msg "删除 $sudoers_file 失败。" + return 1 + } +} + +# 下载文件,失败时给出明确提示并返回非零 +download_file() { + local url="$1" dest="$2" + if curl -fsSL --retry 2 --max-time 60 -o "$dest" "$url"; then + return 0 + fi + err_msg "下载失败: $url" + return 1 +} + +update_limit_cron() { + local action="$1" limit_script="$2" current_cron new_cron cron_error + current_cron=$(mktemp) || return 1 + new_cron=$(mktemp) || { + rm -f "$current_cron" + return 1 + } + cron_error=$(mktemp) || { + rm -f "$current_cron" "$new_cron" + return 1 + } + if ! crontab -l > "$current_cron" 2> "$cron_error"; then + if grep -qi 'no crontab' "$cron_error"; then + : > "$current_cron" + else + rm -f "$current_cron" "$new_cron" "$cron_error" + err_msg "无法读取现有 crontab,已取消修改。" + return 1 + fi + fi + rm -f "$cron_error" + + if ! awk -v script="$limit_script" ' + $0 == "# KEJILION_LIMIT_BEGIN" { + if (in_block) exit 2 + in_block = 1 + next + } + $0 == "# KEJILION_LIMIT_END" { + if (!in_block) exit 2 + in_block = 0 + next + } + in_block { next } + $0 == "* * * * * ~/Limiting_Shut_down.sh" { next } + $0 == "* * * * * " script { next } + $0 == "0 1 1 * * reboot" { next } + { print } + END { if (in_block) exit 2 } + ' "$current_cron" > "$new_cron"; then + rm -f "$current_cron" "$new_cron" + err_msg "检测到不完整的限流 crontab 标记,已保留原定时任务。" + return 1 + fi + + if [ "$action" = "enable" ]; then + { + echo "# KEJILION_LIMIT_BEGIN" + printf '* * * * * /bin/bash %q\n' "$limit_script" + echo "0 1 1 * * /sbin/reboot" + echo "# KEJILION_LIMIT_END" + } >> "$new_cron" + elif [ "$action" != "disable" ]; then + rm -f "$current_cron" "$new_cron" + err_msg "未知的限流定时任务操作: $action" + return 1 + fi + + if crontab "$new_cron"; then + rm -f "$current_cron" "$new_cron" + return 0 + fi + rm -f "$current_cron" "$new_cron" + return 1 +} + +# 版本号比较:$1 严格大于 $2 时返回 0,用于避免自更新把脚本降级 +version_gt() { + [ "$1" = "$2" ] && return 1 + local IFS=. i x y + local a=($1) b=($2) + for ((i = 0; i < ${#a[@]} || i < ${#b[@]}; i++)); do + x=${a[i]:-0}; y=${b[i]:-0} + x=${x//[!0-9]/}; y=${y//[!0-9]/} + x=${x:-0}; y=${y:-0} + [ "$x" -gt "$y" ] && return 0 + [ "$x" -lt "$y" ] && return 1 + done + return 1 +} @@ -39,39 +240,38 @@ ipv6_address=$(curl -s --max-time 1 ipv6.ip.sb) +package_installed() { + [ "$(dpkg-query -W -f='${db:Status-Abbrev}' "$1" 2>/dev/null)" = "ii " ] +} + install() { if [ $# -eq 0 ]; then echo "未提供软件包参数!" return 1 fi + local package + local missing_packages=() for package in "$@"; do - if ! command -v "$package" &>/dev/null; then - echo "正在安装 $package..." - if command -v dnf &>/dev/null; then - dnf -y update && dnf install -y "$package" - elif command -v yum &>/dev/null; then - yum -y update && yum -y install "$package" - elif command -v apt &>/dev/null; then - apt update -y && apt install -y "$package" - elif command -v apk &>/dev/null; then - apk update && apk add "$package" - else - echo "未知的包管理器!" - return 1 - fi - else + if package_installed "$package"; then echo "$package 已经安装。" + else + missing_packages+=("$package") fi done - return 0 -} + [ "${#missing_packages[@]}" -gt 0 ] || return 0 + if ! command -v apt-get &>/dev/null; then + err_msg "未找到 apt-get,当前系统环境不受支持。" + return 1 + fi - -install_dependency() { - clear - install wget socat unzip tar + echo "正在安装: ${missing_packages[*]}" + if apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -- "${missing_packages[@]}"; then + return 0 + fi + err_msg "安装失败: ${missing_packages[*]}" + return 1 } @@ -81,23 +281,13 @@ remove() { return 1 fi - for package in "$@"; do - echo "正在卸载 $package..." - if command -v dnf &>/dev/null; then - dnf remove -y "${package}*" - elif command -v yum &>/dev/null; then - yum remove -y "${package}*" - elif command -v apt &>/dev/null; then - apt purge -y "${package}*" - elif command -v apk &>/dev/null; then - apk del "${package}*" - else - echo "未知的包管理器!" - return 1 - fi - done + if ! command -v apt-get &>/dev/null; then + err_msg "未找到 apt-get,当前系统环境不受支持。" + return 1 + fi - return 0 + echo "正在卸载: $*" + DEBIAN_FRONTEND=noninteractive apt-get purge -y -- "$@" } @@ -109,92 +299,156 @@ break_end() { clear } +# 用当前脚本自身替换进程,回到主菜单。 +# 用 exec 而非再开子进程,既不依赖 /usr/local/bin/k 是否存在(非 root 时 cp 会失败), +# 也避免每次“返回主菜单”都叠加一层进程。 kejilion() { - k - exit + exec bash "$SCRIPT_PATH" } install_add_docker() { - if [ -f "/etc/alpine-release" ]; then - apk update - apk add docker docker-compose - rc-update add docker default - service docker start - else - country=$(curl -s ipinfo.io/country) - if [ "$country" = "CN" ]; then - cd ~ - curl -sS -O "${GITHUB_RAW_URL}/kejilion/docker/main/install" && chmod +x install - sh install --mirror Aliyun - rm -f install - cat > /etc/docker/daemon.json << EOF -{ - "registry-mirrors": ["https://docker.kejilion.pro"] -} -EOF - else - curl -fsSL https://get.docker.com | sh - fi - systemctl start docker - systemctl enable docker + local installer + installer=$(mktemp) || return 1 + if ! download_file "https://get.docker.com" "$installer" || + ! sh "$installer"; then + rm -f "$installer" + err_msg "Docker 安装失败。" + return 1 + fi + rm -f "$installer" + if ! systemctl enable --now docker; then + err_msg "Docker 服务启动失败。" + return 1 fi - sleep 2 } install_docker() { if ! command -v docker &>/dev/null; then - install_add_docker - else - echo "Docker环境已经安装" + install_add_docker || return 1 + elif ! docker info >/dev/null 2>&1; then + if ! systemctl enable --now docker || ! docker info >/dev/null 2>&1; then + err_msg "Docker 已安装,但守护进程无法启动。" + return 1 + fi fi + echo "Docker环境已经就绪" } add_swap() { - # 获取当前系统中所有的 swap 分区 - swap_partitions=$(grep -E '^/dev/' /proc/swaps | awk '{print $1}') - - # 遍历并删除所有的 swap 分区 - for partition in $swap_partitions; do - swapoff "$partition" - wipefs -a "$partition" # 清除文件系统标识符 - mkswap -f "$partition" - done - - # 确保 /swapfile 不再被使用 - swapoff /swapfile - - # 删除旧的 /swapfile - rm -f /swapfile - - # 创建新的 swap 分区 - dd if=/dev/zero of=/swapfile bs=1M count=$new_swap - chmod 600 /swapfile - mkswap /swapfile - swapon /swapfile - - if [ -f /etc/alpine-release ]; then - echo "/swapfile swap swap defaults 0 0" >> /etc/fstab - echo "nohup swapon /swapfile" >> /etc/local.d/swap.start - chmod +x /etc/local.d/swap.start - rc-update add local - else - echo "/swapfile swap swap defaults 0 0" >> /etc/fstab + # 先校验大小,避免把已有的 /swapfile 删掉之后才发现参数不合法 + if ! [[ "$new_swap" =~ ^[0-9]+$ ]] || [ "$new_swap" -le 0 ]; then + err_msg "虚拟内存大小必须是大于 0 的整数(单位 MB),当前输入: '${new_swap}'" + return 1 fi - echo -e "虚拟内存大小已调整为${huang}${new_swap}${bai}MB" + # 先完整创建新文件,成功后再替换旧 swapfile,避免磁盘不足时丢失原配置 + local new_file=/swapfile.new old_file=/swapfile.old old_swap_active=0 fstab_tmp + if [ -e "$old_file" ]; then + err_msg "检测到未处理的恢复文件 $old_file,请先人工确认后再重试。" + return 1 + fi + if [ -e /swapfile ] && [ ! -f /swapfile ]; then + err_msg "检测到 /swapfile 不是普通文件,已取消调整。" + return 1 + fi + rm -f "$new_file" + if ! dd if=/dev/zero of="$new_file" bs=1M count="$new_swap" status=none || + ! chmod 600 "$new_file" || + ! mkswap "$new_file" >/dev/null; then + err_msg "创建新 swapfile 失败,请检查磁盘剩余空间。" + rm -f "$new_file" + return 1 + fi + + if awk 'NR > 1 && $1 == "/swapfile" { found = 1 } END { exit !found }' /proc/swaps; then + old_swap_active=1 + if ! swapoff /swapfile; then + err_msg "无法停用当前 swapfile,已保留原配置。" + rm -f "$new_file" + return 1 + fi + fi + if [ -f /swapfile ] && ! mv /swapfile "$old_file"; then + err_msg "无法备份当前 swapfile,已保留原配置。" + rm -f "$new_file" + [ "$old_swap_active" -eq 0 ] || swapon /swapfile 2>/dev/null + return 1 + fi + if mv "$new_file" /swapfile && swapon /swapfile; then + : + else + err_msg "启用新 swapfile 失败,正在恢复原文件。" + rm -f "$new_file" + swapoff /swapfile 2>/dev/null + rm -f /swapfile + if [ -f "$old_file" ]; then + mv "$old_file" /swapfile + [ "$old_swap_active" -eq 0 ] || swapon /swapfile 2>/dev/null + fi + return 1 + fi + + # 原子更新 fstab,并把重复的 /swapfile 记录归并为一条。 + fstab_tmp=$(mktemp /etc/.fstab.kejilion.XXXXXX) || true + if [ -z "$fstab_tmp" ] || + ! awk ' + $1 == "/swapfile" { + if (!seen) print "/swapfile swap swap defaults 0 0" + seen = 1 + next + } + { print } + END { if (!seen) print "/swapfile swap swap defaults 0 0" } + ' /etc/fstab > "$fstab_tmp" || + ! chmod --reference=/etc/fstab "$fstab_tmp" 2>/dev/null || + ! chown --reference=/etc/fstab "$fstab_tmp" 2>/dev/null || + ! mv -f "$fstab_tmp" /etc/fstab; then + [ -z "$fstab_tmp" ] || rm -f "$fstab_tmp" + err_msg "写入 /etc/fstab 失败,正在恢复原 swapfile。" + swapoff /swapfile 2>/dev/null + rm -f /swapfile + if [ -f "$old_file" ]; then + mv "$old_file" /swapfile + [ "$old_swap_active" -eq 0 ] || swapon /swapfile 2>/dev/null + fi + return 1 + fi + if ! rm -f "$old_file"; then + err_msg "新 swapfile 已启用,但无法删除恢复文件 $old_file。" + return 1 + fi + + echo -e "swapfile 已设置为${huang}${new_swap}${bai}MB(若系统另有 swap 分区,总量会大于该值)" +} + +# 兜底场景专用:只在现有 swap 不足时才调整。 +# 跑测试/装内核前的 swap 只是给低内存机器补位,直接调 add_swap 会把已有的 +# 4G swap 缩成 1G。菜单里用户显式指定大小的地方仍应直接调 add_swap, +# 否则就无法主动调小。 +ensure_swap() { + local current + current=$(LC_ALL=C free -m | awk '/^Swap:/{print $2}') + current=${current:-0} + if [[ "$current" =~ ^[0-9]+$ ]] && [[ "$new_swap" =~ ^[0-9]+$ ]] && [ "$current" -ge "$new_swap" ]; then + echo "当前虚拟内存 ${current}MB 已满足需求(目标 ${new_swap}MB),跳过调整。" + return 0 + fi + add_swap } cluster_python3() { - cd ~/cluster/ - curl -sS -O "${GITHUB_RAW_URL}/kejilion/python-for-vps/main/cluster/${py_task}" - python3 ~/cluster/$py_task + if [ ! -d ~/cluster ]; then + err_msg "集群环境尚未安装,请先执行“1. 安装集群环境”。" + return 1 + fi + download_file "${GITHUB_RAW_URL}/kejilion/python-for-vps/main/cluster/${py_task}" ~/cluster/"$py_task" || return 1 + python3 ~/cluster/"$py_task" } - f2b_status() { docker restart fail2ban sleep 3 @@ -202,7 +456,19 @@ f2b_status() { } f2b_status_xxx() { - docker exec -it fail2ban fail2ban-client status $xxx + docker exec -it fail2ban fail2ban-client status "$xxx" +} + +# 等待容器把配置目录创建出来,原先固定 sleep 3 在慢机器上会导致 +# cd 失败、配置文件被写到错误的目录 +f2b_wait_config_dir() { + local dir="$1" i + for i in $(seq 1 30); do + [ -d "$dir" ] && return 0 + sleep 1 + done + err_msg "等待 fail2ban 配置目录超时: $dir" + return 1 } f2b_install_sshd() { @@ -219,38 +485,23 @@ f2b_install_sshd() { -v /path/to/fail2ban/config:/config \ -v /var/log:/var/log:ro \ --restart unless-stopped \ - lscr.io/linuxserver/fail2ban:latest + lscr.io/linuxserver/fail2ban:latest || { + err_msg "Fail2Ban 容器启动失败。" + return 1 + } - sleep 3 - if grep -q 'Alpine' /etc/issue; then - cd /path/to/fail2ban/config/fail2ban/filter.d - curl -sS -O "${GITHUB_RAW_URL}/kejilion/config/main/fail2ban/alpine-sshd.conf" - curl -sS -O "${GITHUB_RAW_URL}/kejilion/config/main/fail2ban/alpine-sshd-ddos.conf" - cd /path/to/fail2ban/config/fail2ban/jail.d/ - curl -sS -O "${GITHUB_RAW_URL}/kejilion/config/main/fail2ban/alpine-ssh.conf" - elif grep -qi 'CentOS' /etc/redhat-release; then - cd /path/to/fail2ban/config/fail2ban/jail.d/ - curl -sS -O "${GITHUB_RAW_URL}/kejilion/config/main/fail2ban/centos-ssh.conf" - else - install rsyslog - systemctl start rsyslog - systemctl enable rsyslog - cd /path/to/fail2ban/config/fail2ban/jail.d/ - curl -sS -O "${GITHUB_RAW_URL}/kejilion/config/main/fail2ban/linux-ssh.conf" - fi + local f2b_url="${GITHUB_RAW_URL}/kejilion/config/main/fail2ban" + local jail_d=/path/to/fail2ban/config/fail2ban/jail.d + + f2b_wait_config_dir "$jail_d" || return 1 + install rsyslog || return 1 + systemctl enable --now rsyslog || return 1 + download_file "${f2b_url}/linux-ssh.conf" "${jail_d}/linux-ssh.conf" || return 1 } f2b_sshd() { - if grep -q 'Alpine' /etc/issue; then - xxx=alpine-sshd - f2b_status_xxx - elif grep -qi 'CentOS' /etc/redhat-release; then - xxx=centos-sshd - f2b_status_xxx - else - xxx=linux-sshd - f2b_status_xxx - fi + xxx=linux-sshd + f2b_status_xxx } @@ -278,8 +529,18 @@ server_reboot() { } output_status() { + # 跳过 lo 等回环/虚拟接口,否则本机内部流量会被计入总量, + # 导致“限流自动关机”提前触发 output=$(awk 'BEGIN { rx_total = 0; tx_total = 0 } - NR > 2 { rx_total += $2; tx_total += $10 } + NR > 2 { + # 以冒号切分,兼容接口名较长时数值与冒号粘连的内核输出格式 + idx = index($0, ":"); + if (idx == 0) next; + iface = substr($0, 1, idx - 1); gsub(/[ \t]/, "", iface); + if (iface == "lo" || iface ~ /^(docker|veth|br-|virbr|tun|tap|wg|tailscale|zt)/) next; + split(substr($0, idx + 1), f, " "); + rx_total += f[1]; tx_total += f[9] + } END { rx_units = "Bytes"; tx_units = "Bytes"; @@ -298,98 +559,61 @@ output_status() { current_timezone() { - if grep -q 'Alpine' /etc/issue; then - date +"%Z %z" - else - timedatectl | grep "Time zone" | awk '{print $3}' - fi - + timedatectl show --property=Timezone --value 2>/dev/null } set_timedate() { - shiqu="$1" - if grep -q 'Alpine' /etc/issue; then - install tzdata - cp /usr/share/zoneinfo/${shiqu} /etc/localtime - hwclock --systohc - else - timedatectl set-timezone ${shiqu} - fi + local shiqu="$1" + timedatectl set-timezone "$shiqu" } linux_update() { - if command -v dnf &>/dev/null; then - dnf -y update - elif command -v yum &>/dev/null; then - yum -y update - elif command -v apt &>/dev/null; then - DEBIAN_FRONTEND=noninteractive apt update -y - DEBIAN_FRONTEND=noninteractive apt full-upgrade -y - elif command -v apk &>/dev/null; then - apk update && apk upgrade - else - echo "未知的包管理器!" + if ! command -v apt-get &>/dev/null; then + err_msg "未找到 apt-get,当前系统环境不受支持。" return 1 fi + apt-get update && DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y } linux_clean() { - if command -v dnf &>/dev/null; then - dnf autoremove -y - dnf clean all - journalctl --rotate - journalctl --vacuum-time=1s - journalctl --vacuum-size=50M - dnf remove $(dnf repoquery --installonly --latest-limit=-1 -q) -y - elif command -v yum &>/dev/null; then - yum autoremove -y - yum clean all - journalctl --rotate - journalctl --vacuum-time=1s - journalctl --vacuum-size=50M - yum remove $(rpm -q kernel | grep -v $(uname -r)) -y - elif command -v apt &>/dev/null; then - apt autoremove --purge -y - apt clean -y - apt autoclean -y - apt remove --purge $(dpkg -l | awk '/^rc/ {print $2}') -y - journalctl --rotate - journalctl --vacuum-time=1s - journalctl --vacuum-size=50M - apt remove --purge $(dpkg -l | awk '/^ii linux-(image|headers)-[^ ]+/{print $2}' | grep -v $(uname -r | sed 's/-.*//') | xargs) -y - elif command -v apk &>/dev/null; then - apk del --purge $(apk info --installed | awk '{print $1}' | grep -v $(apk info --available | awk '{print $1}')) - apk autoremove - apk cache clean - rm -rf /var/log/* - rm -rf /var/cache/apk/* - else - echo "未知的包管理器!" + local old_pkgs + if ! command -v apt-get &>/dev/null; then + err_msg "未找到 apt-get,当前系统环境不受支持。" return 1 fi + DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y + apt-get clean + old_pkgs=$(dpkg -l | awk '/^rc/ {print $2}') + [ -n "$old_pkgs" ] && DEBIAN_FRONTEND=noninteractive apt-get purge -y $old_pkgs + journalctl --rotate + journalctl --vacuum-time=1s + journalctl --vacuum-size=50M + # 不自动批量删除内核,保留当前内核和可启动的回退版本 } -bbr_on() { - -cat > /etc/sysctl.conf << EOF -net.core.default_qdisc=fq_pie -net.ipv4.tcp_congestion_control=bbr -EOF -sysctl -p - +# 只增量修改指定的内核参数,不再用 > 整体覆盖 /etc/sysctl.conf, +# 否则用户原有的全部内核调优配置都会丢失 +set_sysctl() { + local key="$1" value="$2" conf=/etc/sysctl.conf escaped_key + touch "$conf" || return 1 + escaped_key=${key//./\\.} + if grep -qE "^[[:space:]]*#?[[:space:]]*${escaped_key}[[:space:]]*=" "$conf"; then + sed -i "s|^[[:space:]]*#\?[[:space:]]*${escaped_key}[[:space:]]*=.*|${key}=${value}|" "$conf" + else + printf '%s=%s\n' "$key" "$value" >> "$conf" + fi } - set_dns() { # 检查机器是否有IPv6地址 @@ -431,221 +655,90 @@ clear -dd_xitong() { - dd_xitong_1() { - echo -e "重装后初始用户名: ${huang}root${bai} 初始密码: ${huang}LeitboGi0ro${bai} 初始端口: ${huang}22${bai}" - echo -e "按任意键继续..." - read -n 1 -s -r -p "" - install wget - wget --no-check-certificate -qO InstallNET.sh "${GITHUB_RAW_URL}/leitbogioro/Tools/master/Linux_reinstall/InstallNET.sh" && chmod a+x InstallNET.sh - } - dd_xitong_2() { - echo -e "重装后初始用户名: ${huang}Administrator${bai} 初始密码: ${huang}Teddysun.com${bai} 初始端口: ${huang}3389${bai}" - echo -e "按任意键继续..." - read -n 1 -s -r -p "" - install wget - wget --no-check-certificate -qO InstallNET.sh "${GITHUB_RAW_URL}/leitbogioro/Tools/master/Linux_reinstall/InstallNET.sh" && chmod a+x InstallNET.sh - } - - dd_xitong_3() { - echo -e "重装后初始用户名: ${huang}root${bai} 初始密码: ${huang}123@@@${bai} 初始端口: ${huang}22${bai}" - echo -e "按任意键继续..." - read -n 1 -s -r -p "" - curl -O "${GITHUB_RAW_URL}/bin456789/reinstall/main/reinstall.sh" - } - - dd_xitong_4() { - echo -e "重装后初始用户名: ${huang}Administrator${bai} 初始密码: ${huang}123@@@${bai} 初始端口: ${huang}3389${bai}" - echo -e "按任意键继续..." - read -n 1 -s -r -p "" - curl -O "${GITHUB_RAW_URL}/bin456789/reinstall/main/reinstall.sh" - } - - while true; do - root_use - echo "请备份数据,将为你重装系统,预计花费15分钟。" - echo -e "${hui}感谢MollyLau大佬和bin456789大佬的脚本支持!${bai} " - echo "------------------------" - echo "1. Debian 12" - echo "2. Debian 11" - echo "3. Debian 10" - echo "4. Debian 9" - echo "------------------------" - echo "11. Ubuntu 24.04" - echo "12. Ubuntu 22.04" - echo "13. Ubuntu 20.04" - echo "14. Ubuntu 18.04" - echo "------------------------" - echo "21. CentOS 9" - echo "22. CentOS 8" - echo "23. CentOS 7" - echo "------------------------" - echo "31. Alpine Linux" - echo "32. Rocky Linux" - echo "33. Alma Linux" - echo "34. Fedora Linux" - echo "------------------------" - echo "41. Windows 11" - echo "42. Windows 10" - echo "43. Windows 7" - echo "44. Windows Server 2022" - echo "45. Windows Server 2019" - echo "46. Windows Server 2016" - echo "------------------------" - echo "0. 返回上一级选单" - echo "------------------------" - read -p "请选择要重装的系统: " sys_choice - case "$sys_choice" in - 1) - dd_xitong_1 - bash InstallNET.sh -debian 12 - reboot - exit - ;; - 2) - dd_xitong_1 - bash InstallNET.sh -debian 11 - reboot - exit - ;; - 3) - dd_xitong_1 - bash InstallNET.sh -debian 10 - reboot - exit - ;; - 4) - dd_xitong_1 - bash InstallNET.sh -debian 9 - reboot - exit - ;; - 11) - dd_xitong_1 - bash InstallNET.sh -ubuntu 24.04 - reboot - exit - ;; - 12) - dd_xitong_1 - bash InstallNET.sh -ubuntu 22.04 - reboot - exit - ;; - 13) - dd_xitong_1 - bash InstallNET.sh -ubuntu 20.04 - reboot - exit - ;; - 14) - dd_xitong_1 - bash InstallNET.sh -ubuntu 18.04 - reboot - exit - ;; - 21) - dd_xitong_1 - bash InstallNET.sh -centos 9 - reboot - exit - ;; - 22) - dd_xitong_1 - bash InstallNET.sh -centos 8 - reboot - exit - ;; - 23) - dd_xitong_1 - bash InstallNET.sh -centos 7 - reboot - exit - ;; - 31) - dd_xitong_1 - bash InstallNET.sh -alpine - reboot - exit - ;; - 32) - dd_xitong_1 - bash InstallNET.sh -rockylinux - reboot - exit - ;; - 33) - dd_xitong_3 - bash reinstall.sh alma - reboot - exit - ;; - 34) - dd_xitong_3 - bash reinstall.sh fedora - reboot - exit - ;; - 41) - dd_xitong_2 - bash InstallNET.sh -windows 11 -lang "cn" - reboot - exit - ;; - 42) - dd_xitong_2 - bash InstallNET.sh -windows 10 -lang "cn" - reboot - exit - ;; - 43) - dd_xitong_4 - URL="https://massgrave.dev/windows_7_links" - web_content=$(wget -q -O - "$URL") - iso_link=$(echo "$web_content" | grep -oP '(?<=href=")[^"]*cn[^"]*windows_7[^"]*professional[^"]*x64[^"]*\.iso') - # bash reinstall.sh windows --image-name 'Windows 7 Professional' --lang zh-cn - # bash reinstall.sh windows --iso='$iso_link' --image-name='Windows 7 PROFESSIONAL' - bash reinstall.sh windows --iso="$iso_link" --image-name='Windows 7 PROFESSIONAL' - reboot - exit - ;; - 44) - dd_xitong_4 - URL="https://massgrave.dev/windows_server_links" - web_content=$(wget -q -O - "$URL") - iso_link=$(echo "$web_content" | grep -oP '(?<=href=")[^"]*cn[^"]*windows_server[^"]*2022[^"]*x64[^"]*\.iso') - bash reinstall.sh windows --iso="$iso_link" --image-name='Windows Server 2022 SERVERDATACENTER' - reboot - exit - ;; - 45) - dd_xitong_2 - bash InstallNET.sh -windows 2019 -lang "cn" - reboot - exit - ;; - 46) - dd_xitong_2 - bash InstallNET.sh -windows 2016 -lang "cn" - reboot - exit - ;; - 0) - break - ;; - *) - echo "无效的选择,请重新输入。" - break - ;; - esac - done +# 探测 CPU 的 x86-64 微架构等级(v1~v4),决定安装哪个 xanmod 包。 +# 下载到临时目录,避免在当前工作目录留下文件、或被 rm -f 误删同名文件 +xanmod_psabi_version() { + local tmpdir ver + tmpdir=$(mktemp -d) || return 1 + if ! download_file "${GITHUB_RAW_URL}/kejilion/sh/main/check_x86-64_psabi.sh" "${tmpdir}/check.sh"; then + rm -rf "$tmpdir" + return 1 + fi + chmod +x "${tmpdir}/check.sh" + # 用 -oE 而非 -oP,busybox grep 不支持 PCRE + ver=$("${tmpdir}/check.sh" | grep -oE 'x86-64-v[0-9]+' | grep -oE '[0-9]+$' | head -n 1) + rm -rf "$tmpdir" + [ -n "$ver" ] || return 1 + echo "$ver" } +xanmod_add_repo() { + local key_file key_tmp source_tmp + install gnupg || return 1 + key_file=$(mktemp) || return 1 + key_tmp=$(mktemp /usr/share/keyrings/.xanmod-archive-keyring.XXXXXX) || { + rm -f "$key_file" + return 1 + } + if ! download_file "${GITHUB_RAW_URL}/kejilion/sh/main/archive.key" "$key_file" || + ! gpg --dearmor --yes -o "$key_tmp" "$key_file" || + ! chmod 644 "$key_tmp" || + ! mv -f "$key_tmp" /usr/share/keyrings/xanmod-archive-keyring.gpg; then + rm -f "$key_file" "$key_tmp" + err_msg "XanMod 签名密钥下载或导入失败。" + return 1 + fi + rm -f "$key_file" + source_tmp=$(mktemp "${XANMOD_SOURCE_FILE}.XXXXXX") || return 1 + if ! printf '%s\n' 'deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] https://deb.xanmod.org releases main' > "$source_tmp" || + ! chmod 644 "$source_tmp" || + ! mv -f "$source_tmp" "$XANMOD_SOURCE_FILE"; then + rm -f "$source_tmp" + err_msg "XanMod 软件源写入失败。" + return 1 + fi +} + +xanmod_install_kernel() { + local version + if ! version=$(xanmod_psabi_version); then + err_msg "无法探测 CPU 微架构等级,已中止内核安装。" + rm -f "$XANMOD_SOURCE_FILE" + return 1 + fi + case "$version" in + 1|2|3|4) ;; + *) + err_msg "CPU 微架构等级异常: $version" + rm -f "$XANMOD_SOURCE_FILE" + return 1 + ;; + esac + if ! apt-get update; then + err_msg "XanMod 软件源更新失败。" + rm -f "$XANMOD_SOURCE_FILE" + return 1 + fi + if ! DEBIAN_FRONTEND=noninteractive apt-get install -y "linux-xanmod-x64v${version}"; then + err_msg "XanMod 内核安装失败(linux-xanmod-x64v${version})。" + rm -f "$XANMOD_SOURCE_FILE" + return 1 + fi + rm -f "$XANMOD_SOURCE_FILE" + return 0 +} + +list_xanmod_packages() { + dpkg-query -W -f='${binary:Package}\t${db:Status-Abbrev}\n' 2>/dev/null | + awk -F '\t' '$2 == "ii " && $1 ~ /xanmod/ { print $1 }' +} bbrv3() { + local fallback_kernel os_id arch + local -a xanmod_packages root_use - if dpkg -l | grep -q 'linux-xanmod'; then + if command -v dpkg-query &>/dev/null && list_xanmod_packages | grep -q .; then while true; do kernel_version=$(uname -r) @@ -663,33 +756,33 @@ bbrv3() { case $sub_choice in 1) - apt purge -y 'linux-*xanmod1*' - update-grub - - # wget -qO - https://dl.xanmod.org/archive.key | gpg --dearmor -o /usr/share/keyrings/xanmod-archive-keyring.gpg --yes - wget -qO - "${GITHUB_RAW_URL}/kejilion/sh/main/archive.key" | gpg --dearmor -o /usr/share/keyrings/xanmod-archive-keyring.gpg --yes - - # 步骤3:添加存储库 - echo 'deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] http://deb.xanmod.org releases main' | tee /etc/apt/sources.list.d/xanmod-release.list - - # version=$(wget -q https://dl.xanmod.org/check_x86-64_psabi.sh && chmod +x check_x86-64_psabi.sh && ./check_x86-64_psabi.sh | grep -oP 'x86-64-v\K\d+|x86-64-v\d+') - version=$(wget -q "${GITHUB_RAW_URL}/kejilion/sh/main/check_x86-64_psabi.sh" && chmod +x check_x86-64_psabi.sh && ./check_x86-64_psabi.sh | grep -oP 'x86-64-v\K\d+|x86-64-v\d+') - - apt update -y - apt install -y linux-xanmod-x64v$version - - echo "XanMod内核已更新。重启后生效" - rm -f /etc/apt/sources.list.d/xanmod-release.list - rm -f check_x86-64_psabi.sh* - - server_reboot - + # 先安装并写入启动菜单,避免更新失败后没有可启动的旧内核 + if xanmod_add_repo && xanmod_install_kernel && update-grub; then + echo "XanMod内核已更新,旧内核已保留作为回退。重启后生效" + server_reboot + else + err_msg "XanMod 内核更新失败,旧内核保持不变。" + fi ;; 2) - apt purge -y 'linux-*xanmod1*' - update-grub - echo "XanMod内核已卸载。重启后生效" - server_reboot + xanmod_packages=() + case "$OS_ID" in + ubuntu) fallback_kernel=linux-generic ;; + debian) fallback_kernel=linux-image-amd64 ;; + esac + mapfile -t xanmod_packages < <(list_xanmod_packages) + if [ "${#xanmod_packages[@]}" -eq 0 ]; then + err_msg "未找到已安装的 XanMod 软件包。" + elif apt-get update && + DEBIAN_FRONTEND=noninteractive apt-get install -y -- "$fallback_kernel" && + DEBIAN_FRONTEND=noninteractive apt-get purge -y -- "${xanmod_packages[@]}" && + update-grub; then + rm -f "$XANMOD_SOURCE_FILE" + echo "XanMod内核已卸载,并已确认系统回退内核可用。重启后生效" + server_reboot + else + err_msg "无法确认回退内核或卸载失败,已取消自动重启。" + fi ;; 0) break # 跳出循环,退出菜单 @@ -714,49 +807,44 @@ bbrv3() { case "$choice" in [Yy]) + # 注意:这里必须用 return,不能用 break —— 当前不在任何循环内, + # break 只会报错并继续往下执行,导致在不支持的系统上照样改内核和 sysctl if [ -r /etc/os-release ]; then - . /etc/os-release - if [ "$ID" != "debian" ] && [ "$ID" != "ubuntu" ]; then - echo "当前环境不支持,仅支持Debian和Ubuntu系统" - break + # 放到子 shell 里取值,避免污染当前 shell 的 ID/VERSION 等变量 + os_id=$(. /etc/os-release && echo "$ID") + if [ "$os_id" != "debian" ] && [ "$os_id" != "ubuntu" ]; then + err_msg "当前环境不支持,仅支持Debian和Ubuntu系统" + return 1 fi else - echo "无法确定操作系统类型" - break + err_msg "无法确定操作系统类型" + return 1 fi # 检查系统架构 - arch=$(dpkg --print-architecture) + arch=$(dpkg --print-architecture 2>/dev/null) if [ "$arch" != "amd64" ]; then - echo "当前环境不支持,仅支持x86_64架构" - break + err_msg "当前环境不支持,仅支持x86_64架构" + return 1 fi new_swap=1024 - add_swap - install wget gnupg + ensure_swap || return 1 - # wget -qO - https://dl.xanmod.org/archive.key | gpg --dearmor -o /usr/share/keyrings/xanmod-archive-keyring.gpg --yes - wget -qO - "${GITHUB_RAW_URL}/kejilion/sh/main/archive.key" | gpg --dearmor -o /usr/share/keyrings/xanmod-archive-keyring.gpg --yes - - # 步骤3:添加存储库 - echo 'deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] http://deb.xanmod.org releases main' | tee /etc/apt/sources.list.d/xanmod-release.list - - # version=$(wget -q https://dl.xanmod.org/check_x86-64_psabi.sh && chmod +x check_x86-64_psabi.sh && ./check_x86-64_psabi.sh | grep -oP 'x86-64-v\K\d+|x86-64-v\d+') - version=$(wget -q "${GITHUB_RAW_URL}/kejilion/sh/main/check_x86-64_psabi.sh" && chmod +x check_x86-64_psabi.sh && ./check_x86-64_psabi.sh | grep -oP 'x86-64-v\K\d+|x86-64-v\d+') - - apt update -y - apt install -y linux-xanmod-x64v$version + xanmod_add_repo || return 1 + xanmod_install_kernel || return 1 + update-grub || { + err_msg "更新启动菜单失败,已取消自动重启。" + return 1 + } # 步骤5:启用BBR3 - cat > /etc/sysctl.conf << EOF -net.core.default_qdisc=fq_pie -net.ipv4.tcp_congestion_control=bbr -EOF - sysctl -p + if ! set_sysctl net.core.default_qdisc fq_pie || + ! set_sysctl net.ipv4.tcp_congestion_control bbr; then + err_msg "写入 BBR3 内核参数失败,已取消自动重启。" + return 1 + fi echo "XanMod内核安装并BBR3启用成功。重启后生效" - rm -f /etc/apt/sources.list.d/xanmod-release.list - rm -f check_x86-64_psabi.sh* server_reboot ;; @@ -784,7 +872,7 @@ echo -e "${kjlan}_ _ ____ _ _ _ _ ____ _ _ " echo "|_/ |___ | | | | | | |\ | " echo "| \_ |___ _| | |___ | |__| | \| " echo " " -echo -e "${kjlan}科技lion一键脚本工具 v$sh_v (支持Ubuntu/Debian/CentOS/Alpine系统)${bai}" +echo -e "${kjlan}科技lion一键脚本工具 v$sh_v (仅支持 Ubuntu / Debian)${bai}" echo -e "${kjlan}-输入${huang}k${kjlan}可快速启动此脚本-${bai}" echo "------------------------" echo "1. 系统信息查询" @@ -825,10 +913,11 @@ case $choice in disk_info=$(df -h | awk '$NF=="/"{printf "%s/%s (%s)", $3, $2, $5}') - country=$(curl -s ipinfo.io/country) - city=$(curl -s ipinfo.io/city) + # 加超时,避免无外网时整个信息页卡住 + country=$(curl -s --max-time 5 ipinfo.io/country) + city=$(curl -s --max-time 5 ipinfo.io/city) - isp_info=$(curl -s ipinfo.io/org) + isp_info=$(curl -s --max-time 5 ipinfo.io/org) cpu_arch=$(uname -m) @@ -849,8 +938,6 @@ case $choice in os_info=$(source /etc/os-release && echo "$PRETTY_NAME") elif [ -f "/etc/debian_version" ]; then os_info="Debian $(cat /etc/debian_version)" - elif [ -f "/etc/redhat-release" ]; then - os_info=$(cat /etc/redhat-release) else os_info="Unknown" fi @@ -861,8 +948,12 @@ case $choice in current_time=$(date "+%Y-%m-%d %I:%M %p") - swap_used=$(free -m | awk 'NR==3{print $3}') - swap_total=$(free -m | awk 'NR==3{print $2}') + # 用 Swap 行定位而非固定行号;强制 C locale,否则中文系统下 free 的行标签 + # 会被本地化,/^Swap:/ 匹配不到而恒显示 0MB/0MB + swap_used=$(LC_ALL=C free -m | awk '/^Swap:/{print $3}') + swap_total=$(LC_ALL=C free -m | awk '/^Swap:/{print $2}') + swap_used=${swap_used:-0} + swap_total=${swap_total:-0} if [ "$swap_total" -eq 0 ]; then swap_percentage=0 @@ -923,56 +1014,30 @@ case $choice in 5) clear - if [ -f "/etc/alpine-release" ]; then - while true; do - clear - congestion_algorithm=$(sysctl -n net.ipv4.tcp_congestion_control) - queue_algorithm=$(sysctl -n net.core.default_qdisc) - echo "当前TCP阻塞算法: $congestion_algorithm $queue_algorithm" - - echo "" - echo "BBR管理" - echo "------------------------" - echo "1. 开启BBRv3 2. 关闭BBRv3(会重启)" - echo "------------------------" - echo "0. 返回上一级选单" - echo "------------------------" - read -p "请输入你的选择: " sub_choice - - case $sub_choice in - 1) - bbr_on - - ;; - 2) - sed -i '/net.core.default_qdisc=fq_pie/d' /etc/sysctl.conf - sed -i '/net.ipv4.tcp_congestion_control=bbr/d' /etc/sysctl.conf - sysctl -p - reboot - ;; - 0) - break # 跳出循环,退出菜单 - ;; - - *) - break # 跳出循环,退出菜单 - ;; - - esac - done - else - install wget - wget --no-check-certificate -O tcpx.sh "${GITHUB_RAW_URL}/ylx2016/Linux-NetSpeed/master/tcpx.sh" - chmod +x tcpx.sh - ./tcpx.sh + tcpx_dir=$(mktemp -d) || { + err_msg "无法创建临时目录。" + echo "按任意键继续..." + read -n 1 -s -r -p "" + continue + } + if download_file "${GITHUB_RAW_URL}/ylx2016/Linux-NetSpeed/master/tcpx.sh" "${tcpx_dir}/tcpx.sh"; then + bash "${tcpx_dir}/tcpx.sh" fi - + rm -rf "$tcpx_dir" ;; 7) clear - install wget - wget -N https://gitlab.com/fscarmen/warp/-/raw/main/menu.sh && bash menu.sh [option] [lisence/url/token] + warp_dir=$(mktemp -d) || { + err_msg "无法创建临时目录。" + echo "按任意键继续..." + read -n 1 -s -r -p "" + continue + } + if download_file "https://gitlab.com/fscarmen/warp/-/raw/main/menu.sh" "${warp_dir}/menu.sh"; then + bash "${warp_dir}/menu.sh" + fi + rm -rf "$warp_dir" ;; 8) @@ -997,7 +1062,7 @@ case $choice in echo "" echo "----硬件性能测试----------" echo "21. yabs性能测试" - echo "22. icu/gb5 CPU性能测试脚本" + echo "22. GB5 CPU性能测试脚本" echo "" echo "----综合性测试-----------" echo "31. bench性能测试" @@ -1078,21 +1143,22 @@ case $choice in 17) clear - bash <(curl -sL bash.icu/speedtest) + bash <(curl -sL "${GITHUB_RAW_URL}/i-abc/Speedtest/main/speedtest.sh") ;; 21) clear + # swap 只是低内存机器的兜底,失败不应中断测试 new_swap=1024 - add_swap + ensure_swap || echo -e "${huang}未能调整虚拟内存,将直接开始测试。${bai}" curl -sL yabs.sh | bash -s -- -i -5 ;; 22) clear new_swap=1024 - add_swap - bash <(curl -sL bash.icu/gb5) + ensure_swap || echo -e "${huang}未能调整虚拟内存,将直接开始测试。${bai}" + bash <(curl -sL "${GITHUB_RAW_URL}/i-abc/GB5/main/gb5-test.sh") ;; 31) @@ -1125,8 +1191,7 @@ case $choice in echo "------------------------" echo "1. 设置脚本启动快捷键 2. 修改登录密码" echo "4. 安装Python指定版本" - echo "7. 优化DNS地址 8. 一键重装系统" - echo "10. 切换优先ipv4/ipv6" + echo "7. 优化DNS地址 10. 切换优先ipv4/ipv6" echo "------------------------" echo "11. 查看端口占用状态 12. 修改虚拟内存大小" echo "13. 用户管理 14. 用户/密码生成器" @@ -1148,10 +1213,14 @@ case $choice in case $sub_choice in 1) clear - read -p "请输入你的快捷按键: " kuaijiejian - echo "alias $kuaijiejian='~/kejilion.sh'" >> ~/.bashrc - source ~/.bashrc - echo "快捷键已设置" + read -r -p "请输入你的快捷按键: " kuaijiejian + if ! [[ "$kuaijiejian" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + err_msg "快捷键只能包含字母、数字和下划线,且不能以数字开头。" + else + sed -i "/^alias ${kuaijiejian}=/d" "$HOME/.bashrc" 2>/dev/null + printf 'alias %s=%q\n' "$kuaijiejian" "bash $SCRIPT_PATH" >> "$HOME/.bashrc" + echo "快捷键已设置,请重新登录终端或执行 source ~/.bashrc 后生效" + fi ;; 2) @@ -1169,38 +1238,37 @@ case $choice in echo "------------" read -p "输入你要安装的python版本号: " py_new_v - if ! grep -q 'export PYENV_ROOT="\$HOME/.pyenv"' ~/.bashrc; then - if command -v yum &>/dev/null; then - yum update -y && yum install git -y - yum groupinstall "Development Tools" -y - yum install openssl-devel bzip2-devel libffi-devel ncurses-devel zlib-devel readline-devel sqlite-devel xz-devel findutils -y + if ! [[ "$py_new_v" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + err_msg "版本号格式不正确,示例: 3.12 或 3.12.4" + break + fi - curl -O https://www.openssl.org/source/openssl-1.1.1u.tar.gz - tar -xzf openssl-1.1.1u.tar.gz - cd openssl-1.1.1u - ./config --prefix=/usr/local/openssl --openssldir=/usr/local/openssl shared zlib - make - make install - echo "/usr/local/openssl/lib" > /etc/ld.so.conf.d/openssl-1.1.1u.conf - ldconfig -v - cd .. - - export LDFLAGS="-L/usr/local/openssl/lib" - export CPPFLAGS="-I/usr/local/openssl/include" - export PKG_CONFIG_PATH="/usr/local/openssl/lib/pkgconfig" - - elif command -v apt &>/dev/null; then - apt update -y && apt install git -y - apt install build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev libgdbm-dev libnss3-dev libedit-dev -y - elif command -v apk &>/dev/null; then - apk update && apk add git - apk add --no-cache bash gcc musl-dev libffi-dev openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev libc6-compat linux-headers make xz-dev build-base ncurses-dev - else - echo "未知的包管理器!" - return 1 + pyenv_installer="" + if [ ! -x "$HOME/.pyenv/bin/pyenv" ]; then + if ! install git build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ + libsqlite3-dev wget curl llvm libncurses-dev xz-utils tk-dev \ + libffi-dev liblzma-dev libgdbm-dev libnss3-dev libedit-dev; then + err_msg "Python 编译依赖安装失败。" + break fi - curl https://pyenv.run | bash + pyenv_installer=$(mktemp) || { + err_msg "无法创建 pyenv 安装临时文件。" + break + } + if ! download_file https://pyenv.run "$pyenv_installer" || ! bash "$pyenv_installer"; then + rm -f "$pyenv_installer" + err_msg "pyenv 安装失败。" + break + fi + rm -f "$pyenv_installer" + fi + + touch "$HOME/.bashrc" || { + err_msg "无法更新 $HOME/.bashrc。" + break + } + if ! grep -Fqx 'export PYENV_ROOT="$HOME/.pyenv"' "$HOME/.bashrc"; then cat << EOF >> ~/.bashrc export PYENV_ROOT="\$HOME/.pyenv" @@ -1209,23 +1277,41 @@ if [[ -d "\$PYENV_ROOT/bin" ]]; then fi eval "\$(pyenv init --path)" eval "\$(pyenv init -)" -eval "\$(pyenv virtualenv-init -)" +if pyenv commands 2>/dev/null | grep -qx virtualenv-init; then + eval "\$(pyenv virtualenv-init -)" +fi EOF fi - sleep 1 - source ~/.bashrc - sleep 1 - pyenv install $py_new_v - pyenv global $py_new_v + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + if ! command -v pyenv &>/dev/null; then + err_msg "pyenv 未安装成功。" + break + fi + eval "$(pyenv init -)" - rm -rf /tmp/python-build.* - rm -rf $(pyenv root)/cache/* - - VERSION=$(python -V 2>&1 | awk '{print $2}') - echo -e "当前python版本号: ${huang}$VERSION${bai}" + version_pattern=${py_new_v//./\\.} + if [[ "$py_new_v" =~ ^[0-9]+\.[0-9]+$ ]]; then + version_pattern="^${version_pattern}\\.[0-9]+$" + else + version_pattern="^${version_pattern}$" + fi + py_target=$(pyenv install --list 2>/dev/null | sed 's/^[[:space:]]*//' | + grep -E "$version_pattern" | sort -V | tail -n 1) + if [ -z "$py_target" ]; then + err_msg "未找到可安装的稳定版本: $py_new_v" + break + fi + if pyenv install -s "$py_target" && pyenv global "$py_target"; then + hash -r + VERSION=$("$PYENV_ROOT/shims/python" -V 2>&1 | awk '{print $2}') + echo -e "当前python版本号: ${huang}$VERSION${bai}" + else + err_msg "Python $py_target 安装或启用失败。" + fi ;; @@ -1263,7 +1349,7 @@ EOF echo "已取消" ;; *) - echo "无效的选择,请输入 Y 或 N。" + echo "无效的选择,请输入 0、1 或 2。" ;; esac @@ -1274,12 +1360,14 @@ EOF ;; - 8) - dd_xitong - ;; 10) root_use - ipv6_disabled=$(sysctl -n net.ipv6.conf.all.disable_ipv6) + # 内核未编译 IPv6 时该 sysctl 不存在,取值为空会导致下面的整数比较报错 + ipv6_disabled=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null) + if [ -z "$ipv6_disabled" ]; then + err_msg "当前内核未启用 IPv6,无法切换网络优先级。" + break + fi echo "" if [ "$ipv6_disabled" -eq 1 ]; then @@ -1314,16 +1402,24 @@ EOF 11) clear - ss -tulnape + if command -v ss &>/dev/null; then + ss -tulnape + elif command -v netstat &>/dev/null; then + netstat -tulnape + else + install iproute2 && ss -tulnape + fi ;; 12) root_use - # 获取当前交换空间信息 - swap_used=$(free -m | awk 'NR==3{print $3}') - swap_total=$(free -m | awk 'NR==3{print $2}') + # 获取当前交换空间信息(强制 C locale,避免中文系统下标签被本地化) + swap_used=$(LC_ALL=C free -m | awk '/^Swap:/{print $3}') + swap_total=$(LC_ALL=C free -m | awk '/^Swap:/{print $2}') + swap_used=${swap_used:-0} + swap_total=${swap_total:-0} if [ "$swap_total" -eq 0 ]; then swap_percentage=0 @@ -1360,11 +1456,16 @@ EOF # 显示所有用户、用户权限、用户组和是否在sudoers中 echo "用户列表" echo "----------------------------------------------------------------------------" - printf "%-24s %-34s %-20s %-10s\n" "用户名" "用户权限" "用户组" "sudo权限" - while IFS=: read -r username _ userid groupid _ _ homedir shell; do - groups=$(groups "$username" | cut -d : -f 2) - sudo_status=$(sudo -n -lU "$username" 2>/dev/null | grep -q '(ALL : ALL)' && echo "Yes" || echo "No") - printf "%-20s %-30s %-20s %-10s\n" "$username" "$homedir" "$groups" "$sudo_status" + printf "%-20s %-30s %-20s %-10s\n" "用户名" "主目录" "用户组" "sudo权限" + # /etc/passwd 共 7 个字段: 用户名:密码:UID:GID:备注:主目录:登录shell + while IFS=: read -r username _ userid groupid _ homedir shell; do + user_groups=$(groups "$username" 2>/dev/null | cut -d: -f2- | sed 's/^ *//') + if sudo -n -lU "$username" 2>/dev/null | grep -q '(ALL'; then + sudo_status="Yes" + else + sudo_status="No" + fi + printf "%-20s %-30s %-20s %-10s\n" "$username" "$homedir" "$user_groups" "$sudo_status" done < /etc/passwd @@ -1373,7 +1474,7 @@ EOF echo "------------------------" echo "1. 创建普通账户 2. 创建高级账户" echo "------------------------" - echo "3. 赋予最高权限 4. 取消最高权限" + echo "3. 授予 sudo 权限 4. 撤销脚本授予的 sudo 权限" echo "------------------------" echo "5. 删除账号" echo "------------------------" @@ -1386,42 +1487,89 @@ EOF # 提示用户输入新用户名 read -p "请输入新用户名: " new_username - # 创建新用户并设置密码 - useradd -m -s /bin/bash "$new_username" - passwd "$new_username" - - echo "操作已完成。" + if ! valid_username "$new_username"; then + err_msg "用户名格式不合法。" + elif id "$new_username" &>/dev/null; then + err_msg "用户 $new_username 已存在。" + elif useradd -m -s /bin/bash "$new_username"; then + if passwd "$new_username"; then + echo "操作已完成。" + else + if userdel -r "$new_username" >/dev/null 2>&1; then + err_msg "密码设置失败,已回滚新建账户。" + else + err_msg "密码设置失败,且新建账户无法自动回滚,请人工检查: $new_username" + fi + fi + else + err_msg "账户创建失败。" + fi ;; 2) # 提示用户输入新用户名 read -p "请输入新用户名: " new_username - # 创建新用户并设置密码 - useradd -m -s /bin/bash "$new_username" - passwd "$new_username" - - # 赋予新用户sudo权限 - echo "$new_username ALL=(ALL:ALL) ALL" | sudo tee -a /etc/sudoers - - echo "操作已完成。" + if ! valid_username "$new_username"; then + err_msg "用户名格式不合法。" + elif id "$new_username" &>/dev/null; then + err_msg "用户 $new_username 已存在。" + elif useradd -m -s /bin/bash "$new_username"; then + if passwd "$new_username" && grant_sudo "$new_username"; then + echo "操作已完成。" + else + rm -f "/etc/sudoers.d/kejilion-$new_username" + if userdel -r "$new_username" >/dev/null 2>&1; then + err_msg "密码设置或 sudo 授权失败,已回滚新建账户。" + else + err_msg "密码设置或 sudo 授权失败,且新建账户无法自动回滚,请人工检查: $new_username" + fi + fi + else + err_msg "账户创建失败,未授予 sudo 权限。" + fi ;; 3) read -p "请输入用户名: " username - # 赋予新用户sudo权限 - echo "$username ALL=(ALL:ALL) ALL" | sudo tee -a /etc/sudoers + if ! id "$username" &>/dev/null; then + err_msg "用户 $username 不存在。" + elif ! valid_existing_username "$username"; then + err_msg "用户名格式不合法。" + elif grant_sudo "$username"; then + echo "操作已完成。" + fi ;; 4) read -p "请输入用户名: " username - # 从sudoers文件中移除用户的sudo权限 - sed -i "/^$username\sALL=(ALL:ALL)\sALL/d" /etc/sudoers + if revoke_sudo "$username"; then + echo "操作已完成。" + fi ;; 5) read -p "请输入要删除的用户名: " username - # 删除用户及其主目录 - userdel -r "$username" + if [ "$username" = "root" ]; then + err_msg "不允许删除 root 用户。" + elif ! id "$username" &>/dev/null; then + err_msg "用户 $username 不存在。" + elif ! valid_existing_username "$username"; then + err_msg "用户名格式不合法。" + elif [ "$username" = "$(id -un)" ]; then + err_msg "不允许删除当前正在运行脚本的用户。" + else + sudo_rule="$username ALL=(ALL:ALL) ALL" + if { [ ! -e "/etc/sudoers.d/kejilion-$username" ] && + ! grep -Fqx -- "$sudo_rule" /etc/sudoers; } || revoke_sudo "$username"; then + if userdel -r "$username"; then + echo "用户 $username 已删除。" + else + err_msg "删除用户 $username 失败。" + fi + else + err_msg "无法安全清理用户的 sudo 规则,已取消删除。" + fi + fi ;; 0) @@ -1562,21 +1710,15 @@ EOF if [[ "${answer,,}" == "y" ]]; then # 获取新的主机名 read -p "请输入新的主机名: " new_hostname - if [ -n "$new_hostname" ]; then - if [ -f /etc/alpine-release ]; then - # Alpine - echo "$new_hostname" > /etc/hostname - hostname "$new_hostname" - else - # 其他系统,如 Debian, Ubuntu, CentOS 等 - hostnamectl set-hostname "$new_hostname" - sed -i "s/$current_hostname/$new_hostname/g" /etc/hostname + if valid_hostname "$new_hostname"; then + if hostnamectl set-hostname "$new_hostname"; then systemctl restart systemd-hostnamed + echo "主机名已更改为: $new_hostname" + else + err_msg "主机名修改失败。" fi - echo "主机名已更改为: $new_hostname" else - echo "无效的主机名。未更改主机名。" - exit 1 + err_msg "主机名格式不合法。未更改主机名。" fi else echo "未更改主机名。" @@ -1585,107 +1727,271 @@ EOF 19) root_use - # 获取系统信息 - source /etc/os-release + # 直接复用脚本开头已解析好的 OS_ID,避免 source /etc/os-release + # 把 ID/VERSION/PRETTY_NAME 等一并注入当前 shell - # 定义 Ubuntu 更新源 - aliyun_ubuntu_source="http://mirrors.aliyun.com/ubuntu/" - official_ubuntu_source="http://archive.ubuntu.com/ubuntu/" - initial_ubuntu_source="" - - # 定义 Debian 更新源 - aliyun_debian_source="http://mirrors.aliyun.com/debian/" - official_debian_source="http://deb.debian.org/debian/" - initial_debian_source="" - - # 定义 CentOS 更新源 - aliyun_centos_source="http://mirrors.aliyun.com/centos/" - official_centos_source="http://mirror.centos.org/centos/" - initial_centos_source="" - - # 获取当前更新源并设置初始源 - case "$ID" in - ubuntu) - initial_ubuntu_source=$(grep -E '^deb ' /etc/apt/sources.list | head -n 1 | awk '{print $2}') - ;; - debian) - initial_debian_source=$(grep -E '^deb ' /etc/apt/sources.list | head -n 1 | awk '{print $2}') - ;; - centos) - initial_centos_source=$(awk -F= '/^baseurl=/ {print $2}' /etc/yum.repos.d/CentOS-Base.repo | head -n 1 | tr -d ' ') + case "$(dpkg --print-architecture 2>/dev/null)" in + amd64|i386) + aliyun_ubuntu_source="https://mirrors.aliyun.com/ubuntu/" + official_ubuntu_source="https://archive.ubuntu.com/ubuntu/" + official_ubuntu_security_source="https://security.ubuntu.com/ubuntu/" ;; *) - echo "未知系统,无法执行切换源脚本" - exit 1 + aliyun_ubuntu_source="https://mirrors.aliyun.com/ubuntu-ports/" + official_ubuntu_source="https://ports.ubuntu.com/ubuntu-ports/" + official_ubuntu_security_source="$official_ubuntu_source" ;; esac + aliyun_debian_source="https://mirrors.aliyun.com/debian/" + official_debian_source="https://deb.debian.org/debian/" + + list_apt_source_files() { + local file pattern + case "$OS_ID" in + ubuntu) pattern='(([^./[:space:]]+\.)?archive\.ubuntu\.com|security\.ubuntu\.com|ports\.ubuntu\.com|mirrors\.aliyun\.com)/(ubuntu|ubuntu-ports)' ;; + debian) pattern='(debian\.org|debian\.net|aliyun\.com)/(debian|debian-security)' ;; + *) return 1 ;; + esac + for file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do + [ -f "$file" ] || continue + case "$file" in + *.sources) + grep -qE '^[[:space:]]*URIs:[[:space:]]*' "$file" && + grep -qE "$pattern" "$file" && printf '%s\n' "$file" + ;; + *) + grep -qE "^[[:space:]]*(deb|deb-src)[[:space:]].*${pattern}" "$file" && printf '%s\n' "$file" + ;; + esac + done + } + + mapfile -t apt_source_files < <(list_apt_source_files) + if [ "${#apt_source_files[@]}" -eq 0 ]; then + err_msg "未找到可识别的 APT 更新源配置。" + break + fi - # 备份当前源 backup_sources() { - case "$ID" in - ubuntu) - cp /etc/apt/sources.list /etc/apt/sources.list.bak - ;; - debian) - cp /etc/apt/sources.list /etc/apt/sources.list.bak - ;; - centos) - if [ ! -f /etc/yum.repos.d/CentOS-Base.repo.bak ]; then - cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak - else - echo "备份已存在,无需重复备份" - fi - ;; - *) - echo "未知系统,无法执行备份操作" - exit 1 - ;; - esac - echo "已备份当前更新源为 /etc/apt/sources.list.bak 或 /etc/yum.repos.d/CentOS-Base.repo.bak" + local file backup created=() + for file in "${apt_source_files[@]}"; do + backup="${file}.kejilion.bak" + if [ -f "$backup" ] && [ ! -L "$backup" ]; then + # 已存在是预期结果而非错误:本函数只保存初始源,不覆盖 + echo "初始备份已存在,跳过: $backup" + continue + elif [ -e "$backup" ] || [ -L "$backup" ]; then + err_msg "备份路径不是普通文件,已取消操作: $backup" + rm -f "${created[@]}" + return 1 + fi + if cp -- "$file" "$backup"; then + created+=("$backup") + else + rm -f "$backup" "${created[@]}" + err_msg "备份更新源失败,已清理本次生成的备份。" + return 1 + fi + done + echo "初始更新源备份已就绪。" + } + + ensure_source_backup() { + local file + for file in "${apt_source_files[@]}"; do + if [ ! -f "${file}.kejilion.bak" ] || [ -L "${file}.kejilion.bak" ]; then + backup_sources || return 1 + break + fi + done + for file in "${apt_source_files[@]}"; do + [ -f "${file}.kejilion.bak" ] && [ ! -L "${file}.kejilion.bak" ] || return 1 + done } - # 还原初始更新源 restore_initial_source() { - case "$ID" in - ubuntu) - cp /etc/apt/sources.list.bak /etc/apt/sources.list - ;; - debian) - cp /etc/apt/sources.list.bak /etc/apt/sources.list - ;; - centos) - cp /etc/yum.repos.d/CentOS-Base.repo.bak /etc/yum.repos.d/CentOS-Base.repo - ;; - *) - echo "未知系统,无法执行还原操作" - exit 1 - ;; - esac - echo "已还原初始更新源" + local file saved rollback_dir ready=1 + rollback_dir=$(mktemp -d) || return 1 + for file in "${apt_source_files[@]}"; do + saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')" + if [ ! -f "${file}.kejilion.bak" ] || [ -L "${file}.kejilion.bak" ] || + ! cp -- "$file" "$saved"; then + ready=0 + break + fi + done + if [ "$ready" -eq 1 ]; then + for file in "${apt_source_files[@]}"; do + cp -- "${file}.kejilion.bak" "$file" || { ready=0; break; } + done + fi + if [ "$ready" -eq 1 ] && apt-get update; then + rm -rf "$rollback_dir" + echo "已还原初始更新源并验证成功。" + return 0 + fi + local rollback_ok=1 + for file in "${apt_source_files[@]}"; do + saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')" + if [ -f "$saved" ] && ! cp -- "$saved" "$file"; then + rollback_ok=0 + fi + done + if [ "$rollback_ok" -eq 1 ]; then + rm -rf "$rollback_dir" + err_msg "还原或验证失败,已恢复还原前的配置。" + else + err_msg "还原失败且部分配置无法回滚,事务快照保留在: $rollback_dir" + fi + return 1 } - # 函数:切换更新源 switch_source() { - case "$ID" in - ubuntu) - sed -i 's|'"$initial_ubuntu_source"'|'"$1"'|g' /etc/apt/sources.list - ;; - debian) - sed -i 's|'"$initial_debian_source"'|'"$1"'|g' /etc/apt/sources.list - ;; - centos) - sed -i "s|^baseurl=.*$|baseurl=$1|g" /etc/yum.repos.d/CentOS-Base.repo - ;; + local mode="$1" file tmp_file rollback_dir saved failed=0 processed=0 is_deb822 + case "$mode" in + mirror|official) ;; *) - echo "未知系统,无法执行切换操作" - exit 1 + err_msg "未知的更新源切换模式: $mode" + return 1 ;; esac + rollback_dir=$(mktemp -d) || return 1 + + # 先完成所有快照,再开始修改,避免快照中途失败时留下半套新配置。 + for file in "${apt_source_files[@]}"; do + saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')" + if ! cp -- "$file" "$saved"; then + failed=1 + break + fi + done + + if [ "$failed" -eq 0 ]; then + for file in "${apt_source_files[@]}"; do + tmp_file=$(mktemp "${file}.XXXXXX") || { failed=1; break; } + case "$file" in + *.sources) is_deb822=1 ;; + *) is_deb822=0 ;; + esac + if ! awk -v mode="$mode" -v distro="$OS_ID" \ + -v ubuntu_archive="$official_ubuntu_source" -v ubuntu_mirror="$aliyun_ubuntu_source" \ + -v ubuntu_security="$official_ubuntu_security_source" -v deb822="$is_deb822" \ + -v debian_archive="$official_debian_source" -v debian_mirror="$aliyun_debian_source" ' + BEGIN { + FS = "\n" + if (deb822) { + RS = "" + } + } + function replacement(uri, security) { + if (distro == "ubuntu" && uri ~ "^https?://mirrors[.]aliyun[.]com/(ubuntu|ubuntu-ports)/?$") + return mode == "mirror" ? ubuntu_mirror : (security ? ubuntu_security : ubuntu_archive) + if (distro == "ubuntu" && uri ~ "^https?://([^/]+[.])?archive[.]ubuntu[.]com/ubuntu/?$") + return mode == "mirror" ? ubuntu_mirror : ubuntu_archive + if (distro == "ubuntu" && uri ~ "^https?://security[.]ubuntu[.]com/ubuntu/?$") + return mode == "mirror" ? ubuntu_mirror : ubuntu_security + if (distro == "ubuntu" && uri ~ "^https?://ports[.]ubuntu[.]com/ubuntu-ports/?$") + return mode == "mirror" ? ubuntu_mirror : ubuntu_archive + if (distro == "debian" && uri ~ "^https?://[^/]*(debian[.]org|debian[.]net|aliyun[.]com)/debian-security/?$") + return mode == "mirror" ? "https://mirrors.aliyun.com/debian-security/" : "https://security.debian.org/debian-security/" + if (distro == "debian" && uri ~ "^https?://[^/]*(debian[.]org|debian[.]net|aliyun[.]com)/debian/?$") + return mode == "mirror" ? debian_mirror : debian_archive + return uri + } + function rewrite(line, security, fields, count, i, result) { + sub(/^[[:space:]]+/, "", line) + count = split(line, fields, /[[:space:]]+/) + result = fields[1] + for (i = 2; i <= count; i++) + result = result " " replacement(fields[i], security) + return result + } + function rewrite_continuation(line, security, fields, count, i, result) { + sub(/^[[:space:]]+/, "", line) + count = split(line, fields, /[[:space:]]+/) + result = "" + for (i = 1; i <= count; i++) + result = result " " replacement(fields[i], security) + return result + } + { + # FS="\n" 下空行的 NF 为 0,末尾输出循环不会执行, + # 不特判会导致换源后原文件里的空行被静默删除 + if (NF == 0) { + print "" + next + } + security = 0 + if (deb822) { + in_suites = 0 + for (i = 1; i <= NF; i++) { + if ($i ~ /^[[:space:]]*Suites:/) { + in_suites = 1 + if ($i ~ /-security([[:space:]]|$)/) security = 1 + } else if ($i ~ /^[[:space:]]+/) { + if (in_suites && $i ~ /-security([[:space:]]|$)/) security = 1 + } else { + in_suites = 0 + } + } + } else if ($0 ~ /-security([[:space:]]|$)/) { + security = 1 + } + in_uris = 0 + for (i = 1; i <= NF; i++) { + if ($i ~ /^[[:space:]]*URIs:[[:space:]]*/) { + $i = rewrite($i, security) + in_uris = 1 + } else if (deb822 && in_uris && $i ~ /^[[:space:]]+/) { + $i = rewrite_continuation($i, security) + } else { + in_uris = 0 + if ($i ~ /^[[:space:]]*deb(-src)?[[:space:]]/) + $i = rewrite($i, security) + } + } + for (i = 1; i <= NF; i++) + printf "%s%s", $i, (i < NF ? "\n" : (deb822 ? "\n\n" : "\n")) + } + ' "$file" > "$tmp_file"; then + rm -f "$tmp_file" + failed=1 + break + fi + if ! chmod --reference="$file" "$tmp_file" 2>/dev/null || + ! chown --reference="$file" "$tmp_file" 2>/dev/null || + ! mv -f "$tmp_file" "$file"; then + rm -f "$tmp_file" + failed=1 + break + fi + processed=$((processed + 1)) + done + fi + if [ "$failed" -eq 0 ] && [ "$processed" -eq "${#apt_source_files[@]}" ] && apt-get update; then + rm -rf "$rollback_dir" + return 0 + fi + local rollback_ok=1 + for file in "${apt_source_files[@]}"; do + saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')" + if [ -f "$saved" ] && ! cp -- "$saved" "$file"; then + rollback_ok=0 + fi + done + if [ "$rollback_ok" -eq 1 ]; then + rm -rf "$rollback_dir" + apt-get update >/dev/null 2>&1 + err_msg "新更新源验证失败,已恢复原配置。" + else + err_msg "新源验证失败且部分配置无法回滚,事务快照保留在: $rollback_dir" + fi + return 1 } # 主菜单 while true; do - case "$ID" in + case "$OS_ID" in ubuntu) echo "Ubuntu 更新源切换脚本" echo "------------------------" @@ -1694,20 +2000,16 @@ EOF echo "Debian 更新源切换脚本" echo "------------------------" ;; - centos) - echo "CentOS 更新源切换脚本" - echo "------------------------" - ;; *) - echo "未知系统,无法执行脚本" - exit 1 + err_msg "未知系统,无法执行脚本" + break ;; esac echo "1. 切换到阿里云源" echo "2. 切换到官方源" echo "------------------------" - echo "3. 备份当前更新源" + echo "3. 备份初始更新源(已备份则跳过)" echo "4. 还原初始更新源" echo "------------------------" echo "0. 返回上一级" @@ -1716,61 +2018,17 @@ EOF case $choice in 1) - backup_sources - case "$ID" in - ubuntu) - switch_source $aliyun_ubuntu_source - ;; - debian) - switch_source $aliyun_debian_source - ;; - centos) - switch_source $aliyun_centos_source - ;; - *) - echo "未知系统,无法执行切换操作" - exit 1 - ;; - esac - echo "已切换到阿里云源" + if ensure_source_backup && switch_source mirror; then + echo "已切换到阿里云源" + fi ;; 2) - backup_sources - case "$ID" in - ubuntu) - switch_source $official_ubuntu_source - ;; - debian) - switch_source $official_debian_source - ;; - centos) - switch_source $official_centos_source - ;; - *) - echo "未知系统,无法执行切换操作" - exit 1 - ;; - esac - echo "已切换到官方源" + if ensure_source_backup && switch_source official; then + echo "已切换到官方源" + fi ;; 3) backup_sources - case "$ID" in - ubuntu) - switch_source $initial_ubuntu_source - ;; - debian) - switch_source $initial_debian_source - ;; - centos) - switch_source $initial_centos_source - ;; - *) - echo "未知系统,无法执行切换操作" - exit 1 - ;; - esac - echo "已切换到初始更新源" ;; 4) restore_initial_source @@ -1793,7 +2051,7 @@ EOF while true; do clear echo "定时任务列表" - crontab -l + crontab -l 2>/dev/null echo "" echo "操作" echo "------------------------" @@ -1806,6 +2064,12 @@ EOF case $sub_choice in 1) read -p "请输入新任务的执行命令: " newquest + if [ -z "$newquest" ]; then + err_msg "执行命令不能为空。" + echo "按任意键继续..." + read -n 1 -s -r -p "" + continue + fi echo "------------------------" echo "1. 每月任务 2. 每周任务" echo "3. 每天任务 4. 每小时任务" @@ -1814,20 +2078,36 @@ EOF case $dingshi in 1) - read -p "选择每月的几号执行任务? (1-30): " day - (crontab -l ; echo "0 0 $day * * $newquest") | crontab - > /dev/null 2>&1 + read -p "选择每月的几号执行任务? (1-28): " day + if [[ "$day" =~ ^[0-9]+$ ]] && [ "$day" -ge 1 ] && [ "$day" -le 28 ]; then + (crontab -l 2>/dev/null ; echo "0 0 $day * * $newquest") | crontab - + else + err_msg "日期必须是 1 到 28 之间的整数。" + fi ;; 2) read -p "选择周几执行任务? (0-6,0代表星期日): " weekday - (crontab -l ; echo "0 0 * * $weekday $newquest") | crontab - > /dev/null 2>&1 + if [[ "$weekday" =~ ^[0-9]+$ ]] && [ "$weekday" -ge 0 ] && [ "$weekday" -le 6 ]; then + (crontab -l 2>/dev/null ; echo "0 0 * * $weekday $newquest") | crontab - + else + err_msg "星期必须是 0 到 6 之间的整数。" + fi ;; 3) read -p "选择每天几点执行任务?(小时,0-23): " hour - (crontab -l ; echo "0 $hour * * * $newquest") | crontab - > /dev/null 2>&1 + if [[ "$hour" =~ ^[0-9]+$ ]] && [ "$hour" -ge 0 ] && [ "$hour" -le 23 ]; then + (crontab -l 2>/dev/null ; echo "0 $hour * * * $newquest") | crontab - + else + err_msg "小时必须是 0 到 23 之间的整数。" + fi ;; 4) - read -p "输入每小时的第几分钟执行任务?(分钟,0-60): " minute - (crontab -l ; echo "$minute * * * * $newquest") | crontab - > /dev/null 2>&1 + read -p "输入每小时的第几分钟执行任务?(分钟,0-59): " minute + if [[ "$minute" =~ ^[0-9]+$ ]] && [ "$minute" -ge 0 ] && [ "$minute" -le 59 ]; then + (crontab -l 2>/dev/null ; echo "$minute * * * * $newquest") | crontab - + else + err_msg "分钟必须是 0 到 59 之间的整数。" + fi ;; *) break # 跳出 @@ -1836,7 +2116,12 @@ EOF ;; 2) read -p "请输入需要删除任务的关键字: " kquest - crontab -l | grep -v "$kquest" | crontab - + # 关键字为空时 grep -v "" 会匹配所有行,导致整份 crontab 被清空 + if [ -z "$kquest" ]; then + err_msg "关键字不能为空,已取消删除。" + else + crontab -l 2>/dev/null | grep -vF "$kquest" | crontab - > /dev/null 2>&1 + fi ;; 3) crontab -e @@ -1871,12 +2156,33 @@ EOF case $host_dns in 1) read -p "请输入新的解析记录 格式: 110.25.5.33 kejilion.pro : " addhost - echo "$addhost" >> /etc/hosts + if [ -z "$addhost" ]; then + err_msg "解析记录不能为空。" + else + echo "$addhost" >> /etc/hosts + fi ;; 2) read -p "请输入需要删除的解析内容关键字: " delhost - sed -i "/$delhost/d" /etc/hosts + # 关键字为空时 sed //d 会清空整个 /etc/hosts + if [ -z "$delhost" ]; then + err_msg "关键字不能为空,已取消删除。" + else + hosts_tmp=$(mktemp /etc/.hosts.kejilion.XXXXXX) || { + err_msg "无法创建 hosts 临时文件。" + continue + } + if awk -v keyword="$delhost" 'index($0, keyword) == 0' /etc/hosts > "$hosts_tmp" && + chmod --reference=/etc/hosts "$hosts_tmp" 2>/dev/null && + chown --reference=/etc/hosts "$hosts_tmp" 2>/dev/null && + mv -f "$hosts_tmp" /etc/hosts; then + echo "匹配的解析记录已删除。" + else + rm -f "$hosts_tmp" + err_msg "更新 /etc/hosts 失败,已保留原文件。" + fi + fi ;; 0) break # 跳出循环,退出菜单 @@ -1964,12 +2270,19 @@ EOF case "$choice" in [Yy]) clear - install_docker - f2b_install_sshd - - cd ~ - f2b_status - echo "Fail2Ban防御程序已开启" + if ! install_docker; then + err_msg "Docker 环境准备失败。" + break + fi + # 只有安装/配置失败才回滚:慢机器上 fail2ban socket 尚未就绪时 + # 状态查询会非零退出,此时容器其实是好的,不该被强删 + if f2b_install_sshd; then + f2b_status || err_msg "Fail2Ban 已安装,但状态查询失败,可稍后用菜单查看运行状态确认。" + echo "Fail2Ban防御程序已开启" + else + docker rm -f fail2ban >/dev/null 2>&1 + err_msg "Fail2Ban 安装或配置失败,已清理未完成的容器。" + fi ;; [Nn]) @@ -1991,45 +2304,95 @@ EOF # 检查是否存在 Limiting_Shut_down.sh 文件 if [ -f ~/Limiting_Shut_down.sh ]; then - # 获取 threshold_gb 的值 - threshold_gb=$(grep -oP 'threshold_gb=\K\d+' ~/Limiting_Shut_down.sh) - echo -e "当前设置的限流阈值为 ${hang}${threshold_gb}${bai}GB" + # 获取 threshold_gb 的值(使用 POSIX ERE,不依赖 PCRE) + threshold_gb=$(grep -oE '^threshold_gb=[0-9]+' ~/Limiting_Shut_down.sh | head -n 1 | cut -d= -f2) + if [ -n "$threshold_gb" ]; then + echo -e "当前设置的限流阈值为 ${huang}${threshold_gb}${bai}GB" + else + err_msg "现有限流脚本的阈值配置异常。" + fi else - echo -e "${hui}前未启用限流关机功能${bai}" + echo -e "${hui}当前未启用限流关机功能${bai}" fi echo echo "------------------------------------------------" - echo "系统每分钟会检测实际流量是否到达阈值,到达后会自动关闭服务器!每月1日重置流量重启服务器。" + echo "系统每分钟检测一次流量,接收或发送任一方向到达阈值就会自动关闭服务器!每月1日重置流量重启服务器。" read -p "1. 开启限流关机功能 2. 停用限流关机功能 0. 退出 : " Limiting case "$Limiting" in 1) - # 输入新的虚拟内存大小 - echo "如果实际服务器就100G流量,可设置阈值为95G,提前关机,以免出现流量误差或溢出." + # 输入新的流量阈值 + echo "按单向流量计算(多数 VPS 只计出站)。如果实际服务器就100G流量,可设置阈值为95G,提前关机,以免出现流量误差或溢出." read -p "请输入流量阈值(单位为GB): " threshold_gb - cd ~ - curl -Ss -O "${GITHUB_RAW_URL}/kejilion/sh/main/Limiting_Shut_down.sh" - chmod +x ~/Limiting_Shut_down.sh - sed -i "s/110/$threshold_gb/g" ~/Limiting_Shut_down.sh - crontab -l | grep -v '~/Limiting_Shut_down.sh' | crontab - - (crontab -l ; echo "* * * * * ~/Limiting_Shut_down.sh") | crontab - > /dev/null 2>&1 - crontab -l | grep -v 'reboot' | crontab - - (crontab -l ; echo "0 1 1 * * reboot") | crontab - > /dev/null 2>&1 - echo "限流关机已设置" + if ! [[ "$threshold_gb" =~ ^[0-9]+$ ]] || [ "$threshold_gb" -le 0 ]; then + err_msg "流量阈值必须是大于 0 的整数(单位 GB),当前输入: '${threshold_gb}'" + elif ! command -v crontab &>/dev/null; then + err_msg "未找到 crontab,无法启用限流关机功能。" + else + limit_script="$HOME/Limiting_Shut_down.sh" + tmp_limit_script=$(mktemp "$HOME/.Limiting_Shut_down.sh.XXXXXX") || { + err_msg "无法创建限流脚本临时文件。" + break + } + limit_backup="" + limit_script_existed=0 + if [ -e "$limit_script" ]; then + limit_backup=$(mktemp "$HOME/.Limiting_Shut_down.sh.backup.XXXXXX") || { + rm -f "$tmp_limit_script" + err_msg "无法创建现有限流脚本的备份。" + break + } + if ! cp -p -- "$limit_script" "$limit_backup"; then + rm -f "$tmp_limit_script" "$limit_backup" + err_msg "无法备份现有限流脚本,已取消更新。" + break + fi + limit_script_existed=1 + fi + if download_file "${UPDATE_RAW_URL}/Limiting_Shut_down.sh" "$tmp_limit_script" && + sed -i "s/^threshold_gb=.*/threshold_gb=${threshold_gb}/" "$tmp_limit_script" && + grep -q "^threshold_gb=${threshold_gb}$" "$tmp_limit_script" && + bash -n "$tmp_limit_script" && + chmod +x "$tmp_limit_script" && + mv -f "$tmp_limit_script" "$limit_script"; then + if update_limit_cron enable "$limit_script"; then + [ -z "$limit_backup" ] || rm -f "$limit_backup" + echo "限流关机已设置" + else + if [ "$limit_script_existed" -eq 1 ]; then + if ! mv -f "$limit_backup" "$limit_script"; then + err_msg "定时任务写入失败,且旧限流脚本恢复失败: $limit_backup" + break + fi + else + rm -f "$limit_script" + fi + err_msg "定时任务写入失败,已恢复原限流脚本。" + fi + else + rm -f "$tmp_limit_script" + [ -z "$limit_backup" ] || rm -f "$limit_backup" + err_msg "限流关机脚本下载或校验失败,已保留原配置。" + fi + fi ;; 0) echo "已取消" ;; 2) - crontab -l | grep -v '~/Limiting_Shut_down.sh' | crontab - - crontab -l | grep -v 'reboot' | crontab - - rm ~/Limiting_Shut_down.sh - echo "已关闭限流关机功能" + if ! command -v crontab &>/dev/null; then + err_msg "未找到 crontab,无法安全停用定时任务。" + elif update_limit_cron disable "$HOME/Limiting_Shut_down.sh"; then + rm -f "$HOME/Limiting_Shut_down.sh" + echo "已关闭限流关机功能" + else + err_msg "定时任务更新失败,限流脚本已保留。" + fi ;; *) - echo "无效的选择,请输入 Y 或 N。" + echo "无效的选择,请输入 0、1 或 2。" ;; esac @@ -2113,23 +2476,71 @@ EOF case $sub_choice in 1) clear - install python3 python3-paramiko speedtest-cli lrzsz - mkdir cluster && cd cluster - touch servers.py - - cat > ./servers.py << EOF + install python3 python3-paramiko speedtest-cli lrzsz || break + mkdir -p ~/cluster || break + if [ -f ~/cluster/servers.py ]; then + echo "集群环境已存在,未覆盖现有服务器列表 ~/cluster/servers.py" + else + cat > ~/cluster/servers.py << EOF servers = [ ] EOF + if chmod 600 ~/cluster/servers.py; then + echo "集群环境已初始化: ~/cluster/servers.py" + else + err_msg "集群环境已创建,但无法收紧服务器列表文件权限。" + fi + fi ;; 2) while true; do clear + if [ ! -f ~/cluster/servers.py ]; then + err_msg "集群环境尚未安装,请先执行“1. 安装集群环境”。" + break + fi + if ! chmod 600 ~/cluster/servers.py; then + err_msg "无法收紧服务器列表文件权限。" + break + fi echo "集群服务器列表" - cat ~/cluster/servers.py + if ! python3 - "$HOME/cluster/servers.py" <<'PY' +import ast +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) +assignment = next( + ( + node + for node in tree.body + if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "servers" for target in node.targets) + ), + None, +) +if assignment is None: + raise SystemExit("servers.py 格式异常,未找到 servers 列表") +servers = ast.literal_eval(assignment.value) +if not isinstance(servers, list) or not all(isinstance(item, dict) for item in servers): + raise SystemExit("servers.py 中的 servers 必须是字典列表") +if not servers: + print("(空)") +for index, item in enumerate(servers, 1): + print( + f"{index}. {item.get('name', '')} " + f"{item.get('hostname', '')}:{item.get('port', '')} " + f"用户: {item.get('username', '')}" + ) +PY + then + err_msg "服务器列表格式异常,请先修复或还原 servers.py。" + break + fi echo "" echo "操作" @@ -2154,14 +2565,120 @@ EOF server_port=${server_port:-22} read -p "服务器用户名(root): " server_username server_username=${server_username:-root} - read -p "服务器用户密码: " server_password + read -s -p "服务器用户密码: " server_password + echo "" - sed -i "/servers = \[/a\ {\"name\": \"$server_name\", \"hostname\": \"$server_ip\", \"port\": $server_port, \"username\": \"$server_username\", \"password\": \"$server_password\", \"remote_path\": \"/home/\"}," ~/cluster/servers.py + if [ -z "$server_name" ] || [ -z "$server_ip" ]; then + err_msg "服务器名称和 IP 不能为空。" + elif ! [[ "$server_port" =~ ^[0-9]+$ ]] || [ "$server_port" -lt 1 ] || [ "$server_port" -gt 65535 ]; then + err_msg "服务器端口必须是 1 到 65535 之间的整数。" + elif ! python3 - "$HOME/cluster/servers.py" "$server_name" "$server_ip" "$server_port" "$server_username" 3<<< "$server_password" <<'PY' +import ast +import os +import pprint +import sys +import tempfile +from pathlib import Path + +path = Path(sys.argv[1]) +entry = { + "name": sys.argv[2], + "hostname": sys.argv[3], + "port": int(sys.argv[4]), + "username": sys.argv[5], + "password": os.fdopen(3).read().rstrip("\n"), + "remote_path": "/home/", +} +content = path.read_text(encoding="utf-8") +tree = ast.parse(content, filename=str(path)) +assignment = next( + ( + node + for node in tree.body + if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "servers" for target in node.targets) + ), + None, +) +if assignment is None: + raise SystemExit("servers.py 格式异常,未找到 servers 列表") +servers = ast.literal_eval(assignment.value) +if not isinstance(servers, list) or not all(isinstance(item, dict) for item in servers): + raise SystemExit("servers.py 中的 servers 必须是字典列表") +servers.append(entry) + +updated = "servers = " + pprint.pformat(servers, width=120) + "\n" +fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) +try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(updated) + handle.flush() + os.fsync(handle.fileno()) + os.chmod(temp_name, 0o600) + os.replace(temp_name, path) +finally: + if os.path.exists(temp_name): + os.unlink(temp_name) +PY + then + err_msg "写入服务器配置失败。" + fi ;; 2) read -p "请输入需要删除的关键字: " rmserver - sed -i "/$rmserver/d" ~/cluster/servers.py + # 通过解析 Python 字面量删除条目,避免按行删除破坏列表结构 + if [ -z "$rmserver" ]; then + err_msg "关键字不能为空,已取消删除。" + elif ! python3 - "$HOME/cluster/servers.py" "$rmserver" <<'PY' +import ast +import os +import pprint +import sys +import tempfile +from pathlib import Path + +path = Path(sys.argv[1]) +keyword = sys.argv[2] +content = path.read_text(encoding="utf-8") +tree = ast.parse(content, filename=str(path)) +assignment = next( + ( + node + for node in tree.body + if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "servers" for target in node.targets) + ), + None, +) +if assignment is None: + raise SystemExit("servers.py 格式异常,未找到 servers 列表") +servers = ast.literal_eval(assignment.value) +if not isinstance(servers, list) or not all(isinstance(item, dict) for item in servers): + raise SystemExit("servers.py 中的 servers 必须是字典列表") +remaining = [item for item in servers if keyword not in " ".join(str(value) for value in item.values())] +removed = len(servers) - len(remaining) +if removed == 0: + print("未找到匹配的服务器条目。") + raise SystemExit(0) + +updated = "servers = " + pprint.pformat(remaining, width=120) + "\n" +fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) +try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(updated) + handle.flush() + os.fsync(handle.fileno()) + os.chmod(temp_name, 0o600) + os.replace(temp_name, path) +finally: + if os.path.exists(temp_name): + os.unlink(temp_name) +print(f"已删除 {removed} 条服务器记录。") +PY + then + err_msg "删除服务器配置失败,原文件保持不变。" + fi ;; 3) install nano @@ -2198,14 +2715,49 @@ EOF 51) read -p "请输入批量执行的命令: " mingling + if [ -z "$mingling" ]; then + err_msg "命令不能为空。" + break + fi py_task=custom_tasks.py - cd ~/cluster/ - curl -sS -O "${GITHUB_RAW_URL}/kejilion/python-for-vps/main/cluster/${py_task}" - sed -i "s#Customtasks#$mingling#g" ~/cluster/$py_task - python3 ~/cluster/$py_task - ;; - 0) - break # 跳出循环,退出菜单 + mkdir -p ~/cluster || break + custom_task_tmp=$(mktemp "$HOME/cluster/.custom_tasks.py.XXXXXX") || { + err_msg "无法创建自定义任务临时文件。" + break + } + if download_file "${GITHUB_RAW_URL}/kejilion/python-for-vps/main/cluster/${py_task}" "$custom_task_tmp"; then + if CUSTOM_TASK="$mingling" python3 - "$custom_task_tmp" <<'PY' +import os +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +content = path.read_text(encoding="utf-8") +# 上游可能用单引号也可能用双引号包裹占位符,两种都要认 +pattern = re.compile(r"""(['"])Customtasks\1""") +if not pattern.search(content): + raise SystemExit("自定义任务脚本格式异常:未找到 Customtasks 占位符") +# 用 lambda 返回替换串,避免命令里的反斜杠被 re.sub 当作转义序列二次解释 +path.write_text( + pattern.sub(lambda _: repr(os.environ["CUSTOM_TASK"]), content, count=1), + encoding="utf-8", +) +PY + then + if mv -f "$custom_task_tmp" ~/cluster/"$py_task"; then + python3 ~/cluster/"$py_task" + else + rm -f "$custom_task_tmp" + err_msg "自定义任务脚本安装失败。" + fi + else + rm -f "$custom_task_tmp" + err_msg "自定义任务脚本生成失败。" + fi + else + rm -f "$custom_task_tmp" + fi ;; 0) break # 跳出循环,退出菜单 @@ -2220,9 +2772,13 @@ EOF ;; 7) clear - echo "将下载服务器列表数据,按任意键下载!" - read -n 1 -s -r -p "" - sz -y ~/cluster/servers.py + if [ ! -f ~/cluster/servers.py ]; then + err_msg "集群环境尚未安装,请先执行“1. 安装集群环境”。" + else + echo "将下载服务器列表数据,按任意键下载!" + read -n 1 -s -r -p "" + sz -y ~/cluster/servers.py + fi ;; @@ -2230,8 +2786,7 @@ EOF clear echo "请上传您的servers.py,按任意键开始上传!" read -n 1 -s -r -p "" - cd ~/cluster/ - rz -y + mkdir -p ~/cluster && (cd ~/cluster/ && rz -y) ;; 9) @@ -2267,18 +2822,27 @@ EOF ;; 00) - cd ~ clear echo "更新日志" echo "------------------------" - echo "全部日志: ${GITHUB_RAW_URL}/kejilion/sh/main/kejilion_sh_log.txt" + changelog=$(curl -fsSL --max-time 20 "${UPDATE_RAW_URL}/kejilion_sh_log.txt") + if [ -n "$changelog" ]; then + echo "全部日志: ${UPDATE_RAW_URL}/kejilion_sh_log.txt" + echo "------------------------" + echo "$changelog" | tail -n 35 + else + echo "(当前更新源未提供更新日志)" + fi echo "------------------------" - curl -s "${GITHUB_RAW_URL}/kejilion/sh/main/kejilion_sh_log.txt" | tail -n 35 - sh_v_new=$(curl -s "${GITHUB_RAW_URL}/kejilion/sh/main/kejilion.sh" | grep -o 'sh_v="[0-9.]*"' | cut -d '"' -f 2) + sh_v_new=$(curl -fsSL --max-time 20 "${UPDATE_RAW_URL}/kejilion.sh" | grep -o 'sh_v="[0-9.]*"' | head -n 1 | cut -d '"' -f 2) - if [ "$sh_v" = "$sh_v_new" ]; then + if [ -z "$sh_v_new" ]; then + err_msg "无法获取远程版本信息,请检查网络或更新源: ${UPDATE_RAW_URL}" + elif [ "$sh_v" = "$sh_v_new" ]; then echo -e "${lv}你已经是最新版本!${huang}v$sh_v${bai}" + elif ! version_gt "$sh_v_new" "$sh_v"; then + echo -e "${lv}本地版本 ${huang}v$sh_v${lv} 高于更新源版本 ${huang}v$sh_v_new${lv},已跳过更新。${bai}" else echo "发现新版本!" echo -e "当前版本 v$sh_v 最新版本 ${huang}v$sh_v_new${bai}" @@ -2287,11 +2851,29 @@ EOF case "$choice" in [Yy]) clear - curl -sS -O "${GITHUB_RAW_URL}/kejilion/sh/main/kejilion.sh" && chmod +x kejilion.sh - cp ./kejilion.sh /usr/local/bin/k > /dev/null 2>&1 - echo -e "${lv}脚本已更新到最新版本!${huang}v$sh_v_new${bai}" - break_end - kejilion + # 在脚本所在目录下载并原子替换,避免更新失败时截断当前脚本 + script_dir=$(dirname "$SCRIPT_PATH") + tmp_sh=$(mktemp "${script_dir}/.kejilion.sh.XXXXXX") || { + err_msg "创建更新临时文件失败。" + echo "按任意键继续..." + read -n 1 -s -r -p "" + continue + } + if download_file "${UPDATE_RAW_URL}/kejilion.sh" "$tmp_sh" && + bash -n "$tmp_sh" && + downloaded_version=$(grep -m1 -oE '^sh_v="[0-9]+([.][0-9]+)*"$' "$tmp_sh" | cut -d '"' -f 2) && + [ "$downloaded_version" = "$sh_v_new" ] && + version_gt "$downloaded_version" "$sh_v" && + chmod +x "$tmp_sh" && + mv -f "$tmp_sh" "$SCRIPT_PATH"; then + cp "$SCRIPT_PATH" /usr/local/bin/k > /dev/null 2>&1 + echo -e "${lv}脚本已更新到最新版本!${huang}v$sh_v_new${bai}" + break_end + exec bash "$SCRIPT_PATH" + else + rm -f "$tmp_sh" + err_msg "更新失败,已保留当前版本。" + fi ;; [Nn]) echo "已取消" @@ -2340,9 +2922,6 @@ else clean|清理) linux_clean ;; - dd|重装) - dd_xitong - ;; bbr3|bbrv3) bbrv3 ;; @@ -2351,5 +2930,3 @@ else ;; esac fi - - diff --git a/kejilion_sh_log.txt b/kejilion_sh_log.txt deleted file mode 100644 index 467cebf..0000000 --- a/kejilion_sh_log.txt +++ /dev/null @@ -1,511 +0,0 @@ -脚本更新日志 ------------------------- -2023-8-13 v1.0.3 -1.甲骨文云的DD脚本,添加了Ubuntu 20.04的重装选项。 -2.LDNMP建站,开放了苹果CMS网站的搭建功能. -3.系统信息查询,增加了内核版本显示,美化了界面。 -4.甲骨文脚本中,添加了开启ROOT登录的选项。 ------------------------- -2023-8-13 v1.0.4 -1.LDNMP建站,开放了独角数卡网站的搭建功能. -2.LDNMP建站,优化了备份全站到远端服务器的稳定性. -3.Docker管理,全局状态信息,添加了所有docker卷的显示. ------------------------- -2023-8-14 v1.1 -Docker管理器全面升级,体验前所未有! --加入了docker容器管理面板 --加入了docker镜像管理面板 --加入了docker网络管理面板 --加入了docker卷管理面板 --删除docker时追加确认信息,拒绝误操作 ------------------------- -2023-8-14 v1.2 -1.新增了11选项,加入了常用面板工具合集! --支持安装各种面板,包括: 宝塔,宝塔国际版,1panel,Nginx Proxy Manager等等,满足更多人群的使用需求! -2.优化了菜单效果 ------------------------- -2023-8-14 v1.3 -新增了12选项,我的工作区功能 --将为你提供5个后台运行的工作区,用来执行后台任务。即使你断开SSH也不会中断, --非常有意思的功能,快去试试吧! ------------------------- -2023-8-14 v1.3.2 -新增了13选项,系统工具 -科技lion一键脚本可以通过设置快捷键唤醒打开了,我设置的k作为脚本打开的快捷键!无需复制长命令了 -加入了ROOT密码修改,切换成ROOT登录模式 -系统设置中还有很多功能没开发,敬请期待! ------------------------- -2023-8-15 v1.4 -全面适配Centos系统,实现Ubuntu,Debian,Centos三大主流系统的适配 -优化LDNMP中PHP输入数据最大时间,解决WordPress网站导入部分主题失败的问题 ------------------------- -2023-8-15 v1.4.1 -选项13,系统工具中,加入了安装Python最新版的选项,感谢群友春风得意马蹄疾的投稿!很好用! ------------------------- -2023-8-15 v1.4.2 -docker管理中增加容器日志查看 -选项13,系统工具中,加入了留言板的选项,可以留下你的宝贵意见也可以在这里聊天,贼好玩! ------------------------- -2023-8-15 v1.4.5 -优化了信息查询运行效率 -信息查询新增了地理位置显示 -优化了脚本内系统判断机制! ------------------------- -2023-8-16 v1.4.6 -LDNMP建站中加入了删除站点删除数据库功能 ------------------------- -2023-8-16 v1.4.7 -选项11中,增加了一键搭建alist多存储文件列表工具的 -选项11中,增加了一键搭建网页版乌班图远程桌面 -选项13中,增加了开放所有端口功能 ------------------------- -2023-8-16 v1.4.8 -系统信息查询中,终于可以显示总流量消耗了!总接收和总发送两个信息 ------------------------- -2023-8-17 v1.4.9 -系统工具中新增SSH端口修改功能 -系统工具中新增优化DNS地址功能 ------------------------- -2023-8-18 v1.5 -系统性优化了代码,去除了无效的代码与空格 -系统信息查询添加了系统时间 -禁用ROOT账户,创建新的账户,更安全! ------------------------- -2023-8-18 v1.5.1 -LDNMP加入了安装bingchatAI聊天网站 -面板工具中添加了哪吒探针脚本整合 ------------------------- -2023-8-18 v1.5.2 -LDNMP加入了更新LDNMP选项 ------------------------- -2023-8-19 v1.5.3 -面板工具添加安装QB离线BT磁力下载面板 -优化IP获取源 ------------------------- -2023-8-20 v1.5.4 -面板工具已安装的工具支持状态检测,可以进行删除了! ------------------------- -2023-8-21 v1.5.5 -系统工具中添加优先ipv4/ipv6选项 -系统工具中添加查看端口占用状态选项 ------------------------- -2023-8-21 v1.5.6 -LDNMP建站添加了定时自动远程备份功能 ------------------------- -2023-8-22 v1.5.7 -面板工具增加了邮件服务器搭建,请确保服务器的25.80.443开放 ------------------------- -2023-8-23 v1.5.8 -面板工具增加了聊天系统搭建 ------------------------- -2023-8-24 v1.5.9 -面板工具增加了禅道项目管理软件搭建 ------------------------- -2023-8-24 v1.6 -面板工具增加了青龙面板搭建 -调整了面板工具列表的排版显示效果 ------------------------- -2023-8-27 v1.6.1 -LDNMP大幅优化安装体验,添加安装进度条和百分比显示,太刁了! ------------------------- -2023-8-28 v1.6.2 -docker管理可以显示容器所属网络,并且可以加入网络和退出网络了 ------------------------- -2023-8-28 v1.6.3 -系统工具中增加修改虚拟内存大小的选项 -系统信息查询中显示虚拟内存占用 ------------------------- -2023-8-29 v1.6.4 -面板工具加入cloudreve网盘的搭建 -面板工具加入简单图床程序搭建 ------------------------- -2023-8-29 v1.6.5 -LDNMP加入了高逼格的flarum论坛搭建 -面板工具加入简单图床程序搭建 ------------------------- -2023-9-1 v1.6.6 -LDNMP环境安装时用户密码将随机生成,提升安全性,安装环境更简单! -LDNMP环境安装时如果安装过docker将自动跳过,节省安装时间 -LDNMP环境更新WordPress到6.3.1版本 ------------------------- -2023-9-1 v1.6.7 -添加了账户管理功能,查看当前账户列表,添加删除账户,账号权限管理等 ------------------------- -2023-9-4 v1.6.8 -独角数卡登录时报错,显示解决办法 ------------------------- -2023-9-6 v1.6.9 -系统工具中添加随机用户密码生成器,方便懒得想用户名和密码的小伙伴 -优化了所有搭建网站与面板后的信息复制体验 ------------------------- -2023-9-11 v1.7 -面板工具中添加emby多媒体管理系统的搭建 ------------------------- -2023-9-15 v1.7.1 -LDNMP建站中可以搭建Bitwarden密码管理平台了 ------------------------- -2023-9-18 v1.7.2 -LDNMP建站将站点信息查询和站点管理合并 -LDNMP站点管理中添加证书重新申请和站点更换域名的功能 ------------------------- -2023-9-25 v1.8 -LDNMP建站增加了服务器与网站防护功能,防御暴力破解,防御网站被攻击 ------------------------- -2023-9-28 v1.8.2 -LDNMP建站优化了运行速度和安全性,增加了频率限制 -LDNMP建站优化了防御程序的高可用性 ------------------------- -2023-10-3 v1.8.3 -系统工具增加系统时区切换功能 ------------------------- -2023-10-7 v1.8.4 -LDNMP建站添加halo博客网站搭建 ------------------------- -2023-10-12 v1.8.5 -LDNMP建站添加优化LDNMP环境选项,可以开启高性能模式,大幅提升网站性能,应对高并发! ------------------------- -2023-10-14 v1.8.6 -面板工具增加了测速流量监控面板的安装 ------------------------- -2023-10-16 v1.8.7 -系统工具中添加开启BBR3加速功能 ------------------------- -2023-10-18 v1.8.8 -系统工具中优化BBR3加速安装流程,可根据CPU型号自行安装适合的内核版本 ------------------------- -2023-10-19 v1.8.9 -系统工具中BBRv3功能增加了更新内核和卸载内核功能 ------------------------- -2023-10-21 v1.9 -开放端口相关优化 -解决部分系统SSH端口切换后重启失联的问题 ------------------------- -2023-10-26 v1.9.1 -LNMP建站管理中添加了站点缓存清理功能 -面板工具中卸载对应应用时添加了应用目录一并删除,删除更彻底! ------------------------- -2023-10-28 v1.9.2 -系统工具中修复了虚拟内存大小重启后还原的问题 ------------------------- -2023-11-07 v1.9.3 -面板工具中增加AdGuardHome去广告软件安装和管理 ------------------------- -2023-11-08 v1.9.4 -系统工具添加了防火墙高级管理功能,可以开关端口,可以IP黑白名单 -未来会上线地域黑白名单等高级功能 ------------------------- -2023-11-09 v1.9.5 -系统工具中防火墙添加udp控制 ------------------------- -2023-11-10 v1.9.6 -测试脚本合集增加了缝合怪一条龙测试 -系统信息查询中添加了系统运行时长显示 ------------------------- -2023-11-10 v1.9.7 -LDNMP建站增加typecho轻量博客的搭建 ------------------------- -2023-11-16 v1.9.8 -面板工具中增加了在线office办公软件安装 ------------------------- -2023-11-21 v1.9.9 -面板工具中增加了雷池WAF防火墙程序安装 ------------------------- -2023-11-28 v2.0 -LDNMP建站中增加仅安装nginx的选项专门服务于站点重定向和站点反向代理 -精简无用的代码,优化执行效率 ------------------------- -2023-11-29 v2.0.1 -LDNMP建站改用cerbot申请证书,更稳定更快速。弃用acme ------------------------- -2023-11-30 v2.0.2 -面板工具修复QB无法登录问题 -面板工具修复RocketChat进入后无限加载问题 -系统工具中添加修改主机名功能 -系统工具中添加服务器重启功能 ------------------------- -2023-12-04 v2.0.3 -LDNMP建站过程中增加了nginx自我检测修复功能 -系统工具添加更新源切换功能,请先在测试环境使用 -LDNMP建站增加自定义上传静态html界面功能 ------------------------- -2023-12-05 v2.0.4 -LDNMP建站中仅安装nginx功能添加安装成功提示,更优雅直观 -LDNMP建站中仅安装nginx功能支持自动更新nginx版本 -优化代码细节,定义调用函数,脚本执行更简洁,提升效率 ------------------------- -2023-12-07 v2.0.5 -LDNMP在站点数据管理中增加查看站点分析报告功能,可以对网站流量进行监控与分析 -主菜单添加手动更新脚本功能 ------------------------- -2023-12-08 v2.0.6 -主菜单中更新日志和脚本更新合并,更新时可以看到更新日志,更直觉 -面板工具中新增了docker管理面板portainer的安装 -面板工具中新增了VScode网页版的安装 ------------------------- -2023-12-15 v2.0.7 -系统工具中添加了定时任务的管理功能 ------------------------- -2023-12-16 v2.0.8 -大量安装软件的代码整合更简单快速安装各类软件包 -优化选项4中的常用工具安装及使用体验,已安装可以提示使用方法 -选项4中新增多款实用工具,如btop现代化监控工具,安装即用,q退出 ------------------------- -2023-12-18 v2.0.9 -优化安装代码,全局调用,安装智能检测,安装软件更快 ------------------------- -2023-12-19 v2.1 -选项4中,新增自定义安装卸载指定的工具 -优化了清理逻辑,清理系统更快更干净 -优化了卸载软件包的逻辑,根据系统执行卸载,更智能 -优化主菜单到二级菜单的交互,二级菜单更沉浸,二级菜单箭头引导调整 ------------------------- -2023-12-20 v2.1.1 -史诗级代码精简,屎山大扫除,脚本运行更快速,脚本大小缩减20% -LDNMP安装环境时增加端口检测功能,端口被占用会无法安装 -面板工具中添加Uptime Kuma监控工具的安装 -面板工具中添加Memos网页备忘录的安装 ------------------------- -2023-12-23 v2.1.2 -面板工具中添加潘多拉GPT镜像站安装 ------------------------- -2023-12-26 v2.1.3 -选项4常用工具中添加跑火车屏保,俄罗斯方块,贪吃蛇,太空入侵者三款小游戏 ------------------------- -2023-12-30 v2.1.4 -LDNMP增加了防止源站IP泄露机制,保护源站IP与证书潜在安全隐患 ------------------------- -2024-01-04 v2.1.5 -脚本添加了启动快捷键,命令行输入k可以快速启动科技lion脚本工具 ------------------------- -2024-01-04 v2.1.6 -脚本添加了启动快捷键,命令行输入k可以快速启动科技lion脚本工具 -面板工具1panel增加了已安装状态,支持查看面板信息修改用户密码,支持卸载面板 ------------------------- -2024-01-06 v2.1.7 -面板工具增加了nextcloud网盘的搭建 ------------------------- -2024-01-09 v2.1.8 -LDNMP建站增加对ipv6的建站支持,解析v6地址建站据说提升站点安全性,性能也有提升! ------------------------- -2024-01-10 v2.1.9 -面板工具增加QD-Today定时任务管理框架的安装 ------------------------- -2024-01-12 v2.2 -面板工具增加了Dockge容器堆栈管理面板的安装 -面板工具增加了LibreSpeed轻量级测速工具的安装 -优化了脚本快捷启动,输入k快速启动脚本支持任何目录下使用 ------------------------- -2024-01-16 v2.2.1 -主菜单添加14选项,VPS集群控制系统,可以一键操控所有VPS执行任务。 -VPS集群控制属于测试版本,请用闲置机器开始体验,有任何问题欢迎留言反馈 ------------------------- -2024-01-17 v2.2.2 -面板工具增加了搜索聚合网站的安装 -优化了集群控制体验,集群环境备份还原卸载等功能上线 ------------------------- -2024-01-18 v2.2.3 -面板工具增加了私有相册系统的安装 ------------------------- -2024-01-21 v2.2.4 -面板工具增加了PDF工具大全应用的安装 ------------------------- -2024-01-23 v2.2.5 -优化了LDNMP建站配置时进度条体验,让读条不至于卡在一个地方很久。拆分配置环节让体验更加顺畅 ------------------------- -2024-01-25 v2.2.6 -精简了LDNMP建站镜像大小,采用官方alpine精简镜像包,更快,更轻,更安全 -脚本适配alpine系统 -系统工具中重装系统选项升级,增加了Debian11 Debian10 ubuntu22.04 centos7 alpine3.19 windows11的安装选项 ------------------------- -2024-01-30 v2.2.7 -LDNMP建站更新halo2的安装版本到2.11 -修复alpine系统下虚拟内存重启后失效的问题 -优化alpine系统下docker安装体验,安装docker应用时自动识别安装docker环境 -修复alpine系统下CPU占用显示异常的问题 ------------------------- -2024-02-1 v2.2.8 -主菜单临时增加p选项,与幻兽帕鲁开服脚本联动 ------------------------- -2024-02-5 v2.2.9 -修改主机名支持alpine系统 ------------------------- -2024-02-8 v2.3 -面板工具增加了drawio在线绘图工具的安装 ------------------------- -2024-02-21 v2.3.1 -主菜单选项12我的工作区增加至10个,更利于多线程后台任务 ------------------------- -2024-02-26 v2.3.2 -系统工具中的选项8中一键重装系统的体验进行优化,重装时展示系统重装后的用户名密码和端口号 -一键重装系统增加了更多Windows版本重装 11 10 2019 2022 -一键重装系统增加了更多版本重装 -一键重装Windows系统默认为中文版了 -主菜单选项5的BBR管理适配了alpine的新界面 ------------------------- -2024-03-06 v2.3.3 -系统工具中新增了host解析设置功能 -优化了alpine系统的主机名修改逻辑 ------------------------- -2024-03-11 v2.3.4 -系统工具中新增fail2banSSH防御程序,防止你的SSH被暴力破解 ------------------------- -2024-03-20 v2.3.5 -面板工具加入了PVE开小鸡的面板,感谢oneclickvirt大佬的一键安装脚本 ------------------------- -2024-03-29 v2.3.6 -LDNMP安装环境时提前设置1G虚拟内存,提升建站环境安装速度和稳定性 ------------------------- -2024-04-01 v2.3.7 -LDNMP改进了防御能力,可以拦截404攻击,守护网站安全。脚本进入选择10再选择35站点防御,防御原来这么简单! ------------------------- -2024-04-02 v2.3.8 -LDNMP站点防御接入cf,添加了cloudflare模式,添加了参数配置选项 ------------------------- -2024-04-07 v2.3.9 -LDNMP申请域名证书,解决证书链不完整的问题,谢谢wuying2021分支提供思路,已整合至主线版本 ------------------------- -2024-04-12 v2.4 -面板工具中添加了24选项 webtop远程桌面程序,alpine中文可视化桌面系统,很好用! ------------------------- -2024-04-14 v2.4.1 -面板工具中添加Sun-Panel导航面板的安装 -LDNMP建站中halo镜像版本更新到最新版本 -测试脚本合集中追加了两项,nxtrace快速回程测试脚本 nxtrace指定IP回程测试脚本 -测试脚本合集中追加了两项,ludashi2020的三网线路测试 -测试脚本合集界面重构,分类更清晰,方便未来扩展使用 ------------------------- -2024-04-18 v2.4.2 -使用docker容器方式部署fail2ban防暴力破解程序,ssh和nginx都能防御 -优化了重启服务器的逻辑,增加了确认提示。 -增加了dnf包管理的适配 -安装BBRV3,cpu测速时将自动分配1024M的虚拟内存。 ------------------------- -2024-04-23 v2.4.3 -面板工具中添加了34选项,一个文件共享平台,可以传文件传图片,做分享链接用 ------------------------- -2024-04-26 v2.4.4 -面板工具中添加了33选项,一个极简朋友圈网页程序,高仿微信朋友圈! ------------------------- -2024-04-29 v2.4.5 -系统工具中添加了限流关机功能,到达限定流量后自动关机,针对小流量怕反撸的机型而设计! ------------------------- -2024-04-30 v2.4.6 -LDNMP建站分类调整,将不需要安装PHP就能搭建的应用移至nginx区块中, -LDNMP建站如果没装环境直接部署网站会弹出检测提示,要求先装环境再建站 -缩小脚本体积,降了5kb,整合了一些老大难代码,模块化更顺畅小巧 ------------------------- -2024-05-04 v2.4.7 -面板工具新增36选项,加入了对AI聚合聊天网站的安装 ------------------------- -2024-05-05 v2.4.8 -LDNMP增加了自定义PHP动态站点功能,你可以上传自己的PHP项目 -LDNMP站点管理中,增加了编辑查看全局和站点配置 ------------------------- -2024-05-09 v2.4.9 -LDNMP静态站点动态站点自定义搭建优化,支持远程下载源码,手动上传源码 -修改主机名大小写Y的支持 -检测脚本添加了xykt大佬的IP质量体检脚本,非常美观实用的脚本 ------------------------- -2024-05-09 v2.4.10 -更新脚本逻辑进行优化,可选择是否更新,并且显示当前和最新的版本号,更智能! ------------------------- -2024-05-09 v2.5 -重构并定义脚本中出现的红绿蓝黄灰白文字颜色,创建未来统一灵活调用文字颜色 -面板工具中添加了MYIP工具箱面板,可以查看当前使用的IP信息与状态 ------------------------- -2024-05-11 v2.5.1 -对docker环境检测进行优化,应用部署将会更稳定 -对一些提示的文字颜色进行了优化,对一些警示文字进行红色黄色标注 ------------------------- -2024-05-13 v2.5.2 -优化脚本更新日志显示逻辑,会只显示最近更新日志。点击URL可查看所有日志。 -优化时区设置,适配 alpine linux 系统。 -系统工具新增了一条龙系统调优功能,一键搞定! -系统工具新增ROOT私钥登录模式,更安全的登录模式。 ------------------------- -2024-05-16 v2.5.3 -系统工具中一键重装增加了Windows7的专业安装支持。 -系统工具中一键重装修改序号显示重复问题。 -优化了ROOT密码登录修改后的兼容性问题。 -LDNMP建站自定义动态站点支持自定义安装扩展。 ------------------------- -2024-05-17 v2.5.4 -系统工具中定时任务添加了月任务,分钟任务的设定。 -系统工具中定时任务添加了使用文本工具直接编辑定时任务的功能。 -系统工具中修复了时区在centos中显示不正确的问题。 -系统信息查询中添加了系统时区的显示。 -系统工具功能列表调整为双列显示。 -系统工具中修改DNS地址,增加国外DNS和国内DNS选项。 ------------------------- -2024-05-20 v2.5.5 -优化docker和docker-compose安装。 -支持docker-compose新用法。 -系统工具中一键重装系统优化了选单体验。 -docker管理中新增切换docker源功能,感谢LinuxMirrors团队的开源项目。 ------------------------- -2024-05-21 v2.5.6 -证书全面采用ECC加密,SSH证书,域名证书等,提升加解密效率和安全性 -修改docker管理中的菜单文字拼写错误。 ------------------------- -2024-05-23 v2.5.7 -工作区增加了自定义工作区,用户可以设定自己命名的工作区,可以创建N个工作区。 -工作区菜单中溢出工作区卸载功能。 -工作区状态改成工作区状态管理,可以删除删除工作区。 ------------------------- -2024-05-27 v2.5.8 -面板工具中的应用支持ipv6的地址访问,如果你小鸡有v6地址可以看到变化。 -docker管理中添加了开启/关闭v6访问功能。如果v6应用无法访问可开启v6访问。 -LDNMP建站增加了反向代理域名的功能。 ------------------------- -2024-05-29 v2.5.9 -系统工具修复重装win7/server2022时无法安装的问题。 ------------------------- -2024-05-30 v2.5.10 -LDNMP搭建自定义动态网站中添加导入宝塔1p数据库备份的环节。 ------------------------- -2024-06-11 v2.5.11 -优化纯v6地址下安装certbot失败的问题。 ------------------------- -2024-06-18 v2.6.0 -甲骨文云保活脚本更新,将内存占用提升到20% -LDNMP建站提升申请域名证书的兼容性,解决了部分申请证书失败的问题 -修复了国内无法安装docker的问题 -修复了国内无法拉取docker镜像的问题 -docker管理中添加了修改daemon.json配置的选项 ------------------------- -2024-06-22 v2.6.1 -系统工具改版了python环境的安装和版本管理功能,原先只能安装最新版,现在可以安装任意版本。 ------------------------- -2024-06-25 v2.6.2 -LDNMP建站提升了多站点接入redis的兼容性。 -LDNMP安装环境时修改了redis的内存限制为512m。 -LDNMP安装环境时修改了redis的内存回收策略,内存占大时对非热点数据进行释放。 -LDNMP站点数据管理中清除缓存中接入redis缓存清理功能。 ------------------------- -2024-06-27 v2.6.3 -系统工具中一键重装添加了Rocky,Alma,Fedora等发行版的重装。 -LDNMP站点防御增加了自动检测高负载开启cf的5秒盾功能。 -系统信息查询优化了部分系统CPU占用显示错误问题。 ------------------------- -2024-06-28 v2.6.4 -系统工具中添加了TG监控预警功能,当系统出现异常时通知用户。 ------------------------- -2024-06-30 v2.6.5 -测试脚本合集中增加了i-abc多功能测速脚本,可以多维度测速 -系统工具中TG预警增加了SSH登录预警 ------------------------- -2024-07-03 v2.6.6 -系统工具中增加了修复SSH漏洞功能,感谢TG群@Peach0131大佬提供脚本支持! -优化了系统更新清理的兼容性,支持更多发行版本的更新和清理。 -面板工具中添加了小雅alist全家桶一键脚本 by DDS-Rem ------------------------- -2024-07-05 v2.6.7 -k的全局用法更新 -k install nano wget | k add nano wget | k 安装 nano wget 使用这三个方式判断系统自动安装软件 -k remove nano wget | k del nano wget | k 卸载 nano wget 使用这三个方式判断系统自动卸载软件 -k update | k 更新 使用这两个方式判断系统自动更新系统 -k clean | k 清理 使用这两个方式判断系统自动清理系统垃圾 -k dd | k 重装 使用这两个方式可以快速打开重装面板 -k bbr3 | k bbrv3 使用这两个方式可以快速打开BBRv3控制面板 -未来还会增加更多k的快捷功能,方便大家日常使用。 ------------------------