// 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/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/matcher/geoip" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/session" "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 cacheStrategy CacheStrategy disableFallback bool ipOption *dns.IPOption hosts *StaticHosts clients []*Client ctx context.Context } // 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) } geoipContainer := geoip.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 { myClientIP := clientIP switch len(ns.ClientIp) { case net.IPv4len, net.IPv6len: myClientIP = ns.ClientIp } client, err := NewClient(ctx, ns, myClientIP, geoipContainer) 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, ctx: ctx, cacheStrategy: config.CacheStrategy, disableFallback: config.DisableFallback, }, 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) ([]net.IP, error) { return s.lookupIPInternal(domain, s.ipOption.Copy()) } // LookupOptions implements dns.Client. func (s *DNS) LookupOptions(domain string, opts ...dns.Option) ([]net.IP, error) { opt := s.ipOption.Copy() for _, o := range opts { if o != nil { o(opt) } } return s.lookupIPInternal(domain, opt) } // LookupIPv4 implements dns.IPv4Lookup. func (s *DNS) LookupIPv4(domain string) ([]net.IP, error) { return s.lookupIPInternal(domain, &dns.IPOption{ IPv4Enable: true, }) } // LookupIPv6 implements dns.IPv6Lookup. func (s *DNS) LookupIPv6(domain string) ([]net.IP, error) { return s.lookupIPInternal(domain, &dns.IPOption{ IPv6Enable: true, }) } func (s *DNS) lookupIPInternal(domain string, option *dns.IPOption) ([]net.IP, error) { if domain == "" { return nil, newError("empty domain name") } if isQuery(option) { return nil, newError("empty option: Impossible.").AtWarning() } // 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. // Skip hosts mapping result in FakeDNS query. if isIPQuery(option) { newError("returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs).WriteToLog() return toNetIP(addrs) } } // Name servers lookup errs := []error{} ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag}) for _, client := range s.sortClients(domain, option) { ips, err := client.QueryIP(ctx, domain, *option, s.cacheStrategy) 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...)) } func (s *DNS) sortClients(domain string, option *dns.IPOption) []*Client { clients := make([]*Client, 0, len(s.clients)) clientUsed := make([]bool, len(s.clients)) clientNames := make([]string, 0, len(s.clients)) domainRules := []string{} defer func() { 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() } }() // Priority domain matching for clientIdx, client := range s.clients { if ids := client.domainMatcher.Match(domain); len(ids) > 0 { if !canQueryOnClient(option, client) { newError("skipping the client " + client.Name()).AtDebug().WriteToLog() continue } for _, id := range ids { rule := client.findRule(id) domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", rule, clientIdx)) } if clientUsed[clientIdx] { continue } clients = append(clients, client) clientNames = append(clientNames, client.Name()) } } if !s.disableFallback { // Default round-robin query for idx, client := range s.clients { if clientUsed[idx] || client.skipFallback { continue } if !canQueryOnClient(option, client) { newError("skipping the client " + client.Name()).AtDebug().WriteToLog() continue } clientUsed[idx] = true clients = append(clients, client) clientNames = append(clientNames, client.Name()) } } return clients } func init() { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { return New(ctx, config.(*Config)) })) }