diff --git a/web/runtime/remote.go b/web/runtime/remote.go index 184e632e..c71714e7 100644 --- a/web/runtime/remote.go +++ b/web/runtime/remote.go @@ -334,7 +334,7 @@ func wireInbound(ib *model.Inbound) url.Values { v.Set("port", strconv.Itoa(ib.Port)) v.Set("protocol", string(ib.Protocol)) v.Set("settings", ib.Settings) - v.Set("streamSettings", ib.StreamSettings) + v.Set("streamSettings", sanitizeStreamSettingsForRemote(ib.StreamSettings)) v.Set("tag", ib.Tag) v.Set("sniffing", ib.Sniffing) if ib.TrafficReset != "" { @@ -342,3 +342,44 @@ func wireInbound(ib *model.Inbound) url.Values { } return v } + +// sanitizeStreamSettingsForRemote strips file-based TLS certificate paths +// from the StreamSettings before sending to a remote node. File paths +// (certificateFile / keyFile) are local to the main panel's filesystem +// and will cause Xray on the remote node to crash if they don't exist there. +// Inline certificate content (certificate / key) is kept intact. +func sanitizeStreamSettingsForRemote(streamSettings string) string { + if streamSettings == "" { + return streamSettings + } + + var stream map[string]any + if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil { + return streamSettings + } + + tlsSettings, ok := stream["tlsSettings"].(map[string]any) + if !ok { + return streamSettings + } + + certificates, ok := tlsSettings["certificates"].([]any) + if !ok { + return streamSettings + } + + for _, cert := range certificates { + c, ok := cert.(map[string]any) + if !ok { + continue + } + delete(c, "certificateFile") + delete(c, "keyFile") + } + + out, err := json.Marshal(stream) + if err != nil { + return streamSettings + } + return string(out) +}