Skip to content

Commit ebc40a2

Browse files
authored
Merge pull request #54 from SubBoost/ryan/dev
release: SubBoost v2.6.0
2 parents f8837fa + 722cc33 commit ebc40a2

42 files changed

Lines changed: 1695 additions & 681 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<h1>SubBoost</h1>
55
<p>
66
<img src="https://img.shields.io/badge/platform-Linux%20%2B%20Docker-lightgrey.svg" alt="平台:Linux + Docker">
7-
<img src="https://img.shields.io/badge/version-2.5.1-green.svg" alt="版本 2.5.1">
7+
<img src="https://img.shields.io/badge/version-2.6.0-green.svg" alt="版本 2.6.0">
88
<a href="https://subboost.org"><img src="https://img.shields.io/badge/app-subboost.org-brightgreen.svg" alt="在线入口"></a>
99
<a href="https://docs.subboost.org"><img src="https://img.shields.io/badge/docs-subboost.org-blue.svg" alt="文档"></a>
1010
<img src="https://img.shields.io/badge/image-GHCR-blue.svg" alt="GHCR 镜像">

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<h1>SubBoost</h1>
55
<p>
66
<img src="https://img.shields.io/badge/platform-Linux%20%2B%20Docker-lightgrey.svg" alt="Platform: Linux + Docker">
7-
<img src="https://img.shields.io/badge/version-2.5.1-green.svg" alt="Version 2.5.1">
7+
<img src="https://img.shields.io/badge/version-2.6.0-green.svg" alt="Version 2.6.0">
88
<a href="https://subboost.org"><img src="https://img.shields.io/badge/app-subboost.org-brightgreen.svg" alt="Online app"></a>
99
<a href="https://docs.subboost.org"><img src="https://img.shields.io/badge/docs-subboost.org-blue.svg" alt="Documentation"></a>
1010
<img src="https://img.shields.io/badge/image-GHCR-blue.svg" alt="GHCR image">

docs/release-notes.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
1-
# SubBoost v2.5.1
1+
# SubBoost v2.6.0
22

33
## 中文
44

55
### 更新重点
66

7-
SubBoost v2.5.1 是一个修复版本,主要修复自定义代理组的默认成员逻辑,并提升自部署安装、更新时的版本来源稳定性。建议 v2.5.0 用户升级。
7+
SubBoost v2.6.0 改进了高级代理组的成员管理,并增强多种订阅格式和 ECH 参数的兼容性。自部署备份流程也更加可靠。建议 v2.5.1 用户升级。
88

99
### 主要变化
1010

11-
- 修复新建自定义代理组时默认成员过宽的问题,避免默认生成出“节点选择”和自定义组互相引用的循环配置
12-
- `🚀 节点选择` 默认不再自动包含自定义代理组,减少代理组套娃导致配置异常或预览卡顿的风险
13-
- 新建普通自定义代理组默认只包含 `DIRECT``REJECT` 和真实节点,保留高级模式里的手动配置能力
14-
- 改进自部署安装和 `subboost update` 的版本来源处理,稳定版安装资产会默认固定到对应 release 版本
15-
- 改进自部署管理脚本的更新来源,减少从指定版本更新时意外跟随其它发布通道的风险
11+
- 高级代理组现在可以一键添加或移除全部节点、全部代理组,并可恢复默认成员;批量添加代理组时会自动跳过可能形成循环引用的项目
12+
- 代理组成员区域会分别显示节点数和代理组数,代理组卡片的节点统计只计算真实节点,不再把 `DIRECT``REJECT` 或其它代理组引用计入节点数量
13+
- 改进 Clash/Mihomo YAML 导入兼容性,可识别以单行对象列表书写的节点配置,并容忍这类列表中常见的缩进不一致
14+
- 修复 VMess、VLESS、Trojan 和 AnyTLS 分享链接中的 ECH 查询服务器名称在转换时丢失的问题
15+
- 自部署备份现在会在数据库导出失败时安全终止,不再把不完整文件当作成功备份;备份保留数量可通过 `SUBBOOST_BACKUP_RETENTION_COUNT` 调整,默认仍保留 10 份
1616

1717
### 升级说明
1818

