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{