Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,37 @@ jobs:
echo "db214c7a2517e63c150d123178d16d102e03a241ccdae4e5e07ffbe9cf56c6f9 $RUNNER_TEMP/mihomo.gz" | sha256sum --check
gzip --decompress "$RUNNER_TEMP/mihomo.gz"
chmod 755 "$RUNNER_TEMP/mihomo"
MIHOMO_BIN="$RUNNER_TEMP/mihomo" python3 -m unittest discover -s tests -p 'test_mihomo_config.py' -v
MIHOMO_BIN="$RUNNER_TEMP/mihomo" python3 -m unittest discover -s tests -p 'test_aggregate.py' -v
mkdir -p "$RUNNER_TEMP/mihomo-data"
for pair in geoip.dat:geoip.dat geosite.dat:geosite.dat GeoLite2-ASN.mmdb:ASN.mmdb country.mmdb:Country.mmdb; do
source_name="${pair%%:*}"
target_name="${pair#*:}"
curl --fail --location --retry 3 --max-time 120 \
"https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/2df2f6281aa2d14d54b8a099b9326f63ba389fd4/$source_name" \
--output "$RUNNER_TEMP/mihomo-data/$target_name"
done
export MIHOMO_BIN="$RUNNER_TEMP/mihomo"
export MIHOMO_DATA_DIR="$RUNNER_TEMP/mihomo-data"
python3 -m unittest discover -s tests -p 'test_mihomo_config.py' -v
python3 -m unittest discover -s tests -p 'test_aggregate*.py' -v

subscription:
name: Validate private subscription Worker
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24'
cache: npm
cache-dependency-path: cloud/subscription/package-lock.json
- name: Install locked dependencies
working-directory: cloud/subscription
run: npm ci --ignore-scripts
- name: Test real workerd and local KV
working-directory: cloud/subscription
run: npm test
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ __pycache__/
# Local Graphify analysis cache (may contain absolute paths and derived metadata)
config/mcporter.json
graphify-out/

# Worker tools and local runtime state
node_modules/
.wrangler/
.dev.vars*

# Account-specific subscription deployment settings
/cloud/subscription/wrangler.jsonc
/cloud/subscription/endpoint.json
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ GCP 和 VPS 真正变化的只有服务器生命周期、连接方式与防火
| 配置 | 默认值 | 说明 |
|---|---|---|
| `CLIENT_CONFIG_ENABLE` | `true` | 设为 `false` 跳过此 profile 的客户端输出,保留状态与凭据;不操作服务器 |
| `CLIENT_TARGET` | `stash` | 生成 Stash 或 `mihomo` 对应字段 |
| `CLIENT_IDENTITY` | 自动选择已有 `mac`,否则第一个身份 | 两个客户端共享此身份;默认同时输出 Stash / Mihomo |
| `AI_STRICT_MODE` | `false` | 默认日常分流:AI 优先保护、国内默认直连;`true` 才将全部公网统一到 AI 线路 |
| `REALITY_PORT` | `443` | Reality 监听端口 |
| `REALITY_TARGET` / `REALITY_SNI` | `1.1.1.1:443` / 空 | Reality 目标与客户端 SNI |
Expand All @@ -100,7 +100,7 @@ GCP 和 VPS 真正变化的只有服务器生命周期、连接方式与防火
| `HY2_OBFS_ENABLE` | `false` | 可选 Salamander 混淆;开启后不再表现为标准 HTTP/3 |
| `HY2_ACME_ENABLE` | `false` | 可选 Cloudflare DNS-01 真实证书 |
| `ANYTLS_PORT` | 随机 | AnyTLS TCP 端口 |
| `DEVICES` | `mac iphone` | 每设备独立 Reality/HY2 凭据及 YAML;AnyTLS 为 profile 共享密码,自动管理 |
| `DEVICES` | `mac iphone` | 服务器既有 Reality/HY2 账号清单,不再按设备输出 YAML;AnyTLS 为 profile 共享密码 |
| `PRIVACY_MODE` | `false` | 仅日常分流生效:国内组默认直连;`true` 则默认代理 |
| `CDN_ENABLE` | `false` | 可选 Cloudflare Tunnel 出口 |
| `CDN_ONLY` | `false` | 仅使用 Cloudflare WS,并关闭直连代理端口 |
Expand Down Expand Up @@ -129,14 +129,12 @@ GCP 和 VPS 真正变化的只有服务器生命周期、连接方式与防火

