← Back

alpine-hy2

适用于 Alpine Linux 的 Hysteria 2 自动化部署脚本 (musl + OpenRC)

curl -fsSL https://scripts.kyuhou.org/alpine-hy2 | sh
alpine-hy2.sh
#!/bin/sh
set -e

# 终端 ANSI 颜色定义 (兼容 POSIX sh / BusyBox ash)
if [ -t 1 ]; then
    C_RESET="\033[0m"
    C_BOLD="\033[1m"
    C_RED="\033[31m"
    C_GREEN="\033[32m"
    C_YELLOW="\033[33m"
    C_BLUE="\033[34m"
    C_CYAN="\033[36m"
    C_GRAY="\033[90m"
else
    C_RESET=""
    C_BOLD=""
    C_RED=""
    C_GREEN=""
    C_YELLOW=""
    C_BLUE=""
    C_CYAN=""
    C_GRAY=""
fi

# 基础目录与常量
INSTALL_DIR="/usr/local/bin"
CONF_DIR="/etc/sing-box"
CERT_DIR="/etc/sing-box/cert"
WORK_DIR="/root/sb_download_cache"

# 异常退出与清理 Trap
on_exit() {
    rm -rf "$WORK_DIR" 2>/dev/null || true
}

on_interrupt() {
    on_exit
    printf "\n${C_RED}❌ 用户已取消操作,安装退出。${C_RESET}\n" >&2
    exit 130
}

trap on_exit EXIT
trap on_interrupt INT TERM

# 安全的交互输入读取函数 (支持直接运行或 curl 管道直连,绝不篡改标准输入流)
ui_read() {
    if [ -r /dev/tty ]; then
        read -r "$@" < /dev/tty
    else
        read -r "$@"
    fi
}

# 交互式输出辅助函数
ui_banner() {
    clear 2>/dev/null || true
    printf "${C_CYAN}${C_BOLD}"
    printf "==========================================================\n"
    printf "    %s\n" "$1"
    printf "==========================================================\n"
    printf "${C_RESET}\n"
}

ui_step() {
    printf "\n${C_BOLD}${C_BLUE}>>> [%s] %s${C_RESET}\n" "$1" "$2"
}

ui_info() {
    printf "  ${C_GRAY}➜${C_RESET} %s\n" "$1"
}

ui_success() {
    printf "  ${C_GREEN}✔ %s${C_RESET}\n" "$1"
}

ui_warn() {
    printf "  ${C_YELLOW}⚠️  %s${C_RESET}\n" "$1"
}

ui_error() {
    printf "  ${C_RED}❌ %s${C_RESET}\n" "$1"
}

