Compare commits

...

12 Commits

Author SHA1 Message Date
风扇滑翔翼
5f504888b6 Add support for internal DNS system 2025-03-09 18:55:37 +00:00
风扇滑翔翼
4999fd5b7b Missing rename 2025-03-09 13:30:15 +00:00
风扇滑翔翼
7f6a825bfe Do some rename 2025-03-09 12:37:54 +00:00
风扇滑翔翼
6d5be86947 Add classic UDP DNS support for ECH Config 2025-03-09 09:43:40 +00:00
风扇滑翔翼
0923f53b21 bugfix 2025-03-04 16:10:53 +00:00
风扇滑翔翼
8554549f2c Refine xray tls ech output format 2025-03-04 16:10:53 +00:00
风扇滑翔翼
cab4321956 Add server support 2025-03-04 16:10:52 +00:00
风扇滑翔翼
26199629f7 Update goech to v0.0.1 2025-03-04 16:10:52 +00:00
风扇滑翔翼
53ceaf87a5 Use sync.Map 2025-03-04 16:10:52 +00:00
风扇滑翔翼
73e98665ac Add new doh server format 2025-03-04 16:10:51 +00:00
风扇滑翔翼
52c46bc508 Update mutex usage 2025-03-04 16:10:51 +00:00
风扇滑翔翼
9bdf866471 ECH: client support TLS Encrypted Client Hello 2025-03-04 16:10:50 +00:00
14 changed files with 517 additions and 36 deletions

View File