## 导入客户端

需要把多台服务器汇总为一份配置、敏感服务固定主备而普通海外自动选线时,使用 [多服务器汇总与日常分流](docs/aggregate-routing.md)。每个业务组保留默认策略和手动排查入口,完整节点列表只维护一处。
多服务器日常分流使用 [双客户端汇总与白名单分流](docs/aggregate-routing.md):默认同时生成 Stash / Mihomo 两份文件,AI 等敏感服务走指定主备,国内域名白名单直连,明确列出的普通海外服务走自动测速,其余 TCP/UDP 由敏感主备兜底。共享身份计划不区分 Mac/iPhone 输出,Stash 可显式发布到 iCloud;Mihomo 可发布到 [Cloudflare 私有 HTTPS 订阅](cloud/subscription/README.md),通过钥匙串令牌复制链接或一键导入。

部署成功后,每个平台默认得到两份名称明确的 YAML:

- `clash-configs/gcloud-mac.yaml`
- `clash-configs/gcloud-iphone.yaml`
- `clash-configs/<profile>-mac.yaml`
- `clash-configs/<profile>-iphone.yaml`
- `clash-configs/stash/<profile>.yaml`(例如 `gcloud.yaml`)
- `clash-configs/mihomo/<profile>.yaml`(Clash Verge)

生成器通过精确文件归属清单管理输出,不按前缀通配符删除。`cstone` 与 `cstone-next` 可以安全共存;
不同 profile 指定相同输出文件会报错。旧版本未登记的文件、移除设备后被手工修改的文件会保留。
Expand All @@ -147,8 +145,11 @@ GCP 和 VPS 真正变化的只有服务器生命周期、连接方式与防火
- Clash Verge:Settings → Profiles → Import
- 其他客户端:使用支持 Reality、Hysteria2 和 AnyTLS 的 Mihomo/Clash.Meta 兼容客户端

`CLIENT_TARGET=stash` 为默认目标,使用 Stash 的 HY2 `auth`、`up-speed/down-speed`、DNS `follow-rule` 和 STUN 协议规则;
`CLIENT_TARGET=mihomo` 生成 Mihomo 字段和 TUN 设置。两种目标共用源规则,但不能把同一份 YAML 视为两端字段完全等价。
默认 `node.py render --profile <profile>` 同时生成两种客户端;`--client stash|mihomo` 可单独更新一份,另一份保持原样。
Stash 使用 HY2 `auth`、`up-speed/down-speed`、DNS `follow-rule` 和 STUN 协议规则;Mihomo 使用对应字段和 TUN 设置。
`CLIENT_IDENTITY` 选定共享的既有凭据身份;留空时优先 `mac`,否则用 `DEVICES` 第一项。Mac/iPhone 共用同一份 Stash 文件;该身份名仅是历史账号键,不再是设备绑定。
历史 `CLIENT_TARGET` 不再控制生成份数,定向输出改用 `--client`。同时输出时 HY2 跳端口间隔须用整数,范围仅支持 `--client mihomo`。
旧设备文件保留供迁移,不再刷新;生成不修改 `DEVICES`、服务器账号或节点密码。两种目标共用源规则,但字段并不完全等价。
Stash 的节点域名独立解析功能需要 iOS/tvOS 3.6 或 macOS 4.3 及以上版本。

**默认使用日常分流:普通国内网站直连,AI 核心服务及已知依赖优先走 AI 线路。**
Expand Down Expand Up @@ -181,7 +182,7 @@ AI 入口全部不可用时,公网业务失败;国内业务 DNS 分支也被

`HY2_PORT_RANGE`、`HY2_OBFS_ENABLE`、`HY2_ACME_ENABLE` 均为可选增强:开启后需要重新部署服务端并重新生成客户端 YAML;默认关闭时不改变已有协议行为。

修改 `DEVICES` 后重跑同一个 profile 的入口,即可增加或撤销设备。
修改 `DEVICES` 后重跑同一个 profile 的部署入口,会增加或撤销相应的服务器身份;这不是客户端输出开关。共享配置后无法仅靠该身份区分或撤销单台设备。