# ---------- 探测系统已有历史配置与证书 ----------
detect_existing() {
    EXIST_DOMAIN=""
    HAS_VALID_CERT=0
    CERT_FILE="$CERT_DIR/server.crt"
    KEY_FILE="$CERT_DIR/server.key"

    # 1. 探测已有有效证书
    if [ -s "$CERT_FILE" ] && [ -s "$KEY_FILE" ]; then
        if command -v openssl >/dev/null 2>&1; then
            if openssl x509 -checkend 86400 -noout -in "$CERT_FILE" >/dev/null 2>&1; then
                EXIST_DOMAIN=$(openssl x509 -noout -text -in "$CERT_FILE" 2>/dev/null | grep -oE "DNS:[^ ,]+" | head -n 1 | cut -d: -f2 || true)
                if [ -n "$EXIST_DOMAIN" ]; then
                    HAS_VALID_CERT=1
                fi
            fi
        fi
    fi

    # 2. 探测现有 sing-box 配置文件中的历史参数
    EXIST_PORT=""
    EXIST_PASS=""
    EXIST_UUID=""
    if [ -f "$CONF_DIR/config.json" ]; then
        if [ -z "$EXIST_DOMAIN" ]; then
            EXIST_DOMAIN=$(grep -oE '"server_name": *"[^"]+"' "$CONF_DIR/config.json" 2>/dev/null | head -n 1 | cut -d'"' -f4 || true)
        fi
        EXIST_PORT=$(grep -oE '"listen_port": *[0-9]+' "$CONF_DIR/config.json" 2>/dev/null | head -n 1 | awk '{print $NF}' || true)
        EXIST_PASS=$(grep -oE '"password": *"[^"]+"' "$CONF_DIR/config.json" 2>/dev/null | head -n 1 | cut -d'"' -f4 || true)
        EXIST_UUID=$(grep -oE '"uuid": *"[^"]+"' "$CONF_DIR/config.json" 2>/dev/null | head -n 1 | cut -d'"' -f4 || true)
    fi

    # 3. 探测 acme.sh 已经保存的 Cloudflare 凭据
    ACME_CONF="${HOME:-/root}/.acme.sh/account.conf"
    SAVED_CF_TOKEN=""
    SAVED_CF_KEY=""
    SAVED_CF_EMAIL=""
    if [ -f "$ACME_CONF" ]; then
        SAVED_CF_TOKEN=$(grep -E "^(SAVED_)?CF_Token=" "$ACME_CONF" 2>/dev/null | head -n 1 | cut -d"'" -f2 | tr -d '"' || true)
        SAVED_CF_KEY=$(grep -E "^(SAVED_)?CF_Key=" "$ACME_CONF" 2>/dev/null | head -n 1 | cut -d"'" -f2 | tr -d '"' || true)
        SAVED_CF_EMAIL=$(grep -E "^(SAVED_)?CF_Email=" "$ACME_CONF" 2>/dev/null | head -n 1 | cut -d"'" -f2 | tr -d '"' || true)
    fi
}

# ---------- 1. 用户交互输入 (Hysteria 2) ----------
ui_banner "sing-box (Hysteria 2) 安装脚本"

# 自动嗅探系统已有历史配置与证书
detect_existing

printf "${C_BOLD}📝 请填写部署配置参数:${C_RESET}\n\n"