@@ -210,6 +210,34 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...)) return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
} }
func (s *DNS) LookupHTTPS(domain string) (map[string]string, error) {
errs := []error{}
ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag})
for _, client := range s.sortClients(domain) {
if strings.EqualFold(client.Name(), "FakeDNS") {
errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name())
continue
}
EnhancedServer, ok := client.server.(EnhancedServer)
if !ok {
continue
}
HTTPSRecord, err := EnhancedServer.QueryHTTPS(ctx, domain, s.disableCache)
if len(HTTPSRecord) > 0 {
return HTTPSRecord, nil
}
if err != nil {
errors.LogInfoInner(s.ctx, err, "failed to lookup HTTPS for domain ", domain, " at server ", client.Name())
errs = append(errs, err)
}
// 5 for RcodeRefused in miekg/dns, hardcode to reduce binary size
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch && err != dns.ErrEmptyResponse && dns.RCodeFromError(err) != 5 {
return nil, err
}
}
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
}
// LookupHosts implements dns.HostsLookup. // LookupHosts implements dns.HostsLookup.
func (s *DNS) LookupHosts(domain string) *net.Address { func (s *DNS) LookupHosts(domain string) *net.Address {
domain = strings.TrimSuffix(domain, ".") domain = strings.TrimSuffix(domain, ".")

View File

@@ -29,6 +29,12 @@ type record struct {
AAAA *IPRecord AAAA *IPRecord
} }
type HTTPSRecord struct {
keypair map[string]string
Expire time.Time
RCode dnsmessage.RCode
}
// IPRecord is a cacheable item for a resolved domain // IPRecord is a cacheable item for a resolved domain
type IPRecord struct { type IPRecord struct {
ReqID uint16 ReqID uint16

View File

@@ -23,6 +23,13 @@ type Server interface {
QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, error) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, error)
} }
// Server is the interface for Enhanced Name Server.
type EnhancedServer interface {
Server
// QueryHTTPS sends HTTPS queries to its configured server.
QueryHTTPS(ctx context.Context, domain string, disableCache bool) (map[string]string, error)
}
// Client is the interface for DNS client. // Client is the interface for DNS client.
type Client struct { type Client struct {
server Server server Server

View File

@@ -12,6 +12,7 @@ import (
"sync" "sync"
"time" "time"
mdns "github.com/miekg/dns"
utls "github.com/refraction-networking/utls" utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
@@ -42,6 +43,8 @@ type DoHNameServer struct {
dohURL string dohURL string
name string name string
queryStrategy QueryStrategy queryStrategy QueryStrategy
HTTPSCache map[string]*HTTPSRecord
} }
// NewDoHNameServer creates DOH/DOHL client object for remote/local resolving. // NewDoHNameServer creates DOH/DOHL client object for remote/local resolving.
@@ -58,6 +61,7 @@ func NewDoHNameServer(url *url.URL, queryStrategy QueryStrategy, dispatcher rout
name: mode + "//" + url.Host, name: mode + "//" + url.Host,
dohURL: url.String(), dohURL: url.String(),
queryStrategy: queryStrategy, queryStrategy: queryStrategy,
HTTPSCache: make(map[string]*HTTPSRecord),
} }
s.cleanup = &task.Periodic{ s.cleanup = &task.Periodic{
Interval: time.Minute, Interval: time.Minute,
@@ -207,6 +211,21 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
common.Must(s.cleanup.Start()) common.Must(s.cleanup.Start())
} }
func (s *DoHNameServer) updateHTTPS(domain string, HTTPSRec *HTTPSRecord) {
s.Lock()
rec, found := s.HTTPSCache[domain]
if !found {
s.HTTPSCache[domain] = HTTPSRec
}
if found && rec.Expire.Before(time.Now()) {
s.HTTPSCache[domain] = HTTPSRec
}
errors.LogInfo(context.Background(), s.name, " got answer: ", domain, " ", "HTTPS", " -> ", HTTPSRec.keypair)
s.pub.Publish(domain+"HTTPS", nil)
s.Unlock()
}
func (s *DoHNameServer) newReqID() uint16 { func (s *DoHNameServer) newReqID() uint16 {
return 0 return 0
} }
@@ -271,6 +290,59 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP n
} }
} }
func (s *DoHNameServer) sendHTTPSQuery(ctx context.Context, domain string) {
errors.LogInfo(ctx, s.name, " querying HTTPS record for: ", domain)
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
deadline = d
} else {
deadline = time.Now().Add(time.Second * 5)
}
dnsCtx := ctx
// reserve internal dns server requested Inbound
if inbound := session.InboundFromContext(ctx); inbound != nil {
dnsCtx = session.ContextWithInbound(dnsCtx, inbound)
}
dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{
Protocol: "https",
SkipDNSResolve: true,
})
var cancel context.CancelFunc
dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline)
defer cancel()
m := new(mdns.Msg)
m.SetQuestion(mdns.Fqdn(domain), mdns.TypeHTTPS)
m.Id = 0
msg, _ := m.Pack()
response, err := s.dohHTTPSContext(dnsCtx, msg)
if err != nil {
errors.LogError(ctx, err, "failed to retrieve HTTPS query response for ", domain)
return
}
respMsg := new(mdns.Msg)
err = respMsg.Unpack(response)
if err != nil {
errors.LogError(ctx, err, "failed to parse HTTPS query response for ", domain)
return
}
var Record = HTTPSRecord{
keypair: map[string]string{},
}
if len(respMsg.Answer) > 0 {
for _, answer := range respMsg.Answer {
if https, ok := answer.(*mdns.HTTPS); ok && https.Hdr.Name == mdns.Fqdn(domain) {
for _, value := range https.Value {
Record.keypair[value.Key().String()] = value.String()
}
}
}
}
Record.Expire = time.Now().Add(time.Duration(respMsg.Answer[0].Header().Ttl) * time.Second)
s.updateHTTPS(domain, &Record)
}
func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) { func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) {
body := bytes.NewBuffer(b) body := bytes.NewBuffer(b)
req, err := http.NewRequest("POST", s.dohURL, body) req, err := http.NewRequest("POST", s.dohURL, body)
@@ -341,6 +413,27 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt
return nil, errRecordNotFound return nil, errRecordNotFound
} }
func (s *DoHNameServer) findRecordsForDomain(domain string, Querytype string) (any, error) {
switch Querytype {
case "HTTPS":
s.RLock()
record, found := s.HTTPSCache[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
}
if len(record.keypair) == 0 {
return nil, dns_feature.ErrEmptyResponse
}
if record.Expire.Before(time.Now()) {
return nil, errRecordNotFound
}
return record, nil
default:
return nil, errors.New("unsupported query type: " + Querytype)
}
}
// QueryIP implements Server. // QueryIP implements Server.
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl
fqdn := Fqdn(domain) fqdn := Fqdn(domain)
@@ -403,3 +496,44 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net
} }
} }
} }
// QueryHTTPS implements EnhancedServer.
func (s *DoHNameServer) QueryHTTPS(ctx context.Context, domain string, disableCache bool) (map[string]string, error) { // nolint: dupl
fqdn := Fqdn(domain)
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying HTTPS for ", domain, " at ", s.name)
} else {
Record, err := s.findRecordsForDomain(fqdn, "HTTPS")
if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", Record.(HTTPSRecord).keypair)
return Record.(HTTPSRecord).keypair, err
}
}
sub := s.pub.Subscribe(fqdn + "HTTPS")
defer sub.Close()
done := make(chan interface{})
go func() {
if sub != nil {
select {
case <-sub.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendHTTPSQuery(ctx, fqdn)
for {
Record, err := s.findRecordsForDomain(fqdn, "HTTPS")
if err != errRecordNotFound {
return Record.(*HTTPSRecord).keypair, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
}
}
}

View File

@@ -24,6 +24,13 @@ type Client interface {
LookupIP(domain string, option IPOption) ([]net.IP, error) LookupIP(domain string, option IPOption) ([]net.IP, error)
} }
type EnhancedClient interface {
Client
// LookupHTTPS returns HTTPS records for the given domain.
LookupHTTPS(domain string) (map[string]string, error)
}
type HostsLookup interface { type HostsLookup interface {
LookupHosts(domain string) *net.Address LookupHosts(domain string) *net.Address
} }

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
go 1.24 go 1.24
require ( require (
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 github.com/OmarTariq612/goech v0.0.1
github.com/cloudflare/circl v1.6.0 github.com/cloudflare/circl v1.6.0
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1

4
go.sum
View File

@@ -1,5 +1,5 @@
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I= github.com/OmarTariq612/goech v0.0.1 h1:/0c918Bk1ik65GXDj2k7SOK78DyZr30Jmq9euy1/HXg=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= github.com/OmarTariq612/goech v0.0.1/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=

View File

@@ -412,6 +412,8 @@ type TLSConfig struct {
MasterKeyLog string `json:"masterKeyLog"` MasterKeyLog string `json:"masterKeyLog"`
ServerNameToVerify string `json:"serverNameToVerify"` ServerNameToVerify string `json:"serverNameToVerify"`
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
ECHConfigList string `json:"echConfigList"`
EchKeySets string `json:"echKeySets"`
} }
// Build implements Buildable. // Build implements Buildable.
@@ -483,6 +485,16 @@ func (c *TLSConfig) Build() (proto.Message, error) {
} }
config.VerifyPeerCertInNames = c.VerifyPeerCertInNames config.VerifyPeerCertInNames = c.VerifyPeerCertInNames
config.EchConfigList = c.ECHConfigList
if c.EchKeySets != "" {
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.EchKeySets)
if err != nil {
return nil, errors.New("invalid ECH Config", c.EchKeySets)
}
config.EchKeySets = EchPrivateKey
}
return config, nil return config, nil
} }

