From cd4631ce9982d8be15f5a0d10df8c79bcf40c82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 16 Oct 2021 21:02:51 +0800 Subject: [PATCH] Merge dns (#722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DNS: add clientip for specific nameserver * Refactoring: DNS App * DNS: add DNS over QUIC support * Feat: add disableCache option for DNS * Feat: add queryStrategy option for DNS * Feat: add disableFallback & skipFallback option for DNS * Feat: DNS hosts support multiple addresses * Feat: DNS transport over TCP * DNS: fix typo & refine code * DNS: refine code * Add disableFallbackIfMatch dns option * Feat: routing and freedom outbound ignore Fake DNS Turn off fake DNS for request sent from Routing and Freedom outbound. Fake DNS now only apply to DNS outbound. This is important for Android, where VPN service take over all system DNS traffic and pass it to core. "UseIp" option can be used in Freedom outbound to avoid getting fake IP and fail connection. * Fix test * Fix dns return * Fix local dns return empty * Apply timeout to dns outbound * Update app/dns/config.go Co-authored-by: Loyalsoldier <10487845+loyalsoldier@users.noreply.github.com> Co-authored-by: Ye Zhihao Co-authored-by: maskedeken <52683904+maskedeken@users.noreply.github.com> Co-authored-by: V2Fly Team <51714622+vcptr@users.noreply.github.com> Co-authored-by: CalmLong <37164399+calmlong@users.noreply.github.com> Co-authored-by: Shelikhoo Co-authored-by: 秋のかえで Co-authored-by: 朱聖黎 Co-authored-by: rurirei <72071920+rurirei@users.noreply.github.com> Co-authored-by: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Co-authored-by: Arthur Morgan <4637240+badO1a5A90@users.noreply.github.com> --- app/dispatcher/default.go | 35 +- app/dns/config.go | 63 +++ app/dns/config.pb.go | 311 +++++++++---- app/dns/config.proto | 19 +- app/dns/dns.go | 290 ++++++++++++ app/dns/{server_test.go => dns_test.go} | 1 - app/dns/hosts.go | 72 ++- app/dns/hosts_test.go | 48 +- app/dns/nameserver.go | 213 ++++++++- app/dns/{dohdns.go => nameserver_doh.go} | 98 ++-- app/dns/nameserver_doh_test.go | 60 +++ app/dns/nameserver_fakedns.go | 8 +- app/dns/nameserver_local.go | 50 ++ ...erver_test.go => nameserver_local_test.go} | 7 +- app/dns/nameserver_quic.go | 389 ++++++++++++++++ app/dns/nameserver_quic_test.go | 43 ++ app/dns/nameserver_tcp.go | 362 +++++++++++++++ app/dns/nameserver_tcp_test.go | 60 +++ app/dns/{udpns.go => nameserver_udp.go} | 91 ++-- app/dns/server.go | 437 ------------------ app/router/command/config.go | 7 + app/router/router.go | 10 +- app/router/router_test.go | 2 +- common/session/session.go | 2 +- features/routing/context.go | 3 + features/routing/session/context.go | 8 + infra/conf/dns.go | 340 +++++++++----- infra/conf/dns_proxy.go | 8 +- infra/conf/dns_test.go | 32 +- infra/conf/freedom.go | 6 +- proxy/dns/config.pb.go | 26 +- proxy/dns/config.proto | 1 + proxy/dns/dns.go | 30 +- 33 files changed, 2252 insertions(+), 880 deletions(-) create mode 100644 app/dns/config.go rename app/dns/{server_test.go => dns_test.go} (99%) rename app/dns/{dohdns.go => nameserver_doh.go} (83%) create mode 100644 app/dns/nameserver_doh_test.go create mode 100644 app/dns/nameserver_local.go rename app/dns/{nameserver_test.go => nameserver_local_test.go} (73%) create mode 100644 app/dns/nameserver_quic.go create mode 100644 app/dns/nameserver_quic_test.go create mode 100644 app/dns/nameserver_tcp.go create mode 100644 app/dns/nameserver_tcp_test.go rename app/dns/{udpns.go => nameserver_udp.go} (75%) delete mode 100644 app/dns/server.go diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index e7679751..2d513eb6 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -309,23 +309,14 @@ 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 route, err := d.router.PickRoute(routingLink); err == nil { - outTag := route.GetOutboundTag() - isPickRoute = true - if h := d.ohm.GetHandler(outTag); h != nil { - newError("taking detour [", outTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx)) + if d.router != nil { + if route, err := d.router.PickRoute(routing_session.AsRoutingContext(ctx)); err == nil { + tag := route.GetOutboundTag() + if h := d.ohm.GetHandler(tag); h != nil { + newError("taking detour [", tag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx)) handler = h } else { - newError("non existing outTag: ", outTag).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + newError("non existing outTag: ", tag).AtWarning().WriteToLog(session.ExportIDToError(ctx)) } } else { newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx)) @@ -345,19 +336,7 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport. if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil { if tag := handler.Tag(); tag != "" { - if isPickRoute { - if inTag != "" { - accessMessage.Detour = inTag + " -> " + tag - } else { - accessMessage.Detour = tag - } - } else { - if inTag != "" { - accessMessage.Detour = inTag + " >> " + tag - } else { - accessMessage.Detour = tag - } - } + accessMessage.Detour = tag } log.Record(accessMessage) } diff --git a/app/dns/config.go b/app/dns/config.go new file mode 100644 index 00000000..6236f7b5 --- /dev/null +++ b/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() +} diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index e810fede..4d7d2a5d 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -74,12 +74,63 @@ 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 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"` @@ -124,6 +175,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 @@ -169,6 +234,11 @@ 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 + DisableCache bool `protobuf:"varint,8,opt,name=disableCache,proto3" json:"disableCache,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"` + DisableFallbackIfMatch bool `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,omitempty"` } func (x *Config) Reset() { @@ -247,6 +317,34 @@ func (x *Config) GetTag() string { return "" } +func (x *Config) GetDisableCache() bool { + if x != nil { + return x.DisableCache + } + return false +} + +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 +} + +func (x *Config) GetDisableFallbackIfMatch() bool { + if x != nil { + return x.DisableFallbackIfMatch + } + return false +} + type NameServer_PriorityDomain struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -366,8 +464,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"` } @@ -441,78 +538,98 @@ 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, 0xa5, 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, 0xef, 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, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x2a, 0x45, 0x0a, - 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d, 0x0a, - 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, - 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, - 0x65, 0x78, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, - 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, - 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, + 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22, 0x0a, 0x0c, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, + 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, 0x12, 0x36, + 0x0a, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, + 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, + 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 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, 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 ( @@ -527,37 +644,39 @@ 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, 2) 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 + (*NameServer)(nil), // 2: xray.app.dns.NameServer + (*Config)(nil), // 3: xray.app.dns.Config + (*NameServer_PriorityDomain)(nil), // 4: xray.app.dns.NameServer.PriorityDomain + (*NameServer_OriginalRule)(nil), // 5: xray.app.dns.NameServer.OriginalRule + nil, // 6: xray.app.dns.Config.HostsEntry + (*Config_HostMapping)(nil), // 7: xray.app.dns.Config.HostMapping + (*net.Endpoint)(nil), // 8: xray.common.net.Endpoint + (*router.GeoIP)(nil), // 9: xray.app.router.GeoIP + (*net.IPOrDomain)(nil), // 10: 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 + 8, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint + 4, // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain + 9, // 2: xray.app.dns.NameServer.geoip:type_name -> xray.app.router.GeoIP + 5, // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule + 8, // 4: xray.app.dns.Config.NameServers:type_name -> xray.common.net.Endpoint + 2, // 5: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer + 6, // 6: xray.app.dns.Config.Hosts:type_name -> xray.app.dns.Config.HostsEntry + 7, // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping + 1, // 8: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy + 0, // 9: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType + 10, // 10: xray.app.dns.Config.HostsEntry.value:type_name -> xray.common.net.IPOrDomain + 0, // 11: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_app_dns_config_proto_init() } @@ -632,7 +751,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: 2, NumMessages: 6, NumExtensions: 0, NumServices: 0, diff --git a/app/dns/config.proto b/app/dns/config.proto index 94488719..e2059e38 100644 --- a/app/dns/config.proto +++ b/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,12 @@ enum DomainMatchingType { Regex = 3; } +enum QueryStrategy { + USE_IP = 0; + USE_IP4 = 1; + USE_IP6 = 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 +67,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 +77,12 @@ message Config { string tag = 6; reserved 7; + + // DisableCache disables DNS cache + bool disableCache = 8; + + QueryStrategy query_strategy = 9; + + bool disableFallback = 10; + bool disableFallbackIfMatch = 11; } diff --git a/app/dns/dns.go b/app/dns/dns.go index e56671e4..112c14b6 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -2,3 +2,293 @@ package dns //go:generate go run github.com/xtls/xray-core/common/errors/errorgen + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/features" + "github.com/xtls/xray-core/features/dns" +) + +// DNS is a DNS rely server. +type DNS struct { + sync.Mutex + tag string + disableCache bool + disableFallback bool + disableFallbackIfMatch bool + ipOption *dns.IPOption + hosts *StaticHosts + clients []*Client + domainMatcher strmatcher.IndexMatcher + matcherInfos []*DomainMatcherInfo +} + +// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher +type DomainMatcherInfo struct { + clientIdx uint16 + domainRuleIdx uint16 +} + +// New creates a new DNS server with given configuration. +func New(ctx context.Context, config *Config) (*DNS, error) { + var tag string + if len(config.Tag) > 0 { + tag = config.Tag + } else { + tag = generateRandomTag() + } + + var clientIP net.IP + switch len(config.ClientIp) { + case 0, net.IPv4len, net.IPv6len: + clientIP = net.IP(config.ClientIp) + default: + return nil, newError("unexpected client IP length ", len(config.ClientIp)) + } + + var ipOption *dns.IPOption + switch config.QueryStrategy { + case QueryStrategy_USE_IP: + ipOption = &dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + } + case QueryStrategy_USE_IP4: + ipOption = &dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, + } + case QueryStrategy_USE_IP6: + ipOption = &dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + } + } + + hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts) + if err != nil { + return nil, newError("failed to create hosts").Base(err) + } + + clients := []*Client{} + domainRuleCount := 0 + for _, ns := range config.NameServer { + domainRuleCount += len(ns.PrioritizedDomain) + } + + // MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1 + matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1) + domainMatcher := &strmatcher.MatcherGroup{} + geoipContainer := router.GeoIPMatcherContainer{} + + for _, endpoint := range config.NameServers { + features.PrintDeprecatedFeatureWarning("simple DNS server") + client, err := NewSimpleClient(ctx, endpoint, clientIP) + if err != nil { + return nil, newError("failed to create client").Base(err) + } + clients = append(clients, client) + } + + for _, ns := range config.NameServer { + clientIdx := len(clients) + updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) error { + midx := domainMatcher.Add(domainRule) + matcherInfos[midx] = &DomainMatcherInfo{ + clientIdx: uint16(clientIdx), + domainRuleIdx: uint16(originalRuleIdx), + } + return nil + } + + myClientIP := clientIP + switch len(ns.ClientIp) { + case net.IPv4len, net.IPv6len: + myClientIP = net.IP(ns.ClientIp) + } + client, err := NewClient(ctx, ns, myClientIP, geoipContainer, &matcherInfos, updateDomain) + if err != nil { + return nil, newError("failed to create client").Base(err) + } + clients = append(clients, client) + } + + // If there is no DNS client in config, add a `localhost` DNS client + if len(clients) == 0 { + clients = append(clients, NewLocalDNSClient()) + } + + return &DNS{ + tag: tag, + hosts: hosts, + ipOption: ipOption, + clients: clients, + domainMatcher: domainMatcher, + matcherInfos: matcherInfos, + disableCache: config.DisableCache, + disableFallback: config.DisableFallback, + disableFallbackIfMatch: config.DisableFallbackIfMatch, + }, nil +} + +// Type implements common.HasType. +func (*DNS) Type() interface{} { + return dns.ClientType() +} + +// Start implements common.Runnable. +func (s *DNS) Start() error { + return nil +} + +// Close implements common.Closable. +func (s *DNS) Close() error { + return nil +} + +// IsOwnLink implements proxy.dns.ownLinkVerifier +func (s *DNS) IsOwnLink(ctx context.Context) bool { + inbound := session.InboundFromContext(ctx) + return inbound != nil && inbound.Tag == s.tag +} + +// LookupIP implements dns.Client. +func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) { + if domain == "" { + return nil, newError("empty domain name") + } + + option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable + option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable + + if !option.IPv4Enable && !option.IPv6Enable { + return nil, dns.ErrEmptyResponse + } + + // Normalize the FQDN form query + if strings.HasSuffix(domain, ".") { + domain = domain[:len(domain)-1] + } + + // Static host lookup + switch addrs := s.hosts.Lookup(domain, option); { + case addrs == nil: // Domain not recorded in static host + break + case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled) + return nil, dns.ErrEmptyResponse + case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement + newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog() + domain = addrs[0].Domain() + default: // Successfully found ip records in static host + newError("returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs).WriteToLog() + return toNetIP(addrs) + } + + // Name servers lookup + errs := []error{} + ctx := session.ContextWithInbound(context.Background(), &session.Inbound{Tag: s.tag}) + for _, client := range s.sortClients(domain) { + if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { + newError("skip DNS resolution for domain ", domain, " at server ", client.Name()).AtDebug().WriteToLog() + continue + } + ips, err := client.QueryIP(ctx, domain, option, s.disableCache) + if len(ips) > 0 { + return ips, nil + } + if err != nil { + newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog() + errs = append(errs, err) + } + if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch { + return nil, err + } + } + + return nil, newError("returning nil for domain ", domain).Base(errors.Combine(errs...)) +} + +// GetIPOption implements ClientWithIPOption. +func (s *DNS) GetIPOption() *dns.IPOption { + return s.ipOption +} + +// SetQueryOption implements ClientWithIPOption. +func (s *DNS) SetQueryOption(isIPv4Enable, isIPv6Enable bool) { + s.ipOption.IPv4Enable = isIPv4Enable + s.ipOption.IPv6Enable = isIPv6Enable +} + +// SetFakeDNSOption implements ClientWithIPOption. +func (s *DNS) SetFakeDNSOption(isFakeEnable bool) { + s.ipOption.FakeEnable = isFakeEnable +} + +func (s *DNS) sortClients(domain string) []*Client { + clients := make([]*Client, 0, len(s.clients)) + clientUsed := make([]bool, len(s.clients)) + clientNames := make([]string, 0, len(s.clients)) + domainRules := []string{} + + // Priority domain matching + hasMatch := false + for _, match := range s.domainMatcher.Match(domain) { + info := s.matcherInfos[match] + client := s.clients[info.clientIdx] + domainRule := client.domains[info.domainRuleIdx] + domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx)) + if clientUsed[info.clientIdx] { + continue + } + clientUsed[info.clientIdx] = true + clients = append(clients, client) + clientNames = append(clientNames, client.Name()) + hasMatch = true + } + + if !(s.disableFallback || s.disableFallbackIfMatch && hasMatch) { + // Default round-robin query + for idx, client := range s.clients { + if clientUsed[idx] || client.skipFallback { + continue + } + clientUsed[idx] = true + clients = append(clients, client) + clientNames = append(clientNames, client.Name()) + } + } + + if len(domainRules) > 0 { + newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog() + } + if len(clientNames) > 0 { + newError("domain ", domain, " will use DNS in order: ", clientNames).AtDebug().WriteToLog() + } + + if len(clients) == 0 { + clients = append(clients, s.clients[0]) + clientNames = append(clientNames, s.clients[0].Name()) + newError("domain ", domain, " will use the first DNS: ", clientNames).AtDebug().WriteToLog() + } + + return clients +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/app/dns/server_test.go b/app/dns/dns_test.go similarity index 99% rename from app/dns/server_test.go rename to app/dns/dns_test.go index feb382fa..8468e653 100644 --- a/app/dns/server_test.go +++ b/app/dns/dns_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/miekg/dns" - "github.com/xtls/xray-core/app/dispatcher" . "github.com/xtls/xray-core/app/dns" "github.com/xtls/xray-core/app/policy" diff --git a/app/dns/hosts.go b/app/dns/hosts.go index 0b584782..64413481 100644 --- a/app/dns/hosts.go +++ b/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,8 @@ 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) @@ -74,19 +57,10 @@ 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 } @@ -100,24 +74,36 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address { filtered = append(filtered, ip) } } - if len(filtered) == 0 { - return nil - } return filtered } -// LookupIP returns IP address for the given domain, if exists in this StaticHosts. -func (h *StaticHosts) LookupIP(domain string, option dns.IPOption) []net.Address { - indices := h.matchers.Match(domain) - if len(indices) == 0 { - return nil - } - ips := []net.Address{} - for _, id := range indices { +func (h *StaticHosts) lookupInternal(domain string) []net.Address { + var ips []net.Address + for _, id := range h.matchers.Match(domain) { ips = append(ips, h.ips[id]...) } - if len(ips) == 1 && ips[0].Family().IsDomain() { - return ips - } - return filterIP(ips, option) + return ips +} + +func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) []net.Address { + switch addrs := h.lookupInternal(domain); { + case len(addrs) == 0: // Not recorded in static hosts, return nil + return nil + case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain + 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) } diff --git a/app/dns/hosts_test.go b/app/dns/hosts_test.go index 2d6929af..81998bdc 100644 --- a/app/dns/hosts_test.go +++ b/app/dns/hosts_test.go @@ -4,7 +4,6 @@ import ( "testing" "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" @@ -20,6 +19,20 @@ func TestStaticHosts(t *testing.T) { {1, 1, 1, 1}, }, }, + { + Type: DomainMatchingType_Full, + Domain: "proxy.v2fly.org", + 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.v2fly.org", + }, + { + Type: DomainMatchingType_Full, + Domain: "proxy2.v2fly.org", + ProxiedDomain: "proxy.v2fly.org", + }, { Type: DomainMatchingType_Subdomain, Domain: "example.cn", @@ -32,6 +45,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 +54,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 +67,33 @@ func TestStaticHosts(t *testing.T) { } { - ips := hosts.LookupIP("www.example.cn", dns.IPOption{ + domain := hosts.Lookup("proxy.v2fly.org", 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.v2fly.org"); diff != "" { + t.Error(diff) + } + } + + { + domain := hosts.Lookup("proxy2.v2fly.org", 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.v2fly.org"); diff != "" { + t.Error(diff) + } + } + + { + ips := hosts.Lookup("www.example.cn", dns.IPOption{ IPv4Enable: true, IPv6Enable: true, }) @@ -66,7 +106,7 @@ func TestStaticHosts(t *testing.T) { } { - ips := hosts.LookupIP("baidu.com", dns.IPOption{ + ips := hosts.Lookup("baidu.com", dns.IPOption{ IPv4Enable: false, IPv6Enable: true, }) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index 2b557099..b15803e6 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -2,40 +2,219 @@ 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" + "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/dns" - "github.com/xtls/xray-core/features/dns/localdns" + "github.com/xtls/xray-core/features/routing" ) -// Client is the interface for DNS client. -type Client interface { +// Server is the interface for Name Server. +type Server interface { // Name of the Client. Name() string - // QueryIP sends IP queries to its configured server. - QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, error) + QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, error) } -type LocalNameServer struct { - client *localdns.Client +// Client is the interface for DNS client. +type Client struct { + server Server + clientIP net.IP + skipFallback bool + domains []string + expectIPs []*router.GeoIPMatcher } -func (s *LocalNameServer) QueryIP(_ context.Context, domain string, option dns.IPOption) ([]net.IP, error) { - if option.IPv4Enable || option.IPv6Enable { - return s.client.LookupIP(domain, option) +var errExpectedIPNonMatch = errors.New("expectIPs not match") + +// NewServer creates a name server object according to the network destination url. +func NewServer(dest net.Destination, dispatcher routing.Dispatcher) (Server, error) { + if address := dest.Address; address.Family().IsDomain() { + u, err := url.Parse(address.Domain()) + if err != nil { + return nil, err + } + switch { + case strings.EqualFold(u.String(), "localhost"): + return NewLocalNameServer(), nil + case strings.EqualFold(u.Scheme, "https"): // DOH Remote mode + return NewDoHNameServer(u, dispatcher) + case strings.EqualFold(u.Scheme, "https+local"): // DOH Local mode + return NewDoHLocalNameServer(u), nil + case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode + return NewQUICNameServer(u) + case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode + return NewTCPNameServer(u, dispatcher) + case strings.EqualFold(u.Scheme, "tcp+local"): // DNS-over-TCP Local mode + return NewTCPLocalNameServer(u) + case strings.EqualFold(u.String(), "fakedns"): + return NewFakeDNSServer(), nil + } + } + if dest.Network == net.Network_Unknown { + dest.Network = net.Network_UDP + } + if dest.Network == net.Network_UDP { // UDP classic DNS mode + return NewClassicNameServer(dest, dispatcher), nil + } + return nil, newError("No available name server could be created from ", dest).AtWarning() +} + +// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs. +func NewClient(ctx context.Context, ns *NameServer, clientIP net.IP, container router.GeoIPMatcherContainer, matcherInfos *[]*DomainMatcherInfo, updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo) error) (*Client, error) { + client := &Client{} + + err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { + // Create a new server for each client for now + server, err := NewServer(ns.Address.AsDestination(), dispatcher) + if err != nil { + return newError("failed to create nameserver").Base(err).AtWarning() + } + + // Priotize local domains with specific TLDs or without any dot to local DNS + if _, isLocalDNS := server.(*LocalNameServer); isLocalDNS { + ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...) + ns.OriginalRules = append(ns.OriginalRules, localTLDsAndDotlessDomainsRule) + // The following lines is a solution to avoid core panics(rule index out of range) when setting `localhost` DNS client in config. + // Because the `localhost` DNS client will apend len(localTLDsAndDotlessDomains) rules into matcherInfos to match `geosite:private` default rule. + // But `matcherInfos` has no enough length to add rules, which leads to core panics (rule index out of range). + // To avoid this, the length of `matcherInfos` must be equal to the expected, so manually append it with Golang default zero value first for later modification. + // Related issues: + // https://github.com/v2fly/v2ray-core/issues/529 + // https://github.com/v2fly/v2ray-core/issues/719 + 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.skipFallback = ns.SkipFallback + client.domains = rules + client.expectIPs = matchers + return nil + }) + return client, err +} + +// NewSimpleClient creates a DNS client with a simple destination. +func NewSimpleClient(ctx context.Context, endpoint *net.Endpoint, clientIP net.IP) (*Client, error) { + client := &Client{} + err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { + server, err := NewServer(endpoint.AsDestination(), dispatcher) + if err != nil { + return newError("failed to create nameserver").Base(err).AtWarning() + } + client.server = server + client.clientIP = clientIP + return nil + }) + + if len(clientIP) > 0 { + switch endpoint.Address.GetAddress().(type) { + case *net.IPOrDomain_Domain: + newError("DNS: client ", endpoint.Address.GetDomain(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog() + case *net.IPOrDomain_Ip: + newError("DNS: client ", endpoint.Address.GetIp(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog() + } } - return nil, newError("neither IPv4 nor IPv6 is enabled") + return client, err } -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, disableCache bool) ([]net.IP, error) { + ctx, cancel := context.WithTimeout(ctx, 4*time.Second) + ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option, disableCache) + cancel() + + if err != nil { + return ips, err } + return c.MatchExpectedIPs(domain, ips) +} + +// MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones. +func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error) { + if len(c.expectIPs) == 0 { + return ips, nil + } + newIps := []net.IP{} + for _, ip := range ips { + for _, matcher := range c.expectIPs { + if matcher.Match(ip) { + newIps = append(newIps, ip) + break + } + } + } + if len(newIps) == 0 { + return nil, errExpectedIPNonMatch + } + newError("domain ", domain, " expectIPs ", newIps, " matched at server ", c.Name()).AtDebug().WriteToLog() + return newIps, nil } diff --git a/app/dns/dohdns.go b/app/dns/nameserver_doh.go similarity index 83% rename from app/dns/dohdns.go rename to app/dns/nameserver_doh.go index 6719428f..6286c3e5 100644 --- a/app/dns/dohdns.go +++ b/app/dns/nameserver_doh.go @@ -32,20 +32,19 @@ import ( type DoHNameServer struct { dispatcher routing.Dispatcher sync.RWMutex - ips map[string]record + ips map[string]*record pub *pubsub.Service cleanup *task.Periodic reqID uint32 - clientIP net.IP httpClient *http.Client dohURL string name string } -// NewDoHNameServer creates DOH client object for remote resolving -func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.IP) (*DoHNameServer, error) { +// NewDoHNameServer creates DOH server object for remote resolving. +func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher) (*DoHNameServer, error) { newError("DNS: created Remote DOH client for ", url.String()).AtInfo().WriteToLog() - s := baseDOHNameServer(url, "DOH", clientIP) + s := baseDOHNameServer(url, "DOH") s.dispatcher = dispatcher tr := &http.Transport{ @@ -104,9 +103,9 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net. } // NewDoHLocalNameServer creates DOH client object for local resolving -func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer { +func NewDoHLocalNameServer(url *url.URL) *DoHNameServer { url.Scheme = "https" - s := baseDOHNameServer(url, "DOHL", clientIP) + s := baseDOHNameServer(url, "DOHL") tr := &http.Transport{ IdleConnTimeout: 90 * time.Second, ForceAttemptHTTP2: true, @@ -136,23 +135,21 @@ 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, Execute: s.Cleanup, } - return s } -// Name returns client name +// Name implements Server. func (s *DoHNameServer) Name() string { return s.name } @@ -184,7 +181,7 @@ func (s *DoHNameServer) Cleanup() error { } if len(s.ips) == 0 { - s.ips = make(map[string]record) + s.ips = make(map[string]*record) } return nil @@ -194,7 +191,10 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) { elapsed := time.Since(req.start) s.Lock() - rec := s.ips[req.domain] + rec, found := s.ips[req.domain] + if !found { + rec = &record{} + } updated := false switch req.reqType { @@ -204,7 +204,7 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) { updated = true } case dnsmessage.TypeAAAA: - addr := make([]net.Address, 0) + addr := make([]net.Address, 0, len(ipRec.IP)) for _, ip := range ipRec.IP { if len(ip.IP()) == net.IPv6len { addr = append(addr, ip) @@ -235,7 +235,7 @@ func (s *DoHNameServer) newReqID() uint16 { return uint16(atomic.AddUint32(&s.reqID, 1)) } -func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option dns_feature.IPOption) { +func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) { newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx)) if s.name+"." == "DOH//"+domain { @@ -243,7 +243,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 { @@ -256,7 +256,7 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option dns 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() + dnsCtx := ctx // reserve internal dns server requested Inbound if inbound := session.InboundFromContext(ctx); inbound != nil { @@ -264,8 +264,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 @@ -330,30 +330,30 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt return nil, errRecordNotFound } + var err4 error + var err6 error 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...) + var ip6 []net.Address + + if option.IPv4Enable { + ips, err4 = record.A.getIPs() } - 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 option.IPv6Enable { + ip6, err6 = record.AAAA.getIPs() + ips = append(ips, ip6...) } if len(ips) > 0 { - return toNetIP(ips), nil + return toNetIP(ips) } - if lastErr != nil { - return nil, lastErr + if err4 != nil { + return nil, err4 + } + + if err6 != nil { + return nil, err6 } if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) { @@ -363,15 +363,19 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt return nil, errRecordNotFound } -// QueryIP is called from dns.Server->queryIPTimeout -func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, error) { // nolint: dupl +// QueryIP implements Server. +func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl fqdn := Fqdn(domain) - ips, err := s.findIPsForDomain(fqdn, option) - if err != errRecordNotFound { - newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() - log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, err + if disableCache { + newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog() + } else { + ips, err := s.findIPsForDomain(fqdn, option) + if err != errRecordNotFound { + newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() + log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) + return ips, err + } } // ipv4 and ipv6 belong to different subscription groups @@ -400,7 +404,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 { diff --git a/app/dns/nameserver_doh_test.go b/app/dns/nameserver_doh_test.go new file mode 100644 index 00000000..d439a93b --- /dev/null +++ b/app/dns/nameserver_doh_test.go @@ -0,0 +1,60 @@ +package dns_test + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + dns_feature "github.com/xtls/xray-core/features/dns" +) + +func TestDOHNameServer(t *testing.T) { + url, err := url.Parse("https+local://1.1.1.1/dns-query") + common.Must(err) + + s := NewDoHLocalNameServer(url) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }, false) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } +} + +func TestDOHNameServerWithCache(t *testing.T) { + url, err := url.Parse("https+local://1.1.1.1/dns-query") + common.Must(err) + + s := NewDoHLocalNameServer(url) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }, false) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }, true) + cancel() + common.Must(err) + if r := cmp.Diff(ips2, ips); r != "" { + t.Fatal(r) + } +} diff --git a/app/dns/nameserver_fakedns.go b/app/dns/nameserver_fakedns.go index 32d8bcd6..54476ac2 100644 --- a/app/dns/nameserver_fakedns.go +++ b/app/dns/nameserver_fakedns.go @@ -20,7 +20,7 @@ func (FakeDNSServer) Name() string { return "FakeDNS" } -func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ dns.IPOption) ([]net.IP, error) { +func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, _ dns.IPOption, _ bool) ([]net.IP, error) { if f.fakeDNSEngine == nil { if err := core.RequireFeatures(ctx, func(fd dns.FakeDNSEngine) { f.fakeDNSEngine = fd @@ -30,9 +30,9 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ dns.IPOpti } ips := f.fakeDNSEngine.GetFakeIPForDomain(domain) - netIP := toNetIP(ips) - if netIP == nil { - return nil, newError("Unable to convert IP to net ip").AtError() + netIP, err := toNetIP(ips) + if err != nil { + return nil, newError("Unable to convert IP to net ip").Base(err).AtError() } newError(f.Name(), " got answer: ", domain, " -> ", ips).AtInfo().WriteToLog() diff --git a/app/dns/nameserver_local.go b/app/dns/nameserver_local.go new file mode 100644 index 00000000..87c70b82 --- /dev/null +++ b/app/dns/nameserver_local.go @@ -0,0 +1,50 @@ +package dns + +import ( + "context" + "github.com/xtls/xray-core/features/dns" + "strings" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns/localdns" +) + +// LocalNameServer is an wrapper over local DNS feature. +type LocalNameServer struct { + client *localdns.Client +} + +const errEmptyResponse = "No address associated with hostname" + +// QueryIP implements Server. +func (s *LocalNameServer) QueryIP(_ context.Context, domain string, _ net.IP, option dns.IPOption, _ bool) (ips []net.IP, err error) { + ips, err = s.client.LookupIP(domain, option) + + if err != nil && strings.HasSuffix(err.Error(), errEmptyResponse) { + err = dns.ErrEmptyResponse + } + + if len(ips) > 0 { + newError("Localhost got answer: ", domain, " -> ", ips).AtInfo().WriteToLog() + } + + return +} + +// 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()} +} diff --git a/app/dns/nameserver_test.go b/app/dns/nameserver_local_test.go similarity index 73% rename from app/dns/nameserver_test.go rename to app/dns/nameserver_local_test.go index 9bd1a4a1..b3d9678c 100644 --- a/app/dns/nameserver_test.go +++ b/app/dns/nameserver_local_test.go @@ -2,22 +2,23 @@ package dns_test import ( "context" + "github.com/xtls/xray-core/common/net" "testing" "time" . "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/features/dns" ) func TestLocalNameServer(t *testing.T) { s := NewLocalNameServer() ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - ips, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + ips, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{ IPv4Enable: true, IPv6Enable: true, FakeEnable: false, - }) + }, false) cancel() common.Must(err) if len(ips) == 0 { diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go new file mode 100644 index 00000000..9c712f17 --- /dev/null +++ b/app/dns/nameserver_quic.go @@ -0,0 +1,389 @@ +package dns + +import ( + "context" + "net/url" + "sync" + "sync/atomic" + "time" + + "github.com/lucas-clemente/quic-go" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/dns" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal/pubsub" + "github.com/xtls/xray-core/common/task" + dns_feature "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/net/dns/dnsmessage" + "golang.org/x/net/http2" +) + +// NextProtoDQ - During connection establishment, DNS/QUIC support is indicated +// by selecting the ALPN token "dq" in the crypto handshake. +const NextProtoDQ = "doq-i00" + +const handshakeTimeout = time.Second * 8 + +// QUICNameServer implemented DNS over QUIC +type QUICNameServer struct { + sync.RWMutex + ips map[string]*record + pub *pubsub.Service + cleanup *task.Periodic + reqID uint32 + name string + destination *net.Destination + session quic.Session +} + +// NewQUICNameServer creates DNS-over-QUIC client object for local resolving +func NewQUICNameServer(url *url.URL) (*QUICNameServer, error) { + newError("DNS: created Local DNS-over-QUIC client for ", url.String()).AtInfo().WriteToLog() + + var err error + port := net.Port(784) + if url.Port() != "" { + port, err = net.PortFromString(url.Port()) + if err != nil { + return nil, err + } + } + dest := net.UDPDestination(net.ParseAddress(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, found := s.ips[req.domain] + if !found { + rec = &record{} + } + 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 := ctx + + // reserve internal dns server requested Inbound + if inbound := session.InboundFromContext(ctx); inbound != nil { + dnsCtx = session.ContextWithInbound(dnsCtx, inbound) + } + + dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{ + Protocol: "quic", + SkipDNSResolve: true, + }) + + var cancel context.CancelFunc + dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline) + defer cancel() + + b, err := dns.PackMessage(r.msg) + if err != nil { + newError("failed to pack dns query").Base(err).AtError().WriteToLog() + return + } + + conn, err := s.openStream(dnsCtx) + if err != nil { + newError("failed to open quic session").Base(err).AtError().WriteToLog() + return + } + + _, err = conn.Write(b.Bytes()) + if err != nil { + newError("failed to send query").Base(err).AtError().WriteToLog() + return + } + + _ = conn.Close() + + respBuf := buf.New() + defer respBuf.Release() + n, err := respBuf.ReadFrom(conn) + if err != nil && n == 0 { + newError("failed to read response").Base(err).AtError().WriteToLog() + return + } + + rec, err := parseResponse(respBuf.Bytes()) + if err != nil { + newError("failed to handle response").Base(err).AtError().WriteToLog() + return + } + s.updateIP(r, rec) + }(req) + } +} + +func (s *QUICNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) { + s.RLock() + record, found := s.ips[domain] + s.RUnlock() + + if !found { + return nil, errRecordNotFound + } + + var err4 error + var err6 error + var ips []net.Address + var ip6 []net.Address + + if option.IPv4Enable { + ips, err4 = record.A.getIPs() + } + + if option.IPv6Enable { + ip6, err6 = record.AAAA.getIPs() + ips = append(ips, ip6...) + } + + if len(ips) > 0 { + return toNetIP(ips) + } + + if err4 != nil { + return nil, err4 + } + + if err6 != nil { + return nil, err6 + } + + if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) { + return nil, dns_feature.ErrEmptyResponse + } + + return nil, errRecordNotFound +} + +// QueryIP is called from dns.Server->queryIPTimeout +func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { + fqdn := Fqdn(domain) + + if disableCache { + newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog() + } else { + ips, err := s.findIPsForDomain(fqdn, option) + if err != errRecordNotFound { + newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() + return ips, err + } + } + + // ipv4 and ipv6 belong to different subscription groups + var sub4, sub6 *pubsub.Subscriber + if option.IPv4Enable { + sub4 = s.pub.Subscribe(fqdn + "4") + defer sub4.Close() + } + if option.IPv6Enable { + sub6 = s.pub.Subscribe(fqdn + "6") + defer sub6.Close() + } + done := make(chan interface{}) + go func() { + if sub4 != nil { + select { + case <-sub4.Wait(): + case <-ctx.Done(): + } + } + if sub6 != nil { + select { + case <-sub6.Wait(): + case <-ctx.Done(): + } + } + close(done) + }() + s.sendQuery(ctx, fqdn, clientIP, option) + + for { + ips, err := s.findIPsForDomain(fqdn, option) + if err != errRecordNotFound { + return ips, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-done: + } + } +} + +func isActive(s quic.Session) bool { + select { + case <-s.Context().Done(): + return false + default: + return true + } +} + +func (s *QUICNameServer) getSession() (quic.Session, error) { + var session quic.Session + s.RLock() + session = s.session + if session != nil && isActive(session) { + s.RUnlock() + return session, nil + } + if session != nil { + // we're recreating the session, let's create a new one + _ = session.CloseWithError(0, "") + } + s.RUnlock() + + s.Lock() + defer s.Unlock() + + var err error + session, err = s.openSession() + if err != nil { + // This does not look too nice, but QUIC (or maybe quic-go) + // doesn't seem stable enough. + // Maybe retransmissions aren't fully implemented in quic-go? + // Anyways, the simple solution is to make a second try when + // it fails to open the QUIC session. + session, err = s.openSession() + if err != nil { + return nil, err + } + } + s.session = session + return session, nil +} + +func (s *QUICNameServer) openSession() (quic.Session, error) { + tlsConfig := tls.Config{} + quicConfig := &quic.Config{ + HandshakeIdleTimeout: handshakeTimeout, + } + + session, err := quic.DialAddrContext(context.Background(), s.destination.NetAddr(), tlsConfig.GetTLSConfig(tls.WithNextProto("http/1.1", http2.NextProtoTLS, NextProtoDQ)), quicConfig) + if err != nil { + return nil, err + } + + return session, nil +} + +func (s *QUICNameServer) openStream(ctx context.Context) (quic.Stream, error) { + session, err := s.getSession() + if err != nil { + return nil, err + } + + // open a new stream + return session.OpenStreamSync(ctx) +} diff --git a/app/dns/nameserver_quic_test.go b/app/dns/nameserver_quic_test.go new file mode 100644 index 00000000..9ed33484 --- /dev/null +++ b/app/dns/nameserver_quic_test.go @@ -0,0 +1,43 @@ +package dns_test + +import ( + "context" + "github.com/xtls/xray-core/features/dns" + "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" +) + +func TestQUICNameServer(t *testing.T) { + url, err := url.Parse("quic://dns.adguard.com") + common.Must(err) + s, err := NewQUICNameServer(url) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }, false) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }, true) + cancel() + common.Must(err) + if r := cmp.Diff(ips2, ips); r != "" { + t.Fatal(r) + } +} diff --git a/app/dns/nameserver_tcp.go b/app/dns/nameserver_tcp.go new file mode 100644 index 00000000..50dc62f0 --- /dev/null +++ b/app/dns/nameserver_tcp.go @@ -0,0 +1,362 @@ +package dns + +import ( + "bytes" + "context" + "encoding/binary" + "net/url" + "sync" + "sync/atomic" + "time" + + "golang.org/x/net/dns/dnsmessage" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "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/features/routing" + "github.com/xtls/xray-core/transport/internet" +) + +// TCPNameServer implemented DNS over TCP (RFC7766). +type TCPNameServer struct { + sync.RWMutex + name string + destination *net.Destination + ips map[string]*record + pub *pubsub.Service + cleanup *task.Periodic + reqID uint32 + dial func(context.Context) (net.Conn, error) +} + +// NewTCPNameServer creates DNS over TCP server object for remote resolving. +func NewTCPNameServer(url *url.URL, dispatcher routing.Dispatcher) (*TCPNameServer, error) { + s, err := baseTCPNameServer(url, "TCP") + if err != nil { + return nil, err + } + + s.dial = func(ctx context.Context) (net.Conn, error) { + link, err := dispatcher.Dispatch(ctx, *s.destination) + if err != nil { + return nil, err + } + + return cnc.NewConnection( + cnc.ConnectionInputMulti(link.Writer), + cnc.ConnectionOutputMulti(link.Reader), + ), nil + } + + return s, nil +} + +// NewTCPLocalNameServer creates DNS over TCP client object for local resolving +func NewTCPLocalNameServer(url *url.URL) (*TCPNameServer, error) { + s, err := baseTCPNameServer(url, "TCPL") + if err != nil { + return nil, err + } + + s.dial = func(ctx context.Context) (net.Conn, error) { + return internet.DialSystem(ctx, *s.destination, nil) + } + + return s, nil +} + +func baseTCPNameServer(url *url.URL, prefix string) (*TCPNameServer, error) { + var err error + port := net.Port(53) + if url.Port() != "" { + port, err = net.PortFromString(url.Port()) + if err != nil { + return nil, err + } + } + dest := net.TCPDestination(net.ParseAddress(url.Hostname()), port) + + s := &TCPNameServer{ + destination: &dest, + ips: make(map[string]*record), + pub: pubsub.NewService(), + name: prefix + "//" + dest.NetAddr(), + } + s.cleanup = &task.Periodic{ + Interval: time.Minute, + Execute: s.Cleanup, + } + + return s, nil +} + +// Name implements Server. +func (s *TCPNameServer) Name() string { + return s.name +} + +// Cleanup clears expired items from cache +func (s *TCPNameServer) 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 *TCPNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) { + elapsed := time.Since(req.start) + + s.Lock() + rec, found := s.ips[req.domain] + if !found { + rec = &record{} + } + 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 *TCPNameServer) newReqID() uint16 { + return uint16(atomic.AddUint32(&s.reqID, 1)) +} + +func (s *TCPNameServer) 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(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) { + dnsCtx := ctx + + if inbound := session.InboundFromContext(ctx); inbound != nil { + dnsCtx = session.ContextWithInbound(dnsCtx, inbound) + } + + dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{ + Protocol: "dns", + SkipDNSResolve: true, + }) + + var cancel context.CancelFunc + dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline) + defer cancel() + + b, err := dns.PackMessage(r.msg) + if err != nil { + newError("failed to pack dns query").Base(err).AtError().WriteToLog() + return + } + + conn, err := s.dial(dnsCtx) + if err != nil { + newError("failed to dial namesever").Base(err).AtError().WriteToLog() + return + } + defer conn.Close() + dnsReqBuf := buf.New() + binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) + dnsReqBuf.Write(b.Bytes()) + b.Release() + + _, err = conn.Write(dnsReqBuf.Bytes()) + if err != nil { + newError("failed to send query").Base(err).AtError().WriteToLog() + return + } + dnsReqBuf.Release() + + respBuf := buf.New() + defer respBuf.Release() + n, err := respBuf.ReadFullFrom(conn, 2) + if err != nil && n == 0 { + newError("failed to read response length").Base(err).AtError().WriteToLog() + return + } + var length int16 + err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) + if err != nil { + newError("failed to parse response length").Base(err).AtError().WriteToLog() + return + } + respBuf.Clear() + n, err = respBuf.ReadFullFrom(conn, int32(length)) + if err != nil && n == 0 { + newError("failed to read response length").Base(err).AtError().WriteToLog() + return + } + + rec, err := parseResponse(respBuf.Bytes()) + if err != nil { + newError("failed to parse DNS over TCP response").Base(err).AtError().WriteToLog() + return + } + + s.updateIP(r, rec) + }(req) + } +} + +func (s *TCPNameServer) 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 err4 error + var err6 error + var ips []net.Address + var ip6 []net.Address + + if option.IPv4Enable { + ips, err4 = record.A.getIPs() + } + + if option.IPv6Enable { + ip6, err6 = record.AAAA.getIPs() + ips = append(ips, ip6...) + } + + if len(ips) > 0 { + return toNetIP(ips) + } + + if err4 != nil { + return nil, err4 + } + + if err6 != nil { + return nil, err6 + } + + return nil, dns_feature.ErrEmptyResponse +} + +// QueryIP implements Server. +func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { + fqdn := Fqdn(domain) + + if disableCache { + newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog() + } else { + ips, err := s.findIPsForDomain(fqdn, option) + if err != errRecordNotFound { + newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() + return ips, err + } + } + + // ipv4 and ipv6 belong to different subscription groups + var sub4, sub6 *pubsub.Subscriber + if option.IPv4Enable { + sub4 = s.pub.Subscribe(fqdn + "4") + defer sub4.Close() + } + if option.IPv6Enable { + sub6 = s.pub.Subscribe(fqdn + "6") + defer sub6.Close() + } + done := make(chan interface{}) + go func() { + if sub4 != nil { + select { + case <-sub4.Wait(): + case <-ctx.Done(): + } + } + if sub6 != nil { + select { + case <-sub6.Wait(): + case <-ctx.Done(): + } + } + close(done) + }() + s.sendQuery(ctx, fqdn, clientIP, option) + + for { + ips, err := s.findIPsForDomain(fqdn, option) + if err != errRecordNotFound { + return ips, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-done: + } + } +} diff --git a/app/dns/nameserver_tcp_test.go b/app/dns/nameserver_tcp_test.go new file mode 100644 index 00000000..4e4ea746 --- /dev/null +++ b/app/dns/nameserver_tcp_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 TestTCPLocalNameServer(t *testing.T) { + url, err := url.Parse("tcp+local://8.8.8.8") + common.Must(err) + s, err := NewTCPLocalNameServer(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, + }, false) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } +} + +func TestTCPLocalNameServerWithCache(t *testing.T) { + url, err := url.Parse("tcp+local://8.8.8.8") + common.Must(err) + s, err := NewTCPLocalNameServer(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, + }, false) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }, true) + cancel() + common.Must(err) + if r := cmp.Diff(ips2, ips); r != "" { + t.Fatal(r) + } +} diff --git a/app/dns/udpns.go b/app/dns/nameserver_udp.go similarity index 75% rename from app/dns/udpns.go rename to app/dns/nameserver_udp.go index c32fe593..d68ffdd0 100644 --- a/app/dns/udpns.go +++ b/app/dns/nameserver_udp.go @@ -22,30 +22,30 @@ import ( "github.com/xtls/xray-core/transport/internet/udp" ) +// ClassicNameServer implemented traditional UDP DNS. type ClassicNameServer struct { sync.RWMutex name string - address net.Destination - ips map[string]record - requests map[uint16]dnsRequest + address *net.Destination + ips map[string]*record + requests map[uint16]*dnsRequest pub *pubsub.Service 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) } s := &ClassicNameServer{ - address: address, - ips: make(map[string]record), - requests: make(map[uint16]dnsRequest), - clientIP: clientIP, + address: &address, + ips: make(map[string]*record), + requests: make(map[uint16]*dnsRequest), pub: pubsub.NewService(), name: strings.ToUpper(address.String()), } @@ -58,10 +58,12 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher return s } +// Name implements Server. func (s *ClassicNameServer) Name() string { return s.name } +// Cleanup clears expired items from cache func (s *ClassicNameServer) Cleanup() error { now := time.Now() s.Lock() @@ -80,6 +82,7 @@ func (s *ClassicNameServer) Cleanup() error { } if record.A == nil && record.AAAA == nil { + newError(s.name, " cleanup ", domain).AtDebug().WriteToLog() delete(s.ips, domain) } else { s.ips[domain] = record @@ -87,7 +90,7 @@ func (s *ClassicNameServer) Cleanup() error { } if len(s.ips) == 0 { - s.ips = make(map[string]record) + s.ips = make(map[string]*record) } for id, req := range s.requests { @@ -97,12 +100,13 @@ func (s *ClassicNameServer) Cleanup() error { } if len(s.requests) == 0 { - s.requests = make(map[uint16]dnsRequest) + s.requests = make(map[uint16]*dnsRequest) } 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 { @@ -134,15 +138,17 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot elapsed := time.Since(req.start) newError(s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog() if len(req.domain) > 0 && (rec.A != nil || rec.AAAA != nil) { - s.updateIP(req.domain, rec) + s.updateIP(req.domain, &rec) } } -func (s *ClassicNameServer) updateIP(domain string, newRec record) { +func (s *ClassicNameServer) updateIP(domain string, newRec *record) { s.Lock() - newError(s.name, " updating IP records for domain:", domain).AtDebug().WriteToLog() - rec := s.ips[domain] + rec, found := s.ips[domain] + if !found { + rec = &record{} + } updated := false if isNewer(rec.A, newRec.A) { @@ -155,6 +161,7 @@ func (s *ClassicNameServer) updateIP(domain string, newRec record) { } if updated { + newError(s.name, " updating IP records for domain:", domain).AtDebug().WriteToLog() s.ips[domain] = rec } if newRec.A != nil { @@ -177,13 +184,13 @@ func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) { id := req.msg.ID req.expire = time.Now().Add(time.Second * 8) - s.requests[id] = *req + 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) @@ -202,7 +209,7 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option Status: log.AccessAccepted, Reason: "", }) - s.udpServer.Dispatch(udpCtx, s.address, b) + s.udpServer.Dispatch(udpCtx, *s.address, b) } } @@ -215,44 +222,48 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.I return nil, errRecordNotFound } + var err4 error + var err6 error var ips []net.Address - var lastErr error + var ip6 []net.Address + if option.IPv4Enable { - a, err := record.A.getIPs() - if err != nil { - lastErr = err - } - ips = append(ips, a...) + ips, err4 = record.A.getIPs() } if option.IPv6Enable { - aaaa, err := record.AAAA.getIPs() - if err != nil { - lastErr = err - } - ips = append(ips, aaaa...) + ip6, err6 = record.AAAA.getIPs() + ips = append(ips, ip6...) } if len(ips) > 0 { - return toNetIP(ips), nil + return toNetIP(ips) } - if lastErr != nil { - return nil, lastErr + if err4 != nil { + return nil, err4 + } + + if err6 != nil { + return nil, err6 } return nil, dns_feature.ErrEmptyResponse } // QueryIP implements Server. -func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, error) { +func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { fqdn := Fqdn(domain) - ips, err := s.findIPsForDomain(fqdn, option) - if err != errRecordNotFound { - newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() - log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, err + if disableCache { + newError("DNS cache is disabled. Querying IP for ", domain, " at ", s.name).AtDebug().WriteToLog() + } else { + ips, err := s.findIPsForDomain(fqdn, option) + if err != errRecordNotFound { + newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() + log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) + return ips, err + } } // ipv4 and ipv6 belong to different subscription groups @@ -281,7 +292,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 { diff --git a/app/dns/server.go b/app/dns/server.go deleted file mode 100644 index 57d3f01b..00000000 --- a/app/dns/server.go +++ /dev/null @@ -1,437 +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" -) - -// 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, - }) - } - - 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") - } - - // 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)) - })) -} diff --git a/app/router/command/config.go b/app/router/command/config.go index e95418dc..a033e731 100644 --- a/app/router/command/config.go +++ b/app/router/command/config.go @@ -28,6 +28,13 @@ 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? +// TODO: please confirm @Vigilans +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} diff --git a/app/router/router.go b/app/router/router.go index 5d2bdc34..8c8b32e4 100644 --- a/app/router/router.go +++ b/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 } diff --git a/app/router/router_test.go b/app/router/router_test.go index cc8e2b3a..c980e238 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -2,6 +2,7 @@ package router_test import ( "context" + "github.com/xtls/xray-core/features/dns" "testing" "github.com/golang/mock/gomock" @@ -10,7 +11,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" diff --git a/common/session/session.go b/common/session/session.go index 832eb960..aa85aa58 100644 --- a/common/session/session.go +++ b/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. diff --git a/features/routing/context.go b/features/routing/context.go index f5a732a4..e7867c32 100644 --- a/features/routing/context.go +++ b/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 } diff --git a/features/routing/session/context.go b/features/routing/session/context.go index 0b37ebfa..ce7e9493 100644 --- a/features/routing/session/context.go +++ b/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{ diff --git a/infra/conf/dns.go b/infra/conf/dns.go index ea86359e..e09bc1eb 100644 --- a/infra/conf/dns.go +++ b/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,22 @@ 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 +124,193 @@ 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 *HostsWrapper `json:"hosts"` + ClientIP *Address `json:"clientIp"` + Tag string `json:"tag"` + QueryStrategy string `json:"queryStrategy"` + DisableCache bool `json:"disableCache"` + DisableFallback bool `json:"disableFallback"` + DisableFallbackIfMatch bool `json:"disableFallbackIfMatch"` } -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 +} + +type HostsWrapper struct { + Hosts map[string]*HostAddress +} + +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, + } +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (m *HostsWrapper) UnmarshalJSON(data []byte) error { + hosts := make(map[string]*HostAddress) + err := json.Unmarshal(data, &hosts) + if err == nil { + m.Hosts = hosts + return nil + } + return newError("invalid DNS hosts").Base(err) +} + +// Build implements Buildable +func (m *HostsWrapper) Build() ([]*dns.Config_HostMapping, error) { + mappings := make([]*dns.Config_HostMapping, 0, 20) + + domains := make([]string, 0, len(m.Hosts)) + for domain := range m.Hosts { + domains = append(domains, domain) + } + sort.Strings(domains) + + for _, domain := range domains { + switch { + case strings.HasPrefix(domain, "domain:"): + domainName := domain[7:] + if len(domainName) == 0 { + return nil, newError("empty domain type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Subdomain + mapping.Domain = domainName + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "geosite:"): + listName := domain[8:] + if len(listName) == 0 { + return nil, newError("empty geosite rule: ", domain) + } + geositeList, err := loadGeositeWithAttr("geosite.dat", listName) + if err != nil { + return nil, newError("failed to load geosite: ", listName).Base(err) + } + for _, d := range geositeList { + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = typeMap[d.Type] + mapping.Domain = d.Value + mappings = append(mappings, mapping) + } + + case strings.HasPrefix(domain, "regexp:"): + regexpVal := domain[7:] + if len(regexpVal) == 0 { + return nil, newError("empty regexp type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Regex + mapping.Domain = regexpVal + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "keyword:"): + keywordVal := domain[8:] + if len(keywordVal) == 0 { + return nil, newError("empty keyword type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Keyword + mapping.Domain = keywordVal + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "full:"): + fullVal := domain[5:] + if len(fullVal) == 0 { + return nil, newError("empty full domain type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Full + mapping.Domain = fullVal + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "dotless:"): + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Regex + switch substr := domain[8:]; { + case substr == "": + mapping.Domain = "^[^.]*$" + case !strings.Contains(substr, "."): + mapping.Domain = "^[^.]*" + substr + "[^.]*$" + default: + return nil, newError("substr in dotless rule should not contain a dot: ", substr) + } + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "ext:"): + kv := strings.Split(domain[4:], ":") + if len(kv) != 2 { + return nil, newError("invalid external resource: ", domain) + } + filename := kv[0] + list := kv[1] + geositeList, err := loadGeositeWithAttr(filename, list) + if err != nil { + return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err) + } + for _, d := range geositeList { + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = typeMap[d.Type] + mapping.Domain = d.Value + mappings = append(mappings, mapping) + } + + default: + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Full + mapping.Domain = domain + mappings = append(mappings, mapping) + } + } + return mappings, nil } // Build implements Buildable func (c *DNSConfig) Build() (*dns.Config, error) { config := &dns.Config{ - Tag: c.Tag, + Tag: c.Tag, + DisableCache: c.DisableCache, + DisableFallback: c.DisableFallback, + DisableFallbackIfMatch: c.DisableFallbackIfMatch, } if c.ClientIP != nil { @@ -139,6 +320,16 @@ 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 + } + for _, server := range c.Servers { ns, err := server.Build() if err != nil { @@ -147,113 +338,12 @@ func (c *DNSConfig) Build() (*dns.Config, error) { config.NameServer = append(config.NameServer, ns) } - if c.Hosts != nil && len(c.Hosts) > 0 { - domains := make([]string, 0, len(c.Hosts)) - for domain := range c.Hosts { - domains = append(domains, domain) - } - sort.Strings(domains) - - for _, domain := range domains { - addr := c.Hosts[domain] - var mappings []*dns.Config_HostMapping - switch { - case strings.HasPrefix(domain, "domain:"): - domainName := domain[7:] - if len(domainName) == 0 { - return nil, newError("empty domain type of rule: ", domain) - } - mapping := getHostMapping(addr) - mapping.Type = dns.DomainMatchingType_Subdomain - mapping.Domain = domainName - mappings = append(mappings, mapping) - - case strings.HasPrefix(domain, "geosite:"): - listName := domain[8:] - if len(listName) == 0 { - return nil, newError("empty geosite rule: ", domain) - } - domains, err := loadGeositeWithAttr("geosite.dat", listName) - if err != nil { - return nil, newError("failed to load geosite: ", listName).Base(err) - } - for _, d := range domains { - mapping := getHostMapping(addr) - mapping.Type = typeMap[d.Type] - mapping.Domain = d.Value - mappings = append(mappings, mapping) - } - - case strings.HasPrefix(domain, "regexp:"): - regexpVal := domain[7:] - if len(regexpVal) == 0 { - return nil, newError("empty regexp type of rule: ", domain) - } - mapping := getHostMapping(addr) - mapping.Type = dns.DomainMatchingType_Regex - mapping.Domain = regexpVal - mappings = append(mappings, mapping) - - case strings.HasPrefix(domain, "keyword:"): - keywordVal := domain[8:] - if len(keywordVal) == 0 { - return nil, newError("empty keyword type of rule: ", domain) - } - mapping := getHostMapping(addr) - mapping.Type = dns.DomainMatchingType_Keyword - mapping.Domain = keywordVal - mappings = append(mappings, mapping) - - case strings.HasPrefix(domain, "full:"): - fullVal := domain[5:] - if len(fullVal) == 0 { - return nil, newError("empty full domain type of rule: ", domain) - } - mapping := getHostMapping(addr) - mapping.Type = dns.DomainMatchingType_Full - mapping.Domain = fullVal - mappings = append(mappings, mapping) - - case strings.HasPrefix(domain, "dotless:"): - mapping := getHostMapping(addr) - mapping.Type = dns.DomainMatchingType_Regex - switch substr := domain[8:]; { - case substr == "": - mapping.Domain = "^[^.]*$" - case !strings.Contains(substr, "."): - mapping.Domain = "^[^.]*" + substr + "[^.]*$" - default: - return nil, newError("substr in dotless rule should not contain a dot: ", substr) - } - mappings = append(mappings, mapping) - - case strings.HasPrefix(domain, "ext:"): - kv := strings.Split(domain[4:], ":") - if len(kv) != 2 { - return nil, newError("invalid external resource: ", domain) - } - filename := kv[0] - list := kv[1] - domains, err := loadGeositeWithAttr(filename, list) - if err != nil { - return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err) - } - for _, d := range domains { - mapping := getHostMapping(addr) - mapping.Type = typeMap[d.Type] - mapping.Domain = d.Value - mappings = append(mappings, mapping) - } - - default: - mapping := getHostMapping(addr) - mapping.Type = dns.DomainMatchingType_Full - mapping.Domain = domain - mappings = append(mappings, mapping) - } - - config.StaticHosts = append(config.StaticHosts, mappings...) + if c.Hosts != nil { + staticHosts, err := c.Hosts.Build() + if err != nil { + return nil, newError("failed to build hosts").Base(err) } + config.StaticHosts = append(config.StaticHosts, staticHosts...) } return config, nil diff --git a/infra/conf/dns_proxy.go b/infra/conf/dns_proxy.go index 3e8cd81f..c96846e3 100644 --- a/infra/conf/dns_proxy.go +++ b/infra/conf/dns_proxy.go @@ -8,9 +8,10 @@ import ( ) type DNSOutboundConfig struct { - Network Network `json:"network"` - Address *Address `json:"address"` - Port uint16 `json:"port"` + Network Network `json:"network"` + Address *Address `json:"address"` + Port uint16 `json:"port"` + UserLevel uint32 `json:"userLevel"` } func (c *DNSOutboundConfig) Build() (proto.Message, error) { @@ -19,6 +20,7 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) { Network: c.Network.Build(), Port: uint32(c.Port), }, + UserLevel: c.UserLevel, } if c.Address != nil { config.Server.Address = c.Address.Build() diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index 59d0dc41..8c583a51 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -69,16 +69,20 @@ func TestDNSConfigParsing(t *testing.T) { "servers": [{ "address": "8.8.8.8", "port": 5353, + "skipFallback": true, "domains": ["domain:example.com"] }], "hosts": { - "example.com": "127.0.0.1", "domain:example.com": "google.com", - "geosite:test": "10.0.0.1", - "keyword:google": "8.8.8.8", - "regexp:.*\\.com": "8.8.4.4" + "example.com": "127.0.0.1", + "keyword:google": ["8.8.8.8", "8.8.4.4"], + "regexp:.*\\.com": "8.8.4.4", + "www.example.org": ["127.0.0.1", "127.0.0.2"] }, - "clientIp": "10.0.0.1" + "clientIp": "10.0.0.1", + "queryStrategy": "UseIPv4", + "disableCache": true, + "disableFallback": true }`, Parser: parserCreator(), Output: &dns.Config{ @@ -93,6 +97,7 @@ func TestDNSConfigParsing(t *testing.T) { Network: net.Network_UDP, Port: 5353, }, + SkipFallback: true, PrioritizedDomain: []*dns.NameServer_PriorityDomain{ { Type: dns.DomainMatchingType_Subdomain, @@ -118,23 +123,26 @@ func TestDNSConfigParsing(t *testing.T) { Domain: "example.com", Ip: [][]byte{{127, 0, 0, 1}}, }, - { - Type: dns.DomainMatchingType_Full, - Domain: "example.com", - Ip: [][]byte{{10, 0, 0, 1}}, - }, { 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: "www.example.org", + Ip: [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}}, + }, }, - ClientIp: []byte{10, 0, 0, 1}, + ClientIp: []byte{10, 0, 0, 1}, + QueryStrategy: dns.QueryStrategy_USE_IP4, + DisableCache: true, + DisableFallback: true, }, }, }) diff --git a/infra/conf/freedom.go b/infra/conf/freedom.go index 7ef1cff1..3ba8e321 100644 --- a/infra/conf/freedom.go +++ b/infra/conf/freedom.go @@ -23,11 +23,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 } diff --git a/proxy/dns/config.pb.go b/proxy/dns/config.pb.go index f2017100..ee527533 100644 --- a/proxy/dns/config.pb.go +++ b/proxy/dns/config.pb.go @@ -28,7 +28,8 @@ type Config struct { // Server is the DNS server address. If specified, this address overrides the // original one. - Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` } func (x *Config) Reset() { @@ -70,6 +71,13 @@ func (x *Config) GetServer() *net.Endpoint { return nil } +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + var File_proxy_dns_config_proto protoreflect.FileDescriptor var file_proxy_dns_config_proto_rawDesc = []byte{ @@ -77,16 +85,18 @@ var file_proxy_dns_config_proto_rawDesc = []byte{ 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 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, 0x22, 0x3b, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 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, 0x06, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x42, 0x4c, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x23, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x64, 0x6e, 0x73, - 0xaa, 0x02, 0x0e, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x44, 0x6e, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x42, 0x4c, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x23, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x64, 0x6e, 0x73, 0xaa, + 0x02, 0x0e, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x44, 0x6e, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/dns/config.proto b/proxy/dns/config.proto index d4f4a308..0b324725 100644 --- a/proxy/dns/config.proto +++ b/proxy/dns/config.proto @@ -12,4 +12,5 @@ message Config { // Server is the DNS server address. If specified, this address overrides the // original one. xray.common.net.Endpoint server = 1; + uint32 user_level = 2; } diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go index ccce6283..0fcd502d 100644 --- a/proxy/dns/dns.go +++ b/proxy/dns/dns.go @@ -4,6 +4,7 @@ import ( "context" "io" "sync" + "time" "github.com/xtls/xray-core/transport/internet/stat" @@ -14,9 +15,11 @@ import ( "github.com/xtls/xray-core/common/net" dns_proto "github.com/xtls/xray-core/common/protocol/dns" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" ) @@ -24,8 +27,8 @@ import ( func init() { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { h := new(Handler) - if err := core.RequireFeatures(ctx, func(dnsClient dns.Client) error { - return h.Init(config.(*Config), dnsClient) + if err := core.RequireFeatures(ctx, func(dnsClient dns.Client, policyManager policy.Manager) error { + return h.Init(config.(*Config), dnsClient, policyManager) }); err != nil { return nil, err } @@ -41,10 +44,13 @@ type Handler struct { client dns.Client ownLinkVerifier ownLinkVerifier server net.Destination + timeout time.Duration } -func (h *Handler) Init(config *Config, dnsClient dns.Client) error { +func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error { h.client = dnsClient + h.timeout = policyManager.ForLevel(config.UserLevel).Timeouts.ConnectionIdle + if v, ok := dnsClient.(ownLinkVerifier); ok { h.ownLinkVerifier = v } @@ -144,6 +150,9 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. } } + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, h.timeout) + request := func() error { defer conn.Close() @@ -157,6 +166,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. return err } + timer.Update() + if !h.isOwnLink(ctx) { isIPQuery, domain, id, qType := parseIPQuery(b.Bytes()) if isIPQuery { @@ -182,6 +193,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. return err } + timer.Update() + if err := writer.WriteMessage(b); err != nil { return err } @@ -222,6 +235,17 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, return } + switch qType { + case dnsmessage.TypeA: + for i, ip := range ips { + ips[i] = ip.To4() + } + case dnsmessage.TypeAAAA: + for i, ip := range ips { + ips[i] = ip.To16() + } + } + b := buf.New() rawBytes := b.Extend(buf.Size) builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{