Skip to content

Commit 0030c04

Browse files
author
anonymous
committed
feat: 管理面板安全加固与并发优化
- 多会话/多实例:session token 落库校验,避免互相挤掉/频繁 401 - 登录限速:按 IP 失败计数,返回 429 + Retry-After - CORS 收敛:默认不对 /api 开放,按 CORS_ALLOWED_ORIGINS 白名单放行 - Slowloris 防护:自定义 http.Server 超时与 MaxHeaderBytes - 默认密码改造:ADMIN_PASSWORD 托管或首次登录强制初始化;env 模式禁用面板改密 - 批量导入优化:单事务+一次性分配 sort/port,新增节点自动生成入站用户名密码 - 供应链校验:构建/CI 下载 sing-box 后做 sha256 校验 - 版本更新与文档对齐
1 parent 01a484b commit 0030c04

16 files changed

Lines changed: 923 additions & 185 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,12 @@ jobs:
185185

186186
- name: Download sing-box
187187
run: |
188-
curl -fLo sing-box.tar.gz "https://github.com/SagerNet/sing-box/releases/download/v${{ env.SINGBOX_VERSION }}/sing-box-${{ env.SINGBOX_VERSION }}-linux-amd64.tar.gz"
189-
tar xzf sing-box.tar.gz
188+
ASSET="sing-box-${{ env.SINGBOX_VERSION }}-linux-amd64.tar.gz"
189+
curl -fLo "$ASSET" "https://github.com/SagerNet/sing-box/releases/download/v${{ env.SINGBOX_VERSION }}/$ASSET"
190+
DIGEST="$(curl -fsSL "https://api.github.com/repos/SagerNet/sing-box/releases/tags/v${{ env.SINGBOX_VERSION }}" | jq -r --arg asset "$ASSET" '.assets[] | select(.name==$asset) | .digest')"
191+
test -n "$DIGEST" && test "$DIGEST" != "null"
192+
echo "${DIGEST#sha256:} $ASSET" | sha256sum -c -
193+
tar xzf "$ASSET"
190194
mv sing-box-*/sing-box .
191195
192196
- name: Create release package
@@ -202,7 +206,6 @@ jobs:
202206
export PATH="$PWD:$PATH"
203207
export CONFIG_DIR="$PWD/config"
204208
export PORT="${PORT:-30000}"
205-
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}"
206209
./main
207210
EOF
208211
chmod +x singbox-proxy-manager-linux-amd64/start.sh
@@ -258,7 +261,6 @@ jobs:
258261
export PATH="$PWD:$PATH"
259262
export CONFIG_DIR="$PWD/config"
260263
export PORT="${PORT:-30000}"
261-
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}"
262264
./main
263265
EOF
264266
chmod +x singbox-proxy-manager-freebsd-amd64/start.sh