# 1.1 域名输入与已有证书复用判断
SKIP_CF_INPUT=0
USER_DOMAIN=""
if [ "$HAS_VALID_CERT" -eq 1 ] && [ -n "$EXIST_DOMAIN" ]; then
    ui_success "检测到系统已存在有效域名证书: ${EXIST_DOMAIN} (有效期正常)"
    printf " ${C_CYAN}?${C_RESET} 是否直接复用该域名与证书?[Y/n] ${C_GRAY}[默认: Y]${C_RESET}: "
    ui_read REUSE_CERT
    REUSE_CERT=$(echo "$REUSE_CERT" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
    if [ "$REUSE_CERT" != "n" ]; then
        USER_DOMAIN="$EXIST_DOMAIN"
        SKIP_CF_INPUT=1
        ui_success "已选定复用域名: ${USER_DOMAIN},将自动跳过 Cloudflare 凭据配置!"
    fi
fi

if [ -z "$USER_DOMAIN" ]; then
    while true; do
        if [ -n "$EXIST_DOMAIN" ]; then
            printf "\n ${C_CYAN}?${C_RESET} 请输入你要绑定的域名 ${C_GRAY}[回车使用已有: %s]${C_RESET}: " "$EXIST_DOMAIN"
            ui_read USER_DOMAIN
            USER_DOMAIN=$(echo "$USER_DOMAIN" | tr -d '[:space:]')
            USER_DOMAIN="${USER_DOMAIN:-$EXIST_DOMAIN}"
        else
            printf "\n ${C_CYAN}?${C_RESET} 请输入你要绑定的域名 ${C_GRAY}(例如: node.example.com)${C_RESET}: "
            ui_read USER_DOMAIN
            USER_DOMAIN=$(echo "$USER_DOMAIN" | tr -d '[:space:]')
        fi
        if [ -n "$USER_DOMAIN" ]; then
            break
        fi
        ui_warn "域名不能为空,请重新输入!"
    done
fi

# 1.2 Cloudflare API 认证方式选择与输入 (若已复用有效证书则完全跳过此步骤)
if [ "$SKIP_CF_INPUT" -ne 1 ]; then
    printf "\n ${C_BOLD}请选择 Cloudflare API 认证方式:${C_RESET}\n"
    printf "  ${C_CYAN}1)${C_RESET} API Token ${C_GRAY}(推荐,仅需 DNS:Edit 权限)${C_RESET}\n"
    printf "  ${C_CYAN}2)${C_RESET} Global API Key ${C_GRAY}(全局 API 密钥,需配合 CF 账号邮箱)${C_RESET}\n"
    while true; do
        printf " ${C_CYAN}?${C_RESET} 请输入选项 [1-2] ${C_GRAY}[默认: 1]${C_RESET}: "
        ui_read CF_AUTH_TYPE
        CF_AUTH_TYPE=$(echo "$CF_AUTH_TYPE" | tr -d '[:space:]')
        CF_AUTH_TYPE="${CF_AUTH_TYPE:-1}"
        if [ "$CF_AUTH_TYPE" = "1" ] || [ "$CF_AUTH_TYPE" = "2" ]; then
            break
        fi
        ui_warn "选项无效,请输入 1 或 2!"
    done

    if [ "$CF_AUTH_TYPE" = "2" ]; then
        while true; do
            if [ -n "$SAVED_CF_EMAIL" ]; then
                printf " ${C_CYAN}?${C_RESET} 请输入 Cloudflare 账号邮箱 ${C_GRAY}[回车使用已保存: %s]${C_RESET}: " "$SAVED_CF_EMAIL"
                ui_read CF_Email
                CF_Email=$(echo "$CF_Email" | tr -d '[:space:]')
                CF_Email="${CF_Email:-$SAVED_CF_EMAIL}"
            else
                printf " ${C_CYAN}?${C_RESET} 请输入 Cloudflare 账号邮箱 (CF_Email): "
                ui_read CF_Email
                CF_Email=$(echo "$CF_Email" | tr -d '[:space:]')
            fi
            if [ -n "$CF_Email" ]; then
                break
            fi
            ui_warn "邮箱不能为空,请重新输入!"
        done

        while true; do
            if [ -n "$SAVED_CF_KEY" ]; then
                printf " ${C_CYAN}?${C_RESET} 请输入 Cloudflare Global API Key ${C_GRAY}[回车使用已保存凭证]${C_RESET}: "
                ui_read CF_Key
                CF_Key=$(echo "$CF_Key" | tr -d '[:space:]')
                CF_Key="${CF_Key:-$SAVED_CF_KEY}"
            else
                printf " ${C_CYAN}?${C_RESET} 请输入 Cloudflare Global API Key: "
                ui_read CF_Key
                CF_Key=$(echo "$CF_Key" | tr -d '[:space:]')
            fi
            if [ -n "$CF_Key" ]; then
                break
            fi
            ui_warn "Global API Key 不能为空,请重新输入!"
        done
        export CF_Email="$CF_Email"
        export CF_Key="$CF_Key"
    else
        while true; do
            if [ -n "$SAVED_CF_TOKEN" ]; then
                printf " ${C_CYAN}?${C_RESET} 请输入 Cloudflare API Token ${C_GRAY}[回车使用已保存凭证]${C_RESET}: "
                ui_read CF_Token
                CF_Token=$(echo "$CF_Token" | tr -d '[:space:]')
                CF_Token="${CF_Token:-$SAVED_CF_TOKEN}"
            else
                printf " ${C_CYAN}?${C_RESET} 请输入 Cloudflare API Token (CF_Token): "
                ui_read CF_Token
                CF_Token=$(echo "$CF_Token" | tr -d '[:space:]')
            fi
            if [ -n "$CF_Token" ]; then
                break
            fi
            ui_warn "API Token 不能为空,请重新输入!"
        done
        export CF_Token="$CF_Token"
    fi
fi

# 1.3 端口输入与校验 (优先使用已有端口或 20000)
DEF_PORT="${EXIST_PORT:-20000}"
while true; do
    printf " ${C_CYAN}?${C_RESET} 请输入 Hysteria 2 监听端口 ${C_GRAY}[默认: %s]${C_RESET}: " "$DEF_PORT"
    ui_read INPUT_PORT
    INPUT_PORT=$(echo "$INPUT_PORT" | tr -d '[:space:]')
    PORT="${INPUT_PORT:-$DEF_PORT}"
    case "$PORT" in
        ''|*[!0-9]*)
            ui_warn "端口必须为纯数字,请重新输入!"
            continue
            ;;
        *)
            if [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ]; then
                break
            else
                ui_warn "端口超出合法范围 (1-65535),请重新输入!"
                continue
            fi
            ;;
    esac
