diff --git a/app/cmd/client.go b/app/cmd/client.go index e913d12b..f5a45613 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -42,6 +42,7 @@ import ( "github.com/apernet/hysteria/extras/v2/obfs" "github.com/apernet/hysteria/extras/v2/realm" "github.com/apernet/hysteria/extras/v2/transport/udphop" + "github.com/apernet/quic-go" ) // Ref: https://ip.skk.moe/stun @@ -170,6 +171,7 @@ type clientConfigQUIC struct { KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"` DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` DisableChromeParrot bool `mapstructure:"disableChromeParrot"` + Version string `mapstructure:"version"` Sockopts clientConfigQUICSockopts `mapstructure:"sockopts"` } @@ -459,7 +461,21 @@ func (c *clientConfig) validateMimic() error { } func (c *clientConfig) fillQUICConfig(hyConfig *client.Config) error { + var version quic.Version + switch c.QUIC.Version { + case "": + case "v1": + version = quic.Version1 + case "v2": + version = quic.Version2 + if !c.QUIC.DisableChromeParrot { + logger.Warn("Chrome parrot is disabled when using QUIC v2") + } + default: + return configError{Field: "quic.version", Err: errors.New("must be v1 or v2")} + } hyConfig.QUICConfig = client.QUICConfig{ + Version: version, InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow, MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow, diff --git a/app/cmd/client_test.go b/app/cmd/client_test.go index 5451910d..76d609ca 100644 --- a/app/cmd/client_test.go +++ b/app/cmd/client_test.go @@ -73,6 +73,7 @@ func TestClientConfig(t *testing.T) { KeepAlivePeriod: 4 * time.Second, DisablePathMTUDiscovery: true, DisableChromeParrot: true, + Version: "v2", Sockopts: clientConfigQUICSockopts{ BindInterface: stringRef("eth0"), FirewallMark: uint32Ref(1234), diff --git a/app/cmd/client_test.yaml b/app/cmd/client_test.yaml index 76022c7e..51353296 100644 --- a/app/cmd/client_test.yaml +++ b/app/cmd/client_test.yaml @@ -49,6 +49,7 @@ quic: keepAlivePeriod: 4s disablePathMTUDiscovery: true disableChromeParrot: true + version: v2 sockopts: bindInterface: eth0 fwmark: 1234 diff --git a/app/go.mod b/app/go.mod index b5e88b5e..88c965a7 100644 --- a/app/go.mod +++ b/app/go.mod @@ -8,6 +8,7 @@ require ( github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 github.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000 + github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/apernet/sing-tun v0.2.6-0.20250920121535-299f04629986 github.com/caddyserver/certmagic v0.25.4 github.com/libdns/cloudflare v0.2.2 @@ -33,7 +34,6 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e // indirect github.com/caddyserver/zerossl v0.1.5 // indirect github.com/database64128/netx-go v0.1.1 // indirect github.com/database64128/tfo-go/v2 v2.3.3 // indirect diff --git a/core/client/client.go b/core/client/client.go index a638cc2e..a4a8ea64 100644 --- a/core/client/client.go +++ b/core/client/client.go @@ -94,6 +94,9 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) { DisablePathManager: true, ChromeParrot: !c.config.QUICConfig.DisableChromeParrot, } + if v := c.config.QUICConfig.Version; v != 0 { + quicConfig.Versions = []quic.Version{v} + } tr := &quic.Transport{Conn: pktConn, DisableGSO: c.config.QUICConfig.DisableGSO} if !c.config.QUICConfig.DisableChromeParrot { // Chrome uses a zero-length source connection ID. This has to be set on the diff --git a/core/client/config.go b/core/client/config.go index 131a213a..7a171ee9 100644 --- a/core/client/config.go +++ b/core/client/config.go @@ -9,6 +9,7 @@ import ( "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/congestion" "github.com/apernet/hysteria/core/v2/internal/pmtud" + "github.com/apernet/quic-go" ) const ( @@ -74,6 +75,13 @@ func (c *Config) verifyAndFill() error { return errors.ConfigError{Field: "QUICConfig.KeepAlivePeriod", Reason: "must be between 2s and 60s"} } c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery + if v := c.QUICConfig.Version; v != 0 && v != quic.Version1 && v != quic.Version2 { + return errors.ConfigError{Field: "QUICConfig.Version", Reason: "must be v1 or v2"} + } + if c.QUICConfig.Version == quic.Version2 { + // The parrot advertises v1 in version_information, and Chrome never offers v2. + c.QUICConfig.DisableChromeParrot = true + } var err error c.CongestionConfig.Type, err = congestion.NormalizeType(c.CongestionConfig.Type) if err != nil { @@ -120,7 +128,8 @@ type QUICConfig struct { KeepAlivePeriod time.Duration DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. DisableGSO bool - DisableChromeParrot bool // Chrome QUIC fingerprint parroting is on by default. + DisableChromeParrot bool // Chrome QUIC fingerprint parroting is on by default. + Version quic.Version // v1 or v2. Zero = quic-go default (v1). } type CongestionConfig struct {