Quellcode durchsuchen

Merge branch 'dns' into features-looutbound

秋のかえで vor 4 Jahren
Ursprung
Commit
ab3b0f843d
71 geänderte Dateien mit 2444 neuen und 1153 gelöschten Zeilen
  1. 3 2
      README.md
  2. 2 7
      app/dispatcher/default.go
  3. 63 0
      app/dns/config.go
  4. 262 96
      app/dns/config.pb.go
  5. 22 2
      app/dns/config.proto
  6. 309 0
      app/dns/dns.go
  7. 27 112
      app/dns/dns_test.go
  8. 31 40
      app/dns/hosts.go
  9. 44 3
      app/dns/hosts_test.go
  10. 188 17
      app/dns/nameserver.go
  11. 38 26
      app/dns/nameserver_doh.go
  12. 60 0
      app/dns/nameserver_doh_test.go
  13. 7 5
      app/dns/nameserver_fakedns.go
  14. 53 0
      app/dns/nameserver_local.go
  15. 5 5
      app/dns/nameserver_local_test.go
  16. 394 0
      app/dns/nameserver_quic.go
  17. 60 0
      app/dns/nameserver_quic_test.go
  18. 21 15
      app/dns/nameserver_udp.go
  19. 16 0
      app/dns/options.go
  20. 0 439
      app/dns/server.go
  21. 6 0
      app/router/command/config.go
  22. 8 2
      app/router/router.go
  23. 2 11
      app/router/router_test.go
  24. 1 1
      common/buf/buffer.go
  25. 1 1
      common/session/session.go
  26. 14 3
      core/config.go
  27. 1 1
      core/core.go
  28. 56 1
      features/dns/client.go
  29. 40 17
      features/dns/localdns/client.go
  30. 3 0
      features/routing/context.go
  31. 2 6
      features/routing/dns/context.go
  32. 8 0
      features/routing/session/context.go
  33. 9 8
      go.mod
  34. 26 35
      go.sum
  35. 97 19
      infra/conf/dns.go
  36. 22 6
      infra/conf/dns_test.go
  37. 3 3
      infra/conf/freedom.go
  38. 5 6
      infra/conf/transport_internet.go
  39. 101 14
      infra/conf/transport_test.go
  40. 3 0
      main/commands/all/tls/cert.go
  41. 18 4
      main/run.go
  42. 5 11
      proxy/dns/dns.go
  43. 4 16
      proxy/freedom/freedom.go
  44. 39 18
      testing/mocks/dns.go
  45. 14 13
      testing/mocks/io.go
  46. 8 7
      testing/mocks/log.go
  47. 8 7
      testing/mocks/mux.go
  48. 26 25
      testing/mocks/outbound.go
  49. 16 15
      testing/mocks/proxy.go
  50. 19 11
      transport/internet/grpc/dial.go
  51. 16 1
      transport/internet/grpc/encoding/hunkconn.go
  52. 31 9
      transport/internet/grpc/encoding/multiconn.go
  53. 3 2
      transport/internet/http/dialer.go
  54. 1 2
      transport/internet/quic/dialer.go
  55. 1 2
      transport/internet/quic/hub.go
  56. 11 0
      transport/internet/sockopt.go
  57. 4 4
      transport/internet/sockopt_darwin.go
  58. 5 4
      transport/internet/sockopt_freebsd.go
  59. 5 4
      transport/internet/sockopt_linux.go
  60. 4 4
      transport/internet/sockopt_windows.go
  61. 4 21
      transport/internet/system_dialer.go
  62. 0 18
      transport/internet/system_dialer_context.go
  63. 7 7
      transport/internet/tcp/dialer.go
  64. 2 1
      transport/internet/tcp/hub.go
  65. 0 10
      transport/internet/tls/config.go
  66. 22 11
      transport/internet/tls/config.pb.go
  67. 3 0
      transport/internet/tls/config.proto
  68. 34 14
      transport/internet/tls/tls.go
  69. 49 0
      transport/internet/websocket/dialer.go
  70. 55 0
      transport/internet/websocket/dialer.html
  71. 17 9
      transport/internet/websocket/hub.go

+ 3 - 2
README.md

@@ -23,8 +23,9 @@
   - [Xray4Magisk](https://github.com/CerteKim/Xray4Magisk)
   - [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
 - Homebrew
-  - [Repository 0](https://github.com/N4FA/homebrew-xray)
-  - [Repository 1](https://github.com/xiruizhao/homebrew-xray)
+  - `brew install xray`
+  - [(Tap) Repository 0](https://github.com/N4FA/homebrew-xray)
+  - [(Tap) Repository 1](https://github.com/xiruizhao/homebrew-xray)
 
 ## Usage
 

+ 2 - 7
app/dispatcher/default.go

@@ -196,7 +196,7 @@ func shouldOverride(ctx context.Context, result SniffResult, request session.Sni
 			return true
 		}
 		if fakeDNSEngine != nil && protocolString != "bittorrent" && p == "fakedns" &&
-			fakeDNSEngine.GetFakeIPRange().Contains(destination.Address.IP()) {
+			destination.Address.Family().IsIP() && fakeDNSEngine.GetFakeIPRange().Contains(destination.Address.IP()) {
 			newError("Using sniffer ", protocolString, " since the fake DNS missed").WriteToLog(session.ExportIDToError(ctx))
 			return true
 		}
@@ -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()
+}

+ 262 - 96
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.8
 // source: app/dns/config.proto
 
 package dns
@@ -79,12 +79,112 @@ func (DomainMatchingType) EnumDescriptor() ([]byte, []int) {
 	return file_app_dns_config_proto_rawDescGZIP(), []int{0}
 }
 
+type QueryStrategy int32
+
+const (
+	QueryStrategy_USE_IP  QueryStrategy = 0
+	QueryStrategy_USE_IP4 QueryStrategy = 1
+	QueryStrategy_USE_IP6 QueryStrategy = 2
+)
+
+// Enum value maps for QueryStrategy.
+var (
+	QueryStrategy_name = map[int32]string{
+		0: "USE_IP",
+		1: "USE_IP4",
+		2: "USE_IP6",
+	}
+	QueryStrategy_value = map[string]int32{
+		"USE_IP":  0,
+		"USE_IP4": 1,
+		"USE_IP6": 2,
+	}
+)
+
+func (x QueryStrategy) Enum() *QueryStrategy {
+	p := new(QueryStrategy)
+	*p = x
+	return p
+}
+
+func (x QueryStrategy) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (QueryStrategy) Descriptor() protoreflect.EnumDescriptor {
+	return file_app_dns_config_proto_enumTypes[1].Descriptor()
+}
+
+func (QueryStrategy) Type() protoreflect.EnumType {
+	return &file_app_dns_config_proto_enumTypes[1]
+}
+
+func (x QueryStrategy) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use QueryStrategy.Descriptor instead.
+func (QueryStrategy) EnumDescriptor() ([]byte, []int) {
+	return file_app_dns_config_proto_rawDescGZIP(), []int{1}
+}
+
+type CacheStrategy int32
+
+const (
+	CacheStrategy_Cache_ALL     CacheStrategy = 0
+	CacheStrategy_Cache_NOERROR CacheStrategy = 1
+	CacheStrategy_Cache_DISABLE CacheStrategy = 2
+)
+
+// Enum value maps for CacheStrategy.
+var (
+	CacheStrategy_name = map[int32]string{
+		0: "Cache_ALL",
+		1: "Cache_NOERROR",
+		2: "Cache_DISABLE",
+	}
+	CacheStrategy_value = map[string]int32{
+		"Cache_ALL":     0,
+		"Cache_NOERROR": 1,
+		"Cache_DISABLE": 2,
+	}
+)
+
+func (x CacheStrategy) Enum() *CacheStrategy {
+	p := new(CacheStrategy)
+	*p = x
+	return p
+}
+
+func (x CacheStrategy) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (CacheStrategy) Descriptor() protoreflect.EnumDescriptor {
+	return file_app_dns_config_proto_enumTypes[2].Descriptor()
+}
+
+func (CacheStrategy) Type() protoreflect.EnumType {
+	return &file_app_dns_config_proto_enumTypes[2]
+}
+
+func (x CacheStrategy) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use CacheStrategy.Descriptor instead.
+func (CacheStrategy) EnumDescriptor() ([]byte, []int) {
+	return file_app_dns_config_proto_rawDescGZIP(), []int{2}
+}
+
 type NameServer struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	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"`
+	SkipFallback      bool                         `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,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 +229,20 @@ func (x *NameServer) GetAddress() *net.Endpoint {
 	return nil
 }
 
+func (x *NameServer) GetClientIp() []byte {
+	if x != nil {
+		return x.ClientIp
+	}
+	return nil
+}
+
+func (x *NameServer) GetSkipFallback() bool {
+	if x != nil {
+		return x.SkipFallback
+	}
+	return false
+}
+
 func (x *NameServer) GetPrioritizedDomain() []*NameServer_PriorityDomain {
 	if x != nil {
 		return x.PrioritizedDomain
@@ -174,6 +288,10 @@ 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 disables DNS cache
+	CacheStrategy   CacheStrategy `protobuf:"varint,8,opt,name=cache_strategy,json=cacheStrategy,proto3,enum=xray.app.dns.CacheStrategy" json:"cache_strategy,omitempty"`
+	QueryStrategy   QueryStrategy `protobuf:"varint,9,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
+	DisableFallback bool          `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"`
 }
 
 func (x *Config) Reset() {
@@ -252,6 +370,27 @@ func (x *Config) GetTag() string {
 	return ""
 }
 
+func (x *Config) GetCacheStrategy() CacheStrategy {
+	if x != nil {
+		return x.CacheStrategy
+	}
+	return CacheStrategy_Cache_ALL
+}
+
+func (x *Config) GetQueryStrategy() QueryStrategy {
+	if x != nil {
+		return x.QueryStrategy
+	}
+	return QueryStrategy_USE_IP
+}
+
+func (x *Config) GetDisableFallback() bool {
+	if x != nil {
+		return x.DisableFallback
+	}
+	return false
+}
+
 type NameServer_PriorityDomain struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -371,8 +510,7 @@ type Config_HostMapping struct {
 	Domain string             `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
 	Ip     [][]byte           `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"`
 	// ProxiedDomain indicates the mapped domain has the same IP address on this
-	// domain. Xray will use this domain for IP queries. This field is only
-	// effective if ip is empty.
+	// domain. Xray will use this domain for IP queries.
 	ProxiedDomain string `protobuf:"bytes,4,opt,name=proxied_domain,json=proxiedDomain,proto3" json:"proxied_domain,omitempty"`
 }
 
@@ -446,77 +584,101 @@ 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, 0xee, 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,
+	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, 0x22, 0x0a, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x46, 0x61,
+	0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b,
+	0x69, 0x70, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 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,
-	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,
+	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, 0xd7, 0x05, 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, 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,
+	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, 0x42, 0x0a, 0x0e,
+	0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x08,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
+	0x64, 0x6e, 0x73, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
+	0x79, 0x52, 0x0d, 0x63, 0x61, 0x63, 0x68, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79,
+	0x12, 0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65,
+	0x67, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
+	0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72,
+	0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61,
+	0x74, 0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46,
+	0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64,
+	0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 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, 0x2a, 0x35, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79,
+	0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f,
+	0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10,
+	0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x2a, 0x44,
+	0x0a, 0x0d, 0x43, 0x61, 0x63, 0x68, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12,
+	0x0d, 0x0a, 0x09, 0x43, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x41, 0x4c, 0x4c, 0x10, 0x00, 0x12, 0x11,
+	0x0a, 0x0d, 0x43, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x4e, 0x4f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10,
+	0x01, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42,
+	0x4c, 0x45, 0x10, 0x02, 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 (
@@ -531,37 +693,41 @@ func file_app_dns_config_proto_rawDescGZIP() []byte {
 	return file_app_dns_config_proto_rawDescData
 }
 
-var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
 var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
 var file_app_dns_config_proto_goTypes = []interface{}{
 	(DomainMatchingType)(0),           // 0: xray.app.dns.DomainMatchingType
-	(*NameServer)(nil),                // 1: xray.app.dns.NameServer
-	(*Config)(nil),                    // 2: xray.app.dns.Config
-	(*NameServer_PriorityDomain)(nil), // 3: xray.app.dns.NameServer.PriorityDomain
-	(*NameServer_OriginalRule)(nil),   // 4: xray.app.dns.NameServer.OriginalRule
-	nil,                               // 5: xray.app.dns.Config.HostsEntry
-	(*Config_HostMapping)(nil),        // 6: xray.app.dns.Config.HostMapping
-	(*net.Endpoint)(nil),              // 7: xray.common.net.Endpoint
-	(*router.GeoIP)(nil),              // 8: xray.app.router.GeoIP
-	(*net.IPOrDomain)(nil),            // 9: xray.common.net.IPOrDomain
+	(QueryStrategy)(0),                // 1: xray.app.dns.QueryStrategy
+	(CacheStrategy)(0),                // 2: xray.app.dns.CacheStrategy
+	(*NameServer)(nil),                // 3: xray.app.dns.NameServer
+	(*Config)(nil),                    // 4: xray.app.dns.Config
+	(*NameServer_PriorityDomain)(nil), // 5: xray.app.dns.NameServer.PriorityDomain
+	(*NameServer_OriginalRule)(nil),   // 6: xray.app.dns.NameServer.OriginalRule
+	nil,                               // 7: xray.app.dns.Config.HostsEntry
+	(*Config_HostMapping)(nil),        // 8: xray.app.dns.Config.HostMapping
+	(*net.Endpoint)(nil),              // 9: xray.common.net.Endpoint
+	(*router.GeoIP)(nil),              // 10: xray.app.router.GeoIP
+	(*net.IPOrDomain)(nil),            // 11: xray.common.net.IPOrDomain
 }
 var file_app_dns_config_proto_depIdxs = []int32{
-	7,  // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint
-	3,  // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain
-	8,  // 2: xray.app.dns.NameServer.geoip:type_name -> xray.app.router.GeoIP
-	4,  // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule
-	7,  // 4: xray.app.dns.Config.NameServers:type_name -> xray.common.net.Endpoint
-	1,  // 5: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer
-	5,  // 6: xray.app.dns.Config.Hosts:type_name -> xray.app.dns.Config.HostsEntry
-	6,  // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping
-	0,  // 8: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType
-	9,  // 9: xray.app.dns.Config.HostsEntry.value:type_name -> xray.common.net.IPOrDomain
-	0,  // 10: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType
-	11, // [11:11] is the sub-list for method output_type
-	11, // [11:11] is the sub-list for method input_type
-	11, // [11:11] is the sub-list for extension type_name
-	11, // [11:11] is the sub-list for extension extendee
-	0,  // [0:11] is the sub-list for field type_name
+	9,  // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint
+	5,  // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain
+	10, // 2: xray.app.dns.NameServer.geoip:type_name -> xray.app.router.GeoIP
+	6,  // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule
+	9,  // 4: xray.app.dns.Config.NameServers:type_name -> xray.common.net.Endpoint
+	3,  // 5: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer
+	7,  // 6: xray.app.dns.Config.Hosts:type_name -> xray.app.dns.Config.HostsEntry
+	8,  // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping
+	2,  // 8: xray.app.dns.Config.cache_strategy:type_name -> xray.app.dns.CacheStrategy
+	1,  // 9: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy
+	0,  // 10: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType
+	11, // 11: xray.app.dns.Config.HostsEntry.value:type_name -> xray.common.net.IPOrDomain
+	0,  // 12: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType
+	13, // [13:13] is the sub-list for method output_type
+	13, // [13:13] is the sub-list for method input_type
+	13, // [13:13] is the sub-list for extension type_name
+	13, // [13:13] is the sub-list for extension extendee
+	0,  // [0:13] is the sub-list for field type_name
 }
 
 func init() { file_app_dns_config_proto_init() }
@@ -636,7 +802,7 @@ func file_app_dns_config_proto_init() {
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_app_dns_config_proto_rawDesc,
-			NumEnums:      1,
+			NumEnums:      3,
 			NumMessages:   6,
 			NumExtensions: 0,
 			NumServices:   0,

+ 22 - 2
app/dns/config.proto

@@ -12,6 +12,8 @@ import "app/router/config.proto";
 
 message NameServer {
   xray.common.net.Endpoint address = 1;
+  bytes client_ip = 5;
+  bool skipFallback = 6;
 
   message PriorityDomain {
     DomainMatchingType type = 1;
@@ -35,6 +37,18 @@ enum DomainMatchingType {
   Regex = 3;
 }
 
+enum QueryStrategy {
+  USE_IP = 0;
+  USE_IP4 = 1;
+  USE_IP6 = 2;
+}
+
+enum CacheStrategy {
+  Cache_ALL = 0;
+  Cache_NOERROR = 1;
+  Cache_DISABLE = 2;
+}
+
 message Config {
   // Nameservers used by this DNS. Only traditional UDP servers are support at
   // the moment. A special value 'localhost' as a domain address can be set to
@@ -59,8 +73,7 @@ message Config {
     repeated bytes ip = 3;
 
     // ProxiedDomain indicates the mapped domain has the same IP address on this
-    // domain. Xray will use this domain for IP queries. This field is only
-    // effective if ip is empty.
+    // domain. Xray will use this domain for IP queries.
     string proxied_domain = 4;
   }
 
@@ -70,4 +83,11 @@ message Config {
   string tag = 6;
 
   reserved 7;
+
+  // DisableCache disables DNS cache
+  CacheStrategy cache_strategy = 8;
+
+  QueryStrategy query_strategy = 9;
+
+  bool disableFallback = 10;
 }

+ 309 - 0
app/dns/dns.go

@@ -2,3 +2,312 @@
 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
+	cacheStrategy   CacheStrategy
+	disableFallback bool
+	ipOption        *dns.IPOption
+	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))
+	}
+
+	var ipOption *dns.IPOption
+	switch config.QueryStrategy {
+	case QueryStrategy_USE_IP:
+		ipOption = &dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		}
+	case QueryStrategy_USE_IP4:
+		ipOption = &dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+			FakeEnable: false,
+		}
+	case QueryStrategy_USE_IP6:
+		ipOption = &dns.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: true,
+			FakeEnable: false,
+		}
+	}
+
+	hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts)
+	if err != nil {
+		return nil, newError("failed to create hosts").Base(err)
+	}
+
+	clients := []*Client{}
+	domainRuleCount := 0
+	for _, ns := range config.NameServer {
+		domainRuleCount += len(ns.PrioritizedDomain)
+	}
+
+	// MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1
+	matcherInfos := make([]DomainMatcherInfo, domainRuleCount+1)
+	domainMatcher := &strmatcher.MatcherGroup{}
+	geoipContainer := router.GeoIPMatcherContainer{}
+
+	for _, endpoint := range config.NameServers {
+		features.PrintDeprecatedFeatureWarning("simple DNS server")
+		client, err := NewSimpleClient(ctx, endpoint, clientIP)
+		if err != nil {
+			return nil, newError("failed to create client").Base(err)
+		}
+		clients = append(clients, client)
+	}
+
+	for _, ns := range config.NameServer {
+		clientIdx := len(clients)
+		updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []DomainMatcherInfo) error {
+			midx := domainMatcher.Add(domainRule)
+			matcherInfos[midx] = DomainMatcherInfo{
+				clientIdx:     uint16(clientIdx),
+				domainRuleIdx: uint16(originalRuleIdx),
+			}
+			return nil
+		}
+
+		myClientIP := clientIP
+		switch len(ns.ClientIp) {
+		case net.IPv4len, net.IPv6len:
+			myClientIP = net.IP(ns.ClientIp)
+		}
+		client, err := NewClient(ctx, ns, myClientIP, geoipContainer, &matcherInfos, updateDomain)
+		if err != nil {
+			return nil, newError("failed to create client").Base(err)
+		}
+		clients = append(clients, client)
+	}
+
+	// If there is no DNS client in config, add a `localhost` DNS client
+	if len(clients) == 0 {
+		clients = append(clients, NewLocalDNSClient())
+	}
+
+	return &DNS{
+		tag:             tag,
+		hosts:           hosts,
+		ipOption:        ipOption,
+		clients:         clients,
+		ctx:             ctx,
+		domainMatcher:   domainMatcher,
+		matcherInfos:    matcherInfos,
+		cacheStrategy:   config.CacheStrategy,
+		disableFallback: config.DisableFallback,
+	}, nil
+}
+
+// Type implements common.HasType.
+func (*DNS) Type() interface{} {
+	return dns.ClientType()
+}
+
+// Start implements common.Runnable.
+func (s *DNS) Start() error {
+	return nil
+}
+
+// Close implements common.Closable.
+func (s *DNS) Close() error {
+	return nil
+}
+
+// IsOwnLink implements proxy.dns.ownLinkVerifier
+func (s *DNS) IsOwnLink(ctx context.Context) bool {
+	inbound := session.InboundFromContext(ctx)
+	return inbound != nil && inbound.Tag == s.tag
+}
+
+// LookupIP implements dns.Client.
+func (s *DNS) LookupIP(domain string) ([]net.IP, error) {
+	return s.lookupIPInternal(domain, s.ipOption.Copy())
+}
+
+// LookupOptions implements dns.Client.
+func (s *DNS) LookupOptions(domain string, opts ...dns.Option) ([]net.IP, error) {
+	opt := s.ipOption.Copy()
+	for _, o := range opts {
+		if o != nil {
+			o(opt)
+		}
+	}
+
+	return s.lookupIPInternal(domain, opt)
+}
+
+// LookupIPv4 implements dns.IPv4Lookup.
+func (s *DNS) LookupIPv4(domain string) ([]net.IP, error) {
+	return s.lookupIPInternal(domain, &dns.IPOption{
+		IPv4Enable: true,
+	})
+}
+
+// LookupIPv6 implements dns.IPv6Lookup.
+func (s *DNS) LookupIPv6(domain string) ([]net.IP, error) {
+	return s.lookupIPInternal(domain, &dns.IPOption{
+		IPv6Enable: true,
+	})
+}
+
+func (s *DNS) lookupIPInternal(domain string, option *dns.IPOption) ([]net.IP, error) {
+	if domain == "" {
+		return nil, newError("empty domain name")
+	}
+	if isQuery(option) {
+		return nil, newError("empty option: Impossible.").AtWarning()
+	}
+
+	// Normalize the FQDN form query
+	if strings.HasSuffix(domain, ".") {
+		domain = domain[:len(domain)-1]
+	}
+
+	// Static host lookup
+	switch addrs := s.hosts.Lookup(domain, option); {
+	case addrs == nil: // Domain not recorded in static host
+		break
+	case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled)
+		return nil, dns.ErrEmptyResponse
+	case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement
+		newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog()
+		domain = addrs[0].Domain()
+	default:
+		// Successfully found ip records in static host.
+		// Skip hosts mapping result in FakeDNS query.
+		if isIPQuery(option) {
+			newError("returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs).WriteToLog()
+			return toNetIP(addrs)
+		}
+	}
+
+	// Name servers lookup
+	errs := []error{}
+	ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag})
+	for _, client := range s.sortClients(domain, option) {
+		ips, err := client.QueryIP(ctx, domain, *option, s.cacheStrategy)
+		if len(ips) > 0 {
+			return ips, nil
+		}
+		if err != nil {
+			newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog()
+			errs = append(errs, err)
+		}
+		if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch {
+			return nil, err
+		}
+	}
+
+	return nil, newError("returning nil for domain ", domain).Base(errors.Combine(errs...))
+}
+
+func (s *DNS) sortClients(domain string, option *dns.IPOption) []*Client {
+	clients := make([]*Client, 0, len(s.clients))
+	clientUsed := make([]bool, len(s.clients))
+	clientNames := make([]string, 0, len(s.clients))
+	domainRules := []string{}
+
+	defer func() {
+		if len(domainRules) > 0 {
+			newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog()
+		}
+		if len(clientNames) > 0 {
+			newError("domain ", domain, " will use DNS in order: ", clientNames).AtDebug().WriteToLog()
+		}
+		if len(clients) == 0 {
+			clients = append(clients, s.clients[0])
+			clientNames = append(clientNames, s.clients[0].Name())
+			newError("domain ", domain, " will use the first DNS: ", clientNames).AtDebug().WriteToLog()
+		}
+	}()
+
+	// Priority domain matching
+	for _, match := range s.domainMatcher.Match(domain) {
+		info := s.matcherInfos[match]
+		client := s.clients[info.clientIdx]
+		domainRule := client.domains[info.domainRuleIdx]
+		if !canQueryOnClient(option, client) {
+			newError("skipping the client " + client.Name()).AtDebug().WriteToLog()
+			continue
+		}
+		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())
+	}
+
+	if !s.disableFallback {
+		// Default round-robin query
+		for idx, client := range s.clients {
+			if clientUsed[idx] || client.skipFallback {
+				continue
+			}
+
+			if !canQueryOnClient(option, client) {
+				newError("skipping the client " + client.Name()).AtDebug().WriteToLog()
+				continue
+			}
+
+			clientUsed[idx] = true
+			clients = append(clients, client)
+			clientNames = append(clientNames, client.Name())
+		}
+	}
+
+	return clients
+}
+
+func init() {
+	common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+		return New(ctx, config.(*Config))
+	}))
+}

