Skip to content

Commit 5ff7c71

Browse files
authored
新增ECH客户端支持 (#3162)
* Add ECH support * Use internet.DialSystem() Why not * Many fixes
1 parent d9181ad commit 5ff7c71

File tree

6 files changed

+216
-17
lines changed

6 files changed

+216
-17
lines changed

infra/conf/cfgcommon/tlscfg/tls.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type TLSConfig struct {
2222
DisableSystemRoot bool `json:"disableSystemRoot"`
2323
PinnedPeerCertificateChainSha256 *[]string `json:"pinnedPeerCertificateChainSha256"`
2424
VerifyClientCertificate bool `json:"verifyClientCertificate"`
25+
ECHConfig string `json:"echConfig"`
26+
ECHDOHServer string `json:"echDohServer"`
2527
}
2628

2729
// Build implements Buildable.
@@ -58,6 +60,16 @@ func (c *TLSConfig) Build() (proto.Message, error) {
5860
}
5961
}
6062

63+
if c.ECHConfig != "" {
64+
ECHConfig, err := base64.StdEncoding.DecodeString(c.ECHConfig)
65+
if err != nil {
66+
return nil, newError("invalid ECH Config", c.ECHConfig)
67+
}
68+
config.EchConfig = ECHConfig
69+
}
70+
71+
config.Ech_DOHserver = c.ECHDOHServer
72+
6173
return config, nil
6274
}
6375

transport/internet/tls/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,14 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
286286
case Config_TLS1_3:
287287
config.MaxVersion = tls.VersionTLS13
288288
}
289+
290+
if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 {
291+
err := ApplyECH(c, config)
292+
if err != nil {
293+
newError("unable to set ECH").AtError().Base(err).WriteToLog()
294+
}
295+
}
296+
289297
return config
290298
}
291299

transport/internet/tls/config.pb.go

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ type Config struct {
234234
MaxVersion Config_TLSVersion `protobuf:"varint,10,opt,name=max_version,json=maxVersion,proto3,enum=v2ray.core.transport.internet.tls.Config_TLSVersion" json:"max_version,omitempty"`
235235
// Whether or not to allow self-signed certificates when pinned_peer_certificate_chain_sha256 is present.
236236
AllowInsecureIfPinnedPeerCertificate bool `protobuf:"varint,11,opt,name=allow_insecure_if_pinned_peer_certificate,json=allowInsecureIfPinnedPeerCertificate,proto3" json:"allow_insecure_if_pinned_peer_certificate,omitempty"`
237+
// ECH Config in bytes format
238+
EchConfig []byte `protobuf:"bytes,16,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"`
239+
// DOH server to query HTTPS record for ECH
240+
Ech_DOHserver string `protobuf:"bytes,17,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"`
237241
}
238242

239243
func (x *Config) Reset() {
@@ -345,6 +349,20 @@ func (x *Config) GetAllowInsecureIfPinnedPeerCertificate() bool {
345349
return false
346350
}
347351

352+
func (x *Config) GetEchConfig() []byte {
353+
if x != nil {
354+
return x.EchConfig
355+
}
356+
return nil
357+
}
358+
359+
func (x *Config) GetEch_DOHserver() string {
360+
if x != nil {
361+
return x.Ech_DOHserver
362+
}
363+
return ""
364+
}
365+
348366
var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
349367

350368
var file_transport_internet_tls_config_proto_rawDesc = []byte{
@@ -376,7 +394,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
376394
0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59,
377395
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x41, 0x55, 0x54, 0x48,
378396
0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x5f, 0x43, 0x4c, 0x49,
379-
0x45, 0x4e, 0x54, 0x10, 0x03, 0x22, 0xb2, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
397+
0x45, 0x4e, 0x54, 0x10, 0x03, 0x22, 0xf6, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
380398
0x12, 0x2d, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75,
381399
0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x06, 0x82, 0xb5, 0x18, 0x02, 0x28, 0x01,
382400
0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12,
@@ -421,22 +439,26 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
421439
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08,
422440
0x52, 0x24, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x49,
423441
0x66, 0x50, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69,
424-
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x49, 0x0a, 0x0a, 0x54, 0x4c, 0x53, 0x56, 0x65, 0x72,
425-
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10,
426-
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x30, 0x10, 0x01, 0x12, 0x0a, 0x0a,
427-
0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x31, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53,
428-
0x31, 0x5f, 0x32, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x33, 0x10,
429-
0x04, 0x3a, 0x17, 0x82, 0xb5, 0x18, 0x13, 0x0a, 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
430-
0x79, 0x12, 0x03, 0x74, 0x6c, 0x73, 0x90, 0xff, 0x29, 0x01, 0x42, 0x84, 0x01, 0x0a, 0x25, 0x63,
431-
0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72,
432-
0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
433-
0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
434-
0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63,
435-
0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74,
436-
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x21,
437-
0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73,
438-
0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c,
439-
0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
442+
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f,
443+
0x6e, 0x66, 0x69, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43,
444+
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4f, 0x48,
445+
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63,
446+
0x68, 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x49, 0x0a, 0x0a, 0x54, 0x4c,
447+
0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61,
448+
0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x30, 0x10,
449+
0x01, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x31, 0x10, 0x02, 0x12, 0x0a, 0x0a,
450+
0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x32, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53,
451+
0x31, 0x5f, 0x33, 0x10, 0x04, 0x3a, 0x17, 0x82, 0xb5, 0x18, 0x13, 0x0a, 0x08, 0x73, 0x65, 0x63,
452+
0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x03, 0x74, 0x6c, 0x73, 0x90, 0xff, 0x29, 0x01, 0x42, 0x84,
453+
0x01, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
454+
0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65,
455+
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
456+
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72,
457+
0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73,
458+
0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c,
459+
0x73, 0xaa, 0x02, 0x21, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x54,
460+
0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65,
461+
0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
440462
}
441463

442464
var (

transport/internet/tls/config.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,10 @@ message Config {
7878

7979
// Whether or not to allow self-signed certificates when pinned_peer_certificate_chain_sha256 is present.
8080
bool allow_insecure_if_pinned_peer_certificate = 11;
81+
82+
// ECH Config in bytes format
83+
bytes ech_config = 16;
84+
85+
// DOH server to query HTTPS record for ECH
86+
string ech_DOHserver = 17;
8187
}

transport/internet/tls/ech.go

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

transport/internet/tls/ech_go122.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build !go1.23
2+
// +build !go1.23
3+
4+
package tls
5+
6+
import (
7+
"crypto/tls"
8+
)
9+
10+
func ApplyECH(c *Config, config *tls.Config) error {
11+
return newError("using ECH require go 1.23 or higher")
12+
}

0 commit comments

Comments
 (0)