mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-04 03:19:34 +00:00
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
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user