mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-11-26 15:16:03 +03:00
cd4631ce99
* DNS: add clientip for specific nameserver * Refactoring: DNS App * DNS: add DNS over QUIC support * Feat: add disableCache option for DNS * Feat: add queryStrategy option for DNS * Feat: add disableFallback & skipFallback option for DNS * Feat: DNS hosts support multiple addresses * Feat: DNS transport over TCP * DNS: fix typo & refine code * DNS: refine code * Add disableFallbackIfMatch dns option * Feat: routing and freedom outbound ignore Fake DNS Turn off fake DNS for request sent from Routing and Freedom outbound. Fake DNS now only apply to DNS outbound. This is important for Android, where VPN service take over all system DNS traffic and pass it to core. "UseIp" option can be used in Freedom outbound to avoid getting fake IP and fail connection. * Fix test * Fix dns return * Fix local dns return empty * Apply timeout to dns outbound * Update app/dns/config.go Co-authored-by: Loyalsoldier <10487845+loyalsoldier@users.noreply.github.com> Co-authored-by: Ye Zhihao <vigilans@foxmail.com> Co-authored-by: maskedeken <52683904+maskedeken@users.noreply.github.com> Co-authored-by: V2Fly Team <51714622+vcptr@users.noreply.github.com> Co-authored-by: CalmLong <37164399+calmlong@users.noreply.github.com> Co-authored-by: Shelikhoo <xiaokangwang@outlook.com> Co-authored-by: 秋のかえで <autmaple@protonmail.com> Co-authored-by: 朱聖黎 <digglife@gmail.com> Co-authored-by: rurirei <72071920+rurirei@users.noreply.github.com> Co-authored-by: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Co-authored-by: Arthur Morgan <4637240+badO1a5A90@users.noreply.github.com>
295 lines
8.5 KiB
Go
295 lines
8.5 KiB
Go
// Package dns is an implementation of core.DNS feature.
|
|
package dns
|
|
|
|
//go:generate go run github.com/xtls/xray-core/common/errors/errorgen
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/xtls/xray-core/app/router"
|
|
"github.com/xtls/xray-core/common"
|
|
"github.com/xtls/xray-core/common/errors"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/common/session"
|
|
"github.com/xtls/xray-core/common/strmatcher"
|
|
"github.com/xtls/xray-core/features"
|
|
"github.com/xtls/xray-core/features/dns"
|
|
)
|
|
|
|
// DNS is a DNS rely server.
|
|
type DNS struct {
|
|
sync.Mutex
|
|
tag string
|
|
disableCache bool
|
|
disableFallback bool
|
|
disableFallbackIfMatch bool
|
|
ipOption *dns.IPOption
|
|
hosts *StaticHosts
|
|
clients []*Client
|
|
domainMatcher strmatcher.IndexMatcher
|
|
matcherInfos []*DomainMatcherInfo
|
|
}
|
|
|
|
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
|
|
type DomainMatcherInfo struct {
|
|
clientIdx uint16
|
|
domainRuleIdx uint16
|
|
}
|
|
|
|
// New creates a new DNS server with given configuration.
|
|
func New(ctx context.Context, config *Config) (*DNS, error) {
|
|
var tag string
|
|
if len(config.Tag) > 0 {
|
|
tag = config.Tag
|
|
} else {
|
|
tag = generateRandomTag()
|
|
}
|
|
|
|
var clientIP net.IP
|
|
switch len(config.ClientIp) {
|
|
case 0, net.IPv4len, net.IPv6len:
|
|
clientIP = net.IP(config.ClientIp)
|
|
default:
|
|
return nil, newError("unexpected client IP length ", len(config.ClientIp))
|
|
}
|
|
|
|
var ipOption *dns.IPOption
|
|
switch config.QueryStrategy {
|
|
case QueryStrategy_USE_IP:
|
|
ipOption = &dns.IPOption{
|
|
IPv4Enable: true,
|
|
IPv6Enable: true,
|
|
FakeEnable: false,
|
|
}
|
|
case QueryStrategy_USE_IP4:
|
|
ipOption = &dns.IPOption{
|
|
IPv4Enable: true,
|
|
IPv6Enable: false,
|
|
FakeEnable: false,
|
|
}
|
|
case QueryStrategy_USE_IP6:
|
|
ipOption = &dns.IPOption{
|
|
IPv4Enable: false,
|
|
IPv6Enable: true,
|
|
FakeEnable: false,
|
|
}
|
|
}
|
|
|
|
hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts)
|
|
if err != nil {
|
|
return nil, newError("failed to create hosts").Base(err)
|
|
}
|
|
|
|
clients := []*Client{}
|
|
domainRuleCount := 0
|
|
for _, ns := range config.NameServer {
|
|
domainRuleCount += len(ns.PrioritizedDomain)
|
|
}
|
|
|
|
// MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1
|
|
matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1)
|
|
domainMatcher := &strmatcher.MatcherGroup{}
|
|
geoipContainer := router.GeoIPMatcherContainer{}
|
|
|
|
for _, endpoint := range config.NameServers {
|
|
features.PrintDeprecatedFeatureWarning("simple DNS server")
|
|
client, err := NewSimpleClient(ctx, endpoint, clientIP)
|
|
if err != nil {
|
|
return nil, newError("failed to create client").Base(err)
|
|
}
|
|
clients = append(clients, client)
|
|
}
|
|
|
|
for _, ns := range config.NameServer {
|
|
clientIdx := len(clients)
|
|
updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) error {
|
|
midx := domainMatcher.Add(domainRule)
|
|
matcherInfos[midx] = &DomainMatcherInfo{
|
|
clientIdx: uint16(clientIdx),
|
|
domainRuleIdx: uint16(originalRuleIdx),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
myClientIP := clientIP
|
|
switch len(ns.ClientIp) {
|
|
case net.IPv4len, net.IPv6len:
|
|
myClientIP = net.IP(ns.ClientIp)
|
|
}
|
|
client, err := NewClient(ctx, ns, myClientIP, geoipContainer, &matcherInfos, updateDomain)
|
|
if err != nil {
|
|
return nil, newError("failed to create client").Base(err)
|
|
}
|
|
clients = append(clients, client)
|
|
}
|
|
|
|
// If there is no DNS client in config, add a `localhost` DNS client
|
|
if len(clients) == 0 {
|
|
clients = append(clients, NewLocalDNSClient())
|
|
}
|
|
|
|
return &DNS{
|
|
tag: tag,
|
|
hosts: hosts,
|
|
ipOption: ipOption,
|
|
clients: clients,
|
|
domainMatcher: domainMatcher,
|
|
matcherInfos: matcherInfos,
|
|
disableCache: config.DisableCache,
|
|
disableFallback: config.DisableFallback,
|
|
disableFallbackIfMatch: config.DisableFallbackIfMatch,
|
|
}, nil
|
|
}
|
|
|
|
// Type implements common.HasType.
|
|
func (*DNS) Type() interface{} {
|
|
return dns.ClientType()
|
|
}
|
|
|
|
// Start implements common.Runnable.
|
|
func (s *DNS) Start() error {
|
|
return nil
|
|
}
|
|
|
|
// Close implements common.Closable.
|
|
func (s *DNS) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// IsOwnLink implements proxy.dns.ownLinkVerifier
|
|
func (s *DNS) IsOwnLink(ctx context.Context) bool {
|
|
inbound := session.InboundFromContext(ctx)
|
|
return inbound != nil && inbound.Tag == s.tag
|
|
}
|
|
|
|
// LookupIP implements dns.Client.
|
|
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
|
|
if domain == "" {
|
|
return nil, newError("empty domain name")
|
|
}
|
|
|
|
option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable
|
|
option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable
|
|
|
|
if !option.IPv4Enable && !option.IPv6Enable {
|
|
return nil, dns.ErrEmptyResponse
|
|
}
|
|
|
|
// Normalize the FQDN form query
|
|
if strings.HasSuffix(domain, ".") {
|
|
domain = domain[:len(domain)-1]
|
|
}
|
|
|
|
// Static host lookup
|
|
switch addrs := s.hosts.Lookup(domain, option); {
|
|
case addrs == nil: // Domain not recorded in static host
|
|
break
|
|
case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled)
|
|
return nil, dns.ErrEmptyResponse
|
|
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement
|
|
newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog()
|
|
domain = addrs[0].Domain()
|
|
default: // Successfully found ip records in static host
|
|
newError("returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs).WriteToLog()
|
|
return toNetIP(addrs)
|
|
}
|
|
|
|
// Name servers lookup
|
|
errs := []error{}
|
|
ctx := session.ContextWithInbound(context.Background(), &session.Inbound{Tag: s.tag})
|
|
for _, client := range s.sortClients(domain) {
|
|
if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") {
|
|
newError("skip DNS resolution for domain ", domain, " at server ", client.Name()).AtDebug().WriteToLog()
|
|
continue
|
|
}
|
|
ips, err := client.QueryIP(ctx, domain, option, s.disableCache)
|
|
if len(ips) > 0 {
|
|
return ips, nil
|
|
}
|
|
if err != nil {
|
|
newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog()
|
|
errs = append(errs, err)
|
|
}
|
|
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return nil, newError("returning nil for domain ", domain).Base(errors.Combine(errs...))
|
|
}
|
|
|
|
// GetIPOption implements ClientWithIPOption.
|
|
func (s *DNS) GetIPOption() *dns.IPOption {
|
|
return s.ipOption
|
|
}
|
|
|
|
// SetQueryOption implements ClientWithIPOption.
|
|
func (s *DNS) SetQueryOption(isIPv4Enable, isIPv6Enable bool) {
|
|
s.ipOption.IPv4Enable = isIPv4Enable
|
|
s.ipOption.IPv6Enable = isIPv6Enable
|
|
}
|
|
|
|
// SetFakeDNSOption implements ClientWithIPOption.
|
|
func (s *DNS) SetFakeDNSOption(isFakeEnable bool) {
|
|
s.ipOption.FakeEnable = isFakeEnable
|
|
}
|
|
|
|
func (s *DNS) sortClients(domain string) []*Client {
|
|
clients := make([]*Client, 0, len(s.clients))
|
|
clientUsed := make([]bool, len(s.clients))
|
|
clientNames := make([]string, 0, len(s.clients))
|
|
domainRules := []string{}
|
|
|
|
// Priority domain matching
|
|
hasMatch := false
|
|
for _, match := range s.domainMatcher.Match(domain) {
|
|
info := s.matcherInfos[match]
|
|
client := s.clients[info.clientIdx]
|
|
domainRule := client.domains[info.domainRuleIdx]
|
|
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
|
if clientUsed[info.clientIdx] {
|
|
continue
|
|
}
|
|
clientUsed[info.clientIdx] = true
|
|
clients = append(clients, client)
|
|
clientNames = append(clientNames, client.Name())
|
|
hasMatch = true
|
|
}
|
|
|
|
if !(s.disableFallback || s.disableFallbackIfMatch && hasMatch) {
|
|
// Default round-robin query
|
|
for idx, client := range s.clients {
|
|
if clientUsed[idx] || client.skipFallback {
|
|
continue
|
|
}
|
|
clientUsed[idx] = true
|
|
clients = append(clients, client)
|
|
clientNames = append(clientNames, client.Name())
|
|
}
|
|
}
|
|
|
|
if len(domainRules) > 0 {
|
|
newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog()
|
|
}
|
|
if len(clientNames) > 0 {
|
|
newError("domain ", domain, " will use DNS in order: ", clientNames).AtDebug().WriteToLog()
|
|
}
|
|
|
|
if len(clients) == 0 {
|
|
clients = append(clients, s.clients[0])
|
|
clientNames = append(clientNames, s.clients[0].Name())
|
|
newError("domain ", domain, " will use the first DNS: ", clientNames).AtDebug().WriteToLog()
|
|
}
|
|
|
|
return clients
|
|
}
|
|
|
|
func init() {
|
|
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
|
return New(ctx, config.(*Config))
|
|
}))
|
|
}
|