diff --git a/go.mod b/go.mod index d11852fa..b1ec34c0 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 @@ -12,22 +10,22 @@ require ( github.com/miekg/dns v1.1.66 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.51.0 - github.com/refraction-networking/utls v1.7.2 + github.com/quic-go/quic-go v0.52.0 + github.com/refraction-networking/utls v1.7.3 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.10.0 github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e - github.com/vishvananda/netlink v1.3.0 - github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d + github.com/vishvananda/netlink v1.3.1 + github.com/xtls/reality v0.0.0-20250527000105-e679ef7bb130 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.38.0 golang.org/x/net v0.40.0 golang.org/x/sync v0.14.0 golang.org/x/sys v0.33.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.72.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 @@ -47,7 +46,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect - github.com/vishvananda/netns v0.0.4 // indirect + github.com/vishvananda/netns v0.0.5 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/text v0.25.0 // indirect diff --git a/go.sum b/go.sum index c7bd40fe..24d724b1 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= @@ -54,10 +52,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc= -github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= -github.com/refraction-networking/utls v1.7.2 h1:XOgYzit7lAKaa7kzAO5BJR9l4X/H200eVUD4s8SF8/s= -github.com/refraction-networking/utls v1.7.2/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= +github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= +github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= +github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo= +github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= @@ -72,12 +70,12 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= -github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= -github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= -github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg= -github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +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-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,8 +141,8 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= 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.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +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? +}