Skip to content

Commit fe9b544

Browse files
authored
Add support for internal DNS system
1 parent 4999fd5 commit fe9b544

File tree

8 files changed

+249
-32
lines changed

8 files changed

+249
-32
lines changed

app/dns/dns.go

+28
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,34 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
210210
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
211211
}
212212

213+
func (s *DNS) LookupHTTPS(domain string) (map[string]string, error) {
214+
errs := []error{}
215+
ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag})
216+
for _, client := range s.sortClients(domain) {
217+
if strings.EqualFold(client.Name(), "FakeDNS") {
218+
errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name())
219+
continue
220+
}
221+
EnhancedServer, ok := client.server.(EnhancedServer)
222+
if !ok {
223+
continue
224+
}
225+
HTTPSRecord, err := EnhancedServer.QueryHTTPS(ctx, domain, s.disableCache)
226+
if len(HTTPSRecord) > 0 {
227+
return HTTPSRecord, nil
228+
}
229+
if err != nil {
230+
errors.LogInfoInner(s.ctx, err, "failed to lookup HTTPS for domain ", domain, " at server ", client.Name())
231+
errs = append(errs, err)
232+
}
233+
// 5 for RcodeRefused in miekg/dns, hardcode to reduce binary size
234+
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch && err != dns.ErrEmptyResponse && dns.RCodeFromError(err) != 5 {
235+
return nil, err
236+
}
237+
}
238+
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
239+
}
240+
213241
// LookupHosts implements dns.HostsLookup.
214242
func (s *DNS) LookupHosts(domain string) *net.Address {
215243
domain = strings.TrimSuffix(domain, ".")

app/dns/dnscommon.go

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ type record struct {
2929
AAAA *IPRecord
3030
}
3131

32+
type HTTPSRecord struct {
33+
keypair map[string]string
34+
Expire time.Time
35+
RCode dnsmessage.RCode
36+
}
37+
3238
// IPRecord is a cacheable item for a resolved domain
3339
type IPRecord struct {
3440
ReqID uint16

app/dns/nameserver.go

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ type Server interface {
2323
QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, error)
2424
}
2525

26+
// Server is the interface for Enhanced Name Server.
27+
type EnhancedServer interface {
28+
Server
29+
// QueryHTTPS sends HTTPS queries to its configured server.
30+
QueryHTTPS(ctx context.Context, domain string, disableCache bool) (map[string]string, error)
31+
}
32+
2633
// Client is the interface for DNS client.
2734
type Client struct {
2835
server Server

app/dns/nameserver_doh.go

+135
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sync"
1313
"time"
1414

15+
mdns "github.com/miekg/dns"
1516
utls "github.com/refraction-networking/utls"
1617
"github.com/xtls/xray-core/common"
1718
"github.com/xtls/xray-core/common/crypto"
@@ -42,6 +43,8 @@ type DoHNameServer struct {
4243
dohURL string
4344
name string
4445
queryStrategy QueryStrategy
46+
47+
HTTPSCache map[string]*HTTPSRecord
4548
}
4649

4750
// NewDoHNameServer creates DOH/DOHL client object for remote/local resolving.
@@ -58,6 +61,7 @@ func NewDoHNameServer(url *url.URL, queryStrategy QueryStrategy, dispatcher rout
5861
name: mode + "//" + url.Host,
5962
dohURL: url.String(),
6063
queryStrategy: queryStrategy,
64+
HTTPSCache: make(map[string]*HTTPSRecord),
6165
}
6266
s.cleanup = &task.Periodic{
6367
Interval: time.Minute,
@@ -207,6 +211,21 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
207211
common.Must(s.cleanup.Start())
208212
}
209213

214+
func (s *DoHNameServer) updateHTTPS(domain string, HTTPSRec *HTTPSRecord) {
215+
s.Lock()
216+
rec, found := s.HTTPSCache[domain]
217+
if !found {
218+
s.HTTPSCache[domain] = HTTPSRec
219+
}
220+
if found && rec.Expire.Before(time.Now()) {
221+
s.HTTPSCache[domain] = HTTPSRec
222+
}
223+
errors.LogInfo(context.Background(), s.name, " got answer: ", domain, " ", "HTTPS", " -> ", HTTPSRec.keypair)
224+
225+
s.pub.Publish(domain+"HTTPS", nil)
226+
s.Unlock()
227+
}
228+
210229
func (s *DoHNameServer) newReqID() uint16 {
211230
return 0
212231
}
@@ -271,6 +290,59 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP n
271290
}
272291
}
273292

