#!/bin/bash sh_v="2.7.0" huang='\033[33m' bai='\033[0m' lv='\033[0;32m' hong='\033[31m' kjlan='\033[96m' hui='\e[37m' # 统一使用 GitHub 官方 Raw 地址,可通过环境变量 GITHUB_RAW_URL 覆盖 GITHUB_RAW_URL="${GITHUB_RAW_URL:-https://raw.githubusercontent.com}" GITHUB_RAW_URL="${GITHUB_RAW_URL%/}" # 本脚本自身的更新源 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 } ip_address() { ipv4_address=$(curl -s ipv4.ip.sb) 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 package_installed "$package"; then echo "$package 已经安装。" else missing_packages+=("$package") fi done [ "${#missing_packages[@]}" -gt 0 ] || return 0 if ! command -v apt-get &>/dev/null; then err_msg "未找到 apt-get,当前系统环境不受支持。" return 1 fi 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 } remove() { if [ $# -eq 0 ]; then echo "未提供软件包参数!" return 1 fi if ! command -v apt-get &>/dev/null; then err_msg "未找到 apt-get,当前系统环境不受支持。" return 1 fi echo "正在卸载: $*" DEBIAN_FRONTEND=noninteractive apt-get purge -y -- "$@" } break_end() { echo -e "${lv}操作完成${bai}" echo "按任意键继续..." read -n 1 -s -r -p "" echo "" clear } # 用当前脚本自身替换进程,回到主菜单。 # 用 exec 而非再开子进程,既不依赖 /usr/local/bin/k 是否存在(非 root 时 cp 会失败), # 也避免每次“返回主菜单”都叠加一层进程。 kejilion() { exec bash "$SCRIPT_PATH" } install_add_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 || 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() { # 先校验大小,避免把已有的 /swapfile 删掉之后才发现参数不合法 if ! [[ "$new_swap" =~ ^[0-9]+$ ]] || [ "$new_swap" -le 0 ]; then err_msg "虚拟内存大小必须是大于 0 的整数(单位 MB),当前输入: '${new_swap}'" return 1 fi # 先完整创建新文件,成功后再替换旧 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() { 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 docker exec -it fail2ban fail2ban-client status } f2b_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() { docker run -d \ --name=fail2ban \ --net=host \ --cap-add=NET_ADMIN \ --cap-add=NET_RAW \ -e PUID=1000 \ -e PGID=1000 \ -e TZ=Etc/UTC \ -e VERBOSITY=-vv \ -v /path/to/fail2ban/config:/config \ -v /var/log:/var/log:ro \ --restart unless-stopped \ lscr.io/linuxserver/fail2ban:latest || { err_msg "Fail2Ban 容器启动失败。" return 1 } 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() { xxx=linux-sshd f2b_status_xxx } server_reboot() { read -p "$(echo -e "${huang}现在重启服务器吗?(Y/N): ${bai}")" rboot case "$rboot" in [Yy]) echo "已重启" reboot ;; [Nn]) echo "已取消" ;; *) echo "无效的选择,请输入 Y 或 N。" ;; esac } output_status() { # 跳过 lo 等回环/虚拟接口,否则本机内部流量会被计入总量, # 导致“限流自动关机”提前触发 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 { rx_units = "Bytes"; tx_units = "Bytes"; if (rx_total > 1024) { rx_total /= 1024; rx_units = "KB"; } if (rx_total > 1024) { rx_total /= 1024; rx_units = "MB"; } if (rx_total > 1024) { rx_total /= 1024; rx_units = "GB"; } if (tx_total > 1024) { tx_total /= 1024; tx_units = "KB"; } if (tx_total > 1024) { tx_total /= 1024; tx_units = "MB"; } if (tx_total > 1024) { tx_total /= 1024; tx_units = "GB"; } printf("总接收: %.2f %s\n总发送: %.2f %s\n", rx_total, rx_units, tx_total, tx_units); }' /proc/net/dev) } current_timezone() { timedatectl show --property=Timezone --value 2>/dev/null } set_timedate() { local shiqu="$1" timedatectl set-timezone "$shiqu" } linux_update() { 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() { 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 # 不自动批量删除内核,保留当前内核和可启动的回退版本 } # 只增量修改指定的内核参数,不再用 > 整体覆盖 /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地址 ipv6_available=0 if [[ $(ip -6 addr | grep -c "inet6") -gt 0 ]]; then ipv6_available=1 fi echo "nameserver $dns1_ipv4" > /etc/resolv.conf echo "nameserver $dns2_ipv4" >> /etc/resolv.conf if [[ $ipv6_available -eq 1 ]]; then echo "nameserver $dns1_ipv6" >> /etc/resolv.conf echo "nameserver $dns2_ipv6" >> /etc/resolv.conf fi echo "DNS地址已更新" echo "------------------------" cat /etc/resolv.conf echo "------------------------" } root_use() { clear [ "$EUID" -ne 0 ] && echo -e "${huang}请注意,该功能需要root用户才能运行!${bai}" && break_end && kejilion } # 探测 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 command -v dpkg-query &>/dev/null && list_xanmod_packages | grep -q .; then while true; do kernel_version=$(uname -r) echo "您已安装xanmod的BBRv3内核" echo "当前内核版本: $kernel_version" echo "" echo "内核管理" echo "------------------------" echo "1. 更新BBRv3内核 2. 卸载BBRv3内核" echo "------------------------" echo "0. 返回上一级选单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) # 先安装并写入启动菜单,避免更新失败后没有可启动的旧内核 if xanmod_add_repo && xanmod_install_kernel && update-grub; then echo "XanMod内核已更新,旧内核已保留作为回退。重启后生效" server_reboot else err_msg "XanMod 内核更新失败,旧内核保持不变。" fi ;; 2) 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 # 跳出循环,退出菜单 ;; *) break # 跳出循环,退出菜单 ;; esac done else clear echo "请备份数据,将为你升级Linux内核开启BBR3" echo "官网介绍: https://xanmod.org/" echo "------------------------------------------------" echo "仅支持Debian/Ubuntu 仅支持x86_64架构" echo "VPS是512M内存的,请提前添加1G虚拟内存,防止因内存不足失联!" echo "------------------------------------------------" read -p "确定继续吗?(Y/N): " choice case "$choice" in [Yy]) # 注意:这里必须用 return,不能用 break —— 当前不在任何循环内, # break 只会报错并继续往下执行,导致在不支持的系统上照样改内核和 sysctl if [ -r /etc/os-release ]; then # 放到子 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 err_msg "无法确定操作系统类型" return 1 fi # 检查系统架构 arch=$(dpkg --print-architecture 2>/dev/null) if [ "$arch" != "amd64" ]; then err_msg "当前环境不支持,仅支持x86_64架构" return 1 fi new_swap=1024 ensure_swap || return 1 xanmod_add_repo || return 1 xanmod_install_kernel || return 1 update-grub || { err_msg "更新启动菜单失败,已取消自动重启。" return 1 } # 步骤5:启用BBR3 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启用成功。重启后生效" server_reboot ;; [Nn]) echo "已取消" ;; *) echo "无效的选择,请输入 Y 或 N。" ;; esac fi } kejilion_sh() { while true; do clear echo -e "${kjlan}_ _ ____ _ _ _ _ ____ _ _ " echo "|_/ |___ | | | | | | |\ | " echo "| \_ |___ _| | |___ | |__| | \| " echo " " echo -e "${kjlan}科技lion一键脚本工具 v$sh_v (仅支持 Ubuntu / Debian)${bai}" echo -e "${kjlan}-输入${huang}k${kjlan}可快速启动此脚本-${bai}" echo "------------------------" echo "1. 系统信息查询" echo "2. 系统更新" echo "3. 系统清理" echo "5. BBR管理 ▶" echo "7. WARP管理 ▶ " echo "8. 测试脚本合集 ▶ " echo "13. 系统工具 ▶ " echo "14. VPS集群控制 ▶ " echo "------------------------" echo "00. 脚本更新" echo "------------------------" echo "0. 退出脚本" echo "------------------------" read -p "请输入你的选择: " choice case $choice in 1) clear # 函数: 获取IPv4和IPv6地址 ip_address if [ "$(uname -m)" == "x86_64" ]; then cpu_info=$(cat /proc/cpuinfo | grep 'model name' | uniq | sed -e 's/model name[[:space:]]*: //') else cpu_info=$(lscpu | grep 'BIOS Model name' | awk -F': ' '{print $2}' | sed 's/^[ \t]*//') fi cpu_usage_percent=$(awk '{u=$2+$4; t=$2+$4+$5; if (NR==1){u1=u; t1=t;} else printf "%.0f\n", (($2+$4-u1) * 100 / (t-t1))}' \ <(grep 'cpu ' /proc/stat) <(sleep 1; grep 'cpu ' /proc/stat)) cpu_cores=$(nproc) mem_info=$(free -b | awk 'NR==2{printf "%.2f/%.2f MB (%.2f%%)", $3/1024/1024, $2/1024/1024, $3*100/$2}') disk_info=$(df -h | awk '$NF=="/"{printf "%s/%s (%s)", $3, $2, $5}') # 加超时,避免无外网时整个信息页卡住 country=$(curl -s --max-time 5 ipinfo.io/country) city=$(curl -s --max-time 5 ipinfo.io/city) isp_info=$(curl -s --max-time 5 ipinfo.io/org) cpu_arch=$(uname -m) hostname=$(hostname) kernel_version=$(uname -r) congestion_algorithm=$(sysctl -n net.ipv4.tcp_congestion_control) queue_algorithm=$(sysctl -n net.core.default_qdisc) # 尝试使用 lsb_release 获取系统信息 os_info=$(lsb_release -ds 2>/dev/null) # 如果 lsb_release 命令失败,则尝试其他方法 if [ -z "$os_info" ]; then # 检查常见的发行文件 if [ -f "/etc/os-release" ]; then os_info=$(source /etc/os-release && echo "$PRETTY_NAME") elif [ -f "/etc/debian_version" ]; then os_info="Debian $(cat /etc/debian_version)" else os_info="Unknown" fi fi output_status current_time=$(date "+%Y-%m-%d %I:%M %p") # 用 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 else swap_percentage=$((swap_used * 100 / swap_total)) fi swap_info="${swap_used}MB/${swap_total}MB (${swap_percentage}%)" runtime=$(cat /proc/uptime | awk -F. '{run_days=int($1 / 86400);run_hours=int(($1 % 86400) / 3600);run_minutes=int(($1 % 3600) / 60); if (run_days > 0) printf("%d天 ", run_days); if (run_hours > 0) printf("%d时 ", run_hours); printf("%d分\n", run_minutes)}') timezone=$(current_timezone) echo "" echo "系统信息查询" echo "------------------------" echo "主机名: $hostname" echo "运营商: $isp_info" echo "------------------------" echo "系统版本: $os_info" echo "Linux版本: $kernel_version" echo "------------------------" echo "CPU架构: $cpu_arch" echo "CPU型号: $cpu_info" echo "CPU核心数: $cpu_cores" echo "------------------------" echo "CPU占用: $cpu_usage_percent%" echo "物理内存: $mem_info" echo "虚拟内存: $swap_info" echo "硬盘占用: $disk_info" echo "------------------------" echo "$output" echo "------------------------" echo "网络拥堵算法: $congestion_algorithm $queue_algorithm" echo "------------------------" echo "公网IPv4地址: $ipv4_address" echo "公网IPv6地址: $ipv6_address" echo "------------------------" echo "地理位置: $country $city" echo "系统时区: $timezone" echo "系统时间: $current_time" echo "------------------------" echo "系统运行时长: $runtime" echo ;; 2) clear linux_update ;; 3) clear linux_clean ;; 5) clear 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 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) while true; do clear echo "▶ 测试脚本合集" echo "" echo "----IP及解锁状态检测-----------" echo "1. ChatGPT解锁状态检测" echo "2. Region流媒体解锁测试" echo "3. yeahwu流媒体解锁检测" echo "4. xykt_IP质量体检脚本" echo "" echo "----网络线路测速-----------" echo "11. besttrace三网回程延迟路由测试" echo "12. mtr_trace三网回程线路测试" echo "13. Superspeed三网测速" echo "14. nxtrace快速回程测试脚本" echo "15. nxtrace指定IP回程测试脚本" echo "16. ludashi2020三网线路测试" echo "17. i-abc多功能测速脚本" echo "" echo "----硬件性能测试----------" echo "21. yabs性能测试" echo "22. GB5 CPU性能测试脚本" echo "" echo "----综合性测试-----------" echo "31. bench性能测试" echo "32. spiritysdx融合怪测评" echo "" echo "------------------------" echo "0. 返回主菜单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) clear bash <(curl -Ls https://cdn.jsdelivr.net/gh/missuo/OpenAI-Checker/openai.sh) ;; 2) clear bash <(curl -L -s check.unlock.media) ;; 3) clear install wget wget -qO- https://github.com/yeahwu/check/raw/main/check.sh | bash ;; 4) clear bash <(curl -Ls IP.Check.Place) ;; 11) clear install wget wget -qO- git.io/besttrace | bash ;; 12) clear curl "${GITHUB_RAW_URL}/zhucaidan/mtr_trace/main/mtr_trace.sh" | bash ;; 13) clear bash <(curl -Lso- https://git.io/superspeed_uxh) ;; 14) clear curl nxtrace.org/nt |bash nexttrace --fast-trace --tcp ;; 15) clear echo "可参考的IP列表" echo "------------------------" echo "北京电信: 219.141.136.12" echo "北京联通: 202.106.50.1" echo "北京移动: 221.179.155.161" echo "上海电信: 202.96.209.133" echo "上海联通: 210.22.97.1" echo "上海移动: 211.136.112.200" echo "广州电信: 58.60.188.222" echo "广州联通: 210.21.196.6" echo "广州移动: 120.196.165.24" echo "成都电信: 61.139.2.69" echo "成都联通: 119.6.6.6" echo "成都移动: 211.137.96.205" echo "湖南电信: 36.111.200.100" echo "湖南联通: 42.48.16.100" echo "湖南移动: 39.134.254.6" echo "------------------------" read -p "输入一个指定IP: " testip curl nxtrace.org/nt |bash nexttrace $testip ;; 16) clear curl "${GITHUB_RAW_URL}/ludashi2020/backtrace/main/install.sh" -sSf | sh ;; 17) clear bash <(curl -sL "${GITHUB_RAW_URL}/i-abc/Speedtest/main/speedtest.sh") ;; 21) clear # swap 只是低内存机器的兜底,失败不应中断测试 new_swap=1024 ensure_swap || echo -e "${huang}未能调整虚拟内存,将直接开始测试。${bai}" curl -sL yabs.sh | bash -s -- -i -5 ;; 22) clear new_swap=1024 ensure_swap || echo -e "${huang}未能调整虚拟内存,将直接开始测试。${bai}" bash <(curl -sL "${GITHUB_RAW_URL}/i-abc/GB5/main/gb5-test.sh") ;; 31) clear curl -Lso- bench.sh | bash ;; 32) clear curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh && chmod +x ecs.sh && bash ecs.sh ;; 0) kejilion ;; *) echo "无效的输入!" ;; esac break_end done ;; 13) while true; do clear echo "▶ 系统工具" echo "------------------------" echo "1. 设置脚本启动快捷键 2. 修改登录密码" echo "4. 安装Python指定版本" echo "7. 优化DNS地址 10. 切换优先ipv4/ipv6" echo "------------------------" echo "11. 查看端口占用状态 12. 修改虚拟内存大小" echo "13. 用户管理 14. 用户/密码生成器" echo "15. 系统时区调整 16. 设置BBR3加速" echo "18. 修改主机名" echo "19. 切换系统更新源 20. 定时任务管理" echo "------------------------" echo "21. 本机host解析 22. fail2banSSH防御程序" echo "23. 限流自动关机" echo "------------------------" echo "31. 留言板" echo "------------------------" echo "99. 重启服务器" echo "------------------------" echo "0. 返回主菜单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) clear 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) clear echo "设置你的登录密码" passwd ;; 4) root_use VERSION=$(python3 -V 2>&1 | awk '{print $2}') echo -e "当前python版本号: ${huang}$VERSION${bai}" echo "------------" echo "推荐版本: 3.12 3.11 3.10 3.9 3.8 2.7" echo "查询更多版本: https://www.python.org/downloads/" echo "------------" read -p "输入你要安装的python版本号: " py_new_v if ! [[ "$py_new_v" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then err_msg "版本号格式不正确,示例: 3.12 或 3.12.4" break fi 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 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" if [[ -d "\$PYENV_ROOT/bin" ]]; then export PATH="\$PYENV_ROOT/bin:\$PATH" fi eval "\$(pyenv init --path)" eval "\$(pyenv init -)" if pyenv commands 2>/dev/null | grep -qx virtualenv-init; then eval "\$(pyenv virtualenv-init -)" fi EOF fi 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 -)" 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 ;; 7) root_use echo "当前DNS地址" echo "------------------------" cat /etc/resolv.conf echo "------------------------" echo "" # 询问用户是否要优化DNS设置 read -p "是否要设置DNS地址?(y/n): " choice if [ "$choice" == "y" ]; then read -p "1. 国外DNS优化 2. 国内DNS优化 0. 退出 : " Limiting case "$Limiting" in 1) dns1_ipv4="1.1.1.1" dns2_ipv4="8.8.8.8" dns1_ipv6="2606:4700:4700::1111" dns2_ipv6="2001:4860:4860::8888" set_dns ;; 2) dns1_ipv4="223.5.5.5" dns2_ipv4="183.60.83.19" dns1_ipv6="2400:3200::1" dns2_ipv6="2400:da00::6666" set_dns ;; 0) echo "已取消" ;; *) echo "无效的选择,请输入 0、1 或 2。" ;; esac else echo "DNS设置未更改" fi ;; 10) root_use # 内核未编译 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 echo "当前网络优先级设置: IPv4 优先" else echo "当前网络优先级设置: IPv6 优先" fi echo "------------------------" echo "" echo "切换的网络优先级" echo "------------------------" echo "1. IPv4 优先 2. IPv6 优先" echo "------------------------" read -p "选择优先的网络: " choice case $choice in 1) sysctl -w net.ipv6.conf.all.disable_ipv6=1 > /dev/null 2>&1 echo "已切换为 IPv4 优先" ;; 2) sysctl -w net.ipv6.conf.all.disable_ipv6=0 > /dev/null 2>&1 echo "已切换为 IPv6 优先" ;; *) echo "无效的选择" ;; esac ;; 11) clear 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 # 获取当前交换空间信息(强制 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 else swap_percentage=$((swap_used * 100 / swap_total)) fi swap_info="${swap_used}MB/${swap_total}MB (${swap_percentage}%)" echo "当前虚拟内存: $swap_info" read -p "是否调整大小?(Y/N): " choice case "$choice" in [Yy]) # 输入新的虚拟内存大小 read -p "请输入虚拟内存大小MB: " new_swap add_swap ;; [Nn]) echo "已取消" ;; *) echo "无效的选择,请输入 Y 或 N。" ;; esac ;; 13) while true; do root_use # 显示所有用户、用户权限、用户组和是否在sudoers中 echo "用户列表" echo "----------------------------------------------------------------------------" 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 echo "" echo "账户操作" echo "------------------------" echo "1. 创建普通账户 2. 创建高级账户" echo "------------------------" echo "3. 授予 sudo 权限 4. 撤销脚本授予的 sudo 权限" echo "------------------------" echo "5. 删除账号" echo "------------------------" echo "0. 返回上一级选单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) # 提示用户输入新用户名 read -p "请输入新用户名: " new_username 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 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 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 if revoke_sudo "$username"; then echo "操作已完成。" fi ;; 5) read -p "请输入要删除的用户名: " 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) break # 跳出循环,退出菜单 ;; *) break # 跳出循环,退出菜单 ;; esac done ;; 14) clear echo "随机用户名" echo "------------------------" for i in {1..5}; do username="user$(< /dev/urandom tr -dc _a-z0-9 | head -c6)" echo "随机用户名 $i: $username" done echo "" echo "随机姓名" echo "------------------------" first_names=("John" "Jane" "Michael" "Emily" "David" "Sophia" "William" "Olivia" "James" "Emma" "Ava" "Liam" "Mia" "Noah" "Isabella") last_names=("Smith" "Johnson" "Brown" "Davis" "Wilson" "Miller" "Jones" "Garcia" "Martinez" "Williams" "Lee" "Gonzalez" "Rodriguez" "Hernandez") # 生成5个随机用户姓名 for i in {1..5}; do first_name_index=$((RANDOM % ${#first_names[@]})) last_name_index=$((RANDOM % ${#last_names[@]})) user_name="${first_names[$first_name_index]} ${last_names[$last_name_index]}" echo "随机用户姓名 $i: $user_name" done echo "" echo "随机UUID" echo "------------------------" for i in {1..5}; do uuid=$(cat /proc/sys/kernel/random/uuid) echo "随机UUID $i: $uuid" done echo "" echo "16位随机密码" echo "------------------------" for i in {1..5}; do password=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c16) echo "随机密码 $i: $password" done echo "" echo "32位随机密码" echo "------------------------" for i in {1..5}; do password=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c32) echo "随机密码 $i: $password" done echo "" ;; 15) root_use while true; do echo "系统时间信息" # 获取当前系统时区 timezone=$(current_timezone) # 获取当前系统时间 current_time=$(date +"%Y-%m-%d %H:%M:%S") # 显示时区和时间 echo "当前系统时区:$timezone" echo "当前系统时间:$current_time" echo "" echo "时区切换" echo "亚洲------------------------" echo "1. 中国上海时间 2. 中国香港时间" echo "3. 日本东京时间 4. 韩国首尔时间" echo "5. 新加坡时间 6. 印度加尔各答时间" echo "7. 阿联酋迪拜时间 8. 澳大利亚悉尼时间" echo "欧洲------------------------" echo "11. 英国伦敦时间 12. 法国巴黎时间" echo "13. 德国柏林时间 14. 俄罗斯莫斯科时间" echo "15. 荷兰尤特赖赫特时间 16. 西班牙马德里时间" echo "美洲------------------------" echo "21. 美国西部时间 22. 美国东部时间" echo "23. 加拿大时间 24. 墨西哥时间" echo "25. 巴西时间 26. 阿根廷时间" echo "------------------------" echo "0. 返回上一级选单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) set_timedate Asia/Shanghai ;; 2) set_timedate Asia/Hong_Kong ;; 3) set_timedate Asia/Tokyo ;; 4) set_timedate Asia/Seoul ;; 5) set_timedate Asia/Singapore ;; 6) set_timedate Asia/Kolkata ;; 7) set_timedate Asia/Dubai ;; 8) set_timedate Australia/Sydney ;; 11) set_timedate Europe/London ;; 12) set_timedate Europe/Paris ;; 13) set_timedate Europe/Berlin ;; 14) set_timedate Europe/Moscow ;; 15) set_timedate Europe/Amsterdam ;; 16) set_timedate Europe/Madrid ;; 21) set_timedate America/Los_Angeles ;; 22) set_timedate America/New_York ;; 23) set_timedate America/Vancouver ;; 24) set_timedate America/Mexico_City ;; 25) set_timedate America/Sao_Paulo ;; 26) set_timedate America/Argentina/Buenos_Aires ;; 0) break ;; # 跳出循环,退出菜单 *) break ;; # 跳出循环,退出菜单 esac done ;; 16) bbrv3 ;; 18) root_use current_hostname=$(hostname) echo "当前主机名: $current_hostname" read -p "是否要更改主机名?(y/n): " answer if [[ "${answer,,}" == "y" ]]; then # 获取新的主机名 read -p "请输入新的主机名: " new_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 else err_msg "主机名格式不合法。未更改主机名。" fi else echo "未更改主机名。" fi ;; 19) root_use # 直接复用脚本开头已解析好的 OS_ID,避免 source /etc/os-release # 把 ID/VERSION/PRETTY_NAME 等一并注入当前 shell 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/" ;; *) 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() { 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() { 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() { local mode="$1" file tmp_file rollback_dir saved failed=0 processed=0 is_deb822 case "$mode" in mirror|official) ;; *) 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 "$OS_ID" in ubuntu) echo "Ubuntu 更新源切换脚本" echo "------------------------" ;; debian) echo "Debian 更新源切换脚本" echo "------------------------" ;; *) err_msg "未知系统,无法执行脚本" break ;; esac echo "1. 切换到阿里云源" echo "2. 切换到官方源" echo "------------------------" echo "3. 备份初始更新源(已备份则跳过)" echo "4. 还原初始更新源" echo "------------------------" echo "0. 返回上一级" echo "------------------------" read -p "请选择操作: " choice case $choice in 1) if ensure_source_backup && switch_source mirror; then echo "已切换到阿里云源" fi ;; 2) if ensure_source_backup && switch_source official; then echo "已切换到官方源" fi ;; 3) backup_sources ;; 4) restore_initial_source ;; 0) break ;; *) echo "无效的选择,请重新输入" ;; esac break_end done ;; 20) while true; do clear echo "定时任务列表" crontab -l 2>/dev/null echo "" echo "操作" echo "------------------------" echo "1. 添加定时任务 2. 删除定时任务 3. 编辑定时任务" echo "------------------------" echo "0. 返回上一级选单" echo "------------------------" read -p "请输入你的选择: " sub_choice 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. 每小时任务" echo "------------------------" read -p "请输入你的选择: " dingshi case $dingshi in 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 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 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-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 # 跳出 ;; esac ;; 2) read -p "请输入需要删除任务的关键字: " kquest # 关键字为空时 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 ;; 0) break # 跳出循环,退出菜单 ;; *) break # 跳出循环,退出菜单 ;; esac done ;; 21) root_use while true; do echo "本机host解析列表" echo "如果你在这里添加解析匹配,将不再使用动态解析了" cat /etc/hosts echo "" echo "操作" echo "------------------------" echo "1. 添加新的解析 2. 删除解析地址" echo "------------------------" echo "0. 返回上一级选单" echo "------------------------" read -p "请输入你的选择: " host_dns case $host_dns in 1) read -p "请输入新的解析记录 格式: 110.25.5.33 kejilion.pro : " addhost if [ -z "$addhost" ]; then err_msg "解析记录不能为空。" else echo "$addhost" >> /etc/hosts fi ;; 2) read -p "请输入需要删除的解析内容关键字: " delhost # 关键字为空时 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 # 跳出循环,退出菜单 ;; *) break # 跳出循环,退出菜单 ;; esac done ;; 22) root_use if docker inspect fail2ban &>/dev/null ; then while true; do clear echo "SSH防御程序已启动" echo "------------------------" echo "1. 查看SSH拦截记录" echo "2. 日志实时监控" echo "------------------------" echo "9. 卸载防御程序" echo "------------------------" echo "0. 退出" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) echo "------------------------" f2b_sshd echo "------------------------" ;; 2) tail -f /path/to/fail2ban/config/log/fail2ban/fail2ban.log break ;; 9) docker rm -f fail2ban rm -rf /path/to/fail2ban echo "Fail2Ban防御程序已卸载" break ;; 0) break ;; *) echo "无效的选择,请重新输入。" ;; esac break_end done elif [ -x "$(command -v fail2ban-client)" ] ; then clear echo "卸载旧版fail2ban" read -p "确定继续吗?(Y/N): " choice case "$choice" in [Yy]) remove fail2ban rm -rf /etc/fail2ban echo "Fail2Ban防御程序已卸载" ;; [Nn]) echo "已取消" ;; *) echo "无效的选择,请输入 Y 或 N。" ;; esac else clear echo "fail2ban是一个SSH防止暴力破解工具" echo "官网介绍: https://github.com/fail2ban/fail2ban" echo "------------------------------------------------" echo "工作原理:研判非法IP恶意高频访问SSH端口,自动进行IP封锁" echo "------------------------------------------------" read -p "确定继续吗?(Y/N): " choice case "$choice" in [Yy]) clear 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]) echo "已取消" ;; *) echo "无效的选择,请输入 Y 或 N。" ;; esac fi ;; 23) root_use echo "当前流量使用情况,重启服务器流量计算会清零!" output_status echo "$output" # 检查是否存在 Limiting_Shut_down.sh 文件 if [ -f ~/Limiting_Shut_down.sh ]; then # 获取 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}" fi echo echo "------------------------------------------------" echo "系统每分钟检测一次流量,接收或发送任一方向到达阈值就会自动关闭服务器!每月1日重置流量重启服务器。" read -p "1. 开启限流关机功能 2. 停用限流关机功能 0. 退出 : " Limiting case "$Limiting" in 1) # 输入新的流量阈值 echo "按单向流量计算(多数 VPS 只计出站)。如果实际服务器就100G流量,可设置阈值为95G,提前关机,以免出现流量误差或溢出." read -p "请输入流量阈值(单位为GB): " threshold_gb 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) 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 "无效的选择,请输入 0、1 或 2。" ;; esac ;; 31) clear install sshpass remote_ip="66.42.61.110" remote_user="liaotian123" remote_file="/home/liaotian123/liaotian.txt" password="kejilionYYDS" # 替换为您的密码 clear echo "科技lion留言板" echo "------------------------" # 显示已有的留言内容 sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_ip}" "cat '${remote_file}'" echo "" echo "------------------------" # 判断是否要留言 read -p "是否要留言?(y/n): " leave_message if [ "$leave_message" == "y" ] || [ "$leave_message" == "Y" ]; then # 输入新的留言内容 read -p "输入你的昵称: " nicheng read -p "输入你的聊天内容: " neirong # 添加新留言到远程文件 sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_ip}" "echo -e '${nicheng}: ${neirong}' >> '${remote_file}'" echo "已添加留言: " echo "${nicheng}: ${neirong}" echo "" else echo "您选择了不留言。" fi echo "留言板操作完成。" ;; 99) clear server_reboot ;; 0) kejilion ;; *) echo "无效的输入!" ;; esac break_end done ;; 14) clear while true; do clear echo "▶ VPS集群控制" echo "你可以远程操控多台VPS一起执行任务(仅支持Ubuntu/Debian)" echo "------------------------" echo "1. 安装集群环境" echo "------------------------" echo "2. 集群控制中心" echo "------------------------" echo "7. 备份集群环境" echo "8. 还原集群环境" echo "9. 卸载集群环境" echo "------------------------" echo "0. 返回主菜单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) clear 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 "集群服务器列表" 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 "操作" echo "------------------------" echo "1. 添加服务器 2. 删除服务器 3. 编辑服务器" echo "------------------------" echo "11. 安装科技lion脚本 12. 更新系统 13. 清理系统" echo "15. 安装BBR3 16. 设置1G虚拟内存" echo "17. 设置时区到上海 18. 开放所有端口" echo "------------------------" echo "51. 自定义指令" echo "------------------------" echo "0. 返回上一级选单" echo "------------------------" read -p "请输入你的选择: " sub_choice case $sub_choice in 1) read -p "服务器名称: " server_name read -p "服务器IP: " server_ip read -p "服务器端口(22): " server_port server_port=${server_port:-22} read -p "服务器用户名(root): " server_username server_username=${server_username:-root} read -s -p "服务器用户密码: " server_password echo "" 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 # 通过解析 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 nano ~/cluster/servers.py ;; 11) py_task=install_kejilion.py cluster_python3 ;; 12) py_task=update.py cluster_python3 ;; 13) py_task=clean.py cluster_python3 ;; 15) py_task=install_bbr3.py cluster_python3 ;; 16) py_task=swap1024.py cluster_python3 ;; 17) py_task=time_shanghai.py cluster_python3 ;; 18) py_task=firewall_close.py cluster_python3 ;; 51) read -p "请输入批量执行的命令: " mingling if [ -z "$mingling" ]; then err_msg "命令不能为空。" break fi py_task=custom_tasks.py 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 # 跳出循环,退出菜单 ;; *) break # 跳出循环,退出菜单 ;; esac done ;; 7) clear if [ ! -f ~/cluster/servers.py ]; then err_msg "集群环境尚未安装,请先执行“1. 安装集群环境”。" else echo "将下载服务器列表数据,按任意键下载!" read -n 1 -s -r -p "" sz -y ~/cluster/servers.py fi ;; 8) clear echo "请上传您的servers.py,按任意键开始上传!" read -n 1 -s -r -p "" mkdir -p ~/cluster && (cd ~/cluster/ && rz -y) ;; 9) clear read -p "请先备份环境,确定要卸载集群控制环境吗?(Y/N): " choice case "$choice" in [Yy]) remove python3-paramiko speedtest-cli lrzsz rm -rf ~/cluster/ ;; [Nn]) echo "已取消" ;; *) echo "无效的选择,请输入 Y 或 N。" ;; esac ;; 0) kejilion ;; *) echo "无效的输入!" ;; esac break_end done ;; 00) clear echo "更新日志" echo "------------------------" 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 "------------------------" 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 [ -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}" echo "------------------------" read -p "确定更新脚本吗?(Y/N): " choice case "$choice" in [Yy]) clear # 在脚本所在目录下载并原子替换,避免更新失败时截断当前脚本 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 "已取消" ;; *) ;; esac fi ;; 0) clear exit ;; *) echo "无效的输入!" ;; esac break_end done } if [ "$#" -eq 0 ]; then # 如果没有参数,运行交互式逻辑 kejilion_sh else # 如果有参数,执行相应函数 case $1 in install|add|安装) shift install "$@" ;; remove|del|卸载) shift remove "$@" ;; update|更新) linux_update ;; clean|清理) linux_clean ;; bbr3|bbrv3) bbrv3 ;; *) echo "无效参数" ;; esac fi