From d0ef5c028db6d4580bf32202ea5ebeb6aec70ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 May 2026 13:11:59 +0800 Subject: [PATCH] hysteria2: Add gecko obfs --- constant/hysteria2.go | 5 +++ docs/configuration/inbound/hysteria2.md | 21 ++++++++- docs/configuration/inbound/hysteria2.zh.md | 23 ++++++++-- docs/configuration/outbound/hysteria2.md | 21 ++++++++- docs/configuration/outbound/hysteria2.zh.md | 23 ++++++++-- go.mod | 2 +- go.sum | 4 +- option/hysteria2.go | 48 +++++++++++++++++++-- protocol/hysteria2/inbound.go | 9 ++++ protocol/hysteria2/outbound.go | 9 ++++ 10 files changed, 149 insertions(+), 16 deletions(-) diff --git a/constant/hysteria2.go b/constant/hysteria2.go index 35c0b14f2..2d6e6ee8b 100644 --- a/constant/hysteria2.go +++ b/constant/hysteria2.go @@ -1,5 +1,10 @@ package constant +const ( + Hysteria2ObfsTypeSalamander = "salamander" + Hysteria2ObfsTypeGecko = "gecko" +) + const ( Hysterai2MasqueradeTypeFile = "file" Hysterai2MasqueradeTypeProxy = "proxy" diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 0769e3c40..28caae87a 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -5,7 +5,8 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.14.0" :material-plus: [bbr_profile](#bbr_profile) - :material-plus: [realm](#realm) + :material-plus: [realm](#realm) + :material-alert: [obfs](#obfstype) !!! quote "Changes in sing-box 1.11.0" @@ -75,7 +76,7 @@ Conflict with `ignore_client_bandwidth`. #### obfs.type -QUIC traffic obfuscator type, only available with `salamander`. +QUIC traffic obfuscator type, one of `salamander` `gecko`. Disabled if empty. @@ -83,6 +84,22 @@ Disabled if empty. QUIC traffic obfuscator password. +#### obfs.min_packet_size + +!!! question "Since sing-box 1.14.0" + +Minimum on-wire packet size in bytes. Gecko only. + +`512` is used by default. + +#### obfs.max_packet_size + +!!! question "Since sing-box 1.14.0" + +Maximum on-wire packet size in bytes. Gecko only. + +`1200` is used by default. + #### users Hysteria2 users diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 4af8a9eed..d59101b38 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -5,7 +5,8 @@ icon: material/alert-decagram !!! quote "sing-box 1.14.0 中的更改" :material-plus: [bbr_profile](#bbr_profile) - :material-plus: [realm](#realm) + :material-plus: [realm](#realm) + :material-alert: [obfs](#obfstype) !!! quote "sing-box 1.11.0 中的更改" @@ -72,13 +73,29 @@ icon: material/alert-decagram #### obfs.type -QUIC 流量混淆器类型,仅可设为 `salamander`。 +QUIC 流量混淆器类型,可选 `salamander` `gecko`。 如果为空则禁用。 #### obfs.password -QUIC 流量混淆器密码. +QUIC 流量混淆器密码。 + +#### obfs.min_packet_size + +!!! question "自 sing-box 1.14.0 起" + +最小线上数据包大小(字节)。仅限 Gecko。 + +默认使用 `512`。 + +#### obfs.max_packet_size + +!!! question "自 sing-box 1.14.0 起" + +最大线上数据包大小(字节)。仅限 Gecko。 + +默认使用 `1200`。 #### users diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 3bcd42eaa..1df36cfc2 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -2,7 +2,8 @@ :material-plus: [hop_interval_max](#hop_interval_max) :material-plus: [bbr_profile](#bbr_profile) - :material-plus: [realm](#realm) + :material-plus: [realm](#realm) + :material-alert: [obfs](#obfstype) !!! quote "Changes in sing-box 1.11.0" @@ -113,7 +114,7 @@ If empty, the BBR congestion control algorithm will be used instead of Hysteria #### obfs.type -QUIC traffic obfuscator type, only available with `salamander`. +QUIC traffic obfuscator type, one of `salamander` `gecko`. Disabled if empty. @@ -121,6 +122,22 @@ Disabled if empty. QUIC traffic obfuscator password. +#### obfs.min_packet_size + +!!! question "Since sing-box 1.14.0" + +Minimum on-wire packet size in bytes. Gecko only. + +`512` is used by default. + +#### obfs.max_packet_size + +!!! question "Since sing-box 1.14.0" + +Maximum on-wire packet size in bytes. Gecko only. + +`1200` is used by default. + #### password Authentication password. diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index a39f3be50..07ec96ce1 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -2,7 +2,8 @@ :material-plus: [hop_interval_max](#hop_interval_max) :material-plus: [bbr_profile](#bbr_profile) - :material-plus: [realm](#realm) + :material-plus: [realm](#realm) + :material-alert: [obfs](#obfstype) !!! quote "sing-box 1.11.0 中的更改" @@ -111,13 +112,29 @@ #### obfs.type -QUIC 流量混淆器类型,仅可设为 `salamander`。 +QUIC 流量混淆器类型,可选 `salamander` `gecko`。 如果为空则禁用。 #### obfs.password -QUIC 流量混淆器密码. +QUIC 流量混淆器密码。 + +#### obfs.min_packet_size + +!!! question "自 sing-box 1.14.0 起" + +最小线上数据包大小(字节)。仅限 Gecko。 + +默认使用 `512`。 + +#### obfs.max_packet_size + +!!! question "自 sing-box 1.14.0 起" + +最大线上数据包大小(字节)。仅限 Gecko。 + +默认使用 `1200`。 #### password diff --git a/go.mod b/go.mod index b1f8addbc..d874a8121 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/sagernet/sing v0.8.12-0.20260625092856-31bbf21d4b12 github.com/sagernet/sing-cloudflared v0.1.1 github.com/sagernet/sing-mux v0.3.5 - github.com/sagernet/sing-quic v0.6.2-0.20260520073201-c8655743eb6e + github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1 diff --git a/go.sum b/go.sum index 00970b53d..60086eeb2 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.1 h1:By29ZWMJl8QU6UcC5pmBv803rYigAoSmz github.com/sagernet/sing-cloudflared v0.1.1/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.2-0.20260520073201-c8655743eb6e h1:CCJxS/iXhUA2KlQ7vNjcYUVGTYjNeHQ5s5MC32WlVPw= -github.com/sagernet/sing-quic v0.6.2-0.20260520073201-c8655743eb6e/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= +github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7 h1:hFLPJ21uNZSbRnzhOKz4Zv0b4F93mpDorWyN93BeRcM= +github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/option/hysteria2.go b/option/hysteria2.go index 86730632a..d20c0bd37 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -38,9 +38,51 @@ type Hysteria2InboundRealm struct { STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"` } -type Hysteria2Obfs struct { - Type string `json:"type,omitempty"` - Password string `json:"password,omitempty"` +type Hysteria2ObfsGecko struct { + MinPacketSize int `json:"min_packet_size,omitempty"` + MaxPacketSize int `json:"max_packet_size,omitempty"` +} + +type _Hysteria2Obfs struct { + Type string `json:"type,omitempty"` + Password string `json:"password,omitempty"` + GeckoOptions Hysteria2ObfsGecko `json:"-"` +} + +type Hysteria2Obfs _Hysteria2Obfs + +func (o Hysteria2Obfs) MarshalJSON() ([]byte, error) { + var v any + switch o.Type { + case C.Hysteria2ObfsTypeSalamander: + case C.Hysteria2ObfsTypeGecko: + v = o.GeckoOptions + default: + return nil, E.New("unknown obfs type: ", o.Type) + } + if v == nil { + return json.Marshal((_Hysteria2Obfs)(o)) + } + return badjson.MarshallObjects((_Hysteria2Obfs)(o), v) +} + +func (o *Hysteria2Obfs) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_Hysteria2Obfs)(o)) + if err != nil { + return err + } + var v any + switch o.Type { + case C.Hysteria2ObfsTypeSalamander: + case C.Hysteria2ObfsTypeGecko: + v = &o.GeckoOptions + default: + return E.New("unknown obfs type: ", o.Type) + } + if v == nil { + return nil + } + return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Obfs)(o), v) } type Hysteria2User struct { diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index d18b3fa62..b706b4920 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -52,6 +52,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return nil, err } var salamanderPassword string + var geckoPassword string + var geckoMinPacketSize, geckoMaxPacketSize int if options.Obfs != nil { if options.Obfs.Password == "" { return nil, E.New("missing obfs password") @@ -59,6 +61,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo switch options.Obfs.Type { case hysteria2.ObfsTypeSalamander: salamanderPassword = options.Obfs.Password + case hysteria2.ObfsTypeGecko: + geckoPassword = options.Obfs.Password + geckoMinPacketSize = options.Obfs.GeckoOptions.MinPacketSize + geckoMaxPacketSize = options.Obfs.GeckoOptions.MaxPacketSize default: return nil, E.New("unknown obfs type: ", options.Obfs.Type) } @@ -154,6 +160,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), SalamanderPassword: salamanderPassword, + GeckoPassword: geckoPassword, + GeckoMinPacketSize: geckoMinPacketSize, + GeckoMaxPacketSize: geckoMaxPacketSize, TLSConfig: tlsConfig, QUICOptions: qtls.QUICOptions{ IdleTimeout: options.IdleTimeout.Build(), diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index f4d4d58d3..7651a0c65 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -59,6 +59,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } var salamanderPassword string + var geckoPassword string + var geckoMinPacketSize, geckoMaxPacketSize int if options.Obfs != nil { if options.Obfs.Password == "" { return nil, E.New("missing obfs password") @@ -66,6 +68,10 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL switch options.Obfs.Type { case hysteria2.ObfsTypeSalamander: salamanderPassword = options.Obfs.Password + case hysteria2.ObfsTypeGecko: + geckoPassword = options.Obfs.Password + geckoMinPacketSize = options.Obfs.GeckoOptions.MinPacketSize + geckoMaxPacketSize = options.Obfs.GeckoOptions.MaxPacketSize default: return nil, E.New("unknown obfs type: ", options.Obfs.Type) } @@ -121,6 +127,9 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), SalamanderPassword: salamanderPassword, + GeckoPassword: geckoPassword, + GeckoMinPacketSize: geckoMinPacketSize, + GeckoMaxPacketSize: geckoMaxPacketSize, Password: options.Password, TLSConfig: tlsConfig, QUICOptions: qtls.QUICOptions{