done

# 1.4 连接密码输入 (优先使用已有密码或随机生成)
if [ -n "$EXIST_PASS" ]; then
    GEN_PASS="$EXIST_PASS"
else
    GEN_PASS=$(tr -dc 'a-zA-Z0-9' < /dev/urandom 2>/dev/null | head -c 16 || echo "Pass$(date +%s)")
fi
printf " ${C_CYAN}?${C_RESET} 请输入连接密码 ${C_GRAY}[回车使用: %s]${C_RESET}: " "$GEN_PASS"
ui_read INPUT_PASS
INPUT_PASS=$(echo "$INPUT_PASS" | tr -d '[:space:]')
PASSWORD="${INPUT_PASS:-$GEN_PASS}"

# 1.5 输入确认面板
printf "\n${C_BOLD}${C_CYAN}------------------- 配置信息确认 -------------------${C_RESET}\n"
printf "  ${C_BOLD}节 点 协 议${C_RESET} : ${C_GREEN}Hysteria 2${C_RESET}\n"
printf "  ${C_BOLD}绑 定 域 名${C_RESET} : ${C_GREEN}%s${C_RESET}\n" "$USER_DOMAIN"
if [ "$SKIP_CF_INPUT" -eq 1 ]; then
    printf "  ${C_BOLD}CF 认证凭证${C_RESET} : ${C_GREEN}自动复用本地已有有效证书 (跳过 DNS 验证)${C_RESET}\n"
elif [ "$CF_AUTH_TYPE" = "2" ]; then
    printf "  ${C_BOLD}CF 认证方式${C_RESET} : ${C_GREEN}Global API Key (${CF_Email})${C_RESET}\n"
else
    printf "  ${C_BOLD}CF 认证方式${C_RESET} : ${C_GREEN}API Token${C_RESET}\n"
fi
printf "  ${C_BOLD}监 听 端 口${C_RESET} : ${C_GREEN}%s${C_RESET}\n" "$PORT"
printf "  ${C_BOLD}认 证 密 码${C_RESET} : ${C_GREEN}%s${C_RESET}\n" "$PASSWORD"
printf "${C_BOLD}${C_CYAN}----------------------------------------------------${C_RESET}\n"
printf "按 ${C_BOLD}[Enter]${C_RESET} 开始自动化部署,或按 ${C_BOLD}[Ctrl+C]${C_RESET} 取消退出..."
ui_read CONFIRM_CONTINUE || true

# ---------- 2. 安装核心系统依赖 (Alpine Linux) ----------
SB_TARGET="musl"
RELOAD_CMD="rc-service sing-box restart 2>/dev/null || true"

ui_step "1/6" "安装最小核心系统依赖 (Alpine)..."
ui_info "正在更新包索引并安装基础依赖 (curl, openssl, ca-certificates, cronie, openrc)..."
apk update
apk add --no-cache curl openssl ca-certificates cronie openrc
ui_success "核心系统依赖安装完成"

# 确保 OpenRC 运行环境就绪
[ -d /run/openrc ] || mkdir -p /run/openrc
touch /run/openrc/softlevel 2>/dev/null || true

# 适配 Cron 服务名称
CRON_SVC="cronie"
if [ ! -f /etc/init.d/cronie ] && [ -f /etc/init.d/crond ]; then
    CRON_SVC="crond"
fi

