mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 07:37:15 +00:00
fix(outbounds): keep subscription tags on their server when reality params rotate
A subscription outbound's tag must stay bound to the upstream server it was assigned to for as long as that server stays in the subscription; balancers and routing rules select by that tag. The identity used to recognise a server across refreshes included every query parameter. A 3x-ui upstream picks a random shortId and SNI of a reality inbound on every request (older releases a random spiderX too), so no reality link was ever recognised, the stable-tag reservation never engaged, and every tag was handed out by list position. Removing or inserting a server then re-pointed existing tags at other servers: sub-germany carried France, sub-sweden Germany, and Sweden became sub-sweden-1. The identity now ignores sid, sni and spx when security=reality, since none of them selects the server. TLS sni still counts: it can pick the backend behind a shared front. Two more paths broke the same rule: - A link repeated in one body (same identity, different remark) shared a single link_identities key, so both tags gained a -N suffix on every refresh. Repeats are now numbered. - Links the core rejects were dropped after tagging, so the stored list that drives positional reuse was shorter than the parsed one and a rotated server behind a dropped link took its neighbour's tag. The filter now runs first; a dropped link's warning names its remark instead of a tag it never used. A mapping an older build already swapped stays swapped: its stored identities no longer match, so positional reuse reproduces it. Deleting and re-adding the subscription reallocates the tags from the remarks. Closes #6556
This commit is contained in:
@@ -48,6 +48,7 @@ func ParseSubscriptionBody(body []byte) ([]Outbound, []string, error) {
|
||||
lines := splitLines(text)
|
||||
var outbounds []Outbound
|
||||
var identities []string
|
||||
seen := map[string]int{}
|
||||
|
||||
for _, ln := range lines {
|
||||
ln = strings.TrimSpace(ln)
|
||||
@@ -59,8 +60,14 @@ func ParseSubscriptionBody(body []byte) ([]Outbound, []string, error) {
|
||||
// Ignore unparseable lines (comments, unsupported protocols, etc.)
|
||||
continue
|
||||
}
|
||||
identity := res.Identity
|
||||
// A repeated identity would share one stored tag, shifting both tags on every refresh.
|
||||
if n := seen[res.Identity]; n > 0 {
|
||||
identity = fmt.Sprintf("%s#%d", res.Identity, n)
|
||||
}
|
||||
seen[res.Identity]++
|
||||
outbounds = append(outbounds, res.Outbound)
|
||||
identities = append(identities, res.Identity)
|
||||
identities = append(identities, identity)
|
||||
}
|
||||
return outbounds, identities, nil
|
||||
}
|
||||
@@ -1047,10 +1054,18 @@ func firstParam(p url.Values, keys ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// realityPerRequestParams are picked per request by subscription servers (3x-ui randomizes
|
||||
// sid/sni, older releases spx too), so they must not split one server into new identities.
|
||||
var realityPerRequestParams = map[string]bool{"sid": true, "sni": true, "spx": true}
|
||||
|
||||
func canonicalQuery(p url.Values) string {
|
||||
// Sort keys for stable identity
|
||||
reality := p.Get("security") == "reality"
|
||||
keys := make([]string, 0, len(p))
|
||||
for k := range p {
|
||||
if reality && realityPerRequestParams[k] {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
// simple sort
|
||||
|
||||
@@ -24,6 +24,17 @@ func TestParseVmessLink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkIdentityKeepsTLSServerName(t *testing.T) {
|
||||
a, errA := ParseLink("vless://uuid@1.2.3.4:443?type=ws&security=tls&sni=a.example.com#node")
|
||||
b, errB := ParseLink("vless://uuid@1.2.3.4:443?type=ws&security=tls&sni=b.example.com#node")
|
||||
if errA != nil || errB != nil {
|
||||
t.Fatalf("parse vless: %v, %v", errA, errB)
|
||||
}
|
||||
if a.Identity == b.Identity {
|
||||
t.Fatalf("TLS links for different SNIs share identity %q", a.Identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVlessLink(t *testing.T) {
|
||||
link := "vless://uuid@1.2.3.4:443?type=ws&security=tls&path=/&host=ex.com#node1"
|
||||
res, err := ParseLink(link)
|
||||
|
||||
Reference in New Issue
Block a user