From 6a211a0bb92a2dac8cbd85673f080cb98dfd40ee Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Thu, 20 Mar 2025 13:09:02 +0100
Subject: [PATCH] DNS: Add `allowUnexpectedIPs` for DnsServerObject (#4497)

Closes https://github.com/XTLS/Xray-core/issues/4424
---
 app/dns/config.pb.go  | 29 +++++++++++++++++++---------
 app/dns/config.proto  |  1 +
 app/dns/nameserver.go | 15 ++++++++++-----
 infra/conf/dns.go     | 44 +++++++++++++++++++++++--------------------
 4 files changed, 55 insertions(+), 34 deletions(-)

diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go
index 05fd6099..33dc904b 100644
--- a/app/dns/config.pb.go
+++ b/app/dns/config.pb.go
@@ -128,13 +128,14 @@ type NameServer struct {
 	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"`
-	QueryStrategy     QueryStrategy                `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
+	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"`
+	QueryStrategy      QueryStrategy                `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
+	AllowUnexpectedIPs bool                         `protobuf:"varint,8,opt,name=allowUnexpectedIPs,proto3" json:"allowUnexpectedIPs,omitempty"`
 }
 
 func (x *NameServer) Reset() {
@@ -216,6 +217,13 @@ func (x *NameServer) GetQueryStrategy() QueryStrategy {
 	return QueryStrategy_USE_IP
 }
 
+func (x *NameServer) GetAllowUnexpectedIPs() bool {
+	if x != nil {
+		return x.AllowUnexpectedIPs
+	}
+	return false
+}
+
 type Config struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -508,7 +516,7 @@ var file_app_dns_config_proto_rawDesc = []byte{
 	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, 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, 0xb2, 0x04, 0x0a, 0x0a,
+	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe2, 0x04, 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,
@@ -534,7 +542,10 @@ var file_app_dns_config_proto_rawDesc = []byte{
 	0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x07, 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, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72, 0x69, 0x6f,
+	0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x2e, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f,
+	0x77, 0x55, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x08,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x6e, 0x65, 0x78, 0x70,
+	0x65, 0x63, 0x74, 0x65, 0x64, 0x49, 0x50, 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,
diff --git a/app/dns/config.proto b/app/dns/config.proto
index 11e0c8de..ea4d5e31 100644
--- a/app/dns/config.proto
+++ b/app/dns/config.proto
@@ -28,6 +28,7 @@ message NameServer {
   repeated xray.app.router.GeoIP geoip = 3;
   repeated OriginalRule original_rules = 4;
   QueryStrategy query_strategy = 7;
+  bool allowUnexpectedIPs = 8;
 }
 
 enum DomainMatchingType {
diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go
index b23cb186..e1f902bf 100644
--- a/app/dns/nameserver.go
+++ b/app/dns/nameserver.go
@@ -25,11 +25,12 @@ type Server interface {
 
 // Client is the interface for DNS client.
 type Client struct {
-	server       Server
-	clientIP     net.IP
-	skipFallback bool
-	domains      []string
-	expectIPs    []*router.GeoIPMatcher
+	server             Server
+	clientIP           net.IP
+	skipFallback       bool
+	domains            []string
+	expectIPs          []*router.GeoIPMatcher
+	allowUnexpectedIPs bool
 }
 
 var errExpectedIPNonMatch = errors.New("expectIPs not match")
@@ -166,6 +167,7 @@ func NewClient(
 		client.skipFallback = ns.SkipFallback
 		client.domains = rules
 		client.expectIPs = matchers
+		client.allowUnexpectedIPs = ns.AllowUnexpectedIPs
 		return nil
 	})
 	return client, err
@@ -203,6 +205,9 @@ func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error)
 		}
 	}
 	if len(newIps) == 0 {
+		if c.allowUnexpectedIPs {
+			return ips, nil
+		}
 		return nil, errExpectedIPNonMatch
 	}
 	errors.LogDebug(context.Background(), "domain ", domain, " expectIPs ", newIps, " matched at server ", c.Name())
diff --git a/infra/conf/dns.go b/infra/conf/dns.go
index 65964f3d..b2289026 100644
--- a/infra/conf/dns.go
+++ b/infra/conf/dns.go
@@ -12,13 +12,14 @@ import (
 )
 
 type NameServerConfig struct {
-	Address       *Address   `json:"address"`
-	ClientIP      *Address   `json:"clientIp"`
-	Port          uint16     `json:"port"`
-	SkipFallback  bool       `json:"skipFallback"`
-	Domains       []string   `json:"domains"`
-	ExpectIPs     StringList `json:"expectIps"`
-	QueryStrategy string     `json:"queryStrategy"`
+	Address            *Address   `json:"address"`
+	ClientIP           *Address   `json:"clientIp"`
+	Port               uint16     `json:"port"`
+	SkipFallback       bool       `json:"skipFallback"`
+	Domains            []string   `json:"domains"`
+	ExpectIPs          StringList `json:"expectIps"`
+	QueryStrategy      string     `json:"queryStrategy"`
+	AllowUnexpectedIPs bool       `json:"allowUnexpectedIps"`
 }
 
 func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
@@ -29,13 +30,14 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 	}
 
 	var advanced struct {
-		Address       *Address   `json:"address"`
-		ClientIP      *Address   `json:"clientIp"`
-		Port          uint16     `json:"port"`
-		SkipFallback  bool       `json:"skipFallback"`
-		Domains       []string   `json:"domains"`
-		ExpectIPs     StringList `json:"expectIps"`
-		QueryStrategy string     `json:"queryStrategy"`
+		Address            *Address   `json:"address"`
+		ClientIP           *Address   `json:"clientIp"`
+		Port               uint16     `json:"port"`
+		SkipFallback       bool       `json:"skipFallback"`
+		Domains            []string   `json:"domains"`
+		ExpectIPs          StringList `json:"expectIps"`
+		QueryStrategy      string     `json:"queryStrategy"`
+		AllowUnexpectedIPs bool       `json:"allowUnexpectedIps"`
 	}
 	if err := json.Unmarshal(data, &advanced); err == nil {
 		c.Address = advanced.Address
@@ -45,6 +47,7 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 		c.Domains = advanced.Domains
 		c.ExpectIPs = advanced.ExpectIPs
 		c.QueryStrategy = advanced.QueryStrategy
+		c.AllowUnexpectedIPs = advanced.AllowUnexpectedIPs
 		return nil
 	}
 
@@ -111,12 +114,13 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 			Address: c.Address.Build(),
 			Port:    uint32(c.Port),
 		},
-		ClientIp:          myClientIP,
-		SkipFallback:      c.SkipFallback,
-		PrioritizedDomain: domains,
-		Geoip:             geoipList,
-		OriginalRules:     originalRules,
-		QueryStrategy:     resolveQueryStrategy(c.QueryStrategy),
+		ClientIp:           myClientIP,
+		SkipFallback:       c.SkipFallback,
+		PrioritizedDomain:  domains,
+		Geoip:              geoipList,
+		OriginalRules:      originalRules,
+		QueryStrategy:      resolveQueryStrategy(c.QueryStrategy),
+		AllowUnexpectedIPs: c.AllowUnexpectedIPs,
 	}, nil
 }