├── 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 |
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 |
--------------------------------------------------------------------------------