Răsfoiți Sursa

Add Fake DNS support (#309)

Co-authored-by: Xiaokang Wang <[email protected]>
Co-authored-by: loyalsoldier <[email protected]>
Co-authored-by: kslr <[email protected]>
yuhan6665 4 ani în urmă
părinte
comite
f50eff5ebb
43 a modificat fișierele cu 1350 adăugiri și 300 ștergeri
  1. 70 28
      app/dispatcher/default.go
  2. 51 0
      app/dispatcher/fakednssniffer.go
  3. 83 13
      app/dispatcher/sniffer.go
  4. 2 0
      app/dns/config.proto
  5. 3 2
      app/dns/dnscommon.go
  6. 22 5
      app/dns/dnscommon_test.go
  7. 3 3
      app/dns/dohdns.go
  8. 9 0
      app/dns/fakedns/errors.generated.go
  9. 136 0
      app/dns/fakedns/fake.go
  10. 5 0
      app/dns/fakedns/fakedns.go
  11. 164 0
      app/dns/fakedns/fakedns.pb.go
  12. 12 0
      app/dns/fakedns/fakedns.proto
  13. 99 0
      app/dns/fakedns/fakedns_test.go
  14. 3 2
      app/dns/hosts.go
  15. 4 3
      app/dns/hosts_test.go
  16. 5 18
      app/dns/nameserver.go
  17. 43 0
      app/dns/nameserver_fakedns.go
  18. 3 1
      app/dns/nameserver_test.go
  19. 19 30
      app/dns/server.go
  20. 110 26
      app/dns/server_test.go
  21. 5 4
      app/dns/udpns.go
  22. 94 82
      app/proxyman/config.pb.go
  23. 5 1
      app/proxyman/config.proto
  24. 1 0
      app/proxyman/inbound/always.go
  25. 1 0
      app/proxyman/inbound/dynamic.go
  26. 11 1
      app/proxyman/inbound/worker.go
  27. 11 2
      app/router/router_test.go
  28. 85 0
      common/cache/lru.go
  29. 86 0
      common/cache/lru_test.go
  30. 1 0
      common/session/session.go
  31. 8 15
      features/dns/client.go
  32. 15 0
      features/dns/fakedns.go
  33. 17 34
      features/dns/localdns/client.go
  34. 9 0
      features/dns/localdns/errors.generated.go
  35. 5 1
      features/routing/dns/context.go
  36. 65 0
      infra/conf/fakedns.go
  37. 5 0
      infra/conf/init.go
  38. 23 0
      infra/conf/lint.go
  39. 21 0
      infra/conf/xray.go
  40. 1 0
      main/distro/all/all.go
  41. 15 17
      proxy/dns/dns.go
  42. 15 8
      proxy/freedom/freedom.go
  43. 5 4
      testing/mocks/dns.go

+ 70 - 28
app/dispatcher/default.go

@@ -15,6 +15,7 @@ import (
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/session"
 	"github.com/xtls/xray-core/core"
+	"github.com/xtls/xray-core/features/dns"
 	"github.com/xtls/xray-core/features/outbound"
 	"github.com/xtls/xray-core/features/policy"
 	"github.com/xtls/xray-core/features/routing"
@@ -175,17 +176,28 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
 	return inboundLink, outboundLink
 }
 
-func shouldOverride(result SniffResult, request session.SniffingRequest) bool {
+func shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool {
 	domain := result.Domain()
 	for _, d := range request.ExcludeForDomain {
 		if domain == d {
 			return false
 		}
 	}
-
-	protocol := result.Protocol()
+	var fakeDNSEngine dns.FakeDNSEngine
+	core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
+		fakeDNSEngine = fdns
+	})
+	protocolString := result.Protocol()
+	if resComp, ok := result.(SnifferResultComposite); ok {
+		protocolString = resComp.ProtocolForDomainResult()
+	}
 	for _, p := range request.OverrideDestinationForProtocol {
-		if strings.HasPrefix(protocol, p) {
+		if strings.HasPrefix(protocolString, p) {
+			return true
+		}
+		if fakeDNSEngine != nil && protocolString != "bittorrent" && p == "fakedns" &&
+			fakeDNSEngine.GetFakeIPRange().Contains(destination.Address.IP()) {
+			newError("Using sniffer ", protocolString, " since the fake DNS missed").WriteToLog(session.ExportIDToError(ctx))
 			return true
 		}
 	}
@@ -210,19 +222,33 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
 		ctx = session.ContextWithContent(ctx, content)
 	}
 	sniffingRequest := content.SniffingRequest
-	if destination.Network != net.Network_TCP || !sniffingRequest.Enabled {
+	switch {
+	case !sniffingRequest.Enabled:
+		go d.routedDispatch(ctx, outbound, destination)
+	case destination.Network != net.Network_TCP:
+		// Only metadata sniff will be used for non tcp connection
+		result, err := sniffer(ctx, nil, true)
+		if err == nil {
+			content.Protocol = result.Protocol()
+			if shouldOverride(ctx, result, sniffingRequest, destination) {
+				domain := result.Domain()
+				newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
+				destination.Address = net.ParseAddress(domain)
+				ob.Target = destination
+			}
+		}
 		go d.routedDispatch(ctx, outbound, destination)
-	} else {
+	default:
 		go func() {
 			cReader := &cachedReader{
 				reader: outbound.Reader.(*pipe.Reader),
 			}
 			outbound.Reader = cReader
-			result, err := sniffer(ctx, cReader)
+			result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
 			if err == nil {
 				content.Protocol = result.Protocol()
 			}
-			if err == nil && shouldOverride(result, sniffingRequest) {
+			if err == nil && shouldOverride(ctx, result, sniffingRequest, destination) {
 				domain := result.Domain()
 				newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
 				destination.Address = net.ParseAddress(domain)
@@ -234,34 +260,50 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
 	return inbound, nil
 }
 
-func sniffer(ctx context.Context, cReader *cachedReader) (SniffResult, error) {
+func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (SniffResult, error) {
 	payload := buf.New()
 	defer payload.Release()
 
-	sniffer := NewSniffer()
-	totalAttempt := 0
-	for {
-		select {
-		case <-ctx.Done():
-			return nil, ctx.Err()
-		default:
-			totalAttempt++
-			if totalAttempt > 2 {
-				return nil, errSniffingTimeout
-			}
+	sniffer := NewSniffer(ctx)
+
+	metaresult, metadataErr := sniffer.SniffMetadata(ctx)
+
+	if metadataOnly {
+		return metaresult, metadataErr
+	}
 
-			cReader.Cache(payload)
-			if !payload.IsEmpty() {
-				result, err := sniffer.Sniff(payload.Bytes())
-				if err != common.ErrNoClue {
-					return result, err
+	contentResult, contentErr := func() (SniffResult, error) {
+		totalAttempt := 0
+		for {
+			select {
+			case <-ctx.Done():
+				return nil, ctx.Err()
+			default:
+				totalAttempt++
+				if totalAttempt > 2 {
+					return nil, errSniffingTimeout
+				}
+
+				cReader.Cache(payload)
+				if !payload.IsEmpty() {
+					result, err := sniffer.Sniff(ctx, payload.Bytes())
+					if err != common.ErrNoClue {
+						return result, err
+					}
+				}
+				if payload.IsFull() {
+					return nil, errUnknownContent
 				}
-			}
-			if payload.IsFull() {
-				return nil, errUnknownContent
 			}
 		}
+	}()
+	if contentErr != nil && metadataErr == nil {
+		return metaresult, nil
+	}
+	if contentErr == nil && metadataErr == nil {
+		return CompositeResult(metaresult, contentResult), nil
 	}
+	return contentResult, contentErr
 }
 
 func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) {

+ 51 - 0
app/dispatcher/fakednssniffer.go

@@ -0,0 +1,51 @@
+// +build !confonly
+
+package dispatcher
+
+import (
+	"context"
+
+	"github.com/xtls/xray-core/core"
+	"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"
+)
+
+// newFakeDNSSniffer Create a Fake DNS metadata sniffer
+func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) {
+	var fakeDNSEngine dns.FakeDNSEngine
+	err := core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
+		fakeDNSEngine = fdns
+	})
+	if err != nil {
+		return protocolSnifferWithMetadata{}, err
+	}
+	if fakeDNSEngine == nil {
+		errNotInit := newError("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
+		return protocolSnifferWithMetadata{}, errNotInit
+	}
+	return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
+		Target := session.OutboundFromContext(ctx).Target
+		if Target.Network == net.Network_TCP || Target.Network == net.Network_UDP {
+			domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(Target.Address)
+			if domainFromFakeDNS != "" {
+				newError("fake dns got domain: ", domainFromFakeDNS, " for ip: ", Target.Address.String()).WriteToLog(session.ExportIDToError(ctx))
+				return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil
+			}
+		}
+		return nil, common.ErrNoClue
+	}, metadataSniffer: true}, nil
+}
+
+type fakeDNSSniffResult struct {
+	domainName string
+}
+
+func (fakeDNSSniffResult) Protocol() string {
+	return "fakedns"
+}
+
+func (f fakeDNSSniffResult) Domain() string {
+	return f.domainName
+}

+ 83 - 13
app/dispatcher/sniffer.go

@@ -1,6 +1,8 @@
 package dispatcher
 
 import (
+	"context"
+
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/protocol/bittorrent"
 	"github.com/xtls/xray-core/common/protocol/http"
@@ -12,30 +14,46 @@ type SniffResult interface {
 	Domain() string
 }
 
-type protocolSniffer func([]byte) (SniffResult, error)
+type protocolSniffer func(context.Context, []byte) (SniffResult, error)
+
+type protocolSnifferWithMetadata struct {
+	protocolSniffer protocolSniffer
+	// A Metadata sniffer will be invoked on connection establishment only, with nil body,
+	// for both TCP and UDP connections
+	// It will not be shown as a traffic type for routing unless there is no other successful sniffing.
+	metadataSniffer bool
+}
 
 type Sniffer struct {
-	sniffer []protocolSniffer
+	sniffer []protocolSnifferWithMetadata
 }
 
-func NewSniffer() *Sniffer {
-	return &Sniffer{
-		sniffer: []protocolSniffer{
-			func(b []byte) (SniffResult, error) { return http.SniffHTTP(b) },
-			func(b []byte) (SniffResult, error) { return tls.SniffTLS(b) },
-			func(b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) },
+func NewSniffer(ctx context.Context) *Sniffer {
+	ret := &Sniffer{
+		sniffer: []protocolSnifferWithMetadata{
+			{func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false},
+			{func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false},
+			{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false},
 		},
 	}
+	if sniffer, err := newFakeDNSSniffer(ctx); err == nil {
+		ret.sniffer = append(ret.sniffer, sniffer)
+	}
+	return ret
 }
 
 var errUnknownContent = newError("unknown content")
 
-func (s *Sniffer) Sniff(payload []byte) (SniffResult, error) {
-	var pendingSniffer []protocolSniffer
-	for _, s := range s.sniffer {
-		result, err := s(payload)
+func (s *Sniffer) Sniff(c context.Context, payload []byte) (SniffResult, error) {
+	var pendingSniffer []protocolSnifferWithMetadata
+	for _, si := range s.sniffer {
+		s := si.protocolSniffer
+		if si.metadataSniffer {
+			continue
+		}
+		result, err := s(c, payload)
 		if err == common.ErrNoClue {
-			pendingSniffer = append(pendingSniffer, s)
+			pendingSniffer = append(pendingSniffer, si)
 			continue
 		}
 
@@ -51,3 +69,55 @@ func (s *Sniffer) Sniff(payload []byte) (SniffResult, error) {
 
 	return nil, errUnknownContent
 }
+
+func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) {
+	var pendingSniffer []protocolSnifferWithMetadata
+	for _, si := range s.sniffer {
+		s := si.protocolSniffer
+		if !si.metadataSniffer {
+			pendingSniffer = append(pendingSniffer, si)
+			continue
+		}
+		result, err := s(c, nil)
+		if err == common.ErrNoClue {
+			pendingSniffer = append(pendingSniffer, si)
+			continue
+		}
+
+		if err == nil && result != nil {
+			return result, nil
+		}
+	}
+
+	if len(pendingSniffer) > 0 {
+		s.sniffer = pendingSniffer
+		return nil, common.ErrNoClue
+	}
+
+	return nil, errUnknownContent
+}
+
+func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult {
+	return &compositeResult{domainResult: domainResult, protocolResult: protocolResult}
+}
+
+type compositeResult struct {
+	domainResult   SniffResult
+	protocolResult SniffResult
+}
+
+func (c compositeResult) Protocol() string {
+	return c.protocolResult.Protocol()
+}
+
+func (c compositeResult) Domain() string {
+	return c.domainResult.Domain()
+}
+
+func (c compositeResult) ProtocolForDomainResult() string {
+	return c.domainResult.Protocol()
+}
+
+type SnifferResultComposite interface {
+	ProtocolForDomainResult() string
+}

+ 2 - 0
app/dns/config.proto

@@ -68,4 +68,6 @@ message Config {
 
   // Tag is the inbound tag of DNS client.
   string tag = 6;
+
+  reserved 7;
 }

+ 3 - 2
app/dns/dnscommon.go

@@ -2,6 +2,7 @@ package dns
 
 import (
 	"encoding/binary"
+	"strings"
 	"time"
 
 	"github.com/xtls/xray-core/common"
@@ -13,7 +14,7 @@ import (
 
 // Fqdn normalize domain make sure it ends with '.'
 func Fqdn(domain string) string {
-	if len(domain) > 0 && domain[len(domain)-1] == '.' {
+	if len(domain) > 0 && strings.HasSuffix(domain, ".") {
 		return domain
 	}
 	return domain + "."
@@ -112,7 +113,7 @@ func genEDNS0Options(clientIP net.IP) *dnsmessage.Resource {
 	return opt
 }
 
-func buildReqMsgs(domain string, option IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest {
+func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest {
 	qA := dnsmessage.Question{
 		Name:  dnsmessage.MustNewName(domain),
 		Type:  dnsmessage.TypeA,

+ 22 - 5
app/dns/dnscommon_test.go

@@ -9,6 +9,7 @@ import (
 	"github.com/miekg/dns"
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/net"
+	dns_feature "github.com/xtls/xray-core/features/dns"
 	"golang.org/x/net/dns/dnsmessage"
 )
 
@@ -92,7 +93,7 @@ func Test_buildReqMsgs(t *testing.T) {
 	}
 	type args struct {
 		domain  string
-		option  IPOption
+		option  dns_feature.IPOption
 		reqOpts *dnsmessage.Resource
 	}
 	tests := []struct {
@@ -100,10 +101,26 @@ func Test_buildReqMsgs(t *testing.T) {
 		args args
 		want int
 	}{
-		{"dual stack", args{"test.com", IPOption{true, true}, nil}, 2},
-		{"ipv4 only", args{"test.com", IPOption{true, false}, nil}, 1},
-		{"ipv6 only", args{"test.com", IPOption{false, true}, nil}, 1},
-		{"none/error", args{"test.com", IPOption{false, false}, nil}, 0},
+		{"dual stack", args{"test.com", dns_feature.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		}, nil}, 2},
+		{"ipv4 only", args{"test.com", dns_feature.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+			FakeEnable: false,
+		}, nil}, 1},
+		{"ipv6 only", args{"test.com", dns_feature.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: true,
+			FakeEnable: false,
+		}, nil}, 1},
+		{"none/error", args{"test.com", dns_feature.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: false,
+			FakeEnable: false,
+		}, nil}, 0},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

+ 3 - 3
app/dns/dohdns.go

@@ -234,7 +234,7 @@ func (s *DoHNameServer) newReqID() uint16 {
 	return uint16(atomic.AddUint32(&s.reqID, 1))
 }
 
-func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
+func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option dns_feature.IPOption) {
 	newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx))
 
 	if s.name+"." == "DOH//"+domain {
@@ -320,7 +320,7 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
 	return ioutil.ReadAll(resp.Body)
 }
 
-func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) {
+func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
 	s.RLock()
 	record, found := s.ips[domain]
 	s.RUnlock()
@@ -363,7 +363,7 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.
 }
 
 // QueryIP is called from dns.Server->queryIPTimeout
-func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
+func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, error) { // nolint: dupl
 	fqdn := Fqdn(domain)
 
 	ips, err := s.findIPsForDomain(fqdn, option)

+ 9 - 0
app/dns/fakedns/errors.generated.go

@@ -0,0 +1,9 @@
+package fakedns
+
+import "github.com/xtls/xray-core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 136 - 0
app/dns/fakedns/fake.go

@@ -0,0 +1,136 @@
+// +build !confonly
+
+package fakedns
+
+import (
+	"context"
+	"math"
+	"math/big"
+	gonet "net"
+	"time"
+
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/cache"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features/dns"
+)
+
+type Holder struct {
+	domainToIP cache.Lru
+	ipRange *gonet.IPNet
+
+	config *FakeDnsPool
+}
+
+func (*Holder) Type() interface{} {
+	return (*dns.FakeDNSEngine)(nil)
+}
+
+func (fkdns *Holder) Start() error {
+	return fkdns.initializeFromConfig()
+}
+
+func (fkdns *Holder) Close() error {
+	fkdns.domainToIP = nil
+	fkdns.ipRange = nil
+	return nil
+}
+
+func NewFakeDNSHolder() (*Holder, error) {
+	var fkdns *Holder
+	var err error
+
+	if fkdns, err = NewFakeDNSHolderConfigOnly(nil); err != nil {
+		return nil, newError("Unable to create Fake Dns Engine").Base(err).AtError()
+	}
+	err = fkdns.initialize("240.0.0.0/8", 65535)
+	if err != nil {
+		return nil, err
+	}
+	return fkdns, nil
+}
+
+func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) {
+	return &Holder{nil, nil, conf}, nil
+}
+
+func (fkdns *Holder) initializeFromConfig() error {
+	return fkdns.initialize(fkdns.config.IpPool, int(fkdns.config.LruSize))
+}
+
+func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
+	var ipRange *gonet.IPNet
+	var err error
+
+	if _, ipRange, err = gonet.ParseCIDR(ipPoolCidr); err != nil {
+		return newError("Unable to parse CIDR for Fake DNS IP assignment").Base(err).AtError()
+	}
+
+	ones, bits := ipRange.Mask.Size()
+	rooms := bits - ones
+	if math.Log2(float64(lruSize)) >= float64(rooms) {
+		return newError("LRU size is bigger than subnet size").AtError()
+	}
+	fkdns.domainToIP = cache.NewLru(lruSize)
+	fkdns.ipRange = ipRange
+	return nil
+}
+
+// GetFakeIPForDomain check and generate a fake IP for a domain name
+func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
+	if v, ok := fkdns.domainToIP.Get(domain); ok {
+		return []net.Address{v.(net.Address)}
+	}
+	var currentTimeMillis = uint64(time.Now().UnixNano() / 1e6)
+	ones, bits := fkdns.ipRange.Mask.Size()
+	rooms := bits - ones
+	if rooms < 64 {
+		currentTimeMillis %= (uint64(1) << rooms)
+	}
+	var bigIntIP = big.NewInt(0).SetBytes(fkdns.ipRange.IP)
+	bigIntIP = bigIntIP.Add(bigIntIP, new(big.Int).SetUint64(currentTimeMillis))
+	var ip net.Address
+	for {
+		ip = net.IPAddress(bigIntIP.Bytes())
+
+		// if we run for a long time, we may go back to beginning and start seeing the IP in use
+		if _, ok := fkdns.domainToIP.PeekKeyFromValue(ip); !ok {
+			break
+		}
+
+		bigIntIP = bigIntIP.Add(bigIntIP, big.NewInt(1))
+		if !fkdns.ipRange.Contains(bigIntIP.Bytes()) {
+			bigIntIP = big.NewInt(0).SetBytes(fkdns.ipRange.IP)
+		}
+	}
+	fkdns.domainToIP.Put(domain, ip)
+	return []net.Address{ip}
+}
+
+// GetDomainFromFakeDNS check if an IP is a fake IP and have corresponding domain name
+func (fkdns *Holder) GetDomainFromFakeDNS(ip net.Address) string {
+	if !ip.Family().IsIP() || !fkdns.ipRange.Contains(ip.IP()) {
+		return ""
+	}
+	if k, ok := fkdns.domainToIP.GetKeyFromValue(ip); ok {
+		return k.(string)
+	}
+	newError("A fake ip request to ", ip, ", however there is no matching domain name in fake DNS").AtInfo().WriteToLog()
+	return ""
+}
+
+// GetFakeIPRange return fake IP range from configuration
+func (fkdns *Holder) GetFakeIPRange() *gonet.IPNet {
+	return fkdns.ipRange
+}
+
+func init() {
+	common.Must(common.RegisterConfig((*FakeDnsPool)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+		var f *Holder
+		var err error
+		if f, err = NewFakeDNSHolderConfigOnly(config.(*FakeDnsPool)); err != nil {
+			return nil, err
+		}
+		return f, nil
+	}))
+}

+ 5 - 0
app/dns/fakedns/fakedns.go

@@ -0,0 +1,5 @@
+// +build !confonly
+
+package fakedns
+
+//go:generate go run github.com/xtls/xray-core/common/errors/errorgen

+ 164 - 0
app/dns/fakedns/fakedns.pb.go

@@ -0,0 +1,164 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.25.0
+// 	protoc        v3.13.0
+// source: app/dns/fakedns/fakedns.proto
+
+package fakedns
+
+import (
+	proto "github.com/golang/protobuf/proto"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
+type FakeDnsPool struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	IpPool  string `protobuf:"bytes,1,opt,name=ip_pool,json=ipPool,proto3" json:"ip_pool,omitempty"` //CIDR of IP pool used as fake DNS IP
+	LruSize int64  `protobuf:"varint,2,opt,name=lruSize,proto3" json:"lruSize,omitempty"`            //Size of Pool for remembering relationship between domain name and IP address
+}
+
+func (x *FakeDnsPool) Reset() {
+	*x = FakeDnsPool{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_dns_fakedns_fakedns_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FakeDnsPool) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FakeDnsPool) ProtoMessage() {}
+
+func (x *FakeDnsPool) ProtoReflect() protoreflect.Message {
+	mi := &file_app_dns_fakedns_fakedns_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FakeDnsPool.ProtoReflect.Descriptor instead.
+func (*FakeDnsPool) Descriptor() ([]byte, []int) {
+	return file_app_dns_fakedns_fakedns_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *FakeDnsPool) GetIpPool() string {
+	if x != nil {
+		return x.IpPool
+	}
+	return ""
+}
+
+func (x *FakeDnsPool) GetLruSize() int64 {
+	if x != nil {
+		return x.LruSize
+	}
+	return 0
+}
+
+var File_app_dns_fakedns_fakedns_proto protoreflect.FileDescriptor
+
+var file_app_dns_fakedns_fakedns_proto_rawDesc = []byte{
+	0x0a, 0x1d, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0x2f, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e,
+	0x73, 0x2f, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
+	0x1a, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e,
+	0x64, 0x6e, 0x73, 0x2e, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x22, 0x40, 0x0a, 0x0b, 0x46,
+	0x61, 0x6b, 0x65, 0x44, 0x6e, 0x73, 0x50, 0x6f, 0x6f, 0x6c, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x70,
+	0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x70, 0x50,
+	0x6f, 0x6f, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x72, 0x75, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6c, 0x72, 0x75, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x5f, 0x0a,
+	0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e,
+	0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x50,
+	0x01, 0x5a, 0x1e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x72,
+	0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0x2f, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e,
+	0x73, 0xaa, 0x02, 0x1a, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41,
+	0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x2e, 0x46, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_app_dns_fakedns_fakedns_proto_rawDescOnce sync.Once
+	file_app_dns_fakedns_fakedns_proto_rawDescData = file_app_dns_fakedns_fakedns_proto_rawDesc
+)
+
+func file_app_dns_fakedns_fakedns_proto_rawDescGZIP() []byte {
+	file_app_dns_fakedns_fakedns_proto_rawDescOnce.Do(func() {
+		file_app_dns_fakedns_fakedns_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_dns_fakedns_fakedns_proto_rawDescData)
+	})
+	return file_app_dns_fakedns_fakedns_proto_rawDescData
+}
+
+var file_app_dns_fakedns_fakedns_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_app_dns_fakedns_fakedns_proto_goTypes = []interface{}{
+	(*FakeDnsPool)(nil), // 0: xray.app.dns.fakedns.FakeDnsPool
+}
+var file_app_dns_fakedns_fakedns_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_app_dns_fakedns_fakedns_proto_init() }
+func file_app_dns_fakedns_fakedns_proto_init() {
+	if File_app_dns_fakedns_fakedns_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_app_dns_fakedns_fakedns_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FakeDnsPool); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_app_dns_fakedns_fakedns_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_app_dns_fakedns_fakedns_proto_goTypes,
+		DependencyIndexes: file_app_dns_fakedns_fakedns_proto_depIdxs,
+		MessageInfos:      file_app_dns_fakedns_fakedns_proto_msgTypes,
+	}.Build()
+	File_app_dns_fakedns_fakedns_proto = out.File
+	file_app_dns_fakedns_fakedns_proto_rawDesc = nil
+	file_app_dns_fakedns_fakedns_proto_goTypes = nil
+	file_app_dns_fakedns_fakedns_proto_depIdxs = nil
+}

+ 12 - 0
app/dns/fakedns/fakedns.proto

@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package xray.app.dns.fakedns;
+option csharp_namespace = "Xray.App.Dns.Fakedns";
+option go_package = "github.com/xtls/xray-core/app/dns/fakedns";
+option java_package = "com.xray.app.dns.fakedns";
+option java_multiple_files = true;
+
+message FakeDnsPool{
+  string ip_pool = 1; //CIDR of IP pool used as fake DNS IP
+  int64  lruSize = 2; //Size of Pool for remembering relationship between domain name and IP address
+}

+ 99 - 0
app/dns/fakedns/fakedns_test.go

@@ -0,0 +1,99 @@
+package fakedns
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/uuid"
+)
+
+func TestNewFakeDnsHolder(_ *testing.T) {
+	_, err := NewFakeDNSHolder()
+	common.Must(err)
+}
+
+func TestFakeDnsHolderCreateMapping(t *testing.T) {
+	fkdns, err := NewFakeDNSHolder()
+	common.Must(err)
+
+	addr := fkdns.GetFakeIPForDomain("fakednstest.v2fly.org")
+	assert.Equal(t, "240.", addr[0].IP().String()[0:4])
+}
+
+func TestFakeDnsHolderCreateMappingMany(t *testing.T) {
+	fkdns, err := NewFakeDNSHolder()
+	common.Must(err)
+
+	addr := fkdns.GetFakeIPForDomain("fakednstest.v2fly.org")
+	assert.Equal(t, "240.", addr[0].IP().String()[0:4])
+
+	addr2 := fkdns.GetFakeIPForDomain("fakednstest2.v2fly.org")
+	assert.Equal(t, "240.", addr2[0].IP().String()[0:4])
+	assert.NotEqual(t, addr[0].IP().String(), addr2[0].IP().String())
+}
+
+func TestFakeDnsHolderCreateMappingManyAndResolve(t *testing.T) {
+	fkdns, err := NewFakeDNSHolder()
+	common.Must(err)
+
+	addr := fkdns.GetFakeIPForDomain("fakednstest.v2fly.org")
+	addr2 := fkdns.GetFakeIPForDomain("fakednstest2.v2fly.org")
+
+	{
+		result := fkdns.GetDomainFromFakeDNS(addr[0])
+		assert.Equal(t, "fakednstest.v2fly.org", result)
+	}
+
+	{
+		result := fkdns.GetDomainFromFakeDNS(addr2[0])
+		assert.Equal(t, "fakednstest2.v2fly.org", result)
+	}
+}
+
+func TestFakeDnsHolderCreateMappingManySingleDomain(t *testing.T) {
+	fkdns, err := NewFakeDNSHolder()
+	common.Must(err)
+
+	addr := fkdns.GetFakeIPForDomain("fakednstest.v2fly.org")
+	addr2 := fkdns.GetFakeIPForDomain("fakednstest.v2fly.org")
+	assert.Equal(t, addr[0].IP().String(), addr2[0].IP().String())
+}
+
+func TestFakeDnsHolderCreateMappingAndRollOver(t *testing.T) {
+	fkdns, err := NewFakeDNSHolderConfigOnly(&FakeDnsPool{
+		IpPool:  "240.0.0.0/12",
+		LruSize: 256,
+	})
+	common.Must(err)
+
+	err = fkdns.Start()
+
+	common.Must(err)
+
+	addr := fkdns.GetFakeIPForDomain("fakednstest.v2fly.org")
+	addr2 := fkdns.GetFakeIPForDomain("fakednstest2.v2fly.org")
+
+	for i := 0; i <= 8192; i++ {
+		{
+			result := fkdns.GetDomainFromFakeDNS(addr[0])
+			assert.Equal(t, "fakednstest.v2fly.org", result)
+		}
+
+		{
+			result := fkdns.GetDomainFromFakeDNS(addr2[0])
+			assert.Equal(t, "fakednstest2.v2fly.org", result)
+		}
+
+		{
+			uuid := uuid.New()
+			domain := uuid.String() + ".fakednstest.v2fly.org"
+			tempAddr := fkdns.GetFakeIPForDomain(domain)
+			rsaddr := tempAddr[0].IP().String()
+
+			result := fkdns.GetDomainFromFakeDNS(net.ParseAddress(rsaddr))
+			assert.Equal(t, domain, result)
+		}
+	}
+}

+ 3 - 2
app/dns/hosts.go

@@ -5,6 +5,7 @@ import (
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/strmatcher"
 	"github.com/xtls/xray-core/features"
+	"github.com/xtls/xray-core/features/dns"
 )
 
 // StaticHosts represents static domain-ip mapping in DNS server.
@@ -92,7 +93,7 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
 	return sh, nil
 }
 
-func filterIP(ips []net.Address, option IPOption) []net.Address {
+func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
 	filtered := make([]net.Address, 0, len(ips))
 	for _, ip := range ips {
 		if (ip.Family().IsIPv4() && option.IPv4Enable) || (ip.Family().IsIPv6() && option.IPv6Enable) {
@@ -106,7 +107,7 @@ func filterIP(ips []net.Address, option IPOption) []net.Address {
 }
 
 // LookupIP returns IP address for the given domain, if exists in this StaticHosts.
-func (h *StaticHosts) LookupIP(domain string, option IPOption) []net.Address {
+func (h *StaticHosts) LookupIP(domain string, option dns.IPOption) []net.Address {
 	indices := h.matchers.Match(domain)
 	if len(indices) == 0 {
 		return nil

+ 4 - 3
app/dns/hosts_test.go

@@ -8,6 +8,7 @@ import (
 	. "github.com/xtls/xray-core/app/dns"
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features/dns"
 )
 
 func TestStaticHosts(t *testing.T) {
@@ -39,7 +40,7 @@ func TestStaticHosts(t *testing.T) {
 	common.Must(err)
 
 	{
-		ips := hosts.LookupIP("example.com", IPOption{
+		ips := hosts.LookupIP("example.com", dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -52,7 +53,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("www.example.cn", IPOption{
+		ips := hosts.LookupIP("www.example.cn", dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
 		})
@@ -65,7 +66,7 @@ func TestStaticHosts(t *testing.T) {
 	}
 
 	{
-		ips := hosts.LookupIP("baidu.com", IPOption{
+		ips := hosts.LookupIP("baidu.com", dns.IPOption{
 			IPv4Enable: false,
 			IPv6Enable: true,
 		})

+ 5 - 18
app/dns/nameserver.go

@@ -4,39 +4,26 @@ import (
 	"context"
 
 	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features/dns"
 	"github.com/xtls/xray-core/features/dns/localdns"
 )
 
-// IPOption is an object for IP query options.
-type IPOption struct {
-	IPv4Enable bool
-	IPv6Enable bool
-}
-
 // Client is the interface for DNS client.
 type Client interface {
 	// Name of the Client.
 	Name() string
 
 	// QueryIP sends IP queries to its configured server.
-	QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error)
+	QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, error)
 }
 
 type LocalNameServer struct {
 	client *localdns.Client
 }
 
-func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
-	if option.IPv4Enable && option.IPv6Enable {
-		return s.client.LookupIP(domain)
-	}
-
-	if option.IPv4Enable {
-		return s.client.LookupIPv4(domain)
-	}
-
-	if option.IPv6Enable {
-		return s.client.LookupIPv6(domain)
+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)
 	}
 
 	return nil, newError("neither IPv4 nor IPv6 is enabled")

+ 43 - 0
app/dns/nameserver_fakedns.go

@@ -0,0 +1,43 @@
+// +build !confonly
+
+package dns
+
+import (
+	"context"
+
+	"github.com/xtls/xray-core/core"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features/dns"
+)
+
+type FakeDNSServer struct {
+	fakeDNSEngine dns.FakeDNSEngine
+}
+
+func NewFakeDNSServer() *FakeDNSServer {
+	return &FakeDNSServer{}
+}
+
+func (FakeDNSServer) Name() string {
+	return "FakeDNS"
+}
+
+func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ dns.IPOption) ([]net.IP, error) {
+	if f.fakeDNSEngine == nil {
+		if err := core.RequireFeatures(ctx, func(fd dns.FakeDNSEngine) {
+			f.fakeDNSEngine = fd
+		}); err != nil {
+			return nil, newError("Unable to locate a fake DNS Engine").Base(err).AtError()
+		}
+	}
+	ips := f.fakeDNSEngine.GetFakeIPForDomain(domain)
+
+	netIP := toNetIP(ips)
+	if netIP == nil {
+		return nil, newError("Unable to convert IP to net ip").AtError()
+	}
+
+	newError(f.Name(), " got answer: ", domain, " -> ", ips).AtInfo().WriteToLog()
+
+	return netIP, nil
+}

+ 3 - 1
app/dns/nameserver_test.go

@@ -7,14 +7,16 @@ import (
 
 	. "github.com/xtls/xray-core/app/dns"
 	"github.com/xtls/xray-core/common"
+	dns_feature "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", IPOption{
+	ips, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
 		IPv4Enable: true,
 		IPv6Enable: true,
+		FakeEnable: false,
 	})
 	cancel()
 	common.Must(err)

+ 19 - 30
app/dns/server.go

@@ -31,6 +31,7 @@ type Server struct {
 	hosts         *StaticHosts
 	clientIP      net.IP
 	clients       []Client             // clientIdx -> Client
+	ctx           context.Context
 	ipIndexMap    []*MultiGeoIPMatcher // clientIdx -> *MultiGeoIPMatcher
 	domainRules   [][]string           // clientIdx -> domainRuleIdx -> DomainRule
 	domainMatcher strmatcher.IndexMatcher
@@ -75,6 +76,7 @@ func generateRandomTag() string {
 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 == "" {
@@ -144,6 +146,9 @@ func New(ctx context.Context, config *Config) (*Server, error) {
 				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()
@@ -295,8 +300,8 @@ func (s *Server) Match(idx int, client Client, domain string, ips []net.IP) ([]n
 	return newIps, nil
 }
 
-func (s *Server) queryIPTimeout(idx int, client Client, domain string, option IPOption) ([]net.IP, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
+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,
@@ -314,31 +319,7 @@ func (s *Server) queryIPTimeout(idx int, client Client, domain string, option IP
 	return ips, err
 }
 
-// LookupIP implements dns.Client.
-func (s *Server) LookupIP(domain string) ([]net.IP, error) {
-	return s.lookupIPInternal(domain, IPOption{
-		IPv4Enable: true,
-		IPv6Enable: true,
-	})
-}
-
-// LookupIPv4 implements dns.IPv4Lookup.
-func (s *Server) LookupIPv4(domain string) ([]net.IP, error) {
-	return s.lookupIPInternal(domain, IPOption{
-		IPv4Enable: true,
-		IPv6Enable: false,
-	})
-}
-
-// LookupIPv6 implements dns.IPv6Lookup.
-func (s *Server) LookupIPv6(domain string) ([]net.IP, error) {
-	return s.lookupIPInternal(domain, IPOption{
-		IPv4Enable: false,
-		IPv6Enable: true,
-	})
-}
-
-func (s *Server) lookupStatic(domain string, option IPOption, depth int32) []net.Address {
+func (s *Server) lookupStatic(domain string, option dns.IPOption, depth int32) []net.Address {
 	ips := s.hosts.LookupIP(domain, option)
 	if ips == nil {
 		return nil
@@ -362,14 +343,15 @@ func toNetIP(ips []net.Address) []net.IP {
 	return netips
 }
 
-func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) {
+// LookupIP implements dns.Client.
+func (s *Server) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
 	if domain == "" {
 		return nil, newError("empty domain name")
 	}
 	domain = strings.ToLower(domain)
 
 	// normalize the FQDN form query
-	if domain[len(domain)-1] == '.' {
+	if strings.HasSuffix(domain, ".") {
 		domain = domain[:len(domain)-1]
 	}
 
@@ -406,6 +388,10 @@ func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, err
 		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
@@ -425,7 +411,10 @@ func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, err
 			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

+ 110 - 26
app/dns/server_test.go

@@ -154,7 +154,11 @@ func TestUDPServerSubnet(t *testing.T) {
 
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
 
-	ips, err := client.LookupIP("google.com")
+	ips, err := client.LookupIP("google.com", feature_dns.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+		FakeEnable: false,
+	})
 	if err != nil {
 		t.Fatal("unexpected error: ", err)
 	}
@@ -209,7 +213,11 @@ func TestUDPServer(t *testing.T) {
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
 
 	{
-		ips, err := client.LookupIP("google.com")
+		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -220,7 +228,11 @@ func TestUDPServer(t *testing.T) {
 	}
 
 	{
-		ips, err := client.LookupIP("facebook.com")
+		ips, err := client.LookupIP("facebook.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -231,7 +243,11 @@ func TestUDPServer(t *testing.T) {
 	}
 
 	{
-		_, err := client.LookupIP("notexist.google.com")
+		_, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err == nil {
 			t.Fatal("nil error")
 		}
@@ -241,8 +257,11 @@ func TestUDPServer(t *testing.T) {
 	}
 
 	{
-		clientv6 := client.(feature_dns.IPv6Lookup)
-		ips, err := clientv6.LookupIPv6("ipv4only.google.com")
+		ips, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != feature_dns.ErrEmptyResponse {
 			t.Fatal("error: ", err)
 		}
@@ -254,7 +273,11 @@ func TestUDPServer(t *testing.T) {
 	dnsServer.Shutdown()
 
 	{
-		ips, err := client.LookupIP("google.com")
+		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -331,7 +354,11 @@ func TestPrioritizedDomain(t *testing.T) {
 	startTime := time.Now()
 
 	{
-		ips, err := client.LookupIP("google.com")
+		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -390,10 +417,12 @@ func TestUDPServerIPv6(t *testing.T) {
 	common.Must(err)
 
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
-	client6 := client.(feature_dns.IPv6Lookup)
-
 	{
-		ips, err := client6.LookupIPv6("ipv6.google.com")
+		ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -456,7 +485,11 @@ func TestStaticHostDomain(t *testing.T) {
 	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
 
 	{
-		ips, err := client.LookupIP("example.com")
+		ips, err := client.LookupIP("example.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -563,7 +596,11 @@ func TestIPMatch(t *testing.T) {
 	startTime := time.Now()
 
 	{
-		ips, err := client.LookupIP("google.com")
+		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -682,7 +719,11 @@ func TestLocalDomain(t *testing.T) {
 	startTime := time.Now()
 
 	{ // Will match dotless:
-		ips, err := client.LookupIP("hostname")
+		ips, err := client.LookupIP("hostname", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -693,7 +734,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match domain:local
-		ips, err := client.LookupIP("hostname.local")
+		ips, err := client.LookupIP("hostname.local", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -704,7 +749,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match static ip
-		ips, err := client.LookupIP("hostnamestatic")
+		ips, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -715,7 +764,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match domain replacing
-		ips, err := client.LookupIP("hostnamealias")
+		ips, err := client.LookupIP("hostnamealias", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -726,7 +779,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
-		ips, err := client.LookupIP("localhost")
+		ips, err := client.LookupIP("localhost", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -737,7 +794,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
-		ips, err := client.LookupIP("localhost-a")
+		ips, err := client.LookupIP("localhost-a", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -748,7 +809,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
-		ips, err := client.LookupIP("localhost-b")
+		ips, err := client.LookupIP("localhost-b", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -759,7 +824,11 @@ func TestLocalDomain(t *testing.T) {
 	}
 
 	{ // Will match dotless:
-		ips, err := client.LookupIP("Mijia Cloud")
+		ips, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -921,7 +990,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	startTime := time.Now()
 
 	{ // Will match server 1,2 and server 1 returns expected ip
-		ips, err := client.LookupIP("google.com")
+		ips, err := client.LookupIP("google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -932,8 +1005,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	}
 
 	{ // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one
-		clientv4 := client.(feature_dns.IPv4Lookup)
-		ips, err := clientv4.LookupIPv4("ipv6.google.com")
+		ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -944,7 +1020,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	}
 
 	{ // Will match server 3,1,2 and server 3 returns expected one
-		ips, err := client.LookupIP("api.google.com")
+		ips, err := client.LookupIP("api.google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}
@@ -955,7 +1035,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
 	}
 
 	{ // Will match server 4,3,1,2 and server 4 returns expected one
-		ips, err := client.LookupIP("v2.api.google.com")
+		ips, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err != nil {
 			t.Fatal("unexpected error: ", err)
 		}

+ 5 - 4
app/dns/udpns.go

@@ -54,7 +54,7 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
 		Execute:  s.Cleanup,
 	}
 	s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse)
-	newError("DNS: created udp client inited for ", address.NetAddr()).AtInfo().WriteToLog()
+	newError("DNS: created UDP client initialized for ", address.NetAddr()).AtInfo().WriteToLog()
 	return s
 }
 
@@ -180,7 +180,7 @@ func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) {
 	s.requests[id] = *req
 }
 
-func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
+func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, 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))
@@ -206,7 +206,7 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option
 	}
 }
 
-func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) {
+func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
 	s.RLock()
 	record, found := s.ips[domain]
 	s.RUnlock()
@@ -244,7 +244,8 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]
 	return nil, dns_feature.ErrEmptyResponse
 }
 
-func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
+// QueryIP implements Server.
+func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, error) {
 	fqdn := Fqdn(domain)
 
 	ips, err := s.findIPsForDomain(fqdn, option)

+ 94 - 82
app/proxyman/config.pb.go

@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.25.0
-// 	protoc        v3.14.0
+// 	protoc        (unknown)
 // source: app/proxyman/config.proto
 
 package proxyman
@@ -239,9 +239,12 @@ type SniffingConfig struct {
 	// Whether or not to enable content sniffing on an inbound connection.
 	Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
 	// Override target destination if sniff'ed protocol is in the given list.
-	// Supported values are "http", "tls".
+	// Supported values are "http", "tls", "fakedns".
 	DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"`
 	DomainsExcluded     []string `protobuf:"bytes,3,rep,name=domains_excluded,json=domainsExcluded,proto3" json:"domains_excluded,omitempty"`
+	// Whether should only try to sniff metadata without waiting for client input.
+	// Can be used to support SMTP like protocol where server send the first message.
+	MetadataOnly bool `protobuf:"varint,4,opt,name=metadata_only,json=metadataOnly,proto3" json:"metadata_only,omitempty"`
 }
 
 func (x *SniffingConfig) Reset() {
@@ -297,6 +300,13 @@ func (x *SniffingConfig) GetDomainsExcluded() []string {
 	return nil
 }
 
+func (x *SniffingConfig) GetMetadataOnly() bool {
+	if x != nil {
+		return x.MetadataOnly
+	}
+	return false
+}
+
 type ReceiverConfig struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -764,7 +774,7 @@ var file_app_proxyman_config_proto_rawDesc = []byte{
 	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2c, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
 	0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x52,
 	0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x78, 0x74, 0x65, 0x72,
-	0x6e, 0x61, 0x6c, 0x10, 0x02, 0x22, 0x88, 0x01, 0x0a, 0x0e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69,
+	0x6e, 0x61, 0x6c, 0x10, 0x02, 0x22, 0xad, 0x01, 0x0a, 0x0e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69,
 	0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
 	0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
 	0x65, 0x64, 0x12, 0x31, 0x0a, 0x14, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f,
@@ -773,86 +783,88 @@ var file_app_proxyman_config_proto_rawDesc = []byte{
 	0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73,
 	0x5f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52,
 	0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64,
-	0x22, 0x90, 0x04, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x61, 0x6e, 0x67,
-	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63,
-	0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61,
-	0x6e, 0x67, 0x65, 0x52, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x33,
-	0x0a, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 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, 0x06, 0x6c, 0x69, 0x73,
-	0x74, 0x65, 0x6e, 0x12, 0x56, 0x0a, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f,
-	0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78,
-	0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53,
-	0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74,
-	0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x4e, 0x0a, 0x0f, 0x73,
-	0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e,
-	0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x53,
-	0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x74, 0x72,
-	0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x40, 0x0a, 0x1c, 0x72,
-	0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f,
-	0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x1a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e,
-	0x61, 0x6c, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a,
-	0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65,
-	0x18, 0x07, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
-	0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4b, 0x6e, 0x6f, 0x77, 0x6e,
-	0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0e, 0x64,
-	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x4e, 0x0a,
-	0x11, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e,
-	0x67, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
-	0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x53, 0x6e, 0x69,
-	0x66, 0x66, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x73, 0x6e, 0x69,
-	0x66, 0x66, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x4a, 0x04, 0x08,
-	0x06, 0x10, 0x07, 0x22, 0xc0, 0x01, 0x0a, 0x14, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48,
-	0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03,
-	0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x4d,
-	0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69,
-	0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79,
-	0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54,
-	0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x10, 0x72, 0x65, 0x63,
-	0x65, 0x69, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x47, 0x0a,
+	0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6f, 0x6e, 0x6c,
+	0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+	0x61, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x90, 0x04, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76,
+	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x70, 0x6f, 0x72, 0x74,
+	0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78,
+	0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50,
+	0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61,
+	0x6e, 0x67, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 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, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x12, 0x56, 0x0a, 0x13, 0x61, 0x6c, 0x6c, 0x6f,
+	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
+	0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x12, 0x61, 0x6c,
+	0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79,
+	0x12, 0x4e, 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69,
+	0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79,
+	0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72,
+	0x6e, 0x65, 0x74, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x52, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73,
+	0x12, 0x40, 0x0a, 0x1c, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x72, 0x69, 0x67,
+	0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x4f,
+	0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69,
+	0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6f, 0x76, 0x65,
+	0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x78, 0x72,
+	0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e,
+	0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x42, 0x02,
+	0x18, 0x01, 0x52, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69,
+	0x64, 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x5f, 0x73,
+	0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e,
+	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61,
+	0x6e, 0x2e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x52, 0x10, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e,
+	0x67, 0x73, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0xc0, 0x01, 0x0a, 0x14, 0x49, 0x6e, 0x62,
+	0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+	0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
+	0x74, 0x61, 0x67, 0x12, 0x4d, 0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f,
+	0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20,
+	0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72,
+	0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+	0x52, 0x10, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e,
+	0x67, 0x73, 0x12, 0x47, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74,
+	0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61,
+	0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e,
+	0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0d, 0x70, 0x72,
+	0x6f, 0x78, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4f,
+	0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb0, 0x02,
+	0x0a, 0x0c, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d,
+	0x0a, 0x03, 0x76, 0x69, 0x61, 0x18, 0x01, 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, 0x03, 0x76, 0x69, 0x61, 0x12, 0x4e, 0x0a,
+	0x0f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72,
+	0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
+	0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73,
+	0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4b, 0x0a,
 	0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18,
-	0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d,
-	0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64,
-	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65,
-	0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75,
-	0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb0, 0x02, 0x0a, 0x0c, 0x53, 0x65, 0x6e,
-	0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x0a, 0x03, 0x76, 0x69, 0x61,
-	0x18, 0x01, 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, 0x03, 0x76, 0x69, 0x61, 0x12, 0x4e, 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x65,
-	0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f,
-	0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x53, 0x74, 0x72, 0x65,
-	0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
-	0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4b, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78,
-	0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72,
-	0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79,
-	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x74,
-	0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x54, 0x0a, 0x12, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c,
-	0x65, 0x78, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f,
-	0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69,
-	0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70,
-	0x6c, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x50, 0x0a, 0x12, 0x4d,
-	0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69,
-	0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x63,
-	0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
-	0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2a, 0x23, 0x0a,
-	0x0e, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x12,
-	0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x4c, 0x53,
-	0x10, 0x01, 0x42, 0x55, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
-	0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x50, 0x01, 0x5a, 0x26, 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, 0x70, 0x72, 0x6f,
-	0x78, 0x79, 0x6d, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70,
-	0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x33,
+	0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61,
+	0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e,
+	0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x70, 0x72, 0x6f,
+	0x78, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x54, 0x0a, 0x12, 0x6d, 0x75,
+	0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
+	0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69,
+	0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x6d,
+	0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73,
+	0x22, 0x50, 0x0a, 0x12, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67,
+	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
+	0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
+	0x63, 0x79, 0x2a, 0x23, 0x0a, 0x0e, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f,
+	0x63, 0x6f, 0x6c, 0x73, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x07,
+	0x0a, 0x03, 0x54, 0x4c, 0x53, 0x10, 0x01, 0x42, 0x55, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78,
+	0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e,
+	0x50, 0x01, 0x5a, 0x26, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61,
+	0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (

+ 5 - 1
app/proxyman/config.proto

@@ -54,9 +54,13 @@ message SniffingConfig {
   bool enabled = 1;
 
   // Override target destination if sniff'ed protocol is in the given list.
-  // Supported values are "http", "tls".
+  // Supported values are "http", "tls", "fakedns".
   repeated string destination_override = 2;
   repeated string domains_excluded = 3;
+  
+  // Whether should only try to sniff metadata without waiting for client input.
+  // Can be used to support SMTP like protocol where server send the first message.
+  bool metadata_only = 4;
 }
 
 message ReceiverConfig {

+ 1 - 0
app/proxyman/inbound/always.go

@@ -133,6 +133,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
 					address:         address,
 					port:            net.Port(port),
 					dispatcher:      h.mux,
+					sniffingConfig:  receiverConfig.GetEffectiveSniffingSettings(),
 					uplinkCounter:   uplinkCounter,
 					downlinkCounter: downlinkCounter,
 					stream:          mss,

+ 1 - 0
app/proxyman/inbound/dynamic.go

@@ -153,6 +153,7 @@ func (h *DynamicInboundHandler) refresh() error {
 				address:         address,
 				port:            port,
 				dispatcher:      h.mux,
+				sniffingConfig:  h.receiverConfig.GetEffectiveSniffingSettings(),
 				uplinkCounter:   uplinkCounter,
 				downlinkCounter: downlinkCounter,
 				stream:          h.streamSettings,

+ 11 - 1
app/proxyman/inbound/worker.go

@@ -98,6 +98,7 @@ func (w *tcpWorker) callback(conn internet.Connection) {
 		content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
 		content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
 		content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
+		content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
 	}
 	ctx = session.ContextWithContent(ctx, content)
 
@@ -235,6 +236,7 @@ type udpWorker struct {
 	tag             string
 	stream          *internet.MemoryStreamConfig
 	dispatcher      routing.Dispatcher
+	sniffingConfig  *proxyman.SniffingConfig
 	uplinkCounter   stats.Counter
 	downlinkCounter stats.Counter
 
@@ -297,7 +299,7 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
 		common.Must(w.checker.Start())
 
 		go func() {
-			ctx := context.Background()
+			ctx := w.ctx
 			sid := session.NewID()
 			ctx = session.ContextWithID(ctx, sid)
 
@@ -311,6 +313,13 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
 				Gateway: net.UDPDestination(w.address, w.port),
 				Tag:     w.tag,
 			})
+			content := new(session.Content)
+			if w.sniffingConfig != nil {
+				content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
+				content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
+				content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
+			}
+			ctx = session.ContextWithContent(ctx, content)
 			if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil {
 				newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
 			}
@@ -451,6 +460,7 @@ func (w *dsWorker) callback(conn internet.Connection) {
 		content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
 		content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
 		content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
+		content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
 	}
 	ctx = session.ContextWithContent(ctx, content)
 

+ 11 - 2
app/router/router_test.go

@@ -9,6 +9,7 @@ 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"
@@ -115,7 +116,11 @@ func TestIPOnDemand(t *testing.T) {
 	defer mockCtl.Finish()
 
 	mockDNS := mocks.NewDNSClient(mockCtl)
-	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
+	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com"), dns.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+		FakeEnable: false,
+	}).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
 
 	r := new(Router)
 	common.Must(r.Init(config, mockDNS, nil))
@@ -150,7 +155,11 @@ func TestIPIfNonMatchDomain(t *testing.T) {
 	defer mockCtl.Finish()
 
 	mockDNS := mocks.NewDNSClient(mockCtl)
-	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
+	mockDNS.EXPECT().LookupIP(gomock.Eq("example.com"), dns.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+		FakeEnable: false,
+	}).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
 
 	r := new(Router)
 	common.Must(r.Init(config, mockDNS, nil))

+ 85 - 0
common/cache/lru.go

@@ -0,0 +1,85 @@
+package cache
+
+import (
+	"container/list"
+	sync "sync"
+)
+
+// Lru simple, fast lru cache implementation
+type Lru interface {
+	Get(key interface{}) (value interface{}, ok bool)
+	GetKeyFromValue(value interface{}) (key interface{}, ok bool)
+	PeekKeyFromValue(value interface{}) (key interface{}, ok bool) // Peek means check but NOT bring to top
+	Put(key, value interface{})
+}
+
+type lru struct {
+	capacity         int
+	doubleLinkedlist *list.List
+	keyToElement     *sync.Map
+	valueToElement   *sync.Map
+	mu               *sync.Mutex
+}
+
+type lruElement struct {
+	key   interface{}
+	value interface{}
+}
+
+// NewLru init a lru cache
+func NewLru(cap int) Lru {
+	return lru{
+		capacity:         cap,
+		doubleLinkedlist: list.New(),
+		keyToElement:     new(sync.Map),
+		valueToElement:   new(sync.Map),
+		mu:               new(sync.Mutex),
+	}
+}
+
+func (l lru) Get(key interface{}) (value interface{}, ok bool) {
+	if v, ok := l.keyToElement.Load(key); ok {
+		element := v.(*list.Element)
+		l.doubleLinkedlist.MoveBefore(element, l.doubleLinkedlist.Front())
+		return element.Value.(lruElement).value, true
+	}
+	return nil, false
+}
+
+func (l lru) GetKeyFromValue(value interface{}) (key interface{}, ok bool) {
+	if k, ok := l.valueToElement.Load(value); ok {
+		element := k.(*list.Element)
+		l.doubleLinkedlist.MoveBefore(element, l.doubleLinkedlist.Front())
+		return element.Value.(lruElement).key, true
+	}
+	return nil, false
+}
+
+func (l lru) PeekKeyFromValue(value interface{}) (key interface{}, ok bool) {
+	if k, ok := l.valueToElement.Load(value); ok {
+		element := k.(*list.Element)
+		return element.Value.(lruElement).key, true
+	}
+	return nil, false
+}
+
+func (l lru) Put(key, value interface{}) {
+	e := lruElement{key, value}
+	if v, ok := l.keyToElement.Load(key); ok {
+		element := v.(*list.Element)
+		element.Value = e
+		l.doubleLinkedlist.MoveBefore(element, l.doubleLinkedlist.Front())
+	} else {
+		l.mu.Lock()
+		element := l.doubleLinkedlist.PushFront(e)
+		l.keyToElement.Store(key, element)
+		l.valueToElement.Store(value, element)
+		if l.doubleLinkedlist.Len() > l.capacity {
+			toBeRemove := l.doubleLinkedlist.Back()
+			l.doubleLinkedlist.Remove(toBeRemove)
+			l.keyToElement.Delete(toBeRemove.Value.(lruElement).key)
+			l.valueToElement.Delete(toBeRemove.Value.(lruElement).value)
+		}
+		l.mu.Unlock()
+	}
+}

+ 86 - 0
common/cache/lru_test.go

@@ -0,0 +1,86 @@
+package cache_test
+
+import (
+	"testing"
+
+	. "github.com/xtls/xray-core/common/cache"
+)
+
+func TestLruReplaceValue(t *testing.T) {
+	lru := NewLru(2)
+	lru.Put(2, 6)
+	lru.Put(1, 5)
+	lru.Put(1, 2)
+	v, _ := lru.Get(1)
+	if v != 2 {
+		t.Error("should get 2", v)
+	}
+	v, _ = lru.Get(2)
+	if v != 6 {
+		t.Error("should get 6", v)
+	}
+}
+
+func TestLruRemoveOld(t *testing.T) {
+	lru := NewLru(2)
+	v, ok := lru.Get(2)
+	if ok {
+		t.Error("should get nil", v)
+	}
+	lru.Put(1, 1)
+	lru.Put(2, 2)
+	v, _ = lru.Get(1)
+	if v != 1 {
+		t.Error("should get 1", v)
+	}
+	lru.Put(3, 3)
+	v, ok = lru.Get(2)
+	if ok {
+		t.Error("should get nil", v)
+	}
+	lru.Put(4, 4)
+	v, ok = lru.Get(1)
+	if ok {
+		t.Error("should get nil", v)
+	}
+	v, _ = lru.Get(3)
+	if v != 3 {
+		t.Error("should get 3", v)
+	}
+	v, _ = lru.Get(4)
+	if v != 4 {
+		t.Error("should get 4", v)
+	}
+}
+
+func TestGetKeyFromValue(t *testing.T) {
+	lru := NewLru(2)
+	lru.Put(3, 3)
+	lru.Put(2, 2)
+	lru.GetKeyFromValue(3)
+	lru.Put(1, 1)
+	v, ok := lru.GetKeyFromValue(2)
+	if ok {
+		t.Error("should get nil", v)
+	}
+	v, _ = lru.GetKeyFromValue(3)
+	if v != 3 {
+		t.Error("should get 3", v)
+	}
+}
+
+func TestPeekKeyFromValue(t *testing.T) {
+	lru := NewLru(2)
+	lru.Put(3, 3)
+	lru.Put(2, 2)
+	lru.PeekKeyFromValue(3)
+	lru.Put(1, 1)
+	v, ok := lru.PeekKeyFromValue(3)
+	if ok {
+		t.Error("should get nil", v)
+	}
+	v, _ = lru.PeekKeyFromValue(2)
+	if v != 2 {
+		t.Error("should get 2", v)
+	}
+}

+ 1 - 0
common/session/session.go

@@ -63,6 +63,7 @@ type SniffingRequest struct {
 	ExcludeForDomain               []string
 	OverrideDestinationForProtocol []string
 	Enabled                        bool
+	MetadataOnly                   bool
 }
 
 // Content is the metadata of the connection content.

+ 8 - 15
features/dns/client.go

@@ -7,6 +7,13 @@ import (
 	"github.com/xtls/xray-core/features"
 )
 
+// IPOption is an object for IP query options.
+type IPOption struct {
+	IPv4Enable bool
+	IPv6Enable bool
+	FakeEnable bool
+}
+
 // Client is a Xray feature for querying DNS information.
 //
 // xray:api:stable
@@ -14,21 +21,7 @@ type Client interface {
 	features.Feature
 
 	// LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses.
-	LookupIP(domain string) ([]net.IP, error)
-}
-
-// IPv4Lookup is an optional feature for querying IPv4 addresses only.
-//
-// xray:api:beta
-type IPv4Lookup interface {
-	LookupIPv4(domain string) ([]net.IP, error)
-}
-
-// IPv6Lookup is an optional feature for querying IPv6 addresses only.
-//
-// xray:api:beta
-type IPv6Lookup interface {
-	LookupIPv6(domain string) ([]net.IP, error)
+	LookupIP(domain string, option IPOption) ([]net.IP, error)
 }
 
 // ClientType returns the type of Client interface. Can be used for implementing common.HasType.

+ 15 - 0
features/dns/fakedns.go

@@ -0,0 +1,15 @@
+package dns
+
+import (
+	gonet "net"
+
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/features"
+)
+
+type FakeDNSEngine interface {
+	features.Feature
+	GetFakeIPForDomain(domain string) []net.Address
+	GetDomainFromFakeDNS(ip net.Address) string
+	GetFakeIPRange() *gonet.IPNet
+}

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

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

+ 9 - 0
features/dns/localdns/errors.generated.go

@@ -0,0 +1,9 @@
+package localdns
+
+import "github.com/xtls/xray-core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 5 - 1
features/routing/dns/context.go

@@ -26,7 +26,11 @@ func (ctx *ResolvableContext) GetTargetIPs() []net.IP {
 	}
 
 	if domain := ctx.GetTargetDomain(); len(domain) != 0 {
-		ips, err := ctx.dnsClient.LookupIP(domain)
+		ips, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: true,
+			FakeEnable: false,
+		})
 		if err == nil {
 			ctx.resolvedIPs = ips
 			return ips

+ 65 - 0
infra/conf/fakedns.go

@@ -0,0 +1,65 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"github.com/xtls/xray-core/app/dns/fakedns"
+)
+
+type FakeDNSConfig struct {
+	IPPool  string `json:"ipPool"`
+	LruSize int64  `json:"poolSize"`
+}
+
+func (f FakeDNSConfig) Build() (proto.Message, error) {
+	return &fakedns.FakeDnsPool{
+		IpPool:  f.IPPool,
+		LruSize: f.LruSize,
+	}, nil
+}
+
+type FakeDNSPostProcessingStage struct{}
+
+func (FakeDNSPostProcessingStage) Process(conf *Config) error {
+	var fakeDNSInUse bool
+
+	if conf.DNSConfig != nil {
+		for _, v := range conf.DNSConfig.Servers {
+			if v.Address.Family().IsDomain() {
+				if v.Address.Domain() == "fakedns" {
+					fakeDNSInUse = true
+				}
+			}
+		}
+	}
+
+	if fakeDNSInUse {
+		if conf.FakeDNS == nil {
+			// Add a Fake DNS Config if there is none
+			conf.FakeDNS = &FakeDNSConfig{
+				IPPool:  "240.0.0.0/8",
+				LruSize: 65535,
+			}
+		}
+		found := false
+		// Check if there is a Outbound with necessary sniffer on
+		var inbounds []InboundDetourConfig
+
+		if len(conf.InboundConfigs) > 0 {
+			inbounds = append(inbounds, conf.InboundConfigs...)
+		}
+		for _, v := range inbounds {
+			if v.SniffingConfig != nil && v.SniffingConfig.Enabled && v.SniffingConfig.DestOverride != nil {
+				for _, dov := range *v.SniffingConfig.DestOverride {
+					if dov == "fakedns" {
+						found = true
+					}
+				}
+			}
+		}
+		if !found {
+			newError("Defined Fake DNS but haven't enabled fake dns sniffing at any inbound.").AtWarning().WriteToLog()
+		}
+	}
+
+	return nil
+}

+ 5 - 0
infra/conf/init.go

@@ -0,0 +1,5 @@
+package conf
+
+func init() {
+	RegisterConfigureFilePostProcessingStage("FakeDNS", &FakeDNSPostProcessingStage{})
+}

+ 23 - 0
infra/conf/lint.go

@@ -0,0 +1,23 @@
+package conf
+
+type ConfigureFilePostProcessingStage interface {
+	Process(conf *Config) error
+}
+
+var configureFilePostProcessingStages map[string]ConfigureFilePostProcessingStage
+
+func RegisterConfigureFilePostProcessingStage(name string, stage ConfigureFilePostProcessingStage) {
+	if configureFilePostProcessingStages == nil {
+		configureFilePostProcessingStages = make(map[string]ConfigureFilePostProcessingStage)
+	}
+	configureFilePostProcessingStages[name] = stage
+}
+
+func PostProcessConfigureFile(conf *Config) error {
+	for k, v := range configureFilePostProcessingStages {
+		if err := v.Process(conf); err != nil {
+			return newError("Rejected by Postprocessing Stage ", k).AtError().Base(err)
+		}
+	}
+	return nil
+}

+ 21 - 0
infra/conf/xray.go

@@ -62,6 +62,7 @@ type SniffingConfig struct {
 	Enabled         bool        `json:"enabled"`
 	DestOverride    *StringList `json:"destOverride"`
 	DomainsExcluded *StringList `json:"domainsExcluded"`
+	MetadataOnly    bool        `json:"metadataOnly"`
 }
 
 // Build implements Buildable.
@@ -74,6 +75,8 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) {
 				p = append(p, "http")
 			case "tls", "https", "ssl":
 				p = append(p, "tls")
+			case "fakedns":
+				p = append(p, "fakedns")
 			default:
 				return nil, newError("unknown protocol: ", protocol)
 			}
@@ -91,6 +94,7 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) {
 		Enabled:             c.Enabled,
 		DestinationOverride: p,
 		DomainsExcluded:     d,
+		MetadataOnly:        c.MetadataOnly,
 	}, nil
 }
 
@@ -395,6 +399,7 @@ type Config struct {
 	API             *APIConfig             `json:"api"`
 	Stats           *StatsConfig           `json:"stats"`
 	Reverse         *ReverseConfig         `json:"reverse"`
+	FakeDNS         *FakeDNSConfig         `json:"fakeDns"`
 }
 
 func (c *Config) findInboundTag(tag string) int {
@@ -448,6 +453,10 @@ func (c *Config) Override(o *Config, fn string) {
 		c.Reverse = o.Reverse
 	}
 
+	if o.FakeDNS != nil {
+		c.FakeDNS = o.FakeDNS
+	}
+
 	// deprecated attrs... keep them for now
 	if o.InboundConfig != nil {
 		c.InboundConfig = o.InboundConfig
@@ -519,6 +528,10 @@ func applyTransportConfig(s *StreamConfig, t *TransportConfig) {
 
 // Build implements Buildable.
 func (c *Config) Build() (*core.Config, error) {
+	if err := PostProcessConfigureFile(c); err != nil {
+		return nil, err
+	}
+
 	config := &core.Config{
 		App: []*serial.TypedMessage{
 			serial.ToTypedMessage(&dispatcher.Config{}),
@@ -585,6 +598,14 @@ func (c *Config) Build() (*core.Config, error) {
 		config.App = append(config.App, serial.ToTypedMessage(r))
 	}
 
+	if c.FakeDNS != nil {
+		r, err := c.FakeDNS.Build()
+		if err != nil {
+			return nil, err
+		}
+		config.App = append(config.App, serial.ToTypedMessage(r))
+	}
+
 	var inbounds []InboundDetourConfig
 
 	if c.InboundConfig != nil {

+ 1 - 0
main/distro/all/all.go

@@ -16,6 +16,7 @@ import (
 
 	// Other optional features.
 	_ "github.com/xtls/xray-core/app/dns"
+	_ "github.com/xtls/xray-core/app/dns/fakedns"
 	_ "github.com/xtls/xray-core/app/log"
 	_ "github.com/xtls/xray-core/app/policy"
 	_ "github.com/xtls/xray-core/app/reverse"

+ 15 - 17
proxy/dns/dns.go

@@ -36,25 +36,13 @@ type ownLinkVerifier interface {
 }
 
 type Handler struct {
-	ipv4Lookup      dns.IPv4Lookup
-	ipv6Lookup      dns.IPv6Lookup
+	client          dns.Client
 	ownLinkVerifier ownLinkVerifier
 	server          net.Destination
 }
 
 func (h *Handler) Init(config *Config, dnsClient dns.Client) error {
-	ipv4lookup, ok := dnsClient.(dns.IPv4Lookup)
-	if !ok {
-		return newError("dns.Client doesn't implement IPv4Lookup")
-	}
-	h.ipv4Lookup = ipv4lookup
-
-	ipv6lookup, ok := dnsClient.(dns.IPv6Lookup)
-	if !ok {
-		return newError("dns.Client doesn't implement IPv6Lookup")
-	}
-	h.ipv6Lookup = ipv6lookup
-
+	h.client = dnsClient
 	if v, ok := dnsClient.(ownLinkVerifier); ok {
 		h.ownLinkVerifier = v
 	}
@@ -209,11 +197,21 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
 	var ips []net.IP
 	var err error
 
+	var ttl uint32 = 600
+
 	switch qType {
 	case dnsmessage.TypeA:
-		ips, err = h.ipv4Lookup.LookupIPv4(domain)
+		ips, err = h.client.LookupIP(domain, dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+			FakeEnable: true,
+		})
 	case dnsmessage.TypeAAAA:
-		ips, err = h.ipv6Lookup.LookupIPv6(domain)
+		ips, err = h.client.LookupIP(domain, dns.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: true,
+			FakeEnable: true,
+		})
 	}
 
 	rcode := dns.RCodeFromError(err)
@@ -241,7 +239,7 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
 	}))
 	common.Must(builder.StartAnswers())
 
-	rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: 600}
+	rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl}
 	for _, ip := range ips {
 		if len(ip) == net.IPv4len {
 			var r dnsmessage.AResource

+ 15 - 8
proxy/freedom/freedom.go

@@ -59,19 +59,26 @@ func (h *Handler) policy() policy.Session {
 }
 
 func (h *Handler) resolveIP(ctx context.Context, domain string, localAddr net.Address) net.Address {
-	var lookupFunc func(string) ([]net.IP, error) = h.dns.LookupIP
-
+	var option dns.IPOption = dns.IPOption{
+		IPv4Enable: true,
+		IPv6Enable: true,
+		FakeEnable: false,
+	}
 	if h.config.DomainStrategy == Config_USE_IP4 || (localAddr != nil && localAddr.Family().IsIPv4()) {
-		if lookupIPv4, ok := h.dns.(dns.IPv4Lookup); ok {
-			lookupFunc = lookupIPv4.LookupIPv4
+		option = dns.IPOption{
+			IPv4Enable: true,
+			IPv6Enable: false,
+			FakeEnable: false,
 		}
 	} else if h.config.DomainStrategy == Config_USE_IP6 || (localAddr != nil && localAddr.Family().IsIPv6()) {
-		if lookupIPv6, ok := h.dns.(dns.IPv6Lookup); ok {
-			lookupFunc = lookupIPv6.LookupIPv6
+		option = dns.IPOption{
+			IPv4Enable: false,
+			IPv6Enable: true,
+			FakeEnable: false,
 		}
 	}
 
-	ips, err := lookupFunc(domain)
+	ips, err := h.dns.LookupIP(domain, option)
 	if err != nil {
 		newError("failed to get IP address for domain ", domain).Base(err).WriteToLog(session.ExportIDToError(ctx))
 	}
@@ -125,7 +132,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
 					Address: ip,
 					Port:    dialDest.Port,
 				}
-				newError("dialing to to ", dialDest).WriteToLog(session.ExportIDToError(ctx))
+				newError("dialing to ", dialDest).WriteToLog(session.ExportIDToError(ctx))
 			}
 		}
 

+ 5 - 4
testing/mocks/dns.go

@@ -6,6 +6,7 @@ package mocks
 
 import (
 	gomock "github.com/golang/mock/gomock"
+	dns "github.com/xtls/xray-core/features/dns"
 	net "net"
 	reflect "reflect"
 )
@@ -48,18 +49,18 @@ func (mr *DNSClientMockRecorder) Close() *gomock.Call {
 }
 
 // LookupIP mocks base method
-func (m *DNSClient) LookupIP(arg0 string) ([]net.IP, error) {
+func (m *DNSClient) LookupIP(arg0 string, arg1 dns.IPOption) ([]net.IP, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "LookupIP", arg0)
+	ret := m.ctrl.Call(m, "LookupIP", arg0, arg1)
 	ret0, _ := ret[0].([]net.IP)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // LookupIP indicates an expected call of LookupIP
-func (mr *DNSClientMockRecorder) LookupIP(arg0 interface{}) *gomock.Call {
+func (mr *DNSClientMockRecorder) LookupIP(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0, arg1)
 }
 
 // Start mocks base method