2 Commits f55938ba0a ... c321933a54

Author SHA1 Message Date
  Javier Guerrero c321933a54 perf: reduce traffic formatter allocations (#6790) 6 hours ago
  Jay Hemnani d43e0c7ead fix(sub): default VLESS client encryption to none when the inbound has none stored (#6781) 6 hours ago

+ 1 - 5
internal/sub/json_service.go

@@ -885,15 +885,11 @@ func (s *SubJsonService) genVless(subReq *SubService, inbound *model.Inbound, st
 	}
 	outbound.StreamSettings = streamSettings
 
-	// Add encryption for VLESS outbound from inbound settings
-	inboundSettings := subReq.linkSettings(inbound)
-	encryption, _ := inboundSettings["encryption"].(string)
-
 	settings := map[string]any{
 		"address":    inbound.Listen,
 		"port":       inbound.Port,
 		"id":         client.ID,
-		"encryption": encryption,
+		"encryption": vlessClientEncryption(subReq.linkSettings(inbound)),
 		"level":      8,
 	}
 	if client.Flow != "" && !inbound.DisableFlow {

+ 33 - 0
internal/sub/json_service_test.go

@@ -2,6 +2,7 @@ package sub
 
 import (
 	"encoding/json"
+	"net/url"
 	"reflect"
 	"testing"
 
@@ -231,6 +232,38 @@ func TestSubJsonServiceVlessFlattened(t *testing.T) {
 	}
 }
 
+// VLESS inbounds stored without settings.encryption (pre-2.6.8 rows, API adds)
+// must still give clients "none": xray refuses a VLESS outbound with "".
+func TestSubVlessEncryptionDefaultsToNone(t *testing.T) {
+	cases := []struct {
+		name     string
+		settings string
+		want     string
+	}{
+		{"missing", `{"clients":[{"id":"uuid-1","email":"user"}],"decryption":"none"}`, "none"},
+		{"empty", `{"clients":[{"id":"uuid-1","email":"user"}],"decryption":"none","encryption":""}`, "none"},
+		{"vlessenc kept", `{"clients":[{"id":"uuid-1","email":"user"}],"decryption":"mlkem768x25519plus.native.600s.d","encryption":"mlkem768x25519plus.native.0rtt.e"}`, "mlkem768x25519plus.native.0rtt.e"},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: tc.settings, StreamSettings: `{"network":"tcp","security":"none"}`}
+
+			settings := outboundSettings(t, NewSubJsonService("", "", "", "", nil).genVless(&SubService{}, inbound, nil, model.Client{ID: "uuid-1"}, ""))
+			if settings["encryption"] != tc.want {
+				t.Fatalf("json outbound encryption = %#v, want %q", settings["encryption"], tc.want)
+			}
+
+			link, err := url.Parse((&SubService{}).genVlessLink(inbound, "user"))
+			if err != nil {
+				t.Fatalf("parse link: %v", err)
+			}
+			if got := link.Query().Get("encryption"); got != tc.want {
+				t.Fatalf("share link encryption = %q, want %q (%s)", got, tc.want, link)
+			}
+		})
+	}
+}
+
 func TestSubJsonServiceVlessFlowSuppressedByDisableFlow(t *testing.T) {
 	inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`, DisableFlow: true}
 	client := model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}

+ 10 - 4
internal/sub/service.go

@@ -1193,6 +1193,15 @@ func vlessEncryptionEnabled(settings map[string]any) bool {
 	return false
 }
 
+// vlessClientEncryption reads the client-side VLESS encryption from inbound settings. Inbounds
+// stored without the field (older panels, API adds) mean "none", as the frontend schema does.
+func vlessClientEncryption(settings map[string]any) string {
+	if encryption, _ := settings["encryption"].(string); encryption != "" {
+		return encryption
+	}
+	return "none"
+}
+
 // vlessFlowAllowed mirrors inboundCanEnableTlsFlow in internal/web/service: Vision runs over
 // TLS/REALITY on raw TCP, or inside VLESS encryption on any transport.
 func vlessFlowAllowed(network, security string, settings map[string]any) bool {
@@ -1218,11 +1227,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	params := make(map[string]string)
 	params["type"] = streamNetwork
 
-	// Add encryption parameter for VLESS from inbound settings
 	settings := s.linkSettings(inbound)
-	if encryption, ok := settings["encryption"].(string); ok {
-		params["encryption"] = encryption
-	}
+	params["encryption"] = vlessClientEncryption(settings)
 
 	applyShareNetworkParams(stream, streamNetwork, params)
 	if finalmask, ok := stream["finalmask"].(map[string]any); ok {

+ 2 - 4
internal/util/common/format.go

@@ -1,8 +1,6 @@
 package common
 
-import (
-	"fmt"
-)
+import "strconv"
 
 // FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
 func FormatTraffic(trafficBytes int64) string {
@@ -14,5 +12,5 @@ func FormatTraffic(trafficBytes int64) string {
 		size /= 1024
 		unitIndex++
 	}
-	return fmt.Sprintf("%.2f%s", size, units[unitIndex])
+	return strconv.FormatFloat(size, 'f', 2, 64) + units[unitIndex]
 }

+ 6 - 0
internal/util/common/format_test.go

@@ -26,3 +26,9 @@ func TestFormatTraffic(t *testing.T) {
 		})
 	}
 }
+
+func BenchmarkFormatTraffic(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		_ = FormatTraffic(1536 + int64(i%64)*1024)
+	}
+}