自签名 HY2 证书在下次成功部署后自动回传 SHA256 指纹并写入 YAML;已有 profile 尚无指纹时保留原兼容行为。
ACME 证书使用可持久写入的 `/var/lib/hysteria/acme`。AnyTLS 的临时自签名证书与 HY2 ACME 相互独立,
Expand Down Expand Up @@ -274,7 +275,7 @@ Both entry points run the same shared pipeline:
2. Generate per-device credentials locally.
3. Provision or secure a reachable host.
4. Install BBR, Xray/Reality, Hysteria2, AnyTLS, systemd units, and security updates.
5. Recover the Reality public key and generate one Stash-first, Mihomo-compatible YAML per device.
5. Recover the Reality public key and generate separate Stash and Mihomo YAML files using a shared existing client identity.

The provider adapters only own host lifecycle, connectivity, and firewall behaviour. Key generation, server configuration, routing rules, optional Cloudflare setup, and client generation remain in `core/`.

Expand Down
28 changes: 25 additions & 3 deletions aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,40 @@
import argparse
from pathlib import Path
import sys
import subprocess

ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / 'core'))
from client_aggregate import generate
from client_aggregate import generate, publish_stash
from client_snapshot import import_snapshot


def main():
parser = argparse.ArgumentParser(description='汇总本地节点;不部署服务器、不显示凭据')
parser.add_argument('action', choices=('render', 'check'))
parser.add_argument('action', choices=('render', 'check', 'publish', 'import'))
parser.add_argument('--profile', required=True, help='保存 aggregate.json 的汇总 profile')
parser.add_argument('--client', choices=('stash', 'mihomo'))
parser.add_argument('--client', choices=('stash', 'mihomo', 'both'),
help='默认同时输出两种客户端,互不覆盖')
parser.add_argument('--icloud-dir', help='Stash 发布目录;默认使用 Stash iCloud Documents')
parser.add_argument('--source', help='首次迁移的本地完整客户端 YAML;不上传')
args = parser.parse_args()
if args.action == 'import':
if not args.source or args.client or args.icloud_dir:
parser.error('import 需要 --source,且不接受 --client/--icloud-dir')
return import_snapshot(ROOT, args.profile, args.source)
if args.source:
parser.error('--source 仅用于 import')
if args.action == 'publish':
if args.client == 'mihomo':
if args.icloud_dir:
parser.error('Mihomo 发布到私有 HTTPS,不接受 --icloud-dir')
return subprocess.call([sys.executable, str(ROOT / 'subscription.py'),
'publish', '--profile', args.profile])
if args.client == 'both':
parser.error('请明确分别发布 stash 或 mihomo,以便核对不同分发渠道')
return publish_stash(ROOT, args.profile, args.icloud_dir)
if args.icloud_dir:
parser.error('--icloud-dir 仅用于 publish')
return generate(ROOT, args.profile, target=args.client, check=args.action == 'check')


Expand Down
3 changes: 3 additions & 0 deletions cloud/subscription/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.wrangler/
.dev.vars*
55 changes: 55 additions & 0 deletions cloud/subscription/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 私有 Mihomo 订阅

使用 Workers Free 与私有 KV,通过 `<worker>.<account>.workers.dev` 提供订阅,无需个人域名。源配置仍在本地,云端仅存验证后的 Mihomo YAML。账户/KV 标识与端点保存在被 Git 忽略的 `wrangler.jsonc` 和 `endpoint.json`,不要把个人部署信息提交到公共仓库。

## 日常使用

在仓库根目录运行:

```bash
python3 aggregate.py render --profile routing
python3 aggregate.py publish --profile routing --client mihomo
python3 subscription.py verify --profile routing
python3 subscription.py copy-link --profile routing
python3 subscription.py import --profile routing
```

`import` 从钥匙串读取令牌,通过标准输入调用 macOS URL handler,使用 `clash-verge://install-config?url=...`。通用 `clash://` 可能被其他客户端注册,因此这里明确指定 Verge。copy-link 每次复制一个链接;日常仅需主配置。系统接受 URL 请求不等于客户端下载成功,必要时使用 copy-link 手动粘贴。已有同一订阅时无需重复导入。调用后客户端可能直接添加订阅;若客户端尚无当前配置,还可能选中它。工具不会主动开启 TUN 或系统代理。未执行导入前不能宣称客户端已经导入或运行通过。