1919
- 建议升级前备份 `/opt/subboost/.env` 和数据库,方便需要时回滚。
20-
- 已安装 v2.5.0 的自部署实例可以继续使用 `subboost update` 更新。
21-
- 普通订阅转换、模板和规则功能不需要手动改环境变量
22-
- 已经手动配置过的高级代理组不会被自动改写;本次主要改变新建和默认生成行为
20+
- 现有 v2.5.1 自部署实例可以继续使用 `subboost update` 更新。
21+
- 本次升级不要求手动迁移数据库,也不要求新增环境变量;只有需要修改默认备份保留数量时,才需要设置 `SUBBOOST_BACKUP_RETENTION_COUNT`
22+
- 现有订阅、模板、规则和高级代理组配置保持兼容,不会因为升级自动改写成员选择
2323

2424
## English
2525

2626
### Highlights
2727

28-
SubBoost v2.5.1 is a patch release that fixes default custom proxy group membership and improves version-source stability for self-hosted install and update flows. v2.5.0 users are encouraged to upgrade.
28+
SubBoost v2.6.0 improves member management for advanced proxy groups and expands compatibility with several subscription formats and ECH parameters. It also makes self-hosted backups more reliable. Users on v2.5.1 are encouraged to upgrade.
2929

3030
### Main Changes
3131

32-
- Fixed overly broad default members for newly created custom proxy groups, preventing default configurations where node selection and custom groups reference each other in a loop.
33-
- `🚀 节点选择` no longer automatically includes custom proxy groups by default, reducing the risk of nested proxy group loops causing invalid output or preview slowdowns.
34-
- Newly created normal custom proxy groups now default to `DIRECT`, `REJECT`, and real nodes only, while manual advanced configuration remains available.
35-
- Improved self-hosted install and `subboost update` version-source handling so stable release assets stay pinned to the matching release version by default.
36-
- Improved the self-hosted manager update source to reduce the risk of a pinned-version update unexpectedly following another release channel.
32+
- Advanced proxy groups can now add or remove all nodes or all proxy groups in one action, and restore their default members. Bulk proxy-group additions automatically skip entries that could create circular references.
33+
- Proxy-group member sections now show separate node and proxy-group counts. Node totals on proxy-group cards count real nodes only, excluding `DIRECT`, `REJECT`, and references to other proxy groups.
34+
- Improved Clash/Mihomo YAML import compatibility for node configurations written as lists of single-line objects, including common indentation inconsistencies in those lists.
35+
- Fixed ECH query server names being lost when converting VMess, VLESS, Trojan, and AnyTLS share links.
36+
- Self-hosted backups now stop safely when the database export fails instead of treating an incomplete file as a successful backup. Backup retention can be adjusted with `SUBBOOST_BACKUP_RETENTION_COUNT`, while the default remains 10 backups.
3737

3838
### Upgrade Notes
3939

4040
- Back up `/opt/subboost/.env` and the database before upgrading so rollback is easier if needed.
41-
- Existing v2.5.0 self-hosted installations can continue to update with `subboost update`.
42-
- Normal subscription conversion, templates, and rules do not require manual environment-variable changes.
43-
- Manually configured advanced proxy groups are not rewritten automatically; this release mainly changes new and default generated behavior.
41+
- Existing v2.5.1 self-hosted installations can continue to update with `subboost update`.
42+
- This upgrade does not require a manual database migration or a new environment variable. Set `SUBBOOST_BACKUP_RETENTION_COUNT` only if you want to change the default backup retention count.
43+
- Existing subscriptions, templates, rules, and advanced proxy-group configurations remain compatible, and upgrading does not automatically rewrite member selections.

local/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@subboost/local",
3-
"version": "2.5.1",
3+
"version": "2.6.0",
44
"license": "AGPL-3.0-only",
55
"private": true,
66
"type": "module",

local/scripts/selfhost-shell.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ ENV
282282
expect(result.status).toBe(0);
283283
expect(result.stdout).toContain("健康检查: 正常");
284284
expect(result.stdout).not.toContain("健康检查: 异常");
285-
// wait_for_health stops once ready succeeds, then status_cmd performs one final live+ready check.
286-
expect(result.stdout).toContain("curl_count=7");
285+
// wait_for_health checks live once per attempt, then status_cmd performs one final live+ready check.
286+
expect(result.stdout).toContain("curl_count=8");
287287
}, 10_000);
288288