+ 27 - 112
app/dns/server_test.go → app/dns/dns_test.go

@@ -101,8 +101,8 @@ func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 			rr, _ := dns.NewRR("localhost-b. IN A 127.0.0.4")
 			ans.Answer = append(ans.Answer, rr)
 
-		case q.Name == "mijia\\ cloud." && q.Qtype == dns.TypeA:
-			rr, _ := dns.NewRR("mijia\\ cloud. IN A 127.0.0.1")
+		case q.Name == "Mijia\\ Cloud." && q.Qtype == dns.TypeA:
+			rr, _ := dns.NewRR("Mijia\\ Cloud. IN A 127.0.0.1")
 			ans.Answer = append(ans.Answer, rr)
 		}
 	}
@@ -154,11 +154,7 @@ func TestUDPServerSubnet(t *testing.T) {
 
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
 
-	ips, err := client.LookupIP("google.com", feature_dns.IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-		FakeEnable: false,
-	})
+	ips, err := client.LookupIP("google.com")
 	if err != nil {
 		t.Fatal("unexpected error: ", err)
 	}
@@ -213,11 +209,7 @@ func TestUDPServer(t *testing.T) {
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
 
 	{
-		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -228,11 +220,7 @@ func TestUDPServer(t *testing.T) {
 	}
 
 	{
-		ips, err := client.LookupIP("facebook.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("facebook.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -243,11 +231,7 @@ func TestUDPServer(t *testing.T) {
 	}
 
 	{
-		_, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		_, err := client.LookupIP("notexist.google.com")
 		if err == nil {
 			t.Fatal("nil error")
 		}
@@ -257,11 +241,8 @@ func TestUDPServer(t *testing.T) {
 	}
 
 	{
-		ips, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{
-			IPv4Enable: false,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		clientv6 := client.(feature_dns.IPv6Lookup)
+		ips, err := clientv6.LookupIPv6("ipv4only.google.com")
 		if err != feature_dns.ErrEmptyResponse {
 			t.Fatal("error: ", err)
 		}
@@ -273,11 +254,7 @@ func TestUDPServer(t *testing.T) {
 	dnsServer.Shutdown()
 
 	{
-		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -354,11 +331,7 @@ func TestPrioritizedDomain(t *testing.T) {
 	startTime := time.Now()
 
 	{
-		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -417,12 +390,9 @@ func TestUDPServerIPv6(t *testing.T) {
 	common.Must(err)
 
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
+	client6 := client.(feature_dns.IPv6Lookup)
 	{
-		ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
-			IPv4Enable: false,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client6.LookupIPv6("ipv6.google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -485,11 +455,7 @@ func TestStaticHostDomain(t *testing.T) {
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
 
 	{
-		ips, err := client.LookupIP("example.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("example.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -596,11 +562,7 @@ func TestIPMatch(t *testing.T) {
 	startTime := time.Now()
 
 	{
-		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -719,11 +681,7 @@ func TestLocalDomain(t *testing.T) {
 	startTime := time.Now()
 
 	{ // Will match dotless:
-		ips, err := client.LookupIP("hostname", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("hostname")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -734,11 +692,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match domain:local
-		ips, err := client.LookupIP("hostname.local", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("hostname.local")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -749,11 +703,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match static ip
-		ips, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("hostnamestatic")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -764,11 +714,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match domain replacing
-		ips, err := client.LookupIP("hostnamealias", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("hostnamealias")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -779,11 +725,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
-		ips, err := client.LookupIP("localhost", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("localhost")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -794,11 +736,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
-		ips, err := client.LookupIP("localhost-a", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("localhost-a")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -809,11 +747,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
-		ips, err := client.LookupIP("localhost-b", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("localhost-b")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -824,11 +758,7 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:
-		ips, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("Mijia Cloud")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -990,11 +920,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	startTime := time.Now()
 
 	{ // Will match server 1,2 and server 1 returns expected ip
-		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -1005,11 +931,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	}
 
 	{ // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one
-		ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: false,
-			FakeEnable: false,
-		})
+		clientv4 := client.(feature_dns.IPv4Lookup)
+		ips, err := clientv4.LookupIPv4("ipv6.google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -1020,11 +943,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	}
 
 	{ // Will match server 3,1,2 and server 3 returns expected one
-		ips, err := client.LookupIP("api.google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("api.google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -1035,11 +954,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	}
 
 	{ // Will match server 4,3,1,2 and server 4 returns expected one
-		ips, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := client.LookupIP("v2.api.google.com")
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}

+ 31 - 40
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)
@@ -66,6 +47,9 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
 		id := g.Add(matcher)
 		ips := make([]net.Address, 0, len(mapping.Ip)+1)
 		switch {
+		case len(mapping.ProxiedDomain) > 0:
+			ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
+
 		case len(mapping.Ip) > 0:
 			for _, ip := range mapping.Ip {
 				addr := net.IPAddress(ip)
@@ -75,49 +59,56 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
 				ips = append(ips, addr)
 			}
 
-		case len(mapping.ProxiedDomain) > 0:
-			ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
-
 		default:
 			return nil, newError("neither IP address nor proxied domain specified for domain: ", mapping.Domain).AtWarning()
 		}
 
-		// Special handling for localhost IPv6. This is a dirty workaround as JSON config supports only single IP mapping.
-		if len(ips) == 1 && ips[0] == net.LocalHostIP {
-			ips = append(ips, net.LocalHostIPv6)
-		}
-
 		sh.ips[id] = ips
 	}
 
 	return sh, nil
 }
 
-func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
+func filterIP(ips []net.Address, option *dns.IPOption) []net.Address {
 	filtered := make([]net.Address, 0, len(ips))
 	for _, ip := range ips {
 		if (ip.Family().IsIPv4() && option.IPv4Enable) || (ip.Family().IsIPv6() && option.IPv6Enable) {
 			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
+		newError("found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it").AtDebug().WriteToLog()
+		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)
 }

+ 44 - 3
app/dns/hosts_test.go

@@ -20,6 +20,20 @@ func TestStaticHosts(t *testing.T) {
 				{1, 1, 1, 1},
 			},
 		},
+		{
+			Type:   DomainMatchingType_Full,
+			Domain: "proxy.example.com",
+			Ip: [][]byte{
+				{1, 2, 3, 4},
+				{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+			},
+			ProxiedDomain: "another-proxy.example.com",
+		},
+		{
+			Type:          DomainMatchingType_Full,
+			Domain:        "proxy2.example.com",
+			ProxiedDomain: "proxy.example.com",
+		},
 		{
 			Type:   DomainMatchingType_Subdomain,
 			Domain: "example.cn",
@@ -32,6 +46,7 @@ func TestStaticHosts(t *testing.T) {
 			Domain: "baidu.com",
 			Ip: [][]byte{
 				{127, 0, 0, 1},
+				{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
 			},
 		},
 	}
@@ -40,7 +55,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 +68,33 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("www.example.cn", dns.IPOption{
+		domain := hosts.Lookup("proxy.example.com", &dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+		})
+		if len(domain) != 1 {
+			t.Error("expect 1 domain, but got ", len(domain))
+		}
+		if diff := cmp.Diff(domain[0].Domain(), "another-proxy.example.com"); diff != "" {
+			t.Error(diff)
+		}
+	}
+
+	{
+		domain := hosts.Lookup("proxy2.example.com", &dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+		})
+		if len(domain) != 1 {
+			t.Error("expect 1 domain, but got ", len(domain))
+		}
+		if diff := cmp.Diff(domain[0].Domain(), "another-proxy.example.com"); diff != "" {
+			t.Error(diff)
+		}
+	}
+
+	{
+		ips := hosts.Lookup("www.example.cn", &dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -66,7 +107,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("baidu.com", dns.IPOption{
+		ips := hosts.Lookup("baidu.com", &dns.IPOption{
 			IPv4Enable: false,
 			IPv6Enable: true,
 		})

+ 188 - 17
app/dns/nameserver.go

@@ -2,40 +2,211 @@ 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, cs CacheStrategy) ([]net.IP, error)
+}
+
+// Client is the interface for DNS client.
+type Client struct {
+	server       Server
+	clientIP     net.IP
+	skipFallback bool
+	domains      []string
+	expectIPs    []*router.GeoIPMatcher
+}
+
+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()
 }
 
-type LocalNameServer struct {
-	client *localdns.Client
+// 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()
+			}
+		}
+
+		client.server = server
+		client.clientIP = clientIP
+		client.domains = rules
+		client.expectIPs = matchers
+		return nil
+	})
+	return client, err
 }
 
-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)
+// 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
 }
 
-func (s *LocalNameServer) Name() string {
-	return "localhost"
+// Name returns the server name the client manages.
+func (c *Client) Name() string {
+	return c.server.Name()
 }
 
-func NewLocalNameServer() *LocalNameServer {
-	newError("DNS: created localhost client").AtInfo().WriteToLog()
-	return &LocalNameServer{
-		client: localdns.New(),
+// 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, cs CacheStrategy) ([]net.IP, error) {
+	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
+	ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option, cs)
+	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
 }

+ 38 - 26
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{
@@ -61,7 +61,8 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.
 				return nil, err
 			}
 
-			dispatcherCtx = session.ContextWithContent(dispatcherCtx, &session.Content{Protocol: "tls"})
+			dispatcherCtx = session.ContextWithContent(dispatcherCtx, session.ContentFromContext(ctx))
+			dispatcherCtx = session.ContextWithInbound(dispatcherCtx, session.InboundFromContext(ctx))
 			dispatcherCtx = log.ContextWithAccessMessage(dispatcherCtx, &log.AccessMessage{
 				From:   "DoH",
 				To:     s.dohURL,
@@ -79,6 +80,12 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.
 			if err != nil {
 				return nil, err
 			}
+			log.Record(&log.AccessMessage{
+				From:   "DoH",
+				To:     s.dohURL,
+				Status: log.AccessAccepted,
+				Detour: "local",
+			})
 
 			cc := common.ChainedClosable{}
 			if cw, ok := link.Writer.(common.Closable); ok {
@@ -103,9 +110,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,
@@ -135,13 +142,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,
@@ -151,7 +157,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
 }
@@ -234,7 +240,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 {
@@ -242,7 +248,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 {
@@ -263,8 +269,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
@@ -348,7 +354,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 {
@@ -362,15 +368,21 @@ 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, cs CacheStrategy) ([]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 cs == CacheStrategy_Cache_DISABLE {
+		newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog()
+	} else {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			if cs == CacheStrategy_Cache_NOERROR && err == nil {
+				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
+			}
+		}
 	}
 
 	// ipv4 and ipv6 belong to different subscription groups
@@ -399,7 +411,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,
+	}, CacheStrategy_Cache_ALL)
+	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,
+	}, CacheStrategy_Cache_ALL)
+	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,
+	}, CacheStrategy_Cache_ALL)
+	cancel()
+	common.Must(err)
+	if r := cmp.Diff(ips2, ips); r != "" {
+		t.Fatal(r)
+	}
+}

+ 7 - 5
app/dns/nameserver_fakedns.go

@@ -16,11 +16,13 @@ func NewFakeDNSServer() *FakeDNSServer {
 	return &FakeDNSServer{}
 }
 
+const FakeDNSName = "FakeDNS"
+
 func (FakeDNSServer) Name() string {
-	return "FakeDNS"
+	return FakeDNSName
 }
 
-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, _ CacheStrategy) ([]net.IP, error) {
 	if f.fakeDNSEngine == nil {
 		if err := core.RequireFeatures(ctx, func(fd dns.FakeDNSEngine) {
 			f.fakeDNSEngine = fd
@@ -30,9 +32,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()

+ 53 - 0
app/dns/nameserver_local.go

@@ -0,0 +1,53 @@
+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, _ CacheStrategy) ([]net.IP, error) {
+	var ips []net.IP
+	var err error
+
+	switch {
+	case option.IPv4Enable && option.IPv6Enable:
+		ips, err = s.client.LookupIP(domain)
+	case option.IPv4Enable:
+		ips, err = s.client.LookupIPv4(domain)
+	case option.IPv6Enable:
+		ips, err = s.client.LookupIPv6(domain)
+	}
+
+	if len(ips) > 0 {
+		newError("Localhost got answer: ", domain, " -> ", ips).AtInfo().WriteToLog()
+	}
+
+	return ips, err
+}
+
+// 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()}
+}

+ 5 - 5
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{
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{
 		IPv4Enable: true,
 		IPv6Enable: true,
-		FakeEnable: false,
-	})
+	}, CacheStrategy_Cache_ALL)
 	cancel()
 	common.Must(err)
 	if len(ips) == 0 {

+ 394 - 0
app/dns/nameserver_quic.go

@@ -0,0 +1,394 @@
+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/log"
+	"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"
+
+// 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,
+			})
+			dnsCtx = log.ContextWithAccessMessage(dnsCtx, &log.AccessMessage{
+				From:   "DoQ",
+				To:     s.name,
+				Status: log.AccessAccepted,
+				Reason: "",
+			})
+
+			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, cs CacheStrategy) ([]net.IP, error) {
+	fqdn := Fqdn(domain)
+
+	if cs == CacheStrategy_Cache_DISABLE {
+		newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog()
+	} else {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			if cs == CacheStrategy_Cache_NOERROR && err == nil {
+				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
+			}
+		}
+	}
+
+	// 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)
+	start := time.Now()
+
+	for {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			log.Record(&log.DNSLog{s.name, domain, ips, log.DNSQueried, time.Since(start), err})
+			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{}
+
+	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*5)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, CacheStrategy_Cache_ALL)
+	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*5)
+	ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, CacheStrategy_Cache_ALL)
+	cancel()
+	common.Must(err)
+	if len(ips) == 0 {
+		t.Error("expect some ips, but got 0")
+	}
+
+	ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+	}, CacheStrategy_Cache_ALL)
+	cancel()
+	common.Must(err)
+	if r := cmp.Diff(ips2, ips); r != "" {
+		t.Fatal(r)
+	}
+}

+ 21 - 15
app/dns/udpns.go → app/dns/nameserver_udp.go

@@ -2,7 +2,6 @@ package dns
 
 import (
 	"context"
-	"github.com/xtls/xray-core/transport/internet"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -32,10 +31,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 +44,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 +56,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 +103,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 +181,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)
@@ -192,7 +193,6 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option
 		if inbound := session.InboundFromContext(ctx); inbound != nil {
 			udpCtx = session.ContextWithInbound(udpCtx, inbound)
 		}
-		udpCtx = internet.ContextWithLookupDomain(udpCtx, internet.LookupDomainFromContext(ctx))
 		udpCtx = session.ContextWithContent(udpCtx, &session.Content{
 			Protocol: "dns",
 		})
@@ -234,7 +234,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 +245,20 @@ 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, cs CacheStrategy) ([]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 cs == CacheStrategy_Cache_DISABLE {
+		newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog()
+	} else {
+		ips, err := s.findIPsForDomain(fqdn, option)
+		if err != errRecordNotFound {
+			if cs == CacheStrategy_Cache_NOERROR && err == nil {
+				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
+			}
+		}
 	}
 
 	// 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 {

+ 16 - 0
app/dns/options.go

@@ -0,0 +1,16 @@
+package dns
+
+import "github.com/xtls/xray-core/features/dns"
+
+func isIPQuery(o *dns.IPOption) bool {
+	return o.IPv4Enable || o.IPv6Enable
+}
+
+func canQueryOnClient(o *dns.IPOption, c *Client) bool {
+	isIPClient := !(c.Name() == FakeDNSName)
+	return isIPClient && isIPQuery(o)
+}
+
+func isQuery(o *dns.IPOption) bool {
+	return !(o.IPv4Enable || o.IPv6Enable || o.FakeEnable)
+}

+ 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
 	}
 

+ 2 - 11
app/router/router_test.go

@@ -9,7 +9,6 @@ import (
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/session"
-	"github.com/xtls/xray-core/features/dns"
 	"github.com/xtls/xray-core/features/outbound"
 	routing_session "github.com/xtls/xray-core/features/routing/session"
 	"github.com/xtls/xray-core/testing/mocks"
@@ -116,11 +115,7 @@ func TestIPOnDemand(t *testing.T) {
 	defer mockCtl.Finish()
 
 	mockDNS := mocks.NewDNSClient(mockCtl)
-	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com"), dns.IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-		FakeEnable: false,
-	}).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
+	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
 
 	r := new(Router)
 	common.Must(r.Init(config, mockDNS, nil))
@@ -155,11 +150,7 @@ func TestIPIfNonMatchDomain(t *testing.T) {
 	defer mockCtl.Finish()
 
 	mockDNS := mocks.NewDNSClient(mockCtl)
-	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com"), dns.IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-		FakeEnable: false,
-	}).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
+	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
 
 	r := new(Router)
 	common.Must(r.Init(config, mockDNS, nil))

+ 1 - 1
common/buf/buffer.go

@@ -38,7 +38,7 @@ func NewExisted(b []byte) *Buffer {
 
 	oLen := len(b)
 	if oLen < Size {
-		b = append(b, make([]byte, Size-oLen)...)
+		b = b[:Size]
 	}
 
 	return &Buffer{

+ 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.

+ 14 - 3
core/config.go

@@ -80,14 +80,25 @@ func getFormat(filename string) string {
 func LoadConfig(formatName string, input interface{}) (*Config, error) {
 	switch v := input.(type) {
 	case cmdarg.Arg:
-
 		formats := make([]string, len(v))
 		hasProtobuf := false
 		for i, file := range v {
-			f := getFormat(file)
-			if f == "" {
+			var f string
+
+			if formatName == "auto" {
+				if file != "stdin:" {
+					f = getFormat(file)
+				} else {
+					f = "json"
+				}
+			} else {
 				f = formatName
 			}
+
+			if f == "" {
+				return nil, newError("Failed to get format of ", file).AtWarning()
+			}
+
 			if f == "protobuf" {
 				hasProtobuf = true
 			}

+ 1 - 1
core/core.go

@@ -18,7 +18,7 @@ import (
 )
 
 var (
-	version  = "1.4.0"
+	version  = "1.4.2"
 	build    = "Custom"
 	codename = "Xray, Penetrates Everything."
 	intro    = "A unified platform for anti-censorship."

+ 56 - 1
features/dns/client.go

@@ -14,6 +14,12 @@ type IPOption struct {
 	FakeEnable bool
 }
 
+func (p *IPOption) Copy() *IPOption {
+	return &IPOption{p.IPv4Enable, p.IPv6Enable, p.FakeEnable}
+}
+
+type Option func(dopt *IPOption) *IPOption
+
 // Client is a Xray feature for querying DNS information.
 //
 // xray:api:stable
@@ -21,7 +27,24 @@ type Client interface {
 	features.Feature
 
 	// LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses.
-	LookupIP(domain string, option IPOption) ([]net.IP, error)
+	LookupIP(domain string) ([]net.IP, error)
+
+	// LookupOptions query IP address for domain with *IPOption.
+	LookupOptions(domain string, opt ...Option) ([]net.IP, error)
+}
+
+// IPv4Lookup is an optional feature for querying IPv4 addresses only.
+//
+// xray:api:beta
+type IPv4Lookup interface {
+	LookupIPv4(domain string) ([]net.IP, error)
+}
+
+// IPv6Lookup is an optional feature for querying IPv6 addresses only.
+//
+// xray:api:beta
+type IPv6Lookup interface {
+	LookupIPv6(domain string) ([]net.IP, error)
 }
 
 // ClientType returns the type of Client interface. Can be used for implementing common.HasType.
@@ -50,3 +73,35 @@ func RCodeFromError(err error) uint16 {
 	}
 	return 0
 }
+
+var (
+	LookupIPv4Only = func(d *IPOption) *IPOption {
+		d.IPv4Enable = true
+		d.IPv6Enable = false
+		return d
+	}
+	LookupIPv6Only = func(d *IPOption) *IPOption {
+		d.IPv4Enable = false
+		d.IPv6Enable = true
+		return d
+	}
+	LookupIP = func(d *IPOption) *IPOption {
+		d.IPv4Enable = true
+		d.IPv6Enable = true
+		return d
+	}
+	LookupFake = func(d *IPOption) *IPOption {
+		d.FakeEnable = true
+		return d
+	}
+	LookupNoFake = func(d *IPOption) *IPOption {
+		d.FakeEnable = false
+		return d
+	}
+
+	LookupAll = func(d *IPOption) *IPOption {
+		LookupIP(d)
+		LookupFake(d)
+		return d
+	}
+)

+ 40 - 17
features/dns/localdns/client.go

@@ -20,41 +20,64 @@ func (*Client) Start() error { return nil }
 func (*Client) Close() error { return nil }
 
 // LookupIP implements Client.
-func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, error) {
+func (*Client) LookupIP(host string) ([]net.IP, error) {
 	ips, err := net.LookupIP(host)
 	if err != nil {
 		return nil, err
 	}
 	parsedIPs := make([]net.IP, 0, len(ips))
-	ipv4 := make([]net.IP, 0, len(ips))
-	ipv6 := make([]net.IP, 0, len(ips))
 	for _, ip := range ips {
 		parsed := net.IPAddress(ip)
 		if parsed != nil {
 			parsedIPs = append(parsedIPs, parsed.IP())
 		}
+	}
+	if len(parsedIPs) == 0 {
+		return nil, dns.ErrEmptyResponse
+	}
+	return parsedIPs, nil
+}
+
+// LookupOptions implements Client.
+func (c *Client) LookupOptions(host string, _ ...dns.Option) ([]net.IP, error) {
+	return c.LookupIP(host)
+}
+
+// LookupIPv4 implements IPv4Lookup.
+func (c *Client) LookupIPv4(host string) ([]net.IP, error) {
+	ips, err := c.LookupIP(host)
+	if err != nil {
+		return nil, err
+	}
+	ipv4 := make([]net.IP, 0, len(ips))
+	for _, ip := range ips {
 		if len(ip) == net.IPv4len {
 			ipv4 = append(ipv4, ip)
 		}
+	}
+	if len(ipv4) == 0 {
+		return nil, dns.ErrEmptyResponse
+	}
+	return ipv4, nil
+}
+
+// LookupIPv6 implements IPv6Lookup.
+func (c *Client) LookupIPv6(host string) ([]net.IP, error) {
+	ips, err := c.LookupIP(host)
+	if err != nil {
+		return nil, err
+	}
+	ipv6 := make([]net.IP, 0, len(ips))
+	for _, ip := range ips {
 		if len(ip) == net.IPv6len {
 			ipv6 = append(ipv6, ip)
 		}
 	}
-	switch {
-	case option.IPv4Enable && option.IPv6Enable:
-		if len(parsedIPs) > 0 {
-			return parsedIPs, nil
-		}
-	case option.IPv4Enable:
-		if len(ipv4) > 0 {
-			return ipv4, nil
-		}
-	case option.IPv6Enable:
-		if len(ipv6) > 0 {
-			return ipv6, nil
-		}
+	if len(ipv6) == 0 {
+		return nil, dns.ErrEmptyResponse
 	}
-	return nil, dns.ErrEmptyResponse
+
+	return ipv6, nil
 }
 
 // New create a new dns.Client that queries localhost for DNS.

+ 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
 }

+ 2 - 6
features/routing/dns/context.go

@@ -26,16 +26,12 @@ func (ctx *ResolvableContext) GetTargetIPs() []net.IP {
 	}
 
 	if domain := ctx.GetTargetDomain(); len(domain) != 0 {
-		ips, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: true,
-			FakeEnable: false,
-		})
+		ips, err := ctx.dnsClient.LookupIP(domain)
 		if err == nil {
 			ctx.resolvedIPs = ips
 			return ips
 		}
-		newError("resolve ip for ", domain).Base(err).WriteToLog()
+		newError("failed to resolve ip for ", domain).Base(err).WriteToLog()
 	}
 
 	return nil

+ 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{

+ 9 - 8
go.mod

@@ -5,22 +5,23 @@ go 1.16
 require (
 	github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
 	github.com/golang/mock v1.5.0
-	github.com/golang/protobuf v1.4.3
+	github.com/golang/protobuf v1.5.2
 	github.com/google/go-cmp v0.5.5
 	github.com/gorilla/websocket v1.4.2
-	github.com/lucas-clemente/quic-go v0.19.3
-	github.com/miekg/dns v1.1.40
+	github.com/lucas-clemente/quic-go v0.20.0
+	github.com/miekg/dns v1.1.41
 	github.com/pelletier/go-toml v1.8.1
 	github.com/pires/go-proxyproto v0.5.0
+	github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b
 	github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c
 	github.com/stretchr/testify v1.7.0
 	github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499
 	go.starlark.net v0.0.0-20210312235212-74c10e2c17dc
-	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
-	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
+	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
+	golang.org/x/net v0.0.0-20210330230544-e57232859fb2
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c
-	google.golang.org/grpc v1.36.0
-	google.golang.org/protobuf v1.25.0
+	golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44
+	google.golang.org/grpc v1.36.1
+	google.golang.org/protobuf v1.26.0
 	h12.io/socks v1.0.2
 )

+ 26 - 35
go.sum

@@ -43,12 +43,9 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
 github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
@@ -62,8 +59,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -98,19 +96,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lucas-clemente/quic-go v0.19.3 h1:eCDQqvGBB+kCTkA0XrAFtNe81FMa0/fn4QSoeAbmiF4=
-github.com/lucas-clemente/quic-go v0.19.3/go.mod h1:ADXpNbTQjq1hIzCpB+y/k5iz4n4z4IwqoLb94Kh5Hu8=
+github.com/lucas-clemente/quic-go v0.20.0 h1:FSU3YN5VnLafHR27Ejs1r1CYMS7XMyIVDzRewkDLNBw=
+github.com/lucas-clemente/quic-go v0.20.0/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
 github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
-github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc=
-github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs=
-github.com/marten-seemann/qtls-go1-15 v0.1.1 h1:LIH6K34bPVttyXnUWixk0bzH6/N07VxbSabxn5A5gZQ=
-github.com/marten-seemann/qtls-go1-15 v0.1.1/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
+github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
+github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
+github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
+github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
-github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
-github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
+github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
@@ -139,6 +137,8 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b h1:lzo71oHzQEz0fKMSjR0BpVzuh2hOHvJTxnN3Rnikmtg=
+github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA=
 github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
@@ -169,7 +169,6 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh
 github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
@@ -180,7 +179,6 @@ github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMI
 github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo=
 github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.starlark.net v0.0.0-20210312235212-74c10e2c17dc h1:pVkptfeOTFfx+zXZo7HEHN3d5LmhatBFvHdm/f2QnpY=
 go.starlark.net v0.0.0-20210312235212-74c10e2c17dc/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
 go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
@@ -191,14 +189,13 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -211,11 +208,11 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210330230544-e57232859fb2 h1:nGCZOty+lVDsc4H2qPFksI5Se296+V+GhMiL/TzmYNk=
+golang.org/x/net v0.0.0-20210330230544-e57232859fb2/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -235,22 +232,19 @@ golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c h1:coiPEfMv+ThsjULRDygLrJVlNE1gDdL2g65s0LhV2os=
-golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -267,7 +261,6 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -285,7 +278,6 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
 google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
 google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
@@ -293,12 +285,11 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE
 google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -307,8 +298,10 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -331,7 +324,5 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
 sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

+ 97 - 19
infra/conf/dns.go

@@ -11,10 +11,12 @@ import (
 )
 
 type NameServerConfig struct {
-	Address   *Address
-	Port      uint16
-	Domains   []string
-	ExpectIPs StringList
+	Address      *Address
+	ClientIP     *Address
+	Port         uint16
+	SkipFallback bool
+	Domains      []string
+	ExpectIPs    StringList
 }
 
 func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
@@ -25,14 +27,18 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 	}
 
 	var advanced struct {
-		Address   *Address   `json:"address"`
-		Port      uint16     `json:"port"`
-		Domains   []string   `json:"domains"`
-		ExpectIPs StringList `json:"expectIps"`
+		Address      *Address   `json:"address"`
+		ClientIP     *Address   `json:"clientIp"`
+		Port         uint16     `json:"port"`
+		SkipFallback bool       `json:"skipFallback"`
+		Domains      []string   `json:"domains"`
+		ExpectIPs    StringList `json:"expectIps"`
 	}
 	if err := json.Unmarshal(data, &advanced); err == nil {
 		c.Address = advanced.Address
+		c.ClientIP = advanced.ClientIP
 		c.Port = advanced.Port
+		c.SkipFallback = advanced.SkipFallback
 		c.Domains = advanced.Domains
 		c.ExpectIPs = advanced.ExpectIPs
 		return nil
@@ -87,12 +93,21 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 		return nil, newError("invalid IP rule: ", c.ExpectIPs).Base(err)
 	}
 
+	var myClientIP []byte
+	if c.ClientIP != nil {
+		if !c.ClientIP.Family().IsIP() {
+			return nil, newError("not an IP address:", c.ClientIP.String())
+		}
+		myClientIP = []byte(c.ClientIP.IP())
+	}
 	return &dns.NameServer{
 		Address: &net.Endpoint{
 			Network: net.Network_UDP,
 			Address: c.Address.Build(),
 			Port:    uint32(c.Port),
 		},
+		ClientIp:          myClientIP,
+		SkipFallback:      c.SkipFallback,
 		PrioritizedDomain: domains,
 		Geoip:             geoipList,
 		OriginalRules:     originalRules,
@@ -108,28 +123,72 @@ 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]*HostAddress `json:"hosts"`
+	ClientIP        *Address                `json:"clientIp"`
+	Tag             string                  `json:"tag"`
+	QueryStrategy   string                  `json:"queryStrategy"`
+	CacheStrategy   string                  `json:"cacheStrategy"`
+	DisableCache    bool                    `json:"disableCache"`
+	DisableFallback bool                    `json:"disableFallback"`
 }
 
-func getHostMapping(addr *Address) *dns.Config_HostMapping {
-	if addr.Family().IsIP() {
-		return &dns.Config_HostMapping{
-			Ip: [][]byte{[]byte(addr.IP())},
+type HostAddress struct {
+	addr  *Address
+	addrs []*Address
+}
+
+// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
+func (h *HostAddress) UnmarshalJSON(data []byte) error {
+	addr := new(Address)
+	var addrs []*Address
+	switch {
+	case json.Unmarshal(data, &addr) == nil:
+		h.addr = addr
+	case json.Unmarshal(data, &addrs) == nil:
+		h.addrs = addrs
+	default:
+		return newError("invalid address")
+	}
+	return nil
+}
+
+func getHostMapping(ha *HostAddress) *dns.Config_HostMapping {
+	if ha.addr != nil {
+		if ha.addr.Family().IsDomain() {
+			return &dns.Config_HostMapping{
+				ProxiedDomain: ha.addr.Domain(),
+			}
 		}
-	} else {
 		return &dns.Config_HostMapping{
-			ProxiedDomain: addr.Domain(),
+			Ip: [][]byte{ha.addr.IP()},
+		}
+	}
+
+	ips := make([][]byte, 0, len(ha.addrs))
+	for _, addr := range ha.addrs {
+		if addr.Family().IsDomain() {
+			return &dns.Config_HostMapping{
+				ProxiedDomain: addr.Domain(),
+			}
 		}
+		ips = append(ips, []byte(addr.IP()))
+	}
+	return &dns.Config_HostMapping{
+		Ip: ips,
 	}
 }
 
 // Build implements Buildable
 func (c *DNSConfig) Build() (*dns.Config, error) {
 	config := &dns.Config{
-		Tag: c.Tag,
+		Tag:             c.Tag,
+		CacheStrategy:   dns.CacheStrategy_Cache_ALL,
+		DisableFallback: c.DisableFallback,
+	}
+
+	if c.DisableCache {
+		config.CacheStrategy = dns.CacheStrategy_Cache_DISABLE
 	}
 
 	if c.ClientIP != nil {
@@ -139,6 +198,25 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
 		config.ClientIp = []byte(c.ClientIP.IP())
 	}
 
+	config.QueryStrategy = dns.QueryStrategy_USE_IP
+	switch strings.ToLower(c.QueryStrategy) {
+	case "useip", "use_ip", "use-ip":
+		config.QueryStrategy = dns.QueryStrategy_USE_IP
+	case "useip4", "useipv4", "use_ip4", "use_ipv4", "use_ip_v4", "use-ip4", "use-ipv4", "use-ip-v4":
+		config.QueryStrategy = dns.QueryStrategy_USE_IP4
+	case "useip6", "useipv6", "use_ip6", "use_ipv6", "use_ip_v6", "use-ip6", "use-ipv6", "use-ip-v6":
+		config.QueryStrategy = dns.QueryStrategy_USE_IP6
+	}
+
+	switch strings.ToLower(c.CacheStrategy) {
+	case "noerror":
+		config.CacheStrategy = dns.CacheStrategy_Cache_NOERROR
+	case "all":
+		config.CacheStrategy = dns.CacheStrategy_Cache_ALL
+	case "disable", "none":
+		config.CacheStrategy = dns.CacheStrategy_Cache_DISABLE
+	}
+
 	for _, server := range c.Servers {
 		ns, err := server.Build()
 		if err != nil {

+ 22 - 6
infra/conf/dns_test.go

@@ -67,17 +67,23 @@ func TestDNSConfigParsing(t *testing.T) {
 			Input: `{
 				"servers": [{
 					"address": "8.8.8.8",
+					"clientIp": "10.0.0.1",
 					"port": 5353,
+					"skipFallback": true,
 					"domains": ["domain:example.com"]
 				}],
 				"hosts": {
 					"example.com": "127.0.0.1",
+					"xtls.github.io": ["1.2.3.4", "5.6.7.8"],
 					"domain:example.com": "google.com",
-					"geosite:test": "10.0.0.1",
-					"keyword:google": "8.8.8.8",
+					"geosite:test": ["127.0.0.1", "127.0.0.2"],
+					"keyword:google": ["8.8.8.8", "8.8.4.4"],
 					"regexp:.*\\.com": "8.8.4.4"
 				},
-				"clientIp": "10.0.0.1"
+				"clientIp": "10.0.0.1",
+				"queryStrategy": "UseIPv4",
+				"cacheStrategy": "disable",
+				"disableFallback": true
 			}`,
 			Parser: parserCreator(),
 			Output: &dns.Config{
@@ -92,6 +98,8 @@ func TestDNSConfigParsing(t *testing.T) {
 							Network: net.Network_UDP,
 							Port:    5353,
 						},
+						ClientIp:     []byte{10, 0, 0, 1},
+						SkipFallback: true,
 						PrioritizedDomain: []*dns.NameServer_PriorityDomain{
 							{
 								Type:   dns.DomainMatchingType_Subdomain,
@@ -120,20 +128,28 @@ func TestDNSConfigParsing(t *testing.T) {
 					{
 						Type:   dns.DomainMatchingType_Full,
 						Domain: "example.com",
-						Ip:     [][]byte{{10, 0, 0, 1}},
+						Ip:     [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}},
 					},
 					{
 						Type:   dns.DomainMatchingType_Keyword,
 						Domain: "google",
-						Ip:     [][]byte{{8, 8, 8, 8}},
+						Ip:     [][]byte{{8, 8, 8, 8}, {8, 8, 4, 4}},
 					},
 					{
 						Type:   dns.DomainMatchingType_Regex,
 						Domain: ".*\\.com",
 						Ip:     [][]byte{{8, 8, 4, 4}},
 					},
+					{
+						Type:   dns.DomainMatchingType_Full,
+						Domain: "xtls.github.io",
+						Ip:     [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
+					},
 				},
-				ClientIp: []byte{10, 0, 0, 1},
+				ClientIp:        []byte{10, 0, 0, 1},
+				QueryStrategy:   dns.QueryStrategy_USE_IP4,
+				CacheStrategy:   dns.CacheStrategy_Cache_DISABLE,
+				DisableFallback: true,
 			},
 		},
 	})

+ 3 - 3
infra/conf/freedom.go

@@ -22,11 +22,11 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
 	config := new(freedom.Config)
 	config.DomainStrategy = freedom.Config_AS_IS
 	switch strings.ToLower(c.DomainStrategy) {
-	case "useip", "use_ip":
+	case "useip", "use_ip", "use-ip":
 		config.DomainStrategy = freedom.Config_USE_IP
-	case "useip4", "useipv4", "use_ipv4", "use_ip_v4", "use_ip4":
+	case "useip4", "useipv4", "use_ip4", "use_ipv4", "use_ip_v4", "use-ip4", "use-ipv4", "use-ip-v4":
 		config.DomainStrategy = freedom.Config_USE_IP4
-	case "useip6", "useipv6", "use_ipv6", "use_ip_v6", "use_ip6":
+	case "useip6", "useipv6", "use_ip6", "use_ipv6", "use_ip_v6", "use-ip6", "use-ipv6", "use-ip-v6":
 		config.DomainStrategy = freedom.Config_USE_IP6
 	}
 

+ 5 - 6
infra/conf/transport_internet.go

@@ -321,6 +321,7 @@ type TLSConfig struct {
 	MaxVersion               string           `json:"maxVersion"`
 	CipherSuites             string           `json:"cipherSuites"`
 	PreferServerCipherSuites bool             `json:"preferServerCipherSuites"`
+	Fingerprint              string           `json:"fingerprint"`
 }
 
 // Build implements Buildable.
@@ -348,6 +349,7 @@ func (c *TLSConfig) Build() (proto.Message, error) {
 	config.MaxVersion = c.MaxVersion
 	config.CipherSuites = c.CipherSuites
 	config.PreferServerCipherSuites = c.PreferServerCipherSuites
+	config.Fingerprint = strings.ToLower(c.Fingerprint)
 	return config, nil
 }
 
@@ -476,22 +478,19 @@ type SocketConfig struct {
 
 // Build implements Buildable.
 func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
-	tfo := int32(-1)
+	tfo := int32(0) // don't invoke setsockopt() for TFO
 	if c.TFO != nil {
 		switch v := c.TFO.(type) {
 		case bool:
 			if v {
 				tfo = 256
 			} else {
-				tfo = 0
+				tfo = -1 // TFO need to be disabled
 			}
 		case float64:
-			if v < 0 {
-				return nil, newError("tcpFastOpen: only boolean and non-negative integer value is acceptable")
-			}
 			tfo = int32(math.Min(v, math.MaxInt32))
 		default:
-			return nil, newError("tcpFastOpen: only boolean and non-negative integer value is acceptable")
+			return nil, newError("tcpFastOpen: only boolean and integer value is acceptable")
 		}
 	}
 	var tproxy internet.SocketConfig_TProxyMode

+ 101 - 14
infra/conf/transport_test.go

@@ -31,6 +31,13 @@ func TestSocketConfig(t *testing.T) {
 		}
 	}
 
+	// test "tcpFastOpen": true, queue length 256 is expected. other parameters are tested here too
+	expectedOutput := &internet.SocketConfig{
+		Mark:           1,
+		Tfo:            256,
+		DomainStrategy: internet.DomainStrategy_USE_IP,
+		DialerProxy:    "tag",
+	}
 	runMultiTestCase(t, []TestCase{
 		{
 			Input: `{
@@ -40,38 +47,118 @@ func TestSocketConfig(t *testing.T) {
 				"dialerProxy": "tag"
 			}`,
 			Parser: createParser(),
-			Output: &internet.SocketConfig{
-				Mark:           1,
-				Tfo:            256,
-				DomainStrategy: internet.DomainStrategy_USE_IP,
-				DialerProxy:    "tag",
-			},
+			Output: expectedOutput,
 		},
 	})
+	if expectedOutput.ParseTFOValue() != 256 {
+		t.Fatalf("unexpected parsed TFO value, which should be 256")
+	}
+
+	// test "tcpFastOpen": false, disabled TFO is expected
+	expectedOutput = &internet.SocketConfig{
+		Mark: 0,
+		Tfo:  -1,
+	}
 	runMultiTestCase(t, []TestCase{
 		{
 			Input: `{
 				"tcpFastOpen": false
 			}`,
 			Parser: createParser(),
-			Output: &internet.SocketConfig{
-				Mark: 0,
-				Tfo:  0,
-			},
+			Output: expectedOutput,
 		},
 	})
+	if expectedOutput.ParseTFOValue() != 0 {
+		t.Fatalf("unexpected parsed TFO value, which should be 0")
+	}
+
+	// test "tcpFastOpen": 65535, queue length 65535 is expected
+	expectedOutput = &internet.SocketConfig{
+		Mark: 0,
+		Tfo:  65535,
+	}
 	runMultiTestCase(t, []TestCase{
 		{
 			Input: `{
 				"tcpFastOpen": 65535
 			}`,
 			Parser: createParser(),
-			Output: &internet.SocketConfig{
-				Mark: 0,
-				Tfo:  65535,
-			},
+			Output: expectedOutput,
 		},
 	})
+	if expectedOutput.ParseTFOValue() != 65535 {
+		t.Fatalf("unexpected parsed TFO value, which should be 65535")
+	}
+
+	// test "tcpFastOpen": -65535, disable TFO is expected
+	expectedOutput = &internet.SocketConfig{
+		Mark: 0,
+		Tfo:  -65535,
+	}
+	runMultiTestCase(t, []TestCase{
+		{
+			Input: `{
+				"tcpFastOpen": -65535
+			}`,
+			Parser: createParser(),
+			Output: expectedOutput,
+		},
+	})
+	if expectedOutput.ParseTFOValue() != 0 {
+		t.Fatalf("unexpected parsed TFO value, which should be 0")
+	}
+
+	// test "tcpFastOpen": 0, no operation is expected
+	expectedOutput = &internet.SocketConfig{
+		Mark: 0,
+		Tfo:  0,
+	}
+	runMultiTestCase(t, []TestCase{
+		{
+			Input: `{
+				"tcpFastOpen": 0
+			}`,
+			Parser: createParser(),
+			Output: expectedOutput,
+		},
+	})
+	if expectedOutput.ParseTFOValue() != -1 {
+		t.Fatalf("unexpected parsed TFO value, which should be -1")
+	}
+
+	// test omit "tcpFastOpen", no operation is expected
+	expectedOutput = &internet.SocketConfig{
+		Mark: 0,
+		Tfo:  0,
+	}
+	runMultiTestCase(t, []TestCase{
+		{
+			Input:  `{}`,
+			Parser: createParser(),
+			Output: expectedOutput,
+		},
+	})
+	if expectedOutput.ParseTFOValue() != -1 {
+		t.Fatalf("unexpected parsed TFO value, which should be -1")
+	}
+
+	// test "tcpFastOpen": null, no operation is expected
+	expectedOutput = &internet.SocketConfig{
+		Mark: 0,
+		Tfo:  0,
+	}
+	runMultiTestCase(t, []TestCase{
+		{
+			Input: `{
+				"tcpFastOpen": null
+			}`,
+			Parser: createParser(),
+			Output: expectedOutput,
+		},
+	})
+	if expectedOutput.ParseTFOValue() != -1 {
+		t.Fatalf("unexpected parsed TFO value, which should be -1")
+	}
 }
 
 func TestTransportConfig(t *testing.T) {

+ 3 - 0
main/commands/all/tls/cert.go

@@ -26,6 +26,9 @@ Arguments:
 	-domain=domain_name 
 		The domain name for the certificate.
 
+	-name=common_name 
+		The common name for the certificate.
+
 	-org=organization 
 		The organization name for the certificate.
 

+ 18 - 4
main/run.go

@@ -11,6 +11,7 @@ import (
 	"regexp"
 	"runtime"
 	"runtime/debug"
+	"strings"
 	"syscall"
 
 	"github.com/xtls/xray-core/common/cmdarg"
@@ -31,7 +32,7 @@ Xray. Multiple assign is accepted.
 The -confdir=dir flag sets a dir with multiple json config
 
 The -format=json flag sets the format of config files. 
-Default "json".
+Default "auto".
 
 The -test flag tells Xray to test config files only, 
 without launching the server
@@ -46,7 +47,7 @@ var (
 	configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main
 	configDir   string
 	test        = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
-	format      = cmdRun.Flag.String("format", "json", "Format of input file.")
+	format      = cmdRun.Flag.String("format", "auto", "Format of input file.")
 
 	/* We have to do this here because Golang's Test will also need to parse flag, before
 	 * main func in this file is run.
@@ -111,13 +112,26 @@ func dirExists(file string) bool {
 	return err == nil && info.IsDir()
 }
 
+func getRegepxByFormat() string {
+	switch strings.ToLower(*format) {
+	case "json":
+		return `^.+\.json$`
+	case "toml":
+		return `^.+\.toml$`
+	case "yaml", "yml":
+		return `^.+\.(yaml|yml)$`
+	default:
+		return `^.+\.(json|toml|yaml|yml)$`
+	}
+}
+
 func readConfDir(dirPath string) {
 	confs, err := ioutil.ReadDir(dirPath)
 	if err != nil {
 		log.Fatalln(err)
 	}
 	for _, f := range confs {
-		matched, err := regexp.MatchString(`^.+\.(json|toml|yaml|yml)$`, f.Name())
+		matched, err := regexp.MatchString(getRegepxByFormat(), f.Name())
 		if err != nil {
 			log.Fatalln(err)
 		}
@@ -160,7 +174,7 @@ func getConfigFilePath() cmdarg.Arg {
 func getConfigFormat() string {
 	f := core.GetFormatByExtension(*format)
 	if f == "" {
-		f = "json"
+		f = "auto"
 	}
 	return f
 }

+ 5 - 11
proxy/dns/dns.go

@@ -43,6 +43,7 @@ type Handler struct {
 
 func (h *Handler) Init(config *Config, dnsClient dns.Client) error {
 	h.client = dnsClient
+
 	if v, ok := dnsClient.(ownLinkVerifier); ok {
 		h.ownLinkVerifier = v
 	}
@@ -198,22 +199,16 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
 	var err error
 
 	var ttl uint32 = 600
+	var opt dns.Option
 
 	switch qType {
 	case dnsmessage.TypeA:
-		ips, err = h.client.LookupIP(domain, dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: false,
-			FakeEnable: true,
-		})
+		opt = dns.LookupIPv4Only
 	case dnsmessage.TypeAAAA:
-		ips, err = h.client.LookupIP(domain, dns.IPOption{
-			IPv4Enable: false,
-			IPv6Enable: true,
-			FakeEnable: true,
-		})
+		opt = dns.LookupIPv6Only
 	}
 
+	ips, err = h.client.LookupOptions(domain, opt, dns.LookupFake)
 	rcode := dns.RCodeFromError(err)
 	if rcode == 0 && len(ips) == 0 && err != dns.ErrEmptyResponse {
 		newError("ip query").Base(err).WriteToLog()
@@ -228,7 +223,6 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
 		RecursionAvailable: true,
 		RecursionDesired:   true,
 		Response:           true,
-		Authoritative:      true,
 	})
 	builder.EnableCompression()
 	common.Must(builder.StartQuestions())

+ 4 - 16
proxy/freedom/freedom.go

@@ -59,26 +59,14 @@ func (h *Handler) policy() policy.Session {
 }
 
 func (h *Handler) resolveIP(ctx context.Context, domain string, localAddr net.Address) net.Address {
-	var option dns.IPOption = dns.IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-		FakeEnable: false,
-	}
+	var opt dns.Option
 	if h.config.DomainStrategy == Config_USE_IP4 || (localAddr != nil && localAddr.Family().IsIPv4()) {
-		option = dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: false,
-			FakeEnable: false,
-		}
+		opt = dns.LookupIPv4Only
 	} else if h.config.DomainStrategy == Config_USE_IP6 || (localAddr != nil && localAddr.Family().IsIPv6()) {
-		option = dns.IPOption{
-			IPv4Enable: false,
-			IPv6Enable: true,
-			FakeEnable: false,
-		}
+		opt = dns.LookupIPv6Only
 	}
 
-	ips, err := h.dns.LookupIP(domain, option)
+	ips, err := h.dns.LookupOptions(domain, opt, dns.LookupNoFake)
 	if err != nil {
 		newError("failed to get IP address for domain ", domain).Base(err).WriteToLog(session.ExportIDToError(ctx))
 	}

+ 39 - 18
testing/mocks/dns.go

@@ -5,36 +5,37 @@
 package mocks
 
 import (
-	gomock "github.com/golang/mock/gomock"
-	dns "github.com/xtls/xray-core/features/dns"
 	net "net"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	dns "github.com/xtls/xray-core/features/dns"
 )
 
-// DNSClient is a mock of Client interface
+// DNSClient is a mock of Client interface.
 type DNSClient struct {
 	ctrl     *gomock.Controller
 	recorder *DNSClientMockRecorder
 }
 
-// DNSClientMockRecorder is the mock recorder for DNSClient
+// DNSClientMockRecorder is the mock recorder for DNSClient.
 type DNSClientMockRecorder struct {
 	mock *DNSClient
 }
 
-// NewDNSClient creates a new mock instance
+// NewDNSClient creates a new mock instance.
 func NewDNSClient(ctrl *gomock.Controller) *DNSClient {
 	mock := &DNSClient{ctrl: ctrl}
 	mock.recorder = &DNSClientMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *DNSClient) EXPECT() *DNSClientMockRecorder {
 	return m.recorder
 }
 
-// Close mocks base method
+// Close mocks base method.
 func (m *DNSClient) Close() error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Close")
@@ -42,28 +43,48 @@ func (m *DNSClient) Close() error {
 	return ret0
 }
 
-// Close indicates an expected call of Close
+// Close indicates an expected call of Close.
 func (mr *DNSClientMockRecorder) Close() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*DNSClient)(nil).Close))
 }
 
-// LookupIP mocks base method
-func (m *DNSClient) LookupIP(arg0 string, arg1 dns.IPOption) ([]net.IP, error) {
+// LookupIP mocks base method.
+func (m *DNSClient) LookupIP(arg0 string) ([]net.IP, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LookupIP", arg0)
+	ret0, _ := ret[0].([]net.IP)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// LookupIP indicates an expected call of LookupIP.
+func (mr *DNSClientMockRecorder) LookupIP(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0)
+}
+
+// LookupOptions mocks base method.
+func (m *DNSClient) LookupOptions(arg0 string, arg1 ...dns.Option) ([]net.IP, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "LookupIP", arg0, arg1)
+	varargs := []interface{}{arg0}
+	for _, a := range arg1 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "LookupOptions", varargs...)
 	ret0, _ := ret[0].([]net.IP)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// LookupIP indicates an expected call of LookupIP
-func (mr *DNSClientMockRecorder) LookupIP(arg0, arg1 interface{}) *gomock.Call {
+// LookupOptions indicates an expected call of LookupOptions.
+func (mr *DNSClientMockRecorder) LookupOptions(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0, arg1)
+	varargs := append([]interface{}{arg0}, arg1...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupOptions", reflect.TypeOf((*DNSClient)(nil).LookupOptions), varargs...)
 }
 
-// Start mocks base method
+// Start mocks base method.
 func (m *DNSClient) Start() error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Start")
@@ -71,13 +92,13 @@ func (m *DNSClient) Start() error {
 	return ret0
 }
 
-// Start indicates an expected call of Start
+// Start indicates an expected call of Start.
 func (mr *DNSClientMockRecorder) Start() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*DNSClient)(nil).Start))
 }
 
-// Type mocks base method
+// Type mocks base method.
 func (m *DNSClient) Type() interface{} {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Type")
@@ -85,7 +106,7 @@ func (m *DNSClient) Type() interface{} {
 	return ret0
 }
 
-// Type indicates an expected call of Type
+// Type indicates an expected call of Type.
 func (mr *DNSClientMockRecorder) Type() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*DNSClient)(nil).Type))

+ 14 - 13
testing/mocks/io.go

@@ -5,34 +5,35 @@
 package mocks
 
 import (
-	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
 )
 
-// Reader is a mock of Reader interface
+// Reader is a mock of Reader interface.
 type Reader struct {
 	ctrl     *gomock.Controller
 	recorder *ReaderMockRecorder
 }
 
-// ReaderMockRecorder is the mock recorder for Reader
+// ReaderMockRecorder is the mock recorder for Reader.
 type ReaderMockRecorder struct {
 	mock *Reader
 }
 
-// NewReader creates a new mock instance
+// NewReader creates a new mock instance.
 func NewReader(ctrl *gomock.Controller) *Reader {
 	mock := &Reader{ctrl: ctrl}
 	mock.recorder = &ReaderMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *Reader) EXPECT() *ReaderMockRecorder {
 	return m.recorder
 }
 
-// Read mocks base method
+// Read mocks base method.
 func (m *Reader) Read(arg0 []byte) (int, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Read", arg0)
@@ -41,36 +42,36 @@ func (m *Reader) Read(arg0 []byte) (int, error) {
 	return ret0, ret1
 }
 
-// Read indicates an expected call of Read
+// Read indicates an expected call of Read.
 func (mr *ReaderMockRecorder) Read(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Reader)(nil).Read), arg0)
 }
 
-// Writer is a mock of Writer interface
+// Writer is a mock of Writer interface.
 type Writer struct {
 	ctrl     *gomock.Controller
 	recorder *WriterMockRecorder
 }
 
-// WriterMockRecorder is the mock recorder for Writer
+// WriterMockRecorder is the mock recorder for Writer.
 type WriterMockRecorder struct {
 	mock *Writer
 }
 
-// NewWriter creates a new mock instance
+// NewWriter creates a new mock instance.
 func NewWriter(ctrl *gomock.Controller) *Writer {
 	mock := &Writer{ctrl: ctrl}
 	mock.recorder = &WriterMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *Writer) EXPECT() *WriterMockRecorder {
 	return m.recorder
 }
 
-// Write mocks base method
+// Write mocks base method.
 func (m *Writer) Write(arg0 []byte) (int, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Write", arg0)
@@ -79,7 +80,7 @@ func (m *Writer) Write(arg0 []byte) (int, error) {
 	return ret0, ret1
 }
 
-// Write indicates an expected call of Write
+// Write indicates an expected call of Write.
 func (mr *WriterMockRecorder) Write(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Writer)(nil).Write), arg0)

+ 8 - 7
testing/mocks/log.go

@@ -5,41 +5,42 @@
 package mocks
 
 import (
+	reflect "reflect"
+
 	gomock "github.com/golang/mock/gomock"
 	log "github.com/xtls/xray-core/common/log"
-	reflect "reflect"
 )
 
-// LogHandler is a mock of Handler interface
+// LogHandler is a mock of Handler interface.
 type LogHandler struct {
 	ctrl     *gomock.Controller
 	recorder *LogHandlerMockRecorder
 }
 
-// LogHandlerMockRecorder is the mock recorder for LogHandler
+// LogHandlerMockRecorder is the mock recorder for LogHandler.
 type LogHandlerMockRecorder struct {
 	mock *LogHandler
 }
 
-// NewLogHandler creates a new mock instance
+// NewLogHandler creates a new mock instance.
 func NewLogHandler(ctrl *gomock.Controller) *LogHandler {
 	mock := &LogHandler{ctrl: ctrl}
 	mock.recorder = &LogHandlerMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *LogHandler) EXPECT() *LogHandlerMockRecorder {
 	return m.recorder
 }
 
-// Handle mocks base method
+// Handle mocks base method.
 func (m *LogHandler) Handle(arg0 log.Message) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "Handle", arg0)
 }
 
-// Handle indicates an expected call of Handle
+// Handle indicates an expected call of Handle.
 func (mr *LogHandlerMockRecorder) Handle(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*LogHandler)(nil).Handle), arg0)

+ 8 - 7
testing/mocks/mux.go

@@ -5,35 +5,36 @@
 package mocks
 
 import (
+	reflect "reflect"
+
 	gomock "github.com/golang/mock/gomock"
 	mux "github.com/xtls/xray-core/common/mux"
-	reflect "reflect"
 )
 
-// MuxClientWorkerFactory is a mock of ClientWorkerFactory interface
+// MuxClientWorkerFactory is a mock of ClientWorkerFactory interface.
 type MuxClientWorkerFactory struct {
 	ctrl     *gomock.Controller
 	recorder *MuxClientWorkerFactoryMockRecorder
 }
 
-// MuxClientWorkerFactoryMockRecorder is the mock recorder for MuxClientWorkerFactory
+// MuxClientWorkerFactoryMockRecorder is the mock recorder for MuxClientWorkerFactory.
 type MuxClientWorkerFactoryMockRecorder struct {
 	mock *MuxClientWorkerFactory
 }
 
-// NewMuxClientWorkerFactory creates a new mock instance
+// NewMuxClientWorkerFactory creates a new mock instance.
 func NewMuxClientWorkerFactory(ctrl *gomock.Controller) *MuxClientWorkerFactory {
 	mock := &MuxClientWorkerFactory{ctrl: ctrl}
 	mock.recorder = &MuxClientWorkerFactoryMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *MuxClientWorkerFactory) EXPECT() *MuxClientWorkerFactoryMockRecorder {
 	return m.recorder
 }
 
-// Create mocks base method
+// Create mocks base method.
 func (m *MuxClientWorkerFactory) Create() (*mux.ClientWorker, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Create")
@@ -42,7 +43,7 @@ func (m *MuxClientWorkerFactory) Create() (*mux.ClientWorker, error) {
 	return ret0, ret1
 }
 
-// Create indicates an expected call of Create
+// Create indicates an expected call of Create.
 func (mr *MuxClientWorkerFactoryMockRecorder) Create() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MuxClientWorkerFactory)(nil).Create))

+ 26 - 25
testing/mocks/outbound.go

@@ -6,35 +6,36 @@ package mocks
 
 import (
 	context "context"
+	reflect "reflect"
+
 	gomock "github.com/golang/mock/gomock"
 	outbound "github.com/xtls/xray-core/features/outbound"
-	reflect "reflect"
 )
 
-// OutboundManager is a mock of Manager interface
+// OutboundManager is a mock of Manager interface.
 type OutboundManager struct {
 	ctrl     *gomock.Controller
 	recorder *OutboundManagerMockRecorder
 }
 
-// OutboundManagerMockRecorder is the mock recorder for OutboundManager
+// OutboundManagerMockRecorder is the mock recorder for OutboundManager.
 type OutboundManagerMockRecorder struct {
 	mock *OutboundManager
 }
 
-// NewOutboundManager creates a new mock instance
+// NewOutboundManager creates a new mock instance.
 func NewOutboundManager(ctrl *gomock.Controller) *OutboundManager {
 	mock := &OutboundManager{ctrl: ctrl}
 	mock.recorder = &OutboundManagerMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *OutboundManager) EXPECT() *OutboundManagerMockRecorder {
 	return m.recorder
 }
 
-// AddHandler mocks base method
+// AddHandler mocks base method.
 func (m *OutboundManager) AddHandler(arg0 context.Context, arg1 outbound.Handler) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddHandler", arg0, arg1)
@@ -42,13 +43,13 @@ func (m *OutboundManager) AddHandler(arg0 context.Context, arg1 outbound.Handler
 	return ret0
 }
 
-// AddHandler indicates an expected call of AddHandler
+// AddHandler indicates an expected call of AddHandler.
 func (mr *OutboundManagerMockRecorder) AddHandler(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*OutboundManager)(nil).AddHandler), arg0, arg1)
 }
 
-// Close mocks base method
+// Close mocks base method.
 func (m *OutboundManager) Close() error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Close")
@@ -56,13 +57,13 @@ func (m *OutboundManager) Close() error {
 	return ret0
 }
 
-// Close indicates an expected call of Close
+// Close indicates an expected call of Close.
 func (mr *OutboundManagerMockRecorder) Close() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*OutboundManager)(nil).Close))
 }
 
-// GetDefaultHandler mocks base method
+// GetDefaultHandler mocks base method.
 func (m *OutboundManager) GetDefaultHandler() outbound.Handler {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "GetDefaultHandler")
@@ -70,13 +71,13 @@ func (m *OutboundManager) GetDefaultHandler() outbound.Handler {
 	return ret0
 }
 
-// GetDefaultHandler indicates an expected call of GetDefaultHandler
+// GetDefaultHandler indicates an expected call of GetDefaultHandler.
 func (mr *OutboundManagerMockRecorder) GetDefaultHandler() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultHandler", reflect.TypeOf((*OutboundManager)(nil).GetDefaultHandler))
 }
 
-// GetHandler mocks base method
+// GetHandler mocks base method.
 func (m *OutboundManager) GetHandler(arg0 string) outbound.Handler {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "GetHandler", arg0)
@@ -84,13 +85,13 @@ func (m *OutboundManager) GetHandler(arg0 string) outbound.Handler {
 	return ret0
 }
 
-// GetHandler indicates an expected call of GetHandler
+// GetHandler indicates an expected call of GetHandler.
 func (mr *OutboundManagerMockRecorder) GetHandler(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHandler", reflect.TypeOf((*OutboundManager)(nil).GetHandler), arg0)
 }
 
-// RemoveHandler mocks base method
+// RemoveHandler mocks base method.
 func (m *OutboundManager) RemoveHandler(arg0 context.Context, arg1 string) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "RemoveHandler", arg0, arg1)
@@ -98,13 +99,13 @@ func (m *OutboundManager) RemoveHandler(arg0 context.Context, arg1 string) error
 	return ret0
 }
 
-// RemoveHandler indicates an expected call of RemoveHandler
+// RemoveHandler indicates an expected call of RemoveHandler.
 func (mr *OutboundManagerMockRecorder) RemoveHandler(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*OutboundManager)(nil).RemoveHandler), arg0, arg1)
 }
 
-// Start mocks base method
+// Start mocks base method.
 func (m *OutboundManager) Start() error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Start")
@@ -112,13 +113,13 @@ func (m *OutboundManager) Start() error {
 	return ret0
 }
 
-// Start indicates an expected call of Start
+// Start indicates an expected call of Start.
 func (mr *OutboundManagerMockRecorder) Start() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*OutboundManager)(nil).Start))
 }
 
-// Type mocks base method
+// Type mocks base method.
 func (m *OutboundManager) Type() interface{} {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Type")
@@ -126,36 +127,36 @@ func (m *OutboundManager) Type() interface{} {
 	return ret0
 }
 
-// Type indicates an expected call of Type
+// Type indicates an expected call of Type.
 func (mr *OutboundManagerMockRecorder) Type() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*OutboundManager)(nil).Type))
 }
 
-// OutboundHandlerSelector is a mock of HandlerSelector interface
+// OutboundHandlerSelector is a mock of HandlerSelector interface.
 type OutboundHandlerSelector struct {
 	ctrl     *gomock.Controller
 	recorder *OutboundHandlerSelectorMockRecorder
 }
 
-// OutboundHandlerSelectorMockRecorder is the mock recorder for OutboundHandlerSelector
+// OutboundHandlerSelectorMockRecorder is the mock recorder for OutboundHandlerSelector.
 type OutboundHandlerSelectorMockRecorder struct {
 	mock *OutboundHandlerSelector
 }
 
-// NewOutboundHandlerSelector creates a new mock instance
+// NewOutboundHandlerSelector creates a new mock instance.
 func NewOutboundHandlerSelector(ctrl *gomock.Controller) *OutboundHandlerSelector {
 	mock := &OutboundHandlerSelector{ctrl: ctrl}
 	mock.recorder = &OutboundHandlerSelectorMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *OutboundHandlerSelector) EXPECT() *OutboundHandlerSelectorMockRecorder {
 	return m.recorder
 }
 
-// Select mocks base method
+// Select mocks base method.
 func (m *OutboundHandlerSelector) Select(arg0 []string) []string {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Select", arg0)
@@ -163,7 +164,7 @@ func (m *OutboundHandlerSelector) Select(arg0 []string) []string {
 	return ret0
 }
 
-// Select indicates an expected call of Select
+// Select indicates an expected call of Select.
 func (mr *OutboundHandlerSelectorMockRecorder) Select(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*OutboundHandlerSelector)(nil).Select), arg0)

+ 16 - 15
testing/mocks/proxy.go

@@ -6,38 +6,39 @@ package mocks
 
 import (
 	context "context"
+	reflect "reflect"
+
 	gomock "github.com/golang/mock/gomock"
 	net "github.com/xtls/xray-core/common/net"
 	routing "github.com/xtls/xray-core/features/routing"
 	transport "github.com/xtls/xray-core/transport"
 	internet "github.com/xtls/xray-core/transport/internet"
-	reflect "reflect"
 )
 
-// ProxyInbound is a mock of Inbound interface
+// ProxyInbound is a mock of Inbound interface.
 type ProxyInbound struct {
 	ctrl     *gomock.Controller
 	recorder *ProxyInboundMockRecorder
 }
 
-// ProxyInboundMockRecorder is the mock recorder for ProxyInbound
+// ProxyInboundMockRecorder is the mock recorder for ProxyInbound.
 type ProxyInboundMockRecorder struct {
 	mock *ProxyInbound
 }
 
-// NewProxyInbound creates a new mock instance
+// NewProxyInbound creates a new mock instance.
 func NewProxyInbound(ctrl *gomock.Controller) *ProxyInbound {
 	mock := &ProxyInbound{ctrl: ctrl}
 	mock.recorder = &ProxyInboundMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *ProxyInbound) EXPECT() *ProxyInboundMockRecorder {
 	return m.recorder
 }
 
-// Network mocks base method
+// Network mocks base method.
 func (m *ProxyInbound) Network() []net.Network {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Network")
@@ -45,13 +46,13 @@ func (m *ProxyInbound) Network() []net.Network {
 	return ret0
 }
 
-// Network indicates an expected call of Network
+// Network indicates an expected call of Network.
 func (mr *ProxyInboundMockRecorder) Network() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Network", reflect.TypeOf((*ProxyInbound)(nil).Network))
 }
 
-// Process mocks base method
+// Process mocks base method.
 func (m *ProxyInbound) Process(arg0 context.Context, arg1 net.Network, arg2 internet.Connection, arg3 routing.Dispatcher) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2, arg3)
@@ -59,36 +60,36 @@ func (m *ProxyInbound) Process(arg0 context.Context, arg1 net.Network, arg2 inte
 	return ret0
 }
 
-// Process indicates an expected call of Process
+// Process indicates an expected call of Process.
 func (mr *ProxyInboundMockRecorder) Process(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*ProxyInbound)(nil).Process), arg0, arg1, arg2, arg3)
 }
 
-// ProxyOutbound is a mock of Outbound interface
+// ProxyOutbound is a mock of Outbound interface.
 type ProxyOutbound struct {
 	ctrl     *gomock.Controller
 	recorder *ProxyOutboundMockRecorder
 }
 
-// ProxyOutboundMockRecorder is the mock recorder for ProxyOutbound
+// ProxyOutboundMockRecorder is the mock recorder for ProxyOutbound.
 type ProxyOutboundMockRecorder struct {
 	mock *ProxyOutbound
 }
 
-// NewProxyOutbound creates a new mock instance
+// NewProxyOutbound creates a new mock instance.
 func NewProxyOutbound(ctrl *gomock.Controller) *ProxyOutbound {
 	mock := &ProxyOutbound{ctrl: ctrl}
 	mock.recorder = &ProxyOutboundMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *ProxyOutbound) EXPECT() *ProxyOutboundMockRecorder {
 	return m.recorder
 }
 
-// Process mocks base method
+// Process mocks base method.
 func (m *ProxyOutbound) Process(arg0 context.Context, arg1 *transport.Link, arg2 internet.Dialer) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2)
@@ -96,7 +97,7 @@ func (m *ProxyOutbound) Process(arg0 context.Context, arg1 *transport.Link, arg2
 	return ret0
 }
 
-// Process indicates an expected call of Process
+// Process indicates an expected call of Process.
 func (mr *ProxyOutboundMockRecorder) Process(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*ProxyOutbound)(nil).Process), arg0, arg1, arg2)

+ 19 - 11
transport/internet/grpc/dial.go

@@ -36,6 +36,7 @@ func init() {
 type dialerConf struct {
 	net.Destination
 	*internet.SocketConfig
+	*tls.Config
 }
 
 var (
@@ -46,14 +47,9 @@ var (
 func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
 	grpcSettings := streamSettings.ProtocolSettings.(*Config)
 
-	config := tls.ConfigFromStreamSettings(streamSettings)
-	var dialOption = grpc.WithInsecure()
+	tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
 
-	if config != nil {
-		dialOption = grpc.WithTransportCredentials(credentials.NewTLS(config.GetTLSConfig()))
-	}
-
-	conn, err := getGrpcClient(ctx, dest, dialOption, streamSettings.SocketSettings)
+	conn, err := getGrpcClient(ctx, dest, tlsConfig, streamSettings.SocketSettings)
 
 	if err != nil {
 		return nil, newError("Cannot dial gRPC").Base(err)
@@ -76,7 +72,7 @@ func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *interne
 	return encoding.NewHunkConn(grpcService, nil), nil
 }
 
-func getGrpcClient(ctx context.Context, dest net.Destination, dialOption grpc.DialOption, sockopt *internet.SocketConfig) (*grpc.ClientConn, error) {
+func getGrpcClient(ctx context.Context, dest net.Destination, tlsConfig *tls.Config, sockopt *internet.SocketConfig) (*grpc.ClientConn, error) {
 	globalDialerAccess.Lock()
 	defer globalDialerAccess.Unlock()
 
@@ -84,12 +80,24 @@ func getGrpcClient(ctx context.Context, dest net.Destination, dialOption grpc.Di
 		globalDialerMap = make(map[dialerConf]*grpc.ClientConn)
 	}
 
-	if client, found := globalDialerMap[dialerConf{dest, sockopt}]; found && client.GetState() != connectivity.Shutdown {
+	if client, found := globalDialerMap[dialerConf{dest, sockopt, tlsConfig}]; found && client.GetState() != connectivity.Shutdown {
 		return client, nil
 	}
 
+	dialOption := grpc.WithInsecure()
+
+	if tlsConfig != nil {
+		dialOption = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig.GetTLSConfig()))
+	}
+
+	var grpcDestHost string
+	if dest.Address.Family().IsDomain() {
+		grpcDestHost = dest.Address.Domain()
+	} else {
+		grpcDestHost = dest.Address.IP().String()
+	}
 	conn, err := grpc.Dial(
-		gonet.JoinHostPort(dest.Address.String(), dest.Port.String()),
+		gonet.JoinHostPort(grpcDestHost, dest.Port.String()),
 		dialOption,
 		grpc.WithConnectParams(grpc.ConnectParams{
 			Backoff: backoff.Config{
@@ -125,6 +133,6 @@ func getGrpcClient(ctx context.Context, dest net.Destination, dialOption grpc.Di
 			return internet.DialSystem(gctx, net.TCPDestination(address, port), sockopt)
 		}),
 	)
-	globalDialerMap[dialerConf{dest, sockopt}] = conn
+	globalDialerMap[dialerConf{dest, sockopt, tlsConfig}] = conn
 	return conn, err
 }

+ 16 - 1
transport/internet/grpc/encoding/hunkconn.go

@@ -5,12 +5,15 @@ import (
 	"io"
 	"net"
 
+	"google.golang.org/grpc/peer"
+
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/net/cnc"
 	"github.com/xtls/xray-core/common/signal/done"
 )
 
 type HunkConn interface {
+	Context() context.Context
 	Send(*Hunk) error
 	Recv() (*Hunk, error)
 	SendMsg(m interface{}) error
@@ -35,11 +38,23 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter
 }
 
 func NewHunkConn(hc HunkConn, cancel context.CancelFunc) net.Conn {
+	var rAddr net.Addr
+	pr, ok := peer.FromContext(hc.Context())
+	if ok {
+		rAddr = pr.Addr
+	} else {
+		rAddr = &net.TCPAddr{
+			IP:   []byte{0, 0, 0, 0},
+			Port: 0,
+		}
+	}
+
 	wrc := NewHunkReadWriter(hc, cancel)
 	return cnc.NewConnection(
 		cnc.ConnectionInput(wrc),
 		cnc.ConnectionOutput(wrc),
 		cnc.ConnectionOnClose(wrc),
+		cnc.ConnectionRemoteAddr(rAddr),
 	)
 }
 
@@ -85,7 +100,7 @@ func (h *HunkReaderWriter) ReadMultiBuffer() (buf.MultiBuffer, error) {
 		}
 	}
 
-	if cap(h.buf) == buf.Size {
+	if cap(h.buf) >= buf.Size {
 		b := h.buf
 		h.index = len(h.buf)
 		return buf.MultiBuffer{buf.NewExisted(b)}, nil

+ 31 - 9
transport/internet/grpc/encoding/multiconn.go

@@ -5,12 +5,15 @@ import (
 	"io"
 	"net"
 
+	"google.golang.org/grpc/peer"
+
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/net/cnc"
 	"github.com/xtls/xray-core/common/signal/done"
 )
 
 type MultiHunkConn interface {
+	Context() context.Context
 	Send(*MultiHunk) error
 	Recv() (*MultiHunk, error)
 	SendMsg(m interface{}) error
@@ -30,11 +33,23 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH
 }
 
 func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc) net.Conn {
+	var rAddr net.Addr
+	pr, ok := peer.FromContext(hc.Context())
+	if ok {
+		rAddr = pr.Addr
+	} else {
+		rAddr = &net.TCPAddr{
+			IP:   []byte{0, 0, 0, 0},
+			Port: 0,
+		}
+	}
+
 	wrc := NewMultiHunkReadWriter(hc, cancel)
 	return cnc.NewConnection(
 		cnc.ConnectionInputMulti(wrc),
 		cnc.ConnectionOutputMulti(wrc),
 		cnc.ConnectionOnClose(wrc),
+		cnc.ConnectionRemoteAddr(rAddr),
 	)
 }
 
@@ -64,16 +79,20 @@ func (h *MultiHunkReaderWriter) ReadMultiBuffer() (buf.MultiBuffer, error) {
 
 	var mb = make(buf.MultiBuffer, 0, len(h.buf))
 	for _, b := range h.buf {
-		if cap(b) >= buf.Size {
-			mb = append(mb, buf.NewExisted(b))
+		if len(b) == 0 {
 			continue
 		}
 
-		nb := buf.New()
-		nb.Extend(int32(len(b)))
-		copy(nb.Bytes(), b)
+		if cap(b) >= buf.Size {
+			mb = append(mb, buf.NewExisted(b))
+		} else {
+			nb := buf.New()
+			nb.Extend(int32(len(b)))
+			copy(nb.Bytes(), b)
+
+			mb = append(mb, nb)
+		}
 
-		mb = append(mb, nb)
 	}
 	return mb, nil
 }
@@ -84,12 +103,15 @@ func (h *MultiHunkReaderWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
 		return io.ErrClosedPipe
 	}
 
-	hunk := &MultiHunk{Data: make([][]byte, len(mb))}
+	hunks := make([][]byte, 0, len(mb))
+
 	for _, b := range mb {
-		hunk.Data = append(hunk.Data, b.Bytes())
+		if b.Len() > 0 {
+			hunks = append(hunks, b.Bytes())
+		}
 	}
 
-	err := h.hc.Send(hunk)
+	err := h.hc.Send(&MultiHunk{Data: hunks})
 	if err != nil {
 		return err
 	}

+ 3 - 2
transport/internet/http/dialer.go

@@ -21,6 +21,7 @@ import (
 type dialerConf struct {
 	net.Destination
 	*internet.SocketConfig
+	*tls.Config
 }
 
 var (
@@ -36,7 +37,7 @@ func getHTTPClient(ctx context.Context, dest net.Destination, tlsSettings *tls.C
 		globalDialerMap = make(map[dialerConf]*http.Client)
 	}
 
-	if client, found := globalDialerMap[dialerConf{dest, sockopt}]; found {
+	if client, found := globalDialerMap[dialerConf{dest, sockopt, tlsSettings}]; found {
 		return client, nil
 	}
 
@@ -92,7 +93,7 @@ func getHTTPClient(ctx context.Context, dest net.Destination, tlsSettings *tls.C
 		Transport: transport,
 	}
 
-	globalDialerMap[dialerConf{dest, sockopt}] = client
+	globalDialerMap[dialerConf{dest, sockopt, tlsSettings}] = client
 	return client, nil
 }
 

+ 1 - 2
transport/internet/quic/dialer.go

@@ -148,8 +148,7 @@ func (s *clientSessions) openConnection(destAddr net.Addr, config *Config, tlsCo
 
 	quicConfig := &quic.Config{
 		ConnectionIDLength: 12,
-		HandshakeTimeout:   time.Second * 8,
-		MaxIdleTimeout:     time.Second * 30,
+		KeepAlive:          true,
 	}
 
 	conn, err := wrapSysConn(rawConn, config)

+ 1 - 2
transport/internet/quic/hub.go

@@ -103,8 +103,7 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
 
 	quicConfig := &quic.Config{
 		ConnectionIDLength:    12,
-		HandshakeTimeout:      time.Second * 8,
-		MaxIdleTimeout:        time.Second * 45,
+		KeepAlive:             true,
 		MaxIncomingStreams:    32,
 		MaxIncomingUniStreams: -1,
 	}

+ 11 - 0
transport/internet/sockopt.go

@@ -17,3 +17,14 @@ func isUDPSocket(network string) bool {
 		return false
 	}
 }
+
+func (v *SocketConfig) ParseTFOValue() int {
+	if v.Tfo == 0 {
+		return -1
+	}
+	tfo := int(v.Tfo)
+	if tfo < 0 {
+		tfo = 0
+	}
+	return tfo
+}

+ 4 - 4
transport/internet/sockopt_darwin.go

@@ -15,12 +15,12 @@ const (
 
 func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error {
 	if isTCPSocket(network) {
-		tfo := config.Tfo
+		tfo := config.ParseTFOValue()
 		if tfo > 0 {
 			tfo = TCP_FASTOPEN_CLIENT
 		}
 		if tfo >= 0 {
-			if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, int(tfo)); err != nil {
+			if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, tfo); err != nil {
 				return err
 			}
 		}
@@ -31,12 +31,12 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
 
 func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error {
 	if isTCPSocket(network) {
-		tfo := config.Tfo
+		tfo := config.ParseTFOValue()
 		if tfo > 0 {
 			tfo = TCP_FASTOPEN_SERVER
 		}
 		if tfo >= 0 {
-			if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, int(tfo)); err != nil {
+			if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, tfo); err != nil {
 				return err
 			}
 		}

+ 5 - 4
transport/internet/sockopt_freebsd.go

@@ -130,7 +130,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
 	}
 
 	if isTCPSocket(network) {
-		tfo := int(config.Tfo)
+		tfo := config.ParseTFOValue()
 		if tfo > 0 {
 			tfo = 1
 		}
@@ -163,9 +163,10 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig)
 		}
 	}
 	if isTCPSocket(network) {
-		if config.Tfo >= 0 {
-			if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, int(config.Tfo)); err != nil {
-				return newError("failed to set TCP_FASTOPEN=", config.Tfo).Base(err)
+		tfo := config.ParseTFOValue()
+		if tfo >= 0 {
+			if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, tfo); err != nil {
+				return newError("failed to set TCP_FASTOPEN=", tfo).Base(err)
 			}
 		}
 	}

+ 5 - 4
transport/internet/sockopt_linux.go

@@ -48,7 +48,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
 	}
 
 	if isTCPSocket(network) {
-		tfo := int(config.Tfo)
+		tfo := config.ParseTFOValue()
 		if tfo > 0 {
 			tfo = 1
 		}
@@ -75,9 +75,10 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig)
 		}
 	}
 	if isTCPSocket(network) {
-		if config.Tfo >= 0 {
-			if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, TCP_FASTOPEN, int(config.Tfo)); err != nil {
-				return newError("failed to set TCP_FASTOPEN=", config.Tfo).Base(err)
+		tfo := config.ParseTFOValue()
+		if tfo >= 0 {
+			if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, TCP_FASTOPEN, tfo); err != nil {
+				return newError("failed to set TCP_FASTOPEN=", tfo).Base(err)
 			}
 		}
 	}

+ 4 - 4
transport/internet/sockopt_windows.go

@@ -8,12 +8,12 @@ const (
 	TCP_FASTOPEN = 15
 )
 
-func setTFO(fd syscall.Handle, tfo int32) error {
+func setTFO(fd syscall.Handle, tfo int) error {
 	if tfo > 0 {
 		tfo = 1
 	}
 	if tfo >= 0 {
-		if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, TCP_FASTOPEN, int(tfo)); err != nil {
+		if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, TCP_FASTOPEN, tfo); err != nil {
 			return err
 		}
 	}
@@ -22,7 +22,7 @@ func setTFO(fd syscall.Handle, tfo int32) error {
 
 func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error {
 	if isTCPSocket(network) {
-		if err := setTFO(syscall.Handle(fd), config.Tfo); err != nil {
+		if err := setTFO(syscall.Handle(fd), config.ParseTFOValue()); err != nil {
 			return err
 		}
 
@@ -33,7 +33,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
 
 func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error {
 	if isTCPSocket(network) {
-		if err := setTFO(syscall.Handle(fd), config.Tfo); err != nil {
+		if err := setTFO(syscall.Handle(fd), config.ParseTFOValue()); err != nil {
 			return err
 		}
 	}

+ 4 - 21
transport/internet/system_dialer.go

@@ -63,40 +63,23 @@ func (d *DefaultSystemDialer) lookupIP(domain string, strategy DomainStrategy, l
 		return nil, nil
 	}
 
-	var option = dns.IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-		FakeEnable: false,
-	}
-
+	var opt dns.Option
 	switch {
 	case strategy == DomainStrategy_USE_IP4 || (localAddr != nil && localAddr.Family().IsIPv4()):
-		option = dns.IPOption{
-			IPv4Enable: true,
-			IPv6Enable: false,
-			FakeEnable: false,
-		}
+		opt = dns.LookupIPv4Only
 	case strategy == DomainStrategy_USE_IP6 || (localAddr != nil && localAddr.Family().IsIPv6()):
-		option = dns.IPOption{
-			IPv4Enable: false,
-			IPv6Enable: true,
-			FakeEnable: false,
-		}
+		opt = dns.LookupIPv6Only
 	case strategy == DomainStrategy_AS_IS:
 		return nil, nil
 	}
 
-	return d.dns.LookupIP(domain, option)
+	return d.dns.LookupOptions(domain, opt, dns.LookupNoFake)
 }
 
 func (d *DefaultSystemDialer) canLookupIP(ctx context.Context, dst net.Destination, sockopt *SocketConfig) bool {
 	if sockopt == nil || dst.Address.Family().IsIP() || d.dns == nil {
 		return false
 	}
-	if dst.Address.Domain() == LookupDomainFromContext(ctx) {
-		newError("infinite loop detected").AtError().WriteToLog(session.ExportIDToError(ctx))
-		return false
-	}
 	return sockopt.DomainStrategy != DomainStrategy_AS_IS
 }
 

+ 0 - 18
transport/internet/system_dialer_context.go

@@ -1,18 +0,0 @@
-package internet
-
-import "context"
-
-type systemDialer int
-
-const systemDialerKey systemDialer = 0
-
-func ContextWithLookupDomain(ctx context.Context, domain string) context.Context {
-	return context.WithValue(ctx, systemDialerKey, domain)
-}
-
-func LookupDomainFromContext(ctx context.Context) string {
-	if domain, ok := ctx.Value(systemDialerKey).(string); ok {
-		return domain
-	}
-	return ""
-}

+ 7 - 7
transport/internet/tcp/dialer.go

@@ -21,14 +21,14 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
 
 	if config := tls.ConfigFromStreamSettings(streamSettings); config != nil {
 		tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
-		/*
-			if config.IsExperiment8357() {
-				conn = tls.UClient(conn, tlsConfig)
-			} else {
-				conn = tls.Client(conn, tlsConfig)
+		if fingerprint, ok := tls.Fingerprints[config.Fingerprint]; ok {
+			conn = tls.UClient(conn, tlsConfig, fingerprint)
+			if err := conn.(*tls.UConn).Handshake(); err != nil {
+				return nil, err
 			}
-		*/
-		conn = tls.Client(conn, tlsConfig)
+		} else {
+			conn = tls.Client(conn, tlsConfig)
+		}
 	} else if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil {
 		xtlsConfig := config.GetXTLSConfig(xtls.WithDestination(dest))
 		conn = xtls.Client(conn, xtlsConfig)

+ 2 - 1
transport/internet/tcp/hub.go

@@ -38,7 +38,8 @@ func ListenTCP(ctx context.Context, address net.Address, port net.Port, streamSe
 		if streamSettings.SocketSettings == nil {
 			streamSettings.SocketSettings = &internet.SocketConfig{}
 		}
-		streamSettings.SocketSettings.AcceptProxyProtocol = l.config.AcceptProxyProtocol
+		streamSettings.SocketSettings.AcceptProxyProtocol =
+			l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol
 	}
 	var listener net.Listener
 	var err error

+ 0 - 10
transport/internet/tls/config.go

@@ -18,8 +18,6 @@ var (
 	globalSessionCache = tls.NewLRUClientSessionCache(128)
 )
 
-const exp8357 = "experiment:8357"
-
 // ParseCertificate converts a cert.Certificate to Certificate.
 func ParseCertificate(c *cert.Certificate) *Certificate {
 	if c != nil {
@@ -240,15 +238,7 @@ func getNewGetCertficateFunc(certs []*tls.Certificate) func(hello *tls.ClientHel
 	}
 }
 
-func (c *Config) IsExperiment8357() bool {
-	return strings.HasPrefix(c.ServerName, exp8357)
-}
-
 func (c *Config) parseServerName() string {
-	if c.IsExperiment8357() {
-		return c.ServerName[len(exp8357):]
-	}
-
 	return c.ServerName
 }
 

+ 22 - 11
transport/internet/tls/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: transport/internet/tls/config.proto
 
 package tls
@@ -200,6 +200,8 @@ type Config struct {
 	CipherSuites string `protobuf:"bytes,9,opt,name=cipher_suites,json=cipherSuites,proto3" json:"cipher_suites,omitempty"`
 	// Whether the server selects its most preferred ciphersuite.
 	PreferServerCipherSuites bool `protobuf:"varint,10,opt,name=prefer_server_cipher_suites,json=preferServerCipherSuites,proto3" json:"prefer_server_cipher_suites,omitempty"`
+	// TLS Client Hello fingerprint (uTLS).
+	Fingerprint string `protobuf:"bytes,11,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"`
 }
 
 func (x *Config) Reset() {
@@ -304,6 +306,13 @@ func (x *Config) GetPreferServerCipherSuites() bool {
 	return false
 }
 
+func (x *Config) GetFingerprint() string {
+	if x != nil {
+		return x.Fingerprint
+	}
+	return ""
+}
+
 var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
 
 var file_transport_internet_tls_config_proto_rawDesc = []byte{
@@ -333,7 +342,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
 	0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10,
 	0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59,
 	0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f,
-	0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xd3, 0x03, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66,
+	0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xf5, 0x03, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66,
 	0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65,
 	0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f,
 	0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, 0x72,
@@ -362,15 +371,17 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
 	0x0a, 0x1b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f,
 	0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, 0x73, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x0a, 0x20,
 	0x01, 0x28, 0x08, 0x52, 0x18, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65,
-	0x72, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x42, 0x73, 0x0a,
-	0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70,
-	0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73,
-	0x50, 0x01, 0x5a, 0x30, 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, 0x74, 0x72,
-	0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
-	0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e,
-	0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54,
-	0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x72, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x12, 0x20, 0x0a,
+	0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x42,
+	0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e,
+	0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74,
+	0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 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,
+	0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
+	0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72,
+	0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
+	0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (

+ 3 - 0
transport/internet/tls/config.proto

@@ -64,4 +64,7 @@ message Config {
 
   // Whether the server selects its most preferred ciphersuite.
   bool prefer_server_cipher_suites = 10;
+
+  // TLS Client Hello fingerprint (uTLS).
+  string fingerprint = 11;
 }

+ 34 - 14
transport/internet/tls/tls.go

@@ -3,6 +3,8 @@ package tls
 import (
 	"crypto/tls"
 
+	utls "github.com/refraction-networking/utls"
+
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/net"
 )
@@ -41,25 +43,43 @@ func Client(c net.Conn, config *tls.Config) net.Conn {
 	return &Conn{Conn: tlsConn}
 }
 
-/*
+// Server initiates a TLS server handshake on the given connection.
+func Server(c net.Conn, config *tls.Config) net.Conn {
+	tlsConn := tls.Server(c, config)
+	return &Conn{Conn: tlsConn}
+}
+
+type UConn struct {
+	*utls.UConn
+}
+
+func (c *UConn) HandshakeAddress() net.Address {
+	if err := c.Handshake(); err != nil {
+		return nil
+	}
+	state := c.ConnectionState()
+	if state.ServerName == "" {
+		return nil
+	}
+	return net.ParseAddress(state.ServerName)
+}
+
+func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn {
+	utlsConn := utls.UClient(c, copyConfig(config), *fingerprint)
+	return &UConn{UConn: utlsConn}
+}
+
 func copyConfig(c *tls.Config) *utls.Config {
 	return &utls.Config{
-		NextProtos:         c.NextProtos,
+		RootCAs:            c.RootCAs,
 		ServerName:         c.ServerName,
 		InsecureSkipVerify: c.InsecureSkipVerify,
-		MinVersion:         utls.VersionTLS12,
-		MaxVersion:         utls.VersionTLS12,
 	}
 }
 
-func UClient(c net.Conn, config *tls.Config) net.Conn {
-	uConfig := copyConfig(config)
-	return utls.Client(c, uConfig)
-}
-*/
-
-// Server initiates a TLS server handshake on the given connection.
-func Server(c net.Conn, config *tls.Config) net.Conn {
-	tlsConn := tls.Server(c, config)
-	return &Conn{Conn: tlsConn}
+var Fingerprints = map[string]*utls.ClientHelloID{
+	"chrome":     &utls.HelloChrome_Auto,
+	"firefox":    &utls.HelloFirefox_Auto,
+	"safari":     &utls.HelloIOS_Auto,
+	"randomized": &utls.HelloRandomized,
 }

+ 49 - 0
transport/internet/websocket/dialer.go

@@ -2,8 +2,12 @@ package websocket
 
 import (
 	"context"
+	_ "embed"
 	"encoding/base64"
+	"fmt"
 	"io"
+	"net/http"
+	"os"
 	"time"
 
 	"github.com/gorilla/websocket"
@@ -15,6 +19,27 @@ import (
 	"github.com/xtls/xray-core/transport/internet/tls"
 )
 
+//go:embed dialer.html
+var webpage []byte
+var conns chan *websocket.Conn
+
+func init() {
+	if addr := os.Getenv("XRAY_BROWSER_DIALER"); addr != "" {
+		conns = make(chan *websocket.Conn, 256)
+		go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.Path == "/websocket" {
+				if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
+					conns <- conn
+				} else {
+					fmt.Println("unexpected error")
+				}
+			} else {
+				w.Write(webpage)
+			}
+		}))
+	}
+}
+
 // Dial dials a WebSocket connection to the given destination.
 func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) {
 	newError("creating connection to ", dest).WriteToLog(session.ExportIDToError(ctx))
@@ -66,6 +91,30 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
 	}
 	uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
 
+	if conns != nil {
+		data := []byte(uri)
+		if ed != nil {
+			data = append(data, " "+base64.RawURLEncoding.EncodeToString(ed)...)
+		}
+		var conn *websocket.Conn
+		for {
+			conn = <-conns
+			if conn.WriteMessage(websocket.TextMessage, data) != nil {
+				conn.Close()
+			} else {
+				break
+			}
+		}
+		if _, p, err := conn.ReadMessage(); err != nil {
+			conn.Close()
+			return nil, err
+		} else if s := string(p); s != "ok" {
+			conn.Close()
+			return nil, newError(s)
+		}
+		return newConnection(conn, conn.RemoteAddr(), nil), nil
+	}
+
 	header := wsSettings.GetRequestHeader()
 	if ed != nil {
 		header.Set("Sec-WebSocket-Protocol", base64.StdEncoding.EncodeToString(ed))

+ 55 - 0
transport/internet/websocket/dialer.html

@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Browser Dialer</title>
+</head>
+<body>
+	<script>
+		// Copyright (c) 2021 XRAY. Mozilla Public License 2.0.
+		var url = "ws://" + window.location.host + "/websocket"
+		var count = 0
+		setInterval(check, 1000)
+		function check() {
+			if (count <= 0) {
+				count += 1
+				console.log("Prepare", url)
+				var ws = new WebSocket(url)
+				var wss = undefined
+				var first = true
+				ws.onmessage = function (event) {
+					if (first) {
+						first = false
+						count -= 1
+						var arr = event.data.split(" ")
+						console.log("Dial", arr[0], arr[1])
+						wss = new WebSocket(arr[0], arr[1])
+						var opened = false
+						wss.onopen = function (event) {
+							opened = true
+							ws.send("ok")
+						}
+						wss.onmessage = function (event) {
+							ws.send(event.data)
+						}
+						wss.onclose = function (event) {
+							ws.close()
+						}
+						wss.onerror = function (event) {
+							!opened && ws.send("fail")
+							wss.close()
+						}
+						check()
+					} else wss.send(event.data)
+				}
+				ws.onclose = function (event) {
+					if (first) count -= 1
+					else wss.close()
+				}
+				ws.onerror = function (event) {
+					ws.close()
+				}
+			}
+		}
+	</script>
+</body>
+</html>

+ 17 - 9
transport/internet/websocket/hub.go

@@ -7,6 +7,7 @@ import (
 	"encoding/base64"
 	"io"
 	"net/http"
+	"strings"
 	"sync"
 	"time"
 
@@ -25,6 +26,8 @@ type requestHandler struct {
 	ln   *Listener
 }
 
+var replacer = strings.NewReplacer("+", "-", "/", "_", "=", "")
+
 var upgrader = &websocket.Upgrader{
 	ReadBufferSize:   4 * 1024,
 	WriteBufferSize:  4 * 1024,
@@ -39,7 +42,17 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
 		writer.WriteHeader(http.StatusNotFound)
 		return
 	}
-	conn, err := upgrader.Upgrade(writer, request, nil)
+
+	var extraReader io.Reader
+	var responseHeader = http.Header{}
+	if str := request.Header.Get("Sec-WebSocket-Protocol"); str != "" {
+		if ed, err := base64.RawURLEncoding.DecodeString(replacer.Replace(str)); err == nil && len(ed) > 0 {
+			extraReader = bytes.NewReader(ed)
+			responseHeader.Set("Sec-WebSocket-Protocol", str)
+		}
+	}
+
+	conn, err := upgrader.Upgrade(writer, request, responseHeader)
 	if err != nil {
 		newError("failed to convert to WebSocket connection").Base(err).WriteToLog()
 		return
@@ -54,12 +67,6 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
 		}
 	}
 
-	var extraReader io.Reader
-	if str := request.Header.Get("Sec-WebSocket-Protocol"); str != "" {
-		if ed, err := base64.StdEncoding.DecodeString(str); err == nil && len(ed) > 0 {
-			extraReader = bytes.NewReader(ed)
-		}
-	}
 	h.ln.addConn(newConnection(conn, remoteAddr, extraReader))
 }
 
@@ -82,7 +89,8 @@ func ListenWS(ctx context.Context, address net.Address, port net.Port, streamSet
 		if streamSettings.SocketSettings == nil {
 			streamSettings.SocketSettings = &internet.SocketConfig{}
 		}
-		streamSettings.SocketSettings.AcceptProxyProtocol = l.config.AcceptProxyProtocol
+		streamSettings.SocketSettings.AcceptProxyProtocol =
+			l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol
 	}
 	var listener net.Listener
 	var err error
@@ -128,7 +136,7 @@ func ListenWS(ctx context.Context, address net.Address, port net.Port, streamSet
 			ln:   l,
 		},
 		ReadHeaderTimeout: time.Second * 4,
-		MaxHeaderBytes:    2048,
+		MaxHeaderBytes:    4096,
 	}
 
 	go func() {