mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
feat(xray): update xray-core to v26.9.9 and follow the udpHop move
Bump xtls/xray-core to 52a412d9e2f5 (v26.9.9) and the three binary pins in DockerInit.sh and release.yml in lockstep. Upstream moved UDP port hopping out of finalmask.quicParams.udpHop and into a standalone "udphop" UDP mask with a different shape (mode / interval / remotePorts / remoteIPs). The old key is gone from QuicParams, and since the config loader ignores unknown fields it is now silently dropped rather than rejected — port hopping just stops. The panel adapts where that key was live: - Both link importers rebuilt quicParams.udpHop from the standard mport param, so an imported hysteria2 link produced an outbound that no longer hops. They now emit a udphop mask in intervalremote mode, which is what the old key did. The mode is required: UDPHop.Build() rejects an empty or unknown one. - validFinalMaskUDPTypes and UdpMaskTypeSchema learn "udphop", otherwise the Go link generator strips the mask from every link and sub, and Zod strips it on the next form round trip. - mport generation (Go and frontend) reads the mask first and keeps reading the legacy key, so inbounds stored before the upgrade still advertise their range. On an inbound the old key was always inert — only hysteria's dialer consumed it — so nothing regresses server-side and no migration is needed. udphop stays out of the mask dropdown on purpose: it is client-only in core, which refuses to wrap a server socket, and that form is shared with the inbound editor.
This commit is contained in:
@@ -760,21 +760,30 @@ func applyHysteria2Obfs(stream map[string]any, p url.Values) {
|
||||
}
|
||||
|
||||
// applyHysteria2Hop rebuilds the UDP port-hopping range from the standard mport
|
||||
// param, which the generator emits as finalmask.quicParams.udpHop.ports. A range
|
||||
// already supplied via fm= wins; the client-side interval falls back to the same
|
||||
// default the panel writes.
|
||||
// param. xray-core 26.9.9 replaced finalmask.quicParams.udpHop with a "udphop"
|
||||
// UDP mask, whose intervalremote mode is what the old key used to do; a mask
|
||||
// already supplied via fm= wins.
|
||||
func applyHysteria2Hop(stream map[string]any, p url.Values) {
|
||||
ports := firstParam(p, "mport")
|
||||
if ports == "" {
|
||||
return
|
||||
}
|
||||
quicParams := ensureChildMap(ensureChildMap(stream, "finalmask"), "quicParams")
|
||||
if udpHop, ok := quicParams["udpHop"].(map[string]any); ok {
|
||||
if existing, _ := udpHop["ports"].(string); existing != "" {
|
||||
finalmask := ensureChildMap(stream, "finalmask")
|
||||
masks, _ := finalmask["udp"].([]any)
|
||||
for _, rawMask := range masks {
|
||||
mask, _ := rawMask.(map[string]any)
|
||||
if maskType, _ := mask["type"].(string); maskType == "udphop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
quicParams["udpHop"] = map[string]any{"ports": ports, "interval": "5-10"}
|
||||
finalmask["udp"] = append(masks, map[string]any{
|
||||
"type": "udphop",
|
||||
"settings": map[string]any{
|
||||
"mode": "intervalremote",
|
||||
"interval": "5-10",
|
||||
"remotePorts": ports,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func ensureChildMap(parent map[string]any, key string) map[string]any {
|
||||
|
||||
@@ -148,16 +148,25 @@ func finalmaskUDP(t *testing.T, res *ParseResult) []any {
|
||||
return udp
|
||||
}
|
||||
|
||||
func hopMask(t *testing.T, res *ParseResult) (map[string]any, bool) {
|
||||
t.Helper()
|
||||
for _, rawMask := range finalmaskUDP(t, res) {
|
||||
mask, _ := rawMask.(map[string]any)
|
||||
if maskType, _ := mask["type"].(string); maskType == "udphop" {
|
||||
settings, _ := mask["settings"].(map[string]any)
|
||||
return settings, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func hopPorts(t *testing.T, res *ParseResult) (string, bool) {
|
||||
t.Helper()
|
||||
stream, _ := res.Outbound["streamSettings"].(map[string]any)
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
quicParams, _ := finalmask["quicParams"].(map[string]any)
|
||||
udpHop, ok := quicParams["udpHop"].(map[string]any)
|
||||
settings, ok := hopMask(t, res)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
ports, _ := udpHop["ports"].(string)
|
||||
ports, _ := settings["remotePorts"].(string)
|
||||
return ports, true
|
||||
}
|
||||
|
||||
@@ -258,11 +267,17 @@ func TestParseHysteria2_Mport(t *testing.T) {
|
||||
{"standard mport", "mport=20000-50000", "20000-50000", true},
|
||||
{"no mport", "sni=ex.com", "", false},
|
||||
{
|
||||
name: "fm udpHop wins over mport",
|
||||
query: "mport=1-2&fm=" + url.QueryEscape(`{"quicParams":{"udpHop":{"ports":"30000-40000","interval":"7-9"}}}`),
|
||||
name: "fm udphop mask wins over mport",
|
||||
query: "mport=1-2&fm=" + url.QueryEscape(`{"udp":[{"type":"udphop","settings":{"mode":"intervalremote","interval":"7-9","remotePorts":"30000-40000"}}]}`),
|
||||
wantPorts: "30000-40000",
|
||||
wantHop: true,
|
||||
},
|
||||
{
|
||||
name: "legacy fm quicParams.udpHop no longer suppresses mport",
|
||||
query: "mport=1-2&fm=" + url.QueryEscape(`{"quicParams":{"udpHop":{"ports":"30000-40000","interval":"7-9"}}}`),
|
||||
wantPorts: "1-2",
|
||||
wantHop: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@@ -281,6 +296,32 @@ func TestParseHysteria2_Mport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// xray-core 26.9.9 rejects a udphop mask whose mode is empty or unknown, so
|
||||
// the mport importer must emit a mode the core's UDPHop.Build() accepts.
|
||||
func TestParseHysteria2_MportEmitsCoreAcceptedMask(t *testing.T) {
|
||||
res, err := ParseLink("hysteria2://auth@1.2.3.4:443?security=tls&mport=20000-50000#node")
|
||||
if err != nil {
|
||||
t.Fatalf("parse hysteria2: %v", err)
|
||||
}
|
||||
settings, ok := hopMask(t, res)
|
||||
if !ok {
|
||||
t.Fatalf("no udphop mask (stream: %v)", res.Outbound["streamSettings"])
|
||||
}
|
||||
if got, _ := settings["mode"].(string); got != "intervalremote" {
|
||||
t.Errorf("mode = %q, want %q", got, "intervalremote")
|
||||
}
|
||||
if got, _ := settings["interval"].(string); got != "5-10" {
|
||||
t.Errorf("interval = %q, want %q", got, "5-10")
|
||||
}
|
||||
stream, _ := res.Outbound["streamSettings"].(map[string]any)
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
if quicParams, ok := finalmask["quicParams"].(map[string]any); ok {
|
||||
if _, dead := quicParams["udpHop"]; dead {
|
||||
t.Error("importer still writes the quicParams.udpHop key the core ignores")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseShadowsocks(t *testing.T) {
|
||||
modernUser := base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:secretpass"))
|
||||
legacyBody := base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:secretpass@1.2.3.4:8388"))
|
||||
|
||||
Reference in New Issue
Block a user