View File

@@ -1,10 +1,8 @@
package tls package tls
import ( import (
"encoding/json"
"encoding/pem" "encoding/pem"
"os" "os"
"strings"
"github.com/OmarTariq612/goech" "github.com/OmarTariq612/goech"
"github.com/cloudflare/circl/hpke" "github.com/cloudflare/circl/hpke"
@@ -13,13 +11,13 @@ import (
) )
var cmdECH = &base.Command{ var cmdECH = &base.Command{
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--json]`, UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem]`,
Short: `Generate TLS-ECH certificates`, Short: `Generate TLS-ECH certificates`,
Long: ` Long: `
Generate TLS-ECH certificates. Generate TLS-ECH certificates.
Set serverName to your custom string: {{.Exec}} tls ech --serverName (string) Set serverName to your custom string: {{.Exec}} tls ech --serverName (string)
Generate into json format: {{.Exec}} tls ech --json Generate into pem format: {{.Exec}} tls ech --pem
`, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled `, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled
} }
@@ -29,7 +27,7 @@ func init() {
var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "") var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "")
var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "") var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "")
var input_json = cmdECH.Flag.Bool("json", false, "True == turn on json output") var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output")
func executeECH(cmd *base.Command, args []string) { func executeECH(cmd *base.Command, args []string) {
var kem hpke.KEM var kem hpke.KEM
@@ -40,30 +38,26 @@ func executeECH(cmd *base.Command, args []string) {
kem = hpke.KEM_X25519_HKDF_SHA256 kem = hpke.KEM_X25519_HKDF_SHA256
} }
echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem) echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem, nil)
common.Must(err) common.Must(err)
configBuffer, _ := echKeySet.ECHConfig.MarshalBinary() // Make single key set to a list with only one element
keyBuffer, _ := echKeySet.MarshalBinary() ECHConfigList := make(goech.ECHConfigList, 1)
ECHConfigList[0] = echKeySet.ECHConfig
ECHKeySetList := make(goech.ECHKeySetList, 1)
ECHKeySetList[0] = echKeySet
configBuffer, _ := ECHConfigList.MarshalBinary()
keyBuffer, _ := ECHKeySetList.MarshalBinary()
configStr, _ := ECHConfigList.ToBase64()
keySetStr, _ := ECHKeySetList.ToBase64()
configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer}))
keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer}))
if *input_json { if *input_pem {
jECHConfigs := map[string]interface{}{
"configs": strings.Split(strings.TrimSpace(string(configPEM)), "\n"),
}
jECHKey := map[string]interface{}{
"key": strings.Split(strings.TrimSpace(string(keyPEM)), "\n"),
}
for _, i := range []map[string]interface{}{jECHConfigs, jECHKey} {
content, err := json.MarshalIndent(i, "", " ")
common.Must(err)
os.Stdout.Write(content)
os.Stdout.WriteString("\n")
}
} else {
os.Stdout.WriteString(configPEM) os.Stdout.WriteString(configPEM)
os.Stdout.WriteString(keyPEM) os.Stdout.WriteString(keyPEM)
} else {
os.Stdout.WriteString("ECH config list: \n" + configStr + "\n")
os.Stdout.WriteString("ECH Key sets: \n" + keySetStr + "\n")
} }
} }

