mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-08-22 09:36:49 +08:00
Compare commits
12 Commits
v25.3.6
...
dev-ECH-in
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5f504888b6 | ||
![]() |
4999fd5b7b | ||
![]() |
7f6a825bfe | ||
![]() |
6d5be86947 | ||
![]() |
0923f53b21 | ||
![]() |
8554549f2c | ||
![]() |
cab4321956 | ||
![]() |
26199629f7 | ||
![]() |
53ceaf87a5 | ||
![]() |
73e98665ac | ||
![]() |
52c46bc508 | ||
![]() |
9bdf866471 |
@@ -85,7 +85,6 @@
|
||||
- [X-flutter](https://github.com/XTLS/X-flutter)
|
||||
- [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray)
|
||||
- iOS & macOS arm64
|
||||
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215)
|
||||
- [FoXray](https://apps.apple.com/app/foxray/id6448898396)
|
||||
- [Streisand](https://apps.apple.com/app/streisand/id6450534064)
|
||||
- macOS arm64 & x64
|
||||
|
@@ -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...))
|
||||
}
|
||||
|
||||
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.
|
||||
func (s *DNS) LookupHosts(domain string) *net.Address {
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
|
@@ -29,6 +29,12 @@ type record struct {
|
||||
AAAA *IPRecord
|
||||
}
|
||||
|
||||
type HTTPSRecord struct {
|
||||
keypair map[string]string
|
||||
Expire time.Time
|
||||
RCode dnsmessage.RCode
|
||||
}
|
||||
|
||||
// IPRecord is a cacheable item for a resolved domain
|
||||
type IPRecord struct {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Client struct {
|
||||
server Server
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
mdns "github.com/miekg/dns"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/crypto"
|
||||
@@ -42,6 +43,8 @@ type DoHNameServer struct {
|
||||
dohURL string
|
||||
name string
|
||||
queryStrategy QueryStrategy
|
||||
|
||||
HTTPSCache map[string]*HTTPSRecord
|
||||
}
|
||||
|
||||
// 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,
|
||||
dohURL: url.String(),
|
||||
queryStrategy: queryStrategy,
|
||||
HTTPSCache: make(map[string]*HTTPSRecord),
|
||||
}
|
||||
s.cleanup = &task.Periodic{
|
||||
Interval: time.Minute,
|
||||
@@ -207,6 +211,21 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
|
||||
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 {
|
||||
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) {
|
||||
body := bytes.NewBuffer(b)
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ import (
|
||||
var (
|
||||
Version_x byte = 25
|
||||
Version_y byte = 3
|
||||
Version_z byte = 6
|
||||
Version_z byte = 3
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -24,6 +24,13 @@ type Client interface {
|
||||
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 {
|
||||
LookupHosts(domain string) *net.Address
|
||||
}
|
||||
|
16
go.mod
16
go.mod
@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
|
||||
go 1.24
|
||||
|
||||
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/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
@@ -22,12 +22,12 @@ require (
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/net v0.37.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
google.golang.org/grpc v1.71.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0
|
||||
h12.io/socks v1.0.3
|
||||
@@ -51,11 +51,11 @@ require (
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
54
go.sum
54
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
|
||||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
|
||||
github.com/OmarTariq612/goech v0.0.1 h1:/0c918Bk1ik65GXDj2k7SOK78DyZr30Jmq9euy1/HXg=
|
||||
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/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
@@ -79,26 +79,24 @@ github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZla
|
||||
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg=
|
||||
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
@@ -107,12 +105,12 @@ golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -121,14 +119,14 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -143,10 +141,10 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
@@ -412,6 +412,8 @@ type TLSConfig struct {
|
||||
MasterKeyLog string `json:"masterKeyLog"`
|
||||
ServerNameToVerify string `json:"serverNameToVerify"`
|
||||
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
|
||||
ECHConfigList string `json:"echConfigList"`
|
||||
EchKeySets string `json:"echKeySets"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
@@ -483,6 +485,16 @@ func (c *TLSConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,8 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/OmarTariq612/goech"
|
||||
"github.com/cloudflare/circl/hpke"
|
||||
@@ -13,13 +11,13 @@ import (
|
||||
)
|
||||
|
||||
var cmdECH = &base.Command{
|
||||
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--json]`,
|
||||
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem]`,
|
||||
Short: `Generate TLS-ECH certificates`,
|
||||
Long: `
|
||||
Generate TLS-ECH certificates.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -29,7 +27,7 @@ func init() {
|
||||
|
||||
var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "")
|
||||
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) {
|
||||
var kem hpke.KEM
|
||||
@@ -40,30 +38,26 @@ func executeECH(cmd *base.Command, args []string) {
|
||||
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)
|
||||
|
||||
configBuffer, _ := echKeySet.ECHConfig.MarshalBinary()
|
||||
keyBuffer, _ := echKeySet.MarshalBinary()
|
||||
// Make single key set to a list with only one element
|
||||
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}))
|
||||
keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer}))
|
||||
if *input_json {
|
||||
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 {
|
||||
if *input_pem {
|
||||
os.Stdout.WriteString(configPEM)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if dst.Address.Family().IsIP() || dnsClient == nil {
|
||||
return false
|
||||
|
@@ -444,6 +444,12 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
|
||||
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
|
||||
}
|
||||
|
@@ -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.
|
||||
// @Critical
|
||||
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() {
|
||||
@@ -361,6 +363,20 @@ func (x *Config) GetVerifyPeerCertInNames() []string {
|
||||
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_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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
@@ -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,
|
||||
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,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61,
|
||||
0x79, 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, 0x30, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 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,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e,
|
||||
0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
|
||||
0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x20, 0x0a,
|
||||
0x0c, 0x65, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x13, 0x20,
|
||||
0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42,
|
||||
0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 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, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
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 (
|
||||
|
@@ -91,4 +91,8 @@ message Config {
|
||||
@Critical
|
||||
*/
|
||||
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