Files
trojanZ/新镜像改造与无损替换方案.md
T
2026-07-26 17:20:23 +08:00

473 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Trojan 管理项目新镜像改造与无损替换方案
## 1. 结论
基于当前 Go 后端和 Vue 前端源码,可以制作一个功能兼容的新镜像,并替换香港、美国两台服务器上现有的 `jrohy/trojan` 容器。用户数据、流量统计、管理员状态、订阅、分享链接、Trojan TLS 回落以及现有 Nginx/Caddy/Gitea 架构均可保留。
但不能只修改现有 Dockerfile 后重新构建。要达到“证书可靠自动续期、端口清晰可配置、容器重启自动恢复、可以无损替换”,必须同时完成以下五项:
1. 持久化 Trojan 配置、管理后台 LevelDB、ACME 账户和证书;
2. 将 Trojan 内部监听端口和客户端公网端口分离;
3. 证书续期成功后校验证书并重启 Trojan,使新证书真正生效;
4. 修复容器内 Trojan 与 Web 服务的启动、状态、日志和健康检查;
5. 从本地前后端源码进行可复现、多阶段镜像构建,不再下载运行时 `latest` 文件。
推荐先发布一个“兼容替换版”镜像,保持当前 `host network + MariaDB + Nginx stream` 的部署模型,以最低风险替换现网;运行稳定后,再单独规划无特权、bridge 网络的容器原生版本。不要把两次架构迁移合并到同一次上线。
## 2. 当前源码确认出的约束
| 位置 | 当前行为 | 对新镜像的影响 |
| --- | --- | --- |
| `trojan-master/core/server.go` | Trojan 配置固定在 `/usr/local/etc/trojan/config.json`,直接覆盖写入 | 必须挂载持久化,并改为备份、临时文件、原子替换和失败回滚 |
| `trojan-master/core/leveldb.go` | 管理状态固定在 `/var/lib/trojan-manager` | 必须迁移,否则管理员密码、JWT 密钥、标题、重置日等会丢失 |
| `trojan-master/trojan/web.go` | `GetDomainAndPort()` 直接返回 `ssl.sni + local_port` | 当前内部端口为 8443,但客户端应连接 443,必须新增 `public_port` |
| `trojan-master/trojan/trojan.go` | `port` 命令修改 `local_port`、开防火墙并直接重启 | 无端口冲突检查、事务和回滚;在特权容器中还可能修改宿主机 iptables |
| `trojan-master/cmd/web.go` | 已支持 `--host``--port` | 能复用现有 CLI 参数,无需重写 Web 监听实现 |
| `trojan-master/asset/trojan-web.service` | 未传参数,默认监听 `0.0.0.0:80` | 新镜像必须显式使用 `127.0.0.1:8081` |
| `trojan-master/trojan/install.go` | ACME standalone 默认使用 80,证书直接引用 acme.sh 内部目录 | 与 Nginx 80 冲突;证书应安装到稳定路径 |
| `trojan-master/install.sh` | 定时任务停止 Web、执行 `acme.sh --cron`、再启动 Web | 没有重启 Trojan,新证书不一定生效;还会造成每天后台短暂中断 |
| `trojan-master/util/command.go` | systemctl 失败时会在线下载替代脚本,控制函数不向 API 返回错误 | 构建不可复现,Web 可能在服务启动失败时仍显示成功 |
| `trojan-master/asset/Dockerfile` | 构建时下载上游最新管理二进制、远程 unit 和 systemctl 脚本 | 当前 Dockerfile 不会包含本地修改过的 Go/前端源码,必须重写 |
| `trojan-master/web/web.go` | 前端通过 `//go:embed templates/*` 嵌入 Go 二进制 | 必须先构建前端,并将 `dist` 复制到 `web/templates` 后再编译 Go |
| `trojan-web-master/src/views/user/index.vue` | 后端返回的端口直接用于分享链接和订阅地址 | 后端旧字段 `port` 应继续返回公网端口 443,保证旧前端兼容 |
| `trojan-web-master/src/utils/request.js` | Axios 超时为 8 秒 | 证书申请和配置切换必须异步执行并返回任务 ID |
## 3. 证书管理结论
### 3.1 不建议让 Caddy直接管理 Trojan 域名证书
当前链路是:
```text
客户端 :443
-> 宿主机 Nginx stream 按 SNI 分流
-> www.* TLS 原样透传至 Trojan :8443
-> 非 Trojan HTTPS 由 Trojan 回落到 Web :8081
```
`www.*` 的 TLS 由 Trojan 终止,Caddy没有接触这条 TLS 会话,因此不能像管理 `git.*` 一样自然地为 Trojan 管理证书。强行读取 Caddy 内部证书存储并复制给 Trojan,会依赖 Caddy 的内部目录结构和续期事件,不适合作为稳定接口。
最终职责保持为:
- `git.hk.chermack.top``git.us.chermack.top`:继续由 Caddy 自动申请和续期;
- `www.hk.chermack.top``www.us.chermack.top`:由新镜像内的 acme.sh 自动管理,并在续期后可靠重启 Trojan;
- Nginx继续保留 `/.well-known/acme-challenge/``127.0.0.1:8082` 的转发。
### 3.2 推荐的自动续期流程
新镜像常驻一个受限的 ACME challenge 服务,仅在 `127.0.0.1:8082` 提供 `/.well-known/acme-challenge/` 下的文件;Nginx 继续把相同路径转发到该端口。acme.sh 使用 webroot,不再临时监听端口,也不停止管理后台:
```bash
acme.sh --issue \
--server letsencrypt \
--webroot /var/lib/trojan-acme-webroot \
--keylength ec-256 \
-d "${ACME_DOMAIN}"
```
证书不再让 Trojan 直接引用 acme.sh 内部工作目录,而是发布到稳定路径:
```bash
acme.sh --install-cert --ecc -d "${ACME_DOMAIN}" \
--fullchain-file /etc/trojan/certs/fullchain.pem \
--key-file /etc/trojan/certs/private.key \
--reloadcmd /usr/local/sbin/reload-trojan-after-cert
```
定时器每天检查一次并增加随机延迟,只有确实续期成功时才执行 reload hook。reload hook 必须:
1. 检查证书和私钥匹配;
2. 检查 SAN 包含配置域名;
3. 比较新旧证书指纹,避免无变化重启;
4. 备份当前证书;
5. 重启 Trojan,并在限定时间内确认 8443 恢复监听;
6. 失败时恢复旧证书并记录错误。
不再每天停止 `trojan-web`,也不删除其他域名的 acme.sh 定时任务。
证书状态由 Go 读取 X.509 元数据,Web 页面只展示域名、签发者、起止时间、剩余天数、最近续期和错误;永不向浏览器返回私钥或任意文件内容。
## 4. 端口模型改造
### 4.1 必须拆分的端口
```text
public_port 客户端公开端口,当前固定为 443
listen_port Trojan 内部监听端口,当前为 8443
fallback_port Trojan 非协议流量回落端口,当前为 8081
web_port 管理后台监听端口,当前为 8081
acme_http_port HTTP-01 challenge 端口,当前为 8082
```
Trojan 原生 `config.json` 继续只保存它支持的字段。新增独立管理配置,避免把未知字段写入 Trojan 配置:
```yaml
schema_version: 1
public:
domain: www.hk.chermack.top
port: 443
runtime:
listen_address: 0.0.0.0
listen_port: 8443
fallback_address: 127.0.0.1
fallback_port: 8081
web:
listen_address: 127.0.0.1
listen_port: 8081
acme:
enabled: true
domain: www.hk.chermack.top
http_port: 8082
renew_before_days: 30
firewall:
manage: false
```
建议路径为 `/etc/trojan-manager/manager.yaml`,宿主机持久化到 `/opt/trojan/config/manager.yaml`
### 4.2 兼容旧前端和旧客户端
保留所有现有接口与 JSON 字段。`/trojan/user` 的兼容响应为:
```json
{
"domain": "www.hk.chermack.top",
"port": 443,
"publicPort": 443,
"listenPort": 8443,
"userList": []
}
```
- 旧前端继续读取 `port`,得到正确的公网端口 443
- 新前端优先读取 `publicPort`
- 未配置 `public_port` 的旧部署自动使用 `local_port`,保持原行为;
- `local_port` 只负责 Trojan 实际监听,不再用于生成分享链接。
### 4.3 端口变更的边界
Web 页面不能通过挂载 Docker Socket 去修改宿主机端口映射,这会给予容器近似 root 的宿主机控制权。建议:
- Web 可以修改并验证应用内部端口和公网宣告端口;
- 宿主机 Nginx upstream、Docker/host-network 参数由配套的 `trojanctl apply` 脚本管理;
- 后端返回 `restartRequired``nginxReloadRequired``containerRecreateRequired`
- `trojanctl apply` 先生成候选配置,再执行 `nginx -t`,成功后才 reload;失败自动恢复旧文件。
当前两台服务器无需频繁改变内部端口,建议长期保持 8443/8081/8082,只将客户端公网端口保持为 443。
## 5. Go 后端改造
### 5.1 配置与事务
新增包建议:
```text
config/manager.go 管理配置结构、默认值、版本迁移
config/transaction.go 文件锁、备份、原子写入和回滚
certificate/manager.go 证书申请、状态、续期和校验
service/manager.go 服务控制接口
service/systemd.go 首版兼容镜像实现
jobs/manager.go 异步任务和状态
health/check.go Web、DB、Trojan、证书检查
```
应用配置流程:
1. 校验域名、端口范围和端口之间的冲突;
2. 生成 manager 配置和 Trojan 候选配置;
3. 检查证书路径、数据库连接和候选 JSON;
4. 备份当前配置;
5. 原子替换并重启相应服务;
6. 检查监听端口和服务状态;
7. 失败则恢复配置并重新启动旧版本。
`SystemctlStart/Stop/Restart` 必须返回 `error`controller 只有在服务实际成功后才能返回 `success`
### 5.2 新增 API
现有 `/trojan/*``/common/*``/auth/*` 接口全部保留。新增:
```text
GET /system/config
POST /system/config/validate
POST /system/config/apply
GET /system/certificate
POST /system/certificate/renew
GET /system/jobs/:id
GET /healthz
GET /readyz
```
申请证书和应用端口变更立即返回任务 ID,前端轮询任务;不能受现有 8 秒 Axios 超时限制。
网络、证书、服务控制接口必须仅允许 `admin`。当前认证中普通用户登录后也能通过统一 middleware 访问受保护路由;新接口必须增加后端管理员校验,不能只靠前端隐藏菜单。初始化接口也应只在尚无管理员时允许注册。
### 5.3 镜像模式下的现有命令
| 功能 | 处理方式 |
| --- | --- |
| 用户、流量、期限、订阅、导入导出 | 原样保留,继续使用现有 MariaDB 表 |
| Trojan 启停、重启、状态、实时日志 | 保留命令/API,服务层返回真实结果 |
| `trojan port` | 保留交互用法,新增非交互 `config set`;使用事务应用 |
| `trojan tls` | 保留自定义证书模式,新增受控 ACME 模式和状态查询 |
| Trojan / trojan-go 切换 | 镜像内预置已固定版本的核心,不再运行时下载 latest |
| `update` / `updateWeb` | 改为检查镜像版本并提示 Compose 升级;不允许容器自改二进制 |
| 自动开防火墙 | Docker 模式默认关闭,公网只开放 Nginx 80/443 与 Gitea 2222 |
完全保留“容器内在线覆盖二进制”的更新方式与不可变镜像互相冲突,因此这一项应明确改为镜像升级;它不影响用户管理和 Trojan 业务功能。
## 6. 前端改造
新增 `src/views/settings/index.vue``src/api/system.js`,页面分为:
1. 网络配置:域名、公网端口、Trojan 监听端口、Web 监听地址/端口;
2. 证书状态:模式、签发者、有效期、剩余天数、最近续期和错误;
3. 应用预览:展示会重启什么、是否需要 Nginx reload 或容器重建;
4. 任务状态:轮询申请/续期/应用任务,展示成功或回滚结果。
需要调整:
- `src/router/index.js`:增加 `/settings`
- `src/views/layout/components/Navbar.vue`:增加“网络与证书”入口;
- `src/views/user/index.vue`:优先使用 `publicPort`
- `src/lang/zh.js``src/lang/en.js`:增加配置、证书和风险提示文案;
- `src/api/trojan.js`:原 API 保留,避免旧页面失效。
若新接口为 404,新设置页只提示“当前后端不支持”,不能影响 Dashboard、用户列表和日志页,从而允许新旧前后端短暂混用和快速回滚。
当前生产前端通过 CDN 加载 Vue、Axios、Element Plus 等依赖,同时在 Vite 中将它们 external。新镜像应将依赖打进静态资源,避免 CDN 不可达造成后台白屏;保留 `base: './'` 和 hash 路由。
## 7. 新镜像与服务器包
### 7.1 交付物
建议不是只交付一个镜像标签,而是交付以下完整包:
```text
trojan-stack/
compose.yaml
runtime.env.example
nginx/
www-http.conf.tpl
tls-sni.conf.tpl
scripts/
migrate-v1.sh
apply.sh
verify.sh
rollback.sh
docs/
upgrade.md
```
镜像建议使用固定版本标签,例如:
```text
REGISTRY/trojan-manager-compat:2.0.0
```
部署时同时记录镜像 digest,禁止使用 `latest`
### 7.2 可复现构建
新 Dockerfile 使用多阶段构建:
```text
Node 固定版本
npm ci -> npm run build
↓ dist
Go 固定版本
dist -> trojan-master/web/templates
go test -> go build
↓ manager binary
Runtime 固定 digest
manager + 固定版本 Trojan 核心 + acme.sh + unit/timer + healthcheck
```
必须固定 Node、Go、Trojan、trojan-go、acme.sh 和基础镜像版本;下载的上游核心必须校验 SHA-256。建议生成 SBOM,并对发布镜像签名。
当前项目采用 GPLv3;发布修改后的二进制或镜像时,应同时提供对应修改源码和构建方式。
### 7.3 第一版兼容运行模式
为避免在一次变更中同时修改数据库网络、服务管理和容器权限,第一版保持现状:
```yaml
services:
trojan:
image: REGISTRY/trojan-manager-compat:2.0.0
container_name: trojan
network_mode: host
privileged: true
restart: always
command: init
env_file:
- /opt/trojan/runtime.env
volumes:
- /opt/trojan/config:/usr/local/etc/trojan
- /opt/trojan/manager:/var/lib/trojan-manager
- /opt/trojan/acme:/root/.acme.sh
- /opt/trojan/certs:/etc/trojan/certs
```
兼容镜像使用真实、预置的服务定义,不在运行时下载 systemctl 替代脚本。镜像内预置并启用:
```text
trojan.service
trojan-web.service
trojan-acme-challenge.service
trojan-acme-renew.service
trojan-acme-renew.timer
```
这能最大程度保留现有 CLI、Web 启停和 `journalctl` 实时日志功能。长期版本再用 s6 等容器进程管理器替换 systemd,并同时重构服务控制与日志;该迁移不与首个兼容替换版本合并。
### 7.4 健康检查
镜像必须包含:
- `GET /healthz`:管理进程存活;
- `GET /readyz`:数据库可访问、Trojan 服务运行、8443 监听、证书未过期;
- Docker `HEALTHCHECK`
- 服务启动失败时非零退出,交给 Docker restart policy 恢复。
## 8. 持久化与数据兼容
替换前必须迁移:
```text
/usr/local/etc/trojan/config.json Trojan 配置、数据库连接、TLS 路径
/var/lib/trojan-manager 管理员、JWT 密钥、标题、重置日等
/root/.acme.sh ACME 账户、域名和续期状态
```
新证书固定发布到:
```text
/etc/trojan/certs/fullchain.pem
/etc/trojan/certs/private.key
```
现有 `trojan-mariadb` 继续独立运行,不在本次更换版本或表结构。上线前备份数据库并记录用户数量;新容器启动后比对。不要在首次启动时自动执行会删表/重建的数据库升级。
## 9. 两台服务器的配置
香港 `/opt/trojan/runtime.env`
```env
ACME_DOMAIN=www.hk.chermack.top
PUBLIC_DOMAIN=www.hk.chermack.top
PUBLIC_PORT=443
TROJAN_LISTEN_PORT=8443
WEB_HOST=127.0.0.1
WEB_PORT=8081
ACME_HTTP_PORT=8082
MANAGE_FIREWALL=false
```
美国:
```env
ACME_DOMAIN=www.us.chermack.top
PUBLIC_DOMAIN=www.us.chermack.top
PUBLIC_PORT=443
TROJAN_LISTEN_PORT=8443
WEB_HOST=127.0.0.1
WEB_PORT=8081
ACME_HTTP_PORT=8082
MANAGE_FIREWALL=false
```
Gitea、PostgreSQL、Caddy 和现有 Nginx 80/443 分流不需要改变。
## 10. 无损替换步骤
### 10.1 上线前
1. 固定新镜像版本和 digest
2. 备份 MariaDB,并记录用户数量和关键表结构;
3. 从旧容器导出 Trojan 配置、LevelDB 和 `.acme.sh`
4. 在宿主机 `/opt/trojan` 建立持久化目录,并限制证书/密钥权限;
5. 使用迁移工具生成 `manager.yaml`,其中 `public_port=443``listen_port=8443`
6. 校验证书域名、证书与私钥匹配、数据库可连接;
7. 保存旧容器的 `docker inspect` 和当前 Nginx 配置。
### 10.2 预演
兼容镜像使用 host network,相同端口不能与旧容器并行。可先用临时端口启动候选容器:
```text
Trojan 18443
Web 18081
ACME 18082
public_port 仍为 443
```
预演检查:
- 管理员登录;
- 用户数量、流量和期限;
- 分享链接和 Clash 配置使用公网 443;
- Web 普通访问及 Trojan TLS 回落;
- Trojan 客户端真实连接;
- 证书状态可读取;
- 服务启停、重启和日志;
- 容器重启后两个服务自动恢复。
### 10.3 正式切换
1. 停止旧 `trojan` 容器,但不删除;
2. 将旧容器重命名为 `trojan-legacy`
3. 将新镜像改回 8443/8081/8082,并以容器名 `trojan` 启动;
4. 保持 Nginx upstream 不变;
5. 验证 `www.*` 管理后台、Trojan 客户端、`git.*` Gitea
6. 执行一次手动证书 dry-run/受控续期测试;
7. 执行 `docker restart trojan` 并验证自动恢复;
8. 在香港先进行宿主机重启验收,观察稳定后再部署美国。
现有宿主机 `trojan-container-services.service` 是旧容器的兜底。新镜像通过自身服务管理通过重启验收后,再停止并禁用该宿主机 unit,避免两套启动逻辑长期并存。
### 10.4 回滚
若任一核心验收失败:
1. 停止新容器;
2. 保留新镜像的持久化目录和日志用于分析;
3.`trojan-legacy` 恢复原名称并启动;
4. 确认 8443/8081 恢复监听;
5. 验证 `www.*` 和 Trojan 客户端;
6. MariaDB 未升级、旧容器未删除,因此无需数据库回滚。
旧容器至少保留一个完整证书续期周期或经两次重启验收后再决定清理。
## 11. 验收标准
只有以下项目全部通过,才视为可替换:
- 两台服务器的现有用户、流量、期限和管理员设置完整;
- 旧前端与新前端均能使用原 API
- 分享链接、二维码、JSON、Clash 订阅均使用公网端口 443;
- `www.hk``www.us` 的 HTTP/HTTPS、Trojan 客户端均正常;
- `git.hk``git.us` 不受影响;
- 证书状态能显示,续期成功后 Trojan 使用新证书;
- 续期检查不会每天停止管理后台;
- 错误证书、端口冲突或服务启动失败会自动回滚;
- `docker restart` 和宿主机重启后服务自动恢复;
- 新镜像断网启动不依赖 GitHub/CDN
- 无 Docker Socket 挂载,内部端口不直接暴露公网;
- 回滚旧容器能够在短时间内恢复服务。
## 12. 推荐实施顺序
1. 增加后端配置模型、`public_port` 兼容逻辑和事务写入;
2. 让服务控制返回真实错误,并完成容器自启动/健康检查;
3. 实现固定证书路径、timer、续期 hook 和证书状态 API
4. 增加前端“网络与证书”页面,保持旧接口不变;
5. 重写多阶段 Dockerfile,完成本地前后端嵌入和固定依赖;
6. 增加迁移、验证、应用和回滚脚本;
7. 自动测试旧 API、数据迁移、端口回滚、证书 reload 和容器重启;
8. 香港灰度,完成容器/宿主机重启验收;
9. 美国复制部署;
10. 稳定后再讨论 bridge 网络、非 privileged 和 MariaDB 升级。
综上,建议采用“兼容镜像 + 配套服务器包 + 分阶段灰度”的方式。它能解决当前重启后服务未拉起、证书续期不完整和端口语义混乱,同时将现网变化控制在 Trojan 容器本身,不牵动 Gitea、Caddy 和公网 Nginx 入口。