diff --git a/common/tls/reality_client.go b/common/tls/reality_client.go index 6753e1db..98d1c823 100644 --- a/common/tls/reality_client.go +++ b/common/tls/reality_client.go @@ -45,10 +45,11 @@ import ( var _ ConfigCompat = (*RealityClientConfig)(nil) type RealityClientConfig struct { - ctx context.Context - uClient *UTLSClientConfig - publicKey []byte - shortID [8]byte + ctx context.Context + uClient *UTLSClientConfig + publicKey []byte + shortID [8]byte + supportX25519MLKEM768 bool } func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { @@ -84,7 +85,7 @@ func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAd return nil, E.New("invalid short_id") } - var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID} + var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID, options.Reality.SupportX25519MLKEM768} if options.KernelRx || options.KernelTx { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") @@ -145,22 +146,24 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn if err != nil { return nil, err } - for _, extension := range uConn.Extensions { - if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { - ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool { - return curveID != utls.X25519MLKEM768 - }) + if !e.supportX25519MLKEM768 { + for _, extension := range uConn.Extensions { + if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { + ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool { + return curveID != utls.X25519MLKEM768 + }) + } + if ks, ok := extension.(*utls.KeyShareExtension); ok { + ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool { + return share.Group != utls.X25519MLKEM768 + }) + } } - if ks, ok := extension.(*utls.KeyShareExtension); ok { - ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool { - return share.Group != utls.X25519MLKEM768 - }) + err = uConn.BuildHandshakeState() + if err != nil { + return nil, err } } - err = uConn.BuildHandshakeState() - if err != nil { - return nil, err - } if len(uConfig.NextProtos) > 0 { for _, extension := range uConn.Extensions { @@ -271,6 +274,7 @@ func (e *RealityClientConfig) Clone() Config { e.uClient.Clone().(*UTLSClientConfig), e.publicKey, e.shortID, + e.supportX25519MLKEM768, } } diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 5929962c..960539c6 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -152,7 +152,8 @@ icon: material/new-box "reality": { "enabled": false, "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", - "short_id": "0123456789abcdef" + "short_id": "0123456789abcdef", + "support_x25519mlkem768": false } } ``` @@ -825,6 +826,16 @@ Public key, generated by `sing-box generate reality-keypair`. A hexadecimal string with zero to eight digits. +#### support_x25519mlkem768 + +==Client only== + +Keep the `X25519MLKEM768` post-quantum hybrid key share in the ClientHello instead of stripping it. + +Required by REALITY servers running `xtls/reality` built after 2026-09-08 (Xray-core >= v26.9.8), which reject +ClientHellos that don't offer it before any plain `X25519` share. Older REALITY servers may fail the handshake +if this key share is present, so it is disabled by default. + #### max_time_difference ==Server only== diff --git a/option/tls.go b/option/tls.go index 5c807d67..d73bb5b1 100644 --- a/option/tls.go +++ b/option/tls.go @@ -250,7 +250,8 @@ type OutboundUTLSOptions struct { } type OutboundRealityOptions struct { - Enabled bool `json:"enabled,omitempty"` - PublicKey string `json:"public_key,omitempty"` - ShortID string `json:"short_id,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PublicKey string `json:"public_key,omitempty"` + ShortID string `json:"short_id,omitempty"` + SupportX25519MLKEM768 bool `json:"support_x25519mlkem768,omitempty"` }