289289
it("uses refreshed release metadata before pulling during update", () => {

local/scripts/subboost.sh

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ set -Eeuo pipefail
33

44
DEFAULT_HOME="/opt/subboost"
55
DEFAULT_STABLE_RELEASE_URL="https://github.com/SubBoost/subboost/releases/latest/download/release.json"
6+
DEFAULT_BACKUP_RETENTION_COUNT="10"
67
SUBBOOST_HOME="${SUBBOOST_HOME:-$DEFAULT_HOME}"
78
ENV_FILE="$SUBBOOST_HOME/.env"
89
COMPOSE_FILE="$SUBBOOST_HOME/docker-compose.yml"
@@ -225,17 +226,25 @@ health_status_text() {
225226
}
226227

227228
health_status_code() {
228-
local port base
229+
local port base live_ok
229230
port="$(port_number "${SUBBOOST_PORT:-3000}")"
230231
base="http://127.0.0.1:$port"
231232
if ! command -v curl >/dev/null 2>&1; then
232233
printf 'curl-missing\n'
233-
elif curl -fsS "$base/api/health/live" >/dev/null 2>&1 && curl -fsS "$base/api/health/ready" >/dev/null 2>&1; then
234-
printf 'ok\n'
235-
elif curl -fsS "$base/api/health/live" >/dev/null 2>&1; then
236-
printf 'not-ready\n'
237234
else
238-
printf 'unhealthy\n'
235+
if curl -fsS "$base/api/health/live" >/dev/null 2>&1; then
236+
live_ok=1
237+
else
238+
live_ok=0
239+
fi
240+
241+
if [ "$live_ok" = "1" ] && curl -fsS "$base/api/health/ready" >/dev/null 2>&1; then
242+
printf 'ok\n'
243+
elif [ "$live_ok" = "1" ]; then
244+
printf 'not-ready\n'
245+
else
246+
printf 'unhealthy\n'
247+
fi
239248
fi
240249
}
241250

@@ -249,6 +258,7 @@ health_status_label() {
249258
}
250259

251260
wait_for_health() {
261+
# Default max wait is about 30 seconds: 15 attempts with a 2-second interval.
252262
local attempts="${SUBBOOST_DOCTOR_HEALTH_ATTEMPTS:-15}"
253263
local interval="${SUBBOOST_DOCTOR_HEALTH_INTERVAL_SECONDS:-2}"
254264
local index status
@@ -336,12 +346,25 @@ backup_cmd() {
336346
sudo_do mkdir -p "$BACKUP_DIR"
337347
local stamp db_tmp db_out env_out
338348
local -a sql_backups env_backups
339-
local i
349+
local -a backup_status
350+
local i backup_retention_count
351+
backup_retention_count="${SUBBOOST_BACKUP_RETENTION_COUNT:-$DEFAULT_BACKUP_RETENTION_COUNT}"
352+
if ! [[ "$backup_retention_count" =~ ^[0-9]+$ ]] || (( backup_retention_count < 1 )); then
353+
die "SUBBOOST_BACKUP_RETENTION_COUNT must be a positive integer"
354+
fi
340355
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
341356
db_tmp="$BACKUP_DIR/subboost-$stamp.sql.gz.partial"
342357
db_out="$BACKUP_DIR/subboost-$stamp.sql.gz"
343358
env_out="$BACKUP_DIR/subboost-$stamp.env"
359+
set +e
344360
compose exec -T db pg_dump -U "${POSTGRES_USER:-subboost}" -d "${POSTGRES_DB:-subboost}" | gzip -c | sudo_do tee "$db_tmp" >/dev/null
361+
backup_status=("${PIPESTATUS[@]}")
362+
set -e
363+
if (( backup_status[0] != 0 || backup_status[1] != 0 || backup_status[2] != 0 )); then
364+
sudo_do rm -f -- "$db_tmp"
365+
say "Backup failed: pg_dump=${backup_status[0]} gzip=${backup_status[1]} write=${backup_status[2]}"
366+
return 1
367+
fi
345368
sudo_do mv "$db_tmp" "$db_out"
346369
sudo_do install -m 600 "$ENV_FILE" "$env_out"
347370

@@ -350,10 +373,10 @@ backup_cmd() {
350373
env_backups=("$BACKUP_DIR"/subboost-*.env)
351374
shopt -u nullglob
352375

353-
for ((i = 0; i < ${#sql_backups[@]} - 10; i++)); do
376+
for ((i = 0; i < ${#sql_backups[@]} - backup_retention_count; i++)); do
354377
sudo_do rm -f -- "${sql_backups[$i]}"
355378
done
356-
for ((i = 0; i < ${#env_backups[@]} - 10; i++)); do
379+
for ((i = 0; i < ${#env_backups[@]} - backup_retention_count; i++)); do
357380
sudo_do rm -f -- "${env_backups[$i]}"
358381
done
359382
say "Backup written:"

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "subboost",
3-
"version": "2.5.1",
3+
"version": "2.6.0",
44
"license": "AGPL-3.0-only",
55
"private": true,
66
"engines": {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildMihomoEchOptsFromShareValue,
4+
isMihomoEchQueryServerName,
5+
isStandardBase64String,
6+
} from "./ech";
7+
8+
describe("Mihomo ECH helpers", () => {
9+
it("classifies share values without promoting arbitrary text", () => {
10+
expect(buildMihomoEchOptsFromShareValue(null)).toEqual({ enable: true });
11+
expect(buildMihomoEchOptsFromShareValue(" ")).toEqual({ enable: true });
12+
expect(buildMihomoEchOptsFromShareValue(" Y29uZmln ")).toEqual({ enable: true, config: "Y29uZmln" });
13+
expect(buildMihomoEchOptsFromShareValue(" cloudflare-ech.com ")).toEqual({
14+
enable: true,
15+
"query-server-name": "cloudflare-ech.com",
16+
});
17+
expect(buildMihomoEchOptsFromShareValue("not base64!")).toEqual({ enable: true });
18+
});
19+
20+
it("keeps Base64 and DNS-name validation bounded and conservative", () => {
21+
expect(isStandardBase64String("+w==")).toBe(true);
22+
expect(isStandardBase64String("dGVzdA")).toBe(false);
23+
expect(isMihomoEchQueryServerName("ech.example.com")).toBe(true);
24+
expect(isMihomoEchQueryServerName("ech.example.com.")).toBe(true);
25+
expect(isMihomoEchQueryServerName("1.1.1.1")).toBe(false);
26+
expect(isMihomoEchQueryServerName("single-label")).toBe(false);
27+
expect(isMihomoEchQueryServerName(`bad_label.${"a".repeat(64)}.example.com`)).toBe(false);
28+
expect(isMihomoEchQueryServerName(`${"a".repeat(100_000)}.example.com`)).toBe(false);
29+
});
30+
});

packages/core/src/mihomo/ech.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const STANDARD_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
2+
const DNS_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
3+
4+
export interface MihomoEchOpts {
5+
enable: true;
6+
config?: string;
7+
"query-server-name"?: string;
8+
}
9+
10+
export function isStandardBase64String(value: string): boolean {
11+
const trimmed = value.trim();
12+
return trimmed.length > 0 && trimmed.length % 4 === 0 && STANDARD_BASE64_PATTERN.test(trimmed);
13+
}
14+
15+
export function isMihomoEchQueryServerName(value: string): boolean {
16+
const trimmed = value.trim();
17+
const hostname = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
18+
if (!hostname || hostname.length > 253 || !hostname.includes(".") || !/[A-Za-z]/.test(hostname)) return false;
19+
20+
return hostname.split(".").every((label) => label.length <= 63 && DNS_LABEL_PATTERN.test(label));
21+
}
22+
23+
export function buildMihomoEchOptsFromShareValue(value: unknown): MihomoEchOpts {
24+
const normalized = typeof value === "string" ? value.trim() : "";
25+
if (!normalized) return { enable: true };
26+
if (isStandardBase64String(normalized)) return { enable: true, config: normalized };
27+
if (isMihomoEchQueryServerName(normalized)) {
28+
return { enable: true, "query-server-name": normalized };
29+
}
30+
return { enable: true };
31+
}

0 commit comments

Comments
 (0)