对 Sub-Store 订阅中的节点进行双栈探测,把可达节点的 server 替换为 Cloudflare 上分配的随机中性域名(一个节点对应一个域名),
再把替换后的订阅同步回 Sub-Store,客户端即可使用域名入站(隐藏真实 IP)。
- 数据库驱动(SQLite):节点身份、探测历史、域名映射全部持久化,幂等可恢复
- 增量探测:新节点 / 入站 IP 变化 / 超 24h 未探测时触发;ws、xhttp 等传输层跳过探测
- 默认每周一次全量探测(可配置)
- 可预留前端主域名(如 ridge.example.com),工具绝不会占用或改动其 DNS 记录
- 一键 Docker 部署:Sub-Store + 探测同步容器
.
├── docker-compose.yml # 一键部署(sub-store + node-domain)
├── Dockerfile # node-domain 镜像(内置 cron 定时)
├── entrypoint.sh # 容器入口:定时任务 + 启动探测
├── node_domain.py # 主工具(node 模式 + --swap 模式)
├── config.example.json # 配置示例(复制为 config.json 填写)
├── nodes.example.json # 节点清单示例(复制为 nodes.json)
├── requirements.txt
└── data/ # 运行时数据(数据库、输出订阅)
cp config.example.json config.json
cp nodes.example.json nodes.json
# 编辑 config.json:填入 Cloudflare 凭据、zone、订阅地址
# 编辑 docker-compose.yml:修改 SUB_STORE_PREFIX 为随机路径
docker compose up -d --build- Sub-Store 前端:
http://<服务器IP>:3000/ - 前端域名请在 Cloudflare 中开启代理(橙云),即可使用 CF 边缘证书,避免自签证书导致客户端 TLS 报错(zone 的 SSL 模式建议保持
Full) - 容器启动即执行一次探测 + 同步
- 之后每 6 小时增量探测、每周日 03:15 全量探测(可用环境变量覆盖 cron)
docker compose exec node-domain cat /app/data/node-domain-swapped.yaml # 替换后的订阅
docker compose logs -f node-domain # 运行日志Sub-Store 订阅导出端点为 /download/<订阅名>,会按客户端 UA 自动选择格式(mihomo/Clash 系返回 clashmeta、Surge 返回 surge、其他返回通用 base64)。
域名化订阅为 node-swapped,客户端直接导入:
https://你的域名/<SUB_STORE_PREFIX>/download/node-swapped?target=clashmeta
(去掉 ?target=clashmeta 则按客户端 UA 自动选择格式。)
开启 replace_originals: true 后,原始订阅自身也会被写回域名版:/download/subfree、/download/ikunkun、
/download/DMIT 等客户端原本使用的链接即可直接拿到域名入站(原始远程地址备份在 originals_file,可手动恢复)。
cd /srv/node-dns
python3 -m pip install -r requirements.txt # 或 apt install python3-yaml
# 预览(不写 Cloudflare)
python3 node_domain.py --config config.json --swap --json
# 实际写入 Cloudflare 并同步 Sub-Store
python3 node_domain.py --config config.json --swap --apply
# 强制全量
python3 node_domain.py --config config.json --swap --apply --full
# 普通节点域名映射模式(node_domain.py 原有功能)
python3 node_domain.py --config config.json --apply配合 systemd timer 或 cron 定时执行即可。
- 节点来源:默认自动发现 Sub-Store 中的全部订阅(排除输出订阅自身),数据源固定为
download/<订阅名>,与客户端看到的内容一致(含订阅自身 process 处理结果),重复运行幂等 - 写回原始订阅:
replace_originals: true时,每个订阅用自身节点渲染域名版并覆盖同名订阅,客户端仍用原download/<原名>链接 - 预留域名:
reserved_domains中的域名(如前端主域名)绝不分配、也绝不改其 DNS 记录;若旧数据误将其映射到节点,会自动释放并重新分配 - 域名跟随入站 IP:每次运行会抓取原始源(
originals_file备份的远程地址)检测节点入站 IP 变化,变化时复用原域名并把 CF 记录更新到新 IP,客户端域名保持稳定 - 可达性重探:已映射域名指向的 IP 按增量周期自动重探,入站失效会在日志标记(映射保留,源恢复或 IP 变化后自动更新)
- 幂等:server 已是域名(含已换过的域名)的节点不探测、不替换,保持原样
- 保留全部节点:同
(name+server+port+transport)的 UUID 变体不丢弃,探测去重、渲染复用同一域名 - 一节点一域名:按
(name+server+port+transport)指纹唯一映射,重复运行不重复分配域名 - ws/xhttp 传输层:跳过 TCP 探测,保留原
server不替换 - 不可达节点:保留原
server,超过增量周期自动重试 - 入站 IP 变化:上次可达且解析 IP 变化时立即重新探测并更新 A/AAAA 记录
- 幂等:探测结果与域名映射写入 SQLite,恢复后自动复用原域名
每日 2 次(北京时间 06:00 / 18:00)把 Sub-Store 状态提交到 VPS 本地 git 仓库 /srv/sub-store-backup,可随时 git log 回溯、git checkout 恢复。
# 1. 上传脚本与 systemd 单元
cp deploy/substore-backup/substore-backup.sh /usr/local/bin/substore-backup.sh
cp deploy/substore-backup/substore-backup.service /etc/systemd/system/
cp deploy/substore-backup/substore-backup.timer /etc/systemd/system/
chmod +x /usr/local/bin/substore-backup.sh
# 2. 首次运行(自动初始化仓库并提交首个快照)
/usr/local/bin/substore-backup.sh
# 3. 启用定时任务
systemctl daemon-reload
systemctl enable --now substore-backup.timer- 备份内容:
sub-store.json(订阅/集合/设置)、root.json、node-domain-swapped.yaml/json - 无变更时跳过提交;运行日志在
/var/log/substore-backup.log - 仓库仅存于 VPS 本地;如需异地备份(私有 GitHub 仓库),提供 token 后在脚本里加
git remote+git push即可
config.json、nodes.json含敏感信息(CF 凭据、真实节点),已加入.gitignore,请勿提交- 建议将 CF 凭据放入环境变量或密钥管理
{ "cf_api_email": "your@email.com", // Cloudflare 账号 "cf_api_key": "your-api-token", // Cloudflare API Token "cf_zone": "example.com", // 托管域名 "probe_db": "data/probe.db", // SQLite 数据库路径 "probe_strategy": "auto", // auto | incremental | full "probe_full_interval_days": 7, // 全量探测间隔(默认 7 天) "probe_incremental_interval_hours": 24, // 增量探测间隔 "probe_skip_transports": ["ws", "xhttp"], // 跳过 TCP 探测的传输层 "reserved_domains": ["ridge.example.com"], // 预留域名:绝不分配/修改(如 Sub-Store 前端主域名) "swap": { "subscription_urls": [], // 可选:额外的原始订阅 URL "substore_subs": [], // 可选:显式指定 Sub-Store 订阅名;省略则自动发现全部订阅(排除输出订阅自身) "replace_originals": false, // true:域名化内容直接写回原始同名订阅(download/<原名> 即域名版) "originals_file": "data/original_sources.json", // 原始订阅远程地址备份(覆盖前保存,仅用于恢复;不参与探测数据源) "output_file": "data/node-domain-swapped.yaml", "sync_mode": "api", // api(推荐)| file "sync_api_base": "http://sub-store:3000", // Sub-Store 后端地址 "sync_api_prefix": "/substore", // 与 SUB_STORE_BACKEND_PREFIX 一致 "sync_substore_sub": "node-swapped" // 写入的 Sub-Store 订阅名 } }