rc-update add "$CRON_SVC" default 2>/dev/null || true
rc-service "$CRON_SVC" restart 2>/dev/null || rc-service "$CRON_SVC" start 2>/dev/null || true
ui_success "Cron 定时服务 ($CRON_SVC) 配置并运行成功"

# ---------- 3. 安装与配置 acme.sh 自动化证书环境 ----------
ui_step "2/6" "配置 acme.sh 自动化证书环境 (匿名注册)..."
ACME_DIR="${HOME:-/root}/.acme.sh"
mkdir -p "$CERT_DIR"

# 检查本地是否已存在当前域名的有效证书 (支持中断重跑自动跳过,避免 Let's Encrypt 限额)
SKIP_CERT_ISSUE=0
if [ -s "$CERT_DIR/server.key" ] && [ -s "$CERT_DIR/server.crt" ]; then
    CERT_VALID=1
    CERT_MATCH=1
    if command -v openssl >/dev/null 2>&1; then
        # 检查证书是否包含当前绑定的域名
        if ! openssl x509 -noout -text -in "$CERT_DIR/server.crt" 2>/dev/null | grep -qE "DNS:${USER_DOMAIN}\b"; then
            CERT_MATCH=0
        fi
        # 检查证书剩余有效期 (至少大于 1 天)
        if ! openssl x509 -checkend 86400 -noout -in "$CERT_DIR/server.crt" >/dev/null 2>&1; then
            CERT_VALID=0
        fi
    fi

    if [ "$CERT_MATCH" -eq 1 ] && [ "$CERT_VALID" -eq 1 ]; then
        ui_success "检测到已有有效证书 ($CERT_DIR/server.crt),自动复用并跳过申请!"
        SKIP_CERT_ISSUE=1
    else
        ui_warn "检测到旧证书已过期或域名不匹配,准备重新申请证书..."
    fi
fi

if [ "$SKIP_CERT_ISSUE" -ne 1 ]; then
    if [ ! -f "$ACME_DIR/acme.sh" ]; then
        ui_info "正在在线安装 acme.sh..."
        curl -sSL https://get.acme.sh | sh
    fi
    ACME_BIN="$ACME_DIR/acme.sh"

    ui_info "设置证书颁发机构为 Let's Encrypt 并匿名注册账号..."
    "$ACME_BIN" --set-default-ca --server letsencrypt
    "$ACME_BIN" --register-account || true

    ui_info "正在通过 Cloudflare DNS-01 验证申请证书..."
    if ! "$ACME_BIN" --issue --dns dns_cf -d "$USER_DOMAIN" --force; then
        ui_warn "DNS-01 证书申请初次未通过,正在按需安装 socat 并重试..."
        if command -v apk >/dev/null 2>&1; then
            apk add --no-cache socat
        elif command -v apt-get >/dev/null 2>&1; then
            export DEBIAN_FRONTEND=noninteractive
            apt-get install -y --no-install-recommends socat
        fi
        "$ACME_BIN" --issue --dns dns_cf -d "$USER_DOMAIN" --force
    fi

    RELOAD_CMD="${RELOAD_CMD:-rc-service sing-box restart 2>/dev/null || systemctl restart sing-box 2>/dev/null || true}"

    ui_info "安装证书至 ${CERT_DIR}..."
    "$ACME_BIN" --install-cert -d "$USER_DOMAIN" \
        --key-file "$CERT_DIR/server.key" \
        --fullchain-file "$CERT_DIR/server.crt" \
        --reloadcmd "$RELOAD_CMD"
    ui_success "证书申请与自动续签绑定完成"
fi

# ---------- 4. 下载并部署 sing-box 二进制 ----------
ui_step "3/6" "下载并部署 sing-box 官方发布版本..."

ARCH=$(uname -m)
case "$ARCH" in
    x86_64|amd64)   SB_ARCH="amd64" ;;
    aarch64|arm64)  SB_ARCH="arm64" ;;
    armv7l|armhf)   SB_ARCH="armv7" ;;
    *) ui_error "不支持的 CPU 架构: $ARCH"; exit 1 ;;
esac

