mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +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:
parent
42d7f62d8b
commit
55d6729955
8 changed files with 209 additions and 17 deletions
|
|
@ -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": {
|
"/panel/api/server/getNewX25519Cert": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"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": {
|
"/panel/api/nodes/add": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,12 @@ export const sections: readonly Section[] = [
|
||||||
summary: 'Generate a fresh UUID v4. Convenience helper for client IDs.',
|
summary: 'Generate a fresh UUID v4. Convenience helper for client IDs.',
|
||||||
response: '{\n "success": true,\n "obj": "550e8400-e29b-41d4-a716-446655440000"\n}',
|
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',
|
method: 'GET',
|
||||||
path: '/panel/api/server/getNewX25519Cert',
|
path: '/panel/api/server/getNewX25519Cert',
|
||||||
|
|
@ -741,6 +747,15 @@ export const sections: readonly Section[] = [
|
||||||
{ name: 'id', in: 'path', type: 'number', desc: 'Node ID.' },
|
{ 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',
|
method: 'POST',
|
||||||
path: '/panel/api/nodes/add',
|
path: '/panel/api/nodes/add',
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ export default function InboundFormModal({
|
||||||
setCertFromPanel,
|
setCertFromPanel,
|
||||||
clearCertFiles,
|
clearCertFiles,
|
||||||
onSecurityChange,
|
onSecurityChange,
|
||||||
} = useSecurityActions({ form, setSaving, messageApi });
|
} = useSecurityActions({ form, setSaving, messageApi, nodeId: typeof wNodeId === 'number' ? wNodeId : null });
|
||||||
|
|
||||||
const toggleExternalProxy = (on: boolean) => {
|
const toggleExternalProxy = (on: boolean) => {
|
||||||
if (on) {
|
if (on) {
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,17 @@ interface UseSecurityActionsArgs {
|
||||||
form: FormInstance<InboundFormValues>;
|
form: FormInstance<InboundFormValues>;
|
||||||
setSaving: Dispatch<SetStateAction<boolean>>;
|
setSaving: Dispatch<SetStateAction<boolean>>;
|
||||||
messageApi: MessageInstance;
|
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
|
// Server-side TLS / Reality key + certificate generation handlers for the
|
||||||
// inbound modal's security tab. Each talks to a /panel server endpoint and
|
// 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
|
// writes the result back into the form. Lifted out of InboundFormModal so
|
||||||
// the modal body stays focused on orchestration.
|
// 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 { t } = useTranslation();
|
||||||
|
|
||||||
const genRealityKeypair = async () => {
|
const genRealityKeypair = async () => {
|
||||||
|
|
@ -112,22 +116,28 @@ export function useSecurityActions({ form, setSaving, messageApi }: UseSecurityA
|
||||||
const setCertFromPanel = async (certName: number) => {
|
const setCertFromPanel = async (certName: number) => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
|
// Node-assigned inbounds run on the node, so their cert files must be the
|
||||||
if (msg?.success) {
|
// node's own paths (fetched through the central panel), not this panel's.
|
||||||
const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
|
const msg = typeof nodeId === 'number'
|
||||||
if (!obj.webCertFile && !obj.webKeyFile) {
|
? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
|
||||||
messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
|
: await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
|
||||||
return;
|
if (!msg?.success) {
|
||||||
}
|
messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
|
||||||
form.setFieldValue(
|
return;
|
||||||
['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
|
|
||||||
obj.webCertFile ?? '',
|
|
||||||
);
|
|
||||||
form.setFieldValue(
|
|
||||||
['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
|
|
||||||
obj.webKeyFile ?? '',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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 {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ func NewNodeController(g *gin.RouterGroup) *NodeController {
|
||||||
func (a *NodeController) initRouter(g *gin.RouterGroup) {
|
func (a *NodeController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/list", a.list)
|
g.GET("/list", a.list)
|
||||||
g.GET("/get/:id", a.get)
|
g.GET("/get/:id", a.get)
|
||||||
|
g.GET("/webCert/:id", a.webCert)
|
||||||
|
|
||||||
g.POST("/add", a.add)
|
g.POST("/add", a.add)
|
||||||
g.POST("/update/:id", a.update)
|
g.POST("/update/:id", a.update)
|
||||||
|
|
@ -64,6 +65,22 @@ func (a *NodeController) get(c *gin.Context) {
|
||||||
jsonObj(c, n, nil)
|
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 {
|
func (a *NodeController) ensureReachable(c *gin.Context, n *model.Node) error {
|
||||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/getConfigJson", a.getConfigJson)
|
g.GET("/getConfigJson", a.getConfigJson)
|
||||||
g.GET("/getDb", a.getDb)
|
g.GET("/getDb", a.getDb)
|
||||||
g.GET("/getNewUUID", a.getNewUUID)
|
g.GET("/getNewUUID", a.getNewUUID)
|
||||||
|
g.GET("/getWebCertFiles", a.getWebCertFiles)
|
||||||
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
||||||
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||||
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||||
|
|
@ -314,6 +315,24 @@ func (a *ServerController) importDB(c *gin.Context) {
|
||||||
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
|
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.
|
// getNewX25519Cert generates a new X25519 certificate.
|
||||||
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||||
cert, err := a.serverService.GetNewX25519Cert()
|
cert, err := a.serverService.GetNewX25519Cert()
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,28 @@ func (r *Remote) UpdatePanel(ctx context.Context) error {
|
||||||
return err
|
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 {
|
func (r *Remote) ResetClientTraffic(ctx context.Context, _ *model.Inbound, email string) error {
|
||||||
_, err := r.do(ctx, http.MethodPost,
|
_, err := r.do(ctx, http.MethodPost,
|
||||||
"panel/api/clients/resetTraffic/"+url.PathEscape(email), nil)
|
"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
|
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
|
// 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.
|
// node so the UI can show per-node success/failure for a bulk request.
|
||||||
type NodeUpdateResult struct {
|
type NodeUpdateResult struct {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue