Files
3x-ui/internal/util/maskcompat/udphop.go
T
MHSanaei ec0a57fc4f fix(finalmask): hop client UDP through the udphop mask
xray-core 26.9.9 dropped finalmask.quicParams.udpHop for the client-only
"udphop" UDP mask {mode, interval, remoteIPs, remotePorts}. The panel kept
writing the old key, so hysteria/xhttp outbounds and JSON-subscription
clients silently stopped hopping ports.

An inbound keeps quicParams.udpHop: it is panel metadata advertised as the
link's mport, and a udphop mask on a listener fails with "udphop: client
only". The conversion (maskcompat.UpgradeLegacyUDPHop, intervalRemote mode)
runs only where a client config is produced:

- UDPHopClientMaskFix seeder over hosts, sub-JSON finalmask, cached
  outbound-subscription outbounds and template outbounds (never inbounds);
- GetXrayConfig heals template outbounds at build time;
- the JSON subscription upgrades each generated client stream;
- the Go and frontend link importers upgrade a legacy fm= param.

The finalmask editor gains a side prop: the client side lifts the dead key
on mount and offers a udphop mask editor (also for xhttp's HTTP/3 dial);
the inbound side keeps the UDP Hop switch, labelled as advertised.
2026-10-11 11:20:34 +02:00

55 lines
1.4 KiB
Go

package maskcompat
import "strings"
// UpgradeLegacyUDPHop moves the quicParams.udpHop range xray-core 26.9.9 stopped reading
// into the client-only "udphop" UDP mask, whose intervalRemote mode is what the key did.
func UpgradeLegacyUDPHop(finalmask any) bool {
fm, _ := finalmask.(map[string]any)
quicParams, _ := fm["quicParams"].(map[string]any)
legacy, found := quicParams["udpHop"]
if !found {
return false
}
delete(quicParams, "udpHop")
if len(quicParams) == 0 {
delete(fm, "quicParams")
}
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(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
}