mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-15 10:25:00 +03:00
fix(sub): export Hysteria2 Gecko obfs in Clash/Mihomo subscriptions
A salamander finalmask carrying a packetSize range is Gecko obfs. The hysteria2:// link generators already export it, but buildHysteriaProxy copied only the password, so Clash/Mihomo received plain salamander and could never complete the handshake against a Gecko server. Emit mihomo's gecko form instead: obfs: gecko plus integer obfs-min-packet-size / obfs-max-packet-size, validated by the same parseHysteriaPacketSize bounds the URI export uses. A password-only mask keeps the plain salamander output. Closes #6822
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -540,8 +541,8 @@ func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.
|
||||
proxy["fingerprint"] = certFingerprint
|
||||
}
|
||||
|
||||
// Salamander obfs (Hysteria2). Read the same finalmask.udp[salamander]
|
||||
// block the subscription link generator uses.
|
||||
// Salamander obfs (Hysteria2), read from the same finalmask.udp[salamander]
|
||||
// block as the link generator; a packetSize range is mihomo's gecko obfs.
|
||||
if finalmask, ok := rawStream["finalmask"].(map[string]any); ok {
|
||||
if udpMasks, ok := finalmask["udp"].([]any); ok {
|
||||
for _, m := range udpMasks {
|
||||
@@ -553,6 +554,13 @@ func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.
|
||||
if pw, ok := settings["password"].(string); ok && pw != "" {
|
||||
proxy["obfs"] = "salamander"
|
||||
proxy["obfs-password"] = pw
|
||||
packetSize, _ := settings["packetSize"].(string)
|
||||
if gecko := parseHysteriaPacketSize(packetSize); gecko != "" {
|
||||
minStr, maxStr := splitHysteriaPacketSize(gecko)
|
||||
proxy["obfs"] = "gecko"
|
||||
proxy["obfs-min-packet-size"], _ = strconv.Atoi(minStr)
|
||||
proxy["obfs-max-packet-size"], _ = strconv.Atoi(maxStr)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,3 +150,43 @@ func TestParseHysteriaPacketSizeBounds(t *testing.T) {
|
||||
t.Fatalf("valid range = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Mihomo reads Gecko only from obfs: gecko plus integer packet-size keys; plain
|
||||
// salamander on a Gecko server never completes the handshake.
|
||||
func TestBuildHysteriaProxyExportsGeckoObfs(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
want map[string]any
|
||||
}{
|
||||
{
|
||||
name: "packetSize exports gecko",
|
||||
settings: `{"password":"pw","packetSize":"512-1200"}`,
|
||||
want: map[string]any{
|
||||
"obfs": "gecko", "obfs-password": "pw",
|
||||
"obfs-min-packet-size": 512, "obfs-max-packet-size": 1200,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "password only stays salamander",
|
||||
settings: `{"password":"pw"}`,
|
||||
want: map[string]any{"obfs": "salamander", "obfs-password": "pw"},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
in := &model.Inbound{
|
||||
Listen: "203.0.113.1", Port: 443, Protocol: model.Hysteria,
|
||||
Settings: `{"version":2}`,
|
||||
StreamSettings: `{"security":"tls","finalmask":{"udp":[{"type":"salamander","settings":` +
|
||||
tc.settings + `}]}}`,
|
||||
}
|
||||
proxy := (&SubClashService{}).buildHysteriaProxy(&SubService{}, in, model.Client{Email: "user", Auth: "secret"}, nil)
|
||||
for _, key := range []string{"obfs", "obfs-password", "obfs-min-packet-size", "obfs-max-packet-size"} {
|
||||
if got, want := proxy[key], tc.want[key]; got != want {
|
||||
t.Errorf("%s = %#v, want %#v", key, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user