Browse Source

fix: gate embedded unencrypted-outbound rejection on running Xray core version (#6028)

Co-authored-by: Matt Van Horn <[email protected]>
Matt Van Horn 14 hours ago
parent
commit
d38c912dc1
2 changed files with 107 additions and 0 deletions
  1. 59 0
      internal/web/service/xray_setting.go
  2. 48 0
      internal/web/service/xray_setting_test.go

+ 59 - 0
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 {

+ 48 - 0
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":{}}`