单 VPS 文件从主配置同源生成,例如 `routing-cstone`、`routing-lax`、`routing-gcloud`。使用 `python3 subscription.py publish --profile routing --server gcloud` 发布,copy-link/import/verify 同样接受 --server。共享计划开启 standalone 后,render/check 同时处理所有文件。详见 [单服务器输出](../../docs/aggregate-routing.md#按主配置节点生成单服务器文件)。

`set-token` 首次生成 256-bit 随机令牌并保存在 macOS 钥匙串(服务名 `Network-Node subscriptions`);已有令牌会复用,不会无故失效旧链接。云端 secret 仅存 SHA256 校验值。完整订阅 URL 属于密码,不写普通文件、命令参数、日志或回复;剪贴板和客户端订阅数据库需要保密。若需撤销泄露令牌,应明确轮换钥匙串条目与云端校验值,并重新导入所有客户端;重新运行 set-token 本身不是轮换。

## 初始部署与维护

已有部署复用当前本地设置,不重复创建 namespace。新部署先确认自己的账户与免费套餐,在本目录执行:

```bash
npm ci
cp wrangler.example.jsonc wrangler.jsonc
cp endpoint.example.json endpoint.json
# 填写 account_id,删除模板中的 kv_namespaces 占位项;填好 endpoint.json
npm test
npx wrangler kv namespace create CONFIGS --binding CONFIGS --update-config
npm run check
npm run deploy
# 回到仓库根目录
python3 subscription.py set-token
python3 aggregate.py publish --profile routing --client mihomo
```

这里的配置文件使用 JSON 语法(Wrangler 接受 JSONC,但发布器按 JSON 读取),不要添加注释或尾随逗号。namespace 创建成功后必须保留返回的绑定 ID;不因后续步骤失败重复创建。

代码部署和 secret put 都是云端写操作。没有设置 TOKEN_SHA256 时所有请求均拒绝,初次部署到写入 secret 之间也不公开配置。Worker 仅支持 GET/HEAD、固定文件路径、单个格式有效的令牌;鉴权在 KV 读取前完成。无公开目录、上传接口或公共存储桶。应用不记录 URL/正文,关闭 Worker observability、日志与 traces,保留平台聚合指标;不承诺云平台完全没有底层访问元数据。

发布器检查本地成品与源一致,再上传固定字节快照;KV API 读回、HTTPS 内容对比、无令牌/错误令牌反向检查都通过才报告成功。验证下载使用 Verge User-Agent;本次默认 Python User-Agent 被 CF 边缘以 1010 拒绝,Verge 标识正常。HTTP 重定向一律拒绝,避免携带令牌跳到其他站点。响应为 private/no-store,建议 24 小时更新。

KV 是最终一致存储,更新后其他地区可能延迟 60 秒或更久。若 HTTPS 仍是旧版本,等待后运行 verify;不要反复上传。验证失败不会自动回滚已写入对象。当前网络下载成功不证明大陆无代理首连可达;必要时先导入本地 YAML。单次发布限制为 1 MiB;免费额度耗尽会失败,本工具不会升级套餐。

## 验证

- `npm test`:真实 workerd + 本地 KV,检查未授权、错误/重复令牌、令牌撤销、无 secret、未知对象、HEAD、禁止写入与内容一致性。
- 仓库 Python 测试含真实 Mihomo 隔离路由验证;`subscription.py verify` 验证实际云端成品。
- 本次验证主配置及单 VPS 成品的 Mihomo schema;未改变运行客户端,未实测 Claude 登录和长连接。

参考:[Verge URI 与响应头](https://www.clashverge.dev/guide/url_schemes.html)、[Verge 协议解析实现](https://github.com/clash-verge-rev/clash-verge-rev/blob/main/src-tauri/src/utils/resolve/scheme.rs)、[Workers 价格](https://developers.cloudflare.com/workers/platform/pricing/)、[KV 免费额度](https://developers.cloudflare.com/kv/platform/pricing/)、[KV 一致性](https://developers.cloudflare.com/kv/concepts/how-kv-works/)。
3 changes: 3 additions & 0 deletions cloud/subscription/endpoint.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"base_url": "https://network-node-subscriptions.your-account.workers.dev"
}
Loading
Loading