Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

新增ECH客户端支持 #3162

Merged
merged 3 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions infra/conf/cfgcommon/tlscfg/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type TLSConfig struct {
DisableSystemRoot bool `json:"disableSystemRoot"`
PinnedPeerCertificateChainSha256 *[]string `json:"pinnedPeerCertificateChainSha256"`
VerifyClientCertificate bool `json:"verifyClientCertificate"`
ECHConfig string `json:"echConfig"`
ECHDOHServer string `json:"echDohServer"`
}

// Build implements Buildable.
Expand Down Expand Up @@ -58,6 +60,16 @@ func (c *TLSConfig) Build() (proto.Message, error) {
}
}

if c.ECHConfig != "" {
ECHConfig, err := base64.StdEncoding.DecodeString(c.ECHConfig)
if err != nil {
return nil, newError("invalid ECH Config", c.ECHConfig)
}
config.EchConfig = ECHConfig
}

config.Ech_DOHserver = c.ECHDOHServer

return config, nil
}

Expand Down
8 changes: 8 additions & 0 deletions transport/internet/tls/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@
case Config_TLS1_3:
config.MaxVersion = tls.VersionTLS13
}

if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 {
err := ApplyECH(c, config)

Check failure on line 291 in transport/internet/tls/config.go

View workflow job for this annotation

GitHub Actions / lint

SA4023(related information): the lhs of the comparison is the 1st return value of this function call (staticcheck)
if err != nil {

Check failure on line 292 in transport/internet/tls/config.go

View workflow job for this annotation

GitHub Actions / lint

SA4023: this comparison is always true (staticcheck)
newError("unable to set ECH").AtError().Base(err).WriteToLog()
}
}

return config
}

Expand Down
56 changes: 39 additions & 17 deletions transport/internet/tls/config.pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ type Config struct {
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"`
// Whether or not to allow self-signed certificates when pinned_peer_certificate_chain_sha256 is present.
AllowInsecureIfPinnedPeerCertificate bool `protobuf:"varint,11,opt,name=allow_insecure_if_pinned_peer_certificate,json=allowInsecureIfPinnedPeerCertificate,proto3" json:"allow_insecure_if_pinned_peer_certificate,omitempty"`
// ECH Config in bytes format
EchConfig []byte `protobuf:"bytes,16,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"`
// DOH server to query HTTPS record for ECH
Ech_DOHserver string `protobuf:"bytes,17,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"`
}

