Files
3x-ui/internal/util/maskcompat/udphop.go
T
MHSanaei 27e45f3039 fix(sub): keep the udphop mask on every JSON-subscription endpoint
UpgradeLegacyUDPHop deleted udpHop from the quicParams map it was handed
in place. The JSON subscription runs it once per endpoint on a finalmask
that mergeFinalMask (host masks) or withLegacyFragmentRanges cloned only
shallowly, so the first endpoint emptied the inbound's shared quicParams
and every later endpoint got {"quicParams":{}} and no udphop mask: a
hysteria inbound with port hopping and two hosts hopped on the first
host's config only.

The upgrade now replaces quicParams and udp on the finalmask it is given
instead of writing into values a merged finalmask shares, as the
frontend's upgradeLegacyUdpHop already does.
2026-10-11 16:35:16 +02:00

62 lines
1.5 KiB
Go

package maskcompat
import (
"maps"
"slices"
"strings"
)
// UpgradeLegacyUDPHop moves the quicParams.udpHop xray-core 26.9.9 ignores into an intervalRemote
// "udphop" mask, rewriting only finalmask's own keys: a merged finalmask shares quicParams and udp.
func UpgradeLegacyUDPHop(finalmask any) bool {
fm, _ := finalmask.(map[string]any)
quicParams, _ := fm["quicParams"].(map[string]any)
legacy, found := quicParams["udpHop"]
if !found {
return false
}
restQuic := maps.Clone(quicParams)
delete(restQuic, "udpHop")
if len(restQuic) == 0 {
delete(fm, "quicParams")
} else {
fm["quicParams"] = restQuic
}
hop, _ := legacy.(map[string]any)
ports := hop["ports"]
if isEmptyHopValue(ports) || hasUDPHopMask(fm) {
return true
}
settings := map[string]any{"mode": "intervalRemote", "remotePorts": ports}
if interval := hop["interval"]; !isEmptyHopValue(interval) {
settings["interval"] = interval
}
masks, _ := fm["udp"].([]any)
fm["udp"] = append(slices.Clip(masks), map[string]any{"type": "udphop", "settings": settings})
return true
}
func hasUDPHopMask(fm map[string]any) bool {
masks, _ := fm["udp"].([]any)
for _, entry := range masks {
mask, _ := entry.(map[string]any)
if maskType, _ := mask["type"].(string); strings.EqualFold(maskType, "udphop") {
return true
}
}
return false
}
func isEmptyHopValue(value any) bool {
switch v := value.(type) {
case nil:
return true
case string:
return strings.TrimSpace(v) == ""
case float64:
return v == 0
}
return false
}