mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-08-23 01:56:48 +08:00
Compare commits
12 Commits
7f23a1cb65
...
dev-ECH-in
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5f504888b6 | ||
![]() |
4999fd5b7b | ||
![]() |
7f6a825bfe | ||
![]() |
6d5be86947 | ||
![]() |
0923f53b21 | ||
![]() |
8554549f2c | ||
![]() |
cab4321956 | ||
![]() |
26199629f7 | ||
![]() |
53ceaf87a5 | ||
![]() |
73e98665ac | ||
![]() |
52c46bc508 | ||
![]() |
9bdf866471 |
@@ -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, ".")
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
255
transport/internet/tls/ech.go
Normal file
255
transport/internet/tls/ech.go
Normal 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")
|
||||||
|
}
|
Reference in New Issue
Block a user