@@ -9,10 +9,6 @@ import (
99 "encoding/base64"
1010 "encoding/binary"
1111 "fmt"
12- utls "github.com/refraction-networking/utls"
13- "github.com/xtls/xray-core/common/crypto"
14- dns2 "github.com/xtls/xray-core/features/dns"
15- "golang.org/x/net/http2"
1612 "io"
1713 "net/http"
1814 "net/url"
@@ -21,6 +17,11 @@ import (
2117 "sync/atomic"
2218 "time"
2319
20+ utls "github.com/refraction-networking/utls"
21+ "github.com/xtls/xray-core/common/crypto"
22+ dns2 "github.com/xtls/xray-core/features/dns"
23+ "golang.org/x/net/http2"
24+
2425 "github.com/miekg/dns"
2526 "github.com/xtls/reality"
2627 "github.com/xtls/reality/hpke"
@@ -52,10 +53,18 @@ func ApplyECH(c *Config, config *tls.Config) error {
5253
5354 // for client
5455 if len (c .EchConfigList ) != 0 {
56+ ECHForceQuery := c .EchForceQuery
57+ switch ECHForceQuery {
58+ case "none" , "half" , "full" :
59+ case "" :
60+ ECHForceQuery = "none" // default to none
61+ default :
62+ panic ("Invalid ECHForceQuery: " + c .EchForceQuery )
63+ }
5564 defer func () {
5665 // if failed to get ECHConfig, use an invalid one to make connection fail
57- if err != nil {
58- if c . EchForceQuery {
66+ if err != nil || len ( ECHConfig ) == 0 {
67+ if ECHForceQuery == "full" {
5968 ECHConfig = []byte {1 , 1 , 4 , 5 , 1 , 4 }
6069 }
6170 }
@@ -106,32 +115,40 @@ type echConfigRecord struct {
106115}
107116
108117var (
109- // key value must be like this: "example.com|udp://1.1.1.1"
118+ // The keys for both maps must be generated by ECHCacheKey().
110119 GlobalECHConfigCache = utils .NewTypedSyncMap [string , * ECHConfigCache ]()
111120 clientForECHDOH = utils .NewTypedSyncMap [string , * http.Client ]()
112121)
113122
123+ // sockopt can be nil if not specified.
124+ // if for clientForECHDOH, domain can be empty.
125+ func ECHCacheKey (server , domain string , sockopt * internet.SocketConfig ) string {
126+ return server + "|" + domain + "|" + fmt .Sprintf ("%p" , sockopt )
127+ }
128+
114129// Update updates the ECH config for given domain and server.
115130// this method is concurrent safe, only one update request will be sent, others get the cache.
116131// if isLockedUpdate is true, it will not try to acquire the lock.
117- func (c * ECHConfigCache ) Update (domain string , server string , isLockedUpdate bool , forceQuery bool , sockopt * internet.SocketConfig ) ([]byte , error ) {
132+ func (c * ECHConfigCache ) Update (domain string , server string , isLockedUpdate bool , forceQuery string , sockopt * internet.SocketConfig ) ([]byte , error ) {
118133 if ! isLockedUpdate {
119134 c .UpdateLock .Lock ()
120135 defer c .UpdateLock .Unlock ()
121136 }
122137 // Double check cache after acquiring lock
123138 configRecord := c .configRecord .Load ()
124- if configRecord .expire .After (time .Now ()) {
139+ if configRecord .expire .After (time .Now ()) && configRecord . err == nil {
125140 errors .LogDebug (context .Background (), "Cache hit for domain after double check: " , domain )
126141 return configRecord .config , configRecord .err
127142 }
128143 // Query ECH config from DNS server
129144 errors .LogDebug (context .Background (), "Trying to query ECH config for domain: " , domain , " with ECH server: " , server )
130145 echConfig , ttl , err := dnsQuery (server , domain , sockopt )
131- if err != nil {
132- if forceQuery || ttl == 0 {
133- return nil , err
134- }
146+ // if in "full", directly return
147+ if err != nil && forceQuery == "full" {
148+ return nil , err
149+ }
150+ if ttl == 0 {
151+ ttl = dns2 .DefaultTTL
135152 }
136153 configRecord = & echConfigRecord {
137154 config : echConfig ,
@@ -144,16 +161,16 @@ func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate boo
144161
145162// QueryRecord returns the ECH config for given domain.
146163// If the record is not in cache or expired, it will query the DNS server and update the cache.
147- func QueryRecord (domain string , server string , forceQuery bool , sockopt * internet.SocketConfig ) ([]byte , error ) {
148- GlobalECHConfigCacheKey := domain + "|" + server + "|" + fmt . Sprintf ( "%p" , sockopt )
164+ func QueryRecord (domain string , server string , forceQuery string , sockopt * internet.SocketConfig ) ([]byte , error ) {
165+ GlobalECHConfigCacheKey := ECHCacheKey ( server , domain , sockopt )
149166 echConfigCache , ok := GlobalECHConfigCache .Load (GlobalECHConfigCacheKey )
150167 if ! ok {
151168 echConfigCache = & ECHConfigCache {}
152169 echConfigCache .configRecord .Store (& echConfigRecord {})
153170 echConfigCache , _ = GlobalECHConfigCache .LoadOrStore (GlobalECHConfigCacheKey , echConfigCache )
154171 }
155172 configRecord := echConfigCache .configRecord .Load ()
156- if configRecord .expire .After (time .Now ()) {
173+ if configRecord .expire .After (time .Now ()) && ( configRecord . err == nil || forceQuery == "none" ) {
157174 errors .LogDebug (context .Background (), "Cache hit for domain: " , domain )
158175 return configRecord .config , configRecord .err
159176 }
@@ -196,7 +213,7 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
196213 return nil , 0 , err
197214 }
198215 var client * http.Client
199- serverKey := server + "|" + fmt . Sprintf ( "%p " , sockopt )
216+ serverKey := ECHCacheKey ( server , " " , sockopt )
200217 if client , _ = clientForECHDOH .Load (serverKey ); client == nil {
201218 // All traffic sent by core should via xray's internet.DialSystem
202219 // This involves the behavior of some Android VPN GUI clients
@@ -307,7 +324,8 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
307324 }
308325 }
309326 }
310- return nil , dns2 .DefaultTTL , dns2 .ErrEmptyResponse
327+ // empty is valid, means no ECH config found
328+ return nil , dns2 .DefaultTTL , nil
311329}
312330
313331// reference github.com/OmarTariq612/goech
0 commit comments