From d43e0c7eaddb913fc2a8084628e3729509192793 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:33:47 +0530 Subject: [PATCH] fix(sub): default VLESS client encryption to none when the inbound has none stored (#6781) VLESS inbounds whose settings carry no "encryption" key (rows saved by panels before the field existed, or inbounds added over the API with only "decryption":"none", as the API docs example does) produced a JSON subscription outbound with "encryption": "". Xray refuses to load that config ("VLESS users: please add/set \"encryption\":\"none\" for every user"), so every VLESS entry of the JSON subscription was unusable. The share link left the parameter out instead. Treat a missing or empty value as "none" for both outputs, matching the frontend inbound schema default and the docs link generator. --- internal/sub/json_service.go | 6 +----- internal/sub/json_service_test.go | 33 +++++++++++++++++++++++++++++++ internal/sub/service.go | 14 +++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 0b8f6cd50..e6cd07e89 100644 --- a/internal/sub/json_service.go +++ b/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 { diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index fa81dd4d5..6b8f88b7f 100644 --- a/internal/sub/json_service_test.go +++ b/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"} diff --git a/internal/sub/service.go b/internal/sub/service.go index fb46921df..48fbacc1e 100644 --- a/internal/sub/service.go +++ b/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 {