# 自动判断目标二进制类型 (musl vs glibc)
if [ -z "$SB_TARGET" ]; then
    if [ -f /etc/alpine-release ]; then
        SB_TARGET="musl"
    else
        SB_TARGET="glibc"
    fi
fi

ui_info "检测到硬件架构: $ARCH (sing-box 目标架构: $SB_ARCH, 类型: $SB_TARGET)"

# 检查本地是否已经安装了有效的 sing-box 内核 (支持重跑跳过重复下载)
SKIP_SB_DOWNLOAD=0
if [ -x "$INSTALL_DIR/sing-box" ]; then
    CURRENT_SB_VER=$("$INSTALL_DIR/sing-box" version 2>/dev/null | head -n 1 | awk '{print $3}' || true)
    if [ -n "$CURRENT_SB_VER" ]; then
        ui_success "检测到已安装有效 sing-box 内核 (v${CURRENT_SB_VER}),直接复用跳过下载!"
        SKIP_SB_DOWNLOAD=1
    fi
fi

if [ "$SKIP_SB_DOWNLOAD" -ne 1 ]; then
    ui_info "正在查询 sing-box 最新发布版本..."
    LATEST_TAG=""

    # 方式 1: 跟踪 GitHub releases 302 重定向 (免 API 限额,速度最快且最稳定)
    LATEST_TAG=$(curl -sI "https://github.com/SagerNet/sing-box/releases/latest" 2>/dev/null | grep -i '^location:' | head -n 1 | tr -d '\r\n' | awk -F'/' '{print $NF}' || true)

    # 方式 2: 若重定向未取到,安全解析 GitHub API (使用严格的精准字段匹配,避免误匹配 reactions)
    if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "latest" ]; then
        LATEST_JSON=$(curl -sSL "https://api.github.com/repos/SagerNet/sing-box/releases/latest" 2>/dev/null || true)
        LATEST_TAG=$(echo "$LATEST_JSON" | grep -oE '"tag_name": *"[^"]+"' | head -n 1 | cut -d'"' -f4 || true)
    fi

    # 版本格式严格校验 (必须是形如 x.y.z 的纯数字语义化版本,杜绝误匹配到 eyes 等字符串)
    VERSION="${LATEST_TAG#v}"
    case "$VERSION" in
        [0-9]*.[0-9]*.[0-9]*)
            ui_success "成功匹配到版本: v${VERSION}"
            ;;
        *)
            VERSION="1.14.3"
            ui_warn "未解析到有效数字版本号,自动降级启用已验证稳定版: v${VERSION}"
            ;;
    esac

    # 定义通用健壮下载函数 (防 set -e 导致脚本提前退出)
    try_download() {
        dl_url="$1"
        dl_out="$2"

        # 优先使用 curl 下载
        if curl -fSL --connect-timeout 10 --max-time 180 "$dl_url" -o "$dl_out"; then
            return 0
        fi

        # curl 失败时按需安装 wget
        ui_warn "curl 下载失败,尝试检查并安装 wget 进行备用下载..."
        if ! command -v wget >/dev/null 2>&1; then
            if command -v apk >/dev/null 2>&1; then
                apk add --no-cache wget || true
            elif command -v apt-get >/dev/null 2>&1; then
                export DEBIAN_FRONTEND=noninteractive
                apt-get update -y && apt-get install -y --no-install-recommends wget || true
            fi
        fi

        # 使用 wget 重试
        if command -v wget >/dev/null 2>&1; then
            if wget -T 15 -t 3 -O "$dl_out" "$dl_url"; then
                return 0
            fi
        fi

        return 1
    }

    # 构造真实的 Release 二进制下载地址
    if [ "$SB_TARGET" = "musl" ]; then
        DOWNLOAD_URL="https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box-${VERSION}-linux-${SB_ARCH}-musl.tar.gz"
    else
        DOWNLOAD_URL="https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box-${VERSION}-linux-${SB_ARCH}.tar.gz"
    fi

    rm -rf "$WORK_DIR"
    mkdir -p "$WORK_DIR" "$INSTALL_DIR"

    ui_info "正在下载: $DOWNLOAD_URL"
    if ! try_download "$DOWNLOAD_URL" "$WORK_DIR/sing-box.tar.gz"; then
        # 如果最新版下载失败,尝试降级稳定版 1.14.3
        ui_warn "当前版本下载失败,尝试降级稳定基准版本 v1.14.3 重试..."
        if [ "$SB_TARGET" = "musl" ]; then
            DOWNLOAD_URL="https://github.com/SagerNet/sing-box/releases/download/v1.14.3/sing-box-1.14.3-linux-${SB_ARCH}-musl.tar.gz"
        else
            DOWNLOAD_URL="https://github.com/SagerNet/sing-box/releases/download/v1.14.3/sing-box-1.14.3-linux-${SB_ARCH}.tar.gz"
        fi

        if ! try_download "$DOWNLOAD_URL" "$WORK_DIR/sing-box.tar.gz"; then
            ui_error "sing-box 内核下载失败,请检查网络是否能连接 GitHub Release!"
            exit 1
        fi
    fi

    # 校验下载包非空
    if [ ! -s "$WORK_DIR/sing-box.tar.gz" ]; then
        ui_error "下载的文件损坏或大小为 0,安装中止!"
        exit 1
    fi

    ui_info "解压并安装二进制文件..."
    tar -zxvf "$WORK_DIR/sing-box.tar.gz" -C "$WORK_DIR/" >/dev/null
    find "$WORK_DIR" -type f -name "sing-box" -exec mv -f {} "$INSTALL_DIR/sing-box" \;
    chmod +x "$INSTALL_DIR/sing-box"
    rm -rf "$WORK_DIR"
    ui_success "sing-box 已成功安装至 $INSTALL_DIR/sing-box"
