Skip to content

Commit 6f84e7d

Browse files
authored
ECH: client support TLS Encrypted Client Hello
1 parent db5f18b commit 6f84e7d

File tree

5 files changed

+190
-10
lines changed

5 files changed

+190
-10
lines changed

infra/conf/transport_internet.go

+11
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,8 @@ type TLSConfig struct {
412412
MasterKeyLog string `json:"masterKeyLog"`
413413
ServerNameToVerify string `json:"serverNameToVerify"`
414414
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
415+
ECHConfig string `json:"echConfig"`
416+
ECHDOHServer string `json:"echDohServer"`
415417
}
416418

417419
// Build implements Buildable.
@@ -476,6 +478,15 @@ func (c *TLSConfig) Build() (proto.Message, error) {
476478
}
477479
config.VerifyPeerCertInNames = c.VerifyPeerCertInNames
478480

481+
if c.ECHConfig != "" {
482+
ECHConfig, err := base64.StdEncoding.DecodeString(c.ECHConfig)
483+
if err != nil {
484+
return nil, errors.New("invalid ECH Config", c.ECHConfig)
485+
}
486+
config.EchConfig = ECHConfig
487+
}
488+
config.Ech_DOHserver = c.ECHDOHServer
489+
479490
return config, nil
480491
}
481492

transport/internet/tls/config.go

+6
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
444444
config.KeyLogWriter = writer
445445
}
446446
}
447+
if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 {
448+
err := ApplyECH(c, config)
449+
if err != nil {
450+
errors.LogError(context.Background(), err)
451+
}
452+
}
447453

448454
return config
449455
}

transport/internet/tls/config.pb.go

+30-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

transport/internet/tls/config.proto

+6
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,10 @@ message Config {
9191
@Critical
9292
*/
9393
repeated string verify_peer_cert_in_names = 17;
94+
95+
96+
97+
bytes ech_config = 20;
98+
99+
string ech_DOHserver = 21;
94100
}

transport/internet/tls/ech.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package tls
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/tls"
7+
"io"
8+
"net/http"
9+
"sync"
10+
"time"
11+
12+
"github.com/miekg/dns"
13+
"github.com/xtls/xray-core/common/errors"
14+
"github.com/xtls/xray-core/common/net"
15+
"github.com/xtls/xray-core/transport/internet"
16+
)
17+
18+
func ApplyECH(c *Config, config *tls.Config) error {
19+
var ECHConfig []byte
20+
var err error
21+
22+
if len(c.EchConfig) > 0 {
23+
ECHConfig = c.EchConfig
24+
} else { // ECH config > DOH lookup
25+
if config.ServerName == "" {
26+
return errors.New("Using DOH for ECH needs serverName")
27+
}
28+
ECHConfig, err = QueryRecord(c.ServerName, c.Ech_DOHserver)
29+
if err != nil {
30+
return err
31+
}
32+
}
33+
34+
config.EncryptedClientHelloConfigList = ECHConfig
35+
return nil
36+
}
37+
38+
type record struct {
39+
record []byte
40+
expire time.Time
41+
}
42+
43+
var (
44+
dnsCache = make(map[string]record)
45+
mutex sync.RWMutex
46+
)
47+
48+
func QueryRecord(domain string, server string) ([]byte, error) {
49+
mutex.Lock()
50+
rec, found := dnsCache[domain]
51+
if found && rec.expire.After(time.Now()) {
52+
mutex.Unlock()
53+
return rec.record, nil
54+
}
55+
mutex.Unlock()
56+
57+
errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server)
58+
record, ttl, err := dohQuery(server, domain)
59+
if err != nil {
60+
return []byte{}, err
61+
}
62+
63+
if ttl < 600 {
64+
ttl = 600
65+
}
66+
67+
mutex.Lock()
68+
defer mutex.Unlock()
69+
rec.record = record
70+
rec.expire = time.Now().Add(time.Second * time.Duration(ttl))
71+
dnsCache[domain] = rec
72+
return record, nil
73+
}
74+
75+
func dohQuery(server string, domain string) ([]byte, uint32, error) {
76+
m := new(dns.Msg)
77+
m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS)
78+
m.Id = 0
79+
msg, err := m.Pack()
80+
if err != nil {
81+
return []byte{}, 0, err
82+
}
83+
tr := &http.Transport{
84+
IdleConnTimeout: 90 * time.Second,
85+
ForceAttemptHTTP2: true,
86+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
87+
dest, err := net.ParseDestination(network + ":" + addr)
88+
if err != nil {
89+
return nil, err
90+
}
91+
conn, err := internet.DialSystem(ctx, dest, nil)
92+
if err != nil {
93+
return nil, err
94+
}
95+
return conn, nil
96+
},
97+
}
98+
client := &http.Client{
99+
Timeout: 5 * time.Second,
100+
Transport: tr,
101+
}
102+
req, err := http.NewRequest("POST", server, bytes.NewReader(msg))
103+
if err != nil {
104+
return []byte{}, 0, err
105+
}
106+
req.Header.Set("Content-Type", "application/dns-message")
107+
resp, err := client.Do(req)
108+
if err != nil {
109+
return []byte{}, 0, err
110+
}
111+
defer resp.Body.Close()
112+
respBody, err := io.ReadAll(resp.Body)
113+
if err != nil {
114+
return []byte{}, 0, err
115+
}
116+
if resp.StatusCode != http.StatusOK {
117+
return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode)
118+
}
119+
respMsg := new(dns.Msg)
120+
err = respMsg.Unpack(respBody)
121+
if err != nil {
122+
return []byte{}, 0, err
123+
}
124+
if len(respMsg.Answer) > 0 {
125+
for _, answer := range respMsg.Answer {
126+
if https, ok := answer.(*dns.HTTPS); ok && https.Hdr.Name == dns.Fqdn(domain) {
127+
for _, v := range https.Value {
128+
if echConfig, ok := v.(*dns.SVCBECHConfig); ok {
129+
errors.LogDebug(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl)
130+
return echConfig.ECH, answer.Header().Ttl, nil
131+
}
132+
}
133+
}
134+
}
135+
}
136+
return []byte{}, 0, errors.New("no ech record found")
137+
}

0 commit comments

Comments
 (0)