Files
fanqiang/sing-box-plus.sh
2026-03-28 00:10:58 +08:00

1269 lines
50 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# ============================================================
# Sing-Box-Plus 管理脚本18 节点:直连 9 + WARP 9
# Version: v4.4.0
# authorAlvin9999
# Repo: https://github.com/Alvin9999-newpac/Sing-Box-Plus
# ============================================================
set -Eeuo pipefail
stty erase ^H # 让退格键在终端里正常工作
# ===== [BEGIN] SBP 引导模块 v2.2.0+(包管理器优先 + 二进制回退) =====
# 模式与哨兵
: "${SBP_SOFT:=0}" # 1=宽松模式(失败尽量继续),默认 0=严格
: "${SBP_SKIP_DEPS:=0}" # 1=启动跳过依赖检查(只在菜单 1) 再装)
: "${SBP_FORCE_DEPS:=0}" # 1=强制重新安装依赖
: "${SBP_BIN_ONLY:=0}" # 1=强制走二进制模式,不用包管理器
: "${SBP_ROOT:=/var/lib/sing-box-plus}"
: "${SBP_BIN_DIR:=${SBP_ROOT}/bin}"
: "${SBP_DEPS_SENTINEL:=/var/lib/sing-box-plus/.deps_ok}"
mkdir -p "$SBP_BIN_DIR" 2>/dev/null || true
export PATH="$SBP_BIN_DIR:$PATH"
# 工具:下载器 + 轻量重试
dl() { # 用法dl <URL> <OUT_PATH>
local url="$1" out="$2"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 --connect-timeout 5 -o "$out" "$url"
elif command -v wget >/dev/null 2>&1; then
timeout 15 wget -qO "$out" --tries=2 "$url"
else
echo "[ERROR] 缺少 curl/wget无法下载 $url"; return 1
fi
}
with_retry() { local n=${1:-3}; shift; local i=1; until "$@"; do [ $i -ge "$n" ] && return 1; sleep $((i*2)); i=$((i+1)); done; }
# 工具:架构探测 + jq 静态兜底
detect_goarch() {
case "$(uname -m)" in
x86_64|amd64) echo amd64 ;;
aarch64|arm64) echo arm64 ;;
armv7l|armv7) echo armv7 ;;
i386|i686) echo 386 ;;
*) echo amd64 ;;
esac
}
ensure_jq_static() {
command -v jq >/dev/null 2>&1 && return 0
local arch out="$SBP_BIN_DIR/jq" url alt
arch="$(detect_goarch)"
url="https://github.com/jqlang/jq/releases/latest/download/jq-linux-${arch}"
alt="https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64"
dl "$url" "$out" || { [ "$arch" = amd64 ] && dl "$alt" "$out" || true; }
chmod +x "$out" 2>/dev/null || true
command -v jq >/dev/null 2>&1
}
# 工具:核心命令自检
sbp_core_ok() {
local need=(curl jq tar unzip openssl)
local b; for b in "${need[@]}"; do command -v "$b" >/dev/null 2>&1 || return 1; done
return 0
}
# —— 包管理器路径 —— #
sbp_detect_pm() {
if command -v apt-get >/dev/null 2>&1; then PM=apt
elif command -v dnf >/dev/null 2>&1; then PM=dnf
elif command -v yum >/dev/null 2>&1; then PM=yum
elif command -v pacman >/dev/null 2>&1; then PM=pacman
elif command -v zypper >/dev/null 2>&1; then PM=zypper
else PM=unknown; fi
[ "$PM" = unknown ] && return 1 || return 0
}
# apt 允许发行信息变化stable→oldstable / Version 变化)
apt_allow_release_change() {
cat >/etc/apt/apt.conf.d/99allow-releaseinfo-change <<'CONF'
Acquire::AllowReleaseInfoChange::Suite "true";
Acquire::AllowReleaseInfoChange::Version "true";
CONF
}
# 刷新软件仓(含各系兜底)
sbp_pm_refresh() {
case "$PM" in
apt)
apt_allow_release_change
[[ -f /etc/apt/sources.list ]] && sed -i 's#^deb http://#deb https://#' /etc/apt/sources.list 2>/dev/null || true
# 修正 bullseye 的 security 行bullseye/updates → debian-security bullseye-security
[[ -f /etc/apt/sources.list ]] && sed -i -E 's#^(deb\s+https?://security\.debian\.org)(/debian-security)?\s+bullseye/updates(.*)$#\1/debian-security bullseye-security\3#' /etc/apt/sources.list || true
local AOPT=""
curl -6 -fsS --connect-timeout 2 https://deb.debian.org >/dev/null 2>&1 || AOPT='-o Acquire::ForceIPv4=true'
if ! with_retry 3 apt-get update -y $AOPT; then
# backports 404 临时注释再试
sed -i 's#^\([[:space:]]*deb .* bullseye-backports.*\)#\# \1#' /etc/apt/sources.list 2>/dev/null || true
with_retry 2 apt-get update -y $AOPT -o Acquire::Check-Valid-Until=false || [ "$SBP_SOFT" = 1 ]
fi
;;
dnf)
dnf clean metadata || true
with_retry 3 dnf makecache || [ "$SBP_SOFT" = 1 ]
;;
yum)
yum clean all || true
with_retry 3 yum makecache fast || true
yum install -y epel-release || true # EL7/老环境便于装 jq 等
;;
pacman)
pacman-key --init >/dev/null 2>&1 || true
pacman-key --populate archlinux >/dev/null 2>&1 || true
with_retry 3 pacman -Syy --noconfirm || [ "$SBP_SOFT" = 1 ]
;;
zypper)
zypper -n ref || zypper -n ref --force || true
;;
esac
}
# 逐包安装(单个失败不拖累整体)
sbp_pm_install() {
case "$PM" in
apt)
local p; apt-get update -y >/dev/null 2>&1 || true
for p in "$@"; do apt-get install -y --no-install-recommends "$p" || true; done
;;
dnf)
local p; for p in "$@"; do dnf install -y "$p" || true; done
;;
yum)
yum install -y epel-release || true
local p; for p in "$@"; do yum install -y "$p" || true; done
;;
pacman)
pacman -Sy --noconfirm || [ "$SBP_SOFT" = 1 ]
local p; for p in "$@"; do pacman -S --noconfirm --needed "$p" || true; done
;;
zypper)
zypper -n ref || true
local p; for p in "$@"; do zypper --non-interactive install "$p" || true; done
;;
esac
}
# 用包管理器装一轮依赖
sbp_install_prereqs_pm() {
sbp_detect_pm || return 1
sbp_pm_refresh
case "$PM" in
apt) CORE=(curl jq tar unzip openssl); EXTRA=(ca-certificates xz-utils uuid-runtime iproute2 iptables ufw) ;;
dnf|yum)CORE=(curl jq tar unzip openssl); EXTRA=(ca-certificates xz util-linux iproute iptables iptables-nft firewalld) ;;
pacman) CORE=(curl jq tar unzip openssl); EXTRA=(ca-certificates xz util-linux iproute2 iptables) ;;
zypper) CORE=(curl jq tar unzip openssl); EXTRA=(ca-certificates xz util-linux iproute2 iptables firewalld) ;;
*) return 1 ;;
esac
sbp_pm_install "${CORE[@]}" "${EXTRA[@]}"
# jq 兜底:安装失败时下载静态 jq
if ! command -v jq >/dev/null 2>&1; then
echo "[INFO] 通过包管理器安装 jq 失败,尝试下载静态 jq ..."
ensure_jq_static || { echo "[ERROR] 无法获取 jq"; return 1; }
fi
# 严格模式:核心仍缺则失败
if ! sbp_core_ok; then
[ "$SBP_SOFT" = 1 ] || return 1
echo "[WARN] 核心依赖未就绪(宽松模式继续)"
fi
return 0
}
# —— 二进制模式:直接获取 sing-box 可执行文件 —— #
install_singbox_binary() {
local arch goarch pkg tmp json url fn
goarch="$(detect_goarch)"
tmp="$(mktemp -d)" || return 1
ensure_jq_static || { echo "[ERROR] 无法获取 jq二进制模式失败"; rm -rf "$tmp"; return 1; }
json="$(with_retry 3 curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest)" || { rm -rf "$tmp"; return 1; }
url="$(printf '%s' "$json" | jq -r --arg a "$goarch" '
.assets[] | select(.name|test("linux-" + $a + "\\.(tar\\.(xz|gz)|zip)$")) | .browser_download_url
' | head -n1)"
if [ -z "$url" ] || [ "$url" = "null" ]; then
echo "[ERROR] 未找到匹配架构($goarch)的 sing-box 资产"; rm -rf "$tmp"; return 1
fi
pkg="$tmp/pkg"
with_retry 3 dl "$url" "$pkg" || { rm -rf "$tmp"; return 1; }
case "$url" in
*.tar.xz) if command -v xz >/dev/null 2>&1; then tar -xJf "$pkg" -C "$tmp"; else echo "[ERROR] 缺少 xz请安装 xz/xz-utils 或换 .tar.gz/.zip"; rm -rf "$tmp"; return 1; fi ;;
*.tar.gz) tar -xzf "$pkg" -C "$tmp" ;;
*.zip) unzip -q "$pkg" -d "$tmp" || { echo "[ERROR] 缺少 unzip"; rm -rf "$tmp"; return 1; } ;;
*) echo "[ERROR] 未知包格式:$url"; rm -rf "$tmp"; return 1 ;;
esac
fn="$(find "$tmp" -type f -name 'sing-box' | head -n1)"
[ -n "$fn" ] || { echo "[ERROR] 包内未找到 sing-box"; rm -rf "$tmp"; return 1; }
install -m 0755 "$fn" "$SBP_BIN_DIR/sing-box" || { rm -rf "$tmp"; return 1; }
rm -rf "$tmp"
echo "[OK] 已安装 sing-box 到 $SBP_BIN_DIR/sing-box"
}
# 证书兜底(有 openssl 就生成;没有就先跳过,由业务决定是否强制)
ensure_tls_cert() {
local dir="$SBP_ROOT"
mkdir -p "$dir"
if command -v openssl >/dev/null 2>&1; then
[[ -f "$dir/private.key" ]] || openssl ecparam -genkey -name prime256v1 -out "$dir/private.key" >/dev/null 2>&1
[[ -f "$dir/cert.pem" ]] || openssl req -new -x509 -days 36500 -key "$dir/private.key" -out "$dir/cert.pem" -subj "/CN=www.bing.com" >/dev/null 2>&1
fi
}
# 标记哨兵
sbp_mark_deps_ok() {
if sbp_core_ok; then
mkdir -p "$(dirname "$SBP_DEPS_SENTINEL")" && : > "$SBP_DEPS_SENTINEL" || true
fi
}
# 入口:装依赖 / 二进制回退
sbp_bootstrap() {
[ "$EUID" -eq 0 ] || { echo "请以 root 运行(或 sudo"; exit 1; }
if [ "$SBP_SKIP_DEPS" = 1 ]; then
echo "[INFO] 已跳过启动时依赖检查SBP_SKIP_DEPS=1"
return 0
fi
# 已就绪则跳过
if [ "$SBP_FORCE_DEPS" != 1 ] && sbp_core_ok && [ -f "$SBP_DEPS_SENTINEL" ] && [ "$SBP_BIN_ONLY" != 1 ]; then
echo "依赖已安装"
return 0
fi
# 强制二进制模式
if [ "$SBP_BIN_ONLY" = 1 ]; then
echo "[INFO] 二进制模式SBP_BIN_ONLY=1"
install_singbox_binary || { echo "[ERROR] 二进制模式安装 sing-box 失败"; exit 1; }
ensure_tls_cert
return 0
fi
# 包管理器优先
if sbp_install_prereqs_pm; then
sbp_mark_deps_ok
return 0
fi
# 回退到二进制模式
echo "[WARN] 包管理器依赖安装失败,切换到二进制模式"
install_singbox_binary || { echo "[ERROR] 二进制模式安装 sing-box 失败"; exit 1; }
ensure_tls_cert
}
# ===== [END] SBP 引导模块 v2.2.0+ =====
# ===== 提前设默认,避免 set -u 早期引用未定义变量导致脚本直接退出 =====
SYSTEMD_SERVICE=${SYSTEMD_SERVICE:-sing-box.service}
BIN_PATH=${BIN_PATH:-/usr/local/bin/sing-box}
SB_DIR=${SB_DIR:-/opt/sing-box}
CONF_JSON=${CONF_JSON:-$SB_DIR/config.json}
DATA_DIR=${DATA_DIR:-$SB_DIR/data}
CERT_DIR=${CERT_DIR:-$SB_DIR/cert}
WGCF_DIR=${WGCF_DIR:-$SB_DIR/wgcf}
# 功能开关(保持稳定默认)
ENABLE_WARP=${ENABLE_WARP:-true}
ENABLE_VLESS_REALITY=${ENABLE_VLESS_REALITY:-true}
ENABLE_VLESS_GRPCR=${ENABLE_VLESS_GRPCR:-true}
ENABLE_TROJAN_REALITY=${ENABLE_TROJAN_REALITY:-true}
ENABLE_HYSTERIA2=${ENABLE_HYSTERIA2:-true}
ENABLE_VMESS_WS=${ENABLE_VMESS_WS:-true}
ENABLE_HY2_OBFS=${ENABLE_HY2_OBFS:-true}
ENABLE_SS2022=${ENABLE_SS2022:-true}
ENABLE_SS=${ENABLE_SS:-true}
ENABLE_TUIC=${ENABLE_TUIC:-true}
# 常量
SCRIPT_NAME="Sing-Box-Plus 管理脚本"
SCRIPT_VERSION="v4.4.0"
REALITY_SERVER=${REALITY_SERVER:-www.microsoft.com}
REALITY_SERVER_PORT=${REALITY_SERVER_PORT:-443}
GRPC_SERVICE=${GRPC_SERVICE:-grpc}
VMESS_WS_PATH=${VMESS_WS_PATH:-/vm}
# 兼容 sing-box 1.12.x 的旧 wireguard 出站
export ENABLE_DEPRECATED_WIREGUARD_OUTBOUND=${ENABLE_DEPRECATED_WIREGUARD_OUTBOUND:-true}
# ===== 颜色 =====
C_RESET="\033[0m"; C_BOLD="\033[1m"; C_DIM="\033[2m"
C_RED="\033[31m"; C_GREEN="\033[32m"; C_YELLOW="\033[33m"
C_BLUE="\033[34m"; C_CYAN="\033[36m"; C_MAGENTA="\033[35m"
hr(){ printf "${C_DIM}=============================================================${C_RESET}\n"; }
# ===== 基础工具 =====
info(){ echo -e "[${C_CYAN}信息${C_RESET}] $*"; }
ok(){ echo -e "[${C_GREEN}成功${C_RESET}] $*"; }
warn(){ echo -e "[${C_YELLOW}警告${C_RESET}] $*"; }
err(){ echo -e "[${C_RED}错误${C_RESET}] $*" >&2; }
die(){ echo -e "[${C_RED}错误${C_RESET}] $*" >&2; exit 1; }
# --- 架构映射uname -m -> 发行资产名 ---
arch_map() {
case "$(uname -m)" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l|armv7) echo "armv7" ;;
armv6l) echo "armv7" ;; # 上游无 armv6回退 armv7
i386|i686) echo "386" ;;
*) echo "amd64" ;;
esac
}
# --- 依赖安装:兼容 apt / yum / dnf / apk / pacman / zypper ---
ensure_deps() {
local pkgs=("$@") miss=()
for p in "${pkgs[@]}"; do command -v "$p" >/dev/null 2>&1 || miss+=("$p"); done
((${#miss[@]}==0)) && return 0
if command -v apt-get >/dev/null 2>&1; then
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y "${miss[@]}" || apt-get install -y --no-install-recommends "${miss[@]}"
elif command -v dnf >/dev/null 2>&1; then
dnf install -y "${miss[@]}"
elif command -v yum >/dev/null 2>&1; then
yum install -y "${miss[@]}"
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache "${miss[@]}"
elif command -v pacman >/dev/null 2>&1; then
pacman -Sy --noconfirm "${miss[@]}"
elif command -v zypper >/dev/null 2>&1; then
zypper --non-interactive install "${miss[@]}"
else
err "无法自动安装依赖:${miss[*]},请手动安装后重试"
return 1
fi
}
b64enc(){ base64 -w 0 2>/dev/null || base64; }
urlenc(){ # 纯 bash urlencode不依赖 python
local s="$1" out="" c
for ((i=0; i<${#s}; i++)); do
c=${s:i:1}
case "$c" in
[a-zA-Z0-9._~-]) out+="$c" ;;
' ') out+="%20" ;;
*) printf -v out "%s%%%02X" "$out" "'$c" ;;
esac
done
printf "%s" "$out"
}
safe_source_env(){ # 安全 source忽略不存在文件
local f="$1"; [[ -f "$f" ]] || return 1
set +u; # 避免未定义变量报错
# shellcheck disable=SC1090
source "$f"
set -u
}
get_ip4(){ # 多源获取公网 IPv4
local ip
ip=$(curl -4 -fsSL ipv4.icanhazip.com 2>/dev/null || true)
[[ -z "$ip" ]] && ip=$(curl -4 -fsSL ifconfig.me 2>/dev/null || true)
[[ -z "$ip" ]] && ip=$(curl -4 -fsSL ip.sb 2>/dev/null || true)
echo "${ip:-127.0.0.1}"
}
get_ip6(){ # 多源获取公网 IPv6无 IPv6 则返回空)
local ip
ip=$(curl -6 -fsSL ipv6.icanhazip.com 2>/dev/null || true)
[[ -z "$ip" ]] && ip=$(curl -6 -fsSL ifconfig.me 2>/dev/null || true)
[[ -z "$ip" ]] && ip=$(curl -6 -fsSL ip.sb 2>/dev/null || true)
echo "${ip:-}"
}
# 兼容旧调用:默认返回 IPv4
get_ip(){ get_ip4; }
# URI/分享链接里IPv6 需要用 [addr] 包起来
fmt_host_for_uri(){
local ip="$1"
[[ "$ip" == *:* ]] && printf '[%s]' "$ip" || printf '%s' "$ip"
}
is_uuid(){ [[ "$1" =~ ^[0-9a-fA-F-]{36}$ ]]; }
ensure_dirs(){ mkdir -p "$SB_DIR" "$DATA_DIR" "$CERT_DIR" "$WGCF_DIR"; }
# ===== 端口18 个互不重复) =====
PORTS=()
gen_port() {
while :; do
p=$(( ( RANDOM % 55536 ) + 10000 ))
[[ $p -le 65535 ]] || continue
[[ " ${PORTS[*]-} " != *" $p "* ]] && { PORTS+=("$p"); echo "$p"; return; }
done
}
rand_ports_reset(){ PORTS=(); }
PORT_VLESSR=""; PORT_VLESS_GRPCR=""; PORT_TROJANR=""; PORT_HY2=""; PORT_VMESS_WS=""
PORT_HY2_OBFS=""; PORT_SS2022=""; PORT_SS=""; PORT_TUIC=""
PORT_VLESSR_W=""; PORT_VLESS_GRPCR_W=""; PORT_TROJANR_W=""; PORT_HY2_W=""; PORT_VMESS_WS_W=""
PORT_HY2_OBFS_W=""; PORT_SS2022_W=""; PORT_SS_W=""; PORT_TUIC_W=""
save_ports(){ cat > "$SB_DIR/ports.env" <<EOF
PORT_VLESSR=$PORT_VLESSR
PORT_VLESS_GRPCR=$PORT_VLESS_GRPCR
PORT_TROJANR=$PORT_TROJANR
PORT_HY2=$PORT_HY2
PORT_VMESS_WS=$PORT_VMESS_WS
PORT_HY2_OBFS=$PORT_HY2_OBFS
PORT_SS2022=$PORT_SS2022
PORT_SS=$PORT_SS
PORT_TUIC=$PORT_TUIC
PORT_VLESSR_W=$PORT_VLESSR_W
PORT_VLESS_GRPCR_W=$PORT_VLESS_GRPCR_W
PORT_TROJANR_W=$PORT_TROJANR_W
PORT_HY2_W=$PORT_HY2_W
PORT_VMESS_WS_W=$PORT_VMESS_WS_W
PORT_HY2_OBFS_W=$PORT_HY2_OBFS_W
PORT_SS2022_W=$PORT_SS2022_W
PORT_SS_W=$PORT_SS_W
PORT_TUIC_W=$PORT_TUIC_W
EOF
}
load_ports(){ safe_source_env "$SB_DIR/ports.env" || return 1; }
save_all_ports(){
rand_ports_reset
for v in PORT_VLESSR PORT_VLESS_GRPCR PORT_TROJANR PORT_HY2 PORT_VMESS_WS PORT_HY2_OBFS PORT_SS2022 PORT_SS PORT_TUIC \
PORT_VLESSR_W PORT_VLESS_GRPCR_W PORT_TROJANR_W PORT_HY2_W PORT_VMESS_WS_W PORT_HY2_OBFS_W PORT_SS2022_W PORT_SS_W PORT_TUIC_W; do
[[ -n "${!v:-}" ]] && PORTS+=("${!v}")
done
[[ -z "${PORT_VLESSR:-}" ]] && PORT_VLESSR=$(gen_port)
[[ -z "${PORT_VLESS_GRPCR:-}" ]] && PORT_VLESS_GRPCR=$(gen_port)
[[ -z "${PORT_TROJANR:-}" ]] && PORT_TROJANR=$(gen_port)
[[ -z "${PORT_HY2:-}" ]] && PORT_HY2=$(gen_port)
[[ -z "${PORT_VMESS_WS:-}" ]] && PORT_VMESS_WS=$(gen_port)
[[ -z "${PORT_HY2_OBFS:-}" ]] && PORT_HY2_OBFS=$(gen_port)
[[ -z "${PORT_SS2022:-}" ]] && PORT_SS2022=$(gen_port)
[[ -z "${PORT_SS:-}" ]] && PORT_SS=$(gen_port)
[[ -z "${PORT_TUIC:-}" ]] && PORT_TUIC=$(gen_port)
[[ -z "${PORT_VLESSR_W:-}" ]] && PORT_VLESSR_W=$(gen_port)
[[ -z "${PORT_VLESS_GRPCR_W:-}" ]] && PORT_VLESS_GRPCR_W=$(gen_port)
[[ -z "${PORT_TROJANR_W:-}" ]] && PORT_TROJANR_W=$(gen_port)
[[ -z "${PORT_HY2_W:-}" ]] && PORT_HY2_W=$(gen_port)
[[ -z "${PORT_VMESS_WS_W:-}" ]] && PORT_VMESS_WS_W=$(gen_port)
[[ -z "${PORT_HY2_OBFS_W:-}" ]] && PORT_HY2_OBFS_W=$(gen_port) || true
[[ -z "${PORT_SS2022_W:-}" ]] && PORT_SS2022_W=$(gen_port)
[[ -z "${PORT_SS_W:-}" ]] && PORT_SS_W=$(gen_port)
[[ -z "${PORT_TUIC_W:-}" ]] && PORT_TUIC_W=$(gen_port)
save_ports
}
# ===== env / creds / warp =====
save_env(){ cat > "$SB_DIR/env.conf" <<EOF
BIN_PATH=$BIN_PATH
ENABLE_VLESS_REALITY=$ENABLE_VLESS_REALITY
ENABLE_VLESS_GRPCR=$ENABLE_VLESS_GRPCR
ENABLE_TROJAN_REALITY=$ENABLE_TROJAN_REALITY
ENABLE_HYSTERIA2=$ENABLE_HYSTERIA2
ENABLE_VMESS_WS=$ENABLE_VMESS_WS
ENABLE_HY2_OBFS=$ENABLE_HY2_OBFS
ENABLE_SS2022=$ENABLE_SS2022
ENABLE_SS=$ENABLE_SS
ENABLE_TUIC=$ENABLE_TUIC
ENABLE_WARP=$ENABLE_WARP
REALITY_SERVER=$REALITY_SERVER
REALITY_SERVER_PORT=$REALITY_SERVER_PORT
GRPC_SERVICE=$GRPC_SERVICE
VMESS_WS_PATH=$VMESS_WS_PATH
EOF
}
load_env(){ safe_source_env "$SB_DIR/env.conf" || true; }
save_creds(){ cat > "$SB_DIR/creds.env" <<EOF
UUID=$UUID
HY2_PWD=$HY2_PWD
REALITY_PRIV=$REALITY_PRIV
REALITY_PUB=$REALITY_PUB
REALITY_SID=$REALITY_SID
HY2_PWD2=$HY2_PWD2
HY2_OBFS_PWD=$HY2_OBFS_PWD
SS2022_KEY=$SS2022_KEY
SS_PWD=$SS_PWD
TUIC_UUID=$TUIC_UUID
TUIC_PWD=$TUIC_PWD
EOF
}
load_creds(){ safe_source_env "$SB_DIR/creds.env" || return 1; }
save_warp(){ cat > "$SB_DIR/warp.env" <<EOF
WARP_PRIVATE_KEY=$WARP_PRIVATE_KEY
WARP_PEER_PUBLIC_KEY=$WARP_PEER_PUBLIC_KEY
WARP_ENDPOINT_HOST=$WARP_ENDPOINT_HOST
WARP_ENDPOINT_PORT=$WARP_ENDPOINT_PORT
WARP_ADDRESS_V4=$WARP_ADDRESS_V4
WARP_ADDRESS_V6=$WARP_ADDRESS_V6
WARP_RESERVED_1=$WARP_RESERVED_1
WARP_RESERVED_2=$WARP_RESERVED_2
WARP_RESERVED_3=$WARP_RESERVED_3
EOF
}
load_warp(){ safe_source_env "$SB_DIR/warp.env" || return 1; }
# 生成 8 字节十六进制16 个 hex 字符)
rand_hex8(){
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 8 | tr -d "\n"
else
# 兜底:没有 openssl 时用 hexdump
hexdump -v -n 8 -e '1/1 "%02x"' /dev/urandom
fi
}
rand_b64_32(){ openssl rand -base64 32 | tr -d "\n"; }
gen_uuid(){
local u=""
if [[ -x "$BIN_PATH" ]]; then u=$("$BIN_PATH" generate uuid 2>/dev/null | head -n1); fi
if [[ -z "$u" ]] && command -v uuidgen >/dev/null 2>&1; then u=$(uuidgen | head -n1); fi
if [[ -z "$u" ]]; then u=$(cat /proc/sys/kernel/random/uuid | head -n1); fi
printf '%s' "$u" | tr -d '\r\n'
}
gen_reality(){ "$BIN_PATH" generate reality-keypair; }
mk_cert(){
local crt="$CERT_DIR/fullchain.pem" key="$CERT_DIR/key.pem"
if [[ ! -s "$crt" || ! -s "$key" ]]; then
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 3650 -nodes \
-keyout "$key" -out "$crt" -subj "/CN=$REALITY_SERVER" \
-addext "subjectAltName=DNS:$REALITY_SERVER" >/dev/null 2>&1
fi
CRT_SHA256=$(openssl x509 -in "$crt" -fingerprint -sha256 -noout \
| sed 's/SHA256 Fingerprint=//;s/://g' | tr 'A-F' 'a-f')
}
ensure_creds(){
[[ -z "${UUID:-}" ]] && UUID=$(gen_uuid)
is_uuid "$UUID" || UUID=$(gen_uuid)
[[ -z "${HY2_PWD:-}" ]] && HY2_PWD=$(rand_b64_32)
if [[ -z "${REALITY_PRIV:-}" || -z "${REALITY_PUB:-}" || -z "${REALITY_SID:-}" ]]; then
readarray -t RKP < <(gen_reality)
REALITY_PRIV=$(printf "%s\n" "${RKP[@]}" | awk '/PrivateKey/{print $2}')
REALITY_PUB=$(printf "%s\n" "${RKP[@]}" | awk '/PublicKey/{print $2}')
REALITY_SID=$(rand_hex8)
fi
[[ -z "${HY2_PWD2:-}" ]] && HY2_PWD2=$(rand_b64_32)
[[ -z "${HY2_OBFS_PWD:-}" ]] && HY2_OBFS_PWD=$(openssl rand -base64 16 | tr -d "\n")
[[ -z "${SS2022_KEY:-}" ]] && SS2022_KEY=$(rand_b64_32)
[[ -z "${SS_PWD:-}" ]] && SS_PWD=$(openssl rand -base64 24 | tr -d "=\n" | tr "+/" "-_")
TUIC_UUID="$UUID"; TUIC_PWD="$UUID"
save_creds
}
# ===== WARPwgcf =====
WGCF_BIN=/usr/local/bin/wgcf
install_wgcf_disabled(){
[[ -x "$WGCF_BIN" ]] && return 0
local GOA url tmp
case "$(arch_map)" in
amd64) GOA=amd64;; arm64) GOA=arm64;; armv7) GOA=armv7;; 386) GOA=386;; *) GOA=amd64;;
esac
url=$(curl -fsSL https://api.github.com/repos/ViRb3/wgcf/releases/latest \
| jq -r ".assets[] | select(.name|test(\"linux_${GOA}$\")) | .browser_download_url" | head -n1)
[[ -n "$url" ]] || { warn "获取 wgcf 下载地址失败"; return 1; }
tmp=$(mktemp -d)
curl -fsSL "$url" -o "$tmp/wgcf"
install -m0755 "$tmp/wgcf" "$WGCF_BIN"
rm -rf "$tmp"
}
# —— Base64 清理 + 补齐:去掉引号/空白,长度 %4==2 补“==”,%4==3 补“=” ——
pad_b64(){
local s="${1:-}"
# 去引号/空格/回车
s="$(printf '%s' "$s" | tr -d '\r\n\" ')"
# 去掉已有尾随 =,按需重加
s="${s%%=*}"
local rem=$(( ${#s} % 4 ))
if (( rem == 2 )); then s="${s}=="
elif (( rem == 3 )); then s="${s}="
fi
printf '%s' "$s"
}
# ===== WARP官方 warp-cliproxy 模式)一键安装/修复 =====
# 说明:
# - 本脚本强制使用官方 cloudflare-warp (warp-cli) 提供本地 SOCKS5 (默认 127.0.0.1:40000)
# - sing-box 的 tag=warp 出站固定走该 SOCKS5
WARP_SOCKS_HOST="${WARP_SOCKS_HOST:-127.0.0.1}"
WARP_SOCKS_PORT="${WARP_SOCKS_PORT:-40000}"
install_warpcli(){
command -v warp-cli >/dev/null 2>&1 && return 0
if command -v apt-get >/dev/null 2>&1; then
info "安装 cloudflare-warp (Debian/Ubuntu)..."
apt-get update -y
apt-get install -y curl gpg lsb-release ca-certificates >/dev/null 2>&1 || true
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/cloudflare-client.list
apt-get update -y
apt-get install -y cloudflare-warp
elif command -v yum >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then
info "安装 cloudflare-warp (CentOS/RHEL)..."
curl -fsSl https://pkg.cloudflareclient.com/cloudflare-warp-ascii.repo | tee /etc/yum.repos.d/cloudflare-warp.repo >/dev/null
if command -v dnf >/dev/null 2>&1; then
dnf install -y cloudflare-warp
else
yum install -y cloudflare-warp
fi
else
err "未识别的包管理器,无法自动安装 cloudflare-warp"
return 1
fi
command -v warp-cli >/dev/null 2>&1
}
ensure_warpcli_proxy(){
[[ "${ENABLE_WARP:-true}" == "true" ]] || return 0
install_warpcli || return 1
systemctl enable --now warp-svc >/dev/null 2>&1 || true
# 已注册则跳过;未注册则自动同意条款
if ! warp-cli registration show >/dev/null 2>&1; then
info "正在初始化 Cloudflare WARP"
# warp-cli 强制检测 TTY非 TTY 拒绝输入,需模拟真实终端注入 y
# 优先级python3 pty最可靠→ expect → 安装 python3 兜底
_warp_reg_ok=0
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PYEOF' 2>/dev/null && _warp_reg_ok=1 || true
import pty, os, time, select, sys
def run():
pid, fd = pty.fork()
if pid == 0:
os.execvp("warp-cli", ["warp-cli", "registration", "new"])
else:
answered = False
for _ in range(30):
r, _, _ = select.select([fd], [], [], 1)
if r:
try:
data = os.read(fd, 4096).decode(errors="ignore")
except OSError:
break
if not answered and ("y/N" in data or "y/n" in data):
time.sleep(0.2)
os.write(fd, b"y\n")
answered = True
if "Success" in data:
sys.exit(0)
try:
ret = os.waitpid(pid, os.WNOHANG)
if ret[0] != 0:
break
except ChildProcessError:
break
try:
os.waitpid(pid, 0)
except Exception:
pass
sys.exit(1)
run()
PYEOF
elif command -v expect >/dev/null 2>&1; then
expect -c '
spawn warp-cli registration new
expect -re {[yY]/[nN]}
send "y\r"
expect eof
' >/dev/null 2>&1 && _warp_reg_ok=1 || true
else
# 尝试安装 python3兜底
warn "未找到 python3/expect尝试安装 python3..."
if command -v apt-get >/dev/null 2>&1; then
apt-get install -y python3 >/dev/null 2>&1 || true
elif command -v dnf >/dev/null 2>&1; then
dnf install -y python3 >/dev/null 2>&1 || true
elif command -v yum >/dev/null 2>&1; then
yum install -y python3 >/dev/null 2>&1 || true
elif command -v pacman >/dev/null 2>&1; then
pacman -Sy --noconfirm python >/dev/null 2>&1 || true
elif command -v zypper >/dev/null 2>&1; then
zypper --non-interactive install python3 >/dev/null 2>&1 || true
fi
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PYEOF' 2>/dev/null && _warp_reg_ok=1 || true
import pty, os, time, select, sys
def run():
pid, fd = pty.fork()
if pid == 0:
os.execvp("warp-cli", ["warp-cli", "registration", "new"])
else:
answered = False
for _ in range(30):
r, _, _ = select.select([fd], [], [], 1)
if r:
try:
data = os.read(fd, 4096).decode(errors="ignore")
except OSError:
break
if not answered and ("y/N" in data or "y/n" in data):
time.sleep(0.2)
os.write(fd, b"y\n")
answered = True
if "Success" in data:
sys.exit(0)
try:
ret = os.waitpid(pid, os.WNOHANG)
if ret[0] != 0:
break
except ChildProcessError:
break
try:
os.waitpid(pid, 0)
except Exception:
pass
sys.exit(1)
run()
PYEOF
else
err "无法自动完成 WARP 注册(缺少 python3/expect请手动运行warp-cli registration new"
return 1
fi
fi
sleep 2
if ! warp-cli registration show >/dev/null 2>&1; then
err "WARP 注册失败请手动运行warp-cli registration new"; return 1
fi
fi
# proxy 模式:不改系统默认路由
warp-cli mode proxy >/dev/null 2>&1 || true
# 连接
warp-cli connect >/dev/null 2>&1 || return 1
# 等待 socks 端口监听
for i in {1..12}; do
if ss -lntp 2>/dev/null | grep -q ":${WARP_SOCKS_PORT}\b" || netstat -lntp 2>/dev/null | grep -q ":${WARP_SOCKS_PORT}\b"; then
break
fi
sleep 1
done
if !( ss -lntp 2>/dev/null | grep -q ":${WARP_SOCKS_PORT}\b" || netstat -lntp 2>/dev/null | grep -q ":${WARP_SOCKS_PORT}\b" ); then
err "WARP SOCKS5 端口 ${WARP_SOCKS_PORT} 未监听warp-svc/warp-cli 可能未正常工作)"
systemctl status warp-svc --no-pager | head -80 || true
journalctl -u warp-svc -n 120 --no-pager || true
return 1
fi
# 真正测试 warp=on
if ! curl -fsSL --proxy "socks5://${WARP_SOCKS_HOST}:${WARP_SOCKS_PORT}" https://cloudflare.com/cdn-cgi/trace | grep -q "warp=on"; then
err "WARP 代理测试失败:未检测到 warp=on"
warp-cli status || true
return 1
fi
ok "WARP proxy 已就绪socks5://${WARP_SOCKS_HOST}:${WARP_SOCKS_PORT}"
return 0
}
# ===== WARPwgcf配置生成/修复(已废弃/不再默认使用,保留旧代码以兼容历史) =====
ensure_wgcf_profile(){
[[ "${ENABLE_WARP:-true}" == "true" ]] || return 0
# 先尝试读取旧 env并做一次规范化补齐
if load_warp 2>/dev/null; then
WARP_PRIVATE_KEY="$(pad_b64 "${WARP_PRIVATE_KEY:-}")"
WARP_PEER_PUBLIC_KEY="$(pad_b64 "${WARP_PEER_PUBLIC_KEY:-}")"
# 允许之前没写 reserved给默认 0
: "${WARP_RESERVED_1:=0}" "${WARP_RESERVED_2:=0}" "${WARP_RESERVED_3:=0}"
save_warp
# 如果关键字段都在,就直接用旧的(已经补齐),无需重建
if [[ -n "$WARP_PRIVATE_KEY" && -n "$WARP_PEER_PUBLIC_KEY" && -n "${WARP_ENDPOINT_HOST:-}" && -n "${WARP_ENDPOINT_PORT:-}" ]]; then
return 0
fi
fi
# 走到这里说明旧 env 不完整;开始用 wgcf 重建
install_wgcf_disabled || { warn "wgcf 安装失败,禁用 WARP 节点"; ENABLE_WARP=false; save_env; return 0; }
local wd="$SB_DIR/wgcf"; mkdir -p "$wd"
if [[ ! -f "$wd/wgcf-account.toml" ]]; then
"$WGCF_BIN" register --accept-tos --config "$wd/wgcf-account.toml" >/dev/null
fi
"$WGCF_BIN" generate --config "$wd/wgcf-account.toml" --profile "$wd/wgcf-profile.conf" >/dev/null
local prof="$wd/wgcf-profile.conf"
# 提取并规范化
WARP_PRIVATE_KEY="$(pad_b64 "$(awk -F'= *' '/^PrivateKey/{gsub(/\r/,"");print $2; exit}' "$prof")")"
WARP_PEER_PUBLIC_KEY="$(pad_b64 "$(awk -F'= *' '/^PublicKey/{gsub(/\r/,"");print $2; exit}' "$prof")")"
# Endpoint 可能是域名或 [IPv6]:port
local ep host port
ep="$(awk -F'= *' '/^Endpoint/{gsub(/\r/,"");print $2; exit}' "$prof" | tr -d '" ')"
if [[ "$ep" =~ ^\[(.+)\]:(.+)$ ]]; then host="${BASH_REMATCH[1]}"; port="${BASH_REMATCH[2]}"; else host="${ep%:*}"; port="${ep##*:}"; fi
WARP_ENDPOINT_HOST="$host"
WARP_ENDPOINT_PORT="$port"
# 内网地址与 reserved
local ad rs
ad="$(awk -F'= *' '/^Address/{gsub(/\r/,"");print $2; exit}' "$prof" | tr -d '" ')"
WARP_ADDRESS_V4="${ad%%,*}"
WARP_ADDRESS_V6="${ad##*,}"
rs="$(awk -F'= *' '/^Reserved/{gsub(/\r/,"");print $2; exit}' "$prof" | tr -d '" ')"
WARP_RESERVED_1="${rs%%,*}"; rs="${rs#*,}"
WARP_RESERVED_2="${rs%%,*}"; WARP_RESERVED_3="${rs##*,}"
: "${WARP_RESERVED_1:=0}" "${WARP_RESERVED_2:=0}" "${WARP_RESERVED_3:=0}"
save_warp
}
# ===== 依赖与安装 =====
install_deps(){
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y ca-certificates curl wget jq tar iproute2 openssl coreutils uuid-runtime >/dev/null 2>&1 || true
}
# ===== 安装 / 更新 sing-boxGitHub Releases=====
install_singbox() {
# 已安装则直接返回
if command -v "$BIN_PATH" >/dev/null 2>&1; then
info "检测到 sing-box: $("$BIN_PATH" version | head -n1)"
return 0
fi
# 依赖
ensure_deps curl jq tar || return 1
command -v xz >/dev/null 2>&1 || ensure_deps xz-utils >/dev/null 2>&1 || true
command -v unzip >/dev/null 2>&1 || ensure_deps unzip >/dev/null 2>&1 || true
local repo="SagerNet/sing-box"
local tag="${SINGBOX_TAG:-latest}" # 允许用环境变量固定版本,如 v1.12.7
local arch; arch="$(arch_map)"
local api url tmp pkg re rel_url
info "下载 sing-box (${arch}) ..."
# 取 release JSON
if [[ "$tag" = "latest" ]]; then
rel_url="https://api.github.com/repos/${repo}/releases/latest"
else
rel_url="https://api.github.com/repos/${repo}/releases/tags/${tag}"
fi
# 资产名匹配:兼容 tar.gz / tar.xz / zip
# 典型名称sing-box-1.12.7-linux-amd64.tar.gz
re="^sing-box-.*-linux-${arch}\\.(tar\\.(gz|xz)|zip)$"
# 先在目标 release 里找;找不到再从所有 releases 里兜底
url="$(curl -fsSL "$rel_url" | jq -r --arg re "$re" '.assets[] | select(.name | test($re)) | .browser_download_url' | head -n1)"
if [[ -z "$url" ]]; then
url="$(curl -fsSL "https://api.github.com/repos/${repo}/releases" \
| jq -r --arg re "$re" '[ .[] | .assets[] | select(.name | test($re)) | .browser_download_url ][0]')"
fi
[[ -n "$url" ]] || { err "下载 sing-box 失败未匹配到发行包arch=${arch} tag=${tag})"; return 1; }
tmp="$(mktemp -d)"; pkg="${tmp}/pkg"
if ! curl -fL "$url" -o "$pkg"; then
rm -rf "$tmp"; err "下载 sing-box 失败"; return 1
fi
# 解压
if echo "$url" | grep -qE '\.tar\.gz$|\.tgz$'; then
tar -xzf "$pkg" -C "$tmp"
elif echo "$url" | grep -qE '\.tar\.xz$'; then
tar -xJf "$pkg" -C "$tmp"
elif echo "$url" | grep -qE '\.zip$'; then
unzip -q "$pkg" -d "$tmp"
else
rm -rf "$tmp"; err "未知包格式:$url"; return 1
fi
# 找到二进制并安装
local bin
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1)"
[[ -n "$bin" ]] || { rm -rf "$tmp"; err "解压失败:未找到 sing-box 可执行文件"; return 1; }
install -m 0755 "$bin" "$BIN_PATH"
rm -rf "$tmp"
info "安装完成:$("$BIN_PATH" version | head -n1)"
}
# ===== systemd =====
write_systemd(){ cat > "/etc/systemd/system/${SYSTEMD_SERVICE}" <<EOF
[Unit]
Description=Sing-Box (Native 18 nodes)
After=network-online.target warp-svc.service
Wants=network-online.target warp-svc.service
Requires=network-online.target
[Service]
Type=simple
Environment=ENABLE_DEPRECATED_LEGACY_DNS_SERVERS=true
ExecStart=${BIN_PATH} run -c ${CONF_JSON} -D ${DATA_DIR}
Restart=on-failure
RestartSec=3
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "${SYSTEMD_SERVICE}" >/dev/null 2>&1 || true
}
# ===== 写 config.json使用你提供的稳定配置逻辑 =====
write_config(){
ensure_dirs; load_env || true; load_creds || true; load_ports || true
ensure_creds; save_all_ports; mk_cert
[[ "$ENABLE_WARP" == "true" ]] && ensure_warpcli_proxy
local CRT="$CERT_DIR/fullchain.pem" KEY="$CERT_DIR/key.pem"
jq -n \
--arg RS "$REALITY_SERVER" --argjson RSP "${REALITY_SERVER_PORT:-443}" --arg UID "$UUID" \
--arg WSHOST "$WARP_SOCKS_HOST" --argjson WSPORT "$WARP_SOCKS_PORT" \
--arg RPR "$REALITY_PRIV" --arg RPB "$REALITY_PUB" --arg SID "$REALITY_SID" \
--arg HY2 "$HY2_PWD" --arg HY22 "$HY2_PWD2" --arg HY2O "$HY2_OBFS_PWD" \
--arg GRPC "$GRPC_SERVICE" --arg VMWS "$VMESS_WS_PATH" --arg CRT "$CRT" --arg KEY "$KEY" \
--arg SS2022 "$SS2022_KEY" --arg SSPWD "$SS_PWD" --arg TUICUUID "$TUIC_UUID" --arg TUICPWD "$TUIC_PWD" \
--argjson P1 "$PORT_VLESSR" --argjson P2 "$PORT_VLESS_GRPCR" --argjson P3 "$PORT_TROJANR" \
--argjson P4 "$PORT_HY2" --argjson P5 "$PORT_VMESS_WS" --argjson P6 "$PORT_HY2_OBFS" \
--argjson P7 "$PORT_SS2022" --argjson P8 "$PORT_SS" --argjson P9 "$PORT_TUIC" \
--argjson PW1 "$PORT_VLESSR_W" --argjson PW2 "$PORT_VLESS_GRPCR_W" --argjson PW3 "$PORT_TROJANR_W" \
--argjson PW4 "$PORT_HY2_W" --argjson PW5 "$PORT_VMESS_WS_W" --argjson PW6 "$PORT_HY2_OBFS_W" \
--argjson PW7 "$PORT_SS2022_W" --argjson PW8 "$PORT_SS_W" --argjson PW9 "$PORT_TUIC_W" \
--arg ENABLE_WARP "$ENABLE_WARP" \
--arg WPRIV "${WARP_PRIVATE_KEY:-}" --arg WPPUB "${WARP_PEER_PUBLIC_KEY:-}" \
--arg WHOST "${WARP_ENDPOINT_HOST:-}" --argjson WPORT "${WARP_ENDPOINT_PORT:-0}" \
--arg W4 "${WARP_ADDRESS_V4:-}" --arg W6 "${WARP_ADDRESS_V6:-}" \
--argjson WR1 "${WARP_RESERVED_1:-0}" --argjson WR2 "${WARP_RESERVED_2:-0}" --argjson WR3 "${WARP_RESERVED_3:-0}" \
'
def inbound_vless($port): {type:"vless", listen:"::", listen_port:$port, users:[{uuid:$UID}], tls:{enabled:true, server_name:$RS, reality:{enabled:true, handshake:{server:$RS, server_port:$RSP}, private_key:$RPR, short_id:[$SID]}}};
def inbound_vless_flow($port): {type:"vless", listen:"::", listen_port:$port, users:[{uuid:$UID, flow:"xtls-rprx-vision"}], tls:{enabled:true, server_name:$RS, reality:{enabled:true, handshake:{server:$RS, server_port:$RSP}, private_key:$RPR, short_id:[$SID]}}};
def inbound_trojan($port): {type:"trojan", listen:"::", listen_port:$port, users:[{password:$UID}], tls:{enabled:true, server_name:$RS, reality:{enabled:true, handshake:{server:$RS, server_port:$RSP}, private_key:$RPR, short_id:[$SID]}}};
def inbound_hy2($port): {type:"hysteria2", listen:"::", listen_port:$port, users:[{name:"hy2", password:$HY2}], tls:{enabled:true, certificate_path:$CRT, key_path:$KEY}};
def inbound_vmess_ws($port): {type:"vmess", listen:"::", listen_port:$port, users:[{uuid:$UID}], transport:{type:"ws", path:$VMWS}};
def inbound_hy2_obfs($port): {type:"hysteria2", listen:"::", listen_port:$port, users:[{name:"hy2", password:$HY22}], obfs:{type:"salamander", password:$HY2O}, tls:{enabled:true, certificate_path:$CRT, key_path:$KEY, alpn:["h3"]}};
def inbound_ss2022($port): {type:"shadowsocks", listen:"::", listen_port:$port, method:"2022-blake3-aes-256-gcm", password:$SS2022};
def inbound_ss($port): {type:"shadowsocks", listen:"::", listen_port:$port, method:"aes-256-gcm", password:$SSPWD};
def inbound_tuic($port): {type:"tuic", listen:"::", listen_port:$port, users:[{uuid:$TUICUUID, password:$TUICPWD}], congestion_control:"bbr", tls:{enabled:true, certificate_path:$CRT, key_path:$KEY, alpn:["h3"]}};
def warp_outbound:
{type:"socks", tag:"warp", server:$WSHOST, server_port:$WSPORT};
{
log:{level:"info", timestamp:true},
dns:{ servers:[ {type:"https", tag:"dns-remote", server:"1.1.1.1", server_port:443, path:"/dns-query"}, {type:"udp", tag:"dns-local", server:"8.8.8.8"} ], strategy:"prefer_ipv4" },
inbounds:[
(inbound_vless_flow($P1) + {tag:"vless-reality"}),
(inbound_vless($P2) + {tag:"vless-grpcr", transport:{type:"grpc", service_name:$GRPC}}),
(inbound_trojan($P3) + {tag:"trojan-reality"}),
(inbound_hy2($P4) + {tag:"hy2"}),
(inbound_vmess_ws($P5) + {tag:"vmess-ws"}),
(inbound_hy2_obfs($P6) + {tag:"hy2-obfs"}),
(inbound_ss2022($P7) + {tag:"ss2022"}),
(inbound_ss($P8) + {tag:"ss"}),
(inbound_tuic($P9) + {tag:"tuic-v5"}),
(inbound_vless_flow($PW1) + {tag:"vless-reality-warp"}),
(inbound_vless($PW2) + {tag:"vless-grpcr-warp", transport:{type:"grpc", service_name:$GRPC}}),
(inbound_trojan($PW3) + {tag:"trojan-reality-warp"}),
(inbound_hy2($PW4) + {tag:"hy2-warp"}),
(inbound_vmess_ws($PW5) + {tag:"vmess-ws-warp"}),
(inbound_hy2_obfs($PW6) + {tag:"hy2-obfs-warp"}),
(inbound_ss2022($PW7) + {tag:"ss2022-warp"}),
(inbound_ss($PW8) + {tag:"ss-warp"}),
(inbound_tuic($PW9) + {tag:"tuic-v5-warp"})
],
outbounds: (
if $ENABLE_WARP=="true" then
[{type:"direct", tag:"direct"}, {type:"block", tag:"block"}, warp_outbound]
else
[{type:"direct", tag:"direct"}, {type:"block", tag:"block"}]
end
),
route: (
if $ENABLE_WARP=="true" then
{ default_domain_resolver:"dns-remote", rules:[
{ inbound: ["vless-reality-warp","vless-grpcr-warp","trojan-reality-warp","hy2-warp","vmess-ws-warp","hy2-obfs-warp","ss2022-warp","ss-warp","tuic-v5-warp"], outbound:"warp" }
],
final:"direct"
}
else
{ final:"direct" }
end
)
}' > "$CONF_JSON"
save_env
}
# ===== 防火墙 =====
open_firewall(){
local rules=()
rules+=("${PORT_VLESSR}/tcp" "${PORT_VLESS_GRPCR}/tcp" "${PORT_TROJANR}/tcp" "${PORT_VMESS_WS}/tcp")
rules+=("${PORT_HY2}/udp" "${PORT_HY2_OBFS}/udp" "${PORT_TUIC}/udp")
rules+=("${PORT_SS2022}/tcp" "${PORT_SS2022}/udp" "${PORT_SS}/tcp" "${PORT_SS}/udp")
rules+=("${PORT_VLESSR_W}/tcp" "${PORT_VLESS_GRPCR_W}/tcp" "${PORT_TROJANR_W}/tcp" "${PORT_VMESS_WS_W}/tcp")
rules+=("${PORT_HY2_W}/udp" "${PORT_HY2_OBFS_W}/udp" "${PORT_TUIC_W}/udp")
rules+=("${PORT_SS2022_W}/tcp" "${PORT_SS2022_W}/udp" "${PORT_SS_W}/tcp" "${PORT_SS_W}/udp")
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q -E "active|活跃"; then
for r in "${rules[@]}"; do ufw allow "$r" >/dev/null 2>&1 || true; done
ufw reload >/dev/null 2>&1 || true
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
systemctl enable --now firewalld >/dev/null 2>&1 || true
for r in "${rules[@]}"; do firewall-cmd --permanent --add-port="$r" >/dev/null 2>&1 || true; done
firewall-cmd --reload >/dev/null 2>&1 || true
else
local p proto
for r in "${rules[@]}"; do
p="${r%/*}"; proto="${r#*/}"
# IPv4
if [[ "$proto" == tcp ]]; then
iptables -C INPUT -p tcp --dport "$p" -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport "$p" -j ACCEPT
fi
if [[ "$proto" == udp ]]; then
iptables -C INPUT -p udp --dport "$p" -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport "$p" -j ACCEPT
fi
# IPv6关键补全
if command -v ip6tables >/dev/null 2>&1; then
if [[ "$proto" == tcp ]]; then
ip6tables -C INPUT -p tcp --dport "$p" -j ACCEPT 2>/dev/null || ip6tables -I INPUT -p tcp --dport "$p" -j ACCEPT
fi
if [[ "$proto" == udp ]]; then
ip6tables -C INPUT -p udp --dport "$p" -j ACCEPT 2>/dev/null || ip6tables -I INPUT -p udp --dport "$p" -j ACCEPT
fi
fi
done
# 保存netfilter-persistent 通常会把 v4/v6 一起保存)
command -v netfilter-persistent >/dev/null 2>&1 && netfilter-persistent save >/dev/null 2>&1 || true
fi
}
# ===== 分享链接(分组输出 + 提示) =====
print_links_grouped(){
load_env; load_creds; load_ports
local mode="${1:-4}" ip host
if [[ "$mode" == "6" ]]; then
ip="$(get_ip6)"
if [[ -z "$ip" ]]; then
warn "未检测到公网 IPv6自动回退到 IPv4"
ip="$(get_ip4)"
mode="4"
fi
else
ip="$(get_ip4)"
fi
host="$(fmt_host_for_uri "$ip")"
local links_direct=() links_warp=()
# 直连9
links_direct+=("vless://${UUID}@${host}:${PORT_VLESSR}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=${REALITY_SERVER}&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}&type=tcp#vless-reality")
links_direct+=("vless://${UUID}@${host}:${PORT_VLESS_GRPCR}?encryption=none&security=reality&sni=${REALITY_SERVER}&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}&type=grpc&serviceName=${GRPC_SERVICE}#vless-grpc-reality")
links_direct+=("trojan://${UUID}@${host}:${PORT_TROJANR}?security=reality&sni=${REALITY_SERVER}&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}&type=tcp#trojan-reality")
links_direct+=("hy2://$(urlenc "${HY2_PWD}")@${host}:${PORT_HY2}?insecure=1&allowInsecure=1&sni=${REALITY_SERVER}#hysteria2")
local VMESS_JSON; VMESS_JSON=$(cat <<JSON
{"v":"2","ps":"vmess-ws","add":"${ip}","port":"${PORT_VMESS_WS}","id":"${UUID}","aid":"0","net":"ws","type":"none","host":"","path":"${VMESS_WS_PATH}","tls":""}
JSON
)
links_direct+=("vmess://$(printf "%s" "$VMESS_JSON" | b64enc)")
links_direct+=("hy2://$(urlenc "${HY2_PWD2}")@${host}:${PORT_HY2_OBFS}?insecure=1&allowInsecure=1&sni=${REALITY_SERVER}&alpn=h3&obfs=salamander&obfs-password=$(urlenc "${HY2_OBFS_PWD}")#hysteria2-obfs")
links_direct+=("ss://$(printf "%s" "2022-blake3-aes-256-gcm:${SS2022_KEY}" | b64enc)@${host}:${PORT_SS2022}#ss2022")
links_direct+=("ss://$(printf "%s" "aes-256-gcm:${SS_PWD}" | b64enc)@${host}:${PORT_SS}#ss")
links_direct+=("tuic://${UUID}:$(urlenc "${UUID}")@${host}:${PORT_TUIC}?congestion_control=bbr&alpn=h3&insecure=1&allowInsecure=1&sni=${REALITY_SERVER}#tuic-v5")
# WARP 9
links_warp+=("vless://${UUID}@${host}:${PORT_VLESSR_W}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=${REALITY_SERVER}&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}&type=tcp#vless-reality-warp")
links_warp+=("vless://${UUID}@${host}:${PORT_VLESS_GRPCR_W}?encryption=none&security=reality&sni=${REALITY_SERVER}&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}&type=grpc&serviceName=${GRPC_SERVICE}#vless-grpc-reality-warp")
links_warp+=("trojan://${UUID}@${host}:${PORT_TROJANR_W}?security=reality&sni=${REALITY_SERVER}&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}&type=tcp#trojan-reality-warp")
links_warp+=("hy2://$(urlenc "${HY2_PWD}")@${host}:${PORT_HY2_W}?insecure=1&allowInsecure=1&sni=${REALITY_SERVER}#hysteria2-warp")
local VMESS_JSON_W; VMESS_JSON_W=$(cat <<JSON
{"v":"2","ps":"vmess-ws-warp","add":"${ip}","port":"${PORT_VMESS_WS_W}","id":"${UUID}","aid":"0","net":"ws","type":"none","host":"","path":"${VMESS_WS_PATH}","tls":""}
JSON
)
links_warp+=("vmess://$(printf "%s" "$VMESS_JSON_W" | b64enc)")
links_warp+=("hy2://$(urlenc "${HY2_PWD2}")@${host}:${PORT_HY2_OBFS_W}?insecure=1&allowInsecure=1&sni=${REALITY_SERVER}&alpn=h3&obfs=salamander&obfs-password=$(urlenc "${HY2_OBFS_PWD}")#hysteria2-obfs-warp")
links_warp+=("ss://$(printf "%s" "2022-blake3-aes-256-gcm:${SS2022_KEY}" | b64enc)@${host}:${PORT_SS2022_W}#ss2022-warp")
links_warp+=("ss://$(printf "%s" "aes-256-gcm:${SS_PWD}" | b64enc)@${host}:${PORT_SS_W}#ss-warp")
links_warp+=("tuic://${UUID}:$(urlenc "${UUID}")@${host}:${PORT_TUIC_W}?congestion_control=bbr&alpn=h3&insecure=1&allowInsecure=1&sni=${REALITY_SERVER}#tuic-v5-warp")
echo -e "${C_BLUE}${C_BOLD}分享链接18 个)${C_RESET}"
hr
echo -e "${C_CYAN}${C_BOLD}【直连节点9${C_RESET}vless-reality / vless-grpc-reality / trojan-reality / vmess-ws / hy2 / hy2-obfs / ss2022 / ss / tuic"
for l in "${links_direct[@]}"; do echo " $l"; done
hr
echo -e "${C_CYAN}${C_BOLD}【WARP 节点9${C_RESET}(同上 9 种,带 -warp"
echo -e "${C_DIM}说明:带 -warp 的 9 个节点走 Cloudflare WARP 出口,流媒体解锁更友好${C_RESET}"
for l in "${links_warp[@]}"; do echo " $l"; done
hr
echo -e "${C_YELLOW}📌 如果你使用 Xray-core v26.2.6+hysteria2 节点的 allowInsecure 已被移除,${C_RESET}"
echo -e "${C_YELLOW} 请改用以下 pinnedPeerCertSha256 节点:${C_RESET}"
echo " hy2://$(urlenc "${HY2_PWD}")@${host}:${PORT_HY2}?sni=${REALITY_SERVER}&pcs=${CRT_SHA256}#hysteria2-pinnedPeerCertSha256"
echo " hy2://$(urlenc "${HY2_PWD}")@${host}:${PORT_HY2_W}?sni=${REALITY_SERVER}&pcs=${CRT_SHA256}#hysteria2-warp-pinnedPeerCertSha256"
hr
}
# ===== BBR =====
enable_bbr(){
if sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr; then
info "BBR 已启用"
else
echo "net.core.default_qdisc=fq" >/etc/sysctl.d/99-bbr.conf
echo "net.ipv4.tcp_congestion_control=bbr" >>/etc/sysctl.d/99-bbr.conf
sysctl --system >/dev/null 2>&1 || true
info "已尝试开启 BBR如内核不支持需自行升级"
fi
}
# ===== 显示状态与 banner =====
sb_service_state(){
systemctl is-active --quiet "${SYSTEMD_SERVICE:-sing-box.service}" && echo -e "${C_GREEN}运行中${C_RESET}" || echo -e "${C_RED}未运行/未安装${C_RESET}"
}
bbr_state(){
sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr && echo -e "${C_GREEN}已启用 BBR${C_RESET}" || echo -e "${C_RED}未启用 BBR${C_RESET}"
}
banner(){
clear >/dev/null 2>&1 || true
hr
echo -e " ${C_CYAN}🚀 ${SCRIPT_NAME} ${SCRIPT_VERSION} 🚀${C_RESET}"
echo -e "${C_CYAN} 脚本更新地址: https://github.com/Alvin9999-newpac/Sing-Box-Plus${C_RESET}"
hr
echo -e "系统加速状态:$(bbr_state)"
echo -e "Sing-Box 启动状态:$(sb_service_state)"
hr
echo -e " ${C_BLUE}1)${C_RESET} 安装/部署18 节点)"
echo -e " ${C_GREEN}2)${C_RESET} 查看分享链接IPv4"
echo -e " ${C_GREEN}6)${C_RESET} 查看分享链接IPv6"
echo -e " ${C_GREEN}3)${C_RESET} 重启服务"
echo -e " ${C_GREEN}4)${C_RESET} 一键更换所有端口"
echo -e " ${C_GREEN}5)${C_RESET} 一键开启 BBR"
echo -e " ${C_RED}8)${C_RESET} 卸载"
echo -e " ${C_RED}0)${C_RESET} 退出"
hr
}
# ===== 业务流程 =====
restart_service(){
systemctl restart "${SYSTEMD_SERVICE}" || die "重启失败"
systemctl --no-pager status "${SYSTEMD_SERVICE}" | sed -n '1,6p' || true
}
rotate_ports(){
ensure_installed_or_hint || return 0
load_ports || true
rand_ports_reset
# 清空 18 项端口变量,触发重新分配不重复端口
PORT_VLESSR=""; PORT_VLESS_GRPCR=""; PORT_TROJANR=""; PORT_HY2=""; PORT_VMESS_WS=""
PORT_HY2_OBFS=""; PORT_SS2022=""; PORT_SS=""; PORT_TUIC=""
PORT_VLESSR_W=""; PORT_VLESS_GRPCR_W=""; PORT_TROJANR_W=""; PORT_HY2_W=""; PORT_VMESS_WS_W=""
PORT_HY2_OBFS_W=""; PORT_SS2022_W=""; PORT_SS_W=""; PORT_TUIC_W=""
save_all_ports # 重新生成并保存 18 个不重复端口
write_config # 用新端口重写 /opt/sing-box/config.json
open_firewall # ★ 新增:把“当前配置中的端口”全部放行
systemctl restart "${SYSTEMD_SERVICE}"
info "已更换端口并重启。"
read -p "回车返回..." _ || true
}
uninstall_all(){
systemctl stop "${SYSTEMD_SERVICE}" >/dev/null 2>&1 || true
systemctl disable "${SYSTEMD_SERVICE}" >/dev/null 2>&1 || true
rm -f "/etc/systemd/system/${SYSTEMD_SERVICE}"
systemctl daemon-reload
rm -rf "$SB_DIR"
echo -e "${C_GREEN}已卸载并清理完成。${C_RESET}"
exit 0
}
deploy_native(){
install_deps
install_singbox
write_config
info "检查配置 ..."
"$BIN_PATH" check -c "$CONF_JSON"
info "写入并启用 systemd 服务 ..."
write_systemd
systemctl restart "${SYSTEMD_SERVICE}" >/dev/null 2>&1 || true
open_firewall
echo; echo -e "${C_BOLD}${C_GREEN}★ 部署完成18 节点)${C_RESET}"; echo
# 打印链接并直接退出
print_links_grouped 4
exit 0
}
ensure_installed_or_hint(){
if [[ ! -f "$CONF_JSON" ]]; then
warn "尚未安装,请先选择 1) 安装/部署18 节点)"
return 1
fi
return 0
}
# ===== 菜单 =====
menu(){
banner
read -rp "选择: " op || true
case "${op:-}" in
1)
sbp_bootstrap # 依赖/二进制回退
set +e # ← 关闭严格退出,避免中途被杀掉
echo -e "${C_BLUE}[信息] 正在检查 sing-box 安装状态...${C_RESET}"
install_singbox || true
ensure_warpcli_proxy || true
write_config || { echo "[ERR] 生成配置失败"; }
write_systemd || true
open_firewall || true
systemctl restart "${SYSTEMD_SERVICE}" || true
set -e # ← 恢复严格模式
print_links_grouped
exit 0 # ← 打印后直接退出
;;
2) if ensure_installed_or_hint; then print_links_grouped 4; exit 0; fi ;;
6) if ensure_installed_or_hint; then print_links_grouped 6; exit 0; fi ;;
3) if ensure_installed_or_hint; then restart_service; fi; read -rp "回车返回..." _ || true; menu ;;
4) if ensure_installed_or_hint; then rotate_ports; fi; menu ;;
5) enable_bbr; read -rp "回车返回..." _ || true; menu ;;
8) uninstall_all ;; # 直接退出
0) exit 0 ;;
*) menu ;;
esac
}
# ===== 入口 =====
menu