浏览代码

refactor: new dns app

AkinoKaede 4 年之前
父节点
当前提交
8884e948fe

+ 1 - 6
app/dispatcher/default.go

@@ -309,15 +309,10 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (Sni
 func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) {
 	var handler outbound.Handler
 
-	skipRoutePick := false
-	if content := session.ContentFromContext(ctx); content != nil {
-		skipRoutePick = content.SkipRoutePick
-	}
-
 	routingLink := routing_session.AsRoutingContext(ctx)
 	inTag := routingLink.GetInboundTag()
 	isPickRoute := false
-	if d.router != nil && !skipRoutePick {
+	if d.router != nil {
 		if route, err := d.router.PickRoute(routingLink); err == nil {
 			outTag := route.GetOutboundTag()
 			isPickRoute = true

+ 63 - 0
app/dns/config.go

@@ -0,0 +1,63 @@
+package dns
+
+import (
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/strmatcher"
+	"github.com/xtls/xray-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 "xray.system." + id.String()
+}

+ 91 - 69
app/dns/config.pb.go

@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.25.0
-// 	protoc        v3.14.0
+// 	protoc        v3.15.6
 // source: app/dns/config.proto
 
 package dns
@@ -85,6 +85,7 @@ type NameServer struct {
 	unknownFields protoimpl.UnknownFields
 
 	Address           *net.Endpoint                `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
+	ClientIp          []byte                       `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"`
 	PrioritizedDomain []*NameServer_PriorityDomain `protobuf:"bytes,2,rep,name=prioritized_domain,json=prioritizedDomain,proto3" json:"prioritized_domain,omitempty"`
 	Geoip             []*router.GeoIP              `protobuf:"bytes,3,rep,name=geoip,proto3" json:"geoip,omitempty"`
 	OriginalRules     []*NameServer_OriginalRule   `protobuf:"bytes,4,rep,name=original_rules,json=originalRules,proto3" json:"original_rules,omitempty"`
@@ -129,6 +130,13 @@ func (x *NameServer) GetAddress() *net.Endpoint {
 	return nil
 }
 
+func (x *NameServer) GetClientIp() []byte {
+	if x != nil {
+		return x.ClientIp
+	}
+	return nil
+}
+
 func (x *NameServer) GetPrioritizedDomain() []*NameServer_PriorityDomain {
 	if x != nil {
 		return x.PrioritizedDomain
@@ -174,6 +182,8 @@ type Config struct {
 	StaticHosts []*Config_HostMapping `protobuf:"bytes,4,rep,name=static_hosts,json=staticHosts,proto3" json:"static_hosts,omitempty"`
 	// Tag is the inbound tag of DNS client.
 	Tag string `protobuf:"bytes,6,opt,name=tag,proto3" json:"tag,omitempty"`
+	// DisableCache Disable DNS cache
+	DisableCache bool `protobuf:"varint,8,opt,name=disableCache,proto3" json:"disableCache,omitempty"`
 }
 
 func (x *Config) Reset() {
@@ -252,6 +262,13 @@ func (x *Config) GetTag() string {
 	return ""
 }
 
+func (x *Config) GetDisableCache() bool {
+	if x != nil {
+		return x.DisableCache
+	}
+	return false
+}
+
 type NameServer_PriorityDomain struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -446,77 +463,82 @@ var file_app_dns_config_proto_rawDesc = []byte{
 	0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69,
 	0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70,
 	0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xad, 0x03, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x03, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
 	0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18,
 	0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d,
 	0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
-	0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x56, 0x0a, 0x12, 0x70, 0x72, 0x69,
-	0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
-	0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
-	0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e,
-	0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x11,
-	0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69,
-	0x6e, 0x12, 0x2c, 0x0a, 0x05, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
-	0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74,
-	0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x05, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x12,
-	0x4c, 0x0a, 0x0e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6c, 0x65,
-	0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
-	0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65,
-	0x72, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d,
-	0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x1a, 0x5e, 0x0a,
-	0x0e, 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
-	0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e,
-	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d,
-	0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52,
-	0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a,
-	0x0c, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a,
-	0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c,
-	0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
-	0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x9f, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
-	0x12, 0x3f, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d,
-	0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
-	0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
-	0x73, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
-	0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
-	0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
-	0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x05,
-	0x48, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72,
-	0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
-	0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x02, 0x18, 0x01,
-	0x52, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65,
-	0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x68,
-	0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61,
-	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
-	0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74,
-	0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67,
-	0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x1a, 0x55, 0x0a, 0x0a, 0x48,
-	0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x05, 0x76,
-	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61,
-	0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f,
-	0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
-	0x38, 0x01, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69,
-	0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
-	0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e,
-	0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79,
-	0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61,
-	0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
-	0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70,
-	0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61,
-	0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65,
-	0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69,
-	0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a,
-	0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f,
-	0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72,
-	0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x42, 0x46,
-	0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64,
-	0x6e, 0x73, 0x50, 0x01, 0x5a, 0x21, 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,
-	0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41,
-	0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69,
+	0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c,
+	0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x56, 0x0a, 0x12, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69,
+	0x74, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e,
+	0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x69,
+	0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x11, 0x70, 0x72, 0x69,
+	0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2c,
+	0x0a, 0x05, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
+	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e,
+	0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x05, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x4c, 0x0a, 0x0e,
+	0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x04,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
+	0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x4f,
+	0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x6f, 0x72, 0x69,
+	0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72,
+	0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x04,
+	0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61,
+	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
+	0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79,
+	0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f, 0x72,
+	0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x75,
+	0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x12,
+	0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69,
+	0x7a, 0x65, 0x22, 0xc9, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3f, 0x0a,
+	0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
+	0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x42, 0x02, 0x18,
+	0x01, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x39,
+	0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64,
+	0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0a, 0x6e,
+	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x05, 0x48, 0x6f, 0x73,
+	0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
+	0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48,
+	0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x02, 0x18, 0x01, 0x52, 0x05, 0x48,
+	0x6f, 0x73, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69,
+	0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49,
+	0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74,
+	0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
+	0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f,
+	0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x69,
+	0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61,
+	0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c,
+	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x1a, 0x55, 0x0a, 0x0a,
+	0x48, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72,
+	0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50,
+	0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
+	0x02, 0x38, 0x01, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70,
+	0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73,
+	0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54,
+	0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
+	0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x02, 0x69,
+	0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x69,
+	0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x2a, 0x45,
+	0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67,
+	0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d,
+	0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a,
+	0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65,
+	0x67, 0x65, 0x78, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61,
+	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x21, 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, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02,
+	0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (

+ 4 - 0
app/dns/config.proto

@@ -12,6 +12,7 @@ import "app/router/config.proto";
 
 message NameServer {
   xray.common.net.Endpoint address = 1;
+  bytes client_ip = 5;
 
   message PriorityDomain {
     DomainMatchingType type = 1;
@@ -70,4 +71,7 @@ message Config {
   string tag = 6;
 
   reserved 7;
+
+  // DisableCache Disable DNS cache
+   bool disableCache = 8;
 }

+ 230 - 0
app/dns/dns.go

@@ -2,3 +2,233 @@
 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
+	hosts         *StaticHosts
+	clients       []*Client
+	ctx           context.Context
+	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, 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,
+		clients:       clients,
+		ctx:           ctx,
+		domainMatcher: domainMatcher,
+		matcherInfos:  matcherInfos,
+		disableCache:  config.DisableCache,
+	}, 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")
+	}
+
+	// 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), " IPs for domain ", domain).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) {
+		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...))
+}
+
+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 - 0
app/dns/server_test.go → app/dns/dns_test.go


+ 26 - 31
app/dns/hosts.go

@@ -14,25 +14,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)
@@ -100,24 +81,38 @@ func filterIP(ips []net.Address, option dns.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 dns.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 filterIP(ips, option)
+	return ips
+}
+
+func (h *StaticHosts) lookup(domain string, option dns.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)
+	}
+}
+
+// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
+func (h *StaticHosts) Lookup(domain string, option dns.IPOption) []net.Address {
+	return h.lookup(domain, option, 5)
 }

+ 3 - 3
app/dns/hosts_test.go

@@ -40,7 +40,7 @@ func TestStaticHosts(t *testing.T) {
 	common.Must(err)
 
 	{
-		ips := hosts.LookupIP("example.com", dns.IPOption{
+		ips := hosts.Lookup("example.com", dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -53,7 +53,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("www.example.cn", dns.IPOption{
+		ips := hosts.Lookup("www.example.cn", dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -66,7 +66,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("baidu.com", dns.IPOption{
+		ips := hosts.Lookup("baidu.com", dns.IPOption{
 			IPv4Enable: false,
 			IPv6Enable: true,
 		})

+ 186 - 17
app/dns/nameserver.go

@@ -2,40 +2,209 @@ package dns
 
 import (
 	"context"
+	"net/url"
+	"strings"
+	"time"
 
+	"github.com/xtls/xray-core/app/router"
+	"github.com/xtls/xray-core/common/errors"
 	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/strmatcher"
+	core "github.com/xtls/xray-core/core"
 	"github.com/xtls/xray-core/features/dns"
-	"github.com/xtls/xray-core/features/dns/localdns"
+	"github.com/xtls/xray-core/features/routing"
 )
 
-// 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 dns.IPOption) ([]net.IP, error)
+	QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]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(_ context.Context, domain string, option dns.IPOption) ([]net.IP, error) {
-	if option.IPv4Enable || option.IPv6Enable {
-		return s.client.LookupIP(domain, option)
+var errExpectedIPNonMatch = errors.New("expectIPs not match")
+
+// 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 strings.EqualFold(u.String(), "localhost"):
+			return NewLocalNameServer(), nil
+		case strings.EqualFold(u.Scheme, "https"): // DOH Remote mode
+			return NewDoHNameServer(u, dispatcher)
+		case strings.EqualFold(u.Scheme, "https+local"): // DOH Local mode
+			return NewDoHLocalNameServer(u), nil
+		case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode
+			return NewQUICNameServer(u)
+		case strings.EqualFold(u.String(), "fakedns"):
+			return NewFakeDNSServer(), 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, matcherInfos *[]DomainMatcherInfo, updateDomainRule func(strmatcher.Matcher, int, []DomainMatcherInfo) 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)
+			// The following lines is a solution to avoid core panics(rule index out of range) when setting `localhost` DNS client in config.
+			// Because the `localhost` DNS client will apend len(localTLDsAndDotlessDomains) rules into matcherInfos to match `geosite:private` default rule.
+			// But `matcherInfos` has no enough length to add rules, which leads to core panics (rule index out of range).
+			// To avoid this, the length of `matcherInfos` must be equal to the expected, so manually append it with Golang default zero value first for later modification.
+			for i := 0; i < len(localTLDsAndDotlessDomains); i++ {
+				*matcherInfos = append(*matcherInfos, DomainMatcherInfo{
+					clientIdx:     uint16(0),
+					domainRuleIdx: uint16(0),
+				})
+			}
+		}
+
+		// 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, *matcherInfos)
+			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 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()
+			}
+		}
 
-	return nil, newError("neither IPv4 nor IPv6 is enabled")
+		client.server = server
+		client.clientIP = clientIP
+		client.domains = rules
+		client.expectIPs = matchers
+		return nil
+	})
+	return client, err
 }
 
-func (s *LocalNameServer) Name() string {
-	return "localhost"
+// 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 client, err
 }
 
-func NewLocalNameServer() *LocalNameServer {
-	newError("DNS: created localhost client").AtInfo().WriteToLog()
-	return &LocalNameServer{
-		client: localdns.New(),
+// Name returns the server name the client manages.
+func (c *Client) Name() string {
+	return c.server.Name()
+}
+
+// QueryIP send DNS query to the name server with the client's IP.
+func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption, disableCache bool) ([]net.IP, error) {
+	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
+	ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option, disableCache)
+	cancel()
+
+	if err != nil {
+		return ips, err
+	}
+	return c.MatchExpectedIPs(domain, ips)
+}
+
+// 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
 }

+ 27 - 25
app/dns/dohdns.go → app/dns/nameserver_doh.go

@@ -42,10 +42,10 @@ type DoHNameServer struct {
 	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()
-	s := baseDOHNameServer(url, "DOH", clientIP)
+	s := baseDOHNameServer(url, "DOH")
 
 	s.dispatcher = dispatcher
 	tr := &http.Transport{
@@ -104,9 +104,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,
@@ -136,13 +136,12 @@ func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer {
 	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,
@@ -152,7 +151,7 @@ func baseDOHNameServer(url *url.URL, prefix string, clientIP net.IP) *DoHNameSer
 	return s
 }
 
-// Name returns client name
+// Name implements Server.
 func (s *DoHNameServer) Name() string {
 	return s.name
 }
@@ -235,7 +234,7 @@ func (s *DoHNameServer) newReqID() uint16 {
 	return uint16(atomic.AddUint32(&s.reqID, 1))
 }
 
-func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option dns_feature.IPOption) {
+func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
 	newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx))
 
 	if s.name+"." == "DOH//"+domain {
@@ -243,7 +242,7 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option dns
 		return
 	}
 
-	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 {
@@ -264,8 +263,8 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option dns
 			}
 
 			dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{
-				Protocol: "https",
-				//SkipRoutePick: true,
+				Protocol:       "https",
+				SkipDNSResolve: true,
 			})
 
 			// forced to use mux for DOH
@@ -349,7 +348,7 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt
 	}
 
 	if len(ips) > 0 {
-		return toNetIP(ips), nil
+		return toNetIP(ips)
 	}
 
 	if lastErr != nil {
@@ -363,15 +362,18 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt
 	return nil, errRecordNotFound
 }
 
-// QueryIP is called from dns.Server->queryIPTimeout
-func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, error) { // nolint: dupl
+// 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)
 
-	ips, err := s.findIPsForDomain(fqdn, option)
-	if err != errRecordNotFound {
-		newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
-		log.Record(&log.DNSLog{s.name, domain, ips, log.DNSCacheHit, 0, err})
-		return ips, err
+	if disableCache {
+		newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog()
+	} else {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
+			return ips, err
+		}
 	}
 
 	// ipv4 and ipv6 belong to different subscription groups
@@ -400,7 +402,7 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_f
 		}
 		close(done)
 	}()
-	s.sendQuery(ctx, fqdn, option)
+	s.sendQuery(ctx, fqdn, clientIP, option)
 	start := time.Now()
 
 	for {

+ 60 - 0
app/dns/nameserver_doh_test.go

@@ -0,0 +1,60 @@
+package dns_test
+
+import (
+	"context"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+
+	. "github.com/xtls/xray-core/app/dns"
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/net"
+	dns_feature "github.com/xtls/xray-core/features/dns"
+)
+
+func TestDOHNameServer(t *testing.T) {
+	url, err := url.Parse("https+local://1.1.1.1/dns-query")
+	common.Must(err)
+
+	s := NewDoHLocalNameServer(url)
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, false)
+	cancel()
+	common.Must(err)
+	if len(ips) == 0 {
+		t.Error("expect some ips, but got 0")
+	}
+}
+
+func TestDOHNameServerWithCache(t *testing.T) {
+	url, err := url.Parse("https+local://1.1.1.1/dns-query")
+	common.Must(err)
+
+	s := NewDoHLocalNameServer(url)
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, false)
+	cancel()
+	common.Must(err)
+	if len(ips) == 0 {
+		t.Error("expect some ips, but got 0")
+	}
+
+	ctx2, cancel := context.WithTimeout(context.Background(), time.Second*2)
+	ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, true)
+	cancel()
+	common.Must(err)
+	if r := cmp.Diff(ips2, ips); r != "" {
+		t.Fatal(r)
+	}
+}

+ 4 - 4
app/dns/nameserver_fakedns.go

@@ -20,7 +20,7 @@ func (FakeDNSServer) Name() string {
 	return "FakeDNS"
 }
 
-func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ dns.IPOption) ([]net.IP, error) {
+func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, _ dns.IPOption, _ bool) ([]net.IP, error) {
 	if f.fakeDNSEngine == nil {
 		if err := core.RequireFeatures(ctx, func(fd dns.FakeDNSEngine) {
 			f.fakeDNSEngine = fd
@@ -30,9 +30,9 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ dns.IPOpti
 	}
 	ips := f.fakeDNSEngine.GetFakeIPForDomain(domain)
 
-	netIP := toNetIP(ips)
-	if netIP == nil {
-		return nil, newError("Unable to convert IP to net ip").AtError()
+	netIP, err := toNetIP(ips)
+	if err != nil {
+		return nil, newError("Unable to convert IP to net ip").Base(err).AtError()
 	}
 
 	newError(f.Name(), " got answer: ", domain, " -> ", ips).AtInfo().WriteToLog()

+ 41 - 0
app/dns/nameserver_local.go

@@ -0,0 +1,41 @@
+package dns
+
+import (
+	"context"
+
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features/dns"
+	"github.com/xtls/xray-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(_ context.Context, domain string, _ net.IP, option dns.IPOption, _ bool) ([]net.IP, error) {
+	if option.IPv4Enable || option.IPv6Enable {
+		return s.client.LookupIP(domain, option)
+	}
+
+	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()}
+}

+ 4 - 4
app/dns/nameserver_test.go → app/dns/nameserver_local_test.go

@@ -7,17 +7,17 @@ import (
 
 	. "github.com/xtls/xray-core/app/dns"
 	"github.com/xtls/xray-core/common"
-	dns_feature "github.com/xtls/xray-core/features/dns"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features/dns"
 )
 
 func TestLocalNameServer(t *testing.T) {
 	s := NewLocalNameServer()
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
-	ips, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
+	ips, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{
 		IPv4Enable: true,
 		IPv6Enable: true,
-		FakeEnable: false,
-	})
+	}, false)
 	cancel()
 	common.Must(err)
 	if len(ips) == 0 {

+ 387 - 0
app/dns/nameserver_quic.go

@@ -0,0 +1,387 @@
+package dns
+
+import (
+	"context"
+	"net/url"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/lucas-clemente/quic-go"
+	"golang.org/x/net/dns/dnsmessage"
+	"golang.org/x/net/http2"
+
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/buf"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/protocol/dns"
+	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/signal/pubsub"
+	"github.com/xtls/xray-core/common/task"
+	dns_feature "github.com/xtls/xray-core/features/dns"
+	"github.com/xtls/xray-core/transport/internet/tls"
+)
+
+// NextProtoDQ - During connection establishment, DNS/QUIC support is indicated
+// by selecting the ALPN token "dq" in the crypto handshake.
+const NextProtoDQ = "doq-i00"
+
+const handshakeTimeout = time.Second * 8
+
+// QUICNameServer implemented DNS over QUIC
+type QUICNameServer struct {
+	sync.RWMutex
+	ips         map[string]record
+	pub         *pubsub.Service
+	cleanup     *task.Periodic
+	reqID       uint32
+	name        string
+	destination net.Destination
+	session     quic.Session
+}
+
+// NewQUICNameServer creates DNS-over-QUIC client object for local resolving
+func NewQUICNameServer(url *url.URL) (*QUICNameServer, error) {
+	newError("DNS: created Local DNS-over-QUIC client for ", url.String()).AtInfo().WriteToLog()
+
+	var err error
+	port := net.Port(784)
+	if url.Port() != "" {
+		port, err = net.PortFromString(url.Port())
+		if err != nil {
+			return nil, err
+		}
+	}
+	dest := net.UDPDestination(net.DomainAddress(url.Hostname()), port)
+
+	s := &QUICNameServer{
+		ips:         make(map[string]record),
+		pub:         pubsub.NewService(),
+		name:        url.String(),
+		destination: dest,
+	}
+	s.cleanup = &task.Periodic{
+		Interval: time.Minute,
+		Execute:  s.Cleanup,
+	}
+
+	return s, nil
+}
+
+// Name returns client name
+func (s *QUICNameServer) Name() string {
+	return s.name
+}
+
+// Cleanup clears expired items from cache
+func (s *QUICNameServer) Cleanup() error {
+	now := time.Now()
+	s.Lock()
+	defer s.Unlock()
+
+	if len(s.ips) == 0 {
+		return newError("nothing to do. stopping...")
+	}
+
+	for domain, record := range s.ips {
+		if record.A != nil && record.A.Expire.Before(now) {
+			record.A = nil
+		}
+		if record.AAAA != nil && record.AAAA.Expire.Before(now) {
+			record.AAAA = nil
+		}
+
+		if record.A == nil && record.AAAA == nil {
+			newError(s.name, " cleanup ", domain).AtDebug().WriteToLog()
+			delete(s.ips, domain)
+		} else {
+			s.ips[domain] = record
+		}
+	}
+
+	if len(s.ips) == 0 {
+		s.ips = make(map[string]record)
+	}
+
+	return nil
+}
+
+func (s *QUICNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
+	elapsed := time.Since(req.start)
+
+	s.Lock()
+	rec := s.ips[req.domain]
+	updated := false
+
+	switch req.reqType {
+	case dnsmessage.TypeA:
+		if isNewer(rec.A, ipRec) {
+			rec.A = ipRec
+			updated = true
+		}
+	case dnsmessage.TypeAAAA:
+		addr := make([]net.Address, 0)
+		for _, ip := range ipRec.IP {
+			if len(ip.IP()) == net.IPv6len {
+				addr = append(addr, ip)
+			}
+		}
+		ipRec.IP = addr
+		if isNewer(rec.AAAA, ipRec) {
+			rec.AAAA = ipRec
+			updated = true
+		}
+	}
+	newError(s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog()
+
+	if updated {
+		s.ips[req.domain] = rec
+	}
+	switch req.reqType {
+	case dnsmessage.TypeA:
+		s.pub.Publish(req.domain+"4", nil)
+	case dnsmessage.TypeAAAA:
+		s.pub.Publish(req.domain+"6", nil)
+	}
+	s.Unlock()
+	common.Must(s.cleanup.Start())
+}
+
+func (s *QUICNameServer) newReqID() uint16 {
+	return uint16(atomic.AddUint32(&s.reqID, 1))
+}
+
+func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
+	newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx))
+
+	reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
+
+	var deadline time.Time
+	if d, ok := ctx.Deadline(); ok {
+		deadline = d
+	} else {
+		deadline = time.Now().Add(time.Second * 5)
+	}
+
+	for _, req := range reqs {
+		go func(r *dnsRequest) {
+			// generate new context for each req, using same context
+			// may cause reqs all aborted if any one encounter an error
+			dnsCtx := context.Background()
+
+			// 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:       "quic",
+				SkipDNSResolve: true,
+			})
+
+			var cancel context.CancelFunc
+			dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline)
+			defer cancel()
+
+			b, err := dns.PackMessage(r.msg)
+			if err != nil {
+				newError("failed to pack dns query").Base(err).AtError().WriteToLog()
+				return
+			}
+
+			conn, err := s.openStream(dnsCtx)
+			if err != nil {
+				newError("failed to open quic session").Base(err).AtError().WriteToLog()
+				return
+			}
+
+			_, err = conn.Write(b.Bytes())
+			if err != nil {
+				newError("failed to send query").Base(err).AtError().WriteToLog()
+				return
+			}
+
+			_ = conn.Close()
+
+			respBuf := buf.New()
+			defer respBuf.Release()
+			n, err := respBuf.ReadFrom(conn)
+			if err != nil && n == 0 {
+				newError("failed to read response").Base(err).AtError().WriteToLog()
+				return
+			}
+
+			rec, err := parseResponse(respBuf.Bytes())
+			if err != nil {
+				newError("failed to handle response").Base(err).AtError().WriteToLog()
+				return
+			}
+			s.updateIP(r, rec)
+		}(req)
+	}
+}
+
+func (s *QUICNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
+	s.RLock()
+	record, found := s.ips[domain]
+	s.RUnlock()
+
+	if !found {
+		return nil, errRecordNotFound
+	}
+
+	var ips []net.Address
+	var lastErr error
+	if option.IPv6Enable && record.AAAA != nil && record.AAAA.RCode == dnsmessage.RCodeSuccess {
+		aaaa, err := record.AAAA.getIPs()
+		if err != nil {
+			lastErr = err
+		}
+		ips = append(ips, aaaa...)
+	}
+
+	if option.IPv4Enable && record.A != nil && record.A.RCode == dnsmessage.RCodeSuccess {
+		a, err := record.A.getIPs()
+		if err != nil {
+			lastErr = err
+		}
+		ips = append(ips, a...)
+	}
+
+	if len(ips) > 0 {
+		return toNetIP(ips)
+	}
+
+	if lastErr != nil {
+		return nil, lastErr
+	}
+
+	if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) {
+		return nil, dns_feature.ErrEmptyResponse
+	}
+
+	return nil, errRecordNotFound
+}
+
+// QueryIP is called from dns.Server->queryIPTimeout
+func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
+	fqdn := Fqdn(domain)
+
+	if disableCache {
+		newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog()
+	} else {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
+			return ips, err
+		}
+	}
+
+	// ipv4 and ipv6 belong to different subscription groups
+	var sub4, sub6 *pubsub.Subscriber
+	if option.IPv4Enable {
+		sub4 = s.pub.Subscribe(fqdn + "4")
+		defer sub4.Close()
+	}
+	if option.IPv6Enable {
+		sub6 = s.pub.Subscribe(fqdn + "6")
+		defer sub6.Close()
+	}
+	done := make(chan interface{})
+	go func() {
+		if sub4 != nil {
+			select {
+			case <-sub4.Wait():
+			case <-ctx.Done():
+			}
+		}
+		if sub6 != nil {
+			select {
+			case <-sub6.Wait():
+			case <-ctx.Done():
+			}
+		}
+		close(done)
+	}()
+	s.sendQuery(ctx, fqdn, clientIP, option)
+
+	for {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			return ips, err
+		}
+
+		select {
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		case <-done:
+		}
+	}
+}
+
+func isActive(s quic.Session) bool {
+	select {
+	case <-s.Context().Done():
+		return false
+	default:
+		return true
+	}
+}
+
+func (s *QUICNameServer) getSession() (quic.Session, error) {
+	var session quic.Session
+	s.RLock()
+	session = s.session
+	if session != nil && isActive(session) {
+		s.RUnlock()
+		return session, nil
+	}
+	if session != nil {
+		// we're recreating the session, let's create a new one
+		_ = session.CloseWithError(0, "")
+	}
+	s.RUnlock()
+
+	s.Lock()
+	defer s.Unlock()
+
+	var err error
+	session, err = s.openSession()
+	if err != nil {
+		// This does not look too nice, but QUIC (or maybe quic-go)
+		// doesn't seem stable enough.
+		// Maybe retransmissions aren't fully implemented in quic-go?
+		// Anyways, the simple solution is to make a second try when
+		// it fails to open the QUIC session.
+		session, err = s.openSession()
+		if err != nil {
+			return nil, err
+		}
+	}
+	s.session = session
+	return session, nil
+}
+
+func (s *QUICNameServer) openSession() (quic.Session, error) {
+	tlsConfig := tls.Config{}
+	quicConfig := &quic.Config{
+		HandshakeTimeout: handshakeTimeout,
+	}
+
+	session, err := quic.DialAddrContext(context.Background(), s.destination.NetAddr(), tlsConfig.GetTLSConfig(tls.WithNextProto("http/1.1", http2.NextProtoTLS, NextProtoDQ)), quicConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	return session, nil
+}
+
+func (s *QUICNameServer) openStream(ctx context.Context) (quic.Stream, error) {
+	session, err := s.getSession()
+	if err != nil {
+		return nil, err
+	}
+
+	// open a new stream
+	return session.OpenStreamSync(ctx)
+}

+ 60 - 0
app/dns/nameserver_quic_test.go

@@ -0,0 +1,60 @@
+package dns_test
+
+import (
+	"context"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+
+	. "github.com/xtls/xray-core/app/dns"
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/net"
+	dns_feature "github.com/xtls/xray-core/features/dns"
+)
+
+func TestQUICNameServer(t *testing.T) {
+	url, err := url.Parse("quic://dns.adguard.com")
+	common.Must(err)
+	s, err := NewQUICNameServer(url)
+	common.Must(err)
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, false)
+	cancel()
+	common.Must(err)
+	if len(ips) == 0 {
+		t.Error("expect some ips, but got 0")
+	}
+}
+
+func TestQUICNameServerWithCache(t *testing.T) {
+	url, err := url.Parse("quic://dns.adguard.com")
+	common.Must(err)
+	s, err := NewQUICNameServer(url)
+	common.Must(err)
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, false)
+	cancel()
+	common.Must(err)
+	if len(ips) == 0 {
+		t.Error("expect some ips, but got 0")
+	}
+
+	ctx2, cancel := context.WithTimeout(context.Background(), time.Second*2)
+	ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, true)
+	cancel()
+	common.Must(err)
+	if r := cmp.Diff(ips2, ips); r != "" {
+		t.Fatal(r)
+	}
+}