.gitlab-ci.yml

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,11 @@ build-freebsd:
166166
cat > singbox-proxy-manager-freebsd-amd64/start.sh << 'EOF'
167167
#!/bin/sh
168168
cd "$(dirname "$0")"
169-
export PATH="$PWD:$PATH"
170-
export CONFIG_DIR="$PWD/config"
171-
export PORT="${PORT:-30000}"
172-
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}"
173-
./main
174-
EOF
169+
export PATH="$PWD:$PATH"
170+
export CONFIG_DIR="$PWD/config"
171+
export PORT="${PORT:-30000}"
172+
./main
173+
EOF
175174
- chmod +x singbox-proxy-manager-freebsd-amd64/start.sh
176175
- tar czf singbox-proxy-manager-freebsd-amd64.tar.gz singbox-proxy-manager-freebsd-amd64/
177176
artifacts:
@@ -195,11 +194,16 @@ build-linux:
195194
script:
196195
- go mod download
197196
- GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o main ./backend
198-
# 下载Linux版sing-box
199-
- apt-get update && apt-get install -y curl
200-
- curl -fLo sing-box.tar.gz "https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz"
201-
- tar xzf sing-box.tar.gz
202-
- mv sing-box-*/sing-box .
197+
# 下载Linux版sing-box
198+
- apt-get update && apt-get install -y --no-install-recommends ca-certificates curl jq
199+
- |
200+
ASSET="sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz"
201+
curl -fLo "$ASSET" "https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/$ASSET"
202+
DIGEST="$(curl -fsSL "https://api.github.com/repos/SagerNet/sing-box/releases/tags/v${SINGBOX_VERSION}" | jq -r --arg asset "$ASSET" '.assets[] | select(.name==$asset) | .digest')"
203+
test -n "$DIGEST" && test "$DIGEST" != "null"
204+
echo "${DIGEST#sha256:} $ASSET" | sha256sum -c -
205+
tar xzf "$ASSET"
206+
- mv sing-box-*/sing-box .
203207
# 创建发布包
204208
- mkdir -p singbox-proxy-manager-linux-amd64/frontend
205209
- mv main singbox-proxy-manager-linux-amd64/
@@ -211,12 +215,11 @@ build-linux:
211215
cat > singbox-proxy-manager-linux-amd64/start.sh << 'EOF'
212216
#!/bin/sh
213217
cd "$(dirname "$0")"
214-
export PATH="$PWD:$PATH"
215-
export CONFIG_DIR="$PWD/config"
216-
export PORT="${PORT:-30000}"
217-
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}"
218-
./main
219-
EOF
218+
export PATH="$PWD:$PATH"
219+
export CONFIG_DIR="$PWD/config"
220+
export PORT="${PORT:-30000}"
221+
./main
222+
EOF
220223
- chmod +x singbox-proxy-manager-linux-amd64/start.sh
221224
- tar czf singbox-proxy-manager-linux-amd64.tar.gz singbox-proxy-manager-linux-amd64/
222225
artifacts:

Dockerfile

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ ARG SINGBOX_VERSION=1.12.12
2020
RUN apt-get update && apt-get install -y --no-install-recommends \
2121
ca-certificates \
2222
curl \
23-
&& curl -Lo /tmp/sing-box.tar.gz "https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz" \
24-
&& tar -xzf /tmp/sing-box.tar.gz -C /tmp \
23+
jq \
24+
&& ASSET="sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz" \
25+
&& curl -fL -o "/tmp/${ASSET}" "https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/${ASSET}" \
26+
&& DIGEST="$(curl -fsSL "https://api.github.com/repos/SagerNet/sing-box/releases/tags/v${SINGBOX_VERSION}" | jq -r --arg asset "${ASSET}" '.assets[] | select(.name==$asset) | .digest')" \
27+
&& test -n "$DIGEST" && test "$DIGEST" != "null" \
28+
&& DIGEST="${DIGEST#sha256:}" \
29+
&& echo "${DIGEST} /tmp/${ASSET}" | sha256sum -c - \
30+
&& tar -xzf "/tmp/${ASSET}" -C /tmp \
2531
&& mv /tmp/sing-box-*/sing-box /usr/local/bin/ \
2632
&& chmod +x /usr/local/bin/sing-box \
2733
&& rm -rf /tmp/sing-box* \
@@ -35,7 +41,7 @@ RUN mkdir -p /app/config
3541

3642
ENV PORT=30000
3743
ENV CONFIG_DIR=/app/config
38-
ENV ADMIN_PASSWORD=admin123
44+
ENV ADMIN_PASSWORD=
3945

4046
EXPOSE 30000
4147
VOLUME ["/app/config"]

README.md

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<div align="center">
44

