适用于 Debian / Ubuntu 等 systemd 体系的 Hysteria 2 自动化部署脚本 (glibc)
curl -fsSL https://scripts.kyuhou.org/debian-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. 安装核心系统依赖 (Debian / Ubuntu) ----------
SB_TARGET="glibc"
RELOAD_CMD="systemctl restart sing-box 2>/dev/null || true"
ui_step "1/6" "安装最小核心系统依赖 (Debian / Ubuntu)..."
ui_info "正在更新包索引并安装基础依赖 (curl, openssl, ca-certificates, cron, systemd, tar)..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y --no-install-recommends curl openssl ca-certificates cron systemd tar procps
ui_success "核心系统依赖安装完成"
# 启动并自启 cron 服务以支持证书自动续签
systemctl enable cron 2>/dev/null || systemctl enable crond 2>/dev/null || true
systemctl restart cron 2>/dev/null || systemctl restart crond 2>/dev/null || systemctl start cron 2>/dev/null || true
ui_success "Cron 定时服务配置并运行成功"
# ---------- 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. 配置 Systemd 服务托管并启动 (Debian / Ubuntu) ----------
ui_step "5/6" "注册 Systemd 服务托管并启动..."
cat << 'SVC_EOF' > /etc/systemd/system/sing-box.service
[Unit]
Description=sing-box service
Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target
[Service]
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
ExecStart=/usr/local/bin/sing-box run -c /etc/sing-box/config.json
Restart=on-failure
RestartSec=5s
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target
SVC_EOF
systemctl daemon-reload
systemctl enable sing-box 2>/dev/null || true
systemctl restart sing-box 2>/dev/null || systemctl start sing-box 2>/dev/null || true
ui_success "sing-box Systemd 服务已注册并启动"
# ---------- 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"