+ 20 - 14
app/dns/udpns.go → app/dns/nameserver_udp.go

@@ -2,12 +2,13 @@ package dns
 
 import (
 	"context"
-	"github.com/xtls/xray-core/transport/internet"
 	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
 
+	"github.com/xtls/xray-core/transport/internet"
+
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/log"
 	"github.com/xtls/xray-core/common/net"
@@ -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()),
 	}
@@ -58,10 +58,12 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
 	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()
@@ -103,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 {
@@ -180,10 +183,10 @@ func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) {
 	s.requests[id] = *req
 }
 
-func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option dns_feature.IPOption) {
+func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.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)
@@ -234,7 +237,7 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.I
 	}
 
 	if len(ips) > 0 {
-		return toNetIP(ips), nil
+		return toNetIP(ips)
 	}
 
 	if lastErr != nil {
@@ -245,14 +248,17 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.I
 }
 
 // QueryIP implements Server.
-func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, error) {
+func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
 	fqdn := Fqdn(domain)
 
-	ips, err := s.findIPsForDomain(fqdn, option)
-	if err != errRecordNotFound {
-		newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
-		log.Record(&log.DNSLog{s.name, domain, ips, log.DNSCacheHit, 0, err})
-		return ips, err
+	if disableCache {
+		newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog()
+	} else {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
+			return ips, err
+		}
 	}
 
 	// ipv4 and ipv6 belong to different subscription groups
@@ -281,7 +287,7 @@ func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option d
 		}
 		close(done)
 	}()