fi

# ---------- 5. 写入 sing-box Hysteria 2 配置 ----------
ui_step "4/6" "生成 Hysteria 2 配置文件..."
mkdir -p "$CONF_DIR"

cat << EOF > "$CONF_DIR/config.json"
{
  "log": {
    "disabled": true,
    "level": "fatal",
    "timestamp": false
  },
  "inbounds": [
    {
      "type": "hysteria2",
      "tag": "hy2-in",
      "listen": "::",
      "listen_port": ${PORT},
      "users": [
        {
          "password": "${PASSWORD}"
        }
      ],
      "ignore_client_bandwidth": false,
      "tls": {
        "enabled": true,
        "server_name": "${USER_DOMAIN}",
        "alpn": [
          "h3"
        ],
        "certificate_path": "${CERT_DIR}/server.crt",
        "key_path": "${CERT_DIR}/server.key"
      },
      "masquerade": "https://bing.com"
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    }
  ]
}
EOF

ui_info "验证配置文件正确性..."
"$INSTALL_DIR/sing-box" check -c "$CONF_DIR/config.json"
ui_success "配置文件校验通过: $CONF_DIR/config.json"

# ---------- 6. 配置 OpenRC 服务托管并启动 (Alpine) ----------
ui_step "5/6" "注册 OpenRC 服务托管并启动..."
cat << 'SVC_EOF' > /etc/init.d/sing-box
#!/sbin/openrc-run

name="sing-box"
description="sing-box Service"
command="/usr/local/bin/sing-box"
command_args="run -c /etc/sing-box/config.json"
command_background="true"
pidfile="/run/${RC_SVCNAME}.pid"
output_log="/dev/null"
error_log="/dev/null"

depend() {
    need net
    after firewall
}
SVC_EOF

chmod +x /etc/init.d/sing-box
rc-update add sing-box default 2>/dev/null || true
rc-service sing-box restart 2>/dev/null || rc-service sing-box start 2>/dev/null || true

# 清除旧的日志文件残留
rm -f /var/log/sing-box.log /var/log/sing-box.err
ui_success "sing-box OpenRC 服务已注册并启动"

# ---------- 7. 验证并输出节点信息 (Hysteria 2) ----------
ui_step "6/6" "检查节点服务运行状态..."
sleep 2

