mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-14 18:09:05 +00:00
TLS ECH: Remove echForceQuery (ECH is forced now if configured) (#6032)
https://github.com/XTLS/Xray-core/pull/5887#issuecomment-4184701517
This commit is contained in:
@@ -469,11 +469,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
|
|||||||
if len(c.EchConfigList) > 0 || len(c.EchServerKeys) > 0 {
|
if len(c.EchConfigList) > 0 || len(c.EchServerKeys) > 0 {
|
||||||
err := ApplyECH(c, config)
|
err := ApplyECH(c, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.EchForceQuery == "full" {
|
|
||||||
errors.LogError(context.Background(), err)
|
errors.LogError(context.Background(), err)
|
||||||
} else {
|
|
||||||
errors.LogInfo(context.Background(), err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ message Config {
|
|||||||
|
|
||||||
string ech_config_list = 19;
|
string ech_config_list = 19;
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
string ech_force_query = 20;
|
string ech_force_query = 20;
|
||||||
|
|
||||||
SocketConfig ech_socket_settings = 21;
|
SocketConfig ech_socket_settings = 21;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
utls "github.com/refraction-networking/utls"
|
utls "github.com/refraction-networking/utls"
|
||||||
"github.com/xtls/xray-core/common/crypto"
|
"github.com/xtls/xray-core/common/crypto"
|
||||||
dns2 "github.com/xtls/xray-core/features/dns"
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -49,21 +48,11 @@ func ApplyECH(c *Config, config *tls.Config) error {
|
|||||||
|
|
||||||
// for client
|
// for client
|
||||||
if len(c.EchConfigList) != 0 {
|
if len(c.EchConfigList) != 0 {
|
||||||
ECHForceQuery := c.EchForceQuery
|
|
||||||
switch ECHForceQuery {
|
|
||||||
case "none", "half", "full":
|
|
||||||
case "":
|
|
||||||
ECHForceQuery = "full" // default to full
|
|
||||||
default:
|
|
||||||
panic("Invalid ECHForceQuery: " + c.EchForceQuery)
|
|
||||||
}
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// if failed to get ECHConfig, use an invalid one to make connection fail
|
// if failed to get ECHConfig, use an invalid one to make connection fail
|
||||||
if err != nil || len(ECHConfig) == 0 {
|
if len(ECHConfig) == 0 {
|
||||||
if ECHForceQuery == "full" {
|
|
||||||
ECHConfig = []byte{1, 1, 4, 5, 1, 4}
|
ECHConfig = []byte{1, 1, 4, 5, 1, 4}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
config.EncryptedClientHelloConfigList = ECHConfig
|
config.EncryptedClientHelloConfigList = ECHConfig
|
||||||
}()
|
}()
|
||||||
// direct base64 config
|
// direct base64 config
|
||||||
@@ -83,7 +72,7 @@ func ApplyECH(c *Config, config *tls.Config) error {
|
|||||||
if nameToQuery == "" {
|
if nameToQuery == "" {
|
||||||
return errors.New("Using DNS for ECH Config needs serverName or use Server 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, DNSServer, c.EchForceQuery, c.EchSocketSettings)
|
ECHConfig, err = QueryRecord(nameToQuery, DNSServer, c.EchSocketSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Failed to query ECH DNS record for domain: ", nameToQuery, " at server: ", DNSServer).Base(err)
|
return errors.New("Failed to query ECH DNS record for domain: ", nameToQuery, " at server: ", DNSServer).Base(err)
|
||||||
}
|
}
|
||||||
@@ -107,7 +96,6 @@ type ECHConfigCache struct {
|
|||||||
type echConfigRecord struct {
|
type echConfigRecord struct {
|
||||||
config []byte
|
config []byte
|
||||||
expire time.Time
|
expire time.Time
|
||||||
err error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -125,39 +113,34 @@ func ECHCacheKey(server, domain string, sockopt *internet.SocketConfig) string {
|
|||||||
// Update updates the ECH config for given domain and server.
|
// 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.
|
// this method is concurrent safe, only one update request will be sent, others get the cache.
|
||||||
// if isLockedUpdate is true, it will not try to acquire the lock.
|
// if isLockedUpdate is true, it will not try to acquire the lock.
|
||||||
func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) {
|
func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, sockopt *internet.SocketConfig) ([]byte, error) {
|
||||||
if !isLockedUpdate {
|
if !isLockedUpdate {
|
||||||
c.UpdateLock.Lock()
|
c.UpdateLock.Lock()
|
||||||
defer c.UpdateLock.Unlock()
|
defer c.UpdateLock.Unlock()
|
||||||
}
|
}
|
||||||
// Double check cache after acquiring lock
|
// Double check cache after acquiring lock
|
||||||
configRecord := c.configRecord.Load()
|
configRecord := c.configRecord.Load()
|
||||||
if configRecord.expire.After(time.Now()) && configRecord.err == nil {
|
if configRecord.expire.After(time.Now()) {
|
||||||
errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain)
|
errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain)
|
||||||
return configRecord.config, configRecord.err
|
return configRecord.config, nil
|
||||||
}
|
}
|
||||||
// Query ECH config from DNS server
|
// Query ECH config from DNS server
|
||||||
errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server)
|
errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server)
|
||||||
echConfig, ttl, err := dnsQuery(server, domain, sockopt)
|
echConfig, ttl, err := dnsQuery(server, domain, sockopt)
|
||||||
// if in "full", directly return
|
if err != nil {
|
||||||
if err != nil && forceQuery == "full" {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ttl == 0 {
|
|
||||||
ttl = dns2.DefaultTTL
|
|
||||||
}
|
|
||||||
configRecord = &echConfigRecord{
|
configRecord = &echConfigRecord{
|
||||||
config: echConfig,
|
config: echConfig,
|
||||||
expire: time.Now().Add(time.Duration(ttl) * time.Second),
|
expire: time.Now().Add(time.Duration(ttl) * time.Second),
|
||||||
err: err,
|
|
||||||
}
|
}
|
||||||
c.configRecord.Store(configRecord)
|
c.configRecord.Store(configRecord)
|
||||||
return configRecord.config, configRecord.err
|
return configRecord.config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryRecord returns the ECH config for given domain.
|
// 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.
|
// 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, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) {
|
func QueryRecord(domain string, server string, sockopt *internet.SocketConfig) ([]byte, error) {
|
||||||
GlobalECHConfigCacheKey := ECHCacheKey(server, domain, sockopt)
|
GlobalECHConfigCacheKey := ECHCacheKey(server, domain, sockopt)
|
||||||
echConfigCache, ok := GlobalECHConfigCache.Load(GlobalECHConfigCacheKey)
|
echConfigCache, ok := GlobalECHConfigCache.Load(GlobalECHConfigCacheKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -166,25 +149,25 @@ func QueryRecord(domain string, server string, forceQuery string, sockopt *inter
|
|||||||
echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(GlobalECHConfigCacheKey, echConfigCache)
|
echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(GlobalECHConfigCacheKey, echConfigCache)
|
||||||
}
|
}
|
||||||
configRecord := echConfigCache.configRecord.Load()
|
configRecord := echConfigCache.configRecord.Load()
|
||||||
if configRecord.expire.After(time.Now()) && (configRecord.err == nil || forceQuery == "none") {
|
if configRecord.expire.After(time.Now()) {
|
||||||
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
|
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
|
||||||
return configRecord.config, configRecord.err
|
return configRecord.config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If expire is zero value, 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
|
// otherwise return old value immediately and update in a goroutine
|
||||||
// but if the cache is too old, wait for update
|
// but if the cache is too old, wait for update
|
||||||
if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*4).Before(time.Now()) {
|
if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*4).Before(time.Now()) {
|
||||||
return echConfigCache.Update(domain, server, false, forceQuery, sockopt)
|
return echConfigCache.Update(domain, server, false, sockopt)
|
||||||
} else {
|
} else {
|
||||||
// If someone already acquired the lock, it means it is updating, do not start another update goroutine
|
// If someone already acquired the lock, it means it is updating, do not start another update goroutine
|
||||||
if echConfigCache.UpdateLock.TryLock() {
|
if echConfigCache.UpdateLock.TryLock() {
|
||||||
go func() {
|
go func() {
|
||||||
defer echConfigCache.UpdateLock.Unlock()
|
defer echConfigCache.UpdateLock.Unlock()
|
||||||
echConfigCache.Update(domain, server, true, forceQuery, sockopt)
|
echConfigCache.Update(domain, server, true, sockopt)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
return configRecord.config, configRecord.err
|
return configRecord.config, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,8 +305,7 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// empty is valid, means no ECH config found
|
return nil, 0, errors.New("no valid ECH config found in DNS response")
|
||||||
return nil, dns2.DefaultTTL, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrInvalidLen = errors.New("goech: invalid length")
|
var ErrInvalidLen = errors.New("goech: invalid length")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tls
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -59,21 +60,11 @@ func TestECHDial(t *testing.T) {
|
|||||||
func TestECHDialFail(t *testing.T) {
|
func TestECHDialFail(t *testing.T) {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
ServerName: "cloudflare.com",
|
ServerName: "cloudflare.com",
|
||||||
EchConfigList: "udp://127.0.0.1",
|
EchConfigList: "udp://0.0.0.0",
|
||||||
EchForceQuery: "half",
|
|
||||||
}
|
}
|
||||||
config.GetTLSConfig()
|
tlsConfig := config.GetTLSConfig()
|
||||||
// check cache
|
ApplyECH(config, tlsConfig)
|
||||||
echConfigCache, ok := GlobalECHConfigCache.Load(ECHCacheKey("udp://127.0.0.1", "cloudflare.com", nil))
|
if !slices.Equal(tlsConfig.EncryptedClientHelloConfigList, []byte{1, 1, 4, 5, 1, 4}) {
|
||||||
if !ok {
|
t.Error("ECH config should be invalid when query failed", " but got ", tlsConfig.EncryptedClientHelloConfigList)
|
||||||
t.Error("ECH config cache not found")
|
|
||||||
}
|
|
||||||
configRecord := echConfigCache.configRecord.Load()
|
|
||||||
if configRecord == nil {
|
|
||||||
t.Error("ECH config record not found in cache")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if configRecord.err == nil {
|
|
||||||
t.Error("unexpected nil error in ECH config record")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user