293+
func (s *DoHNameServer) sendHTTPSQuery(ctx context.Context, domain string) {
294+
errors.LogInfo(ctx, s.name, " querying HTTPS record for: ", domain)
295+
var deadline time.Time
296+
if d, ok := ctx.Deadline(); ok {
297+
deadline = d
298+
} else {
299+
deadline = time.Now().Add(time.Second * 5)
300+
}
301+
dnsCtx := ctx
302+
// reserve internal dns server requested Inbound
303+
if inbound := session.InboundFromContext(ctx); inbound != nil {
304+
dnsCtx = session.ContextWithInbound(dnsCtx, inbound)
305+
}
306+
dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{
307+
Protocol: "https",
308+
SkipDNSResolve: true,
309+
})
310+
var cancel context.CancelFunc
311+
dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline)
312+
defer cancel()
313+
314+
m := new(mdns.Msg)
315+
m.SetQuestion(mdns.Fqdn(domain), mdns.TypeHTTPS)
316+
m.Id = 0
317+
msg, _ := m.Pack()
318+
response, err := s.dohHTTPSContext(dnsCtx, msg)
319+
if err != nil {
320+
errors.LogError(ctx, err, "failed to retrieve HTTPS query response for ", domain)
321+
return
322+
}
323+
respMsg := new(mdns.Msg)
324+
err = respMsg.Unpack(response)
325+
if err != nil {
326+
errors.LogError(ctx, err, "failed to parse HTTPS query response for ", domain)
327+
return
328+
}
329+
var Record = HTTPSRecord{
330+
keypair: map[string]string{},
331+
}
332+
if len(respMsg.Answer) > 0 {
333+
for _, answer := range respMsg.Answer {
334+
if https, ok := answer.(*mdns.HTTPS); ok && https.Hdr.Name == mdns.Fqdn(domain) {
335+
for _, value := range https.Value {
336+
Record.keypair[value.Key().String()] = value.String()
337+
}
338+
}
339+
}
340+
}
341+
Record.Expire = time.Now().Add(time.Duration(respMsg.Answer[0].Header().Ttl) * time.Second)
342+
343+
s.updateHTTPS(domain, &Record)
344+
}
345+
274346
func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) {
275347
body := bytes.NewBuffer(b)
276348
req, err := http.NewRequest("POST", s.dohURL, body)
@@ -341,6 +413,27 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt
341413
return nil, errRecordNotFound
342414
}
343415

416+
func (s *DoHNameServer) findRecordsForDomain(domain string, Querytype string) (any, error) {
417+
switch Querytype {
418+
case "HTTPS":
419+
s.RLock()
420+
record, found := s.HTTPSCache[domain]
421+
s.RUnlock()
422+
if !found {
423+
return nil, errRecordNotFound
424+
}
425+
if len(record.keypair) == 0 {
426+
return nil, dns_feature.ErrEmptyResponse
427+
}
428+
if record.Expire.Before(time.Now()) {
429+
return nil, errRecordNotFound
430+
}
431+
return record, nil
432+
default:
433+
return nil, errors.New("unsupported query type: " + Querytype)
434+
}
435+
}
436+
344437
// QueryIP implements Server.
345438
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl
346439
fqdn := Fqdn(domain)
@@ -403,3 +496,45 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net
403496
}
404497
}
405498
}
499+
500+
// QueryHTTPS implements EnhancedServer.
501+
func (s *DoHNameServer) QueryHTTPS(ctx context.Context, domain string, disableCache bool) (map[string]string, error) { // nolint: dupl
502+
fqdn := Fqdn(domain)
503+
504+
if disableCache {
505+
errors.LogDebug(ctx, "DNS cache is disabled. Querying HTTPS for ", domain, " at ", s.name)
506+
} else {
507+
Record, err := s.findRecordsForDomain(fqdn, "HTTPS")
508+
if err == nil || err == dns_feature.ErrEmptyResponse {
509+
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", Record.(HTTPSRecord).keypair)
510+
return Record.(HTTPSRecord).keypair, err
511+
}
512+
}
513+
sub := s.pub.Subscribe(fqdn + "HTTPS")
514+
defer sub.Close()
515+
done := make(chan interface{})
516+
go func() {
517+
if sub != nil {
518+
select {
519+
case <-sub.Wait():
520+
case <-ctx.Done():
521+
}
522+
}
523+
close(done)
524+
}()
525+
s.sendHTTPSQuery(ctx, fqdn)
526+
527+
for {
528+
Record, err := s.findRecordsForDomain(fqdn, "HTTPS")
529+
if err != errRecordNotFound {
530+
errors.LogDebug(ctx, s.name, " got HTTPS answer: ", domain, " -> ", Record.(*HTTPSRecord).keypair)
531+
return Record.(*HTTPSRecord).keypair, err
532+
}
533+
534+
select {
535+
case <-ctx.Done():
536+
return nil, ctx.Err()
537+
case <-done:
538+
}
539+
}
540+
}

features/dns/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type Client interface {
2222

2323
// LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses.
2424
LookupIP(domain string, option IPOption) ([]net.IP, error)
25+
// LooupHTTPS returns HTTPS records for the given domain.
26+
LookupHTTPS(domain string) (map[string]string, error)
2527
}
2628

2729
type HostsLookup interface {

testing/mocks/dns.go

+47-32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

transport/internet/dialer.go

+8
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ func lookupIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]
106106
return ips, err
107107
}
108108

109+
func LookupHTTPS(domain string) (map[string]string, error) {
110+
if dnsClient == nil {
111+
return nil, nil
112+
}
113+
HTTPSRecord, err := dnsClient.LookupHTTPS(domain)
114+
return HTTPSRecord, err
115+
}
116+
109117
func canLookupIP(ctx context.Context, dst net.Destination, sockopt *SocketConfig) bool {
110118
if dst.Address.Family().IsIP() || dnsClient == nil {
111119
return false

0 commit comments

Comments
 (0)