-	s.sendQuery(ctx, fqdn, option)
+	s.sendQuery(ctx, fqdn, clientIP, option)
 	start := time.Now()
 
 	for {

+ 0 - 439
app/dns/server.go

@@ -1,439 +0,0 @@
-package dns
-
-//go:generate go run github.com/xtls/xray-core/common/errors/errorgen
-
-import (
-	"context"
-	"fmt"
-	"log"
-	"net/url"
-	"strings"
-	"sync"
-	"time"
-
-	"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/common/uuid"
-	core "github.com/xtls/xray-core/core"
-	"github.com/xtls/xray-core/features"
-	"github.com/xtls/xray-core/features/dns"
-	"github.com/xtls/xray-core/features/routing"
-	"github.com/xtls/xray-core/transport/internet"
-)
-
-// Server is a DNS rely server.
-type Server struct {
-	sync.Mutex
-	hosts         *StaticHosts
-	clientIP      net.IP
-	clients       []Client // clientIdx -> Client
-	ctx           context.Context
-	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 "xray.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)),
-		ctx:     ctx,
-		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()
-
-		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, server.clientIP))
-
-		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, server.clientIP)
-				if err != nil {
-					log.Fatalln(newError("DNS config error").Base(err))
-				}
-				server.clients[idx] = c
-			}))
-
-		case address.Family().IsDomain() && address.Domain() == "fakedns":
-			server.clients = append(server.clients, NewFakeDNSServer())
-
-		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, server.clientIP)
-				}))
-			}
-		}
-		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 dns.IPOption) ([]net.IP, error) {
-	ctx, cancel := context.WithTimeout(s.ctx, time.Second*4)
-	if len(s.tag) > 0 {
-		ctx = session.ContextWithInbound(ctx, &session.Inbound{
-			Tag: s.tag,
-		})
-	}
-	ctx = internet.ContextWithLookupDomain(ctx, domain)
-	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
-}
-
-func (s *Server) lookupStatic(domain string, option dns.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
-}
-
-// LookupIP implements dns.Client.
-func (s *Server) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
-	if domain == "" {
-		return nil, newError("empty domain name")
-	}
-	domain = strings.ToLower(domain)
-
-	// normalize the FQDN form query
-	if strings.HasSuffix(domain, ".") {
-		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]
-			if !option.FakeEnable && strings.EqualFold(matchedClient.Name(), "FakeDNS") {
-				newError("skip DNS resolution for domain ", domain, " at server ", matchedClient.Name()).AtDebug().WriteToLog()
-				continue
-			}
-			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
-		}
-		if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") {
-			newError("skip DNS resolution for domain ", domain, " at server ", client.Name()).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))
-	}))
-}

