|
|
@@ -9,10 +9,6 @@ import (
|
|
|
"encoding/base64"
|
|
|
"encoding/binary"
|
|
|
"fmt"
|
|
|
- utls "github.com/refraction-networking/utls"
|
|
|
- "github.com/xtls/xray-core/common/crypto"
|
|
|
- dns2 "github.com/xtls/xray-core/features/dns"
|
|
|
- "golang.org/x/net/http2"
|
|
|
"io"
|
|
|
"net/http"
|
|
|
"net/url"
|
|
|
@@ -21,6 +17,11 @@ import (
|
|
|
"sync/atomic"
|
|
|
"time"
|
|
|
|
|
|
+ utls "github.com/refraction-networking/utls"
|
|
|
+ "github.com/xtls/xray-core/common/crypto"
|
|
|
+ dns2 "github.com/xtls/xray-core/features/dns"
|
|
|
+ "golang.org/x/net/http2"
|
|
|
+
|
|
|
"github.com/miekg/dns"
|
|
|
"github.com/xtls/reality"
|
|
|
"github.com/xtls/reality/hpke"
|
|
|
@@ -52,10 +53,18 @@ func ApplyECH(c *Config, config *tls.Config) error {
|
|
|
|
|
|
// for client
|
|
|
if len(c.EchConfigList) != 0 {
|
|
|
+ ECHForceQuery := c.EchForceQuery
|
|
|
+ switch ECHForceQuery {
|
|
|
+ case "none", "half", "full":
|
|
|
+ case "":
|
|
|
+ ECHForceQuery = "none" // default to none
|
|
|
+ default:
|
|
|
+ panic("Invalid ECHForceQuery: " + c.EchForceQuery)
|
|
|
+ }
|
|
|
defer func() {
|
|
|
// if failed to get ECHConfig, use an invalid one to make connection fail
|
|
|
- if err != nil {
|
|
|
- if c.EchForceQuery {
|
|
|
+ if err != nil || len(ECHConfig) == 0 {
|
|
|
+ if ECHForceQuery == "full" {
|
|
|
ECHConfig = []byte{1, 1, 4, 5, 1, 4}
|
|
|
}
|
|
|
}
|
|
|
@@ -106,32 +115,40 @@ type echConfigRecord struct {
|
|
|
}
|
|
|
|
|
|
var (
|
|
|
- // key value must be like this: "example.com|udp://1.1.1.1"
|
|
|
+ // The keys for both maps must be generated by ECHCacheKey().
|
|
|
GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]()
|
|
|
clientForECHDOH = utils.NewTypedSyncMap[string, *http.Client]()
|
|
|
)
|
|
|
|
|
|
+// sockopt can be nil if not specified.
|
|
|
+// if for clientForECHDOH, domain can be empty.
|
|
|
+func ECHCacheKey(server, domain string, sockopt *internet.SocketConfig) string {
|
|
|
+ return server + "|" + domain + "|" + fmt.Sprintf("%p", sockopt)
|
|
|
+}
|
|
|
+
|
|
|
// 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 will not try to acquire the lock.
|
|
|
-func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, forceQuery bool, sockopt *internet.SocketConfig) ([]byte, error) {
|
|
|
+func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) {
|
|
|
if !isLockedUpdate {
|
|
|
c.UpdateLock.Lock()
|
|
|
defer c.UpdateLock.Unlock()
|
|
|
}
|
|
|
// Double check cache after acquiring lock
|
|
|
configRecord := c.configRecord.Load()
|
|
|
- if configRecord.expire.After(time.Now()) {
|
|
|
+ if configRecord.expire.After(time.Now()) && configRecord.err == nil {
|
|
|
errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain)
|
|
|
return configRecord.config, configRecord.err
|
|
|
}
|
|
|
// 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, sockopt)
|
|
|
- if err != nil {
|
|
|
- if forceQuery || ttl == 0 {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
+ // if in "full", directly return
|
|
|
+ if err != nil && forceQuery == "full" {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if ttl == 0 {
|
|
|
+ ttl = dns2.DefaultTTL
|
|
|
}
|
|
|
configRecord = &echConfigRecord{
|
|
|
config: echConfig,
|
|
|
@@ -144,8 +161,8 @@ func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate boo
|
|
|
|
|
|
// 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, forceQuery bool, sockopt *internet.SocketConfig) ([]byte, error) {
|
|
|
- GlobalECHConfigCacheKey := domain + "|" + server + "|" + fmt.Sprintf("%p", sockopt)
|
|
|
+func QueryRecord(domain string, server string, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) {
|
|
|
+ GlobalECHConfigCacheKey := ECHCacheKey(server, domain, sockopt)
|
|
|
echConfigCache, ok := GlobalECHConfigCache.Load(GlobalECHConfigCacheKey)
|
|
|
if !ok {
|
|
|
echConfigCache = &ECHConfigCache{}
|
|
|
@@ -153,7 +170,7 @@ func QueryRecord(domain string, server string, forceQuery bool, sockopt *interne
|
|
|
echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(GlobalECHConfigCacheKey, echConfigCache)
|
|
|
}
|
|
|
configRecord := echConfigCache.configRecord.Load()
|
|
|
- if configRecord.expire.After(time.Now()) {
|
|
|
+ if configRecord.expire.After(time.Now()) && (configRecord.err == nil || forceQuery == "none") {
|
|
|
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
|
|
|
return configRecord.config, configRecord.err
|
|
|
}
|
|
|
@@ -196,7 +213,7 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
|
|
|
return nil, 0, err
|
|
|
}
|
|
|
var client *http.Client
|
|
|
- serverKey := server + "|" + fmt.Sprintf("%p", sockopt)
|
|
|
+ serverKey := ECHCacheKey(server, "", sockopt)
|
|
|
if client, _ = clientForECHDOH.Load(serverKey); client == nil {
|
|
|
// All traffic sent by core should via xray's internet.DialSystem
|
|
|
// This involves the behavior of some Android VPN GUI clients
|
|
|
@@ -307,7 +324,8 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- return nil, dns2.DefaultTTL, dns2.ErrEmptyResponse
|
|
|
+ // empty is valid, means no ECH config found
|
|
|
+ return nil, dns2.DefaultTTL, nil
|
|
|
}
|
|
|
|
|
|
// reference github.com/OmarTariq612/goech
|