Make Nginx the default public gateway
This commit is contained in:
1 parent
6d5973bea3
commit
ff4c36bcce
10 files changed
+136
-66
No files matched your search
@@ -330,7 +330,6 @@ func (s *Server) RegisterRoutes(r *gin.Engine) {
|
||||
admin.PUT("/system/identity", idp.Configure)
|
||||
admin.POST("/system/identity/gateway", identityprovider.SecureGateway(), s.identityGateway)
|
||||
admin.PUT("/system/tls/domain", s.setTLSDomain)
|
||||
admin.PUT("/system/tls/mode", s.setTLSMode)
|
||||
admin.GET("/system/tls/nginx-preview", s.nginxPreview)
|
||||
admin.GET("/system/tls/nginx-setup", s.nginxSetupStatus)
|
||||
admin.POST("/system/tls/nginx-setup", s.initializeNginx)
|
||||
|
||||
@@ -27,7 +27,7 @@ func (s *Server) nginxSetupStatus(c *gin.Context) {
|
||||
func (s *Server) activateNginx(c *gin.Context) {
|
||||
status := tlsservice.Status()
|
||||
if status.Mode != "nginx" || status.ConfiguredMode != "nginx" || status.ConfiguredDomain == "" {
|
||||
c.JSON(http.StatusPreconditionFailed, gin.H{"error": "请先启用 Nginx 模式并保存公网入口;切换入口模式需重启 opsx-vpn"})
|
||||
c.JSON(http.StatusPreconditionFailed, gin.H{"error": "请先完成旧入口迁移并保存 Nginx 公网入口"})
|
||||
return
|
||||
}
|
||||
scheme := status.ConfiguredScheme
|
||||
@@ -127,27 +127,10 @@ func (s *Server) nginxPreview(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"http_site": httpSite, "https_site": httpsSite,
|
||||
"public_base": status.ConfiguredPublicBase,
|
||||
"warning": "仅预览:保存并重启后按协议启用入口;认证域名须单独配置。",
|
||||
"warning": "仅预览:首次启用站点需手动执行,已启用的 Nginx 入口保存后热重载。",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) setTLSMode(c *gin.Context) {
|
||||
var req struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入入口模式"})
|
||||
return
|
||||
}
|
||||
if err := tlsservice.ConfigureMode(service.BaseDir(), req.Mode); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
store.Audit(c.GetString("username"), "tls_mode_configured", req.Mode, "需检查外部入口后重启")
|
||||
status := tlsservice.Status()
|
||||
c.JSON(http.StatusOK, gin.H{"status": status, "restart_required": status.RestartRequired})
|
||||
}
|
||||
|
||||
func (s *Server) setTLSDomain(c *gin.Context) {
|
||||
var req struct {
|
||||
Domain string `json:"domain"`
|
||||
@@ -167,6 +150,10 @@ func (s *Server) setTLSDomain(c *gin.Context) {
|
||||
port = *req.Port
|
||||
}
|
||||
status := tlsservice.Status()
|
||||
if status.Mode != "nginx" || status.ConfiguredMode != "nginx" {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "当前使用旧 Go 直监听入口,请按部署文档迁移至 Nginx 后再修改公网入口"})
|
||||
return
|
||||
}
|
||||
newBase := tlsservice.PublicBaseURL(scheme, strings.ToLower(strings.TrimSpace(req.Domain)), port)
|
||||
if newBase == "" || port < 0 || port > 65535 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "公网地址、协议或端口无效;IPv4 地址仅支持 HTTP"})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"vpn-platform/backend/internal/tlsservice"
|
||||
)
|
||||
|
||||
func TestTLSStatusRequiresAdmin(t *testing.T) {
|
||||
@@ -47,3 +49,21 @@ func TestTLSStatusRequiresAdmin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyGoGatewayRejectsPublicEntryChanges(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_MODE", "autocert")
|
||||
t.Setenv("VPN_TLS_DOMAIN", "")
|
||||
if err := tlsservice.Start(http.NotFoundHandler(), t.TempDir()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.PUT("/api/system/tls/domain", (&Server{}).setTLSDomain)
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/system/tls/domain", bytes.NewBufferString(`{"domain":"other.example.com","scheme":"https","port":443}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusConflict {
|
||||
t.Fatalf("legacy Go mode should require manual migration: %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func configFromEnvironment(dataDir string) (configuration, error) {
|
||||
mode = raw
|
||||
}
|
||||
if mode == "" {
|
||||
mode = "autocert"
|
||||
mode = "nginx"
|
||||
}
|
||||
if mode != "autocert" && mode != "nginx" {
|
||||
return configuration{}, fmt.Errorf("VPN_TLS_MODE 仅支持 autocert 或 nginx")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDomainValidationAndRestart(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_MODE", "autocert")
|
||||
for _, domain := range []string{"test-link.appx365.com", "a.b"} {
|
||||
if !ValidDomain(domain) {
|
||||
t.Errorf("valid domain rejected: %s", domain)
|
||||
@@ -73,6 +74,7 @@ func TestHTTPIPv4PublicEntry(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFailedHTTPSBindReleasesHTTPPort(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_MODE", "autocert")
|
||||
t.Setenv("VPN_TLS_DOMAIN", "test-link.appx365.com")
|
||||
occupied, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -118,6 +120,30 @@ func TestNginxModeDoesNotBindGoListeners(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultModeIsNginxAndExplicitGoModeIsPreserved(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_MODE", "")
|
||||
t.Setenv("VPN_TLS_DOMAIN", "")
|
||||
t.Setenv("VPN_TLS_SCHEME", "")
|
||||
t.Setenv("VPN_TLS_PORT", "")
|
||||
dir := t.TempDir()
|
||||
if err := ConfigurePublicEntry(dir, "https", "vpn.example.com", 443); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Start(http.NotFoundHandler(), dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := Status(); got.Mode != "nginx" || got.Enabled || got.ConfiguredPublicBase != "https://vpn.example.com" {
|
||||
t.Fatalf("default must keep the public listener under Nginx control: %+v", got)
|
||||
}
|
||||
if err := writePersisted(dir, "tls-mode", "autocert"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := configFromEnvironment(dir)
|
||||
if err != nil || config.mode != "autocert" {
|
||||
t.Fatalf("explicit legacy mode must be readable: %+v, %v", config, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxEntryCanAdoptWithoutRestart(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_MODE", "")
|
||||
t.Setenv("VPN_TLS_DOMAIN", "")
|
||||
|
||||
+4
-4
@@ -56,13 +56,13 @@ sudo ss -tlnp | grep ':18080'
|
||||
|
||||
内置方案复用管理域名与证书;先在「设置 → 飞书身份」启用身份服务并同步公网路由,Nginx 仅允许固定 `/id/` 身份路径。创建并启动唯一一条使用当前域名的 OpenVPN OIDC 测试专线后再次同步,才会把 `/oauth2/` 转发到其本机认证组件 `127.0.0.1:19090+`。未启动专线时不会开放 `/oauth2/` 路由。不要把认证端口或 OpenVPN 管理 socket 暴露到公网。详见 [接入说明](../README.md#openvpn-飞书--oidc-浏览器认证)。
|
||||
|
||||
该域名须由你控制并解析到公网入口。管理员可在“设置 → 域名与证书”保存 `test-link.appx365.com`,并选择 Go 或 Nginx 入口模式;`VPN_TLS_DOMAIN` 和 `VPN_TLS_MODE` 环境变量优先于页面配置。两种模式只能选一种来占用 TCP 80/443,保存配置并不会安装 Nginx、开放安全组或签发证书。
|
||||
该域名须由你控制并解析到公网入口。管理员可在“设置 → 域名与证书”保存 `test-link.appx365.com`,由 Nginx 处理公网监听;`VPN_TLS_DOMAIN` 等环境变量优先于页面配置。保存配置并不会自动安装 Nginx、开放安全组或签发证书。HTTP 可以使用 IPv4 与自定义端口;HTTPS 签发仍要求域名。
|
||||
|
||||
Go 模式(默认)使用 `golang.org/x/crypto/acme/autocert` 监听 80/443,处理 HTTP-01、自动签发和续签;首次访问 `https://test-link.appx365.com/api/health` 才开始签发。证书和 ACME 账号密钥在 `data/acme`,须备份。此模式下 80 的非挑战请求会跳转 HTTPS,443 的管理站点沿用 Gin 路由,**不会**对未知路径执行 Nginx `return 444`。如需严格的公网路径白名单,使用下面的 Nginx 模式。端口空闲、公网 DNS 和云安全组 TCP 80/443 通畅是两种模式的共同前置条件。
|
||||
旧实例如果显式配置 `data/tls-mode=autocert` 或 `VPN_TLS_MODE=autocert`,仍沿用 Go 直监听 80/443 以避免升级时断网,但页面不再提供切换入口模式,也不允许在旧模式下保存新的公网入口。迁移时先备份入口模式、程序和证书,在维护窗口内准备 Nginx/Certbot 与可回退的站点配置,停用占用公网端口的 Go 管理进程,将入口模式显式改为 `nginx`,启用 Nginx 站点,再启动管理进程并核对登录、OIDC 发现文档、OpenVPN 浏览器回调和证书续签。任何验证失败都应恢复备份的配置、站点和程序。不能只修改模式文件后重启:未准备好站点会让公网入口不可用。新实例未写模式配置时默认为 `nginx`,不会自行监听公网端口。
|
||||
|
||||
### Nginx 模式:HTTP-01 与路径白名单
|
||||
|
||||
“设置 → 首次引导”先检查 root、默认出口、IPv4 转发;管理员可单独启用 IPv4 转发。OpenVPN、WireGuard、IPSec 等依赖仍在“创建专线”选择相应协议时单独安装,不会因进入引导页而批量安装。“可选 · HTTPS 公网入口 → 准备 Nginx / Certbot”会读取 `/etc/os-release`,仅在 Ubuntu 使用 `apt-get`、TencentOS Server 4 使用 `dnf` 安装 Nginx/Certbot,并创建 `/var/lib/opsx-vpn/acme/.well-known/acme-challenge`。页面会实时显示安装日志;需以 root 运行平台,其他系统显示不支持。Ubuntu 上如果已有 `/usr/sbin/policy-rc.d`,初始化会拒绝覆盖该文件;安装期间临时阻止软件包脚本自动启动 Nginx,正常结束后移除临时策略。若进程在安装时被强制杀死,检查并移除该临时策略后再试。TencentOS 上安装后也需核实发行版的软件包脚本没有启动默认站点;如发行版仓库没有 Certbot,页面报告失败,由管理员检查软件源。初始化不改写已有站点,不会主动启用 80/443,也不会签发证书。
|
||||
“设置 → 首次引导”先检查 root、默认出口、IPv4 转发;管理员可单独启用 IPv4 转发。OpenVPN、WireGuard、IPSec 等依赖仍在“创建专线”选择相应协议时单独安装,不会因进入引导页而批量安装。“Nginx 入口与证书 → 准备 Nginx / Certbot”会读取 `/etc/os-release`,仅在 Ubuntu 使用 `apt-get`、TencentOS Server 4 使用 `dnf` 安装 Nginx/Certbot,并创建 `/var/lib/opsx-vpn/acme/.well-known/acme-challenge`。页面会实时显示安装日志;需以 root 运行平台,其他系统显示不支持。Ubuntu 上如果已有 `/usr/sbin/policy-rc.d`,初始化会拒绝覆盖该文件;安装期间临时阻止软件包脚本自动启动 Nginx,正常结束后移除临时策略。若进程在安装时被强制杀死,检查并移除该临时策略后再试。TencentOS 上安装后也需核实发行版的软件包脚本没有启动默认站点;如发行版仓库没有 Certbot,页面报告失败,由管理员检查软件源。初始化不改写已有站点,不会主动启用 80/443,也不会签发证书。
|
||||
|
||||
“预览 Nginx 配置”仅生成当前域名和后端实际注册 API 路由对应的两段站点配置,不会写入系统目录或启动服务。第一段监听 80,仅从 `/var/lib/opsx-vpn/acme` 提供 `/.well-known/acme-challenge/`;其他路径 `return 444`。第二段在证书**已签发后**监听 443:只反代已知前端页面、构建资源和已注册 API,其余路径 `return 444`;未知 TLS 主机名拒绝握手。切换模式必须按以下顺序进行(先备份现有站点,检查是否已有服务占用 80/443):
|
||||
|
||||
@@ -71,7 +71,7 @@ Go 模式(默认)使用 `golang.org/x/crypto/acme/autocert` 监听 80/443,
|
||||
3. 证书签出后程序自动追加 HTTPS 站点、检查配置并重载,同时安装固定续签钩子(`nginx -t` + `systemctl reload nginx`)。确认 Certbot 续签定时器运行,执行 `certbot renew --dry-run` 检查续签;生产验证时再核对 `--run-deploy-hooks` 对钩子的实际效果。证书状态页读取实际的证书到期日;不要仅以站点文件存在判断公网可用。
|
||||
|
||||
2026-10-22 生产实例已启用 `test-link.appx365.com`,证书有效至 2027-01-19,`certbot renew --dry-run --run-deploy-hooks --non-interactive` 模拟续签成功。当前管理服务仍在公网 `:18080` 监听,沿用 HTTP 地址的客户端可绕过 Nginx 的路径白名单;完成客户端地址迁移后,需限制 18080 仅回环访问。模拟续签执行钩子时 `nginx -t` 的正常 stderr 被 Certbot 记录为警告,但退出成功。
|
||||
4. 页面保存入口模式为 Nginx,启动或重启 opsx-vpn 并设置 `VPN_LISTEN_ADDR=127.0.0.1:18080`;确认 `https://<域名>/api/health`、登录及各业务页面可用,未知路径断开,80 的挑战仍可读,然后收紧 18080 公网安全组。若现有业务必须从公网直连 18080,先迁移这些客户端。
|
||||
4. 确认入口模式为 Nginx,启动或重启 opsx-vpn 并设置 `VPN_LISTEN_ADDR=127.0.0.1:18080`;确认 `https://<域名>/api/health`、登录及各业务页面可用,未知路径断开,80 的挑战仍可读,然后收紧 18080 公网安全组。若现有业务必须从公网直连 18080,先迁移这些客户端。
|
||||
|
||||
不要只开放 `/oauth2/callback`:认证组件还需要 `/oauth2/` 下的登录入口。内置身份服务方案通过设置页同步已托管的 Nginx 站点;外部发行者或独立认证域名仍须单独配置,不能直接复用管理站点路径。若代理和 VPN 主机分离,预览里的 `127.0.0.1` 指代理主机自身,应改成受限私网地址并保护传输链路。回退时恢复原 Nginx 配置,运行 `nginx -t` 后重载,不删除 `data/`、签名密钥或已有证书。
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# SDD-007:公网入口收敛为 Nginx
|
||||
|
||||
| 字段 | 内容 |
|
||||
|---|---|
|
||||
| 版本 / 变更级别 | 0.1 / S3(公网监听及旧模式兼容) |
|
||||
| 设计状态 | 已形成 |
|
||||
| 实现状态 | 已实现;待生产验证 |
|
||||
| 负责人 / 日期 | opsx-vpn / 2026-10-26 |
|
||||
| 关联需求 | 页面不再展示 Go/Nginx 模式选择;Nginx 统一承接新公网入口 |
|
||||
| 批准状态 | 用户要求继续实施上述评估方案 |
|
||||
|
||||
## 1. 背景、目标与范围
|
||||
|
||||
- 当前行为:`tlsservice.configFromEnvironment` 在没有模式配置时默认 `autocert`;`Start` 在该模式监听 HTTP-01 与 HTTPS 端口,`nginx` 模式只运行管理服务;`PUT /api/system/tls/mode` 可写入任一模式;`GatewaySettings.vue` 的模式选择与证书申请会写这个配置。生产运行 Nginx,管理端为 18080;此前磁盘模式误设 `autocert` 导致重启时与 Nginx 争用 80。
|
||||
- 目标:新安装默认 Nginx,页面只配置协议、地址、端口和证书;不再通过 Web/API 切换监听所有者。已有 `autocert` 配置维持旧运行行为直到管理员显式迁移。
|
||||
- 非目标:本次不自动接管现有 Go 直监听实例、不关闭公网 18080(需核对旧客户端)、不修改当前生产域名或证书。
|
||||
- 保留:HTTP IPv4、自定义端口、Nginx 白名单、证书申请/续签、OIDC 活跃回调保护和旧 Go 模式的启动兼容性。
|
||||
|
||||
## 2. 需求与约束
|
||||
|
||||
- FR-01:没有显式模式配置的新实例从 Nginx 模式启动,不绑定外部 80/443;HTTPS 仍要求域名和证书,HTTP 可以 IPv4。
|
||||
- FR-02:页面移除模式选择和对模式写入接口的调用,展示 Nginx 入口及当前协议/端口;旧 Go 模式显示迁移提醒并禁止页面保存入口和触发 Nginx 证书流程。
|
||||
- FR-03:移除 `PUT /api/system/tls/mode`;旧持久化或环境配置 `autocert` 保留可读并继续按旧方式启动,迁移需管理员按文档停机处理,不能隐式改配置。
|
||||
- NFR-01:现有 Nginx + HTTPS 站点、管理监听、OpenVPN 认证进程不变;失败时可恢复旧二进制,Nginx 配置不变。
|
||||
|
||||
## 3. 架构与方案
|
||||
|
||||
`tlsservice` 仅把缺省模式改为 `nginx`,保留读取显式旧模式及旧 listener 代码。后台移除模式写接口并拒绝旧模式通过域名保存误建新入口。前端去掉模式 selector 和 mode PUT 调用,旧模式给出明确迁移提示,要求通过运维流程准备 Nginx 后切换。Nginx 入口继续使用当前渲染、检查、热重载和 Certbot webroot 流程。内网管理端口绑定方式与公网路径白名单为独立任务。
|
||||
|
||||
## 4. 接口、数据与配置契约
|
||||
|
||||
- `GET /api/system/tls` 保留 `mode` 与 `configured_mode` 字段以兼容旧客户端;`PUT /api/system/tls/mode` 移除(调用返回 404/405)。
|
||||
- `data/tls-mode=autocert` 和 `VPN_TLS_MODE=autocert` 仍作为旧配置读取;未配置时默认为 `nginx`。无自动数据库/文件迁移。
|
||||
- 旧模式运行时 `PUT /api/system/tls/domain` 返回 409,避免产生重启后意外生效的入口草稿。
|
||||
|
||||
## 5. 风险与验收
|
||||
|
||||
| 风险或失败场景 | 防护 / 恢复 | 可观察信号 |
|
||||
|---|---|---|
|
||||
| 旧实例自动切换导致公网中断 | 显式 `autocert` 保持不变 | 状态仍为 autocert,原 listener 仍工作 |
|
||||
| 旧模式下误保存新域名 | API 返回 409,文件不变 | 409、原入口仍响应 |
|
||||
| Nginx 未安装的新部署没有公网入口 | 页面初始化提示、需明确启用站点 | 站点状态 none,管理端继续运行 |
|
||||
| 管理 18080 直连绕过白名单 | 保留页面现有告警,后续迁移旧客户端并限制端口 | `management_direct_public` |
|
||||
|
||||
| AC | 对应需求 | 操作 | 预期结果 | 验证方式 | 结果 |
|
||||
|---|---|---|---|---|---|
|
||||
| AC-01 | FR-01 | 空配置 Start | 模式 nginx、不绑定 Go 公网监听 | Go 单测 | 已通过 Go 单测 |
|
||||
| AC-02 | FR-02 | 打开公网入口页面并保存 | 无模式控件/模式 PUT,已有 HTTPS 值不变 | 构建及 UI 代码检查 | 构建及代码检查通过;未执行浏览器交互 |
|
||||
| AC-03 | FR-03 | 显式旧配置启动、模式 PUT、旧模式改域名 | 保持旧状态;模式 PUT 不可用,域名保存为 409 | Go/API 单测 | 旧模式读取与 409 单测通过;路由检查通过 |
|
||||
| AC-04 | NFR-01 | 远端发布后核对 | Nginx 80/443、管理 18080、登录页、OIDC 与 OpenVPN 均正常 | 线上只读探测 | Not Run |
|
||||
|
||||
## 6. 发布与恢复
|
||||
|
||||
本地 Go 测试和前端构建后,提交代码并在服务器确认当前托管站点、配置、进程及工作树。备份管理二进制到 `/tmp/opsx-vpn-backups`,更新代码、编译前后端、精准重启管理进程;**不修改生产 `tls-mode`、域名、证书或 Nginx 站点,不重启 VPN 进程**。验证 HTTP/HTTPS/管理端、OIDC discovery 和 OpenVPN 存活。异常时用备份二进制恢复并保留 Nginx 配置。旧 Go 实例迁移另行安排维护窗口:先准备证书与 Nginx 配置及回滚方案,再停 Go 公网监听、将模式改为 nginx、启动 Nginx 和管理进程并核对回调;不得直接在页面切换。
|
||||
|
||||
## 7. 待决问题
|
||||
|
||||
| 问题 | 是否阻塞 | 推荐方案 |
|
||||
|---|---|---|
|
||||
| 何时收紧 18080 公网访问 | 否 | 核实旧客户端地址迁移后再改为回环监听并调整安全组 |
|
||||
@@ -900,7 +900,7 @@
|
||||
"domain": "Configured domain",
|
||||
"activeDomain": "Active domain",
|
||||
"saveDomain": "Save domain",
|
||||
"restartRequired": "Gateway mode saved; switching between Go and Nginx requires restarting the management service",
|
||||
"restartRequired": "Public entry saved; check the gateway site status",
|
||||
"siteActivationRequired": "Entry saved; prepare and enable the Nginx site below for first use",
|
||||
"pendingEntry": "Nginx still serves {address}; save to validate and apply the new entry",
|
||||
"applied": "Public entry updated; Nginx configuration is active",
|
||||
@@ -917,14 +917,11 @@
|
||||
"automatic": "Automatic (autocert)",
|
||||
"ports": "HTTP / HTTPS ports",
|
||||
"externalPorts": "Managed by external Nginx; listener status not checked",
|
||||
"setupHint": "Changes to an active Nginx gateway apply on save. Prepare a certificate for initial setup. Only switching between Go and Nginx modes requires a restart.",
|
||||
"setupHint": "Nginx owns the public gateway. Active sites reload on save; initial HTTPS setup needs a certificate.",
|
||||
"refresh": "Refresh status",
|
||||
"openHttps": "Open HTTPS health check",
|
||||
"loadFailed": "Could not load certificate status",
|
||||
"mode": "Public entry mode",
|
||||
"goMode": "Go automatic certificate (binds 80/443)",
|
||||
"nginxMode": "Nginx proxy (external 80/443)",
|
||||
"saveMode": "Save entry mode",
|
||||
"legacyModeHint": "This server still uses the legacy Go public listener. Migrate to Nginx using the deployment guide before editing the gateway.",
|
||||
"nginxHint": "An active Nginx gateway reloads on save. Initial setup requires enabling the site separately; the management service remains on port 18080.",
|
||||
"externalRenewal": "Managed by Certbot",
|
||||
"previewNginx": "Preview Nginx configuration",
|
||||
@@ -940,13 +937,13 @@
|
||||
"siteReady": "HTTPS site and certificate are ready",
|
||||
"siteFailed": "Gateway or certificate setup failed; see output",
|
||||
"directPortWarning": "The management service still listens publicly on port 18080, bypassing Nginx path rules. Migrate clients using the old HTTP address, then bind the service to 127.0.0.1:18080.",
|
||||
"setupTitle": "Optional HTTPS public gateway",
|
||||
"setupTitle": "Nginx gateway and certificate",
|
||||
"setupStepCheck": "Detect system",
|
||||
"setupStepInstall": "Install dependencies",
|
||||
"setupStepConfigure": "Configure gateway and certificate",
|
||||
"detectedSystem": "Server OS",
|
||||
"setupUnsupported": "Automatic setup requires root on Ubuntu or TencentOS Server 4",
|
||||
"setupScope": "Use only if you choose the Nginx gateway. Installs Nginx / Certbot and prepares the validation directory. It does not install VPN protocol dependencies, enable ports, or issue certificates.",
|
||||
"setupScope": "Installs Nginx / Certbot and prepares the validation directory. It does not install VPN protocol dependencies, enable ports, or issue certificates.",
|
||||
"setupButton": "Prepare Nginx / Certbot",
|
||||
"setupComplete": "Dependencies are ready; configure the site and certificate next",
|
||||
"setupFailed": "Initialization failed; inspect the output",
|
||||
|
||||
@@ -900,7 +900,7 @@
|
||||
"domain": "配置域名",
|
||||
"activeDomain": "当前监听域名",
|
||||
"saveDomain": "保存域名",
|
||||
"restartRequired": "入口模式已保存;切换 Go 与 Nginx 模式需重启管理服务",
|
||||
"restartRequired": "公网入口配置已保存;请检查站点状态",
|
||||
"siteActivationRequired": "入口已保存;首次启用站点请到下方准备并启用 Nginx",
|
||||
"pendingEntry": "当前 Nginx 仍使用 {address};点击保存将校验并更新入口",
|
||||
"applied": "公网入口已更新,Nginx 配置已生效",
|
||||
@@ -917,14 +917,11 @@
|
||||
"automatic": "自动(autocert)",
|
||||
"ports": "HTTP / HTTPS 端口",
|
||||
"externalPorts": "由外部 Nginx 管理,尚未检测监听状态",
|
||||
"setupHint": "已启用的 Nginx 入口保存后立即生效;首次启用站点需准备证书。只有切换 Go 与 Nginx 模式才需重启。",
|
||||
"setupHint": "由 Nginx 管理公网入口;已启用的站点保存后立即重载,首次启用 HTTPS 需准备证书。",
|
||||
"refresh": "刷新状态",
|
||||
"openHttps": "打开 HTTPS 健康检查",
|
||||
"loadFailed": "读取证书状态失败",
|
||||
"mode": "公网入口模式",
|
||||
"goMode": "Go 自动证书(进程监听 80/443)",
|
||||
"nginxMode": "Nginx 代理(外部监听 80/443)",
|
||||
"saveMode": "保存入口模式",
|
||||
"legacyModeHint": "当前仍由旧版 Go 模式直接监听公网。请按部署文档迁移至 Nginx 后修改入口。",
|
||||
"nginxHint": "已启用的 Nginx 入口保存后立即重载;首次配置需单独启用站点。管理服务继续监听 18080。",
|
||||
"externalRenewal": "由 Certbot 管理",
|
||||
"previewNginx": "预览 Nginx 配置",
|
||||
@@ -940,13 +937,13 @@
|
||||
"siteReady": "HTTPS 站点与证书已就绪",
|
||||
"siteFailed": "站点或证书配置失败,请查看输出",
|
||||
"directPortWarning": "管理服务仍直接监听公网 18080,访问该端口会绕过 Nginx 的路径限制。请先迁移使用旧 HTTP 地址的客户端,再将管理服务改为仅监听 127.0.0.1:18080。",
|
||||
"setupTitle": "可选 · HTTPS 公网入口",
|
||||
"setupTitle": "Nginx 入口与证书",
|
||||
"setupStepCheck": "检测系统",
|
||||
"setupStepInstall": "安装依赖",
|
||||
"setupStepConfigure": "配置入口与证书",
|
||||
"detectedSystem": "服务器系统",
|
||||
"setupUnsupported": "自动初始化仅支持以 root 运行的 Ubuntu 或 TencentOS Server 4",
|
||||
"setupScope": "仅在选择 Nginx 入口时点击:按需安装 Nginx / Certbot 并准备验证目录;不会安装 VPN 协议依赖、启用端口或签发证书。",
|
||||
"setupScope": "按需安装 Nginx / Certbot 并准备验证目录;不会安装 VPN 协议依赖、启用端口或签发证书。",
|
||||
"setupButton": "准备 Nginx / Certbot",
|
||||
"setupComplete": "依赖已准备,继续配置站点与证书",
|
||||
"setupFailed": "初始化失败,请查看输出",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<el-alert type="info" :closable="false" :title="$t('tls.publicEntryDesc')" style="margin-bottom:16px" />
|
||||
<el-alert v-if="legacyMode" type="warning" :closable="false" :title="$t('tls.legacyModeHint')" style="margin-bottom:16px" />
|
||||
|
||||
<el-form label-position="top" @submit.prevent="savePublicEntry">
|
||||
<el-form-item :label="$t('tls.scheme')">
|
||||
@@ -44,18 +45,12 @@
|
||||
</el-input>
|
||||
<div class="tls-hint">{{ $t('tls.publicBaseHint') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('tls.mode')">
|
||||
<el-select v-model="entry.mode" style="width:100%">
|
||||
<el-option value="autocert" :label="$t('tls.goMode')" />
|
||||
<el-option value="nginx" :label="$t('tls.nginxMode')" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="tlsSaving" @click="savePublicEntry">{{ $t('tls.saveEntry') }}</el-button>
|
||||
<el-button type="primary" :loading="tlsSaving" :disabled="legacyMode" @click="savePublicEntry">{{ $t('tls.saveEntry') }}</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-alert v-if="tlsStatus?.restart_required" type="warning" :closable="false" :title="$t('tls.restartRequired')" style="margin-top:12px" />
|
||||
<el-alert v-else-if="entry.mode === 'nginx' && nginxSetup?.site_stage === 'none' && tlsStatus?.configured_domain" type="info" :closable="false" :title="$t('tls.siteActivationRequired')" style="margin-top:12px" />
|
||||
<el-alert v-else-if="entry.mode === 'nginx' && tlsStatus?.public_base && tlsStatus?.configured_public_base !== tlsStatus?.public_base" type="warning" :closable="false" :title="$t('tls.pendingEntry', { address: tlsStatus.public_base })" style="margin-top:12px" />
|
||||
<el-alert v-else-if="!legacyMode && nginxSetup?.site_stage === 'none' && tlsStatus?.configured_domain" type="info" :closable="false" :title="$t('tls.siteActivationRequired')" style="margin-top:12px" />
|
||||
<el-alert v-else-if="!legacyMode && tlsStatus?.public_base && tlsStatus?.configured_public_base !== tlsStatus?.public_base" type="warning" :closable="false" :title="$t('tls.pendingEntry', { address: tlsStatus.public_base })" style="margin-top:12px" />
|
||||
<el-alert v-if="tlsStatus?.management_direct_public" type="warning" :closable="false" :title="$t('tls.directPortWarning')" style="margin-top:12px" />
|
||||
</el-card>
|
||||
|
||||
@@ -64,7 +59,7 @@
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<b>{{ $t('tls.certListTitle') }}</b>
|
||||
<el-button type="primary" @click="openAddCert">{{ $t('tls.addCert') }}</el-button>
|
||||
<el-button type="primary" :disabled="legacyMode" @click="openAddCert">{{ $t('tls.addCert') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="certRows" size="default" v-loading="tlsLoading">
|
||||
@@ -100,7 +95,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- Nginx 准备(折叠) -->
|
||||
<el-collapse v-if="entry.mode === 'nginx'">
|
||||
<el-collapse v-if="!legacyMode">
|
||||
<el-collapse-item :title="$t('tls.setupTitle')" name="setup">
|
||||
<el-steps :active="nginxSetup?.site_stage === 'https' ? 3 : nginxSetup?.nginx_installed && nginxSetup?.certbot_installed && nginxSetup?.webroot_ready ? 2 : 1" finish-status="success" style="margin-bottom:16px">
|
||||
<el-step :title="$t('tls.setupStepCheck')" />
|
||||
@@ -229,12 +224,12 @@ const nginxActivateLog = ref('')
|
||||
const nginxActivateLogView = ref(null)
|
||||
const certDialog = ref(false)
|
||||
const certSubmitting = ref(false)
|
||||
const legacyMode = computed(() => tlsStatus.value?.mode === 'autocert' || tlsStatus.value?.configured_mode === 'autocert')
|
||||
|
||||
const entry = reactive({
|
||||
scheme: 'http',
|
||||
host: '',
|
||||
port: 80,
|
||||
mode: 'nginx',
|
||||
})
|
||||
|
||||
const certForm = reactive({
|
||||
@@ -270,7 +265,6 @@ const certRows = computed(() => {
|
||||
|
||||
function onSchemeChange() {
|
||||
entry.port = defaultPort(entry.scheme)
|
||||
if (entry.scheme === 'http') entry.mode = 'nginx'
|
||||
}
|
||||
|
||||
function resetCertForm() {
|
||||
@@ -374,7 +368,6 @@ async function loadTLS() {
|
||||
entry.scheme = scheme === 'http' ? 'http' : 'https'
|
||||
const port = Number(data.configured_port || data.port)
|
||||
entry.port = port > 0 ? port : defaultPort(entry.scheme)
|
||||
entry.mode = entry.host ? (data.configured_mode || data.mode || 'autocert') : 'nginx'
|
||||
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.loadFailed')) }
|
||||
finally { tlsLoading.value = false }
|
||||
}
|
||||
@@ -383,11 +376,6 @@ async function savePublicEntry() {
|
||||
if (!entry.host) return ElMessage.warning(t('tls.hostRequired'))
|
||||
tlsSaving.value = true
|
||||
try {
|
||||
let modeRes = null
|
||||
if (entry.mode !== tlsStatus.value?.configured_mode) {
|
||||
const result = await api.put('/system/tls/mode', { mode: entry.mode })
|
||||
modeRes = result.data
|
||||
}
|
||||
const { data: domainRes } = await api.put('/system/tls/domain', {
|
||||
domain: entry.host,
|
||||
scheme: entry.scheme,
|
||||
@@ -395,7 +383,7 @@ async function savePublicEntry() {
|
||||
})
|
||||
tlsStatus.value = domainRes.status
|
||||
nginxPreview.value = null
|
||||
if (domainRes.restart_required || modeRes?.restart_required) ElMessage.success(t('tls.restartRequired'))
|
||||
if (domainRes.restart_required) ElMessage.success(t('tls.restartRequired'))
|
||||
else if (domainRes.site_activation_required) ElMessage.success(t('tls.siteActivationRequired'))
|
||||
else ElMessage.success(t(domainRes.applied ? 'tls.applied' : 'tls.saved'))
|
||||
await refreshAll()
|
||||
@@ -463,17 +451,13 @@ async function submitCert() {
|
||||
await loadTLS()
|
||||
return
|
||||
}
|
||||
// HTTP-01: ensure domain saved, nginx mode, then activate
|
||||
// HTTP-01: save the public domain, then activate the Nginx site.
|
||||
const host = (certForm.host || entry.host || '').trim()
|
||||
if (!host) return ElMessage.warning(t('tls.hostRequired'))
|
||||
entry.host = host.split(/[\s,]+/)[0]
|
||||
await api.put('/system/tls/domain', {
|
||||
domain: entry.host, scheme: entry.scheme, port: entry.port,
|
||||
})
|
||||
if (entry.mode !== 'nginx') {
|
||||
await api.put('/system/tls/mode', { mode: 'nginx' })
|
||||
entry.mode = 'nginx'
|
||||
}
|
||||
await streamActivate()
|
||||
ElMessage.success(t('tls.siteReady'))
|
||||
certDialog.value = false
|
||||
|
||||
Reference in new issue
Block a user