diff --git a/internal/web/service/xray_setting.go b/internal/web/service/xray_setting.go index 65c43fc91..7a1a4ef12 100644 --- a/internal/web/service/xray_setting.go +++ b/internal/web/service/xray_setting.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "encoding/json" "slices" + "strconv" + "strings" "github.com/mhsanaei/3x-ui/v3/internal/util/common" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -16,6 +18,11 @@ type XraySettingService struct { SettingService } +const ( + unencryptedOutboundProhibitedError = "without TLS or other encryption is prohibited unless the server address is a private IP or domain" + unencryptedOutboundMinimumVersion = "26.7.11" +) + func (s *XraySettingService) SaveXraySetting(newXraySettings string) error { // The frontend round-trips the whole getXraySetting response back // through the textarea, so if it has ever received a wrapped @@ -46,8 +53,15 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { if err := json.Unmarshal(xrayConfig.OutboundConfigs, &outbounds); err != nil { return common.NewError("xray template config invalid: outbounds is not an array:", err) } + coreVersion := "Unknown" + if p != nil { + coreVersion = p.GetXrayVersion() + } for _, outbound := range outbounds { if err := xray.ValidateOutboundConfig(outbound); err != nil { + if shouldSkipLegacyUnencryptedOutboundRejection(coreVersion, err) { + continue + } tagged := struct { Tag string `json:"tag"` }{} @@ -59,6 +73,51 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { return nil } +// shouldSkipLegacyUnencryptedOutboundRejection lets an older running Xray +// core accept an outbound that the newer embedded validator rejects solely +// because it is unencrypted and targets a public address. Unknown or malformed +// versions preserve the embedded validator's strict behavior. +func shouldSkipLegacyUnencryptedOutboundRejection(coreVersion string, err error) bool { + if err == nil || !strings.Contains(err.Error(), unencryptedOutboundProhibitedError) { + return false + } + comparison, ok := compareXrayCoreVersions(coreVersion, unencryptedOutboundMinimumVersion) + return ok && comparison < 0 +} + +func compareXrayCoreVersions(a, b string) (int, bool) { + aParts, okA := parseXrayCoreVersionParts(a) + bParts, okB := parseXrayCoreVersionParts(b) + if !okA || !okB { + return 0, false + } + for i := range len(aParts) { + if aParts[i] > bParts[i] { + return 1, true + } + if aParts[i] < bParts[i] { + return -1, true + } + } + return 0, true +} + +func parseXrayCoreVersionParts(version string) ([3]int, bool) { + var result [3]int + parts := strings.Split(strings.TrimPrefix(strings.TrimSpace(version), "v"), ".") + if len(parts) != len(result) { + return result, false + } + for i, part := range parts { + n, err := strconv.Atoi(part) + if err != nil { + return result, false + } + result[i] = n + } + return result, true +} + func (s *XraySettingService) UpdateWarpXraySetting(warpData map[string]string, warpConfig map[string]any) error { template, err := s.GetXrayConfigTemplate() if err != nil { diff --git a/internal/web/service/xray_setting_test.go b/internal/web/service/xray_setting_test.go index 5fe54df72..a1270cd1a 100644 --- a/internal/web/service/xray_setting_test.go +++ b/internal/web/service/xray_setting_test.go @@ -2,10 +2,58 @@ package service import ( "encoding/json" + "errors" "strings" "testing" ) +func TestShouldSkipLegacyUnencryptedOutboundRejection(t *testing.T) { + prohibited := errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain") + + tests := []struct { + name string + version string + err error + want bool + }{ + {name: "older core", version: "26.4.25", err: prohibited, want: true}, + {name: "boundary version", version: "26.7.11", err: prohibited, want: false}, + {name: "newer core", version: "26.10.0", err: prohibited, want: false}, + {name: "unknown version", version: "Unknown", err: prohibited, want: false}, + {name: "unparseable version", version: "26.7", err: prohibited, want: false}, + {name: "empty version", version: "", err: prohibited, want: false}, + {name: "unrelated validation error", version: "26.4.25", err: errors.New("invalid outbound"), want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldSkipLegacyUnencryptedOutboundRejection(tt.version, tt.err); got != tt.want { + t.Fatalf("shouldSkipLegacyUnencryptedOutboundRejection(%q, %v) = %v, want %v", tt.version, tt.err, got, tt.want) + } + }) + } +} + +func TestCompareXrayCoreVersions(t *testing.T) { + tests := []struct { + a, b string + want int + ok bool + }{ + {a: "26.7.11", b: "26.7.11", want: 0, ok: true}, + {a: "26.10.0", b: "26.7.11", want: 1, ok: true}, + {a: "v26.4.25", b: "26.7.11", want: -1, ok: true}, + {a: "Unknown", b: "26.7.11", want: 0, ok: false}, + } + + for _, tt := range tests { + got, ok := compareXrayCoreVersions(tt.a, tt.b) + if got != tt.want || ok != tt.ok { + t.Errorf("compareXrayCoreVersions(%q, %q) = (%d, %v), want (%d, %v)", tt.a, tt.b, got, ok, tt.want, tt.ok) + } + } +} + func TestUnwrapXrayTemplateConfig(t *testing.T) { real := `{"log":{},"inbounds":[],"outbounds":[],"routing":{}}`