mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-21 13: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"
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
@@ -540,8 +541,8 @@ func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.
|
|||||||
proxy["fingerprint"] = certFingerprint
|
proxy["fingerprint"] = certFingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
// Salamander obfs (Hysteria2). Read the same finalmask.udp[salamander]
|
// Salamander obfs (Hysteria2), read from the same finalmask.udp[salamander]
|
||||||
// block the subscription link generator uses.
|
// block as the link generator; a packetSize range is mihomo's gecko obfs.
|
||||||
if finalmask, ok := rawStream["finalmask"].(map[string]any); ok {
|
if finalmask, ok := rawStream["finalmask"].(map[string]any); ok {
|
||||||
if udpMasks, ok := finalmask["udp"].([]any); ok {
|
if udpMasks, ok := finalmask["udp"].([]any); ok {
|
||||||
for _, m := range udpMasks {
|
for _, m := range udpMasks {
|
||||||
@@ -553,6 +554,13 @@ func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.
|
|||||||
if pw, ok := settings["password"].(string); ok && pw != "" {
|
if pw, ok := settings["password"].(string); ok && pw != "" {
|
||||||
proxy["obfs"] = "salamander"
|
proxy["obfs"] = "salamander"
|
||||||
proxy["obfs-password"] = pw
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,3 +150,43 @@ func TestParseHysteriaPacketSizeBounds(t *testing.T) {
|
|||||||
t.Fatalf("valid range = %q", got)
|
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