From caa1a5513bc638c99de73be60b51532d6be960dc 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, 8 Feb 2025 10:07:42 +0000 Subject: [PATCH 01/27] ECH: client support TLS Encrypted Client Hello --- infra/conf/transport_internet.go | 11 +++ transport/internet/tls/config.go | 6 ++ transport/internet/tls/config.pb.go | 40 ++++++-- transport/internet/tls/config.proto | 6 ++ transport/internet/tls/ech.go | 137 ++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 transport/internet/tls/ech.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 8e1ab9e3c8d0..74be9901c7e8 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -412,6 +412,8 @@ type TLSConfig struct { MasterKeyLog string `json:"masterKeyLog"` ServerNameToVerify string `json:"serverNameToVerify"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` + ECHConfig string `json:"echConfig"` + ECHDOHServer string `json:"echDohServer"` } // Build implements Buildable. @@ -483,6 +485,15 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.VerifyPeerCertInNames = c.VerifyPeerCertInNames + if c.ECHConfig != "" { + ECHConfig, err := base64.StdEncoding.DecodeString(c.ECHConfig) + if err != nil { + return nil, errors.New("invalid ECH Config", c.ECHConfig) + } + config.EchConfig = ECHConfig + } + config.Ech_DOHserver = c.ECHDOHServer + return config, nil } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 517bf40bcd92..ff37cc5c66e5 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -444,6 +444,12 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { config.KeyLogWriter = writer } } + if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 { + err := ApplyECH(c, config) + if err != nil { + errors.LogError(context.Background(), err) + } + } return config } diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index bc45dc4e0271..5808e6e417e2 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -217,6 +217,8 @@ type Config struct { // @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried. // @Critical VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` + EchConfig []byte `protobuf:"bytes,20,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` + Ech_DOHserver string `protobuf:"bytes,21,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"` } func (x *Config) Reset() { @@ -361,6 +363,20 @@ func (x *Config) GetVerifyPeerCertInNames() []string { return nil } +func (x *Config) GetEchConfig() []byte { + if x != nil { + return x.EchConfig + } + return nil +} + +func (x *Config) GetEch_DOHserver() string { + if x != nil { + return x.Ech_DOHserver + } + return "" +} + var File_transport_internet_tls_config_proto protoreflect.FileDescriptor var file_transport_internet_tls_config_proto_rawDesc = []byte{ @@ -392,7 +408,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, - 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x9a, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xde, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, @@ -442,15 +458,19 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, - 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, - 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4f, 0x48, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, + 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, + 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, + 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 3fac25afb201..01ea5eb5b507 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -91,4 +91,10 @@ message Config { @Critical */ repeated string verify_peer_cert_in_names = 17; + + + + bytes ech_config = 20; + + string ech_DOHserver = 21; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go new file mode 100644 index 000000000000..46518af437d0 --- /dev/null +++ b/transport/internet/tls/ech.go @@ -0,0 +1,137 @@ +package tls + +import ( + "bytes" + "context" + "crypto/tls" + "io" + "net/http" + "sync" + "time" + + "github.com/miekg/dns" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" +) + +func ApplyECH(c *Config, config *tls.Config) error { + var ECHConfig []byte + var err error + + if len(c.EchConfig) > 0 { + ECHConfig = c.EchConfig + } else { // ECH config > DOH lookup + if config.ServerName == "" { + return errors.New("Using DOH for ECH needs serverName") + } + ECHConfig, err = QueryRecord(c.ServerName, c.Ech_DOHserver) + if err != nil { + return err + } + } + + config.EncryptedClientHelloConfigList = ECHConfig + return nil +} + +type record struct { + record []byte + expire time.Time +} + +var ( + dnsCache = make(map[string]record) + mutex sync.RWMutex +) + +func QueryRecord(domain string, server string) ([]byte, error) { + mutex.Lock() + rec, found := dnsCache[domain] + if found && rec.expire.After(time.Now()) { + mutex.Unlock() + return rec.record, nil + } + mutex.Unlock() + + errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) + record, ttl, err := dohQuery(server, domain) + if err != nil { + return []byte{}, err + } + + if ttl < 600 { + ttl = 600 + } + + mutex.Lock() + defer mutex.Unlock() + rec.record = record + rec.expire = time.Now().Add(time.Second * time.Duration(ttl)) + dnsCache[domain] = rec + return record, nil +} + +func dohQuery(server string, domain string) ([]byte, uint32, error) { + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) + m.Id = 0 + msg, err := m.Pack() + if err != nil { + return []byte{}, 0, err + } + tr := &http.Transport{ + IdleConnTimeout: 90 * time.Second, + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, err + } + conn, err := internet.DialSystem(ctx, dest, nil) + if err != nil { + return nil, err + } + return conn, nil + }, + } + client := &http.Client{ + Timeout: 5 * time.Second, + Transport: tr, + } + req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) + if err != nil { + return []byte{}, 0, err + } + req.Header.Set("Content-Type", "application/dns-message") + resp, err := client.Do(req) + if err != nil { + return []byte{}, 0, err + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return []byte{}, 0, err + } + if resp.StatusCode != http.StatusOK { + return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode) + } + respMsg := new(dns.Msg) + err = respMsg.Unpack(respBody) + if err != nil { + return []byte{}, 0, err + } + if len(respMsg.Answer) > 0 { + for _, answer := range respMsg.Answer { + if https, ok := answer.(*dns.HTTPS); ok && https.Hdr.Name == dns.Fqdn(domain) { + for _, v := range https.Value { + if echConfig, ok := v.(*dns.SVCBECHConfig); ok { + errors.LogDebug(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl) + return echConfig.ECH, answer.Header().Ttl, nil + } + } + } + } + } + return []byte{}, 0, errors.New("no ech record found") +} From 88b942e8367049b843cfd5dd343df21f15ba6f88 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, 8 Feb 2025 12:12:12 +0000 Subject: [PATCH 02/27] Update mutex usage --- transport/internet/tls/ech.go | 79 ++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 46518af437d0..086eb5c3a6d5 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -36,50 +36,79 @@ func ApplyECH(c *Config, config *tls.Config) error { } type record struct { - record []byte - expire time.Time + echConfig []byte + expire time.Time } var ( - dnsCache = make(map[string]record) - mutex sync.RWMutex + dnsCache = make(map[string]record) + // global Lock? I'm not sure if I need finer grained locks. + // If we do this, we will need to nest another layer of struct + dnsCacheLock sync.RWMutex + updating sync.Mutex ) + +// QueryRecord returns the ECH config for given domain. +// If the record is not in cache or expired, it will query the DOH server and update the cache. func QueryRecord(domain string, server string) ([]byte, error) { - mutex.Lock() - rec, found := dnsCache[domain] - if found && rec.expire.After(time.Now()) { - mutex.Unlock() - return rec.record, nil - } - mutex.Unlock() + dnsCacheLock.RLock() + rec, found := dnsCache[domain] + dnsCacheLock.RUnlock() + if found && rec.expire.After(time.Now()) { + errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) + return rec.echConfig, nil + } - errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) - record, ttl, err := dohQuery(server, domain) - if err != nil { - return []byte{}, err - } + updating.Lock() + defer updating.Unlock() + // Try to get cache again after lock, in case another goroutine has updated it + // This might happen when the core tring is just stared and multiple goroutines are trying to query the same domain + dnsCacheLock.RLock() + rec, found = dnsCache[domain] + dnsCacheLock.RUnlock() + if found && rec.expire.After(time.Now()) { + errors.LogDebug(context.Background(), "ECH Config cache hit for domain: ", domain, " after trying to get update lock") + return rec.echConfig, nil + } - if ttl < 600 { - ttl = 600 - } + // Query ECH config from DOH server + errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) + echConfig, ttl, err := dohQuery(server, domain) + if err != nil { + return []byte{}, err + } - mutex.Lock() - defer mutex.Unlock() - rec.record = record - rec.expire = time.Now().Add(time.Second * time.Duration(ttl)) - dnsCache[domain] = rec - return record, nil + // Set minimum TTL to 600 seconds + if ttl < 600 { + ttl = 600 + } + + // Get write lock and update cache + dnsCacheLock.Lock() + defer dnsCacheLock.Unlock() + newRecored := record{ + echConfig: echConfig, + expire: time.Now().Add(time.Second * time.Duration(ttl)), + } + dnsCache[domain] = newRecored + return echConfig, nil } + +// dohQuery is the real func for sending type65 query for given domain to given DOH server. +// return ECH config, TTL and error func dohQuery(server string, domain string) ([]byte, uint32, error) { m := new(dns.Msg) m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) + // always 0 in DOH m.Id = 0 msg, err := m.Pack() if err != nil { return []byte{}, 0, err } + // All traffic sent by core should via xray's internet.DialSystem + // This involves the behavior of some Android VPN GUI clients tr := &http.Transport{ IdleConnTimeout: 90 * time.Second, ForceAttemptHTTP2: true, From 79c39ad56fb43de22faf80511282c0b0ff4ca70d 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, 8 Feb 2025 18:29:09 +0000 Subject: [PATCH 03/27] Add new doh server format --- transport/internet/tls/ech.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 086eb5c3a6d5..c56d0f2c1a68 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "io" "net/http" + "strings" "sync" "time" @@ -19,13 +20,28 @@ func ApplyECH(c *Config, config *tls.Config) error { var ECHConfig []byte var err error + nameToQuery := c.ServerName + var DOHServer string + + parts := strings.Split(c.Ech_DOHserver, "+") + if len(parts) == 2 { + // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" + nameToQuery = parts[0] + DOHServer = parts[1] + } else if len(parts) == 1 { + // normal format + DOHServer = parts[0] + } else { + return errors.New("Invalid ECH DOH server format: ", c.Ech_DOHserver) + } + if len(c.EchConfig) > 0 { ECHConfig = c.EchConfig } else { // ECH config > DOH lookup - if config.ServerName == "" { - return errors.New("Using DOH for ECH needs serverName") + if nameToQuery == "" { + return errors.New("Using DOH for ECH needs serverName or use dohServer format example.com+https://1.1.1.1/dns-query") } - ECHConfig, err = QueryRecord(c.ServerName, c.Ech_DOHserver) + ECHConfig, err = QueryRecord(nameToQuery, DOHServer) if err != nil { return err } @@ -41,14 +57,13 @@ type record struct { } var ( - dnsCache = make(map[string]record) + dnsCache = make(map[string]record) // global Lock? I'm not sure if I need finer grained locks. // If we do this, we will need to nest another layer of struct dnsCacheLock sync.RWMutex updating sync.Mutex ) - // QueryRecord returns the ECH config for given domain. // If the record is not in cache or expired, it will query the DOH server and update the cache. func QueryRecord(domain string, server string) ([]byte, error) { @@ -95,7 +110,6 @@ func QueryRecord(domain string, server string) ([]byte, error) { return echConfig, nil } - // dohQuery is the real func for sending type65 query for given domain to given DOH server. // return ECH config, TTL and error func dohQuery(server string, domain string) ([]byte, uint32, error) { From 5bae374f6139110a8b59f35b41b88cad3b240ecb 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, 15 Feb 2025 15:19:18 +0000 Subject: [PATCH 04/27] Use sync.Map --- transport/internet/tls/ech.go | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index c56d0f2c1a68..a5a6a3059c1b 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -57,19 +57,17 @@ type record struct { } var ( - dnsCache = make(map[string]record) - // global Lock? I'm not sure if I need finer grained locks. + dnsCache sync.Map + // global Lock? I'm not sure if this needs finer grained locks. // If we do this, we will need to nest another layer of struct - dnsCacheLock sync.RWMutex - updating sync.Mutex + updating sync.Mutex ) // QueryRecord returns the ECH config for given domain. // If the record is not in cache or expired, it will query the DOH server and update the cache. func QueryRecord(domain string, server string) ([]byte, error) { - dnsCacheLock.RLock() - rec, found := dnsCache[domain] - dnsCacheLock.RUnlock() + val, found := dnsCache.Load(domain) + rec, _ := val.(record) if found && rec.expire.After(time.Now()) { errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) return rec.echConfig, nil @@ -79,9 +77,8 @@ func QueryRecord(domain string, server string) ([]byte, error) { defer updating.Unlock() // Try to get cache again after lock, in case another goroutine has updated it // This might happen when the core tring is just stared and multiple goroutines are trying to query the same domain - dnsCacheLock.RLock() - rec, found = dnsCache[domain] - dnsCacheLock.RUnlock() + val, found = dnsCache.Load(domain) + rec, _ = val.(record) if found && rec.expire.After(time.Now()) { errors.LogDebug(context.Background(), "ECH Config cache hit for domain: ", domain, " after trying to get update lock") return rec.echConfig, nil @@ -99,14 +96,12 @@ func QueryRecord(domain string, server string) ([]byte, error) { ttl = 600 } - // Get write lock and update cache - dnsCacheLock.Lock() - defer dnsCacheLock.Unlock() + // Update cache newRecored := record{ echConfig: echConfig, expire: time.Now().Add(time.Second * time.Duration(ttl)), } - dnsCache[domain] = newRecored + dnsCache.Store(domain, newRecored) return echConfig, nil } From 7bbf7a702c60406de8ebed254022bf8ecb8b1a2a 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: Thu, 27 Feb 2025 07:14:03 +0000 Subject: [PATCH 05/27] Update goech to v0.0.1 --- go.mod | 2 +- go.sum | 8 ++++---- main/commands/all/tls/ech.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 62ef9bdc811f..a58c1e78aedb 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/xtls/xray-core go 1.24 require ( - github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 + 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 diff --git a/go.sum b/go.sum index d4f9161df55c..682295e277fd 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I= -github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +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= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index d4e17f9be3f1..184f20f43cfa 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -40,7 +40,7 @@ func executeECH(cmd *base.Command, args []string) { kem = hpke.KEM_X25519_HKDF_SHA256 } - echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem) + echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem, nil) common.Must(err) configBuffer, _ := echKeySet.ECHConfig.MarshalBinary() From ebf4d19c7de6aac9615e36bb2b7b02225aa1b190 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, 1 Mar 2025 19:23:56 +0000 Subject: [PATCH 06/27] Add server support --- infra/conf/transport_internet.go | 8 ++++ main/commands/all/tls/ech.go | 9 +++- transport/internet/tls/config.go | 2 +- transport/internet/tls/config.pb.go | 38 +++++++++++------ transport/internet/tls/config.proto | 6 +-- transport/internet/tls/ech.go | 66 ++++++++++++++++++++--------- 6 files changed, 88 insertions(+), 41 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 74be9901c7e8..6b174b371a02 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -414,6 +414,7 @@ type TLSConfig struct { VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHConfig string `json:"echConfig"` ECHDOHServer string `json:"echDohServer"` + EchKeySets string `json:"echKeySets"` } // Build implements Buildable. @@ -492,6 +493,13 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.EchConfig = ECHConfig } + if c.EchKeySets != "" { + EchPrivateKey, err := base64.StdEncoding.DecodeString(c.EchKeySets) + if err != nil { + return nil, errors.New("invalid ECH Config", c.EchKeySets) + } + config.EchKeySets = EchPrivateKey + } config.Ech_DOHserver = c.ECHDOHServer return config, nil diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index 184f20f43cfa..c8aa2953b7a7 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -43,8 +43,13 @@ func executeECH(cmd *base.Command, args []string) { echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem, nil) common.Must(err) - configBuffer, _ := echKeySet.ECHConfig.MarshalBinary() - keyBuffer, _ := echKeySet.MarshalBinary() + // 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() configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index ff37cc5c66e5..945fa311cb0f 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { config.KeyLogWriter = writer } } - if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 { + if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 || len(c.EchKeySets) > 0 { err := ApplyECH(c, config) if err != nil { errors.LogError(context.Background(), err) diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 5808e6e417e2..ef4834517924 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -217,8 +217,9 @@ type Config struct { // @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried. // @Critical VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` - EchConfig []byte `protobuf:"bytes,20,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` - Ech_DOHserver string `protobuf:"bytes,21,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"` + EchConfig []byte `protobuf:"bytes,18,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` + Ech_DOHserver string `protobuf:"bytes,19,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"` + EchKeySets []byte `protobuf:"bytes,20,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"` } func (x *Config) Reset() { @@ -377,6 +378,13 @@ func (x *Config) GetEch_DOHserver() string { return "" } +func (x *Config) GetEchKeySets() []byte { + if x != nil { + return x.EchKeySets + } + return nil +} + var File_transport_internet_tls_config_proto protoreflect.FileDescriptor var file_transport_internet_tls_config_proto_rawDesc = []byte{ @@ -408,7 +416,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, - 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xde, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x80, 0x07, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, @@ -459,18 +467,20 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, + 0x66, 0x69, 0x67, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4f, 0x48, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, - 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, - 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, - 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, - 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, - 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, + 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x63, 0x68, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, + 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, + 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, + 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, + 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 01ea5eb5b507..09b939995111 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -92,9 +92,9 @@ message Config { */ repeated string verify_peer_cert_in_names = 17; + bytes ech_config = 18; + string ech_DOHserver = 19; - bytes ech_config = 20; - - string ech_DOHserver = 21; + bytes ech_key_sets = 20; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index a5a6a3059c1b..572db5b3a4c3 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -4,12 +4,14 @@ import ( "bytes" "context" "crypto/tls" + "fmt" "io" "net/http" "strings" "sync" "time" + "github.com/OmarTariq612/goech" "github.com/miekg/dns" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" @@ -23,31 +25,53 @@ func ApplyECH(c *Config, config *tls.Config) error { nameToQuery := c.ServerName var DOHServer string - parts := strings.Split(c.Ech_DOHserver, "+") - if len(parts) == 2 { - // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" - nameToQuery = parts[0] - DOHServer = parts[1] - } else if len(parts) == 1 { - // normal format - DOHServer = parts[0] - } else { - return errors.New("Invalid ECH DOH server format: ", c.Ech_DOHserver) - } - - if len(c.EchConfig) > 0 { - ECHConfig = c.EchConfig - } else { // ECH config > DOH lookup - if nameToQuery == "" { - return errors.New("Using DOH for ECH needs serverName or use dohServer format example.com+https://1.1.1.1/dns-query") + if len(c.EchConfig) != 0 { + parts := strings.Split(c.Ech_DOHserver, "+") + if len(parts) == 2 { + // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" + nameToQuery = parts[0] + DOHServer = parts[1] + } else if len(parts) == 1 { + // normal format + DOHServer = parts[0] + } else { + return errors.New("Invalid ECH DOH server format: ", c.Ech_DOHserver) } - ECHConfig, err = QueryRecord(nameToQuery, DOHServer) - if err != nil { - return err + + if len(c.EchConfig) > 0 { + ECHConfig = c.EchConfig + } else { // ECH config > DOH lookup + if nameToQuery == "" { + return errors.New("Using DOH for ECH needs serverName or use dohServer format example.com+https://1.1.1.1/dns-query") + } + ECHConfig, err = QueryRecord(nameToQuery, DOHServer) + if err != nil { + return err + } } + + config.EncryptedClientHelloConfigList = ECHConfig } - config.EncryptedClientHelloConfigList = ECHConfig + if len(c.EchKeySets) != 0 { + var keys []tls.EncryptedClientHelloKey + KeySets, err := goech.UnmarshalECHKeySetList(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 + fmt.Println(config.EncryptedClientHelloKeys) + } return nil } From e3bd972d03bd7032c121642ff4ecb0e29477f667 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, 1 Mar 2025 19:38:41 +0000 Subject: [PATCH 07/27] Refine xray tls ech output format --- main/commands/all/tls/ech.go | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index c8aa2953b7a7..701cf937374c 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -1,10 +1,8 @@ package tls import ( - "encoding/json" "encoding/pem" "os" - "strings" "github.com/OmarTariq612/goech" "github.com/cloudflare/circl/hpke" @@ -13,13 +11,13 @@ import ( ) var cmdECH = &base.Command{ - UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--json]`, + UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem]`, Short: `Generate TLS-ECH certificates`, Long: ` Generate TLS-ECH certificates. Set serverName to your custom string: {{.Exec}} tls ech --serverName (string) -Generate into json format: {{.Exec}} tls ech --json +Generate into pem format: {{.Exec}} tls ech --pem `, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled } @@ -29,7 +27,7 @@ func init() { var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "") var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "") -var input_json = cmdECH.Flag.Bool("json", false, "True == turn on json output") +var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output") func executeECH(cmd *base.Command, args []string) { var kem hpke.KEM @@ -50,25 +48,16 @@ func executeECH(cmd *base.Command, args []string) { ECHKeySetList[0] = echKeySet configBuffer, _ := ECHConfigList.MarshalBinary() keyBuffer, _ := ECHKeySetList.MarshalBinary() + configStr, _ := ECHConfigList.ToBase64() + keySetStr, _ := ECHKeySetList.ToBase64() configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) - if *input_json { - jECHConfigs := map[string]interface{}{ - "configs": strings.Split(strings.TrimSpace(string(configPEM)), "\n"), - } - jECHKey := map[string]interface{}{ - "key": strings.Split(strings.TrimSpace(string(keyPEM)), "\n"), - } - - for _, i := range []map[string]interface{}{jECHConfigs, jECHKey} { - content, err := json.MarshalIndent(i, "", " ") - common.Must(err) - os.Stdout.Write(content) - os.Stdout.WriteString("\n") - } - } else { + if *input_pem { 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") } } From b21630c38c859fcf42893a145ab81e63763720fc 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: Sun, 2 Mar 2025 13:30:58 +0000 Subject: [PATCH 08/27] bugfix --- transport/internet/tls/ech.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 572db5b3a4c3..20156fde4d1e 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -25,7 +25,7 @@ func ApplyECH(c *Config, config *tls.Config) error { nameToQuery := c.ServerName var DOHServer string - if len(c.EchConfig) != 0 { + if len(c.EchConfig) != 0 || len(c.Ech_DOHserver) != 0 { parts := strings.Split(c.Ech_DOHserver, "+") if len(parts) == 2 { // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" From 2417c281c0c721e1a1527f65c09f3b1acb01dd33 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: Sun, 9 Mar 2025 09:43:40 +0000 Subject: [PATCH 09/27] Add classic UDP DNS support for ECH Config --- infra/conf/transport_internet.go | 4 +- transport/internet/tls/config.go | 2 +- transport/internet/tls/config.pb.go | 10 +-- transport/internet/tls/config.proto | 2 +- transport/internet/tls/ech.go | 130 ++++++++++++++++++---------- 5 files changed, 91 insertions(+), 57 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 6b174b371a02..2da0bd9525df 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -413,7 +413,7 @@ type TLSConfig struct { ServerNameToVerify string `json:"serverNameToVerify"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHConfig string `json:"echConfig"` - ECHDOHServer string `json:"echDohServer"` + ECHDNSServer string `json:"echDnsServer"` EchKeySets string `json:"echKeySets"` } @@ -500,7 +500,7 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.EchKeySets = EchPrivateKey } - config.Ech_DOHserver = c.ECHDOHServer + config.Ech_DNSserver = c.ECHDNSServer return config, nil } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 945fa311cb0f..4bbb510b4d3b 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { config.KeyLogWriter = writer } } - if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 || len(c.EchKeySets) > 0 { + if len(c.EchConfig) > 0 || len(c.Ech_DNSserver) > 0 || len(c.EchKeySets) > 0 { err := ApplyECH(c, config) if err != nil { errors.LogError(context.Background(), err) diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index ef4834517924..e8d218a4cc98 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -218,7 +218,7 @@ type Config struct { // @Critical VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` EchConfig []byte `protobuf:"bytes,18,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` - Ech_DOHserver string `protobuf:"bytes,19,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"` + Ech_DNSserver string `protobuf:"bytes,19,opt,name=ech_DNSserver,json=echDNSserver,proto3" json:"ech_DNSserver,omitempty"` EchKeySets []byte `protobuf:"bytes,20,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"` } @@ -371,9 +371,9 @@ func (x *Config) GetEchConfig() []byte { return nil } -func (x *Config) GetEch_DOHserver() string { +func (x *Config) GetEch_DNSserver() string { if x != nil { - return x.Ech_DOHserver + return x.Ech_DNSserver } return "" } @@ -468,9 +468,9 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4f, 0x48, 0x73, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4e, 0x53, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, - 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x63, 0x68, + 0x44, 0x4e, 0x53, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 09b939995111..df3bcb05c97c 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -94,7 +94,7 @@ message Config { bytes ech_config = 18; - string ech_DOHserver = 19; + string ech_DNSserver = 19; bytes ech_key_sets = 20; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 20156fde4d1e..cbe13042da28 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -25,8 +25,8 @@ func ApplyECH(c *Config, config *tls.Config) error { nameToQuery := c.ServerName var DOHServer string - if len(c.EchConfig) != 0 || len(c.Ech_DOHserver) != 0 { - parts := strings.Split(c.Ech_DOHserver, "+") + if len(c.EchConfig) != 0 || len(c.Ech_DNSserver) != 0 { + parts := strings.Split(c.Ech_DNSserver, "+") if len(parts) == 2 { // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" nameToQuery = parts[0] @@ -35,7 +35,7 @@ func ApplyECH(c *Config, config *tls.Config) error { // normal format DOHServer = parts[0] } else { - return errors.New("Invalid ECH DOH server format: ", c.Ech_DOHserver) + return errors.New("Invalid ECH DOH server format: ", c.Ech_DNSserver) } if len(c.EchConfig) > 0 { @@ -133,55 +133,89 @@ func QueryRecord(domain string, server string) ([]byte, error) { // return ECH config, TTL and error func dohQuery(server string, domain string) ([]byte, uint32, error) { m := new(dns.Msg) + var dnsResolve []byte m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) - // always 0 in DOH - m.Id = 0 - msg, err := m.Pack() - if err != nil { - return []byte{}, 0, err - } - // All traffic sent by core should via xray's internet.DialSystem - // This involves the behavior of some Android VPN GUI clients - tr := &http.Transport{ - IdleConnTimeout: 90 * time.Second, - ForceAttemptHTTP2: true, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - dest, err := net.ParseDestination(network + ":" + addr) - if err != nil { - return nil, err - } - conn, err := internet.DialSystem(ctx, dest, nil) - if err != nil { - return nil, err - } - return conn, nil - }, - } - client := &http.Client{ - Timeout: 5 * time.Second, - Transport: tr, - } - req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) - if err != nil { - return []byte{}, 0, err - } - req.Header.Set("Content-Type", "application/dns-message") - resp, err := client.Do(req) - if err != nil { - return []byte{}, 0, err - } - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return []byte{}, 0, err - } - if resp.StatusCode != http.StatusOK { - return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode) + // for DOH server + if strings.HasPrefix(server, "https://") { + // always 0 in DOH + m.Id = 0 + msg, err := m.Pack() + if err != nil { + return []byte{}, 0, err + } + // All traffic sent by core should via xray's internet.DialSystem + // This involves the behavior of some Android VPN GUI clients + tr := &http.Transport{ + IdleConnTimeout: 90 * time.Second, + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, err + } + conn, err := internet.DialSystem(ctx, dest, nil) + if err != nil { + return nil, err + } + return conn, nil + }, + } + client := &http.Client{ + Timeout: 5 * time.Second, + Transport: tr, + } + req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) + if err != nil { + return []byte{}, 0, err + } + req.Header.Set("Content-Type", "application/dns-message") + resp, err := client.Do(req) + if err != nil { + return []byte{}, 0, err + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return []byte{}, 0, err + } + if resp.StatusCode != http.StatusOK { + return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode) + } + dnsResolve = respBody + } else if strings.HasPrefix(server, "udp://") { // for classic udp dns server + udpServerAddr := server[len("udp://"):] + // default port 53 if not specified + if !strings.Contains(udpServerAddr, ":") { + udpServerAddr = udpServerAddr + ":53" + } + dest, err := net.ParseDestination("udp" + ":" + udpServerAddr) + if err != nil { + return nil, 0, errors.New("failed to parse udp dns server ", udpServerAddr, " for ECH: ", err) + } + dnsTimeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // use xray's internet.DialSystem as mentioned above + conn, err := internet.DialSystem(dnsTimeoutCtx, dest, nil) + defer conn.Close() + if err != nil { + return []byte{}, 0, err + } + msg, err := m.Pack() + if err != nil { + return []byte{}, 0, err + } + conn.Write(msg) + udpResponse := make([]byte, 512) + _, err = conn.Read(udpResponse) + if err != nil { + return []byte{}, 0, err + } + dnsResolve = udpResponse } respMsg := new(dns.Msg) - err = respMsg.Unpack(respBody) + err := respMsg.Unpack(dnsResolve) if err != nil { - return []byte{}, 0, err + return []byte{}, 0, errors.New("failed to unpack dns response for ECH: ", err) } if len(respMsg.Answer) > 0 { for _, answer := range respMsg.Answer { From ff4b10e802a0386872fe1d3a3ccf30387cddd2c3 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: Sun, 9 Mar 2025 11:40:10 +0000 Subject: [PATCH 10/27] Do some rename --- infra/conf/transport_internet.go | 13 ++------ transport/internet/tls/config.go | 2 +- transport/internet/tls/config.pb.go | 46 +++++++++++------------------ transport/internet/tls/config.proto | 6 ++-- transport/internet/tls/ech.go | 42 +++++++++++++++----------- 5 files changed, 48 insertions(+), 61 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 2da0bd9525df..592d6508af5d 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -412,8 +412,7 @@ type TLSConfig struct { MasterKeyLog string `json:"masterKeyLog"` ServerNameToVerify string `json:"serverNameToVerify"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` - ECHConfig string `json:"echConfig"` - ECHDNSServer string `json:"echDnsServer"` + ECHConfigList string `json:"echConfigList"` EchKeySets string `json:"echKeySets"` } @@ -486,13 +485,8 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.VerifyPeerCertInNames = c.VerifyPeerCertInNames - if c.ECHConfig != "" { - ECHConfig, err := base64.StdEncoding.DecodeString(c.ECHConfig) - if err != nil { - return nil, errors.New("invalid ECH Config", c.ECHConfig) - } - config.EchConfig = ECHConfig - } + config.EchConfigList = c.ECHConfigList + if c.EchKeySets != "" { EchPrivateKey, err := base64.StdEncoding.DecodeString(c.EchKeySets) if err != nil { @@ -500,7 +494,6 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.EchKeySets = EchPrivateKey } - config.Ech_DNSserver = c.ECHDNSServer return config, nil } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 4bbb510b4d3b..65f853bcd19b 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { config.KeyLogWriter = writer } } - if len(c.EchConfig) > 0 || len(c.Ech_DNSserver) > 0 || len(c.EchKeySets) > 0 { + if len(c.EchConfigList) > 0 || len(c.EchKeySets) > 0 { err := ApplyECH(c, config) if err != nil { errors.LogError(context.Background(), err) diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index e8d218a4cc98..064c795a890c 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -217,9 +217,8 @@ type Config struct { // @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried. // @Critical VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` - EchConfig []byte `protobuf:"bytes,18,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` - Ech_DNSserver string `protobuf:"bytes,19,opt,name=ech_DNSserver,json=echDNSserver,proto3" json:"ech_DNSserver,omitempty"` - EchKeySets []byte `protobuf:"bytes,20,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"` + EchConfigList string `protobuf:"bytes,18,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` + EchKeySets []byte `protobuf:"bytes,19,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"` } func (x *Config) Reset() { @@ -364,16 +363,9 @@ func (x *Config) GetVerifyPeerCertInNames() []string { return nil } -func (x *Config) GetEchConfig() []byte { +func (x *Config) GetEchConfigList() string { if x != nil { - return x.EchConfig - } - return nil -} - -func (x *Config) GetEch_DNSserver() string { - if x != nil { - return x.Ech_DNSserver + return x.EchConfigList } return "" } @@ -416,7 +408,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, - 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x80, 0x07, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe4, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, @@ -466,21 +458,19 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4e, 0x53, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, - 0x44, 0x4e, 0x53, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x63, 0x68, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, - 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, - 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, - 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, - 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x20, 0x0a, + 0x0c, 0x65, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x13, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, + 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, + 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index df3bcb05c97c..3e032183db25 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -92,9 +92,7 @@ message Config { */ repeated string verify_peer_cert_in_names = 17; - bytes ech_config = 18; + string ech_config_list = 18; - string ech_DNSserver = 19; - - bytes ech_key_sets = 20; + bytes ech_key_sets = 19; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index cbe13042da28..716bde34fbfa 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/tls" - "fmt" "io" "net/http" "strings" @@ -25,22 +24,28 @@ func ApplyECH(c *Config, config *tls.Config) error { nameToQuery := c.ServerName var DOHServer string - if len(c.EchConfig) != 0 || len(c.Ech_DNSserver) != 0 { - parts := strings.Split(c.Ech_DNSserver, "+") - if len(parts) == 2 { - // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" - nameToQuery = parts[0] - DOHServer = parts[1] - } else if len(parts) == 1 { - // normal format - DOHServer = parts[0] - } else { - return errors.New("Invalid ECH DOH server format: ", c.Ech_DNSserver) - } - - if len(c.EchConfig) > 0 { - ECHConfig = c.EchConfig - } else { // ECH config > DOH lookup + // for client + if len(c.EchConfigList) != 0 { + // direct base64 config + if strings.HasPrefix(c.EchConfigList, "base64") { + Base64ECHConfigList := c.EchConfigList[len("base64://"):] + ECHConfigList, err := goech.ECHConfigListFromBase64(Base64ECHConfigList) + if err != nil { + return errors.New("Failed to unmarshal ECHConfigList: ", err) + } + ECHConfig, _ = ECHConfigList.MarshalBinary() + } else { // query config from dns + parts := strings.Split(c.EchConfigList, "+") + if len(parts) == 2 { + // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" + nameToQuery = parts[0] + DOHServer = parts[1] + } else if len(parts) == 1 { + // normal format + DOHServer = parts[0] + } else { + return errors.New("Invalid ECH DNS server format: ", c.EchConfigList) + } if nameToQuery == "" { return errors.New("Using DOH for ECH needs serverName or use dohServer format example.com+https://1.1.1.1/dns-query") } @@ -53,6 +58,7 @@ func ApplyECH(c *Config, config *tls.Config) error { config.EncryptedClientHelloConfigList = ECHConfig } + // for server if len(c.EchKeySets) != 0 { var keys []tls.EncryptedClientHelloKey KeySets, err := goech.UnmarshalECHKeySetList(c.EchKeySets) @@ -70,8 +76,8 @@ func ApplyECH(c *Config, config *tls.Config) error { PrivateKey: ECHPrivateKey}) } config.EncryptedClientHelloKeys = keys - fmt.Println(config.EncryptedClientHelloKeys) } + return nil } From f099e2611d24c2075f12532f555de2d6bff66f3d 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: Sun, 9 Mar 2025 13:30:15 +0000 Subject: [PATCH 11/27] Missing rename --- transport/internet/tls/ech.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 716bde34fbfa..498199ff0ac3 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -22,7 +22,7 @@ func ApplyECH(c *Config, config *tls.Config) error { var err error nameToQuery := c.ServerName - var DOHServer string + var DNSServer string // for client if len(c.EchConfigList) != 0 { @@ -37,19 +37,19 @@ func ApplyECH(c *Config, config *tls.Config) error { } else { // query config from dns parts := strings.Split(c.EchConfigList, "+") if len(parts) == 2 { - // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" + // parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query" nameToQuery = parts[0] - DOHServer = parts[1] + DNSServer = parts[1] } else if len(parts) == 1 { // normal format - DOHServer = parts[0] + DNSServer = parts[0] } else { return errors.New("Invalid ECH DNS server format: ", c.EchConfigList) } if nameToQuery == "" { - return errors.New("Using DOH for ECH needs serverName or use dohServer format example.com+https://1.1.1.1/dns-query") + return errors.New("Using DNS for ECH Config needs serverName or use Server format example.com+https://1.1.1.1/dns-query") } - ECHConfig, err = QueryRecord(nameToQuery, DOHServer) + ECHConfig, err = QueryRecord(nameToQuery, DNSServer) if err != nil { return err } @@ -94,7 +94,7 @@ var ( ) // QueryRecord returns the ECH config for given domain. -// If the record is not in cache or expired, it will query the DOH server and update the cache. +// If the record is not in cache or expired, it will query the DNS server and update the cache. func QueryRecord(domain string, server string) ([]byte, error) { val, found := dnsCache.Load(domain) rec, _ := val.(record) @@ -114,9 +114,9 @@ func QueryRecord(domain string, server string) ([]byte, error) { return rec.echConfig, nil } - // Query ECH config from DOH server + // Query ECH config from DNS server errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) - echConfig, ttl, err := dohQuery(server, domain) + echConfig, ttl, err := dnsQuery(server, domain) if err != nil { return []byte{}, err } @@ -135,9 +135,9 @@ func QueryRecord(domain string, server string) ([]byte, error) { return echConfig, nil } -// dohQuery is the real func for sending type65 query for given domain to given DOH server. +// dnsQuery is the real func for sending type65 query for given domain to given DNS server. // return ECH config, TTL and error -func dohQuery(server string, domain string) ([]byte, uint32, error) { +func dnsQuery(server string, domain string) ([]byte, uint32, error) { m := new(dns.Msg) var dnsResolve []byte m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) From afdcad533c166edd8fb308312b39d9daaccf00cd 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, 10 May 2025 09:21:36 +0000 Subject: [PATCH 12/27] Update utls and little change --- transport/internet/tls/ech.go | 16 ++++++++-------- transport/internet/tls/tls.go | 13 +++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 498199ff0ac3..4f2a597e2f43 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -27,14 +27,8 @@ func ApplyECH(c *Config, config *tls.Config) error { // for client if len(c.EchConfigList) != 0 { // direct base64 config - if strings.HasPrefix(c.EchConfigList, "base64") { - Base64ECHConfigList := c.EchConfigList[len("base64://"):] - ECHConfigList, err := goech.ECHConfigListFromBase64(Base64ECHConfigList) - if err != nil { - return errors.New("Failed to unmarshal ECHConfigList: ", err) - } - ECHConfig, _ = ECHConfigList.MarshalBinary() - } else { // query config from dns + if strings.Contains(c.EchConfigList, "://") { + // query config from dns parts := strings.Split(c.EchConfigList, "+") if len(parts) == 2 { // parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query" @@ -53,6 +47,12 @@ func ApplyECH(c *Config, config *tls.Config) error { if err != nil { return err } + } else { + ECHConfigList, err := goech.ECHConfigListFromBase64(c.EchConfigList) + if err != nil { + return errors.New("Failed to unmarshal ECHConfigList: ", err) + } + ECHConfig, _ = ECHConfigList.MarshalBinary() } config.EncryptedClientHelloConfigList = ECHConfig diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index 28c7bf63e603..20b9716dfee4 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -128,12 +128,13 @@ func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) ne func copyConfig(c *tls.Config) *utls.Config { return &utls.Config{ - Rand: c.Rand, - RootCAs: c.RootCAs, - ServerName: c.ServerName, - InsecureSkipVerify: c.InsecureSkipVerify, - VerifyPeerCertificate: c.VerifyPeerCertificate, - KeyLogWriter: c.KeyLogWriter, + Rand: c.Rand, + RootCAs: c.RootCAs, + ServerName: c.ServerName, + InsecureSkipVerify: c.InsecureSkipVerify, + VerifyPeerCertificate: c.VerifyPeerCertificate, + KeyLogWriter: c.KeyLogWriter, + EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList, } } From 7d516ee81fcd1f96673cc2fae5853b69166e5bdc 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 13/27] Change dependence to reality Co-authored-by: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> --- go.mod | 9 ++- go.sum | 14 ++-- main/commands/all/tls/ech.go | 51 ++++++++------ transport/internet/tls/ech.go | 127 +++++++++++++++++++++++++++++----- 4 files changed, 149 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index a58c1e78aedb..eeb9127941b1 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-20250725142056-5b52a03d4fb7 + github.com/xtls/reality v0.0.0-20250527000105-e679ef7bb130 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.40.0 golang.org/x/net v0.42.0 golang.org/x/sync v0.16.0 golang.org/x/sys v0.34.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.74.2 + 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 @@ -35,7 +33,8 @@ require ( ) require ( - github.com/andybalholm/brotli v1.0.6 // indirect + 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-20200812162917-85c65e2d0165 // indirect github.com/google/btree v1.1.2 // indirect diff --git a/go.sum b/go.sum index 682295e277fd..1cc11a3b2a5f 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= @@ -77,8 +75,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-20250725142056-5b52a03d4fb7 h1:Ript0vN+nSO33+Vj4n0mgNY5M+oOxFQJdrJ1VnwTBO0= -github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= +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= @@ -141,10 +139,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-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index 701cf937374c..4ca1411c5d0a 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() - - configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) - keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) + 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() + 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 4f2a597e2f43..94a9225b672e 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? +} From e2fe7fe59d4fd2f20e770e3ddfe210aebe4beae6 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 15:39:42 +0000 Subject: [PATCH 14/27] Allow concurrent DNS queries --- transport/internet/tls/ech.go | 96 +++++++++++++++++------------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 94a9225b672e..e624fc9bddd6 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -34,7 +34,7 @@ func ApplyECH(c *Config, config *tls.Config) error { if len(c.EchConfigList) != 0 { // direct base64 config if strings.Contains(c.EchConfigList, "://") { - // query config from dns + // query config from dns parts := strings.Split(c.EchConfigList, "+") if len(parts) == 2 { // parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query" @@ -71,62 +71,62 @@ func ApplyECH(c *Config, config *tls.Config) error { } config.EncryptedClientHelloKeys = KeySets } - + return nil } -type record struct { - echConfig []byte - expire time.Time +type ECHConfigCache struct { + echConfig []byte + expire time.Time + updateLock sync.Mutex } var ( - dnsCache sync.Map - // global Lock? I'm not sure if this needs finer grained locks. - // If we do this, we will need to nest another layer of struct - updating sync.Mutex + GlobalECHConfigCache map[string]*ECHConfigCache + GlobalECHConfigCacheAccess sync.Mutex ) // QueryRecord returns the ECH config for given domain. // If the record is not in cache or expired, it will query the DNS server and update the cache. func QueryRecord(domain string, server string) ([]byte, error) { - val, found := dnsCache.Load(domain) - rec, _ := val.(record) - if found && rec.expire.After(time.Now()) { - errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) - return rec.echConfig, nil + // Global cache init + GlobalECHConfigCacheAccess.Lock() + if GlobalECHConfigCache == nil { + GlobalECHConfigCache = make(map[string]*ECHConfigCache) } - updating.Lock() - defer updating.Unlock() - // Try to get cache again after lock, in case another goroutine has updated it - // This might happen when the core tring is just stared and multiple goroutines are trying to query the same domain - val, found = dnsCache.Load(domain) - rec, _ = val.(record) - if found && rec.expire.After(time.Now()) { - errors.LogDebug(context.Background(), "ECH Config cache hit for domain: ", domain, " after trying to get update lock") - return rec.echConfig, nil + echConfigCache := GlobalECHConfigCache[domain] + if echConfigCache != nil && echConfigCache.expire.After(time.Now()) { + errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) + GlobalECHConfigCacheAccess.Unlock() + return echConfigCache.echConfig, nil } + if echConfigCache == nil { + echConfigCache = &ECHConfigCache{} + GlobalECHConfigCache[domain] = echConfigCache + } + GlobalECHConfigCacheAccess.Unlock() + echConfigCache.updateLock.Lock() + defer echConfigCache.updateLock.Unlock() + // Double check cache after acquiring lock + if echConfigCache.expire.After(time.Now()) { + errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) + return echConfigCache.echConfig, nil + } // Query ECH config from DNS server errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) echConfig, ttl, err := dnsQuery(server, domain) if err != nil { - return []byte{}, err + return nil, err } - // Set minimum TTL to 600 seconds if ttl < 600 { ttl = 600 } - - // Update cache - newRecored := record{ - echConfig: echConfig, - expire: time.Now().Add(time.Second * time.Duration(ttl)), - } - dnsCache.Store(domain, newRecored) - return echConfig, nil + echConfigCache.echConfig = echConfig + echConfigCache.expire = time.Now().Add(time.Second * time.Duration(ttl)) + return echConfigCache.echConfig, nil } // dnsQuery is the real func for sending type65 query for given domain to given DNS server. @@ -301,10 +301,10 @@ 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, + 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}, @@ -316,19 +316,19 @@ func GenerateECHKeySet(configID uint8, domain string, kem uint16) (reality.EchCo {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_256_GCM}, {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_ChaCha20Poly1305}, }, - MaxNameLength: 0, - Extensions: nil, + 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 + 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? } From 442f6b723905f9115817c36be829c0c7a04a9ad7 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: Mon, 30 Jun 2025 14:16:01 +0000 Subject: [PATCH 15/27] Use old value and update in other goroutine --- transport/internet/tls/ech.go | 53 ++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index e624fc9bddd6..8327f359218b 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -12,6 +12,7 @@ import ( "net/http" "strings" "sync" + "sync/atomic" "time" "github.com/miekg/dns" @@ -76,11 +77,31 @@ func ApplyECH(c *Config, config *tls.Config) error { } type ECHConfigCache struct { - echConfig []byte - expire time.Time + echConfig atomic.Pointer[[]byte] + expire *time.Time updateLock sync.Mutex } +func (c *ECHConfigCache) update(domain string, server string) ([]byte, error) { + c.updateLock.Lock() + defer c.updateLock.Unlock() + // Double check cache after acquiring lock + if c.expire.After(time.Now()) { + errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) + return *c.echConfig.Load(), nil + } + // Query ECH config from DNS server + errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) + echConfig, ttl, err := dnsQuery(server, domain) + if err != nil { + return nil, err + } + c.echConfig.Store(&echConfig) + expire := time.Now().Add(time.Duration(ttl) * time.Second) + c.expire = &expire + return *c.echConfig.Load(), nil +} + var ( GlobalECHConfigCache map[string]*ECHConfigCache GlobalECHConfigCacheAccess sync.Mutex @@ -99,7 +120,7 @@ func QueryRecord(domain string, server string) ([]byte, error) { if echConfigCache != nil && echConfigCache.expire.After(time.Now()) { errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) GlobalECHConfigCacheAccess.Unlock() - return echConfigCache.echConfig, nil + return *echConfigCache.echConfig.Load(), nil } if echConfigCache == nil { echConfigCache = &ECHConfigCache{} @@ -107,26 +128,14 @@ func QueryRecord(domain string, server string) ([]byte, error) { } GlobalECHConfigCacheAccess.Unlock() - echConfigCache.updateLock.Lock() - defer echConfigCache.updateLock.Unlock() - // Double check cache after acquiring lock - if echConfigCache.expire.After(time.Now()) { - errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) - return echConfigCache.echConfig, nil - } - // Query ECH config from DNS server - errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) - echConfig, ttl, err := dnsQuery(server, domain) - if err != nil { - return nil, err - } - // Set minimum TTL to 600 seconds - if ttl < 600 { - ttl = 600 + // If expire is nil, it means we are in initial state, wait for the query to finish + // otherwise return old value immediately and update in a goroutine + if echConfigCache.expire == nil { + return echConfigCache.update(domain, server) + } else { + go echConfigCache.update(domain, server) + return *echConfigCache.echConfig.Load(), nil } - echConfigCache.echConfig = echConfig - echConfigCache.expire = time.Now().Add(time.Second * time.Duration(ttl)) - return echConfigCache.echConfig, nil } // dnsQuery is the real func for sending type65 query for given domain to given DNS server. From d6f1ec481ef33a89222da9339781ab60e4c3705c 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: Mon, 30 Jun 2025 18:09:58 +0000 Subject: [PATCH 16/27] Reduce meaningless goroutines --- transport/internet/tls/ech.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 8327f359218b..dedf9d055722 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -133,7 +133,10 @@ func QueryRecord(domain string, server string) ([]byte, error) { if echConfigCache.expire == nil { return echConfigCache.update(domain, server) } else { - go echConfigCache.update(domain, server) + // If someone already acquired the lock, it means it is updating, do not start another update goroutine + if echConfigCache.updateLock.TryLock() { + go echConfigCache.update(domain, server) + } return *echConfigCache.echConfig.Load(), nil } } From efd1538a2901ff6e50bcc42f97ee6af7ab957fd8 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: Mon, 30 Jun 2025 18:26:32 +0000 Subject: [PATCH 17/27] Fix nil panic --- transport/internet/tls/ech.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index dedf9d055722..03a78dd829ce 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -77,8 +77,9 @@ func ApplyECH(c *Config, config *tls.Config) error { } type ECHConfigCache struct { - echConfig atomic.Pointer[[]byte] - expire *time.Time + echConfig atomic.Pointer[[]byte] + expire atomic.Pointer[time.Time] + // updateLock is not for preventing concurrent read/write, but for preventing concurrent update updateLock sync.Mutex } @@ -86,7 +87,7 @@ func (c *ECHConfigCache) update(domain string, server string) ([]byte, error) { c.updateLock.Lock() defer c.updateLock.Unlock() // Double check cache after acquiring lock - if c.expire.After(time.Now()) { + if c.expire.Load().After(time.Now()) { errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) return *c.echConfig.Load(), nil } @@ -98,7 +99,7 @@ func (c *ECHConfigCache) update(domain string, server string) ([]byte, error) { } c.echConfig.Store(&echConfig) expire := time.Now().Add(time.Duration(ttl) * time.Second) - c.expire = &expire + c.expire.Store(&expire) return *c.echConfig.Load(), nil } @@ -117,20 +118,21 @@ func QueryRecord(domain string, server string) ([]byte, error) { } echConfigCache := GlobalECHConfigCache[domain] - if echConfigCache != nil && echConfigCache.expire.After(time.Now()) { - errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) - GlobalECHConfigCacheAccess.Unlock() - return *echConfigCache.echConfig.Load(), nil - } if echConfigCache == nil { echConfigCache = &ECHConfigCache{} + echConfigCache.expire.Store(&time.Time{}) // zero value means initial state GlobalECHConfigCache[domain] = echConfigCache } + if echConfigCache != nil && echConfigCache.expire.Load().After(time.Now()) { + errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) + GlobalECHConfigCacheAccess.Unlock() + return *echConfigCache.echConfig.Load(), nil + } GlobalECHConfigCacheAccess.Unlock() - // If expire is nil, it means we are in initial state, wait for the query to finish + // If expire is zero value, it means we are in initial state, wait for the query to finish // otherwise return old value immediately and update in a goroutine - if echConfigCache.expire == nil { + if *echConfigCache.expire.Load() == (time.Time{}) { return echConfigCache.update(domain, server) } else { // If someone already acquired the lock, it means it is updating, do not start another update goroutine From a15f47d5d08ce57ba635cb6548c10f7bf5b131b1 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: Mon, 30 Jun 2025 18:48:55 +0000 Subject: [PATCH 18/27] Add test --- transport/internet/tls/ech_test.go | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 transport/internet/tls/ech_test.go diff --git a/transport/internet/tls/ech_test.go b/transport/internet/tls/ech_test.go new file mode 100644 index 000000000000..f0bb3c566122 --- /dev/null +++ b/transport/internet/tls/ech_test.go @@ -0,0 +1,43 @@ +package tls_test + +import ( + "io" + "net/http" + "strings" + "sync" + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/transport/internet/tls" +) + +func TestECHDial(t *testing.T) { + config := &Config{ + ServerName: "encryptedsni.com", + EchConfigList: "udp://1.1.1.1", + } + // test concurrent Dial(to test cache problem) + wg := sync.WaitGroup{} + for range 10 { + wg.Add(1) + go func() { + TLSConfig := config.GetTLSConfig() + TLSConfig.NextProtos = []string{"http/1.1"} + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: TLSConfig, + }, + } + resp, err := client.Get("https://encryptedsni.com/cdn-cgi/trace") + common.Must(err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + common.Must(err) + if !strings.Contains(string(body), "sni=encrypted") { + t.Error("ECH Dial success but SNI is not encrypted") + } + wg.Done() + }() + } + wg.Wait() +} From 90e2efc4ed3759f25f9db95863551b1f9c2d6726 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: Fri, 11 Jul 2025 17:14:01 +0000 Subject: [PATCH 19/27] sync --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index eeb9127941b1..6d620b9a7572 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/xtls/xray-core go 1.24 require ( + 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 @@ -18,14 +19,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-20250527000105-e679ef7bb130 + github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.40.0 golang.org/x/net v0.42.0 golang.org/x/sync v0.16.0 golang.org/x/sys v0.34.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.72.1 + google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.6 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 @@ -33,8 +34,7 @@ require ( ) require ( - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 // indirect github.com/google/btree v1.1.2 // indirect diff --git a/go.sum b/go.sum index 1cc11a3b2a5f..181db0314327 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -75,8 +75,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-20250527000105-e679ef7bb130 h1:v/TVypWnLferyoaNHh6a8oyggj9APBUzfl1OOgXNbpw= -github.com/xtls/reality v0.0.0-20250527000105-e679ef7bb130/go.mod h1:bJdU3ExzfUlY40Xxfibq3THW9IHiE8mHu/tEzud5JWM= +github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7 h1:Ript0vN+nSO33+Vj4n0mgNY5M+oOxFQJdrJ1VnwTBO0= +github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= 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= @@ -139,10 +139,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-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/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From cb72a79f5b0cfd8e8127c4f3089cae3fb5bff1d5 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, 26 Jul 2025 04:58:41 +0000 Subject: [PATCH 20/27] Rename --- infra/conf/transport_internet.go | 10 +++++----- transport/internet/tls/config.go | 2 +- transport/internet/tls/config.pb.go | 30 ++++++++++++++--------------- transport/internet/tls/config.proto | 2 +- transport/internet/tls/ech.go | 4 ++-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 592d6508af5d..9fbe22413bfc 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -413,7 +413,7 @@ type TLSConfig struct { ServerNameToVerify string `json:"serverNameToVerify"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHConfigList string `json:"echConfigList"` - EchKeySets string `json:"echKeySets"` + ECHSeverKeys string `json:"echSeverKeys"` } // Build implements Buildable. @@ -487,12 +487,12 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.EchConfigList = c.ECHConfigList - if c.EchKeySets != "" { - EchPrivateKey, err := base64.StdEncoding.DecodeString(c.EchKeySets) + if c.ECHSeverKeys != "" { + EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHSeverKeys) if err != nil { - return nil, errors.New("invalid ECH Config", c.EchKeySets) + return nil, errors.New("invalid ECH Config", c.ECHSeverKeys) } - config.EchKeySets = EchPrivateKey + config.EchSeverKeys = EchPrivateKey } return config, nil diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 65f853bcd19b..68e7f2e3e70b 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { config.KeyLogWriter = writer } } - if len(c.EchConfigList) > 0 || len(c.EchKeySets) > 0 { + if len(c.EchConfigList) > 0 || len(c.EchSeverKeys) > 0 { err := ApplyECH(c, config) if err != nil { errors.LogError(context.Background(), err) diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 064c795a890c..9e4187312451 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -218,7 +218,7 @@ type Config struct { // @Critical VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` EchConfigList string `protobuf:"bytes,18,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` - EchKeySets []byte `protobuf:"bytes,19,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"` + EchSeverKeys []byte `protobuf:"bytes,19,opt,name=ech_sever_keys,json=echSeverKeys,proto3" json:"ech_sever_keys,omitempty"` } func (x *Config) Reset() { @@ -370,9 +370,9 @@ func (x *Config) GetEchConfigList() string { return "" } -func (x *Config) GetEchKeySets() []byte { +func (x *Config) GetEchSeverKeys() []byte { if x != nil { - return x.EchKeySets + return x.EchSeverKeys } return nil } @@ -408,7 +408,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, - 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe4, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe8, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, @@ -460,17 +460,17 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x20, 0x0a, - 0x0c, 0x65, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x13, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, - 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, - 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, - 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x24, 0x0a, + 0x0e, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x63, 0x68, 0x53, 0x65, 0x76, 0x65, 0x72, 0x4b, + 0x65, 0x79, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, + 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, + 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 3e032183db25..629ff927841a 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -94,5 +94,5 @@ message Config { string ech_config_list = 18; - bytes ech_key_sets = 19; + bytes ech_sever_keys = 19; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 03a78dd829ce..93cce47617f0 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -65,8 +65,8 @@ func ApplyECH(c *Config, config *tls.Config) error { } // for server - if len(c.EchKeySets) != 0 { - KeySets, err := ConvertToGoECHKeys(c.EchKeySets) + if len(c.EchSeverKeys) != 0 { + KeySets, err := ConvertToGoECHKeys(c.EchSeverKeys) if err != nil { return errors.New("Failed to unmarshal ECHKeySetList: ", err) } From a3e347b27ef88ed0c95928c48f7b01c7fe84df0a 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, 26 Jul 2025 05:18:27 +0000 Subject: [PATCH 21/27] Restore ECHConfig from keysets --- main/commands/all/tls/ech.go | 57 ++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index 4ca1411c5d0a..a5dc8b2b74f6 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -13,13 +13,14 @@ import ( ) var cmdECH = &base.Command{ - UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem]`, + UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem] [-i "ECHSeverKeys (base64.StdEncoding)"]`, Short: `Generate TLS-ECH certificates`, Long: ` Generate TLS-ECH certificates. Set serverName to your custom string: {{.Exec}} tls ech --serverName (string) Generate into pem format: {{.Exec}} tls ech --pem +Restore ECHConfigs from ECHSeverKeys: {{.Exec}} tls ech -i "ECHSeverKeys (base64.StdEncoding)" `, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled } @@ -27,7 +28,9 @@ func init() { cmdECH.Run = executeECH } -var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "") +var input_echSeverKeys = cmdECH.Flag.String("i", "", "ECHSeverKeys (base64.StdEncoding)") + +// var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "") var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "") var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output") @@ -37,24 +40,46 @@ func executeECH(cmd *base.Command, args []string) { // if *input_pqSignatureSchemesEnabled { // kem = 0x30 // hpke.KEM_X25519_KYBER768_DRAFT00 // } else { - kem = hpke.DHKEM_X25519_HKDF_SHA256 + kem = hpke.DHKEM_X25519_HKDF_SHA256 // } - echKeySet, priv, err := tls.GenerateECHKeySet(0, *input_serverName, kem) + echConfig, priv, err := tls.GenerateECHKeySet(0, *input_serverName, kem) common.Must(err) - 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() + var configBuffer, keyBuffer []byte + if *input_echSeverKeys == "" { + configBytes, _ := tls.MarshalBinary(echConfig) + 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() + } else { + keySetsByte, err := base64.StdEncoding.DecodeString(*input_echSeverKeys) + if err != nil { + os.Stdout.WriteString("Failed to decode ECHSeverKeys: " + err.Error() + "\n") + return + } + keyBuffer = keySetsByte + KeySets, err := tls.ConvertToGoECHKeys(keySetsByte) + if err != nil { + os.Stdout.WriteString("Failed to decode ECHSeverKeys: " + err.Error() + "\n") + return + } + var b cryptobyte.Builder + for _, keySet := range KeySets { + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes(keySet.Config) + }) + } + configBuffer, _ = b.Bytes() + } if *input_pem { configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) From af41ee4919ddb8a89b3666a551b79eb0832443e9 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, 26 Jul 2025 06:02:50 +0000 Subject: [PATCH 22/27] rename --- main/commands/all/tls/ech.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index a5dc8b2b74f6..06fb74b6c0b5 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -88,6 +88,6 @@ func executeECH(cmd *base.Command, args []string) { os.Stdout.WriteString(keyPEM) } else { os.Stdout.WriteString("ECH config list: \n" + base64.StdEncoding.EncodeToString(configBuffer) + "\n") - os.Stdout.WriteString("ECH Key sets: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n") + os.Stdout.WriteString("ECH server keys: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n") } } From bc8ba765b03606aee19b9ff2ffb30f3081611ffb 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, 26 Jul 2025 06:22:42 +0000 Subject: [PATCH 23/27] Bug fix and refine --- transport/internet/tls/ech.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 93cce47617f0..39ee26a945fe 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -80,12 +80,17 @@ type ECHConfigCache struct { echConfig atomic.Pointer[[]byte] expire atomic.Pointer[time.Time] // updateLock is not for preventing concurrent read/write, but for preventing concurrent update - updateLock sync.Mutex + UpdateLock sync.Mutex } -func (c *ECHConfigCache) update(domain string, server string) ([]byte, error) { - c.updateLock.Lock() - defer c.updateLock.Unlock() +// Update updates the ECH config for given domain and server. +// this method is concurrent safe, only one update request will be sent, others get the cache. +// if isLockedUpdate is true, it means pass the lock to this function, it will release the lock after update. +func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool) ([]byte, error) { + if !isLockedUpdate { + c.UpdateLock.Lock() + } + defer c.UpdateLock.Unlock() // Double check cache after acquiring lock if c.expire.Load().After(time.Now()) { errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) @@ -132,12 +137,13 @@ func QueryRecord(domain string, server string) ([]byte, error) { // If expire is zero value, it means we are in initial state, wait for the query to finish // otherwise return old value immediately and update in a goroutine - if *echConfigCache.expire.Load() == (time.Time{}) { - return echConfigCache.update(domain, server) + // but if the cache is too old, wait for update + if *echConfigCache.expire.Load() == (time.Time{}) || echConfigCache.expire.Load().Add(time.Hour*6).Before(time.Now()) { + return echConfigCache.Update(domain, server, false) } else { // If someone already acquired the lock, it means it is updating, do not start another update goroutine - if echConfigCache.updateLock.TryLock() { - go echConfigCache.update(domain, server) + if echConfigCache.UpdateLock.TryLock() { + go echConfigCache.Update(domain, server, true) } return *echConfigCache.echConfig.Load(), nil } @@ -210,7 +216,12 @@ func dnsQuery(server string, domain string) ([]byte, uint32, error) { defer cancel() // use xray's internet.DialSystem as mentioned above conn, err := internet.DialSystem(dnsTimeoutCtx, dest, nil) - defer conn.Close() + defer func() { + err := conn.Close() + if err != nil { + errors.LogDebug(context.Background(), "Failed to close connection: ", err) + } + }() if err != nil { return []byte{}, 0, err } From db545227652820d1ec4d69621c858da75e0f0527 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, 26 Jul 2025 07:22:21 +0000 Subject: [PATCH 24/27] Remove global lock --- transport/internet/tls/ech.go | 67 ++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 39ee26a945fe..fa0a4da051b0 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -20,6 +20,7 @@ import ( "github.com/xtls/reality/hpke" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/transport/internet" "golang.org/x/crypto/cryptobyte" ) @@ -77,24 +78,31 @@ func ApplyECH(c *Config, config *tls.Config) error { } type ECHConfigCache struct { - echConfig atomic.Pointer[[]byte] - expire atomic.Pointer[time.Time] + configRecord atomic.Pointer[echConfigRecord] // updateLock is not for preventing concurrent read/write, but for preventing concurrent update UpdateLock sync.Mutex } +type echConfigRecord struct { + config []byte + expire time.Time +} + +var GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]() + // Update updates the ECH config for given domain and server. // this method is concurrent safe, only one update request will be sent, others get the cache. -// if isLockedUpdate is true, it means pass the lock to this function, it will release the lock after update. +// if isLockedUpdate is true, it will not try to acquire the lock. func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool) ([]byte, error) { if !isLockedUpdate { - c.UpdateLock.Lock() + c.UpdateLock.Lock() + defer c.UpdateLock.Unlock() } - defer c.UpdateLock.Unlock() // Double check cache after acquiring lock - if c.expire.Load().After(time.Now()) { + configRecord := c.configRecord.Load() + if configRecord.expire.After(time.Now()) { errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) - return *c.echConfig.Load(), nil + return configRecord.config, nil } // Query ECH config from DNS server errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) @@ -102,50 +110,43 @@ func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate boo if err != nil { return nil, err } - c.echConfig.Store(&echConfig) - expire := time.Now().Add(time.Duration(ttl) * time.Second) - c.expire.Store(&expire) - return *c.echConfig.Load(), nil + configRecord = &echConfigRecord{ + config: echConfig, + expire: time.Now().Add(time.Duration(ttl) * time.Second), + } + c.configRecord.Store(configRecord) + return configRecord.config, nil } -var ( - GlobalECHConfigCache map[string]*ECHConfigCache - GlobalECHConfigCacheAccess sync.Mutex -) - // QueryRecord returns the ECH config for given domain. // If the record is not in cache or expired, it will query the DNS server and update the cache. func QueryRecord(domain string, server string) ([]byte, error) { - // Global cache init - GlobalECHConfigCacheAccess.Lock() - if GlobalECHConfigCache == nil { - GlobalECHConfigCache = make(map[string]*ECHConfigCache) - } - - echConfigCache := GlobalECHConfigCache[domain] - if echConfigCache == nil { + echConfigCache, ok := GlobalECHConfigCache.Load(domain) + if !ok { echConfigCache = &ECHConfigCache{} - echConfigCache.expire.Store(&time.Time{}) // zero value means initial state - GlobalECHConfigCache[domain] = echConfigCache + echConfigCache.configRecord.Store(&echConfigRecord{}) + echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(domain, echConfigCache) } - if echConfigCache != nil && echConfigCache.expire.Load().After(time.Now()) { + configRecord := echConfigCache.configRecord.Load() + if configRecord.expire.After(time.Now()) { errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) - GlobalECHConfigCacheAccess.Unlock() - return *echConfigCache.echConfig.Load(), nil + return configRecord.config, nil } - GlobalECHConfigCacheAccess.Unlock() // If expire is zero value, it means we are in initial state, wait for the query to finish // otherwise return old value immediately and update in a goroutine // but if the cache is too old, wait for update - if *echConfigCache.expire.Load() == (time.Time{}) || echConfigCache.expire.Load().Add(time.Hour*6).Before(time.Now()) { + if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*6).Before(time.Now()) { return echConfigCache.Update(domain, server, false) } else { // If someone already acquired the lock, it means it is updating, do not start another update goroutine if echConfigCache.UpdateLock.TryLock() { - go echConfigCache.Update(domain, server, true) + go func() { + defer echConfigCache.UpdateLock.Unlock() + echConfigCache.Update(domain, server, true) + }() } - return *echConfigCache.echConfig.Load(), nil + return configRecord.config, nil } } From 61fcfb8bf132a06a1170a511ab26c8e652e22cdc 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, 26 Jul 2025 07:27:49 +0000 Subject: [PATCH 25/27] typo --- infra/conf/transport_internet.go | 10 +++++----- main/commands/all/tls/ech.go | 14 ++++++------- transport/internet/tls/config.go | 2 +- transport/internet/tls/config.pb.go | 31 +++++++++++++++-------------- transport/internet/tls/config.proto | 2 +- transport/internet/tls/ech.go | 4 ++-- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 9fbe22413bfc..86a3b003ac8d 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -413,7 +413,7 @@ type TLSConfig struct { ServerNameToVerify string `json:"serverNameToVerify"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHConfigList string `json:"echConfigList"` - ECHSeverKeys string `json:"echSeverKeys"` + ECHServerKeys string `json:"echServerKeys"` } // Build implements Buildable. @@ -487,12 +487,12 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.EchConfigList = c.ECHConfigList - if c.ECHSeverKeys != "" { - EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHSeverKeys) + if c.ECHServerKeys != "" { + EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys) if err != nil { - return nil, errors.New("invalid ECH Config", c.ECHSeverKeys) + return nil, errors.New("invalid ECH Config", c.ECHServerKeys) } - config.EchSeverKeys = EchPrivateKey + config.EchServerKeys = EchPrivateKey } return config, nil diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index 06fb74b6c0b5..18ecb602307b 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -13,14 +13,14 @@ import ( ) var cmdECH = &base.Command{ - UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem] [-i "ECHSeverKeys (base64.StdEncoding)"]`, + UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem] [-i "ECHServerKeys (base64.StdEncoding)"]`, Short: `Generate TLS-ECH certificates`, Long: ` Generate TLS-ECH certificates. Set serverName to your custom string: {{.Exec}} tls ech --serverName (string) Generate into pem format: {{.Exec}} tls ech --pem -Restore ECHConfigs from ECHSeverKeys: {{.Exec}} tls ech -i "ECHSeverKeys (base64.StdEncoding)" +Restore ECHConfigs from ECHServerKeys: {{.Exec}} tls ech -i "ECHServerKeys (base64.StdEncoding)" `, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled } @@ -28,7 +28,7 @@ func init() { cmdECH.Run = executeECH } -var input_echSeverKeys = cmdECH.Flag.String("i", "", "ECHSeverKeys (base64.StdEncoding)") +var input_echServerKeys = cmdECH.Flag.String("i", "", "ECHServerKeys (base64.StdEncoding)") // var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "") var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "") @@ -47,7 +47,7 @@ func executeECH(cmd *base.Command, args []string) { common.Must(err) var configBuffer, keyBuffer []byte - if *input_echSeverKeys == "" { + if *input_echServerKeys == "" { configBytes, _ := tls.MarshalBinary(echConfig) var b cryptobyte.Builder b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { @@ -61,15 +61,15 @@ func executeECH(cmd *base.Command, args []string) { b2.AddBytes(configBytes) keyBuffer, _ = b2.Bytes() } else { - keySetsByte, err := base64.StdEncoding.DecodeString(*input_echSeverKeys) + keySetsByte, err := base64.StdEncoding.DecodeString(*input_echServerKeys) if err != nil { - os.Stdout.WriteString("Failed to decode ECHSeverKeys: " + err.Error() + "\n") + os.Stdout.WriteString("Failed to decode ECHServerKeys: " + err.Error() + "\n") return } keyBuffer = keySetsByte KeySets, err := tls.ConvertToGoECHKeys(keySetsByte) if err != nil { - os.Stdout.WriteString("Failed to decode ECHSeverKeys: " + err.Error() + "\n") + os.Stdout.WriteString("Failed to decode ECHServerKeys: " + err.Error() + "\n") return } var b cryptobyte.Builder diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 68e7f2e3e70b..cb030776c9de 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { config.KeyLogWriter = writer } } - if len(c.EchConfigList) > 0 || len(c.EchSeverKeys) > 0 { + if len(c.EchConfigList) > 0 || len(c.EchServerKeys) > 0 { err := ApplyECH(c, config) if err != nil { errors.LogError(context.Background(), err) diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 9e4187312451..fc719c7b057b 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -218,7 +218,7 @@ type Config struct { // @Critical VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` EchConfigList string `protobuf:"bytes,18,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` - EchSeverKeys []byte `protobuf:"bytes,19,opt,name=ech_sever_keys,json=echSeverKeys,proto3" json:"ech_sever_keys,omitempty"` + EchServerKeys []byte `protobuf:"bytes,19,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"` } func (x *Config) Reset() { @@ -370,9 +370,9 @@ func (x *Config) GetEchConfigList() string { return "" } -func (x *Config) GetEchSeverKeys() []byte { +func (x *Config) GetEchServerKeys() []byte { if x != nil { - return x.EchSeverKeys + return x.EchServerKeys } return nil } @@ -408,7 +408,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, - 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe8, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xea, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, @@ -460,17 +460,18 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x24, 0x0a, - 0x0e, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, - 0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x63, 0x68, 0x53, 0x65, 0x76, 0x65, 0x72, 0x4b, - 0x65, 0x79, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, - 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, - 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a, + 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73, + 0x18, 0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x65, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, + 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, + 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 629ff927841a..0e78651fa702 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -94,5 +94,5 @@ message Config { string ech_config_list = 18; - bytes ech_sever_keys = 19; + bytes ech_server_keys = 19; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index fa0a4da051b0..369f8d9932f5 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -66,8 +66,8 @@ func ApplyECH(c *Config, config *tls.Config) error { } // for server - if len(c.EchSeverKeys) != 0 { - KeySets, err := ConvertToGoECHKeys(c.EchSeverKeys) + if len(c.EchServerKeys) != 0 { + KeySets, err := ConvertToGoECHKeys(c.EchServerKeys) if err != nil { return errors.New("Failed to unmarshal ECHKeySetList: ", err) } From 9bd719aa1a0b6bb1bc426cf32d694d3ba83a455c 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, 26 Jul 2025 08:04:37 +0000 Subject: [PATCH 26/27] Fmt --- infra/conf/transport_internet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 86a3b003ac8d..e7da658b5270 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -413,7 +413,7 @@ type TLSConfig struct { ServerNameToVerify string `json:"serverNameToVerify"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHConfigList string `json:"echConfigList"` - ECHServerKeys string `json:"echServerKeys"` + ECHServerKeys string `json:"echServerKeys"` } // Build implements Buildable. From 53cf8ca599c42f3582411effea764e40f0cffc91 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, 26 Jul 2025 08:10:15 +0000 Subject: [PATCH 27/27] Reuse Client --- transport/internet/tls/ech.go | 49 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 369f8d9932f5..982235dbef5b 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -88,7 +88,10 @@ type echConfigRecord struct { expire time.Time } -var GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]() +var ( + GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]() + clientForECHDOH = utils.NewTypedSyncMap[string, *http.Client]() +) // Update updates the ECH config for given domain and server. // this method is concurrent safe, only one update request will be sent, others get the cache. @@ -164,26 +167,30 @@ func dnsQuery(server string, domain string) ([]byte, uint32, error) { if err != nil { return []byte{}, 0, err } - // All traffic sent by core should via xray's internet.DialSystem - // This involves the behavior of some Android VPN GUI clients - tr := &http.Transport{ - IdleConnTimeout: 90 * time.Second, - ForceAttemptHTTP2: true, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - dest, err := net.ParseDestination(network + ":" + addr) - if err != nil { - return nil, err - } - conn, err := internet.DialSystem(ctx, dest, nil) - if err != nil { - return nil, err - } - return conn, nil - }, - } - client := &http.Client{ - Timeout: 5 * time.Second, - Transport: tr, + var client *http.Client + if client, _ = clientForECHDOH.Load(server); client == nil { + // All traffic sent by core should via xray's internet.DialSystem + // This involves the behavior of some Android VPN GUI clients + tr := &http.Transport{ + IdleConnTimeout: 90 * time.Second, + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, err + } + conn, err := internet.DialSystem(ctx, dest, nil) + if err != nil { + return nil, err + } + return conn, nil + }, + } + c := &http.Client{ + Timeout: 5 * time.Second, + Transport: tr, + } + client, _ = clientForECHDOH.LoadOrStore(server, c) } req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) if err != nil {