110 lines
11 KiB
Markdown
110 lines
11 KiB
Markdown
# 域名迁移 runbook —— `alotbuy.com` → `ai1net.com`(47 · 2026-09-19 建立)
|
||||
|
|
|
|||
|
|
> 本文件与同目录两个 `*.conf` 一起放在 47 的 `/www/server/panel/vhost/nginx/_pending-ai1net/`(**待启用**,nginx 只 include 该目录的 `*.conf` 一层,子目录天然不生效)。
|
|||
|
|
> 目标:把线上部署的 DSH 服务域名由 `alotbuy.com` 切到 `ai1net.com`(门户 + 实例子域 + 中继兜底入口 + 平台 env)。
|
|||
|
|
|
|||
|
|
## 0. 现状(2026-09-19 取证)
|
|||
|
|
|
|||
|
|
| 项 | 现状 |
|
|||
|
|
|---|---|
|
|||
|
|
| 门户 vhost | `/www/server/panel/vhost/nginx/alotbuy.com.conf`:`alotbuy.com www.alotbuy.com *.alotbuy.com` → 3080,`/dshs-relay` → 20080,证书 `live/alotbuy.com`(SAN `alotbuy.com` + `*.alotbuy.com`) |
|
|||
|
|
| 实例子域 | **`<user>.alotbuy.com`**(一级标签,由门户 vhost 的 `*.alotbuy.com` 承载,无需第二张证书) |
|
|||
|
|
| 旧名 301 | `dsh.alotbuy.com.conf`:`dsh.alotbuy.com` / `*.dsh.alotbuy.com` → 301 到 `alotbuy.com` / `<label>.alotbuy.com`,证书 `live/dsh.alotbuy.com` |
|
|||
|
|
| 中继兜底 | `relay-direct.conf`:`relay-direct.alotbuy.com`(443),两端点 `/dshs-relay` + `= /dshs-overlay/bootstrap`(Host 改写成 `alotbuy.com`) |
|
|||
|
|
| 平台 env | `/etc/dshs.env`:`DSHS_BASE_DOMAIN=alotbuy.com`、`DSHS_COOKIE_DOMAIN=.alotbuy.com`、`DSHS_PORT=3080` |
|
|||
|
|
| 中继引导种子 | `lib/config.js` 内置 `['https://alotbuy.com/dshs-relay']`;env 覆盖键 = **`DSHS_OVERLAY_BOOTSTRAP_SEEDS`**(`splitList`,可多值) |
|
|||
|
|
| 证书签发 | certbot 1.22.0 + **dns-cloudflare** 插件;凭据 `/etc/cloudflare.ini`(**令牌仅覆盖 alotbuy.com 一个 zone**) |
|
|||
|
|
|
|||
|
|
**ai1net.com 侧已就绪的部分**:DNS 已在 Cloudflare(`ai1net.com` / `www` / `*` / `<x>.dsh` 均解析到 CF)、CF→源站(47.77.182.89)链路已验证可达(用 80 端口探针文件经 CF 取回原文)、LE **http-01 路径可用**。
|
|||
|
|
|
|||
|
|
## 1. 🔴 唯一阻塞项(需用户侧提供)
|
|||
|
|
|
|||
|
|
**`*.ai1net.com` 通配证书必须走 DNS-01**(LE 对通配符只认 DNS-01),而 47 上现存的 CF 令牌**只覆盖 alotbuy.com**(实测 `/zones` 只返回 1 个 zone)⇒ 我无法为 ai1net.com 写 `_acme-challenge` TXT。
|
|||
|
|
|
|||
|
|
两条可选路径(任选其一):
|
|||
|
|
- **A. 给一份 ai1net.com 域的 CF API 令牌**(权限最小化:`Zone → DNS → Edit`,Zone Resources = 仅 `ai1net.com`)
|
|||
|
|
→ 落到 `/etc/cloudflare-ai1net.ini`(`dns_cloudflare_api_token = <令牌>`,mode 600)
|
|||
|
|
→ 我执行 S1:`certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/cloudflare-ai1net.ini --dns-cloudflare-propagation-seconds 60 -d ai1net.com -d '*.ai1net.com' --cert-name ai1net.com`
|
|||
|
|
- **B. 在 CF 面板生成 Origin CA 证书**(主机名填 `ai1net.com, *.ai1net.com`),把 cert + key 文本给我
|
|||
|
|
→ 落到 `/etc/ssl/ai1net/{fullchain.pem,privkey.pem}`,`ai1net.com.conf` 里两处证书路径改指它
|
|||
|
|
→ 无需给我任何 API 权限;代价是证书由 CF 侧管理、不参与 certbot 自动续期。
|
|||
|
|
|
|||
|
|
## 2. cutover 步骤(**证书就绪后**按序执行,每步带验收)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
V=/www/server/panel/vhost/nginx
|
|||
|
|
# S1 证书(见 §1;A 路径用 certbot,B 路径跳过)
|
|||
|
|
certbot certificates | grep -A3 ai1net.com
|
|||
|
|
|
|||
|
|
# S2 启用两份 vhost(⛔ 证书不存在时**不要**做这步 ⇒ nginx 起不来 = 门户全挂)
|
|||
|
|
cp -a $V/_pending-ai1net/ai1net.com.conf $V/ai1net.com.conf
|
|||
|
|
cp -a $V/_pending-ai1net/relay-direct.ai1net.com.conf $V/relay-direct.ai1net.com.conf
|
|||
|
|
nginx -t && nginx -s reload
|
|||
|
|
# 验收:curl -s --http1.1 -o /dev/null -w '%{http_code}\n' https://ai1net.com/portal.html ⇒ 200
|
|||
|
|
|
|||
|
|
# S3 平台 env 切换(备份 → 改两项 → 追加种子(加性,保留 alotbuy 作第二种子)→ 重启)
|
|||
|
|
cp -a /etc/dshs.env /etc/dshs.env.bak-dom-$(date +%Y%m%d-%H%M%S)
|
|||
|
|
sed -i 's/^DSHS_BASE_DOMAIN=.*/DSHS_BASE_DOMAIN=ai1net.com/' /etc/dshs.env
|
|||
|
|
sed -i 's/^DSHS_COOKIE_DOMAIN=.*/DSHS_COOKIE_DOMAIN=.ai1net.com/' /etc/dshs.env
|
|||
|
|
grep -q '^DSHS_OVERLAY_BOOTSTRAP_SEEDS=' /etc/dshs.env || \
|
|||
|
|
echo 'DSHS_OVERLAY_BOOTSTRAP_SEEDS=https://ai1net.com/dshs-relay,https://alotbuy.com/dshs-relay' >> /etc/dshs.env
|
|||
|
|
systemctl restart dshs
|
|||
|
|
# 验收:实例地址变成 <user>.ai1net.com 且能登录;中继仍在线(探针 29 PASS / 0 FAIL / 0 SKIP)
|
|||
|
|
|
|||
|
|
# S4 旧名连带项(**必须同批**,否则留半破状态)
|
|||
|
|
# S4a relay-direct.conf(alotbuy 版)的 Host 改写也必须指向新基底域:
|
|||
|
|
# proxy_set_header Host alotbuy.com; → ai1net.com
|
|||
|
|
# S4b dsh.alotbuy.com.conf(旧名 301)的 map 目标改指新域(⚠️ 只改两行,别碰 server_name/正则):
|
|||
|
|
# default alotbuy.com; → ai1net.com;
|
|||
|
|
# ~^(?<label>[^.]+)\.dsh\.alotbuy\.com$ $label.alotbuy.com; → $label.ai1net.com;
|
|||
|
|
nginx -t && nginx -s reload
|
|||
|
|
|
|||
|
|
# S5 worker 侧种子(可选,加性):/etc/dshs-worker.env 加同名多值 SEEDS,再 restart dshs-worker
|
|||
|
|
# ⚠️ 改前 `--scene all` 演练含观察窗口,预算 ≥8 min;⛔ 别套短超时
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. 回滚(任一步失败都能退回)
|
|||
|
|
|
|||
|
|
1. 删两份新 vhost ⇒ `nginx -t && nginx -s reload`(门户立即回到 alotbuy 版)。
|
|||
|
|
2. `cp -a /etc/dshs.env.bak-dom-<ts> /etc/dshs.env` ⇒ `systemctl restart dshs`(回到 `alotbuy.com` / `.alotbuy.com`)。
|
|||
|
|
3. S4 的两处 sed 反向改回(alotbuy 版 vhost 与旧名 301 都可原样恢复)。
|
|||
|
|
4. 证书可留(不影响 alotbuy);`certbot delete --cert-name ai1net.com` 可彻底清掉。
|
|||
|
|
|
|||
|
|
## 4. 已知影响面(提前说明,非阻塞)
|
|||
|
|
|
|||
|
|
- **切换瞬间所有人需要在新域重登一次**:cookie 域由 `.alotbuy.com` 变 `.ai1net.com` ⇒ 旧 cookie 不再随请求发送(无法双域共存,一个 cookie 只能挂一个 Domain)。
|
|||
|
|
- **实例地址由 `<user>.alotbuy.com` 变为 `<user>.ai1net.com`**:旧地址在 alotbuy 版 vhost 仍可用(两套 vhost 并存期间);若要彻底退役旧域,再补一条 `alotbuy.com → ai1net.com` 的 301(本轮**不做**,留待你确认退役时机)。
|
|||
|
|
- `work.alotbuy.com`(Gitea · 154.40.35.3)**与本次无关**,不动。
|
|||
|
|
- 中继引导种子改成「新域为主、旧域为备」⇒ 两域任一存活都不影响 worker 入网。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. ✅ 执行结果(2026-09-19 06:1x–06:4x · 已全部落地)
|
|||
|
|
|
|||
|
|
| 步 | 状态 | 关键读数 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **S1 证书** | ✅ | `certbot certonly --dns-cloudflare`(凭据 `/etc/cloudflare-ai1net.ini`,mode 600,`/zones` 自检 = 可管 zone 数 **1** = `ai1net.com`);结果 `Successfully received certificate.`|`CN = ai1net.com`,issuer `Let's Encrypt YR1`,`notAfter = Dec 17 21:16:42 2026`,SAN = `ai1net.com` + `*.ai1net.com`|复用既有 ACME 账号 `63fe5a37…`|**certbot 已建自动续期任务** |
|
|||
|
|
| **S2 vhost** | ✅ | `ai1net.com.conf`(6811 B)+ `relay-direct.ai1net.com.conf`(2737 B)拷入 `/www/server/panel/vhost/nginx/`(`nginx -t` 失败即自动 `rm -f` 回滚);源站验收:`https://ai1net.com/portal.html`=200、`https://dsh.ai1net.com/`=200、SNI = `CN=ai1net.com`,alotbuy 仍 200 |
|
|||
|
|
| **S3 平台 env** | ✅ | 备份 `/etc/dshs.env.bak-dom-20260919-061626`(684 B);`DSHS_BASE_DOMAIN=ai1net.com`、`DSHS_COOKIE_DOMAIN=.ai1net.com`;`restart dshs` ⇒ active;日志 `[relay-client] registered host=manager network=ops`、`presence SNAP 2 条` |
|
|||
|
|
| **S4 连带项** | ✅ | 备份 `relay-direct.conf.bak-dom-20260919-061658`(3635 B)、`dsh.alotbuy.com.conf.bak-dom-20260919-061658`(982 B);`relay-direct.conf` 第 44 行 Host → `ai1net.com`;`dsh.alotbuy.com.conf` map 两目标 → `ai1net.com` / `$label.ai1net.com`;`nginx -t` + reload;验收:`relay-direct.ai1net /dshs-overlay/bootstrap`=200、`/unknown`=404、`relay-direct.alotbuy /dshs-overlay/bootstrap`=200、`ai1net 门户`=200 |
|
|||
|
|
| **S5 worker** | ✅ | **必须做**(见 §5.2);106 `DSHS_RENDEZVOUS_URL` + `relay-client --url` 由旧域切新域,两机补同一份 5 条 `SEEDS` |
|
|||
|
|
|
|||
|
|
### 5.1 端到端验收(经真实链路)
|
|||
|
|
|
|||
|
|
- 门户:`https://ai1net.com/portal.html` = **200 / 50726 B / title=管理门户**(与 `alotbuy.com` 逐字节同源同大小)。
|
|||
|
|
- 实例子域(`mksess.cjs` 临时会话,用完即删):`admin.ai1net.com` ⇒ **302 → `?token=…`**(实例已就绪);`guest.ai1net.com` ⇒ **302 → `ai1net.com/wake.html?next=…`**(实例未跑 ⇒ 走过渡页拉起);**无 cookie** ⇒ 302 → `ai1net.com/login.html`。
|
|||
|
|
- 中继:`/dshs-relay` 在 `ai1net.com` / `alotbuy.com` / `relay-direct.ai1net.com` 三处**响应逐字一致**(`dshs relay: WebSocket upgrade only`)。
|
|||
|
|
- 覆盖网络探针:**29 PASS / 0 FAIL / 0 SKIP(rc=0)**;`OBS-21` 两路径 **count=5 hosts=5**。
|
|||
|
|
|
|||
|
|
### 5.2 🔴 执行中发现并修掉的两个真问题(**均已在生产验证**)
|
|||
|
|
|
|||
|
|
**(甲)种子列表「双载体」冲突 —— 我引入的回归。**
|
|||
|
|
S3 原脚本把 `DSHS_OVERLAY_BOOTSTRAP_SEEDS` **重复追加**进 `/etc/dshs.env`;而该文件(`EnvironmentFile`)在本单元里**压过** drop-in 的 `Environment=` ⇒ 生效列表被窄化成 2 条(`ai1net` + `alotbuy`,且**两条同落 47 一台中继**),丢掉 `relay-direct.*` 与 `106.54.21.172` ⇒ **Manager 不再对 106 建拨号会话**。
|
|||
|
|
⇒ **修法**:删除 `/etc/dshs.env` 里的重复键(回到 drop-in 是"唯一载体"的既定纪律),并把 drop-in 的列表升级成 5 条、主入口改到新域;同时给地址覆盖加 `relay-direct.ai1net.com=47.77.182.89`。备份 = `overlay-443fb.conf.bak-dom-20260919-063350`(2124 B) + `/etc/dshs.env.bak-seeds-20260919-063350`(772 B)。
|
|||
|
|
|
|||
|
|
**(乙)`w-106` 用户实例子域 500 `解析不出落点` —— 既有缺口,被本轮验收照出来。**
|
|||
|
|
现象 = 归属在 `w-106` 的用户(`guest` / `dbg2mx897` / `pocuimwkrr`)打开实例子域 = **500** `host "w-106" 声明 via=relay 但解析不出落点:拒绝回落到 endpoint`。
|
|||
|
|
机理 = 平台对 `via=relay` host 的可达性**只读单一** `DSHS_RELAY_STATUS_URL`(`http://127.0.0.1:20080/status` = **47 中继**),而 `w-106` 的**实时在线态在 106 中继上**(106 `/status` 明写 `online: ["w-106(session=… ports=19000/21001 …)"]`),47 中继那份是 `online:false` 的**陈旧条目** ⇒ `RelayRendezvous.resolve()` 返回 `undefined` ⇒ `agentBaseUrlOf()` **按设计拒绝回落**(R4/P0-3:relay 语义下回落会打到本机同号端口)⇒ 500。
|
|||
|
|
⇒ **修法(本轮的 S5 正好覆盖)**:给 106 的 worker 一份以新域为主入口的 `SEEDS` ⇒ worker 把**主入口落回 47 中继**注册 ⇒ 47 `/status` 该端点由 `online:false` 转 **`online:true`** ⇒ 解析恢复。**验证**:`guest.ai1net.com` 由 500 → **302(wake.html)**。
|
|||
|
|
⚠️ **该 500 在"恢复 5 条候选"之后仍然复现** ⇒ **不是**由(甲)单独造成的;真正修好它的是本步。
|
|||
|
|
🔴 **遗留(未修 · 已上报)**:只要 worker 因抖动漂到 **106 自家中继**,47 中继即再度视其为离线 ⇒ 同一 500 会回来,而探针此时**仍是全绿**(序㊾ 让它按并集看 ⇒ 观测面绿、控制面红)。⇒ 正解 = 把「两台中继并集」这条口径**从探针推广到控制面**(同一族问题),属**独立立项**,⛔ 本轮未动。
|