mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-18 12:05:53 +00:00
fix: preserve TLS cert file paths when deploying inbound to remote node
When creating a Hysteria (or any TLS-required) inbound from the central panel and deploying it to a remote node, sanitizeStreamSettingsForRemote was unconditionally stripping certificateFile / keyFile from the TLS settings. This left Xray on the remote node with a TLS block containing no certificate, causing Xray to crash and the inbounds page to hang. The fix: only strip cert file paths when inline certificate content (certificate / key arrays) is also present in the same entry — those file paths are then truly redundant. When only file paths are present the user explicitly entered paths that live on the remote node's filesystem; they are now passed through untouched. Fixes #4370
This commit is contained in:
parent
ae6f13b533
commit
1f052c0e8f
2 changed files with 129 additions and 6 deletions
|
|
@ -344,10 +344,15 @@ func wireInbound(ib *model.Inbound) url.Values {
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeStreamSettingsForRemote strips file-based TLS certificate paths
|
// sanitizeStreamSettingsForRemote strips file-based TLS certificate paths
|
||||||
// from the StreamSettings before sending to a remote node. File paths
|
// from the StreamSettings before sending to a remote node, but ONLY when
|
||||||
// (certificateFile / keyFile) are local to the main panel's filesystem
|
// inline certificate content (certificate / key) is also present in the same
|
||||||
// and will cause Xray on the remote node to crash if they don't exist there.
|
// entry. In that case the file paths are redundant and stripping them avoids
|
||||||
// Inline certificate content (certificate / key) is kept intact.
|
// confusion when the central panel's local paths don't exist on the remote.
|
||||||
|
//
|
||||||
|
// When a certificate entry contains ONLY file paths (no inline content) the
|
||||||
|
// paths are left untouched: the user explicitly entered paths that exist on
|
||||||
|
// the remote node's filesystem, and removing them would leave Xray with TLS
|
||||||
|
// configured but no certificate, causing Xray to crash on the remote node.
|
||||||
func sanitizeStreamSettingsForRemote(streamSettings string) string {
|
func sanitizeStreamSettingsForRemote(streamSettings string) string {
|
||||||
if streamSettings == "" {
|
if streamSettings == "" {
|
||||||
return streamSettings
|
return streamSettings
|
||||||
|
|
@ -368,18 +373,40 @@ func sanitizeStreamSettingsForRemote(streamSettings string) string {
|
||||||
return streamSettings
|
return streamSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changed := false
|
||||||
for _, cert := range certificates {
|
for _, cert := range certificates {
|
||||||
c, ok := cert.(map[string]any)
|
c, ok := cert.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Only strip file paths when inline content is present so that the
|
||||||
|
// remote Xray still has a valid certificate to use.
|
||||||
|
hasCertFile := c["certificateFile"] != nil && c["certificateFile"] != ""
|
||||||
|
hasKeyFile := c["keyFile"] != nil && c["keyFile"] != ""
|
||||||
|
hasCertInline := isNonEmptySlice(c["certificate"])
|
||||||
|
hasKeyInline := isNonEmptySlice(c["key"])
|
||||||
|
if hasCertFile && hasCertInline {
|
||||||
delete(c, "certificateFile")
|
delete(c, "certificateFile")
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if hasKeyFile && hasKeyInline {
|
||||||
delete(c, "keyFile")
|
delete(c, "keyFile")
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
return streamSettings
|
||||||
|
}
|
||||||
out, err := json.Marshal(stream)
|
out, err := json.Marshal(stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return streamSettings
|
return streamSettings
|
||||||
}
|
}
|
||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNonEmptySlice reports whether v is a non-nil, non-empty JSON array value.
|
||||||
|
func isNonEmptySlice(v any) bool {
|
||||||
|
s, ok := v.([]any)
|
||||||
|
return ok && len(s) > 0
|
||||||
|
}
|
||||||
|
|
|
||||||
96
web/runtime/remote_test.go
Normal file
96
web/runtime/remote_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSanitizeStreamSettingsForRemote(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
// wantCertFile / wantKeyFile: expected presence after sanitize
|
||||||
|
wantCertFile bool
|
||||||
|
wantKeyFile bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "file paths only — kept intact (remote node paths)",
|
||||||
|
input: `{
|
||||||
|
"tlsSettings": {
|
||||||
|
"certificates": [{
|
||||||
|
"certificateFile": "/etc/ssl/cert.crt",
|
||||||
|
"keyFile": "/etc/ssl/key.key"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantCertFile: true,
|
||||||
|
wantKeyFile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inline content only — unchanged",
|
||||||
|
input: `{
|
||||||
|
"tlsSettings": {
|
||||||
|
"certificates": [{
|
||||||
|
"certificate": ["-----BEGIN CERTIFICATE-----"],
|
||||||
|
"key": ["-----BEGIN PRIVATE KEY-----"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantCertFile: false,
|
||||||
|
wantKeyFile: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both file paths and inline content — file paths stripped (redundant)",
|
||||||
|
input: `{
|
||||||
|
"tlsSettings": {
|
||||||
|
"certificates": [{
|
||||||
|
"certificateFile": "/etc/ssl/cert.crt",
|
||||||
|
"keyFile": "/etc/ssl/key.key",
|
||||||
|
"certificate": ["-----BEGIN CERTIFICATE-----"],
|
||||||
|
"key": ["-----BEGIN PRIVATE KEY-----"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantCertFile: false,
|
||||||
|
wantKeyFile: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty stream settings",
|
||||||
|
input: "",
|
||||||
|
// empty input returns empty, nothing to check
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.input == "" {
|
||||||
|
if got := sanitizeStreamSettingsForRemote(tc.input); got != "" {
|
||||||
|
t.Errorf("expected empty string, got %q", got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := sanitizeStreamSettingsForRemote(tc.input)
|
||||||
|
var out map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(got), &out); err != nil {
|
||||||
|
t.Fatalf("output is not valid JSON: %v\noutput: %s", err, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
tls, _ := out["tlsSettings"].(map[string]any)
|
||||||
|
certs, _ := tls["certificates"].([]any)
|
||||||
|
if len(certs) == 0 {
|
||||||
|
t.Fatal("certificates array missing in output")
|
||||||
|
}
|
||||||
|
cert, _ := certs[0].(map[string]any)
|
||||||
|
|
||||||
|
_, hasCertFile := cert["certificateFile"]
|
||||||
|
_, hasKeyFile := cert["keyFile"]
|
||||||
|
|
||||||
|
if hasCertFile != tc.wantCertFile {
|
||||||
|
t.Errorf("certificateFile present=%v, want %v", hasCertFile, tc.wantCertFile)
|
||||||
|
}
|
||||||
|
if hasKeyFile != tc.wantKeyFile {
|
||||||
|
t.Errorf("keyFile present=%v, want %v", hasKeyFile, tc.wantKeyFile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue