Browse Source

Refactoring: DNS App (#169)

Co-authored-by: loyalsoldier <10487845+Loyalsoldier@users.noreply.github.com>
Ye Zhihao 4 years ago
parent
commit
d8c03f10b5

+ 65 - 0
app/dns/config.go

@@ -0,0 +1,65 @@
+// +build !confonly
+
+package dns
+
+import (
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/strmatcher"
+	"v2ray.com/core/common/uuid"
+)
+
+var typeMap = map[DomainMatchingType]strmatcher.Type{
+	DomainMatchingType_Full:      strmatcher.Full,
+	DomainMatchingType_Subdomain: strmatcher.Domain,
+	DomainMatchingType_Keyword:   strmatcher.Substr,
+	DomainMatchingType_Regex:     strmatcher.Regex,
+}
+
+// References:
+// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
+// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
+var localTLDsAndDotlessDomains = []*NameServer_PriorityDomain{
+	{Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot
+	{Type: DomainMatchingType_Subdomain, Domain: "local"},
+	{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
+	{Type: DomainMatchingType_Subdomain, Domain: "localhost"},
+	{Type: DomainMatchingType_Subdomain, Domain: "lan"},
+	{Type: DomainMatchingType_Subdomain, Domain: "home.arpa"},
+	{Type: DomainMatchingType_Subdomain, Domain: "example"},
+	{Type: DomainMatchingType_Subdomain, Domain: "invalid"},
+	{Type: DomainMatchingType_Subdomain, Domain: "test"},
+}
+
+var localTLDsAndDotlessDomainsRule = &NameServer_OriginalRule{
+	Rule: "geosite:private",
+	Size: uint32(len(localTLDsAndDotlessDomains)),
+}
+
+func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
+	strMType, f := typeMap[t]
+	if !f {
+		return nil, newError("unknown mapping type", t).AtWarning()
+	}
+	matcher, err := strMType.New(domain)
+	if err != nil {
+		return nil, newError("failed to create str matcher").Base(err)
+	}
+	return matcher, nil
+}
+
+func toNetIP(addrs []net.Address) ([]net.IP, error) {
+	ips := make([]net.IP, 0, len(addrs))
+	for _, addr := range addrs {
+		if addr.Family().IsIP() {
+			ips = append(ips, addr.IP())
+		} else {
+			return nil, newError("Failed to convert address", addr, "to Net IP.").AtWarning()
+		}
+	}
+	return ips, nil
+}
+
+func generateRandomTag() string {
+	id := uuid.New()
+	return "v2ray.system." + id.String()
+}

+ 245 - 0
app/dns/dns.go

@@ -1,4 +1,249 @@
+// +build !confonly
+
 // Package dns is an implementation of core.DNS feature.
 package dns
 
 //go:generate go run v2ray.com/core/common/errors/errorgen
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	"v2ray.com/core/app/router"
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/errors"
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/session"
+	"v2ray.com/core/common/strmatcher"
+	"v2ray.com/core/features"
+	"v2ray.com/core/features/dns"
+)
+
+// DNS is a DNS rely server.
+type DNS struct {
+	sync.Mutex
+	tag     string
+	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))
+	}
+
+	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) 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, updateDomain)
+		if err != nil {
+			return nil, newError("failed to create client").Base(err)
+		}
+		clients = append(clients, client)
+	}
+
+	if len(clients) == 0 {
+		clients = append(clients, NewLocalDNSClient())
+	}
+
+	return &DNS{
+		tag:           tag,
+		hosts:         hosts,
+		clients:       clients,
+		domainMatcher: domainMatcher,
+		matcherInfos:  matcherInfos,
+	}, 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, IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	})
+}
+
+// LookupIPv4 implements dns.IPv4Lookup.
+func (s *DNS) LookupIPv4(domain string) ([]net.IP, error) {
+	return s.lookupIPInternal(domain, IPOption{
+		IPv4Enable: true,
+		IPv6Enable: false,
+	})
+}
+
+// LookupIPv6 implements dns.IPv6Lookup.
+func (s *DNS) LookupIPv6(domain string) ([]net.IP, error) {
+	return s.lookupIPInternal(domain, IPOption{
+		IPv4Enable: false,
+		IPv6Enable: true,
+	})
+}
+
+func (s *DNS) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) {
+	if domain == "" {
+		return nil, newError("empty domain name")
+	}
+
+	// Normalize the FQDN form query
+	if domain[len(domain)-1] == '.' {
+		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), " IPs for domain ", domain).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) {
+		ips, err := client.QueryIP(ctx, domain, option)
+		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) []*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
+	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())
+	}
+
+	// Default round-robin query
+	for idx, client := range s.clients {
+		if clientUsed[idx] {
+			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()
+	}
+	return clients
+}
+
+func init() {
+	common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+		return New(ctx, config.(*Config))
+	}))
+}

+ 0 - 1
app/dns/server_test.go → app/dns/dns_test.go

@@ -6,7 +6,6 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/miekg/dns"
-
 	"v2ray.com/core"
 	"v2ray.com/core/app/dispatcher"
 	. "v2ray.com/core/app/dns"

+ 25 - 33
app/dns/hosts.go

@@ -15,25 +15,6 @@ type StaticHosts struct {
 	matchers *strmatcher.MatcherGroup
 }
 
-var typeMap = map[DomainMatchingType]strmatcher.Type{
-	DomainMatchingType_Full:      strmatcher.Full,
-	DomainMatchingType_Subdomain: strmatcher.Domain,
-	DomainMatchingType_Keyword:   strmatcher.Substr,
-	DomainMatchingType_Regex:     strmatcher.Regex,
-}
-
-func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
-	strMType, f := typeMap[t]
-	if !f {
-		return nil, newError("unknown mapping type", t).AtWarning()
-	}
-	matcher, err := strMType.New(domain)
-	if err != nil {
-		return nil, newError("failed to create str matcher").Base(err)
-	}
-	return matcher, nil
-}
-
 // NewStaticHosts creates a new StaticHosts instance.
 func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDomain) (*StaticHosts, error) {
 	g := new(strmatcher.MatcherGroup)
@@ -101,24 +82,35 @@ func filterIP(ips []net.Address, option IPOption) []net.Address {
 			filtered = append(filtered, ip)
 		}
 	}
-	if len(filtered) == 0 {
-		return nil
-	}
 	return filtered
 }
 
-// LookupIP returns IP address for the given domain, if exists in this StaticHosts.
-func (h *StaticHosts) LookupIP(domain string, option IPOption) []net.Address {
-	indices := h.matchers.Match(domain)
-	if len(indices) == 0 {
-		return nil
-	}
-	ips := []net.Address{}
-	for _, id := range indices {
+func (h *StaticHosts) lookupInternal(domain string) []net.Address {
+	var ips []net.Address
+	for _, id := range h.matchers.Match(domain) {
 		ips = append(ips, h.ips[id]...)
 	}
-	if len(ips) == 1 && ips[0].Family().IsDomain() {
-		return ips
+	return ips
+}
+
+func (h *StaticHosts) lookup(domain string, option IPOption, maxDepth int) []net.Address {
+	switch addrs := h.lookupInternal(domain); {
+	case len(addrs) == 0: // Not recorded in static hosts, return nil
+		return nil
+	case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
+		if maxDepth > 0 {
+			unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1)
+			if unwrapped != nil {
+				return unwrapped
+			}
+		}
+		return addrs
+	default: // IP record found, return a non-nil IP array
+		return filterIP(addrs, option)
 	}
-	return filterIP(ips, option)
+}
+
+// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
+func (h *StaticHosts) Lookup(domain string, option IPOption) []net.Address {
+	return h.lookup(domain, option, 5)
 }

+ 3 - 4
app/dns/hosts_test.go

@@ -4,7 +4,6 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-
 	. "v2ray.com/core/app/dns"
 	"v2ray.com/core/common"
 	"v2ray.com/core/common/net"
@@ -39,7 +38,7 @@ func TestStaticHosts(t *testing.T) {
 	common.Must(err)
 
 	{
-		ips := hosts.LookupIP("v2ray.com", IPOption{
+		ips := hosts.Lookup("v2ray.com", IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -52,7 +51,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("www.v2ray.cn", IPOption{
+		ips := hosts.Lookup("www.v2ray.cn", IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -65,7 +64,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("baidu.com", IPOption{
+		ips := hosts.Lookup("baidu.com", IPOption{
 			IPv4Enable: false,
 			IPv6Enable: true,
 		})

+ 168 - 22
app/dns/nameserver.go

@@ -4,9 +4,15 @@ package dns
 
 import (
 	"context"
+	"net/url"
+	"time"
 
+	"v2ray.com/core"
+	"v2ray.com/core/app/router"
+	"v2ray.com/core/common/errors"
 	"v2ray.com/core/common/net"
-	"v2ray.com/core/features/dns/localdns"
+	"v2ray.com/core/common/strmatcher"
+	"v2ray.com/core/features/routing"
 )
 
 // IPOption is an object for IP query options.
@@ -15,42 +21,182 @@ type IPOption struct {
 	IPv6Enable bool
 }
 
-// Client is the interface for DNS client.
-type Client interface {
+// Server is the interface for Name Server.
+type Server interface {
 	// Name of the Client.
 	Name() string
-
 	// QueryIP sends IP queries to its configured server.
-	QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error)
+	QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error)
 }
 
-type LocalNameServer struct {
-	client *localdns.Client
+// Client is the interface for DNS client.
+type Client struct {
+	server    Server
+	clientIP  net.IP
+	domains   []string
+	expectIPs []*router.GeoIPMatcher
 }
 
-func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
-	if option.IPv4Enable && option.IPv6Enable {
-		return s.client.LookupIP(domain)
-	}
+var errExpectedIPNonMatch = errors.New("expectIPs not match")
 
-	if option.IPv4Enable {
-		return s.client.LookupIPv4(domain)
+// NewServer creates a name server object according to the network destination url.
+func NewServer(dest net.Destination, dispatcher routing.Dispatcher) (Server, error) {
+	if address := dest.Address; address.Family().IsDomain() {
+		u, err := url.Parse(address.Domain())
+		if err != nil {
+			return nil, err
+		}
+		switch {
+		case u.String() == "localhost":
+			return NewLocalNameServer(), nil
+		case u.Scheme == "https": // DOH Remote mode
+			return NewDoHNameServer(u, dispatcher)
+		case u.Scheme == "https+local": // DOH Local mode
+			return NewDoHLocalNameServer(u), nil
+		}
+	}
+	if dest.Network == net.Network_Unknown {
+		dest.Network = net.Network_UDP
+	}
+	if dest.Network == net.Network_UDP { // UDP classic DNS mode
+		return NewClassicNameServer(dest, dispatcher), nil
 	}
+	return nil, newError("No available name server could be created from ", dest).AtWarning()
+}
+
+// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs.
+func NewClient(ctx context.Context, ns *NameServer, clientIP net.IP, container router.GeoIPMatcherContainer, updateDomainRule func(strmatcher.Matcher, int) error) (*Client, error) {
+	client := &Client{}
+	err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
+		// Create a new server for each client for now
+		server, err := NewServer(ns.Address.AsDestination(), dispatcher)
+		if err != nil {
+			return newError("failed to create nameserver").Base(err).AtWarning()
+		}
+
+		// Priotize local domains with specific TLDs or without any dot to local DNS
+		if _, isLocalDNS := server.(*LocalNameServer); isLocalDNS {
+			ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
+			ns.OriginalRules = append(ns.OriginalRules, localTLDsAndDotlessDomainsRule)
+		}
+
+		// Establish domain rules
+		var rules []string
+		ruleCurr := 0
+		ruleIter := 0
+		for _, domain := range ns.PrioritizedDomain {
+			domainRule, err := toStrMatcher(domain.Type, domain.Domain)
+			if err != nil {
+				return newError("failed to create prioritized domain").Base(err).AtWarning()
+			}
+			originalRuleIdx := ruleCurr
+			if ruleCurr < len(ns.OriginalRules) {
+				rule := ns.OriginalRules[ruleCurr]
+				if ruleCurr >= len(rules) {
+					rules = append(rules, rule.Rule)
+				}
+				ruleIter++
+				if ruleIter >= int(rule.Size) {
+					ruleIter = 0
+					ruleCurr++
+				}
+			} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
+				rules = append(rules, domainRule.String())
+				ruleCurr++
+			}
+			err = updateDomainRule(domainRule, originalRuleIdx)
+			if err != nil {
+				return newError("failed to create prioritized domain").Base(err).AtWarning()
+			}
+		}
+
+		// Establish expected IPs
+		var matchers []*router.GeoIPMatcher
+		for _, geoip := range ns.Geoip {
+			matcher, err := container.Add(geoip)
+			if err != nil {
+				return newError("failed to create ip matcher").Base(err).AtWarning()
+			}
+			matchers = append(matchers, matcher)
+		}
 
-	if option.IPv6Enable {
-		return s.client.LookupIPv6(domain)
+		if len(clientIP) > 0 {
+			switch ns.Address.Address.GetAddress().(type) {
+			case *net.IPOrDomain_Domain:
+				newError("DNS: client ", ns.Address.Address.GetDomain(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
+			case *net.IPOrDomain_Ip:
+				newError("DNS: client ", ns.Address.Address.GetIp(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
+			}
+		}
+
+		client.server = server
+		client.clientIP = clientIP
+		client.domains = rules
+		client.expectIPs = matchers
+		return nil
+	})
+	return client, err
+}
+
+// NewSimpleClient creates a DNS client with a simple destination.
+func NewSimpleClient(ctx context.Context, endpoint *net.Endpoint, clientIP net.IP) (*Client, error) {
+	client := &Client{}
+	err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
+		server, err := NewServer(endpoint.AsDestination(), dispatcher)
+		if err != nil {
+			return newError("failed to create nameserver").Base(err).AtWarning()
+		}
+		client.server = server
+		client.clientIP = clientIP
+		return nil
+	})
+
+	if len(clientIP) > 0 {
+		switch endpoint.Address.GetAddress().(type) {
+		case *net.IPOrDomain_Domain:
+			newError("DNS: client ", endpoint.Address.GetDomain(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
+		case *net.IPOrDomain_Ip:
+			newError("DNS: client ", endpoint.Address.GetIp(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
+		}
 	}
 
-	return nil, newError("neither IPv4 nor IPv6 is enabled")
+	return client, err
+}
+
+// Name returns the server name the client manages.
+func (c *Client) Name() string {
+	return c.server.Name()
 }
 
-func (s *LocalNameServer) Name() string {
-	return "localhost"
+// QueryIP send DNS query to the name server with the client's IP.
+func (c *Client) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
+	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
+	ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option)
+	cancel()
+
+	if err != nil {
+		return ips, err
+	}
+	return c.MatchExpectedIPs(domain, ips)
 }
 
-func NewLocalNameServer() *LocalNameServer {
-	newError("DNS: created localhost client").AtInfo().WriteToLog()
-	return &LocalNameServer{
-		client: localdns.New(),
+// MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones.
+func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error) {
+	if len(c.expectIPs) == 0 {
+		return ips, nil
+	}
+	newIps := []net.IP{}
+	for _, ip := range ips {
+		for _, matcher := range c.expectIPs {
+			if matcher.Match(ip) {
+				newIps = append(newIps, ip)
+				break
+			}
+		}
+	}
+	if len(newIps) == 0 {
+		return nil, errExpectedIPNonMatch
 	}
+	newError("domain ", domain, " expectIPs ", newIps, " matched at server ", c.Name()).AtDebug().WriteToLog()
+	return newIps, nil
 }

+ 17 - 26
app/dns/dohdns.go → app/dns/nameserver_doh.go

@@ -35,19 +35,15 @@ type DoHNameServer struct {
 	pub        *pubsub.Service
 	cleanup    *task.Periodic
 	reqID      uint32
-	clientIP   net.IP
 	httpClient *http.Client
 	dohURL     string
 	name       string
 }
 
-// NewDoHNameServer creates DOH client object for remote resolving
-func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.IP) (*DoHNameServer, error) {
+// NewDoHNameServer creates DOH server object for remote resolving.
+func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher) (*DoHNameServer, error) {
 	newError("DNS: created Remote DOH client for ", url.String()).AtInfo().WriteToLog()
-	if clientIP != nil {
-		newError("DNS: Remote DOH client ", url.String(), " uses clientip ", clientIP.String()).AtInfo().WriteToLog()
-	}
-	s := baseDOHNameServer(url, "DOH", clientIP)
+	s := baseDOHNameServer(url, "DOH")
 
 	// Dispatched connection will be closed (interrupted) after each request
 	// This makes DOH inefficient without a keep-alived connection
@@ -87,9 +83,9 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.
 }
 
 // NewDoHLocalNameServer creates DOH client object for local resolving
-func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer {
+func NewDoHLocalNameServer(url *url.URL) *DoHNameServer {
 	url.Scheme = "https"
-	s := baseDOHNameServer(url, "DOHL", clientIP)
+	s := baseDOHNameServer(url, "DOHL")
 	tr := &http.Transport{
 		IdleConnTimeout:   90 * time.Second,
 		ForceAttemptHTTP2: true,
@@ -110,29 +106,24 @@ func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer {
 		Transport: tr,
 	}
 	newError("DNS: created Local DOH client for ", url.String()).AtInfo().WriteToLog()
-	if clientIP != nil {
-		newError("DNS: Local DOH client ", url.String(), " uses clientip ", clientIP.String()).AtInfo().WriteToLog()
-	}
 	return s
 }
 
-func baseDOHNameServer(url *url.URL, prefix string, clientIP net.IP) *DoHNameServer {
+func baseDOHNameServer(url *url.URL, prefix string) *DoHNameServer {
 	s := &DoHNameServer{
-		ips:      make(map[string]record),
-		clientIP: clientIP,
-		pub:      pubsub.NewService(),
-		name:     prefix + "//" + url.Host,
-		dohURL:   url.String(),
+		ips:    make(map[string]record),
+		pub:    pubsub.NewService(),
+		name:   prefix + "//" + url.Host,
+		dohURL: url.String(),
 	}
 	s.cleanup = &task.Periodic{
 		Interval: time.Minute,
 		Execute:  s.Cleanup,
 	}
-
 	return s
 }
 
-// Name returns client name
+// Name implements Server.
 func (s *DoHNameServer) Name() string {
 	return s.name
 }
@@ -215,10 +206,10 @@ func (s *DoHNameServer) newReqID() uint16 {
 	return uint16(atomic.AddUint32(&s.reqID, 1))
 }
 
-func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
+func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) {
 	newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx))
 
-	reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP))
+	reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
 
 	var deadline time.Time
 	if d, ok := ctx.Deadline(); ok {
@@ -322,7 +313,7 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.
 	}
 
 	if len(ips) > 0 {
-		return toNetIP(ips), nil
+		return toNetIP(ips)
 	}
 
 	if lastErr != nil {
@@ -336,8 +327,8 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.
 	return nil, errRecordNotFound
 }
 
-// QueryIP is called from dns.Server->queryIPTimeout
-func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
+// QueryIP implements Server.
+func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) { // nolint: dupl
 	fqdn := Fqdn(domain)
 
 	ips, err := s.findIPsForDomain(fqdn, option)
@@ -372,7 +363,7 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOpt
 		}
 		close(done)
 	}()
-	s.sendQuery(ctx, fqdn, option)
+	s.sendQuery(ctx, fqdn, clientIP, option)
 
 	for {
 		ips, err := s.findIPsForDomain(fqdn, option)

+ 50 - 0
app/dns/nameserver_local.go

@@ -0,0 +1,50 @@
+// +build !confonly
+
+package dns
+
+import (
+	"context"
+
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/features/dns/localdns"
+)
+
+// LocalNameServer is an wrapper over local DNS feature.
+type LocalNameServer struct {
+	client *localdns.Client
+}
+
+// QueryIP implements Server.
+func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) {
+	if option.IPv4Enable && option.IPv6Enable {
+		return s.client.LookupIP(domain)
+	}
+
+	if option.IPv4Enable {
+		return s.client.LookupIPv4(domain)
+	}
+
+	if option.IPv6Enable {
+		return s.client.LookupIPv6(domain)
+	}
+
+	return nil, newError("neither IPv4 nor IPv6 is enabled")
+}
+
+// Name implements Server.
+func (s *LocalNameServer) Name() string {
+	return "localhost"
+}
+
+// NewLocalNameServer creates localdns server object for directly lookup in system DNS.
+func NewLocalNameServer() *LocalNameServer {
+	newError("DNS: created localhost client").AtInfo().WriteToLog()
+	return &LocalNameServer{
+		client: localdns.New(),
+	}
+}
+
+// NewLocalDNSClient creates localdns client object for directly lookup in system DNS.
+func NewLocalDNSClient() *Client {
+	return &Client{server: NewLocalNameServer()}
+}

+ 2 - 1
app/dns/nameserver_test.go → app/dns/nameserver_local_test.go

@@ -7,12 +7,13 @@ import (
 
 	. "v2ray.com/core/app/dns"
 	"v2ray.com/core/common"
+	"v2ray.com/core/common/net"
 )
 
 func TestLocalNameServer(t *testing.T) {
 	s := NewLocalNameServer()
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
-	ips, err := s.QueryIP(ctx, "google.com", IPOption{
+	ips, err := s.QueryIP(ctx, "google.com", net.IP{}, IPOption{
 		IPv4Enable: true,
 		IPv6Enable: true,
 	})

+ 12 - 11
app/dns/udpns.go → app/dns/nameserver_udp.go

@@ -22,6 +22,7 @@ import (
 	"v2ray.com/core/transport/internet/udp"
 )
 
+// ClassicNameServer implemented traditional UDP DNS.
 type ClassicNameServer struct {
 	sync.RWMutex
 	name      string
@@ -32,10 +33,10 @@ type ClassicNameServer struct {
 	udpServer *udp.Dispatcher
 	cleanup   *task.Periodic
 	reqID     uint32
-	clientIP  net.IP
 }
 
-func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, clientIP net.IP) *ClassicNameServer {
+// NewClassicNameServer creates udp server object for remote resolving.
+func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher) *ClassicNameServer {
 	// default to 53 if unspecific
 	if address.Port == 0 {
 		address.Port = net.Port(53)
@@ -45,7 +46,6 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
 		address:  address,
 		ips:      make(map[string]record),
 		requests: make(map[uint16]dnsRequest),
-		clientIP: clientIP,
 		pub:      pubsub.NewService(),
 		name:     strings.ToUpper(address.String()),
 	}
@@ -55,16 +55,15 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
 	}
 	s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse)
 	newError("DNS: created UDP client inited for ", address.NetAddr()).AtInfo().WriteToLog()
-	if clientIP != nil {
-		newError("DNS: UDP client ", address.NetAddr(), " uses clientip ", clientIP.String()).AtInfo().WriteToLog()
-	}
 	return s
 }
 
+// Name implements Server.
 func (s *ClassicNameServer) Name() string {
 	return s.name
 }
 
+// Cleanup clears expired items from cache
 func (s *ClassicNameServer) Cleanup() error {
 	now := time.Now()
 	s.Lock()
@@ -106,6 +105,7 @@ func (s *ClassicNameServer) Cleanup() error {
 	return nil
 }
 
+// HandleResponse handles udp response packet from remote DNS server.
 func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) {
 	ipRec, err := parseResponse(packet.Payload.Bytes())
 	if err != nil {
@@ -183,10 +183,10 @@ func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) {
 	s.requests[id] = *req
 }
 
-func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
+func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) {
 	newError(s.name, " querying DNS for: ", domain).AtDebug().WriteToLog(session.ExportIDToError(ctx))
 
-	reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP))
+	reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
 
 	for _, req := range reqs {
 		s.addPendingRequest(req)
@@ -230,7 +230,7 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]
 	}
 
 	if len(ips) > 0 {
-		return toNetIP(ips), nil
+		return toNetIP(ips)
 	}
 
 	if lastErr != nil {
@@ -240,7 +240,8 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]
 	return nil, dns_feature.ErrEmptyResponse
 }
 
-func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
+// QueryIP implements Server.
+func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) {
 	fqdn := Fqdn(domain)
 
 	ips, err := s.findIPsForDomain(fqdn, option)
@@ -275,7 +276,7 @@ func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option I
 		}
 		close(done)
 	}()
-	s.sendQuery(ctx, fqdn, option)
+	s.sendQuery(ctx, fqdn, clientIP, option)
 
 	for {
 		ips, err := s.findIPsForDomain(fqdn, option)

+ 0 - 456
app/dns/server.go

@@ -1,456 +0,0 @@
-// +build !confonly
-
-package dns
-
-//go:generate go run v2ray.com/core/common/errors/errorgen
-
-import (
-	"context"
-	"fmt"
-	"log"
-	"net/url"
-	"strings"
-	"sync"
-	"time"
-
-	"v2ray.com/core"
-	"v2ray.com/core/app/router"
-	"v2ray.com/core/common"
-	"v2ray.com/core/common/errors"
-	"v2ray.com/core/common/net"
-	"v2ray.com/core/common/session"
-	"v2ray.com/core/common/strmatcher"
-	"v2ray.com/core/common/uuid"
-	"v2ray.com/core/features"
-	"v2ray.com/core/features/dns"
-	"v2ray.com/core/features/routing"
-)
-
-// Server is a DNS rely server.
-type Server struct {
-	sync.Mutex
-	hosts         *StaticHosts
-	clientIP      net.IP
-	clients       []Client             // clientIdx -> Client
-	ipIndexMap    []*MultiGeoIPMatcher // clientIdx -> *MultiGeoIPMatcher
-	domainRules   [][]string           // clientIdx -> domainRuleIdx -> DomainRule
-	domainMatcher strmatcher.IndexMatcher
-	matcherInfos  []DomainMatcherInfo // matcherIdx -> DomainMatcherInfo
-	tag           string
-}
-
-// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
-type DomainMatcherInfo struct {
-	clientIdx     uint16
-	domainRuleIdx uint16
-}
-
-// MultiGeoIPMatcher for match
-type MultiGeoIPMatcher struct {
-	matchers []*router.GeoIPMatcher
-}
-
-var errExpectedIPNonMatch = errors.New("expectIPs not match")
-
-// Match check ip match
-func (c *MultiGeoIPMatcher) Match(ip net.IP) bool {
-	for _, matcher := range c.matchers {
-		if matcher.Match(ip) {
-			return true
-		}
-	}
-	return false
-}
-
-// HasMatcher check has matcher
-func (c *MultiGeoIPMatcher) HasMatcher() bool {
-	return len(c.matchers) > 0
-}
-
-func generateRandomTag() string {
-	id := uuid.New()
-	return "v2ray.system." + id.String()
-}
-
-// New creates a new DNS server with given configuration.
-func New(ctx context.Context, config *Config) (*Server, error) {
-	server := &Server{
-		clients: make([]Client, 0, len(config.NameServers)+len(config.NameServer)),
-		tag:     config.Tag,
-	}
-	if server.tag == "" {
-		server.tag = generateRandomTag()
-	}
-	if len(config.ClientIp) > 0 {
-		if len(config.ClientIp) != net.IPv4len && len(config.ClientIp) != net.IPv6len {
-			return nil, newError("unexpected IP length", len(config.ClientIp))
-		}
-		server.clientIP = net.IP(config.ClientIp)
-	}
-
-	hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts)
-	if err != nil {
-		return nil, newError("failed to create hosts").Base(err)
-	}
-	server.hosts = hosts
-
-	addNameServer := func(ns *NameServer) int {
-		endpoint := ns.Address
-		address := endpoint.Address.AsAddress()
-
-		var myClientIP net.IP
-		if len(ns.ClientIp) == net.IPv4len || len(ns.ClientIp) == net.IPv6len {
-			myClientIP = net.IP(ns.ClientIp)
-		} else {
-			myClientIP = server.clientIP
-		}
-
-		switch {
-		case address.Family().IsDomain() && address.Domain() == "localhost":
-			server.clients = append(server.clients, NewLocalNameServer())
-			// Priotize local domains with specific TLDs or without any dot to local DNS
-			// References:
-			// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
-			// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
-			localTLDsAndDotlessDomains := []*NameServer_PriorityDomain{
-				{Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot
-				{Type: DomainMatchingType_Subdomain, Domain: "local"},
-				{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
-				{Type: DomainMatchingType_Subdomain, Domain: "localhost"},
-				{Type: DomainMatchingType_Subdomain, Domain: "lan"},
-				{Type: DomainMatchingType_Subdomain, Domain: "home.arpa"},
-				{Type: DomainMatchingType_Subdomain, Domain: "example"},
-				{Type: DomainMatchingType_Subdomain, Domain: "invalid"},
-				{Type: DomainMatchingType_Subdomain, Domain: "test"},
-			}
-			ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
-
-		case address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https+local://"):
-			// URI schemed string treated as domain
-			// DOH Local mode
-			u, err := url.Parse(address.Domain())
-			if err != nil {
-				log.Fatalln(newError("DNS config error").Base(err))
-			}
-			server.clients = append(server.clients, NewDoHLocalNameServer(u, myClientIP))
-
-		case address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https://"):
-			// DOH Remote mode
-			u, err := url.Parse(address.Domain())
-			if err != nil {
-				log.Fatalln(newError("DNS config error").Base(err))
-			}
-			idx := len(server.clients)
-			server.clients = append(server.clients, nil)
-
-			// need the core dispatcher, register DOHClient at callback
-			common.Must(core.RequireFeatures(ctx, func(d routing.Dispatcher) {
-				c, err := NewDoHNameServer(u, d, myClientIP)
-				if err != nil {
-					log.Fatalln(newError("DNS config error").Base(err))
-				}
-				server.clients[idx] = c
-			}))
-
-		default:
-			// UDP classic DNS mode
-			dest := endpoint.AsDestination()
-			if dest.Network == net.Network_Unknown {
-				dest.Network = net.Network_UDP
-			}
-			if dest.Network == net.Network_UDP {
-				idx := len(server.clients)
-				server.clients = append(server.clients, nil)
-
-				common.Must(core.RequireFeatures(ctx, func(d routing.Dispatcher) {
-					server.clients[idx] = NewClassicNameServer(dest, d, myClientIP)
-				}))
-			}
-		}
-		server.ipIndexMap = append(server.ipIndexMap, nil)
-		return len(server.clients) - 1
-	}
-
-	if len(config.NameServers) > 0 {
-		features.PrintDeprecatedFeatureWarning("simple DNS server")
-		for _, destPB := range config.NameServers {
-			addNameServer(&NameServer{Address: destPB})
-		}
-	}
-
-	if len(config.NameServer) > 0 {
-		clientIndices := []int{}
-		domainRuleCount := 0
-		for _, ns := range config.NameServer {
-			idx := addNameServer(ns)
-			clientIndices = append(clientIndices, idx)
-			domainRuleCount += len(ns.PrioritizedDomain)
-		}
-
-		domainRules := make([][]string, len(server.clients))
-		domainMatcher := &strmatcher.MatcherGroup{}
-		matcherInfos := make([]DomainMatcherInfo, domainRuleCount+1) // matcher index starts from 1
-		var geoIPMatcherContainer router.GeoIPMatcherContainer
-		for nidx, ns := range config.NameServer {
-			idx := clientIndices[nidx]
-
-			// Establish domain rule matcher
-			rules := []string{}
-			ruleCurr := 0
-			ruleIter := 0
-			for _, domain := range ns.PrioritizedDomain {
-				matcher, err := toStrMatcher(domain.Type, domain.Domain)
-				if err != nil {
-					return nil, newError("failed to create prioritized domain").Base(err).AtWarning()
-				}
-				midx := domainMatcher.Add(matcher)
-				if midx >= uint32(len(matcherInfos)) { // This rarely happens according to current matcher's implementation
-					newError("expanding domain matcher info array to size ", midx, " when adding ", matcher).AtDebug().WriteToLog()
-					matcherInfos = append(matcherInfos, make([]DomainMatcherInfo, midx-uint32(len(matcherInfos))+1)...)
-				}
-				info := &matcherInfos[midx]
-				info.clientIdx = uint16(idx)
-				if ruleCurr < len(ns.OriginalRules) {
-					info.domainRuleIdx = uint16(ruleCurr)
-					rule := ns.OriginalRules[ruleCurr]
-					if ruleCurr >= len(rules) {
-						rules = append(rules, rule.Rule)
-					}
-					ruleIter++
-					if ruleIter >= int(rule.Size) {
-						ruleIter = 0
-						ruleCurr++
-					}
-				} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
-					info.domainRuleIdx = uint16(len(rules))
-					rules = append(rules, matcher.String())
-				}
-			}
-			domainRules[idx] = rules
-
-			// only add to ipIndexMap if GeoIP is configured
-			if len(ns.Geoip) > 0 {
-				var matchers []*router.GeoIPMatcher
-				for _, geoip := range ns.Geoip {
-					matcher, err := geoIPMatcherContainer.Add(geoip)
-					if err != nil {
-						return nil, newError("failed to create ip matcher").Base(err).AtWarning()
-					}
-					matchers = append(matchers, matcher)
-				}
-				matcher := &MultiGeoIPMatcher{matchers: matchers}
-				server.ipIndexMap[idx] = matcher
-			}
-		}
-		server.domainRules = domainRules
-		server.domainMatcher = domainMatcher
-		server.matcherInfos = matcherInfos
-	}
-
-	if len(server.clients) == 0 {
-		server.clients = append(server.clients, NewLocalNameServer())
-		server.ipIndexMap = append(server.ipIndexMap, nil)
-	}
-
-	return server, nil
-}
-
-// Type implements common.HasType.
-func (*Server) Type() interface{} {
-	return dns.ClientType()
-}
-
-// Start implements common.Runnable.
-func (s *Server) Start() error {
-	return nil
-}
-
-// Close implements common.Closable.
-func (s *Server) Close() error {
-	return nil
-}
-
-func (s *Server) IsOwnLink(ctx context.Context) bool {
-	inbound := session.InboundFromContext(ctx)
-	return inbound != nil && inbound.Tag == s.tag
-}
-
-// Match check dns ip match geoip
-func (s *Server) Match(idx int, client Client, domain string, ips []net.IP) ([]net.IP, error) {
-	var matcher *MultiGeoIPMatcher
-	if idx < len(s.ipIndexMap) {
-		matcher = s.ipIndexMap[idx]
-	}
-	if matcher == nil {
-		return ips, nil
-	}
-
-	if !matcher.HasMatcher() {
-		newError("domain ", domain, " server has no valid matcher: ", client.Name(), " idx:", idx).AtDebug().WriteToLog()
-		return ips, nil
-	}
-
-	newIps := []net.IP{}
-	for _, ip := range ips {
-		if matcher.Match(ip) {
-			newIps = append(newIps, ip)
-		}
-	}
-	if len(newIps) == 0 {
-		return nil, errExpectedIPNonMatch
-	}
-	newError("domain ", domain, " expectIPs ", newIps, " matched at server ", client.Name(), " idx:", idx).AtDebug().WriteToLog()
-	return newIps, nil
-}
-
-func (s *Server) queryIPTimeout(idx int, client Client, domain string, option IPOption) ([]net.IP, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
-	if len(s.tag) > 0 {
-		ctx = session.ContextWithInbound(ctx, &session.Inbound{
-			Tag: s.tag,
-		})
-	}
-	ips, err := client.QueryIP(ctx, domain, option)
-	cancel()
-
-	if err != nil {
-		return ips, err
-	}
-
-	ips, err = s.Match(idx, client, domain, ips)
-	return ips, err
-}
-
-// LookupIP implements dns.Client.
-func (s *Server) LookupIP(domain string) ([]net.IP, error) {
-	return s.lookupIPInternal(domain, IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-	})
-}
-
-// LookupIPv4 implements dns.IPv4Lookup.
-func (s *Server) LookupIPv4(domain string) ([]net.IP, error) {
-	return s.lookupIPInternal(domain, IPOption{
-		IPv4Enable: true,
-		IPv6Enable: false,
-	})
-}
-
-// LookupIPv6 implements dns.IPv6Lookup.
-func (s *Server) LookupIPv6(domain string) ([]net.IP, error) {
-	return s.lookupIPInternal(domain, IPOption{
-		IPv4Enable: false,
-		IPv6Enable: true,
-	})
-}
-
-func (s *Server) lookupStatic(domain string, option IPOption, depth int32) []net.Address {
-	ips := s.hosts.LookupIP(domain, option)
-	if ips == nil {
-		return nil
-	}
-	if ips[0].Family().IsDomain() && depth < 5 {
-		if newIPs := s.lookupStatic(ips[0].Domain(), option, depth+1); newIPs != nil {
-			return newIPs
-		}
-	}
-	return ips
-}
-
-func toNetIP(ips []net.Address) []net.IP {
-	if len(ips) == 0 {
-		return nil
-	}
-	netips := make([]net.IP, 0, len(ips))
-	for _, ip := range ips {
-		netips = append(netips, ip.IP())
-	}
-	return netips
-}
-
-func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) {
-	if domain == "" {
-		return nil, newError("empty domain name")
-	}
-
-	// normalize the FQDN form query
-	if domain[len(domain)-1] == '.' {
-		domain = domain[:len(domain)-1]
-	}
-
-	ips := s.lookupStatic(domain, option, 0)
-	if ips != nil && ips[0].Family().IsIP() {
-		newError("returning ", len(ips), " IPs for domain ", domain).WriteToLog()
-		return toNetIP(ips), nil
-	}
-
-	if ips != nil && ips[0].Family().IsDomain() {
-		newdomain := ips[0].Domain()
-		newError("domain replaced: ", domain, " -> ", newdomain).WriteToLog()
-		domain = newdomain
-	}
-
-	var lastErr error
-	var matchedClient Client
-	if s.domainMatcher != nil {
-		indices := s.domainMatcher.Match(domain)
-		domainRules := []string{}
-		matchingDNS := []string{}
-		for _, idx := range indices {
-			info := s.matcherInfos[idx]
-			rule := s.domainRules[info.clientIdx][info.domainRuleIdx]
-			domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", rule, info.clientIdx))
-			matchingDNS = append(matchingDNS, s.clients[info.clientIdx].Name())
-		}
-		if len(domainRules) > 0 {
-			newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog()
-		}
-		if len(matchingDNS) > 0 {
-			newError("domain ", domain, " uses following DNS first: ", matchingDNS).AtDebug().WriteToLog()
-		}
-		for _, idx := range indices {
-			clientIdx := int(s.matcherInfos[idx].clientIdx)
-			matchedClient = s.clients[clientIdx]
-			ips, err := s.queryIPTimeout(clientIdx, matchedClient, domain, option)
-			if len(ips) > 0 {
-				return ips, nil
-			}
-			if err == dns.ErrEmptyResponse {
-				return nil, err
-			}
-			if err != nil {
-				newError("failed to lookup ip for domain ", domain, " at server ", matchedClient.Name()).Base(err).WriteToLog()
-				lastErr = err
-			}
-		}
-	}
-
-	for idx, client := range s.clients {
-		if client == matchedClient {
-			newError("domain ", domain, " at server ", client.Name(), " idx:", idx, " already lookup failed, just ignore").AtDebug().WriteToLog()
-			continue
-		}
-
-		ips, err := s.queryIPTimeout(idx, client, domain, option)
-		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()
-			lastErr = err
-		}
-		if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch {
-			return nil, err
-		}
-	}
-
-	return nil, newError("returning nil for domain ", domain).Base(lastErr)
-}
-
-func init() {
-	common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
-		return New(ctx, config.(*Config))
-	}))
-}