feat(hysteria2): emit UDP port hopping in subscriptions and share links

UDP Hop (finalmask.quicParams.udpHop.ports) was configurable but never surfaced in generated configs, so clients kept using the single listening port (#4789).

Share links (frontend genHysteriaLink + sub genHysteriaLink) now keep a numeric port in the authority and carry the hop range as the v2rayN-compatible mport query param, so v2rayN and other System.Uri-based importers can parse the link. Clash output sets mihomos native ports field.

Closes #4789
This commit is contained in:
MHSanaei
2026-06-02 15:01:18 +02:00
parent 2f12b34635
commit 13d02f01fc
5 changed files with 87 additions and 0 deletions

View File

@@ -779,3 +779,37 @@ func TestHasFinalMaskContent(t *testing.T) {
t.Fatal("non-empty map should count as content")
}
}
func TestHysteriaHopPorts(t *testing.T) {
withHop := func(ports any) map[string]any {
return map[string]any{
"finalmask": map[string]any{
"quicParams": map[string]any{
"udpHop": map[string]any{"ports": ports, "interval": "5-10"},
},
},
}
}
cases := []struct {
name string
stream map[string]any
want string
}{
{"range", withHop("20000-50000"), "20000-50000"},
{"trimmed", withHop(" 443,20000-50000 "), "443,20000-50000"},
{"empty string", withHop(""), ""},
{"non-string", withHop(float64(443)), ""},
{"no udpHop", map[string]any{"finalmask": map[string]any{"quicParams": map[string]any{}}}, ""},
{"no finalmask", map[string]any{}, ""},
{"nil stream", nil, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := hysteriaHopPorts(tc.stream); got != tc.want {
t.Fatalf("hysteriaHopPorts() = %q, want %q", got, tc.want)
}
})
}
}