mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-31 15:37:14 +00:00
fix(sub): bake Host VLESS Route into subscription UUIDs
The Host VLESS Route field was stored and shown in the panel but never applied to any generated subscription (raw, JSON, Clash), so the UUID was emitted unmodified (#5655). Xray reads the route from the UUID's 3rd group (bytes 6-7, net.PortFromBytes) and masks those bytes to zero before authenticating, so a value can be baked into the share/JSON/Clash UUIDs without breaking the user match. A shared applyVlessRoute helper encodes a single 0-65535 value as the 3rd group; empty/invalid/non-UUID input is left unchanged, so legacy data never yields a broken link and no DB migration is needed. The field was wrongly validated as a multi-segment port spec (that form belongs to the separate server-side routing rule). It is now a single value 0-65535, with frontend validation, link-preview parity (genVlessLink/hostToExternalProxyEntry), hint + error translations across all 13 locales, and tests on every path. Closes #5655
This commit is contained in:
@@ -239,7 +239,7 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound,
|
||||
proxy["cipher"] = cipher
|
||||
case model.VLESS:
|
||||
proxy["type"] = "vless"
|
||||
proxy["uuid"] = client.ID
|
||||
proxy["uuid"] = applyVlessRoute(client.ID, hostVlessRoute(ep))
|
||||
var inboundSettings map[string]any
|
||||
_ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
streamSecurity, _ := stream["security"].(string)
|
||||
|
||||
@@ -77,7 +77,7 @@ func (s *SubService) buildEndpointLinks(
|
||||
eps []ShareEndpoint,
|
||||
params map[string]string,
|
||||
baseSecurity string,
|
||||
makeLink func(dest string, port int) string,
|
||||
makeLink func(e ShareEndpoint) string,
|
||||
makeRemark func(e ShareEndpoint) string,
|
||||
) string {
|
||||
links := make([]string, 0, len(eps))
|
||||
@@ -92,7 +92,7 @@ func (s *SubService) buildEndpointLinks(
|
||||
applyEndpointHostPath(e, nextParams)
|
||||
applyEndpointAllowInsecure(e, nextParams, securityToApply)
|
||||
links = append(links, buildLinkWithParamsAndSecurity(
|
||||
makeLink(e.Address, e.Port),
|
||||
makeLink(e),
|
||||
nextParams,
|
||||
makeRemark(e),
|
||||
securityToApply,
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestBuildEndpointLinks_ParamForm(t *testing.T) {
|
||||
externalProxyToEndpoint(map[string]any{"forceTls": "none", "dest": "b.example.com", "port": float64(80), "remark": "B"}),
|
||||
}
|
||||
got := s.buildEndpointLinks(eps, params, "tls",
|
||||
func(dest string, port int) string { return fmt.Sprintf("vless://uid@%s", joinHostPort(dest, port)) },
|
||||
func(e ShareEndpoint) string { return fmt.Sprintf("vless://uid@%s", joinHostPort(e.Address, e.Port)) },
|
||||
func(e ShareEndpoint) string { return s.genRemark(in, "user", e.Remark, "") },
|
||||
)
|
||||
want := "vless://uid@a.example.com:8443?fp=chrome&security=tls&sni=a.sni&type=tcp#ib-A-user\n" +
|
||||
|
||||
@@ -104,6 +104,9 @@ func hostToExternalProxyMap(h *model.Host, defaultDest string, defaultPort int)
|
||||
if h.FinalMask != "" {
|
||||
ep["finalMask"] = h.FinalMask
|
||||
}
|
||||
if h.VlessRoute != "" {
|
||||
ep["vlessRoute"] = h.VlessRoute
|
||||
}
|
||||
return ep
|
||||
}
|
||||
|
||||
|
||||
@@ -210,7 +210,9 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
|
||||
case "vmess":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, jsonMux(mux, hostMux)))
|
||||
case "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client, jsonMux(mux, hostMux)))
|
||||
vc := client
|
||||
vc.ID = applyVlessRoute(client.ID, hostVlessRoute(extPrxy))
|
||||
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, vc, jsonMux(mux, hostMux)))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client, jsonMux(mux, hostMux)))
|
||||
case "hysteria":
|
||||
|
||||
@@ -698,8 +698,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
externalProxies,
|
||||
params,
|
||||
security,
|
||||
func(dest string, port int) string {
|
||||
return fmt.Sprintf("vless://%s@%s", uuid, joinHostPort(dest, port))
|
||||
func(ep map[string]any, dest string, port int) string {
|
||||
return fmt.Sprintf("vless://%s@%s", applyVlessRoute(uuid, hostVlessRoute(ep)), joinHostPort(dest, port))
|
||||
},
|
||||
func(ep map[string]any) string {
|
||||
return s.endpointRemark(inbound, email, ep, streamNetwork)
|
||||
@@ -749,7 +749,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
externalProxies,
|
||||
params,
|
||||
security,
|
||||
func(dest string, port int) string {
|
||||
func(_ map[string]any, dest string, port int) string {
|
||||
return fmt.Sprintf("trojan://%s@%s", password, joinHostPort(dest, port))
|
||||
},
|
||||
func(ep map[string]any) string {
|
||||
@@ -842,7 +842,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
externalProxies,
|
||||
proxyParams,
|
||||
security,
|
||||
func(dest string, port int) string {
|
||||
func(_ map[string]any, dest string, port int) string {
|
||||
return fmt.Sprintf("ss://%s@%s", userInfo, joinHostPort(dest, port))
|
||||
},
|
||||
func(ep map[string]any) string {
|
||||
@@ -1697,7 +1697,7 @@ func (s *SubService) buildExternalProxyURLLinks(
|
||||
externalProxies []any,
|
||||
params map[string]string,
|
||||
baseSecurity string,
|
||||
makeLink func(dest string, port int) string,
|
||||
makeLink func(ep map[string]any, dest string, port int) string,
|
||||
makeRemark func(ep map[string]any) string,
|
||||
) string {
|
||||
eps := make([]ShareEndpoint, 0, len(externalProxies))
|
||||
@@ -1705,7 +1705,9 @@ func (s *SubService) buildExternalProxyURLLinks(
|
||||
ep, _ := externalProxy.(map[string]any)
|
||||
eps = append(eps, externalProxyToEndpoint(ep))
|
||||
}
|
||||
return s.buildEndpointLinks(eps, params, baseSecurity, makeLink, func(e ShareEndpoint) string {
|
||||
return s.buildEndpointLinks(eps, params, baseSecurity, func(e ShareEndpoint) string {
|
||||
return makeLink(e.ep, e.Address, e.Port)
|
||||
}, func(e ShareEndpoint) string {
|
||||
return makeRemark(e.ep)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// xray reads the route from UUID bytes 6-7 (net.PortFromBytes) and masks them to
|
||||
// zero before auth, so baking a 0-65535 value into the 3rd group routes without
|
||||
// breaking the user match. Empty/invalid/non-UUID input is returned unchanged.
|
||||
func applyVlessRoute(id, route string) string {
|
||||
route = strings.TrimSpace(route)
|
||||
if route == "" {
|
||||
return id
|
||||
}
|
||||
n, err := strconv.Atoi(route)
|
||||
if err != nil || n < 0 || n > 65535 {
|
||||
return id
|
||||
}
|
||||
u, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return id
|
||||
}
|
||||
u[6] = byte(n >> 8)
|
||||
u[7] = byte(n)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func hostVlessRoute(ep map[string]any) string {
|
||||
v, _ := ep["vlessRoute"].(string)
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func TestHostToExternalProxyMap_VlessRoute(t *testing.T) {
|
||||
with := hostToExternalProxyMap(&model.Host{VlessRoute: "443"}, "d.example.com", 443)
|
||||
if with["vlessRoute"] != "443" {
|
||||
t.Fatalf(`ep["vlessRoute"] = %v, want "443"`, with["vlessRoute"])
|
||||
}
|
||||
without := hostToExternalProxyMap(&model.Host{}, "d.example.com", 443)
|
||||
if _, ok := without["vlessRoute"]; ok {
|
||||
t.Fatalf("empty VlessRoute must not add the key: %v", without["vlessRoute"])
|
||||
}
|
||||
}
|
||||
|
||||
// seedSubInbound's client UUID is 11111111-2222-4333-8444-<port>, so route 443
|
||||
// -> 01bb, 53 -> 0035, and a route-less host keeps 4333.
|
||||
func TestSub_HostVlessRoute_RawMultiHost(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
ib := seedSubInbound(t, "s1", "vr", 4500, 1, wsTLSStream)
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "A", Address: "a.cdn.com", Port: 8443, Security: "tls", VlessRoute: "443"})
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 2, Remark: "B", Address: "b.cdn.com", Port: 8443, Security: "tls", VlessRoute: "53"})
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 3, Remark: "C", Address: "c.cdn.com", Port: 8443, Security: "tls"})
|
||||
|
||||
links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSubs: %v", err)
|
||||
}
|
||||
parts := strings.Split(strings.Join(links, "\n"), "\n")
|
||||
if len(parts) != 3 {
|
||||
t.Fatalf("want 3 host links, got %d: %v", len(parts), parts)
|
||||
}
|
||||
if !strings.Contains(parts[0], "vless://11111111-2222-01bb-8444-") {
|
||||
t.Fatalf("host A (route 443) must encode 01bb: %s", parts[0])
|
||||
}
|
||||
if !strings.Contains(parts[1], "vless://11111111-2222-0035-8444-") {
|
||||
t.Fatalf("host B (route 53) must encode 0035: %s", parts[1])
|
||||
}
|
||||
if !strings.Contains(parts[2], "vless://11111111-2222-4333-8444-") {
|
||||
t.Fatalf("host C (no route) must keep the original 3rd group: %s", parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub_HostVlessRoute_JSON(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
ib := seedSubInbound(t, "s1", "vrj", 4501, 1, wsTLSStream)
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "J", Address: "j.cdn.com", Port: 8443, Security: "tls", VlessRoute: "443"})
|
||||
|
||||
js := NewSubJsonService("", "", "", NewSubService(""))
|
||||
out, _, err := js.GetJson("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetJson: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "11111111-2222-01bb-8444-") {
|
||||
t.Fatalf("json outbound id should encode route 443 (01bb):\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "11111111-2222-4333-8444-") {
|
||||
t.Fatalf("original id 3rd group must be replaced in json:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub_HostVlessRoute_Clash(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
ib := seedSubInbound(t, "s1", "vrc", 4502, 1, wsTLSStream)
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "C", Address: "c.cdn.com", Port: 8443, Security: "tls", VlessRoute: "443"})
|
||||
|
||||
clash := NewSubClashService(false, "", NewSubService(""))
|
||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
}
|
||||
if !strings.Contains(yaml, "11111111-2222-01bb-8444-") {
|
||||
t.Fatalf("clash proxy uuid should encode route 443 (01bb):\n%s", yaml)
|
||||
}
|
||||
if strings.Contains(yaml, "11111111-2222-4333-8444-") {
|
||||
t.Fatalf("original uuid 3rd group must be replaced in clash:\n%s", yaml)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestApplyVlessRoute(t *testing.T) {
|
||||
const id = "11111111-2222-4333-8444-555555555555"
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
route string
|
||||
want string
|
||||
}{
|
||||
{"empty route unchanged", id, "", id},
|
||||
{"whitespace route unchanged", id, " ", id},
|
||||
{"443 -> 01bb", id, "443", "11111111-2222-01bb-8444-555555555555"},
|
||||
{"53 -> 0035", id, "53", "11111111-2222-0035-8444-555555555555"},
|
||||
{"0 -> 0000", id, "0", "11111111-2222-0000-8444-555555555555"},
|
||||
{"65535 -> ffff", id, "65535", "11111111-2222-ffff-8444-555555555555"},
|
||||
{"trimmed value", id, " 443 ", "11111111-2222-01bb-8444-555555555555"},
|
||||
{"out of range high unchanged", id, "65536", id},
|
||||
{"negative unchanged", id, "-1", id},
|
||||
{"non-numeric unchanged", id, "abc", id},
|
||||
{"legacy multi-segment unchanged", id, "53,443", id},
|
||||
{"non-uuid id unchanged", "short", "443", "short"},
|
||||
{"empty id unchanged", "", "443", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := applyVlessRoute(tt.id, tt.route); got != tt.want {
|
||||
t.Fatalf("applyVlessRoute(%q, %q) = %q, want %q", tt.id, tt.route, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostVlessRoute(t *testing.T) {
|
||||
if got := hostVlessRoute(map[string]any{"vlessRoute": "443"}); got != "443" {
|
||||
t.Fatalf(`hostVlessRoute = %q, want "443"`, got)
|
||||
}
|
||||
if got := hostVlessRoute(map[string]any{}); got != "" {
|
||||
t.Fatalf(`hostVlessRoute(missing) = %q, want ""`, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user