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:
Sanaei
2026-09-15 22:39:59 +02:00
parent 5008906c4c
commit c9e62451e6
4 changed files with 158 additions and 7 deletions
+11
View File
@@ -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)