From ced213d5a597de0bcfdfe304855a731801bfa63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sat, 28 Jun 2025 14:45:54 +0000 Subject: [PATCH] Change dependence to reality Co-authored-by: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> --- go.mod | 7 +- go.sum | 14 ++-- main/commands/all/tls/ech.go | 49 +++++++------ transport/internet/tls/ech.go | 127 +++++++++++++++++++++++++++++----- 4 files changed, 147 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index 3537851b..73514775 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,6 @@ module github.com/xtls/xray-core go 1.24 require ( - github.com/OmarTariq612/goech v0.0.1 - github.com/cloudflare/circl v1.6.1 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 @@ -20,14 +18,14 @@ require ( github.com/stretchr/testify v1.10.0 github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/vishvananda/netlink v1.3.1 - github.com/xtls/reality v0.0.0-20250608132114-50752aec6bfb + github.com/xtls/reality v0.0.0-20250527000105-e679ef7bb130 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.39.0 golang.org/x/net v0.41.0 golang.org/x/sync v0.15.0 golang.org/x/sys v0.33.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.73.0 + google.golang.org/grpc v1.72.1 google.golang.org/protobuf v1.36.6 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 @@ -36,6 +34,7 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index 6a4f448f..3197271a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/OmarTariq612/goech v0.0.1 h1:/0c918Bk1ik65GXDj2k7SOK78DyZr30Jmq9euy1/HXg= -github.com/OmarTariq612/goech v0.0.1/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= @@ -78,8 +76,8 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xtls/reality v0.0.0-20250608132114-50752aec6bfb h1:X6ziJCMsFF8Ac/0F3W7+UbFdHZTu+r5nZ/smksHVxNQ= -github.com/xtls/reality v0.0.0-20250608132114-50752aec6bfb/go.mod h1:yD47RN65bDLZgyHWMfFDiqlzrq4usDMt/Xzsk6tMbhw= +github.com/xtls/reality v0.0.0-20250527000105-e679ef7bb130 h1:v/TVypWnLferyoaNHh6a8oyggj9APBUzfl1OOgXNbpw= +github.com/xtls/reality v0.0.0-20250527000105-e679ef7bb130/go.mod h1:bJdU3ExzfUlY40Xxfibq3THW9IHiE8mHu/tEzud5JWM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= @@ -143,10 +141,10 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index 701cf937..4ca1411c 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -1,13 +1,15 @@ package tls import ( + "encoding/base64" "encoding/pem" "os" - "github.com/OmarTariq612/goech" - "github.com/cloudflare/circl/hpke" + "github.com/xtls/reality/hpke" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/main/commands/base" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/crypto/cryptobyte" ) var cmdECH = &base.Command{ @@ -30,34 +32,37 @@ var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "" var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output") func executeECH(cmd *base.Command, args []string) { - var kem hpke.KEM + var kem uint16 - if *input_pqSignatureSchemesEnabled { - kem = hpke.KEM_X25519_KYBER768_DRAFT00 - } else { - kem = hpke.KEM_X25519_HKDF_SHA256 - } + // if *input_pqSignatureSchemesEnabled { + // kem = 0x30 // hpke.KEM_X25519_KYBER768_DRAFT00 + // } else { + kem = hpke.DHKEM_X25519_HKDF_SHA256 + // } - echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem, nil) + echKeySet, priv, err := tls.GenerateECHKeySet(0, *input_serverName, kem) common.Must(err) - // Make single key set to a list with only one element - ECHConfigList := make(goech.ECHConfigList, 1) - ECHConfigList[0] = echKeySet.ECHConfig - ECHKeySetList := make(goech.ECHKeySetList, 1) - ECHKeySetList[0] = echKeySet - configBuffer, _ := ECHConfigList.MarshalBinary() - keyBuffer, _ := ECHKeySetList.MarshalBinary() - configStr, _ := ECHConfigList.ToBase64() - keySetStr, _ := ECHKeySetList.ToBase64() + configBytes, _ := tls.MarshalBinary(echKeySet) + var b cryptobyte.Builder + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes(configBytes) + }) + configBuffer, _ := b.Bytes() + var b2 cryptobyte.Builder + b2.AddUint16(uint16(len(priv))) + b2.AddBytes(priv) + b2.AddUint16(uint16(len(configBytes))) + b2.AddBytes(configBytes) + keyBuffer, _ := b2.Bytes() - configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) - keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) if *input_pem { + configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) + keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) os.Stdout.WriteString(configPEM) os.Stdout.WriteString(keyPEM) } else { - os.Stdout.WriteString("ECH config list: \n" + configStr + "\n") - os.Stdout.WriteString("ECH Key sets: \n" + keySetStr + "\n") + os.Stdout.WriteString("ECH config list: \n" + base64.StdEncoding.EncodeToString(configBuffer) + "\n") + os.Stdout.WriteString("ECH Key sets: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n") } } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 4f2a597e..94a9225b 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -3,18 +3,24 @@ package tls import ( "bytes" "context" + "crypto/ecdh" + "crypto/rand" "crypto/tls" + "encoding/base64" + "encoding/binary" "io" "net/http" "strings" "sync" "time" - "github.com/OmarTariq612/goech" "github.com/miekg/dns" + "github.com/xtls/reality" + "github.com/xtls/reality/hpke" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/transport/internet" + "golang.org/x/crypto/cryptobyte" ) func ApplyECH(c *Config, config *tls.Config) error { @@ -48,11 +54,10 @@ func ApplyECH(c *Config, config *tls.Config) error { return err } } else { - ECHConfigList, err := goech.ECHConfigListFromBase64(c.EchConfigList) + ECHConfig, err = base64.StdEncoding.DecodeString(c.EchConfigList) if err != nil { return errors.New("Failed to unmarshal ECHConfigList: ", err) } - ECHConfig, _ = ECHConfigList.MarshalBinary() } config.EncryptedClientHelloConfigList = ECHConfig @@ -60,22 +65,11 @@ func ApplyECH(c *Config, config *tls.Config) error { // for server if len(c.EchKeySets) != 0 { - var keys []tls.EncryptedClientHelloKey - KeySets, err := goech.UnmarshalECHKeySetList(c.EchKeySets) + KeySets, err := ConvertToGoECHKeys(c.EchKeySets) if err != nil { return errors.New("Failed to unmarshal ECHKeySetList: ", err) } - for idx, keySet := range KeySets { - ECHConfig, err := keySet.ECHConfig.MarshalBinary() - ECHPrivateKey, err := keySet.PrivateKey.MarshalBinary() - if err != nil { - return errors.New("Failed to marshal ECHKey in index: ", idx, "with err: ", err) - } - keys = append(keys, tls.EncryptedClientHelloKey{ - Config: ECHConfig, - PrivateKey: ECHPrivateKey}) - } - config.EncryptedClientHelloKeys = keys + config.EncryptedClientHelloKeys = KeySets } return nil @@ -237,3 +231,104 @@ func dnsQuery(server string, domain string) ([]byte, uint32, error) { } return []byte{}, 0, errors.New("no ech record found") } + +// reference github.com/OmarTariq612/goech +func MarshalBinary(ech reality.EchConfig) ([]byte, error) { + var b cryptobyte.Builder + b.AddUint16(ech.Version) + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddUint8(ech.ConfigID) + child.AddUint16(ech.KemID) + child.AddUint16(uint16(len(ech.PublicKey))) + child.AddBytes(ech.PublicKey) + child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + for _, cipherSuite := range ech.SymmetricCipherSuite { + child.AddUint16(cipherSuite.KDFID) + child.AddUint16(cipherSuite.AEADID) + } + }) + child.AddUint8(ech.MaxNameLength) + child.AddUint8(uint8(len(ech.PublicName))) + child.AddBytes(ech.PublicName) + child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + for _, extention := range ech.Extensions { + child.AddUint16(extention.Type) + child.AddBytes(extention.Data) + } + }) + }) + return b.Bytes() +} + +var ErrInvalidLen = errors.New("goech: invalid length") + +func ConvertToGoECHKeys(data []byte) ([]tls.EncryptedClientHelloKey, error) { + var keys []tls.EncryptedClientHelloKey + s := cryptobyte.String(data) + for !s.Empty() { + if len(s) < 2 { + return keys, ErrInvalidLen + } + keyLength := int(binary.BigEndian.Uint16(s[:2])) + if len(s) < keyLength+4 { + return keys, ErrInvalidLen + } + configLength := int(binary.BigEndian.Uint16(s[keyLength+2 : keyLength+4])) + if len(s) < 2+keyLength+2+configLength { + return keys, ErrInvalidLen + } + child := cryptobyte.String(s[:2+keyLength+2+configLength]) + var ( + sk, config cryptobyte.String + ) + if !child.ReadUint16LengthPrefixed(&sk) || !child.ReadUint16LengthPrefixed(&config) || !child.Empty() { + return keys, ErrInvalidLen + } + if !s.Skip(2 + keyLength + 2 + configLength) { + return keys, ErrInvalidLen + } + keys = append(keys, tls.EncryptedClientHelloKey{ + Config: config, + PrivateKey: sk, + }) + } + return keys, nil +} + +const ExtensionEncryptedClientHello = 0xfe0d +const KDF_HKDF_SHA384 = 0x0002 +const KDF_HKDF_SHA512 = 0x0003 + +func GenerateECHKeySet(configID uint8, domain string, kem uint16) (reality.EchConfig, []byte, error) { + config := reality.EchConfig{ + Version: ExtensionEncryptedClientHello, + ConfigID: configID, + PublicName: []byte(domain), + KemID: kem, + SymmetricCipherSuite: []reality.EchCipher{ + {KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_AES_128_GCM}, + {KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_AES_256_GCM}, + {KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_ChaCha20Poly1305}, + {KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_AES_128_GCM}, + {KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_AES_256_GCM}, + {KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_ChaCha20Poly1305}, + {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_128_GCM}, + {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_256_GCM}, + {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_ChaCha20Poly1305}, + }, + MaxNameLength: 0, + Extensions: nil, + } + // if kem == hpke.DHKEM_X25519_HKDF_SHA256 { + curve := ecdh.X25519() + priv := make([]byte, 32) //x25519 + _, err := io.ReadFull(rand.Reader, priv) + if err != nil { + return config, nil, err + } + privKey, _ := curve.NewPrivateKey(priv) + config.PublicKey = privKey.PublicKey().Bytes(); + return config, priv, nil + // } + // TODO: add mlkem768 (former kyber768 draft00). The golang mlkem private key is 64 bytes seed? +}