5-
![Version](https://img.shields.io/badge/version-1.1.2-blue.svg)
5+
![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)
66
![License](https://img.shields.io/badge/license-MIT-green.svg)
77
![SingBox](https://img.shields.io/badge/sing--box-1.12.12-orange.svg)
88

@@ -46,8 +46,9 @@
4646
git clone https://github.com/cheluen/singbox-proxy-manager.git
4747
cd singbox-proxy-manager
4848

49-
# 2. 修改默认密码(重要!)
50-
# 编辑 docker-compose.yml,修改 ADMIN_PASSWORD
49+
# 2. 可选:设置管理员密码(推荐)
50+
# 方式 A:通过环境变量 ADMIN_PASSWORD 指定固定管理员密码(面板内将无法修改)
51+
# 方式 B:不设置 ADMIN_PASSWORD,首次打开面板会要求设置管理员密码(可在面板内修改)
5152
nano docker-compose.yml
5253

5354
# 3. 启动服务
@@ -66,15 +67,17 @@ docker compose logs -f
6667
### 1. 登录系统
6768

6869
- 默认端口:`30000`
69-
- 默认密码:`admin123`**请立即修改!**
70+
- **不再提供默认密码**
71+
- 若设置 `ADMIN_PASSWORD` 且不为空:登录密码为该值;面板内无法修改管理员密码(需修改环境变量并重启服务)
72+
- 若未设置 `ADMIN_PASSWORD`:首次打开管理面板会要求先设置管理员密码(设置后可在面板内修改)
7073

7174
### 2. 添加节点
7275

7376
#### 方式一:单个添加
7477
1. 点击「添加节点」按钮
7578
2. 粘贴分享链接(支持 vless://、vmess://、hysteria2:// 等)
7679
3. 设置入站端口(留空自动分配)
77-
4. 设置认证用户名密码(可选
80+
4. 系统会为每个新节点自动生成入站用户名/密码(可在面板内单独或批量修改
7881

7982
#### 方式二:批量导入
8083
1. 点击「批量导入」
@@ -89,8 +92,8 @@ docker compose logs -f
8992
代理类型:HTTP 或 SOCKS5
9093
服务器:您的服务器IP
9194
端口:30001、30002、30003...(对应各节点)
92-
用户名:在管理界面设置的用户名
93-
密码:在管理界面设置的密码
95+
用户名:在管理界面查看/修改(新节点会自动生成)
96+
密码:在管理界面查看/修改(新节点会自动生成)
9497
```
9598

9699
### 4. 管理节点
@@ -113,7 +116,16 @@ docker compose logs -f
113116
environment:
114117
- PORT=30000 # 管理界面端口
115118
- CONFIG_DIR=/app/config # 配置文件目录
116-
- ADMIN_PASSWORD=admin123 # 管理密码(请修改!)
119+
- ADMIN_PASSWORD= # 可选:管理员密码(不为空则使用该值;面板内无法修改)
120+
- CORS_ALLOWED_ORIGINS= # 可选:管理 API 允许的跨域来源(逗号分隔;默认不启用 CORS)
121+
- LOGIN_RATE_LIMIT_WINDOW_SECONDS=60 # 可选:登录限速窗口(秒)
122+
- LOGIN_RATE_LIMIT_MAX_ATTEMPTS=10 # 可选:窗口内最大失败次数
123+
- LOGIN_RATE_LIMIT_BLOCK_SECONDS=600 # 可选:触发限速后的封禁时间(秒)
124+
- HTTP_READ_HEADER_TIMEOUT=5s # 可选:管理 API 读请求头超时
125+
- HTTP_READ_TIMEOUT=15s # 可选:读请求体超时
126+
- HTTP_WRITE_TIMEOUT=30s # 可选:写响应超时
127+
- HTTP_IDLE_TIMEOUT=60s # 可选:空闲连接超时
128+
- HTTP_MAX_HEADER_BYTES=1048576 # 可选:最大请求头大小(字节)
117129
- TURSO_DATABASE_URL=${TURSO_DATABASE_URL} # 可选:Turso 远程数据库 URL(需与 TURSO_AUTH_TOKEN 一起设置)
118130
- TURSO_AUTH_TOKEN=${TURSO_AUTH_TOKEN} # 可选:Turso 认证 Token(需与 TURSO_DATABASE_URL 一起设置)
119131
```
@@ -226,11 +238,9 @@ docker compose up -d
226238

227239
## 🔒 安全建议
228240

229-
1. **立即修改默认密码**
230-
```bash
231-
# 编辑 docker-compose.yml
232-
- ADMIN_PASSWORD=您的强密码
233-
```
241+
1. **设置强管理员密码**
242+
- 推荐:通过环境变量 `ADMIN_PASSWORD` 配置固定密码(面板内无法修改,需改环境变量并重启)
243+
- 或者:不设置 `ADMIN_PASSWORD`,首次打开面板时设置一个强密码(后续可在面板内修改)
234244

235245
2. **限制访问 IP**(可选)
236246
```bash

backend/api/auth_sessions.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package api
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/sha256"
6+
"crypto/subtle"
7+
"database/sql"
8+
"encoding/base64"
9+
"encoding/hex"
10+
"errors"
11+
"strings"
12+
"time"
13+
14+
"github.com/gin-gonic/gin"
15+
)
16+
17+
const adminSessionDuration = 24 * time.Hour
18+
19+
func normalizeAuthToken(headerValue string) string {
20+
token := strings.TrimSpace(headerValue)
21+
if token == "" {
22+
return ""
23+
}
24+
if strings.HasPrefix(strings.ToLower(token), "bearer ") {
25+
return strings.TrimSpace(token[7:])
26+
}
27+
return token
28+
}
29+
30+
func constantTimeEqual(expected string, actual string) bool {
31+
if len(expected) != len(actual) {
32+
return false
33+
}
34+
return subtle.ConstantTimeCompare([]byte(expected), []byte(actual)) == 1
35+
}
36+
37+
func hashSessionToken(token string) string {
38+
sum := sha256.Sum256([]byte(token))
39+
return hex.EncodeToString(sum[:])
40+
}
41+
42+
func (h *Handler) isValidAdminSession(token string) (bool, error) {
43+
if token == "" {
44+
return false, nil
45+
}
46+
47+
tokenHash := hashSessionToken(token)
48+
var expiresAt int64
49+
err := h.db.QueryRow("SELECT expires_at FROM admin_sessions WHERE token_hash = ? LIMIT 1", tokenHash).Scan(&expiresAt)
50+
switch err {
51+
case nil:
52+
if time.Now().Unix() > expiresAt {
53+
_, _ = h.db.Exec("DELETE FROM admin_sessions WHERE token_hash = ?", tokenHash)
54+
return false, nil
55+
}
56+
return true, nil
57+
case sql.ErrNoRows:
58+
return false, nil
59+
default:
60+
return false, err
61+
}
62+
}
63+
64+
func (h *Handler) createAdminSession(c *gin.Context) (string, time.Time, error) {
65+
expiry := time.Now().Add(adminSessionDuration)
66+
userAgent := ""
67+
ip := ""
68+
if c != nil && c.Request != nil {
69+
userAgent = c.Request.UserAgent()
70+
ip = c.ClientIP()
71+
}
72+
73+
for i := 0; i < 3; i++ {
74+
tokenBytes := make([]byte, 32)
75+
if _, err := rand.Read(tokenBytes); err != nil {
76+
return "", time.Time{}, err
77+
}
78+
token := base64.URLEncoding.EncodeToString(tokenBytes)
79+
tokenHash := hashSessionToken(token)
80+
81+
if _, err := h.db.Exec(
82+
"INSERT INTO admin_sessions (token_hash, expires_at, user_agent, ip) VALUES (?, ?, ?, ?)",
83+
tokenHash,
84+
expiry.Unix(),
85+
userAgent,
86+
ip,
87+
); err != nil {
88+
continue
89+
}
90+
return token, expiry, nil
91+
}
92+
93+
return "", time.Time{}, errors.New("failed to create session token")
94+
}

backend/api/credentials.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package api
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/base64"
6+
)
7+
8+
func generateRandomString(length int) (string, error) {
9+
if length <= 0 {
10+
return "", nil
11+
}
12+
13+
// RawURLEncoding expands by 4/3, round up enough bytes.
14+
byteLen := (length*3)/4 + 2
15+
b := make([]byte, byteLen)
16+
if _, err := rand.Read(b); err != nil {
17+
return "", err
18+
}
19+
s := base64.RawURLEncoding.EncodeToString(b)
20+
if len(s) < length {
21+
// Extremely unlikely, but keep correctness.
22+
return s, nil
23+
}
24+
return s[:length], nil
25+
}

0 commit comments

Comments
 (0)