STATUS_OK=0
if command -v systemctl >/dev/null 2>&1 && [ -f /etc/systemd/system/sing-box.service ]; then
    systemctl is-active --quiet sing-box && STATUS_OK=1
    SVC_MGR_CMD="systemctl {start|stop|restart|status} sing-box"
else
    rc-service sing-box status 2>/dev/null | grep -qi "started" && STATUS_OK=1
    SVC_MGR_CMD="rc-service sing-box {start|stop|restart|status}"
fi

if [ "$STATUS_OK" -ne 1 ]; then
    ui_error "服务启动失败,请检查配置文件是否正确!"
    exit 1
fi

ui_info "正在查询公网 IP 地址..."
SERVER_IP=$(curl -s4m 5 https://api.ipify.org || curl -s4m 5 https://ifconfig.me || echo "你的公网IP")
HY2_LINK="hysteria2://${PASSWORD}@${USER_DOMAIN}:${PORT}?sni=${USER_DOMAIN}&alpn=h3#Hysteria2-Node"

# 最终成果输出面板
clear 2>/dev/null || true
printf "${C_GREEN}${C_BOLD}"
printf "==========================================================\n"
printf "🎉 sing-box (Hysteria 2) 部署成功!\n"
printf "==========================================================\n"
printf "${C_RESET}"
printf "  ${C_BOLD}服务器地址 (域名) :${C_RESET} ${C_CYAN}%s${C_RESET}\n" "${USER_DOMAIN}"
printf "  ${C_BOLD}服务器真实 IP     :${C_RESET} ${C_CYAN}%s${C_RESET}\n" "${SERVER_IP}"
printf "  ${C_BOLD}连接端口 (UDP)    :${C_RESET} ${C_CYAN}%s${C_RESET}\n" "${PORT}"
printf "  ${C_BOLD}认证密码          :${C_RESET} ${C_CYAN}%s${C_RESET}\n" "${PASSWORD}"
printf "  ${C_BOLD}SNI 伪装域名      :${C_RESET} ${C_CYAN}%s${C_RESET}\n" "${USER_DOMAIN}"
printf "  ${C_BOLD}ALPN              :${C_RESET} ${C_CYAN}h3${C_RESET}\n"
printf "  ${C_BOLD}运行环境          :${C_RESET} ${C_GRAY}%s (%s)${C_RESET}\n" "${SB_TARGET:-native}" "$(uname -s)"
printf "${C_CYAN}----------------------------------------------------------${C_RESET}\n"
printf "${C_BOLD}🔗 Hysteria 2 通用导入链接:${C_RESET}\n\n"
printf "${C_GREEN}%s${C_RESET}\n\n" "${HY2_LINK}"
printf "${C_CYAN}----------------------------------------------------------${C_RESET}\n"
printf "${C_BOLD}📋 sing-box 客户端 Outbound 配置片段:${C_RESET}\n"
cat << CLIENT_EOF
{
  "type": "hysteria2",
  "tag": "hy2-out",
  "server": "${USER_DOMAIN}",
  "server_port": ${PORT},
  "password": "${PASSWORD}",
  "tls": {
    "enabled": true,
    "server_name": "${USER_DOMAIN}",
    "alpn": ["h3"],
    "insecure": false
  }
}
CLIENT_EOF
printf "${C_CYAN}----------------------------------------------------------${C_RESET}\n"
printf "${C_BOLD}📋 Mihomo (Clash Meta) 客户端 Proxy 配置片段:${C_RESET}\n"
cat << MIHOMO_EOF
- name: "Hysteria2-Node"
  type: hysteria2
  server: ${USER_DOMAIN}
  port: ${PORT}
  password: ${PASSWORD}
  sni: ${USER_DOMAIN}
  alpn:
    - h3
  skip-cert-verify: false
MIHOMO_EOF
printf "${C_CYAN}==========================================================${C_RESET}\n"
printf "管理服务命令: ${C_YELLOW}%s${C_RESET}\n" "${SVC_MGR_CMD}"
printf "${C_CYAN}==========================================================${C_RESET}\n"