func (x *Config) Reset() {
Expand Down Expand Up @@ -345,6 +349,20 @@ func (x *Config) GetAllowInsecureIfPinnedPeerCertificate() bool {
return false
}

func (x *Config) GetEchConfig() []byte {
if x != nil {
return x.EchConfig
}
return nil
}

func (x *Config) GetEch_DOHserver() string {
if x != nil {
return x.Ech_DOHserver
}
return ""
}

var File_transport_internet_tls_config_proto protoreflect.FileDescriptor

var file_transport_internet_tls_config_proto_rawDesc = []byte{
Expand Down Expand Up @@ -376,7 +394,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x41, 0x55, 0x54, 0x48,
0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x5f, 0x43, 0x4c, 0x49,
0x45, 0x4e, 0x54, 0x10, 0x03, 0x22, 0xb2, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x45, 0x4e, 0x54, 0x10, 0x03, 0x22, 0xf6, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x12, 0x2d, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75,
0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x06, 0x82, 0xb5, 0x18, 0x02, 0x28, 0x01,
0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12,
Expand Down Expand Up @@ -421,22 +439,26 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08,
0x52, 0x24, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x49,
0x66, 0x50, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x49, 0x0a, 0x0a, 0x54, 0x4c, 0x53, 0x56, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10,
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x30, 0x10, 0x01, 0x12, 0x0a, 0x0a,
0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x31, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53,
0x31, 0x5f, 0x32, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x33, 0x10,
0x04, 0x3a, 0x17, 0x82, 0xb5, 0x18, 0x13, 0x0a, 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
0x79, 0x12, 0x03, 0x74, 0x6c, 0x73, 0x90, 0xff, 0x29, 0x01, 0x42, 0x84, 0x01, 0x0a, 0x25, 0x63,
0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72,
0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63,
0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74,
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x21,
0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73,
0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c,
0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4f, 0x48,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63,
0x68, 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x49, 0x0a, 0x0a, 0x54, 0x4c,
0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61,
0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x30, 0x10,
0x01, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x31, 0x10, 0x02, 0x12, 0x0a, 0x0a,
0x06, 0x54, 0x4c, 0x53, 0x31, 0x5f, 0x32, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x4c, 0x53,
0x31, 0x5f, 0x33, 0x10, 0x04, 0x3a, 0x17, 0x82, 0xb5, 0x18, 0x13, 0x0a, 0x08, 0x73, 0x65, 0x63,
0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x03, 0x74, 0x6c, 0x73, 0x90, 0xff, 0x29, 0x01, 0x42, 0x84,
0x01, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72,
0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73,
0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c,
0x73, 0xaa, 0x02, 0x21, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x54,
0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65,
0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}

var (
Expand Down
6 changes: 6 additions & 0 deletions transport/internet/tls/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,10 @@ message Config {

// Whether or not to allow self-signed certificates when pinned_peer_certificate_chain_sha256 is present.
bool allow_insecure_if_pinned_peer_certificate = 11;

// ECH Config in bytes format
bytes ech_config = 16;

// DOH server to query HTTPS record for ECH
string ech_DOHserver = 17;
}
139 changes: 139 additions & 0 deletions transport/internet/tls/ech.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//go:build go1.23
// +build go1.23

package tls

import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"sync"
"time"

"github.com/miekg/dns"
"github.com/v2fly/v2ray-core/v5/common/net"
"github.com/v2fly/v2ray-core/v5/transport/internet"
)

func ApplyECH(c *Config, config *tls.Config) error {
var ECHConfig []byte
var err error

if len(c.EchConfig) > 0 {
ECHConfig = c.EchConfig
} else { // ECH config > DOH lookup
if config.ServerName == "" {
return newError("Using DOH for ECH needs serverName")
}
ECHConfig, err = QueryRecord(c.ServerName, c.Ech_DOHserver)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider the situation that server address will be filled into config.ServerName if no c.ServerName specified and server address is a domain name, should use ECHConfig, err = QueryRecord(config.ServerName, c.Ech_DOHserver) here.

if err != nil {
return err
}
}

config.EncryptedClientHelloConfigList = ECHConfig
return nil
}

type record struct {
record []byte
expire time.Time
}

var (
dnsCache = make(map[string]record)
mutex sync.RWMutex
)

func QueryRecord(domain string, server string) ([]byte, error) {
mutex.Lock()
rec, found := dnsCache[domain]
if found && rec.expire.After(time.Now()) {
mutex.Unlock()
return rec.record, nil
}
mutex.Unlock()

newError("Trying to query ECH config for domain: ", domain, " with ECH server: ", server).AtDebug().WriteToLog()
record, ttl, err := dohQuery(server, domain)
if err != nil {
return []byte{}, err
}

if ttl < 600 {
ttl = 600
}

mutex.Lock()
defer mutex.Unlock()
rec.record = record
rec.expire = time.Now().Add(time.Second * time.Duration(ttl))
dnsCache[domain] = rec
return record, nil
}

func dohQuery(server string, domain string) ([]byte, uint32, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS)
m.Id = 0
msg, err := m.Pack()
if err != nil {
return []byte{}, 0, err
}
tr := &http.Transport{
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
return conn, nil
},
}
client := &http.Client{
Timeout: 5 * time.Second,
Transport: tr,
}
req, err := http.NewRequest("POST", server, bytes.NewReader(msg))
if err != nil {
return []byte{}, 0, err
}
req.Header.Set("Content-Type", "application/dns-message")
resp, err := client.Do(req)
if err != nil {
return []byte{}, 0, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, 0, err
}
if resp.StatusCode != http.StatusOK {
return []byte{}, 0, newError("query failed with response code:", resp.StatusCode)
}
respMsg := new(dns.Msg)
err = respMsg.Unpack(respBody)
if err != nil {
return []byte{}, 0, err
}
if len(respMsg.Answer) > 0 {
for _, answer := range respMsg.Answer {
if https, ok := answer.(*dns.HTTPS); ok && https.Hdr.Name == dns.Fqdn(domain) {
for _, v := range https.Value {
if echConfig, ok := v.(*dns.SVCBECHConfig); ok {
newError(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl).AtDebug().WriteToLog()
return echConfig.ECH, answer.Header().Ttl, nil
}
}
}
}
}
return []byte{}, 0, newError("no ech record found")
}
12 changes: 12 additions & 0 deletions transport/internet/tls/ech_go122.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build !go1.23
// +build !go1.23

package tls

import (
"crypto/tls"
)

func ApplyECH(c *Config, config *tls.Config) error {

Check failure on line 10 in transport/internet/tls/ech_go122.go

View workflow job for this annotation

GitHub Actions / lint

SA4023(related information): github.com/v2fly/v2ray-core/v5/transport/internet/tls.ApplyECH never returns a nil interface value (staticcheck)
return newError("using ECH require go 1.23 or higher")
}
Loading