├── README.md ├── install-singbox.sh ├── install-singbox-panel.sh ├── install-singbox-all.sh ├── install-singbox-yyds0.sh ├── install-singbox-yyds1.sh └── install-singbox-yyds2.sh /README.md: -------------------------------------------------------------------------------- 1 | # Sing-box 多协议一键部署脚本 2 | 3 | 一个强大的 Sing-box 自动化部署工具,支持SS2022/HY2/TUIC/VLESS Reality 协议自选部署和线路机 VLESS Reality 中转的完整解决方案。 4 | 5 | --- 6 | 7 | ## ✨ 主要特性 8 | 9 | ### 🎯 部署机功能 10 | 11 | - ✅ **一键安装** - 自动部署 Sing-box 最新服务端 12 | - ✅ **密钥生成** - 自动生成 密钥和配置文件 13 | - ✅ **多系统支持** - 支持 Alpine, Debian, Ubuntu, CentOS, RHEL, Fedora 等操作系统 14 | - ✅ **开机自启** - 自动配置 Systemd / OpenRC 开机自启 15 | - ✅ **公网 IP** - 自动获取公网 IP 并生成客户端链接 16 | - ✅ **管理工具** - 集成 sb 命令行工具,功能齐全 17 | 18 | ### 🔗 线路机功能 19 | 20 | - ✅ **一键生成** - 从落地机直接生成线路机安装脚本 21 | - ✅ **Reality 入站** - 自动部署 VLESS + TLS Reality 入站 22 | - ✅ **灵活端口** - 支持自动寻找空闲端口或手动指定 23 | - ✅ **流量转发** - 自动转发流量到落地机节点 24 | - ✅ **完整链接** - 生成可用的 VLESS Reality 客户端链接 25 | 26 | ## 🙏 特别鸣谢以下商家对本项目的赞助支持 27 | 28 |
29 | 30 | 31 | 32 | 38 | 44 | 50 | 51 |
33 | 34 | AliceNetworks 35 |
AliceNetworks 36 |
37 |
39 | 40 | 懒猫云 41 |
懒猫云 42 |
43 |
45 | 46 | 拼好鸡 47 |
拼好鸡 48 |
49 |
52 | 53 |
54 | 55 | 56 | ## ✅ 一键部署命令 57 | 58 | 安装全功能 sing-box: 59 | 60 | ```bash 61 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/caigouzi121380/singbox-deploy/main/install-singbox-yyds.sh)" 62 | -------------------------------------------------------------------------------- /install-singbox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # ----------------------- 5 | # 颜色输出函数 6 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 7 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 8 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 9 | 10 | # ----------------------- 11 | # 检测系统类型 12 | detect_os() { 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | OS_ID="${ID:-}" 16 | OS_ID_LIKE="${ID_LIKE:-}" 17 | else 18 | OS_ID="" 19 | OS_ID_LIKE="" 20 | fi 21 | 22 | if echo "$OS_ID $OS_ID_LIKE" | grep -qi "alpine"; then 23 | OS="alpine" 24 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 25 | OS="debian" 26 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 27 | OS="redhat" 28 | else 29 | OS="unknown" 30 | fi 31 | } 32 | 33 | detect_os 34 | info "检测到系统: $OS (${OS_ID:-unknown})" 35 | 36 | # ----------------------- 37 | # 检查 root 权限 38 | check_root() { 39 | if [ "$(id -u)" != "0" ]; then 40 | err "此脚本需要 root 权限运行" 41 | err "请使用: sudo bash -c \"\$(curl -fsSL ...)\" 或切换到 root 用户" 42 | exit 1 43 | fi 44 | } 45 | 46 | check_root 47 | 48 | # ----------------------- 49 | # 安装依赖 50 | install_deps() { 51 | info "安装系统依赖..." 52 | 53 | case "$OS" in 54 | alpine) 55 | apk update || { err "apk update 失败"; exit 1; } 56 | apk add --no-cache bash curl ca-certificates openssl openrc || { 57 | err "依赖安装失败" 58 | exit 1 59 | } 60 | 61 | # 确保 OpenRC 运行 62 | if ! rc-service --list 2>/dev/null | grep -q "^openrc"; then 63 | rc-update add openrc boot >/dev/null 2>&1 || true 64 | rc-service openrc start >/dev/null 2>&1 || true 65 | fi 66 | ;; 67 | debian) 68 | export DEBIAN_FRONTEND=noninteractive 69 | apt-get update -y || { err "apt update 失败"; exit 1; } 70 | apt-get install -y curl ca-certificates openssl || { 71 | err "依赖安装失败" 72 | exit 1 73 | } 74 | ;; 75 | redhat) 76 | yum install -y curl ca-certificates openssl || { 77 | err "依赖安装失败" 78 | exit 1 79 | } 80 | ;; 81 | *) 82 | warn "未识别的系统类型,尝试继续..." 83 | ;; 84 | esac 85 | 86 | info "依赖安装完成" 87 | } 88 | 89 | install_deps 90 | 91 | # ----------------------- 92 | # 端口和密码输入(支持环境变量) 93 | get_config() { 94 | # 支持通过环境变量传参,方便自动化部署 95 | if [ -n "${SINGBOX_PORT:-}" ]; then 96 | PORT="$SINGBOX_PORT" 97 | info "使用环境变量端口: $PORT" 98 | else 99 | echo "" 100 | read -p "请输入端口(留空则随机 10000-60000): " USER_PORT 101 | if [ -z "$USER_PORT" ]; then 102 | PORT=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 103 | info "使用随机端口: $PORT" 104 | else 105 | if ! [[ "$USER_PORT" =~ ^[0-9]+$ ]] || [ "$USER_PORT" -lt 1 ] || [ "$USER_PORT" -gt 65535 ]; then 106 | err "端口必须为 1-65535 的数字" 107 | exit 1 108 | fi 109 | PORT="$USER_PORT" 110 | fi 111 | fi 112 | 113 | if [ -n "${SINGBOX_PASSWORD:-}" ]; then 114 | USER_PWD="$SINGBOX_PASSWORD" 115 | info "使用环境变量密码" 116 | else 117 | echo "" 118 | read -p "请输入密码(留空则自动生成 Base64 密钥): " USER_PWD 119 | fi 120 | } 121 | 122 | get_config 123 | 124 | # ----------------------- 125 | # 安装 sing-box 126 | install_singbox() { 127 | info "开始安装 sing-box..." 128 | 129 | # 检查是否已安装 130 | if command -v sing-box >/dev/null 2>&1; then 131 | CURRENT_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 132 | warn "检测到已安装 sing-box: $CURRENT_VERSION" 133 | read -p "是否重新安装?(y/N): " REINSTALL 134 | if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then 135 | info "跳过 sing-box 安装" 136 | return 0 137 | fi 138 | fi 139 | 140 | case "$OS" in 141 | alpine) 142 | info "使用 Edge 仓库安装 sing-box" 143 | apk update || { err "apk update 失败"; exit 1; } 144 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 145 | err "sing-box 安装失败" 146 | exit 1 147 | } 148 | ;; 149 | debian|redhat) 150 | # 原官方安装脚本 151 | bash <(curl -fsSL https://sing-box.app/install.sh) || { 152 | err "sing-box 安装失败" 153 | err "请检查网络连接或手动安装" 154 | exit 1 155 | } 156 | ;; 157 | *) 158 | err "未支持的系统,无法安装 sing-box" 159 | exit 1 160 | ;; 161 | esac 162 | 163 | # 验证安装 164 | if ! command -v sing-box >/dev/null 2>&1; then 165 | err "sing-box 安装后未找到可执行文件" 166 | exit 1 167 | fi 168 | 169 | INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 170 | info "sing-box 安装成功: $INSTALLED_VERSION" 171 | } 172 | 173 | install_singbox 174 | 175 | # ----------------------- 176 | # 生成密码 177 | KEY_BYTES=16 178 | METHOD="2022-blake3-aes-128-gcm" 179 | 180 | generate_psk() { 181 | if [ -n "${USER_PWD:-}" ]; then 182 | PSK="$USER_PWD" 183 | info "使用指定密码" 184 | else 185 | info "自动生成密码..." 186 | 187 | # 优先使用 sing-box 188 | if command -v sing-box >/dev/null 2>&1; then 189 | PSK=$(sing-box generate rand --base64 "$KEY_BYTES" 2>/dev/null | tr -d '\n\r' || true) 190 | fi 191 | 192 | # 备选: openssl 193 | if [ -z "${PSK:-}" ] && command -v openssl >/dev/null 2>&1; then 194 | PSK=$(openssl rand -base64 "$KEY_BYTES" | tr -d '\n\r') 195 | fi 196 | 197 | # 最后备选: /dev/urandom 198 | if [ -z "${PSK:-}" ]; then 199 | PSK=$(head -c "$KEY_BYTES" /dev/urandom | base64 | tr -d '\n\r') 200 | fi 201 | 202 | if [ -z "${PSK:-}" ]; then 203 | err "密码生成失败" 204 | exit 1 205 | fi 206 | 207 | info "密码生成成功" 208 | fi 209 | } 210 | 211 | generate_psk 212 | 213 | # ----------------------- 214 | # 生成配置文件 215 | CONFIG_PATH="/etc/sing-box/config.json" 216 | 217 | create_config() { 218 | info "生成配置文件: $CONFIG_PATH" 219 | 220 | mkdir -p "$(dirname "$CONFIG_PATH")" 221 | 222 | cat > "$CONFIG_PATH" </dev/null 2>&1; then 249 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 250 | info "配置文件验证通过" 251 | else 252 | warn "配置文件验证失败,但将继续..." 253 | fi 254 | fi 255 | } 256 | 257 | create_config 258 | 259 | # ----------------------- 260 | # 设置服务 261 | setup_service() { 262 | info "配置系统服务..." 263 | 264 | if [ "$OS" = "alpine" ]; then 265 | # Alpine OpenRC 服务 266 | SERVICE_PATH="/etc/init.d/sing-box" 267 | 268 | cat > "$SERVICE_PATH" <<'OPENRC' 269 | #!/sbin/openrc-run 270 | 271 | name="sing-box" 272 | description="Sing-box Proxy Server" 273 | command="/usr/bin/sing-box" 274 | command_args="run -c /etc/sing-box/config.json" 275 | pidfile="/run/${RC_SVCNAME}.pid" 276 | command_background="yes" 277 | output_log="/var/log/sing-box.log" 278 | error_log="/var/log/sing-box.err" 279 | 280 | depend() { 281 | need net 282 | after firewall 283 | } 284 | 285 | start_pre() { 286 | checkpath --directory --mode 0755 /var/log 287 | checkpath --directory --mode 0755 /run 288 | } 289 | 290 | start_post() { 291 | sleep 1 292 | if [ -f "$pidfile" ]; then 293 | einfo "Sing-box started successfully (PID: $(cat $pidfile))" 294 | else 295 | ewarn "Sing-box may not have started correctly" 296 | fi 297 | } 298 | OPENRC 299 | 300 | chmod +x "$SERVICE_PATH" 301 | 302 | # 添加到开机自启 303 | rc-update add sing-box default >/dev/null 2>&1 || warn "添加开机自启失败" 304 | 305 | # 启动服务 306 | rc-service sing-box restart || { 307 | err "服务启动失败,查看日志:" 308 | tail -20 /var/log/sing-box.err 2>/dev/null || tail -20 /var/log/sing-box.log 2>/dev/null || true 309 | exit 1 310 | } 311 | 312 | sleep 2 313 | 314 | if rc-service sing-box status >/dev/null 2>&1; then 315 | info "✅ OpenRC 服务已启动" 316 | else 317 | err "服务状态异常" 318 | exit 1 319 | fi 320 | 321 | else 322 | # Systemd 服务 323 | SERVICE_PATH="/etc/systemd/system/sing-box.service" 324 | 325 | cat > "$SERVICE_PATH" <<'SYSTEMD' 326 | [Unit] 327 | Description=Sing-box Proxy Server 328 | Documentation=https://sing-box.sagernet.org 329 | After=network.target nss-lookup.target 330 | Wants=network.target 331 | 332 | [Service] 333 | Type=simple 334 | User=root 335 | WorkingDirectory=/etc/sing-box 336 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 337 | ExecReload=/bin/kill -HUP $MAINPID 338 | Restart=on-failure 339 | RestartSec=10s 340 | LimitNOFILE=1048576 341 | 342 | [Install] 343 | WantedBy=multi-user.target 344 | SYSTEMD 345 | 346 | systemctl daemon-reload 347 | systemctl enable sing-box >/dev/null 2>&1 348 | systemctl restart sing-box || { 349 | err "服务启动失败,查看日志:" 350 | journalctl -u sing-box -n 30 --no-pager 351 | exit 1 352 | } 353 | 354 | sleep 2 355 | 356 | if systemctl is-active sing-box >/dev/null 2>&1; then 357 | info "✅ Systemd 服务已启动" 358 | else 359 | err "服务状态异常" 360 | systemctl status sing-box --no-pager 361 | exit 1 362 | fi 363 | fi 364 | 365 | info "服务配置完成: $SERVICE_PATH" 366 | } 367 | 368 | setup_service 369 | 370 | # ----------------------- 371 | # 获取公网 IP 372 | get_public_ip() { 373 | local ip="" 374 | for url in \ 375 | "https://api.ipify.org" \ 376 | "https://ipinfo.io/ip" \ 377 | "https://ifconfig.me" \ 378 | "https://icanhazip.com" \ 379 | "https://ipecho.net/plain"; do 380 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 381 | if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 382 | echo "$ip" 383 | return 0 384 | fi 385 | done 386 | return 1 387 | } 388 | 389 | PUB_IP=$(get_public_ip || echo "YOUR_SERVER_IP") 390 | if [ "$PUB_IP" = "YOUR_SERVER_IP" ]; then 391 | warn "无法获取公网 IP,请手动替换" 392 | else 393 | info "检测到公网 IP: $PUB_IP" 394 | fi 395 | 396 | # ----------------------- 397 | # 生成 SS URI 398 | generate_uri() { 399 | local host="$PUB_IP" 400 | local tag="singbox-ss2022" 401 | local userinfo="${METHOD}:${PSK}" 402 | 403 | # SIP002 格式 (URL编码) 404 | local encoded_userinfo 405 | if command -v python3 >/dev/null 2>&1; then 406 | encoded_userinfo=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$userinfo', safe=''))" 2>/dev/null || echo "$userinfo") 407 | else 408 | encoded_userinfo=$(printf "%s" "$userinfo" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 409 | fi 410 | 411 | # Base64 格式 412 | local base64_userinfo=$(printf "%s" "$userinfo" | base64 -w0 2>/dev/null || printf "%s" "$userinfo" | base64 | tr -d '\n') 413 | 414 | echo "ss://${encoded_userinfo}@${host}:${PORT}#${tag}" 415 | echo "ss://${base64_userinfo}@${host}:${PORT}#${tag}" 416 | } 417 | 418 | # ----------------------- 419 | # 最终输出 420 | echo "" 421 | echo "==========================================" 422 | info "🎉 Sing-box 部署完成!" 423 | echo "==========================================" 424 | echo "" 425 | info "📋 配置信息:" 426 | echo " 端口: $PORT" 427 | echo " 方法: $METHOD" 428 | echo " 密码: $PSK" 429 | echo " 服务器: $PUB_IP" 430 | echo "" 431 | info "📁 文件位置:" 432 | echo " 配置: $CONFIG_PATH" 433 | echo " 服务: $SERVICE_PATH" 434 | echo "" 435 | info "🔗 客户端链接:" 436 | generate_uri | while IFS= read -r line; do 437 | echo " $line" 438 | done 439 | echo "" 440 | info "🔧 管理命令:" 441 | if [ "$OS" = "alpine" ]; then 442 | echo " 启动: rc-service sing-box start" 443 | echo " 停止: rc-service sing-box stop" 444 | echo " 重启: rc-service sing-box restart" 445 | echo " 状态: rc-service sing-box status" 446 | echo " 日志: tail -f /var/log/sing-box.log" 447 | else 448 | echo " 启动: systemctl start sing-box" 449 | echo " 停止: systemctl stop sing-box" 450 | echo " 重启: systemctl restart sing-box" 451 | echo " 状态: systemctl status sing-box" 452 | echo " 日志: journalctl -u sing-box -f" 453 | fi 454 | echo "" 455 | echo "==========================================" 456 | -------------------------------------------------------------------------------- /install-singbox-panel.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # ----------------------- 5 | # 颜色输出函数 6 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 7 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 8 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 9 | 10 | # ----------------------- 11 | # 检测系统类型 12 | detect_os() { 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | OS_ID="${ID:-}" 16 | OS_ID_LIKE="${ID_LIKE:-}" 17 | else 18 | OS_ID="" 19 | OS_ID_LIKE="" 20 | fi 21 | 22 | if echo "$OS_ID $OS_ID_LIKE" | grep -qi "alpine"; then 23 | OS="alpine" 24 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 25 | OS="debian" 26 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 27 | OS="redhat" 28 | else 29 | OS="unknown" 30 | fi 31 | } 32 | 33 | detect_os 34 | info "检测到系统: $OS (${OS_ID:-unknown})" 35 | 36 | # ----------------------- 37 | # 检查 root 权限 38 | check_root() { 39 | if [ "$(id -u)" != "0" ]; then 40 | err "此脚本需要 root 权限运行" 41 | err "请使用: sudo bash -c \"\$(curl -fsSL ...)\" 或切换到 root 用户" 42 | exit 1 43 | fi 44 | } 45 | 46 | check_root 47 | 48 | # ----------------------- 49 | # 安装依赖 50 | install_deps() { 51 | info "安装系统依赖..." 52 | 53 | case "$OS" in 54 | alpine) 55 | apk update || { err "apk update 失败"; exit 1; } 56 | apk add --no-cache bash curl ca-certificates openssl openrc || { 57 | err "依赖安装失败" 58 | exit 1 59 | } 60 | 61 | # 确保 OpenRC 运行 62 | if ! rc-service --list 2>/dev/null | grep -q "^openrc"; then 63 | rc-update add openrc boot >/dev/null 2>&1 || true 64 | rc-service openrc start >/dev/null 2>&1 || true 65 | fi 66 | ;; 67 | debian) 68 | export DEBIAN_FRONTEND=noninteractive 69 | apt-get update -y || { err "apt update 失败"; exit 1; } 70 | apt-get install -y curl ca-certificates openssl || { 71 | err "依赖安装失败" 72 | exit 1 73 | } 74 | ;; 75 | redhat) 76 | yum install -y curl ca-certificates openssl || { 77 | err "依赖安装失败" 78 | exit 1 79 | } 80 | ;; 81 | *) 82 | warn "未识别的系统类型,尝试继续..." 83 | ;; 84 | esac 85 | 86 | info "依赖安装完成" 87 | } 88 | 89 | install_deps 90 | 91 | # ----------------------- 92 | # 端口和密码输入(支持环境变量) 93 | get_config() { 94 | # 支持通过环境变量传参,方便自动化部署 95 | if [ -n "${SINGBOX_PORT:-}" ]; then 96 | PORT="$SINGBOX_PORT" 97 | info "使用环境变量端口: $PORT" 98 | else 99 | echo "" 100 | read -p "请输入端口(留空则随机 10000-60000): " USER_PORT 101 | if [ -z "$USER_PORT" ]; then 102 | PORT=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 103 | info "使用随机端口: $PORT" 104 | else 105 | if ! [[ "$USER_PORT" =~ ^[0-9]+$ ]] || [ "$USER_PORT" -lt 1 ] || [ "$USER_PORT" -gt 65535 ]; then 106 | err "端口必须为 1-65535 的数字" 107 | exit 1 108 | fi 109 | PORT="$USER_PORT" 110 | fi 111 | fi 112 | 113 | if [ -n "${SINGBOX_PASSWORD:-}" ]; then 114 | USER_PWD="$SINGBOX_PASSWORD" 115 | info "使用环境变量密码" 116 | else 117 | echo "" 118 | read -p "请输入密码(留空则自动生成 Base64 密钥): " USER_PWD 119 | fi 120 | } 121 | 122 | get_config 123 | 124 | # ----------------------- 125 | # 安装 sing-box 126 | install_singbox() { 127 | info "开始安装 sing-box..." 128 | 129 | # 检查是否已安装 130 | if command -v sing-box >/dev/null 2>&1; then 131 | CURRENT_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 132 | warn "检测到已安装 sing-box: $CURRENT_VERSION" 133 | read -p "是否重新安装?(y/N): " REINSTALL 134 | if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then 135 | info "跳过 sing-box 安装" 136 | return 0 137 | fi 138 | fi 139 | 140 | case "$OS" in 141 | alpine) 142 | info "使用 Edge 仓库安装 sing-box" 143 | apk update || { err "apk update 失败"; exit 1; } 144 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 145 | err "sing-box 安装失败" 146 | exit 1 147 | } 148 | ;; 149 | debian|redhat) 150 | # 原官方安装脚本 151 | bash <(curl -fsSL https://sing-box.app/install.sh) || { 152 | err "sing-box 安装失败" 153 | err "请检查网络连接或手动安装" 154 | exit 1 155 | } 156 | ;; 157 | *) 158 | err "未支持的系统,无法安装 sing-box" 159 | exit 1 160 | ;; 161 | esac 162 | 163 | # 验证安装 164 | if ! command -v sing-box >/dev/null 2>&1; then 165 | err "sing-box 安装后未找到可执行文件" 166 | exit 1 167 | fi 168 | 169 | INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 170 | info "sing-box 安装成功: $INSTALLED_VERSION" 171 | } 172 | 173 | install_singbox 174 | 175 | # ----------------------- 176 | # 生成密码 177 | KEY_BYTES=16 178 | METHOD="2022-blake3-aes-128-gcm" 179 | 180 | generate_psk() { 181 | if [ -n "${USER_PWD:-}" ]; then 182 | PSK="$USER_PWD" 183 | info "使用指定密码" 184 | else 185 | info "自动生成密码..." 186 | 187 | # 优先使用 sing-box 188 | if command -v sing-box >/dev/null 2>&1; then 189 | PSK=$(sing-box generate rand --base64 "$KEY_BYTES" 2>/dev/null | tr -d '\n\r' || true) 190 | fi 191 | 192 | # 备选: openssl 193 | if [ -z "${PSK:-}" ] && command -v openssl >/dev/null 2>&1; then 194 | PSK=$(openssl rand -base64 "$KEY_BYTES" | tr -d '\n\r') 195 | fi 196 | 197 | # 最后备选: /dev/urandom 198 | if [ -z "${PSK:-}" ]; then 199 | PSK=$(head -c "$KEY_BYTES" /dev/urandom | base64 | tr -d '\n\r') 200 | fi 201 | 202 | if [ -z "${PSK:-}" ]; then 203 | err "密码生成失败" 204 | exit 1 205 | fi 206 | 207 | info "密码生成成功" 208 | fi 209 | } 210 | 211 | generate_psk 212 | 213 | # ----------------------- 214 | # 生成配置文件 215 | CONFIG_PATH="/etc/sing-box/config.json" 216 | 217 | create_config() { 218 | info "生成配置文件: $CONFIG_PATH" 219 | 220 | mkdir -p "$(dirname "$CONFIG_PATH")" 221 | 222 | cat > "$CONFIG_PATH" </dev/null 2>&1; then 249 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 250 | info "配置文件验证通过" 251 | else 252 | warn "配置文件验证失败,但将继续..." 253 | fi 254 | fi 255 | } 256 | 257 | create_config 258 | 259 | # ----------------------- 260 | # 设置服务 261 | setup_service() { 262 | info "配置系统服务..." 263 | 264 | if [ "$OS" = "alpine" ]; then 265 | # Alpine OpenRC 服务 266 | SERVICE_PATH="/etc/init.d/sing-box" 267 | 268 | cat > "$SERVICE_PATH" <<'OPENRC' 269 | #!/sbin/openrc-run 270 | 271 | name="sing-box" 272 | description="Sing-box Proxy Server" 273 | command="/usr/bin/sing-box" 274 | command_args="run -c /etc/sing-box/config.json" 275 | pidfile="/run/${RC_SVCNAME}.pid" 276 | command_background="yes" 277 | output_log="/var/log/sing-box.log" 278 | error_log="/var/log/sing-box.err" 279 | 280 | depend() { 281 | need net 282 | after firewall 283 | } 284 | 285 | start_pre() { 286 | checkpath --directory --mode 0755 /var/log 287 | checkpath --directory --mode 0755 /run 288 | } 289 | 290 | start_post() { 291 | sleep 1 292 | if [ -f "$pidfile" ]; then 293 | einfo "Sing-box started successfully (PID: $(cat $pidfile))" 294 | else 295 | ewarn "Sing-box may not have started correctly" 296 | fi 297 | } 298 | OPENRC 299 | 300 | chmod +x "$SERVICE_PATH" 301 | 302 | # 添加到开机自启 303 | rc-update add sing-box default >/dev/null 2>&1 || warn "添加开机自启失败" 304 | 305 | # 启动服务 306 | rc-service sing-box restart || { 307 | err "服务启动失败,查看日志:" 308 | tail -20 /var/log/sing-box.err 2>/dev/null || tail -20 /var/log/sing-box.log 2>/dev/null || true 309 | exit 1 310 | } 311 | 312 | sleep 2 313 | 314 | if rc-service sing-box status >/dev/null 2>&1; then 315 | info "✅ OpenRC 服务已启动" 316 | else 317 | err "服务状态异常" 318 | exit 1 319 | fi 320 | 321 | else 322 | # Systemd 服务 323 | SERVICE_PATH="/etc/systemd/system/sing-box.service" 324 | 325 | cat > "$SERVICE_PATH" <<'SYSTEMD' 326 | [Unit] 327 | Description=Sing-box Proxy Server 328 | Documentation=https://sing-box.sagernet.org 329 | After=network.target nss-lookup.target 330 | Wants=network.target 331 | 332 | [Service] 333 | Type=simple 334 | User=root 335 | WorkingDirectory=/etc/sing-box 336 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 337 | ExecReload=/bin/kill -HUP $MAINPID 338 | Restart=on-failure 339 | RestartSec=10s 340 | LimitNOFILE=1048576 341 | 342 | [Install] 343 | WantedBy=multi-user.target 344 | SYSTEMD 345 | 346 | systemctl daemon-reload 347 | systemctl enable sing-box >/dev/null 2>&1 348 | systemctl restart sing-box || { 349 | err "服务启动失败,查看日志:" 350 | journalctl -u sing-box -n 30 --no-pager 351 | exit 1 352 | } 353 | 354 | sleep 2 355 | 356 | if systemctl is-active sing-box >/dev/null 2>&1; then 357 | info "✅ Systemd 服务已启动" 358 | else 359 | err "服务状态异常" 360 | systemctl status sing-box --no-pager 361 | exit 1 362 | fi 363 | fi 364 | 365 | info "服务配置完成: $SERVICE_PATH" 366 | } 367 | 368 | setup_service 369 | 370 | # ----------------------- 371 | # 获取公网 IP 372 | get_public_ip() { 373 | local ip="" 374 | for url in \ 375 | "https://api.ipify.org" \ 376 | "https://ipinfo.io/ip" \ 377 | "https://ifconfig.me" \ 378 | "https://icanhazip.com" \ 379 | "https://ipecho.net/plain"; do 380 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 381 | if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 382 | echo "$ip" 383 | return 0 384 | fi 385 | done 386 | return 1 387 | } 388 | 389 | PUB_IP=$(get_public_ip || echo "YOUR_SERVER_IP") 390 | if [ "$PUB_IP" = "YOUR_SERVER_IP" ]; then 391 | warn "无法获取公网 IP,请手动替换" 392 | else 393 | info "检测到公网 IP: $PUB_IP" 394 | fi 395 | 396 | # ----------------------- 397 | # 生成 SS URI 398 | generate_uri() { 399 | local host="$PUB_IP" 400 | local tag="singbox-ss2022" 401 | local userinfo="${METHOD}:${PSK}" 402 | 403 | # SIP002 格式 (URL编码) 404 | local encoded_userinfo 405 | if command -v python3 >/dev/null 2>&1; then 406 | encoded_userinfo=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$userinfo" 2>/dev/null || echo "$userinfo") 407 | else 408 | encoded_userinfo=$(printf "%s" "$userinfo" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 409 | fi 410 | 411 | # Base64 格式 412 | local base64_userinfo=$(printf "%s" "$userinfo" | base64 -w0 2>/dev/null || printf "%s" "$userinfo" | base64 | tr -d '\n') 413 | 414 | echo "ss://${encoded_userinfo}@${host}:${PORT}#${tag}" 415 | echo "ss://${base64_userinfo}@${host}:${PORT}#${tag}" 416 | } 417 | 418 | # ----------------------- 419 | # 最终输出 420 | echo "" 421 | echo "==========================================" 422 | info "🎉 Sing-box 部署完成!" 423 | echo "==========================================" 424 | echo "" 425 | info "📋 配置信息:" 426 | echo " 端口: $PORT" 427 | echo " 方法: $METHOD" 428 | echo " 密码: $PSK" 429 | echo " 服务器: $PUB_IP" 430 | echo "" 431 | info "📁 文件位置:" 432 | echo " 配置: $CONFIG_PATH" 433 | echo " 服务: $SERVICE_PATH" 434 | echo "" 435 | info "🔗 客户端链接:" 436 | generate_uri | while IFS= read -r line; do 437 | echo " $line" 438 | done 439 | echo "" 440 | info "🔧 管理命令:" 441 | if [ "$OS" = "alpine" ]; then 442 | echo " 启动: rc-service sing-box start" 443 | echo " 停止: rc-service sing-box stop" 444 | echo " 重启: rc-service sing-box restart" 445 | echo " 状态: rc-service sing-box status" 446 | echo " 日志: tail -f /var/log/sing-box.log" 447 | else 448 | echo " 启动: systemctl start sing-box" 449 | echo " 停止: systemctl stop sing-box" 450 | echo " 重启: systemctl restart sing-box" 451 | echo " 状态: systemctl status sing-box" 452 | echo " 日志: journalctl -u sing-box -f" 453 | fi 454 | echo "" 455 | echo "==========================================" 456 | 457 | # ----------------------- 458 | # Create `sb` management script at /usr/local/bin/sb 459 | # (Do not modify other parts of the original script; sb is added as a separate tool) 460 | SB_PATH="/usr/local/bin/sb" 461 | 462 | info "正在创建 sb 管理脚本: $SB_PATH" 463 | 464 | cat > "$SB_PATH" <<'SB_SCRIPT' 465 | #!/usr/bin/env bash 466 | set -euo pipefail 467 | 468 | # Simple Sing-box manager (sb) 469 | # Supports: view SS URI, view config path, edit config, reset port/password, 470 | # start/stop/restart/status, update, uninstall (delete all). 471 | # Uses /etc/sing-box/config.json and saves ss uri to /etc/sing-box/ss_uri.txt 472 | 473 | # colors 474 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 475 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 476 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 477 | 478 | CONFIG_PATH="/etc/sing-box/config.json" 479 | SS_URI_PATH="/etc/sing-box/ss_uri.txt" 480 | BIN_PATH="/usr/bin/sing-box" 481 | SERVICE_NAME="sing-box" 482 | 483 | # detect OS 484 | detect_os() { 485 | if [ -f /etc/os-release ]; then 486 | . /etc/os-release 487 | ID="${ID:-}" 488 | ID_LIKE="${ID_LIKE:-}" 489 | else 490 | ID="" 491 | ID_LIKE="" 492 | fi 493 | 494 | if echo "$ID $ID_LIKE" | grep -qi "alpine"; then 495 | OS="alpine" 496 | elif echo "$ID $ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 497 | OS="debian" 498 | elif echo "$ID $ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 499 | OS="redhat" 500 | else 501 | OS="unknown" 502 | fi 503 | } 504 | 505 | detect_os 506 | 507 | # service helpers 508 | service_start() { 509 | if [ "$OS" = "alpine" ]; then 510 | rc-service "$SERVICE_NAME" start 511 | else 512 | systemctl start "$SERVICE_NAME" 513 | fi 514 | } 515 | service_stop() { 516 | if [ "$OS" = "alpine" ]; then 517 | rc-service "$SERVICE_NAME" stop 518 | else 519 | systemctl stop "$SERVICE_NAME" 520 | fi 521 | } 522 | service_restart() { 523 | if [ "$OS" = "alpine" ]; then 524 | rc-service "$SERVICE_NAME" restart 525 | else 526 | systemctl restart "$SERVICE_NAME" 527 | fi 528 | } 529 | service_status() { 530 | if [ "$OS" = "alpine" ]; then 531 | rc-service "$SERVICE_NAME" status 532 | else 533 | systemctl status "$SERVICE_NAME" --no-pager 534 | fi 535 | } 536 | 537 | # Extract fields from config.json (method, password, port) 538 | read_config_fields() { 539 | if [ ! -f "$CONFIG_PATH" ]; then 540 | err "未找到配置文件: $CONFIG_PATH" 541 | return 1 542 | fi 543 | 544 | # Try python3 first 545 | if command -v python3 >/dev/null 2>&1; then 546 | METHOD=$(python3 - <<'PY' 547 | import json,sys 548 | c=json.load(open('/etc/sing-box/config.json')) 549 | try: 550 | m=c['inbounds'][0].get('method','') 551 | except Exception: 552 | m='' 553 | print(m) 554 | PY 555 | ) 556 | PSK=$(python3 - <<'PY' 557 | import json,sys 558 | c=json.load(open('/etc/sing-box/config.json')) 559 | try: 560 | p=c['inbounds'][0].get('password','') 561 | except Exception: 562 | p='' 563 | print(p) 564 | PY 565 | ) 566 | PORT=$(python3 - <<'PY' 567 | import json,sys 568 | c=json.load(open('/etc/sing-box/config.json')) 569 | try: 570 | port=c['inbounds'][0].get('listen_port','') 571 | except Exception: 572 | port='' 573 | print(port) 574 | PY 575 | ) 576 | else 577 | # fallback: simple grep+sed (works with the config format generated by installer) 578 | METHOD=$(grep -m1 '"method"' "$CONFIG_PATH" 2>/dev/null | sed -E 's/.*"method"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true) 579 | PSK=$(grep -m1 '"password"' "$CONFIG_PATH" 2>/dev/null | sed -E 's/.*"password"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true) 580 | PORT=$(grep -m1 '"listen_port"' "$CONFIG_PATH" 2>/dev/null | sed -E 's/.*"listen_port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/' || true) 581 | fi 582 | 583 | METHOD="${METHOD:-}" 584 | PSK="${PSK:-}" 585 | PORT="${PORT:-}" 586 | } 587 | 588 | # generate ss uri from current config and save to SS_URI_PATH 589 | generate_and_save_uri() { 590 | read_config_fields || return 1 591 | 592 | # get public ip 593 | PUBLIC_IP="" 594 | for url in "https://api.ipify.org" "https://ipinfo.io/ip" "https://ifconfig.me" "https://icanhazip.com" "https://ipecho.net/plain"; do 595 | PUBLIC_IP=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 596 | if [ -n "$PUBLIC_IP" ]; then break; fi 597 | done 598 | if [ -z "$PUBLIC_IP" ]; then PUBLIC_IP="YOUR_SERVER_IP"; fi 599 | 600 | # build URIs 601 | userinfo="${METHOD}:${PSK}" 602 | 603 | # url-encoded userinfo 604 | if command -v python3 >/dev/null 2>&1; then 605 | encoded_userinfo=$(python3 - </dev/null || printf "%s" "$userinfo" | base64 | tr -d '\n') 615 | 616 | echo "ss://${encoded_userinfo}@${PUBLIC_IP}:${PORT}#singbox-ss2022" > "$SS_URI_PATH" 617 | echo "ss://${base64_userinfo}@${PUBLIC_IP}:${PORT}#singbox-ss2022" >> "$SS_URI_PATH" 618 | 619 | info "SS URI 已写入: $SS_URI_PATH" 620 | } 621 | 622 | # View SS URI (每次直接从配置生成) 623 | action_view_uri() { 624 | info "正在从配置生成 SS URI..." 625 | generate_and_save_uri || { err "生成 SS URI 失败"; return 1; } 626 | 627 | # 显示生成的 SS URI 628 | sed -n '1,200p' "$SS_URI_PATH" 629 | } 630 | 631 | # View config path 632 | action_view_config() { 633 | echo "$CONFIG_PATH" 634 | } 635 | 636 | # Edit config 637 | action_edit_config() { 638 | if [ ! -f "$CONFIG_PATH" ]; then 639 | err "配置文件不存在: $CONFIG_PATH" 640 | return 1 641 | fi 642 | 643 | if command -v nano >/dev/null 2>&1; then 644 | ${EDITOR:-nano} "$CONFIG_PATH" 645 | else 646 | ${EDITOR:-vi} "$CONFIG_PATH" 647 | fi 648 | 649 | # validate 650 | if command -v sing-box >/dev/null 2>&1; then 651 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 652 | info "配置校验通过,重启服务" 653 | service_restart || warn "重启失败" 654 | # regenerate ss uri 655 | generate_and_save_uri || true 656 | else 657 | warn "配置校验失败,请手动检查。服务未被重启。" 658 | fi 659 | else 660 | warn "未检测到 sing-box 可执行文件,无法校验或重启" 661 | fi 662 | } 663 | 664 | # Reset port & password 665 | action_reset_port_pwd() { 666 | [ -f "$CONFIG_PATH" ] || { err "配置文件不存在: $CONFIG_PATH"; return 1; } 667 | 668 | read -p "输入新端口(回车随机 10000-60000): " new_port 669 | [ -z "$new_port" ] && new_port=$((RANDOM % 50001 + 10000)) 670 | 671 | read -p "输入新密码(回车随机生成 Base64 密钥): " new_pwd 672 | [ -z "$new_pwd" ] && new_pwd=$(head -c 16 /dev/urandom | base64 | tr -d '\n\r') 673 | 674 | # 停止 sing-box 服务 675 | info "正在停止 sing-box 服务..." 676 | service_stop || warn "停止服务失败" 677 | 678 | # 直接覆盖配置文件 679 | cat > "$CONFIG_PATH" </dev/null 2>&1; then 726 | NEW_VER=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 727 | info "当前 sing-box 版本: $NEW_VER" 728 | service_restart || warn "重启失败" 729 | else 730 | warn "更新后未检测到 sing-box 可执行文件" 731 | fi 732 | } 733 | 734 | # Uninstall sing-box (no confirmation, as requested) 735 | action_uninstall() { 736 | info "正在卸载 sing-box..." 737 | service_stop || true 738 | if [ "$OS" = "alpine" ]; then 739 | rc-update del "$SERVICE_NAME" default >/dev/null 2>&1 || true 740 | [ -f "/etc/init.d/$SERVICE_NAME" ] && rm -f "/etc/init.d/$SERVICE_NAME" 741 | apk del sing-box >/dev/null 2>&1 || true 742 | else 743 | systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true 744 | systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true 745 | [ -f "/etc/systemd/system/$SERVICE_NAME.service" ] && rm -f "/etc/systemd/system/$SERVICE_NAME.service" 746 | systemctl daemon-reload >/dev/null 2>&1 || true 747 | fi 748 | rm -rf /etc/sing-box /var/log/sing-box* /usr/local/bin/sb "$BIN_PATH" >/dev/null 2>&1 || true 749 | info "卸载完成" 750 | } 751 | 752 | # Menu 753 | while true; do 754 | cat <<'MENU' 755 | 756 | ========================== 757 | Sing-box 管理面板 (快捷指令sb) 758 | ========================== 759 | 1) 查看 SS URI 760 | 2) 查看配置文件路径 761 | 3) 编辑配置文件 762 | 4) 重置密码/端口 763 | 5) 启动服务 764 | 6) 停止服务 765 | 7) 重启服务 766 | 8) 查看状态 767 | 9) 更新 sing-box 768 | 10) 卸载 sing-box(无确认) 769 | 0) 退出 770 | ========================== 771 | MENU 772 | 773 | read -p "请输入选项: " opt 774 | case "${opt:-}" in 775 | 1) action_view_uri ;; 776 | 2) action_view_config ;; 777 | 3) action_edit_config ;; 778 | 4) action_reset_port_pwd ;; 779 | 5) service_start && info "已发送启动命令" ;; 780 | 6) service_stop && info "已发送停止命令" ;; 781 | 7) service_restart && info "已发送重启命令" ;; 782 | 8) service_status ;; 783 | 9) action_update ;; 784 | 10) action_uninstall; exit 0 ;; 785 | 0) exit 0 ;; 786 | *) warn "无效选项" ;; 787 | esac 788 | 789 | echo "" 790 | done 791 | SB_SCRIPT 792 | 793 | # set executable 794 | chmod +x "$SB_PATH" || warn "无法设置 $SB_PATH 为可执行" 795 | 796 | info "sb 已创建:请输入 sb 运行管理面板" 797 | 798 | # end of script 799 | -------------------------------------------------------------------------------- /install-singbox-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # ----------------------- 5 | # 颜色输出函数 6 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 7 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 8 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 9 | 10 | # ----------------------- 11 | # 检测系统类型 12 | detect_os() { 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | OS_ID="${ID:-}" 16 | OS_ID_LIKE="${ID_LIKE:-}" 17 | else 18 | OS_ID="" 19 | OS_ID_LIKE="" 20 | fi 21 | 22 | if echo "$OS_ID $OS_ID_LIKE" | grep -qi "alpine"; then 23 | OS="alpine" 24 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 25 | OS="debian" 26 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 27 | OS="redhat" 28 | else 29 | OS="unknown" 30 | fi 31 | } 32 | 33 | detect_os 34 | info "检测到系统: $OS (${OS_ID:-unknown})" 35 | 36 | # ----------------------- 37 | # 检查 root 权限 38 | check_root() { 39 | if [ "$(id -u)" != "0" ]; then 40 | err "此脚本需要 root 权限运行" 41 | err "请使用: sudo bash -c \"\$(curl -fsSL ...)\" 或切换到 root 用户" 42 | exit 1 43 | fi 44 | } 45 | 46 | check_root 47 | 48 | # ----------------------- 49 | # 安装依赖 50 | install_deps() { 51 | info "安装系统依赖..." 52 | 53 | case "$OS" in 54 | alpine) 55 | apk update || { err "apk update 失败"; exit 1; } 56 | apk add --no-cache bash curl ca-certificates openssl openrc || { 57 | err "依赖安装失败" 58 | exit 1 59 | } 60 | 61 | # 确保 OpenRC 运行 62 | if ! rc-service --list 2>/dev/null | grep -q "^openrc"; then 63 | rc-update add openrc boot >/dev/null 2>&1 || true 64 | rc-service openrc start >/dev/null 2>&1 || true 65 | fi 66 | ;; 67 | debian) 68 | export DEBIAN_FRONTEND=noninteractive 69 | apt-get update -y || { err "apt update 失败"; exit 1; } 70 | apt-get install -y curl ca-certificates openssl || { 71 | err "依赖安装失败" 72 | exit 1 73 | } 74 | ;; 75 | redhat) 76 | yum install -y curl ca-certificates openssl || { 77 | err "依赖安装失败" 78 | exit 1 79 | } 80 | ;; 81 | *) 82 | warn "未识别的系统类型,尝试继续..." 83 | ;; 84 | esac 85 | 86 | info "依赖安装完成" 87 | } 88 | 89 | install_deps 90 | 91 | # ----------------------- 92 | # 端口和密码输入(支持环境变量) 93 | get_config() { 94 | # 支持通过环境变量传参,方便自动化部署 95 | if [ -n "${SINGBOX_PORT:-}" ]; then 96 | PORT="$SINGBOX_PORT" 97 | info "使用环境变量端口: $PORT" 98 | else 99 | echo "" 100 | read -p "请输入端口(留空则随机 10000-60000): " USER_PORT 101 | if [ -z "$USER_PORT" ]; then 102 | PORT=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 103 | info "使用随机端口: $PORT" 104 | else 105 | if ! [[ "$USER_PORT" =~ ^[0-9]+$ ]] || [ "$USER_PORT" -lt 1 ] || [ "$USER_PORT" -gt 65535 ]; then 106 | err "端口必须为 1-65535 的数字" 107 | exit 1 108 | fi 109 | PORT="$USER_PORT" 110 | fi 111 | fi 112 | 113 | if [ -n "${SINGBOX_PASSWORD:-}" ]; then 114 | USER_PWD="$SINGBOX_PASSWORD" 115 | info "使用环境变量密码" 116 | else 117 | echo "" 118 | read -p "请输入密码(留空则自动生成 Base64 密钥): " USER_PWD 119 | fi 120 | } 121 | 122 | get_config 123 | 124 | # ----------------------- 125 | # 安装 sing-box 126 | install_singbox() { 127 | info "开始安装 sing-box..." 128 | 129 | # 检查是否已安装 130 | if command -v sing-box >/dev/null 2>&1; then 131 | CURRENT_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 132 | warn "检测到已安装 sing-box: $CURRENT_VERSION" 133 | read -p "是否重新安装?(y/N): " REINSTALL 134 | if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then 135 | info "跳过 sing-box 安装" 136 | return 0 137 | fi 138 | fi 139 | 140 | case "$OS" in 141 | alpine) 142 | info "使用 Edge 仓库安装 sing-box" 143 | apk update || { err "apk update 失败"; exit 1; } 144 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 145 | err "sing-box 安装失败" 146 | exit 1 147 | } 148 | ;; 149 | debian|redhat) 150 | # 原官方安装脚本 151 | bash <(curl -fsSL https://sing-box.app/install.sh) || { 152 | err "sing-box 安装失败" 153 | err "请检查网络连接或手动安装" 154 | exit 1 155 | } 156 | ;; 157 | *) 158 | err "未支持的系统,无法安装 sing-box" 159 | exit 1 160 | ;; 161 | esac 162 | 163 | # 验证安装 164 | if ! command -v sing-box >/dev/null 2>&1; then 165 | err "sing-box 安装后未找到可执行文件" 166 | exit 1 167 | fi 168 | 169 | INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 170 | info "sing-box 安装成功: $INSTALLED_VERSION" 171 | } 172 | 173 | install_singbox 174 | 175 | # ----------------------- 176 | # 生成密码 177 | KEY_BYTES=16 178 | METHOD="2022-blake3-aes-128-gcm" 179 | 180 | generate_psk() { 181 | if [ -n "${USER_PWD:-}" ]; then 182 | PSK="$USER_PWD" 183 | info "使用指定密码" 184 | else 185 | info "自动生成密码..." 186 | 187 | # 优先使用 sing-box 188 | if command -v sing-box >/dev/null 2>&1; then 189 | PSK=$(sing-box generate rand --base64 "$KEY_BYTES" 2>/dev/null | tr -d '\n\r' || true) 190 | fi 191 | 192 | # 备选: openssl 193 | if [ -z "${PSK:-}" ] && command -v openssl >/dev/null 2>&1; then 194 | PSK=$(openssl rand -base64 "$KEY_BYTES" | tr -d '\n\r') 195 | fi 196 | 197 | # 最后备选: /dev/urandom 198 | if [ -z "${PSK:-}" ]; then 199 | PSK=$(head -c "$KEY_BYTES" /dev/urandom | base64 | tr -d '\n\r') 200 | fi 201 | 202 | if [ -z "${PSK:-}" ]; then 203 | err "密码生成失败" 204 | exit 1 205 | fi 206 | 207 | info "密码生成成功" 208 | fi 209 | } 210 | 211 | generate_psk 212 | 213 | # ----------------------- 214 | # 生成配置文件 215 | CONFIG_PATH="/etc/sing-box/config.json" 216 | 217 | create_config() { 218 | info "生成配置文件: $CONFIG_PATH" 219 | 220 | mkdir -p "$(dirname "$CONFIG_PATH")" 221 | 222 | cat > "$CONFIG_PATH" </dev/null 2>&1; then 249 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 250 | info "配置文件验证通过" 251 | else 252 | warn "配置文件验证失败,但将继续..." 253 | fi 254 | fi 255 | } 256 | 257 | create_config 258 | 259 | # ----------------------- 260 | # 设置服务 261 | setup_service() { 262 | info "配置系统服务..." 263 | 264 | if [ "$OS" = "alpine" ]; then 265 | # Alpine OpenRC 服务 266 | SERVICE_PATH="/etc/init.d/sing-box" 267 | 268 | cat > "$SERVICE_PATH" <<'OPENRC' 269 | #!/sbin/openrc-run 270 | 271 | name="sing-box" 272 | description="Sing-box Proxy Server" 273 | command="/usr/bin/sing-box" 274 | command_args="run -c /etc/sing-box/config.json" 275 | pidfile="/run/${RC_SVCNAME}.pid" 276 | command_background="yes" 277 | output_log="/var/log/sing-box.log" 278 | error_log="/var/log/sing-box.err" 279 | 280 | depend() { 281 | need net 282 | after firewall 283 | } 284 | 285 | start_pre() { 286 | checkpath --directory --mode 0755 /var/log 287 | checkpath --directory --mode 0755 /run 288 | } 289 | 290 | start_post() { 291 | sleep 1 292 | if [ -f "$pidfile" ]; then 293 | einfo "Sing-box started successfully (PID: $(cat $pidfile))" 294 | else 295 | ewarn "Sing-box may not have started correctly" 296 | fi 297 | } 298 | OPENRC 299 | 300 | chmod +x "$SERVICE_PATH" 301 | 302 | # 添加到开机自启 303 | rc-update add sing-box default >/dev/null 2>&1 || warn "添加开机自启失败" 304 | 305 | # 启动服务 306 | rc-service sing-box restart || { 307 | err "服务启动失败,查看日志:" 308 | tail -20 /var/log/sing-box.err 2>/dev/null || tail -20 /var/log/sing-box.log 2>/dev/null || true 309 | exit 1 310 | } 311 | 312 | sleep 2 313 | 314 | if rc-service sing-box status >/dev/null 2>&1; then 315 | info "✅ OpenRC 服务已启动" 316 | else 317 | err "服务状态异常" 318 | exit 1 319 | fi 320 | 321 | else 322 | # Systemd 服务 323 | SERVICE_PATH="/etc/systemd/system/sing-box.service" 324 | 325 | cat > "$SERVICE_PATH" <<'SYSTEMD' 326 | [Unit] 327 | Description=Sing-box Proxy Server 328 | Documentation=https://sing-box.sagernet.org 329 | After=network.target nss-lookup.target 330 | Wants=network.target 331 | 332 | [Service] 333 | Type=simple 334 | User=root 335 | WorkingDirectory=/etc/sing-box 336 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 337 | ExecReload=/bin/kill -HUP $MAINPID 338 | Restart=on-failure 339 | RestartSec=10s 340 | LimitNOFILE=1048576 341 | 342 | [Install] 343 | WantedBy=multi-user.target 344 | SYSTEMD 345 | 346 | systemctl daemon-reload 347 | systemctl enable sing-box >/dev/null 2>&1 348 | systemctl restart sing-box || { 349 | err "服务启动失败,查看日志:" 350 | journalctl -u sing-box -n 30 --no-pager 351 | exit 1 352 | } 353 | 354 | sleep 2 355 | 356 | if systemctl is-active sing-box >/dev/null 2>&1; then 357 | info "✅ Systemd 服务已启动" 358 | else 359 | err "服务状态异常" 360 | systemctl status sing-box --no-pager 361 | exit 1 362 | fi 363 | fi 364 | 365 | info "服务配置完成: $SERVICE_PATH" 366 | } 367 | 368 | setup_service 369 | 370 | # ----------------------- 371 | # 获取公网 IP 372 | get_public_ip() { 373 | local ip="" 374 | for url in \ 375 | "https://api.ipify.org" \ 376 | "https://ipinfo.io/ip" \ 377 | "https://ifconfig.me" \ 378 | "https://icanhazip.com" \ 379 | "https://ipecho.net/plain"; do 380 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 381 | if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 382 | echo "$ip" 383 | return 0 384 | fi 385 | done 386 | return 1 387 | } 388 | 389 | PUB_IP=$(get_public_ip || echo "YOUR_SERVER_IP") 390 | if [ "$PUB_IP" = "YOUR_SERVER_IP" ]; then 391 | warn "无法获取公网 IP,请手动替换" 392 | else 393 | info "检测到公网 IP: $PUB_IP" 394 | fi 395 | 396 | # ----------------------- 397 | # 生成 SS URI 398 | generate_uri() { 399 | local host="$PUB_IP" 400 | local tag="singbox-ss2022" 401 | local userinfo="${METHOD}:${PSK}" 402 | 403 | # SIP002 格式 (URL编码) 404 | local encoded_userinfo 405 | if command -v python3 >/dev/null 2>&1; then 406 | encoded_userinfo=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$userinfo" 2>/dev/null || echo "$userinfo") 407 | else 408 | encoded_userinfo=$(printf "%s" "$userinfo" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 409 | fi 410 | 411 | # Base64 格式 412 | local base64_userinfo=$(printf "%s" "$userinfo" | base64 -w0 2>/dev/null || printf "%s" "$userinfo" | base64 | tr -d '\n') 413 | 414 | echo "ss://${encoded_userinfo}@${host}:${PORT}#${tag}" 415 | echo "ss://${base64_userinfo}@${host}:${PORT}#${tag}" 416 | } 417 | 418 | # ----------------------- 419 | # 最终输出 420 | echo "" 421 | echo "==========================================" 422 | info "🎉 Sing-box 部署完成!" 423 | echo "==========================================" 424 | echo "" 425 | info "📋 配置信息:" 426 | echo " 端口: $PORT" 427 | echo " 方法: $METHOD" 428 | echo " 密码: $PSK" 429 | echo " 服务器: $PUB_IP" 430 | echo "" 431 | info "📁 文件位置:" 432 | echo " 配置: $CONFIG_PATH" 433 | echo " 服务: $SERVICE_PATH" 434 | echo "" 435 | info "🔗 客户端链接:" 436 | generate_uri | while IFS= read -r line; do 437 | echo " $line" 438 | done 439 | echo "" 440 | info "🔧 管理命令:" 441 | if [ "$OS" = "alpine" ]; then 442 | echo " 启动: rc-service sing-box start" 443 | echo " 停止: rc-service sing-box stop" 444 | echo " 重启: rc-service sing-box restart" 445 | echo " 状态: rc-service sing-box status" 446 | echo " 日志: tail -f /var/log/sing-box.log" 447 | else 448 | echo " 启动: systemctl start sing-box" 449 | echo " 停止: systemctl stop sing-box" 450 | echo " 重启: systemctl restart sing-box" 451 | echo " 状态: systemctl status sing-box" 452 | echo " 日志: journalctl -u sing-box -f" 453 | fi 454 | echo "" 455 | echo "==========================================" 456 | 457 | # ----------------------- 458 | # Create `sb` management script at /usr/local/bin/sb 459 | # (Do not modify other parts of the original script; sb is added as a separate tool) 460 | SB_PATH="/usr/local/bin/sb" 461 | 462 | info "正在创建 sb 管理脚本: $SB_PATH" 463 | 464 | cat > "$SB_PATH" <<'SB_SCRIPT' 465 | #!/usr/bin/env bash 466 | set -euo pipefail 467 | 468 | # ----------------------- 469 | # 颜色输出函数 470 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 471 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 472 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 473 | 474 | CONFIG_PATH="/etc/sing-box/config.json" 475 | SS_URI_PATH="/etc/sing-box/ss_uri.txt" 476 | BIN_PATH="/usr/bin/sing-box" 477 | SERVICE_NAME="sing-box" 478 | 479 | # detect OS 480 | detect_os() { 481 | if [ -f /etc/os-release ]; then 482 | . /etc/os-release 483 | ID="${ID:-}" 484 | ID_LIKE="${ID_LIKE:-}" 485 | else 486 | ID="" 487 | ID_LIKE="" 488 | fi 489 | 490 | if echo "$ID $ID_LIKE" | grep -qi "alpine"; then 491 | OS="alpine" 492 | elif echo "$ID $ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 493 | OS="debian" 494 | elif echo "$ID $ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 495 | OS="redhat" 496 | else 497 | OS="unknown" 498 | fi 499 | } 500 | 501 | detect_os 502 | 503 | # service helpers 504 | service_start() { 505 | if [ "$OS" = "alpine" ]; then 506 | rc-service "$SERVICE_NAME" start 507 | else 508 | systemctl start "$SERVICE_NAME" 509 | fi 510 | } 511 | service_stop() { 512 | if [ "$OS" = "alpine" ]; then 513 | rc-service "$SERVICE_NAME" stop 514 | else 515 | systemctl stop "$SERVICE_NAME" 516 | fi 517 | } 518 | service_restart() { 519 | if [ "$OS" = "alpine" ]; then 520 | rc-service "$SERVICE_NAME" restart 521 | else 522 | systemctl restart "$SERVICE_NAME" 523 | fi 524 | } 525 | service_status() { 526 | if [ "$OS" = "alpine" ]; then 527 | rc-service "$SERVICE_NAME" status 528 | else 529 | systemctl status "$SERVICE_NAME" --no-pager 530 | fi 531 | } 532 | 533 | # Extract fields from config.json (method, password, port) 534 | read_config_fields() { 535 | if [ ! -f "$CONFIG_PATH" ]; then 536 | err "未找到配置文件: $CONFIG_PATH" 537 | return 1 538 | fi 539 | 540 | if command -v python3 >/dev/null 2>&1; then 541 | METHOD=$(python3 - <<'PY' 542 | import json,sys 543 | c=json.load(open('/etc/sing-box/config.json')) 544 | try: 545 | m=c['inbounds'][0].get('method','') 546 | except Exception: 547 | m='' 548 | print(m) 549 | PY 550 | ) 551 | PSK=$(python3 - <<'PY' 552 | import json,sys 553 | c=json.load(open('/etc/sing-box/config.json')) 554 | try: 555 | p=c['inbounds'][0].get('password','') 556 | except Exception: 557 | p='' 558 | print(p) 559 | PY 560 | ) 561 | PORT=$(python3 - <<'PY' 562 | import json,sys 563 | c=json.load(open('/etc/sing-box/config.json')) 564 | try: 565 | port=c['inbounds'][0].get('listen_port','') 566 | except Exception: 567 | port='' 568 | print(port) 569 | PY 570 | ) 571 | else 572 | METHOD=$(grep -m1 '"method"' "$CONFIG_PATH" 2>/dev/null | sed -E 's/.*"method"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true) 573 | PSK=$(grep -m1 '"password"' "$CONFIG_PATH" 2>/dev/null | sed -E 's/.*"password"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true) 574 | PORT=$(grep -m1 '"listen_port"' "$CONFIG_PATH" 2>/dev/null | sed -E 's/.*"listen_port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/' || true) 575 | fi 576 | 577 | METHOD="${METHOD:-}" 578 | PSK="${PSK:-}" 579 | PORT="${PORT:-}" 580 | } 581 | 582 | # generate ss uri from current config and save to SS_URI_PATH 583 | generate_and_save_uri() { 584 | read_config_fields || return 1 585 | 586 | PUBLIC_IP="" 587 | for url in "https://api.ipify.org" "https://ipinfo.io/ip" "https://ifconfig.me" "https://icanhazip.com" "https://ipecho.net/plain"; do 588 | PUBLIC_IP=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 589 | if [ -n "$PUBLIC_IP" ]; then break; fi 590 | done 591 | if [ -z "$PUBLIC_IP" ]; then PUBLIC_IP="YOUR_SERVER_IP"; fi 592 | 593 | userinfo="${METHOD}:${PSK}" 594 | 595 | if command -v python3 >/dev/null 2>&1; then 596 | encoded_userinfo=$(python3 - </dev/null || printf "%s" "$userinfo" | base64 | tr -d '\n') 606 | 607 | echo "ss://${encoded_userinfo}@${PUBLIC_IP}:${PORT}#singbox-ss2022" > "$SS_URI_PATH" 608 | echo "ss://${base64_userinfo}@${PUBLIC_IP}:${PORT}#singbox-ss2022" >> "$SS_URI_PATH" 609 | 610 | info "SS URI 已写入: $SS_URI_PATH" 611 | } 612 | 613 | # View SS URI 614 | action_view_uri() { 615 | info "正在从配置生成 SS URI..." 616 | generate_and_save_uri || { err "生成 SS URI 失败"; return 1; } 617 | 618 | sed -n '1,200p' "$SS_URI_PATH" 619 | } 620 | 621 | # View config path 622 | action_view_config() { 623 | echo "$CONFIG_PATH" 624 | } 625 | 626 | # Edit config 627 | action_edit_config() { 628 | if [ ! -f "$CONFIG_PATH" ]; then 629 | err "配置文件不存在: $CONFIG_PATH" 630 | return 1 631 | fi 632 | 633 | if command -v nano >/dev/null 2>&1; then 634 | ${EDITOR:-nano} "$CONFIG_PATH" 635 | else 636 | ${EDITOR:-vi} "$CONFIG_PATH" 637 | fi 638 | 639 | if command -v sing-box >/dev/null 2>&1; then 640 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 641 | info "配置校验通过,重启服务" 642 | service_restart || warn "重启失败" 643 | generate_and_save_uri || true 644 | else 645 | warn "配置校验失败,请手动检查。服务未被重启。" 646 | fi 647 | else 648 | warn "未检测到 sing-box 可执行文件,无法校验或重启" 649 | fi 650 | } 651 | 652 | # Reset port & password 653 | action_reset_port_pwd() { 654 | [ -f "$CONFIG_PATH" ] || { err "配置文件不存在: $CONFIG_PATH"; return 1; } 655 | 656 | read -p "输入新端口(回车随机 10000-60000): " new_port 657 | [ -z "$new_port" ] && new_port=$((RANDOM % 50001 + 10000)) 658 | 659 | read -p "输入新密码(回车随机生成 Base64 密钥): " new_pwd 660 | [ -z "$new_pwd" ] && new_pwd=$(head -c 16 /dev/urandom | base64 | tr -d '\n\r') 661 | 662 | info "正在停止 sing-box 服务..." 663 | service_stop || warn "停止服务失败" 664 | 665 | cat > "$CONFIG_PATH" </dev/null 2>&1; then 710 | NEW_VER=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 711 | info "当前 sing-box 版本: $NEW_VER" 712 | service_restart || warn "重启失败" 713 | else 714 | warn "更新后未检测到 sing-box 可执行文件" 715 | fi 716 | } 717 | 718 | # Uninstall sing-box 719 | action_uninstall() { 720 | info "正在卸载 sing-box..." 721 | service_stop || true 722 | if [ "$OS" = "alpine" ]; then 723 | rc-update del "$SERVICE_NAME" default >/dev/null 2>&1 || true 724 | [ -f "/etc/init.d/$SERVICE_NAME" ] && rm -f "/etc/init.d/$SERVICE_NAME" 725 | apk del sing-box >/dev/null 2>&1 || true 726 | else 727 | systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true 728 | systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true 729 | [ -f "/etc/systemd/system/$SERVICE_NAME.service" ] && rm -f "/etc/systemd/system/$SERVICE_NAME.service" 730 | systemctl daemon-reload >/dev/null 2>&1 || true 731 | fi 732 | rm -rf /etc/sing-box /var/log/sing-box* /usr/local/bin/sb "$BIN_PATH" >/dev/null 2>&1 || true 733 | info "卸载完成" 734 | } 735 | 736 | # ----------------------- 737 | # 新增功能:生成线路机一键安装脚本 738 | action_generate_relay_script() { 739 | info "准备生成线路机一键安装脚本..." 740 | read_config_fields || return 1 741 | 742 | PUBLIC_IP="" 743 | for url in \ 744 | "https://api.ipify.org" \ 745 | "https://ipinfo.io/ip" \ 746 | "https://ifconfig.me" \ 747 | "https://icanhazip.com" \ 748 | "https://ipecho.net/plain"; do 749 | 750 | PUBLIC_IP=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]') 751 | if [ -n "$PUBLIC_IP" ]; then break; fi 752 | done 753 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 754 | 755 | info "落地机出口节点:${PUBLIC_IP}:${PORT} 方法:${METHOD}" 756 | 757 | RELAY_SCRIPT_PATH="/tmp/relay-install.sh" 758 | 759 | cat > "$RELAY_SCRIPT_PATH" << 'RELAY_TEMPLATE' 760 | #!/usr/bin/env bash 761 | set -euo pipefail 762 | INBOUND_IP="__INBOUND_IP__" 763 | INBOUND_PORT="__INBOUND_PORT__" 764 | INBOUND_METHOD="__INBOUND_METHOD__" 765 | INBOUND_PASSWORD="__INBOUND_PASSWORD__" 766 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 767 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 768 | if [ "$(id -u)" != "0" ]; then 769 | err "必须以 root 运行" 770 | exit 1 771 | fi 772 | detect_os() { 773 | . /etc/os-release 2>/dev/null || true 774 | case "$ID" in 775 | alpine) OS=alpine ;; 776 | debian|ubuntu) OS=debian ;; 777 | centos|rhel|fedora) OS=redhat ;; 778 | *) OS=unknown ;; 779 | esac 780 | } 781 | detect_os 782 | info "检测到系统: $OS" 783 | install_deps() { 784 | info "安装依赖..." 785 | case "$OS" in 786 | alpine) 787 | apk update 788 | apk add --no-cache curl jq bash openssl ca-certificates 789 | ;; 790 | debian) 791 | apt-get update -y 792 | apt-get install -y curl jq bash openssl ca-certificates 793 | ;; 794 | redhat) 795 | yum install -y curl jq bash openssl ca-certificates 796 | ;; 797 | esac 798 | } 799 | install_deps 800 | install_singbox() { 801 | info "安装 sing-box..." 802 | case "$OS" in 803 | alpine) 804 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box 805 | ;; 806 | *) 807 | bash <(curl -fsSL https://sing-box.app/install.sh) 808 | ;; 809 | esac 810 | } 811 | install_singbox 812 | UUID=$(cat /proc/sys/kernel/random/uuid) 813 | info "生成 Reality 密钥对" 814 | # 生成 Reality 密钥对 815 | REALITY_KEYS=$(sing-box generate reality-keypair) 816 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}') 817 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}') 818 | 819 | info "Reality PK: $REALITY_PK" 820 | info "Reality PUB: $REALITY_PUB" 821 | # 生成随机 Short ID (8字节 hex) 822 | REALITY_SID=$(sing-box generate rand 8 --hex) 823 | info "Reality SID: $REALITY_SID" 824 | 825 | read -p "输入线路机监听端口(留空则随机 20000-65000): " USER_PORT 826 | if [ -z "$USER_PORT" ]; then 827 | LISTEN_PORT=$(shuf -i 20000-65000 -n 1 2>/dev/null || echo $((RANDOM % 45001 + 20000))) 828 | info "使用随机端口: $LISTEN_PORT" 829 | else 830 | if ! [[ "$USER_PORT" =~ ^[0-9]+$ ]] || [ "$USER_PORT" -lt 1 ] || [ "$USER_PORT" -gt 65535 ]; then 831 | err "端口必须为 1-65535 的数字" 832 | exit 1 833 | fi 834 | LISTEN_PORT="$USER_PORT" 835 | fi 836 | info "线路机监听端口: $LISTEN_PORT" 837 | 838 | mkdir -p /etc/sing-box 839 | cat > /etc/sing-box/config.json < /etc/init.d/sing-box << 'SVC' 902 | #!/sbin/openrc-run 903 | name="sing-box" 904 | description="SingBox service" 905 | 906 | command="/usr/bin/sing-box" 907 | command_args="run -c /etc/sing-box/config.json" 908 | command_background="yes" 909 | pidfile="/run/sing-box.pid" 910 | 911 | depend() { 912 | need net 913 | } 914 | SVC 915 | chmod +x /etc/init.d/sing-box 916 | rc-update add sing-box default 917 | rc-service sing-box restart 918 | else 919 | cat > /etc/systemd/system/sing-box.service << 'SYSTEMD' 920 | [Unit] 921 | Description=Sing-box Relay 922 | After=network.target 923 | [Service] 924 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 925 | Restart=on-failure 926 | [Install] 927 | WantedBy=multi-user.target 928 | SYSTEMD 929 | systemctl daemon-reload 930 | systemctl enable sing-box 931 | systemctl restart sing-box 932 | fi 933 | PUB_IP=$(curl -s https://api.ipify.org || echo "YOUR_RELAY_IP") 934 | echo "" 935 | echo "✅ 安装完成" 936 | echo "VLESS Reality 中转节点:" 937 | echo "vless://$UUID@$PUB_IP:$LISTEN_PORT?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=$REALITY_PUB&sid=$REALITY_SID#relay" 938 | echo "" 939 | RELAY_TEMPLATE 940 | 941 | sed -i "s|__INBOUND_IP__|$PUBLIC_IP|g" "$RELAY_SCRIPT_PATH" 942 | sed -i "s|__INBOUND_PORT__|$PORT|g" "$RELAY_SCRIPT_PATH" 943 | sed -i "s|__INBOUND_METHOD__|$METHOD|g" "$RELAY_SCRIPT_PATH" 944 | sed -i "s|__INBOUND_PASSWORD__|$PSK|g" "$RELAY_SCRIPT_PATH" 945 | 946 | chmod +x "$RELAY_SCRIPT_PATH" 947 | 948 | echo "" 949 | info "✅ 线路机脚本已生成:$RELAY_SCRIPT_PATH" 950 | echo "" 951 | info "请手动复制以下内容到线路机,保存为 /tmp/relay-install.sh,并执行:chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 952 | echo "------------------------------------------" 953 | cat "$RELAY_SCRIPT_PATH" 954 | echo "------------------------------------------" 955 | echo "" 956 | info "在线路机执行命令示例:" 957 | echo " # nano /tmp/relay-install.sh 保存后执行" 958 | echo " chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 959 | echo "" 960 | info "复制完成后,即可在线路机完成 sing-box 中转节点部署。" 961 | } 962 | 963 | # ----------------------- 964 | # Menu 965 | while true; do 966 | cat <<'MENU' 967 | 968 | ========================== 969 | Sing-box 管理面板 (快捷指令sb) 970 | ========================== 971 | 1) 查看 SS URI 972 | 2) 查看配置文件路径 973 | 3) 编辑配置文件 974 | 4) 重置密码/端口 975 | 5) 启动服务 976 | 6) 停止服务 977 | 7) 重启服务 978 | 8) 查看状态 979 | 9) 更新 sing-box 980 | 10) 生成线路机出口一键安装脚本 981 | 11) 卸载 sing-box(无确认) 982 | 0) 退出 983 | ========================== 984 | MENU 985 | 986 | read -p "请输入选项: " opt 987 | case "${opt:-}" in 988 | 1) action_view_uri ;; 989 | 2) action_view_config ;; 990 | 3) action_edit_config ;; 991 | 4) action_reset_port_pwd ;; 992 | 5) service_start && info "已发送启动命令" ;; 993 | 6) service_stop && info "已发送停止命令" ;; 994 | 7) service_restart && info "已发送重启命令" ;; 995 | 8) service_status ;; 996 | 9) action_update ;; 997 | 10) action_generate_relay_script ;; 998 | 11) action_uninstall; exit 0 ;; 999 | 0) exit 0 ;; 1000 | *) warn "无效选项" ;; 1001 | esac 1002 | 1003 | echo "" 1004 | done 1005 | SB_SCRIPT 1006 | 1007 | # set executable 1008 | chmod +x "$SB_PATH" || warn "无法设置 $SB_PATH 为可执行" 1009 | 1010 | info "sb 已创建:请输入 sb 运行管理面板" 1011 | 1012 | # end of script 1013 | -------------------------------------------------------------------------------- /install-singbox-yyds0.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # ----------------------- 5 | # 颜色输出函数 6 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 7 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 8 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 9 | 10 | # ----------------------- 11 | # 检测系统类型 12 | detect_os() { 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | OS_ID="${ID:-}" 16 | OS_ID_LIKE="${ID_LIKE:-}" 17 | else 18 | OS_ID="" 19 | OS_ID_LIKE="" 20 | fi 21 | 22 | if echo "$OS_ID $OS_ID_LIKE" | grep -qi "alpine"; then 23 | OS="alpine" 24 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 25 | OS="debian" 26 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 27 | OS="redhat" 28 | else 29 | OS="unknown" 30 | fi 31 | } 32 | 33 | detect_os 34 | info "检测到系统: $OS (${OS_ID:-unknown})" 35 | 36 | # ----------------------- 37 | # 检查 root 权限 38 | check_root() { 39 | if [ "$(id -u)" != "0" ]; then 40 | err "此脚本需要 root 权限" 41 | err "请使用: sudo bash -c \"\$(curl -fsSL ...)\" 或切换到 root 用户" 42 | exit 1 43 | fi 44 | } 45 | 46 | check_root 47 | 48 | # ----------------------- 49 | # 安装依赖 50 | install_deps() { 51 | info "安装系统依赖..." 52 | 53 | case "$OS" in 54 | alpine) 55 | apk update || { err "apk update 失败"; exit 1; } 56 | apk add --no-cache bash curl ca-certificates openssl openrc jq || { 57 | err "依赖安装失败" 58 | exit 1 59 | } 60 | 61 | if ! rc-service --list 2>/dev/null | grep -q "^openrc"; then 62 | rc-update add openrc boot >/dev/null 2>&1 || true 63 | rc-service openrc start >/dev/null 2>&1 || true 64 | fi 65 | ;; 66 | debian) 67 | export DEBIAN_FRONTEND=noninteractive 68 | apt-get update -y || { err "apt update 失败"; exit 1; } 69 | apt-get install -y curl ca-certificates openssl jq || { 70 | err "依赖安装失败" 71 | exit 1 72 | } 73 | ;; 74 | redhat) 75 | yum install -y curl ca-certificates openssl jq || { 76 | err "依赖安装失败" 77 | exit 1 78 | } 79 | ;; 80 | *) 81 | warn "未识别的系统类型,尝试继续..." 82 | ;; 83 | esac 84 | 85 | info "依赖安装完成" 86 | } 87 | 88 | install_deps 89 | 90 | # ----------------------- 91 | # 配置端口和密码 92 | get_config() { 93 | info "=== 配置 Shadowsocks (SS) ===" 94 | if [ -n "${SINGBOX_PORT_SS:-}" ]; then 95 | PORT_SS="$SINGBOX_PORT_SS" 96 | info "使用环境变量端口 (SS): $PORT_SS" 97 | else 98 | read -p "请输入 SS 端口(留空则随机 10000-60000): " USER_PORT_SS 99 | if [ -z "$USER_PORT_SS" ]; then 100 | PORT_SS=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 101 | info "使用随机端口 (SS): $PORT_SS" 102 | else 103 | PORT_SS="$USER_PORT_SS" 104 | fi 105 | fi 106 | 107 | if [ -n "${SINGBOX_PASSWORD_SS:-}" ]; then 108 | PSK_SS="$SINGBOX_PASSWORD_SS" 109 | info "使用环境变量密码 (SS)" 110 | else 111 | read -p "请输入 SS 密码(留空则自动生成 Base64 密钥): " USER_PSK_SS 112 | if [ -z "$USER_PSK_SS" ]; then 113 | PSK_SS=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 114 | info "已自动生成 SS 密码" 115 | else 116 | PSK_SS="$USER_PSK_SS" 117 | fi 118 | fi 119 | 120 | info "=== 配置 Hysteria2 (HY2) ===" 121 | if [ -n "${SINGBOX_PORT_HY2:-}" ]; then 122 | PORT_HY2="$SINGBOX_PORT_HY2" 123 | info "使用环境变量端口 (HY2): $PORT_HY2" 124 | else 125 | read -p "请输入 HY2 端口(留空则随机 10000-60000): " USER_PORT_HY2 126 | if [ -z "$USER_PORT_HY2" ]; then 127 | PORT_HY2=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 128 | info "使用随机端口 (HY2): $PORT_HY2" 129 | else 130 | PORT_HY2="$USER_PORT_HY2" 131 | fi 132 | fi 133 | 134 | if [ -n "${SINGBOX_PASSWORD_HY2:-}" ]; then 135 | PSK_HY2="$SINGBOX_PASSWORD_HY2" 136 | info "使用环境变量密码 (HY2)" 137 | else 138 | read -p "请输入 HY2 密码(留空则自动生成 Base64 密钥): " USER_PSK_HY2 139 | if [ -z "$USER_PSK_HY2" ]; then 140 | PSK_HY2=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 141 | info "已自动生成 HY2 密码" 142 | else 143 | PSK_HY2="$USER_PSK_HY2" 144 | fi 145 | fi 146 | 147 | info "=== 配置 VLESS Reality ===" 148 | if [ -n "${SINGBOX_PORT_REALITY:-}" ]; then 149 | PORT_REALITY="$SINGBOX_PORT_REALITY" 150 | info "使用环境变量端口 (Reality): $PORT_REALITY" 151 | else 152 | read -p "请输入 VLESS Reality 端口(留空则随机 10000-60000): " USER_PORT_REALITY 153 | if [ -z "$USER_PORT_REALITY" ]; then 154 | PORT_REALITY=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 155 | info "使用随机端口 (Reality): $PORT_REALITY" 156 | else 157 | PORT_REALITY="$USER_PORT_REALITY" 158 | fi 159 | fi 160 | 161 | UUID=$(cat /proc/sys/kernel/random/uuid) 162 | info "已生成 UUID: $UUID" 163 | } 164 | 165 | get_config 166 | 167 | # ----------------------- 168 | # 安装 sing-box 169 | install_singbox() { 170 | info "开始安装 sing-box..." 171 | 172 | if command -v sing-box >/dev/null 2>&1; then 173 | CURRENT_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 174 | warn "检测到已安装 sing-box: $CURRENT_VERSION" 175 | read -p "是否重新安装?(y/N): " REINSTALL 176 | if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then 177 | info "跳过 sing-box 安装" 178 | return 0 179 | fi 180 | fi 181 | 182 | case "$OS" in 183 | alpine) 184 | info "使用 Edge 仓库安装 sing-box" 185 | apk update || { err "apk update 失败"; exit 1; } 186 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 187 | err "sing-box 安装失败" 188 | exit 1 189 | } 190 | ;; 191 | debian|redhat) 192 | bash <(curl -fsSL https://sing-box.app/install.sh) || { 193 | err "sing-box 安装失败" 194 | exit 1 195 | } 196 | ;; 197 | *) 198 | err "未支持的系统,无法安装 sing-box" 199 | exit 1 200 | ;; 201 | esac 202 | 203 | if ! command -v sing-box >/dev/null 2>&1; then 204 | err "sing-box 安装后未找到可执行文件" 205 | exit 1 206 | fi 207 | 208 | INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 209 | info "sing-box 安装成功: $INSTALLED_VERSION" 210 | } 211 | 212 | install_singbox 213 | 214 | # ----------------------- 215 | # 生成 Reality 密钥对和自签名证书 216 | generate_reality_keys() { 217 | info "生成 Reality 密钥对..." 218 | REALITY_KEYS=$(sing-box generate reality-keypair) 219 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}' | tr -d '\r') 220 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}' | tr -d '\r') 221 | REALITY_SID=$(sing-box generate rand 8 --hex) 222 | 223 | # 立即保存公钥和 SID 224 | mkdir -p /etc/sing-box 225 | echo -n "$REALITY_PUB" > /etc/sing-box/.reality_pub 226 | echo -n "$REALITY_SID" > /etc/sing-box/.reality_sid 227 | 228 | info "Reality PK: $REALITY_PK" 229 | info "Reality PUB: $REALITY_PUB" 230 | info "Reality SID: $REALITY_SID" 231 | } 232 | 233 | generate_reality_keys 234 | 235 | # ----------------------- 236 | # 生成 HY2 自签名证书 237 | generate_hy2_cert() { 238 | info "生成 HY2 自签名证书..." 239 | mkdir -p /etc/sing-box/certs 240 | 241 | if [ ! -f /etc/sing-box/certs/fullchain.pem ] || [ ! -f /etc/sing-box/certs/privkey.pem ]; then 242 | openssl req -x509 -newkey rsa:2048 -nodes \ 243 | -keyout /etc/sing-box/certs/privkey.pem \ 244 | -out /etc/sing-box/certs/fullchain.pem \ 245 | -days 3650 \ 246 | -subj "/CN=www.bing.com" || { 247 | err "证书生成失败" 248 | exit 1 249 | } 250 | info "HY2 证书已生成" 251 | else 252 | info "HY2 证书已存在" 253 | fi 254 | } 255 | 256 | generate_hy2_cert 257 | 258 | # ----------------------- 259 | # 生成配置文件 260 | CONFIG_PATH="/etc/sing-box/config.json" 261 | 262 | create_config() { 263 | info "生成配置文件: $CONFIG_PATH" 264 | 265 | mkdir -p "$(dirname "$CONFIG_PATH")" 266 | 267 | cat > "$CONFIG_PATH" </dev/null 2>&1 \ 335 | && info "配置文件验证通过" \ 336 | || warn "配置文件验证失败,但继续执行" 337 | 338 | mkdir -p /etc/sing-box 339 | cat > /etc/sing-box/.config_cache < "$SERVICE_PATH" <<'OPENRC' 366 | #!/sbin/openrc-run 367 | 368 | name="sing-box" 369 | description="Sing-box Proxy Server" 370 | command="/usr/bin/sing-box" 371 | command_args="run -c /etc/sing-box/config.json" 372 | pidfile="/run/${RC_SVCNAME}.pid" 373 | command_background="yes" 374 | output_log="/var/log/sing-box.log" 375 | error_log="/var/log/sing-box.err" 376 | 377 | depend() { 378 | need net 379 | after firewall 380 | } 381 | 382 | start_pre() { 383 | checkpath --directory --mode 0755 /var/log 384 | checkpath --directory --mode 0755 /run 385 | } 386 | OPENRC 387 | 388 | chmod +x "$SERVICE_PATH" 389 | rc-update add sing-box default >/dev/null 2>&1 || warn "添加开机自启失败" 390 | rc-service sing-box restart || { 391 | err "服务启动失败" 392 | tail -20 /var/log/sing-box.err 2>/dev/null || tail -20 /var/log/sing-box.log 2>/dev/null || true 393 | exit 1 394 | } 395 | 396 | sleep 2 397 | if rc-service sing-box status >/dev/null 2>&1; then 398 | info "✅ OpenRC 服务已启动" 399 | else 400 | err "服务状态异常" 401 | exit 1 402 | fi 403 | 404 | else 405 | SERVICE_PATH="/etc/systemd/system/sing-box.service" 406 | 407 | cat > "$SERVICE_PATH" <<'SYSTEMD' 408 | [Unit] 409 | Description=Sing-box Proxy Server 410 | Documentation=https://sing-box.sagernet.org 411 | After=network.target nss-lookup.target 412 | Wants=network.target 413 | 414 | [Service] 415 | Type=simple 416 | User=root 417 | WorkingDirectory=/etc/sing-box 418 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 419 | ExecReload=/bin/kill -HUP $MAINPID 420 | Restart=on-failure 421 | RestartSec=10s 422 | LimitNOFILE=1048576 423 | 424 | [Install] 425 | WantedBy=multi-user.target 426 | SYSTEMD 427 | 428 | systemctl daemon-reload 429 | systemctl enable sing-box >/dev/null 2>&1 430 | systemctl restart sing-box || { 431 | err "服务启动失败" 432 | journalctl -u sing-box -n 30 --no-pager 433 | exit 1 434 | } 435 | 436 | sleep 2 437 | if systemctl is-active sing-box >/dev/null 2>&1; then 438 | info "✅ Systemd 服务已启动" 439 | else 440 | err "服务状态异常" 441 | exit 1 442 | fi 443 | fi 444 | 445 | info "服务配置完成: $SERVICE_PATH" 446 | } 447 | 448 | setup_service 449 | 450 | # ----------------------- 451 | # 获取公网 IP 452 | get_public_ip() { 453 | local ip="" 454 | for url in \ 455 | "https://api.ipify.org" \ 456 | "https://ipinfo.io/ip" \ 457 | "https://ifconfig.me" \ 458 | "https://icanhazip.com" \ 459 | "https://ipecho.net/plain"; do 460 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 461 | if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 462 | echo "$ip" 463 | return 0 464 | fi 465 | done 466 | return 1 467 | } 468 | 469 | PUB_IP=$(get_public_ip || echo "YOUR_SERVER_IP") 470 | if [ "$PUB_IP" = "YOUR_SERVER_IP" ]; then 471 | warn "无法获取公网 IP,请手动替换" 472 | else 473 | info "检测到公网 IP: $PUB_IP" 474 | fi 475 | 476 | # ----------------------- 477 | # 生成链接 478 | generate_uris() { 479 | local host="$PUB_IP" 480 | 481 | # SS URI 482 | local ss_userinfo="2022-blake3-aes-128-gcm:${PSK_SS}" 483 | if command -v python3 >/dev/null 2>&1; then 484 | ss_encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$ss_userinfo" 2>/dev/null || echo "$ss_userinfo") 485 | else 486 | ss_encoded=$(printf "%s" "$ss_userinfo" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 487 | fi 488 | ss_b64=$(printf "%s" "$ss_userinfo" | base64 -w0 2>/dev/null || printf "%s" "$ss_userinfo" | base64 | tr -d '\n') 489 | 490 | # HY2 URI 491 | if command -v python3 >/dev/null 2>&1; then 492 | hy2_encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$PSK_HY2") 493 | else 494 | hy2_encoded=$(printf "%s" "$PSK_HY2" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 495 | fi 496 | 497 | echo "=== Shadowsocks (SS) ===" 498 | echo "ss://${ss_encoded}@${host}:${PORT_SS}#singbox-ss" 499 | echo "ss://${ss_b64}@${host}:${PORT_SS}#singbox-ss" 500 | echo "" 501 | 502 | # HY2 URI 503 | echo "=== Hysteria2 (HY2) ===" 504 | echo "hy2://${hy2_encoded}@${host}:${PORT_HY2}/?sni=www.bing.com&insecure=1#singbox-hy2" 505 | echo "" 506 | 507 | # VLESS Reality URI 508 | echo "=== VLESS Reality ===" 509 | echo "vless://${UUID}@${host}:${PORT_REALITY}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}#singbox-reality" 510 | } 511 | 512 | # ----------------------- 513 | # 最终输出 514 | echo "" 515 | echo "==========================================" 516 | info "🎉 Sing-box 多协议部署完成!" 517 | echo "==========================================" 518 | echo "" 519 | info "📋 配置信息:" 520 | echo " SS 端口: $PORT_SS | 密码: $PSK_SS" 521 | echo " HY2 端口: $PORT_HY2 | 密码: $PSK_HY2" 522 | echo " Reality 端口: $PORT_REALITY | UUID: $UUID" 523 | echo " 服务器: $PUB_IP" 524 | echo "" 525 | info "📂 文件位置:" 526 | echo " 配置: $CONFIG_PATH" 527 | echo " 证书: /etc/sing-box/certs/" 528 | echo " 服务: $SERVICE_PATH" 529 | echo "" 530 | info "🔗 客户端链接:" 531 | generate_uris | while IFS= read -r line; do 532 | echo " $line" 533 | done 534 | echo "" 535 | info "📧 管理命令:" 536 | if [ "$OS" = "alpine" ]; then 537 | echo " 启动: rc-service sing-box start" 538 | echo " 停止: rc-service sing-box stop" 539 | echo " 重启: rc-service sing-box restart" 540 | echo " 状态: rc-service sing-box status" 541 | echo " 日志: tail -f /var/log/sing-box.log" 542 | else 543 | echo " 启动: systemctl start sing-box" 544 | echo " 停止: systemctl stop sing-box" 545 | echo " 重启: systemctl restart sing-box" 546 | echo " 状态: systemctl status sing-box" 547 | echo " 日志: journalctl -u sing-box -f" 548 | fi 549 | echo "" 550 | echo "==========================================" 551 | # ----------------------- 552 | # Create `sb` management script at /usr/local/bin/sb 553 | 554 | SB_PATH="/usr/local/bin/sb" 555 | 556 | info "正在创建 sb 管理脚本: $SB_PATH" 557 | 558 | cat > "$SB_PATH" <<'SB_SCRIPT' 559 | #!/usr/bin/env bash 560 | set -euo pipefail 561 | 562 | # ----------------------- 563 | # sb 管理面板(无 python3,使用 jq) 564 | # 兼容: alpine / debian / redhat 565 | # 依赖: jq, curl, openssl 或 /dev/urandom 566 | # ----------------------- 567 | 568 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 569 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 570 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 571 | 572 | CONFIG_PATH="${CONFIG_PATH:-/etc/sing-box/config.json}" 573 | URI_PATH="${URI_PATH:-/etc/sing-box/uris.txt}" 574 | REALITY_PUB_FILE="${REALITY_PUB_FILE:-/etc/sing-box/.reality_pub}" 575 | SERVICE_NAME="${SERVICE_NAME:-sing-box}" 576 | BIN_PATH="${BIN_PATH:-/usr/bin/sing-box}" 577 | 578 | # detect OS 579 | detect_os() { 580 | if [ -f /etc/os-release ]; then 581 | . /etc/os-release 582 | ID="${ID:-}" 583 | ID_LIKE="${ID_LIKE:-}" 584 | else 585 | ID="" 586 | ID_LIKE="" 587 | fi 588 | 589 | if echo "$ID $ID_LIKE" | grep -qi "alpine"; then 590 | OS="alpine" 591 | elif echo "$ID $ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 592 | OS="debian" 593 | elif echo "$ID $ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 594 | OS="redhat" 595 | else 596 | OS="unknown" 597 | fi 598 | } 599 | 600 | detect_os 601 | 602 | # service helpers 603 | service_start() { 604 | if [ "$OS" = "alpine" ]; then 605 | rc-service "$SERVICE_NAME" start || return $? 606 | else 607 | systemctl start "$SERVICE_NAME" || return $? 608 | fi 609 | } 610 | service_stop() { 611 | if [ "$OS" = "alpine" ]; then 612 | rc-service "$SERVICE_NAME" stop || return $? 613 | else 614 | systemctl stop "$SERVICE_NAME" || return $? 615 | fi 616 | } 617 | service_restart() { 618 | if [ "$OS" = "alpine" ]; then 619 | rc-service "$SERVICE_NAME" restart || return $? 620 | else 621 | systemctl restart "$SERVICE_NAME" || return $? 622 | fi 623 | } 624 | service_status() { 625 | if [ "$OS" = "alpine" ]; then 626 | rc-service "$SERVICE_NAME" status || return $? 627 | else 628 | systemctl status "$SERVICE_NAME" --no-pager || return $? 629 | fi 630 | } 631 | 632 | # Safe random 633 | rand_b64() { 634 | if command -v openssl >/dev/null 2>&1; then 635 | openssl rand -base64 16 | tr -d '\n\r' 636 | else 637 | head -c 16 /dev/urandom | base64 | tr -d '\n\r' 638 | fi 639 | } 640 | 641 | # URL-encode minimal (for SS userinfo like "method:password") 642 | # encode only a small set of characters common in userinfo 643 | url_encode_min() { 644 | local s="$1" 645 | printf "%s" "$s" | sed -e 's/%/%25/g' \ 646 | -e 's/:/%3A/g' \ 647 | -e 's/+/%2B/g' \ 648 | -e 's/\//%2F/g' \ 649 | -e 's/=/\%3D/g' 650 | } 651 | 652 | 653 | # read JSON fields from config using jq 654 | read_config_fields() { 655 | if [ ! -f "$CONFIG_PATH" ]; then 656 | err "未找到配置文件: $CONFIG_PATH" 657 | return 1 658 | fi 659 | 660 | # Shadowsocks 661 | SS_PORT=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 662 | SS_PSK=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .password // empty' "$CONFIG_PATH" | head -n1 || true) 663 | SS_METHOD=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .method // empty' "$CONFIG_PATH" | head -n1 || true) 664 | 665 | # Hysteria2 666 | HY2_PORT=$(jq -r '.inbounds[] | select(.type=="hysteria2") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 667 | HY2_PSK=$(jq -r '.inbounds[] | select(.type=="hysteria2") | .users[0].password // empty' "$CONFIG_PATH" | head -n1 || true) 668 | 669 | # VLESS / Reality 670 | REALITY_PORT=$(jq -r '.inbounds[] | select(.type=="vless") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 671 | REALITY_UUID=$(jq -r '.inbounds[] | select(.type=="vless") | .users[0].uuid // empty' "$CONFIG_PATH" | head -n1 || true) 672 | REALITY_PK=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.private_key // empty' "$CONFIG_PATH" | head -n1 || true) 673 | REALITY_SID=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.short_id[0] // empty' "$CONFIG_PATH" | head -n1 || true) 674 | 675 | # fallback defaults 676 | SS_PORT="${SS_PORT:-}" 677 | SS_PSK="${SS_PSK:-}" 678 | SS_METHOD="${SS_METHOD:-}" 679 | HY2_PORT="${HY2_PORT:-}" 680 | HY2_PSK="${HY2_PSK:-}" 681 | REALITY_PORT="${REALITY_PORT:-}" 682 | REALITY_UUID="${REALITY_UUID:-}" 683 | REALITY_PK="${REALITY_PK:-}" 684 | REALITY_SID="${REALITY_SID:-}" 685 | } 686 | 687 | # get public IP (tries multiple endpoints) 688 | get_public_ip() { 689 | local ip="" 690 | for url in "https://api.ipify.org" "https://ipinfo.io/ip" "https://ifconfig.me" "https://icanhazip.com" "https://ipecho.net/plain"; do 691 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 692 | if [ -n "$ip" ]; then 693 | echo "$ip" 694 | return 0 695 | fi 696 | done 697 | return 1 698 | } 699 | 700 | # generate and save URIs 701 | generate_and_save_uris() { 702 | read_config_fields || return 1 703 | 704 | PUBLIC_IP=$(get_public_ip || true) 705 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 706 | 707 | # SS: two formats: percent-encoded userinfo and base64 userinfo 708 | ss_userinfo="${SS_METHOD}:${SS_PSK}" 709 | # percent encode minimal 710 | ss_encoded=$(url_encode_min "$ss_userinfo") 711 | ss_b64=$(printf "%s" "$ss_userinfo" | base64 -w0 2>/dev/null || printf "%s" "$ss_userinfo" | base64 | tr -d '\n') 712 | hy2_encoded=$(url_encode_min "$HY2_PSK") 713 | hy2_uri="hy2://${hy2_encoded}@${PUBLIC_IP}:${HY2_PORT}/?sni=www.bing.com&insecure=1#singbox-hy2" 714 | 715 | 716 | # reality pubkey read file or from config (fallback) 717 | if [ -f "$REALITY_PUB_FILE" ]; then 718 | REALITY_PUB=$(cat "$REALITY_PUB_FILE") 719 | else 720 | # try to extract pub from config if stored there 721 | REALITY_PUB=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.public_key // empty' "$CONFIG_PATH" | head -n1 || true) 722 | REALITY_PUB="${REALITY_PUB:-UNKNOWN}" 723 | fi 724 | 725 | reality_uri="vless://${REALITY_UUID}@${PUBLIC_IP}:${REALITY_PORT}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}#singbox-reality" 726 | 727 | { 728 | echo "=== Shadowsocks (SS) ===" 729 | echo "ss://${ss_encoded}@${PUBLIC_IP}:${SS_PORT}#singbox-ss" 730 | echo "ss://${ss_b64}@${PUBLIC_IP}:${SS_PORT}#singbox-ss" 731 | echo "" 732 | echo "=== Hysteria2 (HY2) ===" 733 | echo "$hy2_uri" 734 | echo "" 735 | echo "=== VLESS Reality ===" 736 | echo "$reality_uri" 737 | } > "$URI_PATH" 738 | 739 | info "URI 已写入: $URI_PATH" 740 | } 741 | 742 | # view URIs (regenerate first) 743 | action_view_uri() { 744 | info "正在生成并显示 URI..." 745 | generate_and_save_uris || { err "生成 URI 失败"; return 1; } 746 | echo "" 747 | sed -n '1,200p' "$URI_PATH" || true 748 | } 749 | 750 | # view config path 751 | action_view_config() { 752 | echo "$CONFIG_PATH" 753 | } 754 | 755 | # edit config: use EDITOR or fallback 756 | action_edit_config() { 757 | if [ ! -f "$CONFIG_PATH" ]; then 758 | err "配置文件不存在: $CONFIG_PATH" 759 | return 1 760 | fi 761 | 762 | if command -v nano >/dev/null 2>&1; then 763 | ${EDITOR:-nano} "$CONFIG_PATH" 764 | else 765 | ${EDITOR:-vi} "$CONFIG_PATH" 766 | fi 767 | 768 | # check with sing-box if available 769 | if command -v sing-box >/dev/null 2>&1; then 770 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 771 | info "配置校验通过,尝试重启服务" 772 | service_restart || warn "重启失败" 773 | generate_and_save_uris || true 774 | else 775 | warn "配置校验失败,服务未重启" 776 | fi 777 | else 778 | warn "未检测到 sing-box,可跳过校验" 779 | fi 780 | } 781 | 782 | # Generic JSON updater helper using jq 783 | # args: jq_filter tempfile 784 | json_update() { 785 | local filter="$1" 786 | local tmp="${CONFIG_PATH}.tmp" 787 | jq "$filter" "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH" 788 | } 789 | 790 | # Reset SS based on current config 791 | action_reset_ss() { 792 | read -p "输入新的 SS 端口(回车保持 $SS_PORT): " new_ss_port 793 | [ -z "$new_ss_port" ] && new_ss_port="$SS_PORT" 794 | 795 | read -p "输入新的 SS 密码(回车随机生成): " new_ss_psk 796 | [ -z "$new_ss_psk" ] && new_ss_psk=$(rand_b64) 797 | 798 | info "正在停止服务..." 799 | service_stop || warn "停止服务失败" 800 | 801 | # 使用当前配置文件为模板,先备份 802 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 803 | 804 | jq --argjson port "$new_ss_port" --arg psk "$new_ss_psk" ' 805 | .inbounds |= map( 806 | if .type=="shadowsocks" then 807 | .listen_port = $port | 808 | .password = $psk 809 | else . 810 | end 811 | ) 812 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 813 | 814 | info "已更新 SS 端口($new_ss_port)与密码(隐藏),正在启动服务..." 815 | service_start || warn "启动服务失败" 816 | sleep 1 817 | generate_and_save_uris || warn "生成 URI 失败" 818 | } 819 | 820 | # Reset HY2 based on current config 821 | action_reset_hy2() { 822 | read -p "输入新的 HY2 端口(回车保持 $HY2_PORT): " new_hy2_port 823 | [ -z "$new_hy2_port" ] && new_hy2_port="$HY2_PORT" 824 | 825 | read -p "输入新的 HY2 密码(回车随机生成): " new_hy2_psk 826 | [ -z "$new_hy2_psk" ] && new_hy2_psk=$(rand_b64) 827 | 828 | info "正在停止服务..." 829 | service_stop || warn "停止服务失败" 830 | 831 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 832 | 833 | jq --argjson port "$new_hy2_port" --arg psk "$new_hy2_psk" ' 834 | .inbounds |= map( 835 | if .type=="hysteria2" then 836 | .listen_port = $port | 837 | (.users[0].password) = $psk 838 | else . 839 | end 840 | ) 841 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 842 | 843 | info "已更新 HY2 端口($new_hy2_port)与密码(隐藏),正在启动服务..." 844 | service_start || warn "启动服务失败" 845 | sleep 1 846 | generate_and_save_uris || warn "生成 URI 失败" 847 | } 848 | 849 | # Reset Reality based on current config 850 | action_reset_reality() { 851 | read -p "输入新的 Reality 端口(回车保持 $REALITY_PORT): " new_reality_port 852 | [ -z "$new_reality_port" ] && new_reality_port="$REALITY_PORT" 853 | 854 | read -p "输入新的 Reality UUID(回车随机生成): " new_reality_uuid 855 | [ -z "$new_reality_uuid" ] && new_reality_uuid=$(cat /proc/sys/kernel/random/uuid) 856 | 857 | info "正在停止服务..." 858 | service_stop || warn "停止服务失败" 859 | 860 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 861 | 862 | jq --argjson port "$new_reality_port" --arg uuid "$new_reality_uuid" ' 863 | .inbounds |= map( 864 | if .type=="vless" then 865 | .listen_port = $port | 866 | (.users[0].uuid) = $uuid 867 | else . 868 | end 869 | ) 870 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 871 | 872 | info "已更新 Reality 端口($new_reality_port)与 UUID(隐藏),正在启动服务..." 873 | service_start || warn "启动服务失败" 874 | sleep 1 875 | generate_and_save_uris || warn "生成 URI 失败" 876 | } 877 | 878 | # Update sing-box 879 | action_update() { 880 | info "开始更新 sing-box..." 881 | if [ "$OS" = "alpine" ]; then 882 | apk update || warn "apk update 失败" 883 | apk add --upgrade --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 884 | warn "apk 更新失败,尝试官方安装脚本" 885 | bash <(curl -fsSL https://sing-box.app/install.sh) || { err "更新失败"; return 1; } 886 | } 887 | else 888 | bash <(curl -fsSL https://sing-box.app/install.sh) || { err "更新失败"; return 1; } 889 | fi 890 | 891 | info "更新完成,尝试重启服务..." 892 | if command -v sing-box >/dev/null 2>&1; then 893 | NEW_VER=$(sing-box version 2>/dev/null | head -n1 || echo "unknown") 894 | info "当前 sing-box 版本: $NEW_VER" 895 | service_restart || warn "重启失败" 896 | else 897 | warn "更新后未检测到 sing-box 可执行文件" 898 | fi 899 | } 900 | 901 | # Uninstall sing-box 902 | action_uninstall() { 903 | info "正在卸载 sing-box..." 904 | service_stop || true 905 | if [ "$OS" = "alpine" ]; then 906 | rc-update del "$SERVICE_NAME" default >/dev/null 2>&1 || true 907 | [ -f "/etc/init.d/$SERVICE_NAME" ] && rm -f "/etc/init.d/$SERVICE_NAME" 908 | apk del sing-box >/dev/null 2>&1 || true 909 | else 910 | systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true 911 | systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true 912 | [ -f "/etc/systemd/system/$SERVICE_NAME.service" ] && rm -f "/etc/systemd/system/$SERVICE_NAME.service" 913 | systemctl daemon-reload >/dev/null 2>&1 || true 914 | fi 915 | rm -rf /etc/sing-box /var/log/sing-box* /usr/local/bin/sb "$BIN_PATH" >/dev/null 2>&1 || true 916 | info "卸载完成" 917 | } 918 | 919 | # Generate relay script (SS out) 920 | action_generate_relay_script() { 921 | read_config_fields || return 1 922 | 923 | PUBLIC_IP=$(get_public_ip || true) 924 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 925 | 926 | RELAY_SCRIPT_PATH="/tmp/relay-install.sh" 927 | 928 | info "正在生成线路机脚本: $RELAY_SCRIPT_PATH" 929 | 930 | cat > "$RELAY_SCRIPT_PATH" <<'RELAY_TEMPLATE' 931 | #!/usr/bin/env bash 932 | set -euo pipefail 933 | 934 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 935 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 936 | 937 | if [ "$(id -u)" != "0" ]; then err "必须以 root 运行"; exit 1; fi 938 | 939 | detect_os(){ 940 | . /etc/os-release 2>/dev/null || true 941 | case "$ID" in 942 | alpine) OS=alpine ;; 943 | debian|ubuntu) OS=debian ;; 944 | centos|rhel|fedora) OS=redhat ;; 945 | *) OS=unknown ;; 946 | esac 947 | } 948 | detect_os 949 | 950 | install_deps(){ 951 | case "$OS" in 952 | alpine) apk update; apk add --no-cache curl jq bash openssl ca-certificates ;; 953 | debian) apt-get update -y; apt-get install -y curl jq bash openssl ca-certificates ;; 954 | redhat) yum install -y curl jq bash openssl ca-certificates ;; 955 | esac 956 | } 957 | install_deps 958 | 959 | install_singbox(){ 960 | case "$OS" in 961 | alpine) apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box ;; 962 | *) bash <(curl -fsSL https://sing-box.app/install.sh) ;; 963 | esac 964 | } 965 | install_singbox 966 | 967 | UUID=$(cat /proc/sys/kernel/random/uuid) 968 | 969 | info "生成 Reality 密钥对" 970 | REALITY_KEYS=$(sing-box generate reality-keypair 2>/dev/null || true) 971 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}' || true) 972 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}' || true) 973 | REALITY_SID=$(sing-box generate rand 8 --hex 2>/dev/null || echo "") 974 | info "Reality PK: $REALITY_PK" 975 | info "Reality PUB: $REALITY_PUB" 976 | info "Reality SID: $REALITY_SID" 977 | 978 | read -p "输入线路机监听端口(留空随机 20000-65000): " USER_PORT 979 | if [ -z "$USER_PORT" ]; then 980 | LISTEN_PORT=$(shuf -i 20000-65000 -n 1 2>/dev/null || echo $((RANDOM % 45001 + 20000))) 981 | else 982 | LISTEN_PORT="$USER_PORT" 983 | fi 984 | 985 | mkdir -p /etc/sing-box 986 | 987 | cat > /etc/sing-box/config.json < /etc/init.d/sing-box << 'SVC' 1034 | #!/sbin/openrc-run 1035 | name="sing-box" 1036 | description="SingBox service" 1037 | 1038 | command="/usr/bin/sing-box" 1039 | command_args="run -c /etc/sing-box/config.json" 1040 | command_background="yes" 1041 | pidfile="/run/sing-box.pid" 1042 | 1043 | depend() { 1044 | need net 1045 | } 1046 | SVC 1047 | chmod +x /etc/init.d/sing-box 1048 | rc-update add sing-box default 1049 | rc-service sing-box restart 1050 | else 1051 | cat > /etc/systemd/system/sing-box.service << 'SYSTEMD' 1052 | [Unit] 1053 | Description=Sing-box Relay 1054 | After=network.target 1055 | [Service] 1056 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 1057 | Restart=on-failure 1058 | [Install] 1059 | WantedBy=multi-user.target 1060 | SYSTEMD 1061 | systemctl daemon-reload 1062 | systemctl enable sing-box 1063 | systemctl restart sing-box 1064 | fi 1065 | # 获取本机公网 IP 1066 | PUB_IP=$(curl -s https://api.ipify.org || echo "YOUR_RELAY_IP") 1067 | 1068 | echo "" 1069 | info "✅ 安装完成" 1070 | 1071 | # ✅ ✅ ✅ 输出节点链接 1072 | echo "===================== 中转节点 Reality 链接 =====================" 1073 | echo "vless://$UUID@$PUB_IP:$LISTEN_PORT?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=$REALITY_PUB&sid=$REALITY_SID#relay" 1074 | echo "==================================================================" 1075 | echo "" 1076 | 1077 | RELAY_TEMPLATE 1078 | 1079 | # 重新填入 SS 出站节点信息 1080 | read_config_fields || return 1 1081 | 1082 | sed -i "s|__INBOUND_IP__|$PUBLIC_IP|g" "$RELAY_SCRIPT_PATH" 1083 | sed -i "s|__INBOUND_PORT__|$SS_PORT|g" "$RELAY_SCRIPT_PATH" 1084 | sed -i "s|__INBOUND_METHOD__|$SS_METHOD|g" "$RELAY_SCRIPT_PATH" 1085 | sed -i "s|__INBOUND_PASSWORD__|$SS_PSK|g" "$RELAY_SCRIPT_PATH" 1086 | 1087 | chmod +x "$RELAY_SCRIPT_PATH" 1088 | 1089 | info "✅ 线路机脚本已生成:$RELAY_SCRIPT_PATH" 1090 | echo "" 1091 | info "请手动复制以下内容到线路机,保存为 /tmp/relay-install.sh,并执行:chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 1092 | echo "------------------------------------------" 1093 | cat "$RELAY_SCRIPT_PATH" 1094 | echo "------------------------------------------" 1095 | echo "" 1096 | info "在线路机执行命令示例:" 1097 | echo " nano /tmp/relay-install.sh 保存后执行" 1098 | echo " chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 1099 | echo "" 1100 | info "复制完成后,即可在线路机完成 sing-box 中转节点部署。" 1101 | } 1102 | 1103 | # Main menu 1104 | while true; do 1105 | cat <<'MENU' 1106 | 1107 | ========================== 1108 | Sing-box 管理面板 (sb) 1109 | ========================== 1110 | 1) 查看三协议链接 (SS/HY2/Reality) 1111 | 2) 查看配置文件路径 1112 | 3) 编辑配置文件 1113 | 4) 重置 SS 端口/密码 1114 | 5) 重置 HY2 端口/密码 1115 | 6) 重置 Reality 端口/UUID 1116 | 7) 启动服务 1117 | 8) 停止服务 1118 | 9) 重启服务 1119 | 10) 查看状态 1120 | 11) 更新 sing-box 1121 | 12) 生成线路机出口脚本 (SS出站) 1122 | 13) 卸载 sing-box 1123 | 0) 退出 1124 | ========================== 1125 | MENU 1126 | 1127 | read -p "请输入选项: " opt 1128 | case "${opt:-}" in 1129 | 1) action_view_uri ;; 1130 | 2) action_view_config ;; 1131 | 3) action_edit_config ;; 1132 | 4) action_reset_ss ;; 1133 | 5) action_reset_hy2 ;; 1134 | 6) action_reset_reality ;; 1135 | 7) service_start && info "已发送启动命令" ;; 1136 | 8) service_stop && info "已发送停止命令" ;; 1137 | 9) service_restart && info "已发送重启命令" ;; 1138 | 10) service_status ;; 1139 | 11) action_update ;; 1140 | 12) action_generate_relay_script ;; 1141 | 13) action_uninstall; exit 0 ;; 1142 | 0) exit 0 ;; 1143 | *) warn "无效选项" ;; 1144 | esac 1145 | 1146 | echo "" 1147 | done 1148 | SB_SCRIPT 1149 | 1150 | chmod +x "$SB_PATH" || warn "无法设置 $SB_PATH 为可执行" 1151 | 1152 | info "sb 已创建:可输入 sb 运行管理面板" 1153 | 1154 | # end of script 1155 | -------------------------------------------------------------------------------- /install-singbox-yyds1.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # ----------------------- 5 | # 颜色输出函数 6 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 7 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 8 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 9 | 10 | # ----------------------- 11 | # 检测系统类型 12 | detect_os() { 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | OS_ID="${ID:-}" 16 | OS_ID_LIKE="${ID_LIKE:-}" 17 | else 18 | OS_ID="" 19 | OS_ID_LIKE="" 20 | fi 21 | 22 | if echo "$OS_ID $OS_ID_LIKE" | grep -qi "alpine"; then 23 | OS="alpine" 24 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 25 | OS="debian" 26 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 27 | OS="redhat" 28 | else 29 | OS="unknown" 30 | fi 31 | } 32 | 33 | detect_os 34 | info "检测到系统: $OS (${OS_ID:-unknown})" 35 | 36 | # ----------------------- 37 | # 检查 root 权限 38 | check_root() { 39 | if [ "$(id -u)" != "0" ]; then 40 | err "此脚本需要 root 权限" 41 | err "请使用: sudo bash -c \"\$(curl -fsSL ...)\" 或切换到 root 用户" 42 | exit 1 43 | fi 44 | } 45 | 46 | check_root 47 | 48 | # ----------------------- 49 | # 安装依赖 50 | install_deps() { 51 | info "安装系统依赖..." 52 | 53 | case "$OS" in 54 | alpine) 55 | apk update || { err "apk update 失败"; exit 1; } 56 | apk add --no-cache bash curl ca-certificates openssl openrc jq || { 57 | err "依赖安装失败" 58 | exit 1 59 | } 60 | 61 | if ! rc-service --list 2>/dev/null | grep -q "^openrc"; then 62 | rc-update add openrc boot >/dev/null 2>&1 || true 63 | rc-service openrc start >/dev/null 2>&1 || true 64 | fi 65 | ;; 66 | debian) 67 | export DEBIAN_FRONTEND=noninteractive 68 | apt-get update -y || { err "apt update 失败"; exit 1; } 69 | apt-get install -y curl ca-certificates openssl jq || { 70 | err "依赖安装失败" 71 | exit 1 72 | } 73 | ;; 74 | redhat) 75 | yum install -y curl ca-certificates openssl jq || { 76 | err "依赖安装失败" 77 | exit 1 78 | } 79 | ;; 80 | *) 81 | warn "未识别的系统类型,尝试继续..." 82 | ;; 83 | esac 84 | 85 | info "依赖安装完成" 86 | } 87 | 88 | install_deps 89 | 90 | # ----------------------- 91 | # 配置节点后缀名 92 | echo "请输入节点名称(留空则默认协议名):" 93 | read -r user_name 94 | # 如果用户输入非空,则添加后缀并覆盖保存到文件 95 | if [[ -n "$user_name" ]]; then 96 | suffix="-${user_name}" 97 | echo "$suffix" > /root/node_names.txt 98 | else 99 | suffix="" 100 | fi 101 | 102 | # ----------------------- 103 | # 配置端口和密码 104 | get_config() { 105 | info "=== 配置 Shadowsocks (SS) ===" 106 | if [ -n "${SINGBOX_PORT_SS:-}" ]; then 107 | PORT_SS="$SINGBOX_PORT_SS" 108 | info "使用环境变量端口 (SS): $PORT_SS" 109 | else 110 | read -p "请输入 SS 端口(留空则随机 10000-60000): " USER_PORT_SS 111 | if [ -z "$USER_PORT_SS" ]; then 112 | PORT_SS=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 113 | info "使用随机端口 (SS): $PORT_SS" 114 | else 115 | PORT_SS="$USER_PORT_SS" 116 | fi 117 | fi 118 | 119 | if [ -n "${SINGBOX_PASSWORD_SS:-}" ]; then 120 | PSK_SS="$SINGBOX_PASSWORD_SS" 121 | info "使用环境变量密码 (SS)" 122 | else 123 | read -p "请输入 SS 密码(留空则自动生成 Base64 密钥): " USER_PSK_SS 124 | if [ -z "$USER_PSK_SS" ]; then 125 | PSK_SS=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 126 | info "已自动生成 SS 密码" 127 | else 128 | PSK_SS="$USER_PSK_SS" 129 | fi 130 | fi 131 | 132 | info "=== 配置 Hysteria2 (HY2) ===" 133 | if [ -n "${SINGBOX_PORT_HY2:-}" ]; then 134 | PORT_HY2="$SINGBOX_PORT_HY2" 135 | info "使用环境变量端口 (HY2): $PORT_HY2" 136 | else 137 | read -p "请输入 HY2 端口(留空则随机 10000-60000): " USER_PORT_HY2 138 | if [ -z "$USER_PORT_HY2" ]; then 139 | PORT_HY2=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 140 | info "使用随机端口 (HY2): $PORT_HY2" 141 | else 142 | PORT_HY2="$USER_PORT_HY2" 143 | fi 144 | fi 145 | 146 | if [ -n "${SINGBOX_PASSWORD_HY2:-}" ]; then 147 | PSK_HY2="$SINGBOX_PASSWORD_HY2" 148 | info "使用环境变量密码 (HY2)" 149 | else 150 | read -p "请输入 HY2 密码(留空则自动生成 Base64 密钥): " USER_PSK_HY2 151 | if [ -z "$USER_PSK_HY2" ]; then 152 | PSK_HY2=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 153 | info "已自动生成 HY2 密码" 154 | else 155 | PSK_HY2="$USER_PSK_HY2" 156 | fi 157 | fi 158 | 159 | info "=== 配置 VLESS Reality ===" 160 | if [ -n "${SINGBOX_PORT_REALITY:-}" ]; then 161 | PORT_REALITY="$SINGBOX_PORT_REALITY" 162 | info "使用环境变量端口 (Reality): $PORT_REALITY" 163 | else 164 | read -p "请输入 VLESS Reality 端口(留空则随机 10000-60000): " USER_PORT_REALITY 165 | if [ -z "$USER_PORT_REALITY" ]; then 166 | PORT_REALITY=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 167 | info "使用随机端口 (Reality): $PORT_REALITY" 168 | else 169 | PORT_REALITY="$USER_PORT_REALITY" 170 | fi 171 | fi 172 | 173 | UUID=$(cat /proc/sys/kernel/random/uuid) 174 | info "已生成 UUID: $UUID" 175 | } 176 | 177 | get_config 178 | 179 | # ----------------------- 180 | # 安装 sing-box 181 | install_singbox() { 182 | info "开始安装 sing-box..." 183 | 184 | if command -v sing-box >/dev/null 2>&1; then 185 | CURRENT_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 186 | warn "检测到已安装 sing-box: $CURRENT_VERSION" 187 | read -p "是否重新安装?(y/N): " REINSTALL 188 | if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then 189 | info "跳过 sing-box 安装" 190 | return 0 191 | fi 192 | fi 193 | 194 | case "$OS" in 195 | alpine) 196 | info "使用 Edge 仓库安装 sing-box" 197 | apk update || { err "apk update 失败"; exit 1; } 198 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 199 | err "sing-box 安装失败" 200 | exit 1 201 | } 202 | ;; 203 | debian|redhat) 204 | bash <(curl -fsSL https://sing-box.app/install.sh) || { 205 | err "sing-box 安装失败" 206 | exit 1 207 | } 208 | ;; 209 | *) 210 | err "未支持的系统,无法安装 sing-box" 211 | exit 1 212 | ;; 213 | esac 214 | 215 | if ! command -v sing-box >/dev/null 2>&1; then 216 | err "sing-box 安装后未找到可执行文件" 217 | exit 1 218 | fi 219 | 220 | INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 221 | info "sing-box 安装成功: $INSTALLED_VERSION" 222 | } 223 | 224 | install_singbox 225 | 226 | # ----------------------- 227 | # 生成 Reality 密钥对和自签名证书 228 | generate_reality_keys() { 229 | info "生成 Reality 密钥对..." 230 | REALITY_KEYS=$(sing-box generate reality-keypair) 231 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}' | tr -d '\r') 232 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}' | tr -d '\r') 233 | REALITY_SID=$(sing-box generate rand 8 --hex) 234 | 235 | # 立即保存公钥和 SID 236 | mkdir -p /etc/sing-box 237 | echo -n "$REALITY_PUB" > /etc/sing-box/.reality_pub 238 | echo -n "$REALITY_SID" > /etc/sing-box/.reality_sid 239 | 240 | info "Reality PK: $REALITY_PK" 241 | info "Reality PUB: $REALITY_PUB" 242 | info "Reality SID: $REALITY_SID" 243 | } 244 | 245 | generate_reality_keys 246 | 247 | # ----------------------- 248 | # 生成 HY2 自签名证书 249 | generate_hy2_cert() { 250 | info "生成 HY2 自签名证书..." 251 | mkdir -p /etc/sing-box/certs 252 | 253 | if [ ! -f /etc/sing-box/certs/fullchain.pem ] || [ ! -f /etc/sing-box/certs/privkey.pem ]; then 254 | openssl req -x509 -newkey rsa:2048 -nodes \ 255 | -keyout /etc/sing-box/certs/privkey.pem \ 256 | -out /etc/sing-box/certs/fullchain.pem \ 257 | -days 3650 \ 258 | -subj "/CN=www.bing.com" || { 259 | err "证书生成失败" 260 | exit 1 261 | } 262 | info "HY2 证书已生成" 263 | else 264 | info "HY2 证书已存在" 265 | fi 266 | } 267 | 268 | generate_hy2_cert 269 | 270 | # ----------------------- 271 | # 生成配置文件 272 | CONFIG_PATH="/etc/sing-box/config.json" 273 | 274 | create_config() { 275 | info "生成配置文件: $CONFIG_PATH" 276 | 277 | mkdir -p "$(dirname "$CONFIG_PATH")" 278 | 279 | cat > "$CONFIG_PATH" </dev/null 2>&1 \ 347 | && info "配置文件验证通过" \ 348 | || warn "配置文件验证失败,但继续执行" 349 | 350 | mkdir -p /etc/sing-box 351 | cat > /etc/sing-box/.config_cache < "$SERVICE_PATH" <<'OPENRC' 378 | #!/sbin/openrc-run 379 | 380 | name="sing-box" 381 | description="Sing-box Proxy Server" 382 | command="/usr/bin/sing-box" 383 | command_args="run -c /etc/sing-box/config.json" 384 | pidfile="/run/${RC_SVCNAME}.pid" 385 | command_background="yes" 386 | output_log="/var/log/sing-box.log" 387 | error_log="/var/log/sing-box.err" 388 | 389 | depend() { 390 | need net 391 | after firewall 392 | } 393 | 394 | start_pre() { 395 | checkpath --directory --mode 0755 /var/log 396 | checkpath --directory --mode 0755 /run 397 | } 398 | OPENRC 399 | 400 | chmod +x "$SERVICE_PATH" 401 | rc-update add sing-box default >/dev/null 2>&1 || warn "添加开机自启失败" 402 | rc-service sing-box restart || { 403 | err "服务启动失败" 404 | tail -20 /var/log/sing-box.err 2>/dev/null || tail -20 /var/log/sing-box.log 2>/dev/null || true 405 | exit 1 406 | } 407 | 408 | sleep 2 409 | if rc-service sing-box status >/dev/null 2>&1; then 410 | info "✅ OpenRC 服务已启动" 411 | else 412 | err "服务状态异常" 413 | exit 1 414 | fi 415 | 416 | else 417 | SERVICE_PATH="/etc/systemd/system/sing-box.service" 418 | 419 | cat > "$SERVICE_PATH" <<'SYSTEMD' 420 | [Unit] 421 | Description=Sing-box Proxy Server 422 | Documentation=https://sing-box.sagernet.org 423 | After=network.target nss-lookup.target 424 | Wants=network.target 425 | 426 | [Service] 427 | Type=simple 428 | User=root 429 | WorkingDirectory=/etc/sing-box 430 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 431 | ExecReload=/bin/kill -HUP $MAINPID 432 | Restart=on-failure 433 | RestartSec=10s 434 | LimitNOFILE=1048576 435 | 436 | [Install] 437 | WantedBy=multi-user.target 438 | SYSTEMD 439 | 440 | systemctl daemon-reload 441 | systemctl enable sing-box >/dev/null 2>&1 442 | systemctl restart sing-box || { 443 | err "服务启动失败" 444 | journalctl -u sing-box -n 30 --no-pager 445 | exit 1 446 | } 447 | 448 | sleep 2 449 | if systemctl is-active sing-box >/dev/null 2>&1; then 450 | info "✅ Systemd 服务已启动" 451 | else 452 | err "服务状态异常" 453 | exit 1 454 | fi 455 | fi 456 | 457 | info "服务配置完成: $SERVICE_PATH" 458 | } 459 | 460 | setup_service 461 | 462 | # ----------------------- 463 | # 获取公网 IP 464 | get_public_ip() { 465 | local ip="" 466 | for url in \ 467 | "https://api.ipify.org" \ 468 | "https://ipinfo.io/ip" \ 469 | "https://ifconfig.me" \ 470 | "https://icanhazip.com" \ 471 | "https://ipecho.net/plain"; do 472 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 473 | if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 474 | echo "$ip" 475 | return 0 476 | fi 477 | done 478 | return 1 479 | } 480 | 481 | PUB_IP=$(get_public_ip || echo "YOUR_SERVER_IP") 482 | if [ "$PUB_IP" = "YOUR_SERVER_IP" ]; then 483 | warn "无法获取公网 IP,请手动替换" 484 | else 485 | info "检测到公网 IP: $PUB_IP" 486 | fi 487 | 488 | # ----------------------- 489 | # 生成链接 490 | generate_uris() { 491 | local host="$PUB_IP" 492 | 493 | # SS URI 494 | local ss_userinfo="2022-blake3-aes-128-gcm:${PSK_SS}" 495 | if command -v python3 >/dev/null 2>&1; then 496 | ss_encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$ss_userinfo" 2>/dev/null || echo "$ss_userinfo") 497 | else 498 | ss_encoded=$(printf "%s" "$ss_userinfo" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 499 | fi 500 | ss_b64=$(printf "%s" "$ss_userinfo" | base64 -w0 2>/dev/null || printf "%s" "$ss_userinfo" | base64 | tr -d '\n') 501 | 502 | # HY2 URI 503 | if command -v python3 >/dev/null 2>&1; then 504 | hy2_encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$PSK_HY2") 505 | else 506 | hy2_encoded=$(printf "%s" "$PSK_HY2" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 507 | fi 508 | 509 | echo "=== Shadowsocks (SS) ===" 510 | echo "ss://${ss_encoded}@${host}:${PORT_SS}#ss${suffix}" 511 | echo "ss://${ss_b64}@${host}:${PORT_SS}#ss${suffix}" 512 | echo "" 513 | 514 | # HY2 URI 515 | echo "=== Hysteria2 (HY2) ===" 516 | echo "hy2://${hy2_encoded}@${host}:${PORT_HY2}/?sni=www.bing.com&insecure=1#hy2${suffix}" 517 | echo "" 518 | 519 | # VLESS Reality URI 520 | echo "=== VLESS Reality ===" 521 | echo "vless://${UUID}@${host}:${PORT_REALITY}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}#reality${suffix}" 522 | } 523 | 524 | # ----------------------- 525 | # 最终输出 526 | echo "" 527 | echo "==========================================" 528 | info "🎉 Sing-box 多协议部署完成!" 529 | echo "==========================================" 530 | echo "" 531 | info "📋 配置信息:" 532 | echo " SS 端口: $PORT_SS | 密码: $PSK_SS" 533 | echo " HY2 端口: $PORT_HY2 | 密码: $PSK_HY2" 534 | echo " Reality 端口: $PORT_REALITY | UUID: $UUID" 535 | echo " 服务器: $PUB_IP" 536 | echo "" 537 | info "📂 文件位置:" 538 | echo " 配置: $CONFIG_PATH" 539 | echo " 证书: /etc/sing-box/certs/" 540 | echo " 服务: $SERVICE_PATH" 541 | echo "" 542 | info "🔗 客户端链接:" 543 | generate_uris | while IFS= read -r line; do 544 | echo " $line" 545 | done 546 | echo "" 547 | info "📧 管理命令:" 548 | if [ "$OS" = "alpine" ]; then 549 | echo " 启动: rc-service sing-box start" 550 | echo " 停止: rc-service sing-box stop" 551 | echo " 重启: rc-service sing-box restart" 552 | echo " 状态: rc-service sing-box status" 553 | echo " 日志: tail -f /var/log/sing-box.log" 554 | else 555 | echo " 启动: systemctl start sing-box" 556 | echo " 停止: systemctl stop sing-box" 557 | echo " 重启: systemctl restart sing-box" 558 | echo " 状态: systemctl status sing-box" 559 | echo " 日志: journalctl -u sing-box -f" 560 | fi 561 | echo "" 562 | echo "==========================================" 563 | # ----------------------- 564 | # Create `sb` management script at /usr/local/bin/sb 565 | 566 | SB_PATH="/usr/local/bin/sb" 567 | 568 | info "正在创建 sb 管理脚本: $SB_PATH" 569 | 570 | cat > "$SB_PATH" <<'SB_SCRIPT' 571 | #!/usr/bin/env bash 572 | set -euo pipefail 573 | 574 | # ----------------------- 575 | # sb 管理面板(无 python3,使用 jq) 576 | # 兼容: alpine / debian / redhat 577 | # 依赖: jq, curl, openssl 或 /dev/urandom 578 | # ----------------------- 579 | 580 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 581 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 582 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 583 | 584 | CONFIG_PATH="${CONFIG_PATH:-/etc/sing-box/config.json}" 585 | URI_PATH="${URI_PATH:-/etc/sing-box/uris.txt}" 586 | REALITY_PUB_FILE="${REALITY_PUB_FILE:-/etc/sing-box/.reality_pub}" 587 | SERVICE_NAME="${SERVICE_NAME:-sing-box}" 588 | BIN_PATH="${BIN_PATH:-/usr/bin/sing-box}" 589 | 590 | # detect OS 591 | detect_os() { 592 | if [ -f /etc/os-release ]; then 593 | . /etc/os-release 594 | ID="${ID:-}" 595 | ID_LIKE="${ID_LIKE:-}" 596 | else 597 | ID="" 598 | ID_LIKE="" 599 | fi 600 | 601 | if echo "$ID $ID_LIKE" | grep -qi "alpine"; then 602 | OS="alpine" 603 | elif echo "$ID $ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 604 | OS="debian" 605 | elif echo "$ID $ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 606 | OS="redhat" 607 | else 608 | OS="unknown" 609 | fi 610 | } 611 | 612 | detect_os 613 | 614 | # service helpers 615 | service_start() { 616 | if [ "$OS" = "alpine" ]; then 617 | rc-service "$SERVICE_NAME" start || return $? 618 | else 619 | systemctl start "$SERVICE_NAME" || return $? 620 | fi 621 | } 622 | service_stop() { 623 | if [ "$OS" = "alpine" ]; then 624 | rc-service "$SERVICE_NAME" stop || return $? 625 | else 626 | systemctl stop "$SERVICE_NAME" || return $? 627 | fi 628 | } 629 | service_restart() { 630 | if [ "$OS" = "alpine" ]; then 631 | rc-service "$SERVICE_NAME" restart || return $? 632 | else 633 | systemctl restart "$SERVICE_NAME" || return $? 634 | fi 635 | } 636 | service_status() { 637 | if [ "$OS" = "alpine" ]; then 638 | rc-service "$SERVICE_NAME" status || return $? 639 | else 640 | systemctl status "$SERVICE_NAME" --no-pager || return $? 641 | fi 642 | } 643 | 644 | # Safe random 645 | rand_b64() { 646 | if command -v openssl >/dev/null 2>&1; then 647 | openssl rand -base64 16 | tr -d '\n\r' 648 | else 649 | head -c 16 /dev/urandom | base64 | tr -d '\n\r' 650 | fi 651 | } 652 | 653 | # URL-encode minimal (for SS userinfo like "method:password") 654 | # encode only a small set of characters common in userinfo 655 | url_encode_min() { 656 | local s="$1" 657 | printf "%s" "$s" | sed -e 's/%/%25/g' \ 658 | -e 's/:/%3A/g' \ 659 | -e 's/+/%2B/g' \ 660 | -e 's/\//%2F/g' \ 661 | -e 's/=/\%3D/g' 662 | } 663 | 664 | 665 | # read JSON fields from config using jq 666 | read_config_fields() { 667 | if [ ! -f "$CONFIG_PATH" ]; then 668 | err "未找到配置文件: $CONFIG_PATH" 669 | return 1 670 | fi 671 | 672 | # Shadowsocks 673 | SS_PORT=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 674 | SS_PSK=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .password // empty' "$CONFIG_PATH" | head -n1 || true) 675 | SS_METHOD=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .method // empty' "$CONFIG_PATH" | head -n1 || true) 676 | 677 | # Hysteria2 678 | HY2_PORT=$(jq -r '.inbounds[] | select(.type=="hysteria2") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 679 | HY2_PSK=$(jq -r '.inbounds[] | select(.type=="hysteria2") | .users[0].password // empty' "$CONFIG_PATH" | head -n1 || true) 680 | 681 | # VLESS / Reality 682 | REALITY_PORT=$(jq -r '.inbounds[] | select(.type=="vless") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 683 | REALITY_UUID=$(jq -r '.inbounds[] | select(.type=="vless") | .users[0].uuid // empty' "$CONFIG_PATH" | head -n1 || true) 684 | REALITY_PK=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.private_key // empty' "$CONFIG_PATH" | head -n1 || true) 685 | REALITY_SID=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.short_id[0] // empty' "$CONFIG_PATH" | head -n1 || true) 686 | 687 | # fallback defaults 688 | SS_PORT="${SS_PORT:-}" 689 | SS_PSK="${SS_PSK:-}" 690 | SS_METHOD="${SS_METHOD:-}" 691 | HY2_PORT="${HY2_PORT:-}" 692 | HY2_PSK="${HY2_PSK:-}" 693 | REALITY_PORT="${REALITY_PORT:-}" 694 | REALITY_UUID="${REALITY_UUID:-}" 695 | REALITY_PK="${REALITY_PK:-}" 696 | REALITY_SID="${REALITY_SID:-}" 697 | } 698 | 699 | # get public IP (tries multiple endpoints) 700 | get_public_ip() { 701 | local ip="" 702 | for url in "https://api.ipify.org" "https://ipinfo.io/ip" "https://ifconfig.me" "https://icanhazip.com" "https://ipecho.net/plain"; do 703 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 704 | if [ -n "$ip" ]; then 705 | echo "$ip" 706 | return 0 707 | fi 708 | done 709 | return 1 710 | } 711 | 712 | # generate and save URIs 713 | generate_and_save_uris() { 714 | read_config_fields || return 1 715 | 716 | PUBLIC_IP=$(get_public_ip || true) 717 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 718 | 719 | # 读取文件内容作为节点后缀 720 | node_suffix=$(cat /root/node_names.txt 2>/dev/null) 721 | 722 | # SS: two formats: percent-encoded userinfo and base64 userinfo 723 | ss_userinfo="${SS_METHOD}:${SS_PSK}" 724 | # percent encode minimal 725 | ss_encoded=$(url_encode_min "$ss_userinfo") 726 | ss_b64=$(printf "%s" "$ss_userinfo" | base64 -w0 2>/dev/null || printf "%s" "$ss_userinfo" | base64 | tr -d '\n') 727 | hy2_encoded=$(url_encode_min "$HY2_PSK") 728 | hy2_uri="hy2://${hy2_encoded}@${PUBLIC_IP}:${HY2_PORT}/?sni=www.bing.com&insecure=1#hy2${node_suffix}" 729 | 730 | 731 | # reality pubkey read file or from config (fallback) 732 | if [ -f "$REALITY_PUB_FILE" ]; then 733 | REALITY_PUB=$(cat "$REALITY_PUB_FILE") 734 | else 735 | # try to extract pub from config if stored there 736 | REALITY_PUB=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.public_key // empty' "$CONFIG_PATH" | head -n1 || true) 737 | REALITY_PUB="${REALITY_PUB:-UNKNOWN}" 738 | fi 739 | 740 | reality_uri="vless://${REALITY_UUID}@${PUBLIC_IP}:${REALITY_PORT}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}#reality${node_suffix}" 741 | 742 | { 743 | echo "=== Shadowsocks (SS) ===" 744 | echo "ss://${ss_encoded}@${PUBLIC_IP}:${SS_PORT}#ss${node_suffix}" 745 | echo "ss://${ss_b64}@${PUBLIC_IP}:${SS_PORT}#ss${node_suffix}" 746 | echo "" 747 | echo "=== Hysteria2 (HY2) ===" 748 | echo "$hy2_uri" 749 | echo "" 750 | echo "=== VLESS Reality ===" 751 | echo "$reality_uri" 752 | } > "$URI_PATH" 753 | 754 | info "URI 已写入: $URI_PATH" 755 | } 756 | 757 | # view URIs (regenerate first) 758 | action_view_uri() { 759 | info "正在生成并显示 URI..." 760 | generate_and_save_uris || { err "生成 URI 失败"; return 1; } 761 | echo "" 762 | sed -n '1,200p' "$URI_PATH" || true 763 | } 764 | 765 | # view config path 766 | action_view_config() { 767 | echo "$CONFIG_PATH" 768 | } 769 | 770 | # edit config: use EDITOR or fallback 771 | action_edit_config() { 772 | if [ ! -f "$CONFIG_PATH" ]; then 773 | err "配置文件不存在: $CONFIG_PATH" 774 | return 1 775 | fi 776 | 777 | if command -v nano >/dev/null 2>&1; then 778 | ${EDITOR:-nano} "$CONFIG_PATH" 779 | else 780 | ${EDITOR:-vi} "$CONFIG_PATH" 781 | fi 782 | 783 | # check with sing-box if available 784 | if command -v sing-box >/dev/null 2>&1; then 785 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 786 | info "配置校验通过,尝试重启服务" 787 | service_restart || warn "重启失败" 788 | generate_and_save_uris || true 789 | else 790 | warn "配置校验失败,服务未重启" 791 | fi 792 | else 793 | warn "未检测到 sing-box,可跳过校验" 794 | fi 795 | } 796 | 797 | # Generic JSON updater helper using jq 798 | # args: jq_filter tempfile 799 | json_update() { 800 | local filter="$1" 801 | local tmp="${CONFIG_PATH}.tmp" 802 | jq "$filter" "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH" 803 | } 804 | 805 | # Reset SS based on current config 806 | action_reset_ss() { 807 | read -p "输入新的 SS 端口(回车保持 $SS_PORT): " new_ss_port 808 | [ -z "$new_ss_port" ] && new_ss_port="$SS_PORT" 809 | 810 | read -p "输入新的 SS 密码(回车随机生成): " new_ss_psk 811 | [ -z "$new_ss_psk" ] && new_ss_psk=$(rand_b64) 812 | 813 | info "正在停止服务..." 814 | service_stop || warn "停止服务失败" 815 | 816 | # 使用当前配置文件为模板,先备份 817 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 818 | 819 | jq --argjson port "$new_ss_port" --arg psk "$new_ss_psk" ' 820 | .inbounds |= map( 821 | if .type=="shadowsocks" then 822 | .listen_port = $port | 823 | .password = $psk 824 | else . 825 | end 826 | ) 827 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 828 | 829 | info "已更新 SS 端口($new_ss_port)与密码(隐藏),正在启动服务..." 830 | service_start || warn "启动服务失败" 831 | sleep 1 832 | generate_and_save_uris || warn "生成 URI 失败" 833 | } 834 | 835 | # Reset HY2 based on current config 836 | action_reset_hy2() { 837 | read -p "输入新的 HY2 端口(回车保持 $HY2_PORT): " new_hy2_port 838 | [ -z "$new_hy2_port" ] && new_hy2_port="$HY2_PORT" 839 | 840 | read -p "输入新的 HY2 密码(回车随机生成): " new_hy2_psk 841 | [ -z "$new_hy2_psk" ] && new_hy2_psk=$(rand_b64) 842 | 843 | info "正在停止服务..." 844 | service_stop || warn "停止服务失败" 845 | 846 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 847 | 848 | jq --argjson port "$new_hy2_port" --arg psk "$new_hy2_psk" ' 849 | .inbounds |= map( 850 | if .type=="hysteria2" then 851 | .listen_port = $port | 852 | (.users[0].password) = $psk 853 | else . 854 | end 855 | ) 856 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 857 | 858 | info "已更新 HY2 端口($new_hy2_port)与密码(隐藏),正在启动服务..." 859 | service_start || warn "启动服务失败" 860 | sleep 1 861 | generate_and_save_uris || warn "生成 URI 失败" 862 | } 863 | 864 | # Reset Reality based on current config 865 | action_reset_reality() { 866 | read -p "输入新的 Reality 端口(回车保持 $REALITY_PORT): " new_reality_port 867 | [ -z "$new_reality_port" ] && new_reality_port="$REALITY_PORT" 868 | 869 | read -p "输入新的 Reality UUID(回车随机生成): " new_reality_uuid 870 | [ -z "$new_reality_uuid" ] && new_reality_uuid=$(cat /proc/sys/kernel/random/uuid) 871 | 872 | info "正在停止服务..." 873 | service_stop || warn "停止服务失败" 874 | 875 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 876 | 877 | jq --argjson port "$new_reality_port" --arg uuid "$new_reality_uuid" ' 878 | .inbounds |= map( 879 | if .type=="vless" then 880 | .listen_port = $port | 881 | (.users[0].uuid) = $uuid 882 | else . 883 | end 884 | ) 885 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 886 | 887 | info "已更新 Reality 端口($new_reality_port)与 UUID(隐藏),正在启动服务..." 888 | service_start || warn "启动服务失败" 889 | sleep 1 890 | generate_and_save_uris || warn "生成 URI 失败" 891 | } 892 | 893 | # Update sing-box 894 | action_update() { 895 | info "开始更新 sing-box..." 896 | if [ "$OS" = "alpine" ]; then 897 | apk update || warn "apk update 失败" 898 | apk add --upgrade --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 899 | warn "apk 更新失败,尝试官方安装脚本" 900 | bash <(curl -fsSL https://sing-box.app/install.sh) || { err "更新失败"; return 1; } 901 | } 902 | else 903 | bash <(curl -fsSL https://sing-box.app/install.sh) || { err "更新失败"; return 1; } 904 | fi 905 | 906 | info "更新完成,尝试重启服务..." 907 | if command -v sing-box >/dev/null 2>&1; then 908 | NEW_VER=$(sing-box version 2>/dev/null | head -n1 || echo "unknown") 909 | info "当前 sing-box 版本: $NEW_VER" 910 | service_restart || warn "重启失败" 911 | else 912 | warn "更新后未检测到 sing-box 可执行文件" 913 | fi 914 | } 915 | 916 | # Uninstall sing-box 917 | action_uninstall() { 918 | info "正在卸载 sing-box..." 919 | service_stop || true 920 | if [ "$OS" = "alpine" ]; then 921 | rc-update del "$SERVICE_NAME" default >/dev/null 2>&1 || true 922 | [ -f "/etc/init.d/$SERVICE_NAME" ] && rm -f "/etc/init.d/$SERVICE_NAME" 923 | apk del sing-box >/dev/null 2>&1 || true 924 | else 925 | systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true 926 | systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true 927 | [ -f "/etc/systemd/system/$SERVICE_NAME.service" ] && rm -f "/etc/systemd/system/$SERVICE_NAME.service" 928 | systemctl daemon-reload >/dev/null 2>&1 || true 929 | fi 930 | rm -rf /etc/sing-box /var/log/sing-box* /usr/local/bin/sb "$BIN_PATH" >/dev/null 2>&1 || true 931 | rm -f /root/node_names.txt >/dev/null 2>&1 || true 932 | info "卸载完成" 933 | } 934 | 935 | # Generate relay script (SS out) 936 | action_generate_relay_script() { 937 | read_config_fields || return 1 938 | 939 | PUBLIC_IP=$(get_public_ip || true) 940 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 941 | 942 | RELAY_SCRIPT_PATH="/tmp/relay-install.sh" 943 | 944 | info "正在生成线路机脚本: $RELAY_SCRIPT_PATH" 945 | 946 | cat > "$RELAY_SCRIPT_PATH" <<'RELAY_TEMPLATE' 947 | #!/usr/bin/env bash 948 | set -euo pipefail 949 | 950 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 951 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 952 | 953 | if [ "$(id -u)" != "0" ]; then err "必须以 root 运行"; exit 1; fi 954 | 955 | detect_os(){ 956 | . /etc/os-release 2>/dev/null || true 957 | case "$ID" in 958 | alpine) OS=alpine ;; 959 | debian|ubuntu) OS=debian ;; 960 | centos|rhel|fedora) OS=redhat ;; 961 | *) OS=unknown ;; 962 | esac 963 | } 964 | detect_os 965 | 966 | install_deps(){ 967 | case "$OS" in 968 | alpine) apk update; apk add --no-cache curl jq bash openssl ca-certificates ;; 969 | debian) apt-get update -y; apt-get install -y curl jq bash openssl ca-certificates ;; 970 | redhat) yum install -y curl jq bash openssl ca-certificates ;; 971 | esac 972 | } 973 | install_deps 974 | 975 | install_singbox(){ 976 | case "$OS" in 977 | alpine) apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box ;; 978 | *) bash <(curl -fsSL https://sing-box.app/install.sh) ;; 979 | esac 980 | } 981 | install_singbox 982 | 983 | UUID=$(cat /proc/sys/kernel/random/uuid) 984 | 985 | info "生成 Reality 密钥对" 986 | REALITY_KEYS=$(sing-box generate reality-keypair 2>/dev/null || true) 987 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}' || true) 988 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}' || true) 989 | REALITY_SID=$(sing-box generate rand 8 --hex 2>/dev/null || echo "") 990 | info "Reality PK: $REALITY_PK" 991 | info "Reality PUB: $REALITY_PUB" 992 | info "Reality SID: $REALITY_SID" 993 | 994 | read -p "输入线路机监听端口(留空随机 20000-65000): " USER_PORT 995 | if [ -z "$USER_PORT" ]; then 996 | LISTEN_PORT=$(shuf -i 20000-65000 -n 1 2>/dev/null || echo $((RANDOM % 45001 + 20000))) 997 | else 998 | LISTEN_PORT="$USER_PORT" 999 | fi 1000 | 1001 | mkdir -p /etc/sing-box 1002 | 1003 | cat > /etc/sing-box/config.json < /etc/init.d/sing-box << 'SVC' 1050 | #!/sbin/openrc-run 1051 | name="sing-box" 1052 | description="SingBox service" 1053 | 1054 | command="/usr/bin/sing-box" 1055 | command_args="run -c /etc/sing-box/config.json" 1056 | command_background="yes" 1057 | pidfile="/run/sing-box.pid" 1058 | 1059 | depend() { 1060 | need net 1061 | } 1062 | SVC 1063 | chmod +x /etc/init.d/sing-box 1064 | rc-update add sing-box default 1065 | rc-service sing-box restart 1066 | else 1067 | cat > /etc/systemd/system/sing-box.service << 'SYSTEMD' 1068 | [Unit] 1069 | Description=Sing-box Relay 1070 | After=network.target 1071 | [Service] 1072 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 1073 | Restart=on-failure 1074 | [Install] 1075 | WantedBy=multi-user.target 1076 | SYSTEMD 1077 | systemctl daemon-reload 1078 | systemctl enable sing-box 1079 | systemctl restart sing-box 1080 | fi 1081 | # 获取本机公网 IP 1082 | PUB_IP=$(curl -s https://api.ipify.org || echo "YOUR_RELAY_IP") 1083 | 1084 | echo "" 1085 | info "✅ 安装完成" 1086 | 1087 | # ✅ ✅ ✅ 输出节点链接 1088 | echo "===================== 中转节点 Reality 链接 =====================" 1089 | echo "vless://$UUID@$PUB_IP:$LISTEN_PORT?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=$REALITY_PUB&sid=$REALITY_SID#relay" 1090 | echo "==================================================================" 1091 | echo "" 1092 | 1093 | RELAY_TEMPLATE 1094 | 1095 | # 重新填入 SS 出站节点信息 1096 | read_config_fields || return 1 1097 | 1098 | sed -i "s|__INBOUND_IP__|$PUBLIC_IP|g" "$RELAY_SCRIPT_PATH" 1099 | sed -i "s|__INBOUND_PORT__|$SS_PORT|g" "$RELAY_SCRIPT_PATH" 1100 | sed -i "s|__INBOUND_METHOD__|$SS_METHOD|g" "$RELAY_SCRIPT_PATH" 1101 | sed -i "s|__INBOUND_PASSWORD__|$SS_PSK|g" "$RELAY_SCRIPT_PATH" 1102 | 1103 | chmod +x "$RELAY_SCRIPT_PATH" 1104 | 1105 | info "✅ 线路机脚本已生成:$RELAY_SCRIPT_PATH" 1106 | echo "" 1107 | info "请手动复制以下内容到线路机,保存为 /tmp/relay-install.sh,并执行:chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 1108 | echo "------------------------------------------" 1109 | cat "$RELAY_SCRIPT_PATH" 1110 | echo "------------------------------------------" 1111 | echo "" 1112 | info "在线路机执行命令示例:" 1113 | echo " nano /tmp/relay-install.sh 保存后执行" 1114 | echo " chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 1115 | echo "" 1116 | info "复制完成后,即可在线路机完成 sing-box 中转节点部署。" 1117 | } 1118 | 1119 | # Main menu 1120 | while true; do 1121 | cat <<'MENU' 1122 | 1123 | ========================== 1124 | Sing-box 管理面板 (sb) 1125 | ========================== 1126 | 1) 查看三协议链接 (SS/HY2/Reality) 1127 | 2) 查看配置文件路径 1128 | 3) 编辑配置文件 1129 | 4) 重置 SS 端口/密码 1130 | 5) 重置 HY2 端口/密码 1131 | 6) 重置 Reality 端口/UUID 1132 | 7) 启动服务 1133 | 8) 停止服务 1134 | 9) 重启服务 1135 | 10) 查看状态 1136 | 11) 更新 sing-box 1137 | 12) 生成线路机出口脚本 (SS出站) 1138 | 13) 卸载 sing-box 1139 | 0) 退出 1140 | ========================== 1141 | MENU 1142 | 1143 | read -p "请输入选项: " opt 1144 | case "${opt:-}" in 1145 | 1) action_view_uri ;; 1146 | 2) action_view_config ;; 1147 | 3) action_edit_config ;; 1148 | 4) action_reset_ss ;; 1149 | 5) action_reset_hy2 ;; 1150 | 6) action_reset_reality ;; 1151 | 7) service_start && info "已发送启动命令" ;; 1152 | 8) service_stop && info "已发送停止命令" ;; 1153 | 9) service_restart && info "已发送重启命令" ;; 1154 | 10) service_status ;; 1155 | 11) action_update ;; 1156 | 12) action_generate_relay_script ;; 1157 | 13) action_uninstall; exit 0 ;; 1158 | 0) exit 0 ;; 1159 | *) warn "无效选项" ;; 1160 | esac 1161 | 1162 | echo "" 1163 | done 1164 | SB_SCRIPT 1165 | 1166 | chmod +x "$SB_PATH" || warn "无法设置 $SB_PATH 为可执行" 1167 | 1168 | info "sb 已创建:可输入 sb 运行管理面板" 1169 | 1170 | # end of script 1171 | -------------------------------------------------------------------------------- /install-singbox-yyds2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # ----------------------- 5 | # 彩色输出函数 6 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 7 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 8 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 9 | 10 | # ----------------------- 11 | # 检测系统类型 12 | detect_os() { 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | OS_ID="${ID:-}" 16 | OS_ID_LIKE="${ID_LIKE:-}" 17 | else 18 | OS_ID="" 19 | OS_ID_LIKE="" 20 | fi 21 | 22 | if echo "$OS_ID $OS_ID_LIKE" | grep -qi "alpine"; then 23 | OS="alpine" 24 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 25 | OS="debian" 26 | elif echo "$OS_ID $OS_ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 27 | OS="redhat" 28 | else 29 | OS="unknown" 30 | fi 31 | } 32 | 33 | detect_os 34 | info "检测到系统: $OS (${OS_ID:-unknown})" 35 | 36 | # ----------------------- 37 | # 检查 root 权限 38 | check_root() { 39 | if [ "$(id -u)" != "0" ]; then 40 | err "此脚本需要 root 权限" 41 | err "请使用: sudo bash -c \"\$(curl -fsSL ...)\" 或切换到 root 用户" 42 | exit 1 43 | fi 44 | } 45 | 46 | check_root 47 | 48 | # ----------------------- 49 | # 安装依赖 50 | install_deps() { 51 | info "安装系统依赖..." 52 | 53 | case "$OS" in 54 | alpine) 55 | apk update || { err "apk update 失败"; exit 1; } 56 | apk add --no-cache bash curl ca-certificates openssl openrc jq || { 57 | err "依赖安装失败" 58 | exit 1 59 | } 60 | ;; 61 | debian) 62 | export DEBIAN_FRONTEND=noninteractive 63 | apt-get update -y || { err "apt update 失败"; exit 1; } 64 | apt-get install -y curl ca-certificates openssl jq || { 65 | err "依赖安装失败" 66 | exit 1 67 | } 68 | ;; 69 | redhat) 70 | yum install -y curl ca-certificates openssl jq || { 71 | err "依赖安装失败" 72 | exit 1 73 | } 74 | ;; 75 | *) 76 | warn "未识别的系统类型,尝试继续..." 77 | ;; 78 | esac 79 | 80 | info "依赖安装完成" 81 | } 82 | 83 | install_deps 84 | 85 | # ----------------------- 86 | # 配置节点名后缀 87 | echo "请输入节点名称(留空则默认协议名):" 88 | read -r user_name 89 | if [[ -n "$user_name" ]]; then 90 | suffix="-${user_name}" 91 | echo "$suffix" > /root/node_names.txt 92 | else 93 | suffix="" 94 | fi 95 | 96 | # ----------------------- 97 | # 配置端口和密码 98 | get_config() { 99 | info "=== 配置 Shadowsocks (SS) ===" 100 | if [ -n "${SINGBOX_PORT_SS:-}" ]; then 101 | PORT_SS="$SINGBOX_PORT_SS" 102 | info "使用环境变量端口 (SS): $PORT_SS" 103 | else 104 | read -p "请输入 SS 端口(留空则随机 10000-60000): " USER_PORT_SS 105 | if [ -z "$USER_PORT_SS" ]; then 106 | PORT_SS=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 107 | info "使用随机端口 (SS): $PORT_SS" 108 | else 109 | PORT_SS="$USER_PORT_SS" 110 | fi 111 | fi 112 | 113 | if [ -n "${SINGBOX_PASSWORD_SS:-}" ]; then 114 | PSK_SS="$SINGBOX_PASSWORD_SS" 115 | info "使用环境变量密码 (SS)" 116 | else 117 | read -p "请输入 SS 密码(留空则自动生成 Base64 密钥): " USER_PSK_SS 118 | if [ -z "$USER_PSK_SS" ]; then 119 | PSK_SS=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 120 | info "已自动生成 SS 密码" 121 | else 122 | PSK_SS="$USER_PSK_SS" 123 | fi 124 | fi 125 | 126 | info "=== 配置 Hysteria2 (HY2) ===" 127 | if [ -n "${SINGBOX_PORT_HY2:-}" ]; then 128 | PORT_HY2="$SINGBOX_PORT_HY2" 129 | info "使用环境变量端口 (HY2): $PORT_HY2" 130 | else 131 | read -p "请输入 HY2 端口(留空则随机 10000-60000): " USER_PORT_HY2 132 | if [ -z "$USER_PORT_HY2" ]; then 133 | PORT_HY2=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 134 | info "使用随机端口 (HY2): $PORT_HY2" 135 | else 136 | PORT_HY2="$USER_PORT_HY2" 137 | fi 138 | fi 139 | 140 | if [ -n "${SINGBOX_PASSWORD_HY2:-}" ]; then 141 | PSK_HY2="$SINGBOX_PASSWORD_HY2" 142 | info "使用环境变量密码 (HY2)" 143 | else 144 | read -p "请输入 HY2 密码(留空则自动生成 Base64 密钥): " USER_PSK_HY2 145 | if [ -z "$USER_PSK_HY2" ]; then 146 | PSK_HY2=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 147 | info "已自动生成 HY2 密码" 148 | else 149 | PSK_HY2="$USER_PSK_HY2" 150 | fi 151 | fi 152 | 153 | info "=== 配置 TUIC ===" 154 | if [ -n "${SINGBOX_PORT_TUIC:-}" ]; then 155 | PORT_TUIC="$SINGBOX_PORT_TUIC" 156 | info "使用环境变量端口 (TUIC): $PORT_TUIC" 157 | else 158 | read -p "请输入 TUIC 端口(留空则随机 10000-60000): " USER_PORT_TUIC 159 | if [ -z "$USER_PORT_TUIC" ]; then 160 | PORT_TUIC=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 161 | info "使用随机端口 (TUIC): $PORT_TUIC" 162 | else 163 | PORT_TUIC="$USER_PORT_TUIC" 164 | fi 165 | fi 166 | 167 | if [ -n "${SINGBOX_PASSWORD_TUIC:-}" ]; then 168 | PSK_TUIC="$SINGBOX_PASSWORD_TUIC" 169 | info "使用环境变量密码 (TUIC)" 170 | else 171 | read -p "请输入 TUIC 密码(留空则自动生成 Base64 密钥): " USER_PSK_TUIC 172 | if [ -z "$USER_PSK_TUIC" ]; then 173 | PSK_TUIC=$(openssl rand -base64 16 | tr -d '\n\r' || head -c 16 /dev/urandom | base64 | tr -d '\n\r') 174 | info "已自动生成 TUIC 密码" 175 | else 176 | PSK_TUIC="$USER_PSK_TUIC" 177 | fi 178 | fi 179 | 180 | if [ -n "${SINGBOX_UUID_TUIC:-}" ]; then 181 | UUID_TUIC="$SINGBOX_UUID_TUIC" 182 | info "使用环境变量 UUID (TUIC)" 183 | else 184 | read -p "请输入 TUIC UUID(留空则自动生成): " USER_UUID_TUIC 185 | if [ -z "$USER_UUID_TUIC" ]; then 186 | UUID_TUIC=$(cat /proc/sys/kernel/random/uuid) 187 | info "已自动生成 TUIC UUID" 188 | else 189 | UUID_TUIC="$USER_UUID_TUIC" 190 | fi 191 | fi 192 | 193 | info "=== 配置 VLESS Reality ===" 194 | if [ -n "${SINGBOX_PORT_REALITY:-}" ]; then 195 | PORT_REALITY="$SINGBOX_PORT_REALITY" 196 | info "使用环境变量端口 (Reality): $PORT_REALITY" 197 | else 198 | read -p "请输入 VLESS Reality 端口(留空则随机 10000-60000): " USER_PORT_REALITY 199 | if [ -z "$USER_PORT_REALITY" ]; then 200 | PORT_REALITY=$(shuf -i 10000-60000 -n 1 2>/dev/null || echo $((RANDOM % 50001 + 10000))) 201 | info "使用随机端口 (Reality): $PORT_REALITY" 202 | else 203 | PORT_REALITY="$USER_PORT_REALITY" 204 | fi 205 | fi 206 | 207 | UUID=$(cat /proc/sys/kernel/random/uuid) 208 | info "已生成 UUID: $UUID" 209 | } 210 | 211 | get_config 212 | 213 | # ----------------------- 214 | # 安装 sing-box 215 | install_singbox() { 216 | info "开始安装 sing-box..." 217 | 218 | if command -v sing-box >/dev/null 2>&1; then 219 | CURRENT_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 220 | warn "检测到已安装 sing-box: $CURRENT_VERSION" 221 | read -p "是否重新安装?(y/N): " REINSTALL 222 | if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then 223 | info "跳过 sing-box 安装" 224 | return 0 225 | fi 226 | fi 227 | 228 | case "$OS" in 229 | alpine) 230 | info "使用 Edge 仓库安装 sing-box" 231 | apk update || { err "apk update 失败"; exit 1; } 232 | apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 233 | err "sing-box 安装失败" 234 | exit 1 235 | } 236 | ;; 237 | debian|redhat) 238 | bash <(curl -fsSL https://sing-box.app/install.sh) || { 239 | err "sing-box 安装失败" 240 | exit 1 241 | } 242 | ;; 243 | *) 244 | err "未支持的系统,无法安装 sing-box" 245 | exit 1 246 | ;; 247 | esac 248 | 249 | if ! command -v sing-box >/dev/null 2>&1; then 250 | err "sing-box 安装后未找到可执行文件" 251 | exit 1 252 | fi 253 | 254 | INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -1 || echo "unknown") 255 | info "sing-box 安装成功: $INSTALLED_VERSION" 256 | } 257 | 258 | install_singbox 259 | 260 | # ----------------------- 261 | # 生成 Reality 密钥对和自签名证书 262 | generate_reality_keys() { 263 | info "生成 Reality 密钥对..." 264 | REALITY_KEYS=$(sing-box generate reality-keypair) 265 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}' | tr -d '\r') 266 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}' | tr -d '\r') 267 | REALITY_SID=$(sing-box generate rand 8 --hex) 268 | 269 | mkdir -p /etc/sing-box 270 | echo -n "$REALITY_PUB" > /etc/sing-box/.reality_pub 271 | echo -n "$REALITY_SID" > /etc/sing-box/.reality_sid 272 | 273 | info "Reality PK: $REALITY_PK" 274 | info "Reality PUB: $REALITY_PUB" 275 | info "Reality SID: $REALITY_SID" 276 | } 277 | 278 | generate_reality_keys 279 | 280 | # ----------------------- 281 | # 生成 HY2/TUIC 自签名证书(共用) 282 | generate_cert() { 283 | info "生成 HY2/TUIC 自签名证书..." 284 | mkdir -p /etc/sing-box/certs 285 | 286 | if [ ! -f /etc/sing-box/certs/fullchain.pem ] || [ ! -f /etc/sing-box/certs/privkey.pem ]; then 287 | openssl req -x509 -newkey rsa:2048 -nodes \ 288 | -keyout /etc/sing-box/certs/privkey.pem \ 289 | -out /etc/sing-box/certs/fullchain.pem \ 290 | -days 3650 \ 291 | -subj "/CN=www.bing.com" || { 292 | err "证书生成失败" 293 | exit 1 294 | } 295 | info "证书已生成" 296 | else 297 | info "证书已存在" 298 | fi 299 | } 300 | 301 | generate_cert 302 | 303 | # ----------------------- 304 | # 生成配置文件 305 | CONFIG_PATH="/etc/sing-box/config.json" 306 | 307 | create_config() { 308 | info "生成配置文件: $CONFIG_PATH" 309 | 310 | mkdir -p "$(dirname "$CONFIG_PATH")" 311 | 312 | cat > "$CONFIG_PATH" </dev/null 2>&1 \ 399 | && info "配置文件验证通过" \ 400 | || warn "配置文件验证失败,但继续执行" 401 | 402 | mkdir -p /etc/sing-box 403 | cat > /etc/sing-box/.config_cache < "$SERVICE_PATH" <<'OPENRC' 433 | #!/sbin/openrc-run 434 | 435 | name="sing-box" 436 | description="Sing-box Proxy Server" 437 | command="/usr/bin/sing-box" 438 | command_args="run -c /etc/sing-box/config.json" 439 | pidfile="/run/${RC_SVCNAME}.pid" 440 | command_background="yes" 441 | output_log="/var/log/sing-box.log" 442 | error_log="/var/log/sing-box.err" 443 | 444 | depend() { 445 | need net 446 | after firewall 447 | } 448 | 449 | start_pre() { 450 | checkpath --directory --mode 0755 /var/log 451 | checkpath --directory --mode 0755 /run 452 | } 453 | OPENRC 454 | 455 | chmod +x "$SERVICE_PATH" 456 | rc-update add sing-box default >/dev/null 2>&1 || warn "添加开机自启失败" 457 | rc-service sing-box restart || { 458 | err "服务启动失败" 459 | tail -20 /var/log/sing-box.err 2>/dev/null || tail -20 /var/log/sing-box.log 2>/dev/null || true 460 | exit 1 461 | } 462 | 463 | sleep 2 464 | if rc-service sing-box status >/dev/null 2>&1; then 465 | info "✅ OpenRC 服务已启动" 466 | else 467 | err "服务状态异常" 468 | exit 1 469 | fi 470 | 471 | else 472 | SERVICE_PATH="/etc/systemd/system/sing-box.service" 473 | 474 | cat > "$SERVICE_PATH" <<'SYSTEMD' 475 | [Unit] 476 | Description=Sing-box Proxy Server 477 | Documentation=https://sing-box.sagernet.org 478 | After=network.target nss-lookup.target 479 | Wants=network.target 480 | 481 | [Service] 482 | Type=simple 483 | User=root 484 | WorkingDirectory=/etc/sing-box 485 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 486 | ExecReload=/bin/kill -HUP $MAINPID 487 | Restart=on-failure 488 | RestartSec=10s 489 | LimitNOFILE=1048576 490 | 491 | [Install] 492 | WantedBy=multi-user.target 493 | SYSTEMD 494 | 495 | systemctl daemon-reload 496 | systemctl enable sing-box >/dev/null 2>&1 497 | systemctl restart sing-box || { 498 | err "服务启动失败" 499 | journalctl -u sing-box -n 30 --no-pager 500 | exit 1 501 | } 502 | 503 | sleep 2 504 | if systemctl is-active sing-box >/dev/null 2>&1; then 505 | info "✅ Systemd 服务已启动" 506 | else 507 | err "服务状态异常" 508 | exit 1 509 | fi 510 | fi 511 | 512 | info "服务配置完成: $SERVICE_PATH" 513 | } 514 | 515 | setup_service 516 | 517 | # ----------------------- 518 | # 获取公网 IP 519 | get_public_ip() { 520 | local ip="" 521 | for url in \ 522 | "https://api.ipify.org" \ 523 | "https://ipinfo.io/ip" \ 524 | "https://ifconfig.me" \ 525 | "https://icanhazip.com" \ 526 | "https://ipecho.net/plain"; do 527 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 528 | if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 529 | echo "$ip" 530 | return 0 531 | fi 532 | done 533 | return 1 534 | } 535 | 536 | PUB_IP=$(get_public_ip || echo "YOUR_SERVER_IP") 537 | if [ "$PUB_IP" = "YOUR_SERVER_IP" ]; then 538 | warn "无法获取公网 IP,请手动替换" 539 | else 540 | info "检测到公网 IP: $PUB_IP" 541 | fi 542 | 543 | # ----------------------- 544 | # 生成链接 545 | generate_uris() { 546 | local host="$PUB_IP" 547 | 548 | # SS URI 549 | local ss_userinfo="2022-blake3-aes-128-gcm:${PSK_SS}" 550 | ss_encoded=$(printf "%s" "$ss_userinfo" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 551 | ss_b64=$(printf "%s" "$ss_userinfo" | base64 -w0 2>/dev/null || printf "%s" "$ss_userinfo" | base64 | tr -d '\n') 552 | 553 | # HY2 URI 554 | hy2_encoded=$(printf "%s" "$PSK_HY2" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 555 | 556 | # TUIC URI 557 | tuic_encoded=$(printf "%s" "$PSK_TUIC" | sed 's/:/%3A/g; s/+/%2B/g; s/\//%2F/g; s/=/%3D/g') 558 | 559 | echo "=== Shadowsocks (SS) ===" 560 | echo "ss://${ss_encoded}@${host}:${PORT_SS}#ss${suffix}" 561 | echo "ss://${ss_b64}@${host}:${PORT_SS}#ss${suffix}" 562 | echo "" 563 | 564 | echo "=== Hysteria2 (HY2) ===" 565 | echo "hy2://${hy2_encoded}@${host}:${PORT_HY2}/?sni=www.bing.com&alpn=h3&insecure=1#hy2${suffix}" 566 | echo "" 567 | 568 | echo "=== TUIC ===" 569 | echo "tuic://${UUID_TUIC}:${tuic_encoded}@${host}:${PORT_TUIC}/?congestion_control=bbr&alpn=h3&sni=www.bing.com&insecure=1#tuic${suffix}" 570 | echo "" 571 | 572 | echo "=== VLESS Reality ===" 573 | echo "vless://${UUID}@${host}:${PORT_REALITY}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}#reality${suffix}" 574 | } 575 | 576 | # ----------------------- 577 | # 最终输出 578 | echo "" 579 | echo "==========================================" 580 | info "🎉 Sing-box 多协议部署完成!" 581 | echo "==========================================" 582 | echo "" 583 | info "📋 配置信息:" 584 | echo " SS 端口: $PORT_SS | 密码: $PSK_SS" 585 | echo " HY2 端口: $PORT_HY2 | 密码: $PSK_HY2" 586 | echo " TUIC 端口: $PORT_TUIC | UUID: $UUID_TUIC | 密码: $PSK_TUIC" 587 | echo " Reality 端口: $PORT_REALITY | UUID: $UUID" 588 | echo " 服务器: $PUB_IP" 589 | echo "" 590 | info "📂 文件位置:" 591 | echo " 配置: $CONFIG_PATH" 592 | echo " 证书: /etc/sing-box/certs/" 593 | echo " 服务: $SERVICE_PATH" 594 | echo "" 595 | info "📜 客户端链接:" 596 | generate_uris | while IFS= read -r line; do 597 | echo " $line" 598 | done 599 | echo "" 600 | info "📧 管理命令:" 601 | if [ "$OS" = "alpine" ]; then 602 | echo " 启动: rc-service sing-box start" 603 | echo " 停止: rc-service sing-box stop" 604 | echo " 重启: rc-service sing-box restart" 605 | echo " 状态: rc-service sing-box status" 606 | echo " 日志: tail -f /var/log/sing-box.log" 607 | else 608 | echo " 启动: systemctl start sing-box" 609 | echo " 停止: systemctl stop sing-box" 610 | echo " 重启: systemctl restart sing-box" 611 | echo " 状态: systemctl status sing-box" 612 | echo " 日志: journalctl -u sing-box -f" 613 | fi 614 | echo "" 615 | echo "==========================================" 616 | # ----------------------- 617 | # Create `sb` management script at /usr/local/bin/sb 618 | 619 | SB_PATH="/usr/local/bin/sb" 620 | 621 | info "正在创建 sb 管理脚本: $SB_PATH" 622 | 623 | cat > "$SB_PATH" <<'SB_SCRIPT' 624 | #!/usr/bin/env bash 625 | set -euo pipefail 626 | 627 | # ----------------------- 628 | # sb 管理面板(无 python3,使用 jq) 629 | # 兼容: alpine / debian / redhat 630 | # 依赖: jq, curl, openssl 或 /dev/urandom 631 | # ----------------------- 632 | 633 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 634 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 635 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 636 | 637 | CONFIG_PATH="${CONFIG_PATH:-/etc/sing-box/config.json}" 638 | URI_PATH="${URI_PATH:-/etc/sing-box/uris.txt}" 639 | REALITY_PUB_FILE="${REALITY_PUB_FILE:-/etc/sing-box/.reality_pub}" 640 | SERVICE_NAME="${SERVICE_NAME:-sing-box}" 641 | BIN_PATH="${BIN_PATH:-/usr/bin/sing-box}" 642 | 643 | # detect OS 644 | detect_os() { 645 | if [ -f /etc/os-release ]; then 646 | . /etc/os-release 647 | ID="${ID:-}" 648 | ID_LIKE="${ID_LIKE:-}" 649 | else 650 | ID="" 651 | ID_LIKE="" 652 | fi 653 | 654 | if echo "$ID $ID_LIKE" | grep -qi "alpine"; then 655 | OS="alpine" 656 | elif echo "$ID $ID_LIKE" | grep -Ei "debian|ubuntu" >/dev/null; then 657 | OS="debian" 658 | elif echo "$ID $ID_LIKE" | grep -Ei "centos|rhel|fedora" >/dev/null; then 659 | OS="redhat" 660 | else 661 | OS="unknown" 662 | fi 663 | } 664 | 665 | detect_os 666 | 667 | # service helpers 668 | service_start() { 669 | if [ "$OS" = "alpine" ]; then 670 | rc-service "$SERVICE_NAME" start || return $? 671 | else 672 | systemctl start "$SERVICE_NAME" || return $? 673 | fi 674 | } 675 | service_stop() { 676 | if [ "$OS" = "alpine" ]; then 677 | rc-service "$SERVICE_NAME" stop || return $? 678 | else 679 | systemctl stop "$SERVICE_NAME" || return $? 680 | fi 681 | } 682 | service_restart() { 683 | if [ "$OS" = "alpine" ]; then 684 | rc-service "$SERVICE_NAME" restart || return $? 685 | else 686 | systemctl restart "$SERVICE_NAME" || return $? 687 | fi 688 | } 689 | service_status() { 690 | if [ "$OS" = "alpine" ]; then 691 | rc-service "$SERVICE_NAME" status || return $? 692 | else 693 | systemctl status "$SERVICE_NAME" --no-pager || return $? 694 | fi 695 | } 696 | 697 | # Safe random 698 | rand_b64() { 699 | if command -v openssl >/dev/null 2>&1; then 700 | openssl rand -base64 16 | tr -d '\n\r' 701 | else 702 | head -c 16 /dev/urandom | base64 | tr -d '\n\r' 703 | fi 704 | } 705 | 706 | # Generate UUID 707 | rand_uuid() { 708 | if [ -f /proc/sys/kernel/random/uuid ]; then 709 | cat /proc/sys/kernel/random/uuid 710 | else 711 | openssl rand -hex 16 | sed 's/\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)/\1\2\3\4-\5\6-\7\8-\9\10-\11\12\13\14\15\16/' 712 | fi 713 | } 714 | 715 | # URL-encode minimal (for userinfo like "method:password") 716 | url_encode_min() { 717 | local s="$1" 718 | printf "%s" "$s" | sed -e 's/%/%25/g' \ 719 | -e 's/:/%3A/g' \ 720 | -e 's/+/%2B/g' \ 721 | -e 's/\//%2F/g' \ 722 | -e 's/=/\%3D/g' 723 | } 724 | 725 | # read JSON fields from config using jq 726 | read_config_fields() { 727 | if [ ! -f "$CONFIG_PATH" ]; then 728 | err "未找到配置文件: $CONFIG_PATH" 729 | return 1 730 | fi 731 | 732 | # Shadowsocks 733 | SS_PORT=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 734 | SS_PSK=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .password // empty' "$CONFIG_PATH" | head -n1 || true) 735 | SS_METHOD=$(jq -r '.inbounds[] | select(.type=="shadowsocks") | .method // empty' "$CONFIG_PATH" | head -n1 || true) 736 | 737 | # Hysteria2 738 | HY2_PORT=$(jq -r '.inbounds[] | select(.type=="hysteria2") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 739 | HY2_PSK=$(jq -r '.inbounds[] | select(.type=="hysteria2") | .users[0].password // empty' "$CONFIG_PATH" | head -n1 || true) 740 | 741 | # TUIC 742 | TUIC_PORT=$(jq -r '.inbounds[] | select(.type=="tuic") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 743 | TUIC_UUID=$(jq -r '.inbounds[] | select(.type=="tuic") | .users[0].uuid // empty' "$CONFIG_PATH" | head -n1 || true) 744 | TUIC_PSK=$(jq -r '.inbounds[] | select(.type=="tuic") | .users[0].password // empty' "$CONFIG_PATH" | head -n1 || true) 745 | 746 | # VLESS / Reality 747 | REALITY_PORT=$(jq -r '.inbounds[] | select(.type=="vless") | .listen_port // empty' "$CONFIG_PATH" | head -n1 || true) 748 | REALITY_UUID=$(jq -r '.inbounds[] | select(.type=="vless") | .users[0].uuid // empty' "$CONFIG_PATH" | head -n1 || true) 749 | REALITY_PK=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.private_key // empty' "$CONFIG_PATH" | head -n1 || true) 750 | REALITY_SID=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.short_id[0] // empty' "$CONFIG_PATH" | head -n1 || true) 751 | 752 | # fallback defaults 753 | SS_PORT="${SS_PORT:-}" 754 | SS_PSK="${SS_PSK:-}" 755 | SS_METHOD="${SS_METHOD:-}" 756 | HY2_PORT="${HY2_PORT:-}" 757 | HY2_PSK="${HY2_PSK:-}" 758 | TUIC_PORT="${TUIC_PORT:-}" 759 | TUIC_UUID="${TUIC_UUID:-}" 760 | TUIC_PSK="${TUIC_PSK:-}" 761 | REALITY_PORT="${REALITY_PORT:-}" 762 | REALITY_UUID="${REALITY_UUID:-}" 763 | REALITY_PK="${REALITY_PK:-}" 764 | REALITY_SID="${REALITY_SID:-}" 765 | } 766 | 767 | # get public IP (tries multiple endpoints) 768 | get_public_ip() { 769 | local ip="" 770 | for url in "https://api.ipify.org" "https://ipinfo.io/ip" "https://ifconfig.me" "https://icanhazip.com" "https://ipecho.net/plain"; do 771 | ip=$(curl -s --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]' || true) 772 | if [ -n "$ip" ]; then 773 | echo "$ip" 774 | return 0 775 | fi 776 | done 777 | return 1 778 | } 779 | 780 | # generate and save URIs 781 | generate_and_save_uris() { 782 | read_config_fields || return 1 783 | 784 | PUBLIC_IP=$(get_public_ip || true) 785 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 786 | 787 | # 读取文件内容作为节点后缀 788 | node_suffix=$(cat /root/node_names.txt 2>/dev/null || true) 789 | 790 | # SS: two formats: percent-encoded userinfo and base64 userinfo 791 | ss_userinfo="${SS_METHOD}:${SS_PSK}" 792 | ss_encoded=$(url_encode_min "$ss_userinfo") 793 | ss_b64=$(printf "%s" "$ss_userinfo" | base64 -w0 2>/dev/null || printf "%s" "$ss_userinfo" | base64 | tr -d '\n') 794 | hy2_encoded=$(url_encode_min "$HY2_PSK") 795 | tuic_encoded=$(url_encode_min "$TUIC_PSK") 796 | 797 | # reality pubkey read file or from config (fallback) 798 | if [ -f "$REALITY_PUB_FILE" ]; then 799 | REALITY_PUB=$(cat "$REALITY_PUB_FILE") 800 | else 801 | REALITY_PUB=$(jq -r '.inbounds[] | select(.type=="vless") | .tls.reality.public_key // empty' "$CONFIG_PATH" | head -n1 || true) 802 | REALITY_PUB="${REALITY_PUB:-UNKNOWN}" 803 | fi 804 | 805 | { 806 | echo "=== Shadowsocks (SS) ===" 807 | echo "ss://${ss_encoded}@${PUBLIC_IP}:${SS_PORT}#ss${node_suffix}" 808 | echo "ss://${ss_b64}@${PUBLIC_IP}:${SS_PORT}#ss${node_suffix}" 809 | echo "" 810 | echo "=== Hysteria2 (HY2) ===" 811 | echo "hy2://${hy2_encoded}@${PUBLIC_IP}:${HY2_PORT}/?sni=www.bing.com&alpn=h3&insecure=1#hy2${node_suffix}" 812 | echo "" 813 | echo "=== TUIC ===" 814 | echo "tuic://${TUIC_UUID}:${tuic_encoded}@${PUBLIC_IP}:${TUIC_PORT}/?congestion_control=bbr&alpn=h3&sni=www.bing.com&insecure=1#tuic${node_suffix}" 815 | echo "" 816 | echo "=== VLESS Reality ===" 817 | echo "vless://${REALITY_UUID}@${PUBLIC_IP}:${REALITY_PORT}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=${REALITY_PUB}&sid=${REALITY_SID}#reality${node_suffix}" 818 | } > "$URI_PATH" 819 | 820 | info "URI 已写入: $URI_PATH" 821 | } 822 | 823 | # view URIs (regenerate first) 824 | action_view_uri() { 825 | info "正在生成并显示 URI..." 826 | generate_and_save_uris || { err "生成 URI 失败"; return 1; } 827 | echo "" 828 | sed -n '1,200p' "$URI_PATH" || true 829 | } 830 | 831 | # view config path 832 | action_view_config() { 833 | echo "$CONFIG_PATH" 834 | } 835 | 836 | # edit config: use EDITOR or fallback 837 | action_edit_config() { 838 | if [ ! -f "$CONFIG_PATH" ]; then 839 | err "配置文件不存在: $CONFIG_PATH" 840 | return 1 841 | fi 842 | 843 | if command -v nano >/dev/null 2>&1; then 844 | ${EDITOR:-nano} "$CONFIG_PATH" 845 | else 846 | ${EDITOR:-vi} "$CONFIG_PATH" 847 | fi 848 | 849 | # check with sing-box if available 850 | if command -v sing-box >/dev/null 2>&1; then 851 | if sing-box check -c "$CONFIG_PATH" >/dev/null 2>&1; then 852 | info "配置校验通过,尝试重启服务" 853 | service_restart || warn "重启失败" 854 | generate_and_save_uris || true 855 | else 856 | warn "配置校验失败,服务未重启" 857 | fi 858 | else 859 | warn "未检测到 sing-box,可跳过校验" 860 | fi 861 | } 862 | 863 | # Reset SS based on current config 864 | action_reset_ss() { 865 | read_config_fields || return 1 866 | 867 | read -p "输入新的 SS 端口(回车保持 $SS_PORT): " new_ss_port 868 | [ -z "$new_ss_port" ] && new_ss_port="$SS_PORT" 869 | 870 | read -p "输入新的 SS 密码(回车随机生成): " new_ss_psk 871 | [ -z "$new_ss_psk" ] && new_ss_psk=$(rand_b64) 872 | 873 | info "正在停止服务..." 874 | service_stop || warn "停止服务失败" 875 | 876 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 877 | 878 | jq --argjson port "$new_ss_port" --arg psk "$new_ss_psk" ' 879 | .inbounds |= map( 880 | if .type=="shadowsocks" then 881 | .listen_port = $port | 882 | .password = $psk 883 | else . 884 | end 885 | ) 886 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 887 | 888 | info "已更新 SS 端口($new_ss_port)与密码(隐藏),正在启动服务..." 889 | service_start || warn "启动服务失败" 890 | sleep 1 891 | generate_and_save_uris || warn "生成 URI 失败" 892 | } 893 | 894 | # Reset HY2 based on current config 895 | action_reset_hy2() { 896 | read_config_fields || return 1 897 | 898 | read -p "输入新的 HY2 端口(回车保持 $HY2_PORT): " new_hy2_port 899 | [ -z "$new_hy2_port" ] && new_hy2_port="$HY2_PORT" 900 | 901 | read -p "输入新的 HY2 密码(回车随机生成): " new_hy2_psk 902 | [ -z "$new_hy2_psk" ] && new_hy2_psk=$(rand_b64) 903 | 904 | info "正在停止服务..." 905 | service_stop || warn "停止服务失败" 906 | 907 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 908 | 909 | jq --argjson port "$new_hy2_port" --arg psk "$new_hy2_psk" ' 910 | .inbounds |= map( 911 | if .type=="hysteria2" then 912 | .listen_port = $port | 913 | (.users[0].password) = $psk 914 | else . 915 | end 916 | ) 917 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 918 | 919 | info "已更新 HY2 端口($new_hy2_port)与密码(隐藏),正在启动服务..." 920 | service_start || warn "启动服务失败" 921 | sleep 1 922 | generate_and_save_uris || warn "生成 URI 失败" 923 | } 924 | 925 | # Reset TUIC based on current config 926 | action_reset_tuic() { 927 | read_config_fields || return 1 928 | 929 | read -p "输入新的 TUIC 端口(回车保持 $TUIC_PORT): " new_tuic_port 930 | [ -z "$new_tuic_port" ] && new_tuic_port="$TUIC_PORT" 931 | 932 | read -p "输入新的 TUIC UUID(回车随机生成): " new_tuic_uuid 933 | [ -z "$new_tuic_uuid" ] && new_tuic_uuid=$(rand_uuid) 934 | 935 | read -p "输入新的 TUIC 密码(回车随机生成): " new_tuic_psk 936 | [ -z "$new_tuic_psk" ] && new_tuic_psk=$(rand_b64) 937 | 938 | info "正在停止服务..." 939 | service_stop || warn "停止服务失败" 940 | 941 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 942 | 943 | jq --argjson port "$new_tuic_port" --arg uuid "$new_tuic_uuid" --arg psk "$new_tuic_psk" ' 944 | .inbounds |= map( 945 | if .type=="tuic" then 946 | .listen_port = $port | 947 | (.users[0].uuid) = $uuid | 948 | (.users[0].password) = $psk 949 | else . 950 | end 951 | ) 952 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 953 | 954 | info "已更新 TUIC 端口($new_tuic_port)、UUID(隐藏)与密码(隐藏),正在启动服务..." 955 | service_start || warn "启动服务失败" 956 | sleep 1 957 | generate_and_save_uris || warn "生成 URI 失败" 958 | } 959 | 960 | # Reset Reality based on current config 961 | action_reset_reality() { 962 | read_config_fields || return 1 963 | 964 | read -p "输入新的 Reality 端口(回车保持 $REALITY_PORT): " new_reality_port 965 | [ -z "$new_reality_port" ] && new_reality_port="$REALITY_PORT" 966 | 967 | read -p "输入新的 Reality UUID(回车随机生成): " new_reality_uuid 968 | [ -z "$new_reality_uuid" ] && new_reality_uuid=$(rand_uuid) 969 | 970 | info "正在停止服务..." 971 | service_stop || warn "停止服务失败" 972 | 973 | cp "$CONFIG_PATH" "${CONFIG_PATH}.bak" 974 | 975 | jq --argjson port "$new_reality_port" --arg uuid "$new_reality_uuid" ' 976 | .inbounds |= map( 977 | if .type=="vless" then 978 | .listen_port = $port | 979 | (.users[0].uuid) = $uuid 980 | else . 981 | end 982 | ) 983 | ' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" && mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH" 984 | 985 | info "已更新 Reality 端口($new_reality_port)与 UUID(隐藏),正在启动服务..." 986 | service_start || warn "启动服务失败" 987 | sleep 1 988 | generate_and_save_uris || warn "生成 URI 失败" 989 | } 990 | 991 | # Update sing-box 992 | action_update() { 993 | info "开始更新 sing-box..." 994 | if [ "$OS" = "alpine" ]; then 995 | apk update || warn "apk update 失败" 996 | apk add --upgrade --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box || { 997 | warn "apk 更新失败,尝试官方安装脚本" 998 | bash <(curl -fsSL https://sing-box.app/install.sh) || { err "更新失败"; return 1; } 999 | } 1000 | else 1001 | bash <(curl -fsSL https://sing-box.app/install.sh) || { err "更新失败"; return 1; } 1002 | fi 1003 | 1004 | info "更新完成,尝试重启服务..." 1005 | if command -v sing-box >/dev/null 2>&1; then 1006 | NEW_VER=$(sing-box version 2>/dev/null | head -n1 || echo "unknown") 1007 | info "当前 sing-box 版本: $NEW_VER" 1008 | service_restart || warn "重启失败" 1009 | else 1010 | warn "更新后未检测到 sing-box 可执行文件" 1011 | fi 1012 | } 1013 | 1014 | # Uninstall sing-box 1015 | action_uninstall() { 1016 | read -p "确认卸载 sing-box?(y/N): " confirm 1017 | if [[ ! "$confirm" =~ ^[Yy]$ ]]; then 1018 | info "已取消卸载" 1019 | return 0 1020 | fi 1021 | 1022 | info "正在卸载 sing-box..." 1023 | service_stop || true 1024 | if [ "$OS" = "alpine" ]; then 1025 | rc-update del "$SERVICE_NAME" default >/dev/null 2>&1 || true 1026 | [ -f "/etc/init.d/$SERVICE_NAME" ] && rm -f "/etc/init.d/$SERVICE_NAME" 1027 | apk del sing-box >/dev/null 2>&1 || true 1028 | else 1029 | systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true 1030 | systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true 1031 | [ -f "/etc/systemd/system/$SERVICE_NAME.service" ] && rm -f "/etc/systemd/system/$SERVICE_NAME.service" 1032 | systemctl daemon-reload >/dev/null 2>&1 || true 1033 | fi 1034 | rm -rf /etc/sing-box /var/log/sing-box* /usr/local/bin/sb "$BIN_PATH" >/dev/null 2>&1 || true 1035 | rm -f /root/node_names.txt >/dev/null 2>&1 || true 1036 | info "卸载完成" 1037 | } 1038 | 1039 | # Generate relay script (SS out) 1040 | action_generate_relay_script() { 1041 | read_config_fields || return 1 1042 | 1043 | PUBLIC_IP=$(get_public_ip || true) 1044 | [ -z "$PUBLIC_IP" ] && PUBLIC_IP="YOUR_SERVER_IP" 1045 | 1046 | RELAY_SCRIPT_PATH="/tmp/relay-install.sh" 1047 | 1048 | info "正在生成线路机脚本: $RELAY_SCRIPT_PATH" 1049 | 1050 | cat > "$RELAY_SCRIPT_PATH" <<'RELAY_TEMPLATE' 1051 | #!/usr/bin/env bash 1052 | set -euo pipefail 1053 | 1054 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 1055 | err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } 1056 | 1057 | if [ "$(id -u)" != "0" ]; then err "必须以 root 运行"; exit 1; fi 1058 | 1059 | detect_os(){ 1060 | . /etc/os-release 2>/dev/null || true 1061 | case "$ID" in 1062 | alpine) OS=alpine ;; 1063 | debian|ubuntu) OS=debian ;; 1064 | centos|rhel|fedora) OS=redhat ;; 1065 | *) OS=unknown ;; 1066 | esac 1067 | } 1068 | detect_os 1069 | 1070 | install_deps(){ 1071 | case "$OS" in 1072 | alpine) apk update; apk add --no-cache curl jq bash openssl ca-certificates ;; 1073 | debian) apt-get update -y; apt-get install -y curl jq bash openssl ca-certificates ;; 1074 | redhat) yum install -y curl jq bash openssl ca-certificates ;; 1075 | esac 1076 | } 1077 | install_deps 1078 | 1079 | install_singbox(){ 1080 | case "$OS" in 1081 | alpine) apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community sing-box ;; 1082 | *) bash <(curl -fsSL https://sing-box.app/install.sh) ;; 1083 | esac 1084 | } 1085 | install_singbox 1086 | 1087 | UUID=$(cat /proc/sys/kernel/random/uuid) 1088 | 1089 | info "生成 Reality 密钥对" 1090 | REALITY_KEYS=$(sing-box generate reality-keypair 2>/dev/null || true) 1091 | REALITY_PK=$(echo "$REALITY_KEYS" | grep "PrivateKey" | awk '{print $NF}' || true) 1092 | REALITY_PUB=$(echo "$REALITY_KEYS" | grep "PublicKey" | awk '{print $NF}' || true) 1093 | REALITY_SID=$(sing-box generate rand 8 --hex 2>/dev/null || echo "") 1094 | info "Reality PK: $REALITY_PK" 1095 | info "Reality PUB: $REALITY_PUB" 1096 | info "Reality SID: $REALITY_SID" 1097 | 1098 | read -p "输入线路机监听端口(留空随机 20000-65000): " USER_PORT 1099 | if [ -z "$USER_PORT" ]; then 1100 | LISTEN_PORT=$(shuf -i 20000-65000 -n 1 2>/dev/null || echo $((RANDOM % 45001 + 20000))) 1101 | else 1102 | LISTEN_PORT="$USER_PORT" 1103 | fi 1104 | 1105 | mkdir -p /etc/sing-box 1106 | 1107 | cat > /etc/sing-box/config.json < /etc/init.d/sing-box << 'SVC' 1154 | #!/sbin/openrc-run 1155 | name="sing-box" 1156 | description="SingBox service" 1157 | 1158 | command="/usr/bin/sing-box" 1159 | command_args="run -c /etc/sing-box/config.json" 1160 | command_background="yes" 1161 | pidfile="/run/sing-box.pid" 1162 | 1163 | depend() { 1164 | need net 1165 | } 1166 | SVC 1167 | chmod +x /etc/init.d/sing-box 1168 | rc-update add sing-box default 1169 | rc-service sing-box restart 1170 | else 1171 | cat > /etc/systemd/system/sing-box.service << 'SYSTEMD' 1172 | [Unit] 1173 | Description=Sing-box Relay 1174 | After=network.target 1175 | [Service] 1176 | ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json 1177 | Restart=on-failure 1178 | [Install] 1179 | WantedBy=multi-user.target 1180 | SYSTEMD 1181 | systemctl daemon-reload 1182 | systemctl enable sing-box 1183 | systemctl restart sing-box 1184 | fi 1185 | 1186 | PUB_IP=$(curl -s https://api.ipify.org || echo "YOUR_RELAY_IP") 1187 | 1188 | echo "" 1189 | info "✅ 安装完成" 1190 | echo "===================== 中转节点 Reality 链接 =====================" 1191 | echo "vless://$UUID@$PUB_IP:$LISTEN_PORT?encryption=none&flow=xtls-rprx-vision&security=reality&sni=addons.mozilla.org&fp=chrome&pbk=$REALITY_PUB&sid=$REALITY_SID#relay" 1192 | echo "==================================================================" 1193 | echo "" 1194 | 1195 | RELAY_TEMPLATE 1196 | 1197 | sed -i "s|__INBOUND_IP__|$PUBLIC_IP|g" "$RELAY_SCRIPT_PATH" 1198 | sed -i "s|__INBOUND_PORT__|$SS_PORT|g" "$RELAY_SCRIPT_PATH" 1199 | sed -i "s|__INBOUND_METHOD__|$SS_METHOD|g" "$RELAY_SCRIPT_PATH" 1200 | sed -i "s|__INBOUND_PASSWORD__|$SS_PSK|g" "$RELAY_SCRIPT_PATH" 1201 | 1202 | chmod +x "$RELAY_SCRIPT_PATH" 1203 | 1204 | info "✅ 线路机脚本已生成:$RELAY_SCRIPT_PATH" 1205 | echo "" 1206 | info "请手动复制以下内容到线路机,保存为 /tmp/relay-install.sh,并执行:chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 1207 | echo "------------------------------------------" 1208 | cat "$RELAY_SCRIPT_PATH" 1209 | echo "------------------------------------------" 1210 | echo "" 1211 | info "在线路机执行命令示例:" 1212 | echo " nano /tmp/relay-install.sh 保存后执行" 1213 | echo " chmod +x /tmp/relay-install.sh && bash /tmp/relay-install.sh" 1214 | echo "" 1215 | info "复制完成后,即可在线路机完成 sing-box 中转节点部署。" 1216 | } 1217 | 1218 | # Main menu 1219 | while true; do 1220 | cat <<'MENU' 1221 | 1222 | ========================== 1223 | Sing-box 管理面板 (快捷指令sb) 1224 | ========================== 1225 | 1) 查看协议链接 (SS/HY2/TUIC/Reality) 1226 | 2) 查看配置文件路径 1227 | 3) 编辑配置文件 1228 | 4) 重置 SS 端口/密码 1229 | 5) 重置 HY2 端口/密码 1230 | 6) 重置 TUIC 端口/UUID/密码 1231 | 7) 重置 Reality 端口/UUID 1232 | 8) 启动服务 1233 | 9) 停止服务 1234 | 10) 重启服务 1235 | 11) 查看状态 1236 | 12) 更新 sing-box 1237 | 13) 生成线路机出口脚本 (SS出站) 1238 | 14) 卸载 sing-box 1239 | 0) 退出 1240 | ========================== 1241 | MENU 1242 | 1243 | read -p "请输入选项: " opt 1244 | case "${opt:-}" in 1245 | 1) action_view_uri ;; 1246 | 2) action_view_config ;; 1247 | 3) action_edit_config ;; 1248 | 4) action_reset_ss ;; 1249 | 5) action_reset_hy2 ;; 1250 | 6) action_reset_tuic ;; 1251 | 7) action_reset_reality ;; 1252 | 8) service_start && info "已发送启动命令" ;; 1253 | 9) service_stop && info "已发送停止命令" ;; 1254 | 10) service_restart && info "已发送重启命令" ;; 1255 | 11) service_status ;; 1256 | 12) action_update ;; 1257 | 13) action_generate_relay_script ;; 1258 | 14) action_uninstall; exit 0 ;; 1259 | 0) exit 0 ;; 1260 | *) warn "无效选项" ;; 1261 | esac 1262 | 1263 | echo "" 1264 | done 1265 | SB_SCRIPT 1266 | 1267 | chmod +x "$SB_PATH" || warn "无法设置 $SB_PATH 为可执行" 1268 | 1269 | info "快捷指令已创建:可输入 sb 运行管理面板" 1270 | 1271 | # end of script 1272 | --------------------------------------------------------------------------------