mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +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:
@@ -419,24 +419,36 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
|
||||
}
|
||||
}
|
||||
|
||||
// Drop core-rejected links before tagging: prevTagByIndex indexes the persisted
|
||||
// (filtered) list, so positions must be counted in that same list.
|
||||
var droppedByCore []string
|
||||
keptLinks, keptIdentities := parsed[:0], identities[:0]
|
||||
for i, ob := range parsed {
|
||||
if _, dropped := filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), []any{map[string]any(ob)}); len(dropped) > 0 {
|
||||
droppedByCore = append(droppedByCore, dropped...)
|
||||
continue
|
||||
}
|
||||
keptLinks = append(keptLinks, ob)
|
||||
keptIdentities = append(keptIdentities, identities[i])
|
||||
}
|
||||
|
||||
// Assign tags with stability (identity reuse, positional fallback, then a
|
||||
// fresh allocation), keeping tags unique within this batch. Extracted into a
|
||||
// pure function so it can be unit-tested without network/DB. Tags are written
|
||||
// back into the parsed outbounds in place.
|
||||
assigned := assignStableTags(parsed, identities, prev, prevTagByIndex, sub.Id, sub.TagPrefix)
|
||||
assigned := assignStableTags(keptLinks, keptIdentities, prev, prevTagByIndex, sub.Id, sub.TagPrefix)
|
||||
|
||||
// Persist identities for next time
|
||||
newIdent := map[string]string{}
|
||||
for i, id := range identities {
|
||||
for i, id := range keptIdentities {
|
||||
newIdent[id] = assigned[i]
|
||||
}
|
||||
identJSON, _ := json.Marshal(newIdent)
|
||||
|
||||
asAny := make([]any, len(parsed))
|
||||
for i := range parsed {
|
||||
asAny[i] = map[string]any(parsed[i])
|
||||
kept := make([]any, len(keptLinks))
|
||||
for i := range keptLinks {
|
||||
kept[i] = map[string]any(keptLinks[i])
|
||||
}
|
||||
kept, droppedByCore := filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), asAny)
|
||||
|
||||
// Persist the outbounds (as compact JSON array)
|
||||
obsJSON, _ := json.Marshal(kept)
|
||||
|
||||
Reference in New Issue
Block a user