Selaa lähdekoodia

fix(nodes): Set Cert from Panel uses the node's own web cert for node inbounds

For an inbound deployed to a node, the button read the central panel's webCertFile/webKeyFile and inserted paths that don't exist on the node, crashing the node's Xray on startup.

Add a token-accessible GET /panel/api/server/getWebCertFiles that returns a panel's own web cert/key paths, Remote.GetWebCertFiles to fetch it from a node, and GET /panel/api/nodes/webCert/:id to proxy it. setCertFromPanel now calls the node endpoint for a node-assigned inbound and the local settings otherwise, warning instead of inserting wrong paths on error/empty.

Fixes #4854
MHSanaei 1 päivä sitten
vanhempi
sitoutus
55d6729955

+ 85 - 0
frontend/public/openapi.json

@@ -1529,6 +1529,43 @@
         }
       }
     },
+    "/panel/api/server/getWebCertFiles": {
+      "get": {
+        "tags": [
+          "Server"
+        ],
+        "summary": "Return this panel's own web TLS certificate and key file paths. The central panel calls it on a node (via the node API token) so \"Set Cert from Panel\" fills a node-assigned inbound with paths that exist on the node.",
+        "operationId": "get_panel_api_server_getWebCertFiles",
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": {
+                    "webCertFile": "/root/cert/example.com/fullchain.pem",
+                    "webKeyFile": "/root/cert/example.com/privkey.pem"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/panel/api/server/getNewX25519Cert": {
       "get": {
         "tags": [
@@ -4016,6 +4053,54 @@
         }
       }
     },
+    "/panel/api/nodes/webCert/{id}": {
+      "get": {
+        "tags": [
+          "Nodes"
+        ],
+        "summary": "Fetch a node's own web TLS certificate/key file paths (proxied to the node). Used by the inbound form's \"Set Cert from Panel\" so a node-assigned inbound gets paths that exist on the node, not the central panel.",
+        "operationId": "get_panel_api_nodes_webCert_id",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "description": "Node ID.",
+            "schema": {
+              "type": "integer"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": {
+                    "webCertFile": "/root/cert/example.com/fullchain.pem",
+                    "webKeyFile": "/root/cert/example.com/privkey.pem"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/panel/api/nodes/add": {
       "post": {
         "tags": [

+ 15 - 0
frontend/src/pages/api-docs/endpoints.ts

@@ -313,6 +313,12 @@ export const sections: readonly Section[] = [
         summary: 'Generate a fresh UUID v4. Convenience helper for client IDs.',
         response: '{\n  "success": true,\n  "obj": "550e8400-e29b-41d4-a716-446655440000"\n}',
       },
+      {
+        method: 'GET',
+        path: '/panel/api/server/getWebCertFiles',
+        summary: 'Return this panel\'s own web TLS certificate and key file paths. The central panel calls it on a node (via the node API token) so "Set Cert from Panel" fills a node-assigned inbound with paths that exist on the node.',
+        response: '{\n  "success": true,\n  "obj": {\n    "webCertFile": "/root/cert/example.com/fullchain.pem",\n    "webKeyFile": "/root/cert/example.com/privkey.pem"\n  }\n}',
+      },
       {
         method: 'GET',
         path: '/panel/api/server/getNewX25519Cert',
@@ -741,6 +747,15 @@ export const sections: readonly Section[] = [
           { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' },
         ],
       },
+      {
+        method: 'GET',
+        path: '/panel/api/nodes/webCert/:id',
+        summary: 'Fetch a node\'s own web TLS certificate/key file paths (proxied to the node). Used by the inbound form\'s "Set Cert from Panel" so a node-assigned inbound gets paths that exist on the node, not the central panel.',
+        params: [
+          { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' },
+        ],
+        response: '{\n  "success": true,\n  "obj": {\n    "webCertFile": "/root/cert/example.com/fullchain.pem",\n    "webKeyFile": "/root/cert/example.com/privkey.pem"\n  }\n}',
+      },
       {
         method: 'POST',
         path: '/panel/api/nodes/add',

+ 1 - 1
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -194,7 +194,7 @@ export default function InboundFormModal({
     setCertFromPanel,
     clearCertFiles,
     onSecurityChange,
-  } = useSecurityActions({ form, setSaving, messageApi });
+  } = useSecurityActions({ form, setSaving, messageApi, nodeId: typeof wNodeId === 'number' ? wNodeId : null });
 
   const toggleExternalProxy = (on: boolean) => {
     if (on) {

+ 26 - 16
frontend/src/pages/inbounds/form/useSecurityActions.ts

@@ -13,13 +13,17 @@ interface UseSecurityActionsArgs {
   form: FormInstance<InboundFormValues>;
   setSaving: Dispatch<SetStateAction<boolean>>;
   messageApi: MessageInstance;
+  // Node the inbound is deployed to (null = central panel). "Set Cert from
+  // Panel" must read the node's own cert paths for a node-assigned inbound —
+  // the central panel's paths don't exist on the node. See issue #4854.
+  nodeId: number | null;
 }
 
 // Server-side TLS / Reality key + certificate generation handlers for the
 // inbound modal's security tab. Each talks to a /panel server endpoint and
 // writes the result back into the form. Lifted out of InboundFormModal so
 // the modal body stays focused on orchestration.
-export function useSecurityActions({ form, setSaving, messageApi }: UseSecurityActionsArgs) {
+export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseSecurityActionsArgs) {
   const { t } = useTranslation();
 
   const genRealityKeypair = async () => {
@@ -112,22 +116,28 @@ export function useSecurityActions({ form, setSaving, messageApi }: UseSecurityA
   const setCertFromPanel = async (certName: number) => {
     setSaving(true);
     try {
-      const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
-      if (msg?.success) {
-        const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
-        if (!obj.webCertFile && !obj.webKeyFile) {
-          messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
-          return;
-        }
-        form.setFieldValue(
-          ['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
-          obj.webCertFile ?? '',
-        );
-        form.setFieldValue(
-          ['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
-          obj.webKeyFile ?? '',
-        );
+      // Node-assigned inbounds run on the node, so their cert files must be the
+      // node's own paths (fetched through the central panel), not this panel's.
+      const msg = typeof nodeId === 'number'
+        ? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
+        : await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
+      if (!msg?.success) {
+        messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
+        return;
+      }
+      const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
+      if (!obj?.webCertFile && !obj?.webKeyFile) {
+        messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
+        return;
       }
+      form.setFieldValue(
+        ['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
+        obj.webCertFile ?? '',
+      );
+      form.setFieldValue(
+        ['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
+        obj.webKeyFile ?? '',
+      );
     } finally {
       setSaving(false);
     }

+ 17 - 0
web/controller/node.go

@@ -28,6 +28,7 @@ func NewNodeController(g *gin.RouterGroup) *NodeController {
 func (a *NodeController) initRouter(g *gin.RouterGroup) {
 	g.GET("/list", a.list)
 	g.GET("/get/:id", a.get)
+	g.GET("/webCert/:id", a.webCert)
 
 	g.POST("/add", a.add)
 	g.POST("/update/:id", a.update)
@@ -64,6 +65,22 @@ func (a *NodeController) get(c *gin.Context) {
 	jsonObj(c, n, nil)
 }
 
+// webCert returns the node's own web TLS certificate/key file paths so the
+// inbound form's "Set Cert from Panel" can fill paths that exist on the node.
+func (a *NodeController) webCert(c *gin.Context) {
+	id, err := strconv.Atoi(c.Param("id"))
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "get"), err)
+		return
+	}
+	files, err := a.nodeService.GetWebCertFiles(id)
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.obtain"), err)
+		return
+	}
+	jsonObj(c, files, nil)
+}
+
 func (a *NodeController) ensureReachable(c *gin.Context, n *model.Node) error {
 	ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
 	defer cancel()

+ 19 - 0
web/controller/server.go

@@ -54,6 +54,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
 	g.GET("/getConfigJson", a.getConfigJson)
 	g.GET("/getDb", a.getDb)
 	g.GET("/getNewUUID", a.getNewUUID)
+	g.GET("/getWebCertFiles", a.getWebCertFiles)
 	g.GET("/getNewX25519Cert", a.getNewX25519Cert)
 	g.GET("/getNewmldsa65", a.getNewmldsa65)
 	g.GET("/getNewmlkem768", a.getNewmlkem768)
@@ -314,6 +315,24 @@ func (a *ServerController) importDB(c *gin.Context) {
 	jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
 }
 
+// getWebCertFiles returns this panel's own web TLS certificate and key file
+// paths. The central panel calls it on a node (via the node's API token) so
+// "Set Cert from Panel" can fill a node-assigned inbound with paths that exist
+// on the node's filesystem instead of the central panel's — see issue #4854.
+func (a *ServerController) getWebCertFiles(c *gin.Context) {
+	certFile, err := a.settingService.GetCertFile()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	keyFile, err := a.settingService.GetKeyFile()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, gin.H{"webCertFile": certFile, "webKeyFile": keyFile}, nil)
+}
+
 // getNewX25519Cert generates a new X25519 certificate.
 func (a *ServerController) getNewX25519Cert(c *gin.Context) {
 	cert, err := a.serverService.GetNewX25519Cert()

+ 22 - 0
web/runtime/remote.go

@@ -328,6 +328,28 @@ func (r *Remote) UpdatePanel(ctx context.Context) error {
 	return err
 }
 
+// WebCertFiles holds a node's own web TLS certificate and key file paths.
+type WebCertFiles struct {
+	WebCertFile string `json:"webCertFile"`
+	WebKeyFile  string `json:"webKeyFile"`
+}
+
+// GetWebCertFiles fetches the node's own web TLS certificate/key file paths so
+// the central panel can offer them as the "Set Cert from Panel" default for a
+// node-assigned inbound — those paths exist on the node, the central panel's
+// don't. See issue #4854.
+func (r *Remote) GetWebCertFiles(ctx context.Context) (*WebCertFiles, error) {
+	env, err := r.do(ctx, http.MethodGet, "panel/api/server/getWebCertFiles", nil)
+	if err != nil {
+		return nil, err
+	}
+	var files WebCertFiles
+	if err := json.Unmarshal(env.Obj, &files); err != nil {
+		return nil, fmt.Errorf("decode web cert files: %w", err)
+	}
+	return &files, nil
+}
+
 func (r *Remote) ResetClientTraffic(ctx context.Context, _ *model.Inbound, email string) error {
 	_, err := r.do(ctx, http.MethodPost,
 		"panel/api/clients/resetTraffic/"+url.PathEscape(email), nil)

+ 24 - 0
web/service/node.go

@@ -382,6 +382,30 @@ func (s *NodeService) SetEnable(id int, enable bool) error {
 	return db.Model(model.Node{}).Where("id = ?", id).Update("enable", enable).Error
 }
 
+// GetWebCertFiles asks a node for its own web TLS certificate/key file paths,
+// used by "Set Cert from Panel" so a node-assigned inbound gets paths that
+// exist on the node rather than the central panel. See issue #4854.
+func (s *NodeService) GetWebCertFiles(id int) (*runtime.WebCertFiles, error) {
+	n, err := s.GetById(id)
+	if err != nil || n == nil {
+		return nil, fmt.Errorf("node not found")
+	}
+	if !n.Enable {
+		return nil, fmt.Errorf("node is disabled")
+	}
+	mgr := runtime.GetManager()
+	if mgr == nil {
+		return nil, fmt.Errorf("runtime manager unavailable")
+	}
+	remote, err := mgr.RemoteFor(n)
+	if err != nil {
+		return nil, err
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	return remote.GetWebCertFiles(ctx)
+}
+
 // NodeUpdateResult reports the outcome of triggering a panel self-update on one
 // node so the UI can show per-node success/failure for a bulk request.
 type NodeUpdateResult struct {