From f30a64f93f3b1af149c1a26e0533fb208b19cf96 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 25 May 2026 15:35:22 +0800 Subject: [PATCH] feat: support hysteria2 gecko obfs --- adapter/outbound/hysteria2.go | 51 ++++++++++++++++++------------ docs/config.yaml | 8 +++-- go.mod | 2 +- go.sum | 4 +-- listener/config/hysteria2.go | 2 ++ listener/inbound/hysteria2.go | 4 +++ listener/inbound/hysteria2_test.go | 15 +++++++++ listener/sing_hysteria2/server.go | 9 ++++++ 8 files changed, 70 insertions(+), 25 deletions(-) diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index f4387d7d..350e50ea 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -38,26 +38,28 @@ type Hysteria2 struct { type Hysteria2Option struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - Ports string `proxy:"ports,omitempty"` - HopInterval string `proxy:"hop-interval,omitempty"` - Up string `proxy:"up,omitempty"` - Down string `proxy:"down,omitempty"` - Password string `proxy:"password,omitempty"` - Obfs string `proxy:"obfs,omitempty"` - ObfsPassword string `proxy:"obfs-password,omitempty"` - SNI string `proxy:"sni,omitempty"` - ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - Certificate string `proxy:"certificate,omitempty"` - PrivateKey string `proxy:"private-key,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - CWND int `proxy:"cwnd,omitempty"` - BBRProfile string `proxy:"bbr-profile,omitempty"` - UdpMTU int `proxy:"udp-mtu,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port,omitempty"` + Ports string `proxy:"ports,omitempty"` + HopInterval string `proxy:"hop-interval,omitempty"` + Up string `proxy:"up,omitempty"` + Down string `proxy:"down,omitempty"` + Password string `proxy:"password,omitempty"` + Obfs string `proxy:"obfs,omitempty"` + ObfsPassword string `proxy:"obfs-password,omitempty"` + ObfsMinPacketSize int `proxy:"obfs-min-packet-size,omitempty"` + ObfsMaxPacketSize int `proxy:"obfs-max-packet-size,omitempty"` + SNI string `proxy:"sni,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` + ALPN []string `proxy:"alpn,omitempty"` + CWND int `proxy:"cwnd,omitempty"` + BBRProfile string `proxy:"bbr-profile,omitempty"` + UdpMTU int `proxy:"udp-mtu,omitempty"` RealmOpts Hysteria2RealmOption `proxy:"realm-opts,omitempty"` @@ -139,6 +141,8 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { outbound.dialer = option.NewDialer(outbound.DialOptions()) var salamanderPassword string + var geckoPassword string + var geckoMinPacketSize, geckoMaxPacketSize int if len(option.Obfs) > 0 { if option.ObfsPassword == "" { return nil, errors.New("missing obfs password") @@ -146,6 +150,10 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { switch option.Obfs { case hysteria2.ObfsTypeSalamander: salamanderPassword = option.ObfsPassword + case hysteria2.ObfsTypeGecko: + geckoPassword = option.ObfsPassword + geckoMinPacketSize = option.ObfsMinPacketSize + geckoMaxPacketSize = option.ObfsMaxPacketSize default: return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs) } @@ -199,6 +207,9 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { SendBPS: utils.StringToBps(option.Up), ReceiveBPS: utils.StringToBps(option.Down), SalamanderPassword: salamanderPassword, + GeckoPassword: geckoPassword, + GeckoMinPacketSize: geckoMinPacketSize, + GeckoMaxPacketSize: geckoMaxPacketSize, Password: option.Password, TLSConfig: tlsClientConfig, QUICConfig: quicConfig, diff --git a/docs/config.yaml b/docs/config.yaml index 38c31f50..dce752ab 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1055,8 +1055,10 @@ proxies: # socks5 # down: "200 Mbps" # 若不写单位,默认为 Mbps # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" password: yourpassword - # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander + # obfs: salamander # 默认为空,如果填写则开启 obfs,目前支持 salamander 和 gecko # obfs-password: yourpassword + # obfs-min-packet-size: 512 # 最小线上数据包大小(字节)。仅限 Gecko。 + # obfs-max-packet-size: 1200 # 最大线上数据包大小(字节)。仅限 Gecko。 # sni: server.com # ech-opts: # enable: true # 必须手动开启 @@ -2033,8 +2035,10 @@ listeners: ## up 和 down 均不写或为 0 则使用 BBR 流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps - # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander + # obfs: salamander # 默认为空,如果填写则开启 obfs,目前支持 salamander 和 gecko # obfs-password: yourpassword + # obfs-min-packet-size: 512 # 最小线上数据包大小(字节)。仅限 Gecko。 + # obfs-max-packet-size: 1200 # 最大线上数据包大小(字节)。仅限 Gecko。 # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" # max-idle-time: 15000 # alpn: diff --git a/go.mod b/go.mod index 8c6b4069..bf92abd1 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/sing v0.5.7 github.com/metacubex/sing-mux v0.3.9 - github.com/metacubex/sing-quic v0.0.0-20260512151354-8475655be853 + github.com/metacubex/sing-quic v0.0.0-20260525071347-7f961b1132a0 github.com/metacubex/sing-shadowsocks v0.2.12 github.com/metacubex/sing-shadowsocks2 v0.2.7 github.com/metacubex/sing-shadowtls v0.0.0-20260517015314-c11c36474edc diff --git a/go.sum b/go.sum index 85f7f365..60e7c8df 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,8 @@ github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM= github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.9 h1:/aoBD2+sK2qsXDlNDe3hkR0GZuFDtwIZhOeGUx9W0Yk= github.com/metacubex/sing-mux v0.3.9/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc= -github.com/metacubex/sing-quic v0.0.0-20260512151354-8475655be853 h1:nZ5WNU6kjj6kBu4+2eMySFkUVGCop64rZnLMm+HPh8w= -github.com/metacubex/sing-quic v0.0.0-20260512151354-8475655be853/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= +github.com/metacubex/sing-quic v0.0.0-20260525071347-7f961b1132a0 h1:0qK4rNASJ4DbzHCAtcLisDx4g459yzQOzncr4FsjAYI= +github.com/metacubex/sing-quic v0.0.0-20260525071347-7f961b1132a0/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A= diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go index a71aab33..1d59b3bc 100644 --- a/listener/config/hysteria2.go +++ b/listener/config/hysteria2.go @@ -12,6 +12,8 @@ type Hysteria2Server struct { Users map[string]string `yaml:"users" json:"users,omitempty"` Obfs string `yaml:"obfs" json:"obfs,omitempty"` ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` + ObfsMinPacketSize int `yaml:"obfs-min-packet-size" json:"obfs-min-packet-size,omitempty"` + ObfsMaxPacketSize int `yaml:"obfs-max-packet-size" json:"obfs-max-packet-size,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go index 6872a97d..99502085 100644 --- a/listener/inbound/hysteria2.go +++ b/listener/inbound/hysteria2.go @@ -14,6 +14,8 @@ type Hysteria2Option struct { Users map[string]string `inbound:"users,omitempty"` Obfs string `inbound:"obfs,omitempty"` ObfsPassword string `inbound:"obfs-password,omitempty"` + ObfsMinPacketSize int `inbound:"obfs-min-packet-size,omitempty"` + ObfsMaxPacketSize int `inbound:"obfs-max-packet-size,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` ClientAuthType string `inbound:"client-auth-type,omitempty"` @@ -98,6 +100,8 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { Users: options.Users, Obfs: options.Obfs, ObfsPassword: options.ObfsPassword, + ObfsMinPacketSize: options.ObfsMinPacketSize, + ObfsMaxPacketSize: options.ObfsMaxPacketSize, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, diff --git a/listener/inbound/hysteria2_test.go b/listener/inbound/hysteria2_test.go index 9da650b2..30cd898c 100644 --- a/listener/inbound/hysteria2_test.go +++ b/listener/inbound/hysteria2_test.go @@ -114,6 +114,21 @@ func TestInboundHysteria2_Salamander(t *testing.T) { testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } +func TestInboundHysteria2_Gecko(t *testing.T) { + inboundOptions := inbound.Hysteria2Option{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + Obfs: "gecko", + ObfsPassword: userUUID, + } + outboundOptions := outbound.Hysteria2Option{ + Fingerprint: tlsFingerprint, + Obfs: "gecko", + ObfsPassword: userUUID, + } + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) +} + func TestInboundHysteria2_Brutal(t *testing.T) { inboundOptions := inbound.Hysteria2Option{ Certificate: tlsCertificate, diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index 8aa4bda2..091ab549 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -100,6 +100,8 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi } var salamanderPassword string + var geckoPassword string + var geckoMinPacketSize, geckoMaxPacketSize int if len(config.Obfs) > 0 { if config.ObfsPassword == "" { return nil, errors.New("missing obfs password") @@ -107,6 +109,10 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi switch config.Obfs { case hysteria2.ObfsTypeSalamander: salamanderPassword = config.ObfsPassword + case hysteria2.ObfsTypeGecko: + geckoPassword = config.ObfsPassword + geckoMinPacketSize = config.ObfsMinPacketSize + geckoMaxPacketSize = config.ObfsMaxPacketSize default: return nil, fmt.Errorf("unknown obfs type: %s", config.Obfs) } @@ -212,6 +218,9 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi SendBPS: utils.StringToBps(config.Up), ReceiveBPS: utils.StringToBps(config.Down), SalamanderPassword: salamanderPassword, + GeckoPassword: geckoPassword, + GeckoMinPacketSize: geckoMinPacketSize, + GeckoMaxPacketSize: geckoMaxPacketSize, TLSConfig: tlsConfig, QUICConfig: quicConfig, IgnoreClientBandwidth: config.IgnoreClientBandwidth,