View File

@@ -106,6 +106,14 @@ func lookupIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]
return ips, err return ips, err
} }
func LookupHTTPS(domain string) (map[string]string, error) {
if dnsClient == nil {
return nil, nil
}
HTTPSRecord, err := dnsClient.(dns.EnhancedClient).LookupHTTPS(domain)
return HTTPSRecord, err
}
func canLookupIP(ctx context.Context, dst net.Destination, sockopt *SocketConfig) bool { func canLookupIP(ctx context.Context, dst net.Destination, sockopt *SocketConfig) bool {
if dst.Address.Family().IsIP() || dnsClient == nil { if dst.Address.Family().IsIP() || dnsClient == nil {
return false return false

View File

@@ -444,6 +444,12 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
config.KeyLogWriter = writer config.KeyLogWriter = writer
} }
} }
if len(c.EchConfigList) > 0 || len(c.EchKeySets) > 0 {
err := ApplyECH(c, config)
if err != nil {
errors.LogError(context.Background(), err)
}
}
return config return config
} }

View File

@@ -217,6 +217,8 @@ type Config struct {
// @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried. // @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried.
// @Critical // @Critical
VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"`
EchConfigList string `protobuf:"bytes,18,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"`
EchKeySets []byte `protobuf:"bytes,19,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"`
} }
func (x *Config) Reset() { func (x *Config) Reset() {
@@ -361,6 +363,20 @@ func (x *Config) GetVerifyPeerCertInNames() []string {
return nil return nil
} }
func (x *Config) GetEchConfigList() string {
if x != nil {
return x.EchConfigList
}
return ""
}
func (x *Config) GetEchKeySets() []byte {
if x != nil {
return x.EchKeySets
}
return nil
}
var File_transport_internet_tls_config_proto protoreflect.FileDescriptor var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
var file_transport_internet_tls_config_proto_rawDesc = []byte{ var file_transport_internet_tls_config_proto_rawDesc = []byte{
@@ -392,7 +408,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a,
0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46,
0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x9a, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe4, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73,
0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c,
0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65,
@@ -442,15 +458,19 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f,
0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15,
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e,
0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x20, 0x0a,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x0c, 0x65, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x13, 0x20,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e,
0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74,
0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x6f, 0x33, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 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, 0x1b, 0x58, 0x72, 0x61, 0x79, 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 ( var (

View File

@@ -91,4 +91,8 @@ message Config {
@Critical @Critical
*/ */
repeated string verify_peer_cert_in_names = 17; repeated string verify_peer_cert_in_names = 17;
string ech_config_list = 18;
bytes ech_key_sets = 19;
} }

View File

@@ -0,0 +1,255 @@
package tls
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/OmarTariq612/goech"
"github.com/miekg/dns"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
)
func ApplyECH(c *Config, config *tls.Config) error {
var ECHConfig []byte
var err error
nameToQuery := c.ServerName
var DNSServer string
// for client
if len(c.EchConfigList) != 0 {
// direct base64 config
if strings.HasPrefix(c.EchConfigList, "base64") {
Base64ECHConfigList := c.EchConfigList[len("base64://"):]
ECHConfigList, err := goech.ECHConfigListFromBase64(Base64ECHConfigList)
if err != nil {
return errors.New("Failed to unmarshal ECHConfigList: ", err)
}
ECHConfig, _ = ECHConfigList.MarshalBinary()
} else { // query config from dns
parts := strings.Split(c.EchConfigList, "+")
if len(parts) == 2 {
// parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query"
nameToQuery = parts[0]
DNSServer = parts[1]
} else if len(parts) == 1 {
// normal format
DNSServer = parts[0]
} else {
return errors.New("Invalid ECH DNS server format: ", c.EchConfigList)
}
if nameToQuery == "" {
return errors.New("Using DNS for ECH Config needs serverName or use Server format example.com+https://1.1.1.1/dns-query")
}
ECHConfig, err = QueryRecord(nameToQuery, DNSServer)
if err != nil {
return err
}
}
config.EncryptedClientHelloConfigList = ECHConfig
}
// for server
if len(c.EchKeySets) != 0 {
var keys []tls.EncryptedClientHelloKey
KeySets, err := goech.UnmarshalECHKeySetList(c.EchKeySets)
if err != nil {
return errors.New("Failed to unmarshal ECHKeySetList: ", err)
}
for idx, keySet := range KeySets {
ECHConfig, err := keySet.ECHConfig.MarshalBinary()
ECHPrivateKey, err := keySet.PrivateKey.MarshalBinary()
if err != nil {
return errors.New("Failed to marshal ECHKey in index: ", idx, "with err: ", err)
}
keys = append(keys, tls.EncryptedClientHelloKey{
Config: ECHConfig,
PrivateKey: ECHPrivateKey})
}
config.EncryptedClientHelloKeys = keys
}
return nil
}
type record struct {
echConfig []byte
expire time.Time
}
var (
dnsCache sync.Map
// global Lock? I'm not sure if this needs finer grained locks.
// If we do this, we will need to nest another layer of struct
updating sync.Mutex
)
// 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) ([]byte, error) {
val, found := dnsCache.Load(domain)
rec, _ := val.(record)
if found && rec.expire.After(time.Now()) {
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
return rec.echConfig, nil
}
updating.Lock()
defer updating.Unlock()
// Try to get cache again after lock, in case another goroutine has updated it
// This might happen when the core tring is just stared and multiple goroutines are trying to query the same domain
val, found = dnsCache.Load(domain)
rec, _ = val.(record)
if found && rec.expire.After(time.Now()) {
errors.LogDebug(context.Background(), "ECH Config cache hit for domain: ", domain, " after trying to get update lock")
return rec.echConfig, nil
}
// 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)
if err != nil {
return []byte{}, err
}
// Set minimum TTL to 600 seconds
if ttl < 600 {
ttl = 600
}
// Update cache
newRecored := record{
echConfig: echConfig,
expire: time.Now().Add(time.Second * time.Duration(ttl)),
}
dnsCache.Store(domain, newRecored)
return echConfig, nil
}
// dnsQuery is the real func for sending type65 query for given domain to given DNS server.
// return ECH config, TTL and error
func dnsQuery(server string, domain string) ([]byte, uint32, error) {
if server == "xray" {
HTTPSRecord, err := internet.LookupHTTPS(domain)
if err !=nil {
return []byte{}, 0, errors.New("failed to lookup HTTPS record with xray internal DNS: ", err)
}
ECH := HTTPSRecord["ech"]
if ECH == "" {
return []byte{}, 0, errors.New("no ech record found")
}
Base64echConfigList, err := goech.ECHConfigListFromBase64(ECH)
if err != nil {
return []byte{}, 0, errors.New("failed to unmarshal ECHConfigList: ", err)
}
echConfigList, _ := Base64echConfigList.MarshalBinary()
return echConfigList, 600, nil
}
m := new(dns.Msg)
var dnsResolve []byte
m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS)
// for DOH server
if strings.HasPrefix(server, "https://") {
// always 0 in DOH
m.Id = 0
msg, err := m.Pack()
if err != nil {
return []byte{}, 0, err
}
// All traffic sent by core should via xray's internet.DialSystem
// This involves the behavior of some Android VPN GUI clients
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, errors.New("query failed with response code:", resp.StatusCode)
}
dnsResolve = respBody
} else if strings.HasPrefix(server, "udp://") { // for classic udp dns server
udpServerAddr := server[len("udp://"):]
// default port 53 if not specified
if !strings.Contains(udpServerAddr, ":") {
udpServerAddr = udpServerAddr + ":53"
}
dest, err := net.ParseDestination("udp" + ":" + udpServerAddr)
if err != nil {
return nil, 0, errors.New("failed to parse udp dns server ", udpServerAddr, " for ECH: ", err)
}
dnsTimeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// use xray's internet.DialSystem as mentioned above
conn, err := internet.DialSystem(dnsTimeoutCtx, dest, nil)
defer conn.Close()
if err != nil {
return []byte{}, 0, err
}
msg, err := m.Pack()
if err != nil {
return []byte{}, 0, err
}
conn.Write(msg)
udpResponse := make([]byte, 512)
_, err = conn.Read(udpResponse)
if err != nil {
return []byte{}, 0, err
}
dnsResolve = udpResponse
}
respMsg := new(dns.Msg)
err := respMsg.Unpack(dnsResolve)
if err != nil {
return []byte{}, 0, errors.New("failed to unpack dns response for ECH: ", 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 {
errors.LogDebug(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl)
return echConfig.ECH, answer.Header().Ttl, nil
}
}
}
}
}
return []byte{}, 0, errors.New("no ech record found")
}