From 56976bbf88b8d884c21b27726949a126565138a0 Mon Sep 17 00:00:00 2001 From: suantea Date: Fri, 11 Sep 2026 15:55:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(caddy):=20=E5=90=8C=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E5=A4=9A=E7=AB=99=E7=82=B9=E5=90=88=E5=B9=B6=E4=B8=BA=E5=8D=95?= =?UTF-8?q?=E4=B8=AA=20Caddy=20server=EF=BC=8C=E4=BF=AE=E5=A4=8D=20Admin?= =?UTF-8?q?=20API=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 每个站点此前独立注册 server(netpanel_),多个站点配置相同监听端口 (如 :42443)时 Caddy 报 listener address repeated,配置加载失败返回 500。 - Start/Stop/UpdateUpstream 改为按端口重建服务器:查询该端口所有 enable+running 的站点,把路由与 TLS 策略合并进同一 server (netpanel_port_) - orderRoutes 保证 host matcher 路由排在 catch-all 之前 - UpdateUpstream 热切换以 overrides 参数注入临时上游,不写库 - 端口下无运行站点时删除对应 server 修复 #97 --- backend/service/caddy/manager.go | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/backend/service/caddy/manager.go b/backend/service/caddy/manager.go index 2899ab2b..2dcc289a 100644 --- a/backend/service/caddy/manager.go +++ b/backend/service/caddy/manager.go @@ -854,6 +854,90 @@ func (m *Manager) setError(id uint, errMsg string) { }) } +// rebuildPortServer 重建指定端口的 Caddy 服务器,将该端口所有运行中的站点合并到同一 server。 +// overrides 提供临时上游地址覆盖(key=站点 ID),不写库,仅用于 UpdateUpstream 热切换。 +func (m *Manager) rebuildPortServer(port int, overrides map[uint]string) error { + var sites []model.CaddySite + m.db.Where("port = ? AND enable = ? AND status = ?", port, true, "running").Find(&sites) + + if len(sites) == 0 { + serverKey := fmt.Sprintf("netpanel_port_%d", port) + m.adminRequest("DELETE", fmt.Sprintf("/config/apps/http/servers/%s", serverKey), nil) + return nil + } + + var allRoutes []interface{} + var allTLSPolicies []interface{} + + for i := range sites { + site := &sites[i] + if overrides != nil { + if up, ok := overrides[site.ID]; ok { + site.UpstreamAddr = up + } + } + + routes, err := m.buildRoutes(site, m.loadRouteDeps([]model.CaddySite{*site})) + if err != nil { + m.log.Errorf("[Caddy] 站点 [%s](%d) 构建路由失败: %v", site.Name, site.ID, err) + continue + } + allRoutes = append(allRoutes, routes...) + + if site.TLSEnable { + if tlsCfg := m.buildTLSConfig(site); tlsCfg != nil { + allTLSPolicies = append(allTLSPolicies, tlsCfg) + } + } + } + + // 排序:host matcher 在前,无 matcher(catch-all)在后 + allRoutes = orderRoutes(allRoutes) + + serverCfg := map[string]interface{}{ + "listen": []string{fmt.Sprintf(":%d", port)}, + "routes": allRoutes, + "automatic_https": map[string]interface{}{ + "disable": true, + }, + "logs": map[string]interface{}{ + "default_logger_name": fmt.Sprintf("netpanel_port_%d", port), + }, + } + if len(allTLSPolicies) > 0 { + serverCfg["tls_connection_policies"] = allTLSPolicies + } + + if cfgJSON, err := json.MarshalIndent(serverCfg, "", " "); err == nil { + m.log.Infof("[Caddy] 端口 %d 服务器配置(共 %d 个站点):\n%s", port, len(sites), string(cfgJSON)) + } + + serverKey := fmt.Sprintf("netpanel_port_%d", port) + m.adminRequest("DELETE", fmt.Sprintf("/config/apps/http/servers/%s", serverKey), nil) + if err := m.adminRequest("PUT", fmt.Sprintf("/config/apps/http/servers/%s", serverKey), serverCfg); err != nil { + return fmt.Errorf("加载端口 %d 服务器配置失败: %w", port, err) + } + return nil +} + +// orderRoutes 将有 host matcher 的路由排在 catch-all 之前,避免后者抢先匹配。 +func orderRoutes(routes []interface{}) []interface{} { + var matched, catchAll []interface{} + for _, r := range routes { + route, ok := r.(map[string]interface{}) + if !ok { + catchAll = append(catchAll, r) + continue + } + if _, hasMatch := route["match"]; hasMatch { + matched = append(matched, r) + } else { + catchAll = append(catchAll, r) + } + } + return append(matched, catchAll...) +} + // GetCaddyDataDir 获取 Caddy 数据目录 func (m *Manager) GetCaddyDataDir() string { return filepath.Join(m.dataDir, "caddy")