+ 6 - 0
app/router/command/config.go

@@ -28,6 +28,12 @@ func (c routingContext) GetTargetPort() net.Port {
 	return net.Port(c.RoutingContext.GetTargetPort())
 }
 
+// GetSkipDNSResolve is a mock implementation here to match the interface,
+// SkipDNSResolve is set from dns module, no use if coming from a protobuf object?
+func (c routingContext) GetSkipDNSResolve() bool {
+	return false
+}
+
 // AsRoutingContext converts a protobuf RoutingContext into an implementation of routing.Context.
 func AsRoutingContext(r *RoutingContext) routing.Context {
 	return routingContext{r}

+ 8 - 2
app/router/router.go

@@ -80,7 +80,13 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
 }
 
 func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) {
-	if r.domainStrategy == Config_IpOnDemand {
+
+	// SkipDNSResolve is set from DNS module.
+	// the DOH remote server maybe a domain name,
+	// this prevents cycle resolving dead loop
+	skipDNSResolve := ctx.GetSkipDNSResolve()
+
+	if r.domainStrategy == Config_IpOnDemand && !skipDNSResolve {
 		ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
 	}
 
@@ -90,7 +96,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
 		}
 	}
 
-	if r.domainStrategy != Config_IpIfNonMatch || len(ctx.GetTargetDomain()) == 0 {
+	if r.domainStrategy != Config_IpIfNonMatch || len(ctx.GetTargetDomain()) == 0 || skipDNSResolve {
 		return nil, ctx, common.ErrNoClue
 	}
 

+ 1 - 1
common/session/session.go

@@ -75,7 +75,7 @@ type Content struct {
 
 	Attributes map[string]string
 
-	SkipRoutePick bool
+	SkipDNSResolve bool
 }
 
 // Sockopt is the settings for socket connection.

+ 3 - 0
features/routing/context.go

@@ -37,4 +37,7 @@ type Context interface {
 
 	// GetAttributes returns extra attributes from the conneciont content.
 	GetAttributes() map[string]string
+
+	// GetSkipDNSResolve returns a flag switch for weather skip dns resolve during route pick.
+	GetSkipDNSResolve() bool
 }

+ 8 - 0
features/routing/session/context.go

@@ -109,6 +109,14 @@ func (ctx *Context) GetAttributes() map[string]string {
 	return ctx.Content.Attributes
 }
 
+// GetSkipDNSResolve implements routing.Context.
+func (ctx *Context) GetSkipDNSResolve() bool {
+	if ctx.Content == nil {
+		return false
+	}
+	return ctx.Content.SkipDNSResolve
+}
+
 // AsRoutingContext creates a context from context.context with session info.
 func AsRoutingContext(ctx context.Context) routing.Context {
 	return &Context{

+ 7 - 5
infra/conf/dns.go

@@ -108,10 +108,11 @@ var typeMap = map[router.Domain_Type]dns.DomainMatchingType{
 
 // DNSConfig is a JSON serializable object for dns.Config.
 type DNSConfig struct {
-	Servers  []*NameServerConfig `json:"servers"`
-	Hosts    map[string]*Address `json:"hosts"`
-	ClientIP *Address            `json:"clientIp"`
-	Tag      string              `json:"tag"`
+	Servers      []*NameServerConfig `json:"servers"`
+	Hosts        map[string]*Address `json:"hosts"`
+	ClientIP     *Address            `json:"clientIp"`
+	Tag          string              `json:"tag"`
+	DisableCache bool                `json:"disableCache"`
 }
 
 func getHostMapping(addr *Address) *dns.Config_HostMapping {
@@ -129,7 +130,8 @@ func getHostMapping(addr *Address) *dns.Config_HostMapping {
 // Build implements Buildable
 func (c *DNSConfig) Build() (*dns.Config, error) {
 	config := &dns.Config{
-		Tag: c.Tag,
+		Tag:          c.Tag,
+		DisableCache: c.DisableCache,
 	}
 
 	if c.ClientIP != nil {