Selaa lähdekoodia

Add address filter support for DNS rules

世界 1 vuosi sitten
vanhempi
sitoutus
0517ceef76
37 muutettua tiedostoa jossa 420 lisäystä ja 232 poistoa
  1. 8 5
      adapter/inbound.go
  2. 3 0
      adapter/router.go
  3. 2 2
      docs/configuration/dns/index.md
  4. 41 1
      docs/configuration/dns/rule.md
  5. 44 3
      docs/configuration/dns/rule.zh.md
  6. 0 4
      docs/configuration/experimental/cache-file.md
  7. 0 4
      docs/configuration/experimental/cache-file.zh.md
  8. 0 4
      docs/configuration/experimental/clash-api.md
  9. 0 4
      docs/configuration/experimental/clash-api.zh.md
  10. 0 4
      docs/configuration/experimental/index.md
  11. 0 4
      docs/configuration/experimental/index.zh.md
  12. 0 4
      docs/configuration/inbound/tun.md
  13. 0 4
      docs/configuration/inbound/tun.zh.md
  14. 0 4
      docs/configuration/outbound/wireguard.md
  15. 0 4
      docs/configuration/outbound/wireguard.zh.md
  16. 0 4
      docs/configuration/route/index.md
  17. 0 4
      docs/configuration/route/index.zh.md
  18. 0 4
      docs/configuration/route/rule.md
  19. 0 4
      docs/configuration/route/rule.zh.md
  20. 0 4
      docs/configuration/rule-set/headless-rule.md
  21. 0 4
      docs/configuration/rule-set/index.md
  22. 0 4
      docs/configuration/rule-set/source-format.md
  23. 0 5
      docs/configuration/shared/tls.md
  24. 0 4
      docs/configuration/shared/tls.zh.md
  25. 34 76
      docs/manual/proxy/client.md
  26. 2 2
      go.mod
  27. 4 4
      go.sum
  28. 3 0
      option/rule_dns.go
  29. 133 54
      route/router_dns.go
  30. 5 1
      route/router_rule.go
  31. 23 1
      route/rule_abstract.go
  32. 3 3
      route/rule_default.go
  33. 91 0
      route/rule_dns.go
  34. 2 2
      route/rule_headless.go
  35. 8 1
      route/rule_item_rule_set.go
  36. 7 0
      route/rule_set_local.go
  37. 7 0
      route/rule_set_remote.go

+ 8 - 5
adapter/inbound.go

@@ -51,11 +51,13 @@ type InboundContext struct {
 
 	// rule cache
 
-	IPCIDRMatchSource       bool
-	SourceAddressMatch      bool
-	SourcePortMatch         bool
-	DestinationAddressMatch bool
-	DestinationPortMatch    bool
+	IPCIDRMatchSource            bool
+	SourceAddressMatch           bool
+	SourcePortMatch              bool
+	DestinationAddressMatch      bool
+	DestinationPortMatch         bool
+	DidMatch                     bool
+	IgnoreDestinationIPCIDRMatch bool
 }
 
 func (c *InboundContext) ResetRuleCache() {
@@ -64,6 +66,7 @@ func (c *InboundContext) ResetRuleCache() {
 	c.SourcePortMatch = false
 	c.DestinationAddressMatch = false
 	c.DestinationPortMatch = false
+	c.DidMatch = false
 }
 
 type inboundContextKey struct{}

+ 3 - 0
adapter/router.go

@@ -86,6 +86,8 @@ type DNSRule interface {
 	Rule
 	DisableCache() bool
 	RewriteTTL() *uint32
+	WithAddressLimit() bool
+	MatchAddressLimit(metadata *InboundContext) bool
 }
 
 type RuleSet interface {
@@ -99,6 +101,7 @@ type RuleSet interface {
 type RuleSetMetadata struct {
 	ContainsProcessRule bool
 	ContainsWIFIRule    bool
+	ContainsIPCIDRRule  bool
 }
 
 type RuleSetStartContext interface {

+ 2 - 2
docs/configuration/dns/index.md

@@ -21,8 +21,8 @@
 
 ### Fields
 
-| Key      | Format                         |
-|----------|--------------------------------|
+| Key      | Format                          |
+|----------|---------------------------------|
 | `server` | List of [DNS Server](./server/) |
 | `rules`  | List of [DNS Rule](./rule/)     |
 | `fakeip` | [FakeIP](./fakeip/)             |

+ 41 - 1
docs/configuration/dns/rule.md

@@ -1,7 +1,13 @@
 ---
-icon: material/alert-decagram
+icon: material/new-box
 ---
 
+!!! quote "Changes in sing-box 1.9.0"
+
+    :material-plus: [geoip](#geoip)  
+    :material-plus: [ip_cidr](#ip_cidr)  
+    :material-plus: [ip_is_private](#ip_is_private)
+
 !!! quote "Changes in sing-box 1.8.0"
 
     :material-plus: [rule_set](#rule_set)  
@@ -53,11 +59,19 @@ icon: material/alert-decagram
         "source_geoip": [
           "private"
         ],
+        "geoip": [
+          "cn"
+        ],
         "source_ip_cidr": [
           "10.0.0.0/24",
           "192.168.0.1"
         ],
         "source_ip_is_private": false,
+        "ip_cidr": [
+          "10.0.0.0/24",
+          "192.168.0.1"
+        ],
+        "ip_is_private": false,
         "source_port": [
           12345
         ],
@@ -312,6 +326,32 @@ Disable cache and save cache in this query.
 
 Rewrite TTL in DNS responses.
 
+### Address Filter Fields
+
+Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped.
+
+!!! note ""
+
+    `ip_cidr` items in included rule sets also takes effect as an address filtering field.
+
+#### geoip
+
+!!! question "Since sing-box 1.9.0"
+
+Match GeoIP with query response.
+
+#### ip_cidr
+
+!!! question "Since sing-box 1.9.0"
+
+Match IP CIDR with query response.
+
+#### ip_is_private
+
+!!! question "Since sing-box 1.9.0"
+
+Match private IP with query response.
+
 ### Logical Fields
 
 #### type

+ 44 - 3
docs/configuration/dns/rule.zh.md

@@ -1,7 +1,13 @@
 ---
-icon: material/alert-decagram
+icon: material/new-box
 ---
 
+!!! quote "sing-box 1.9.0 中的更改"
+
+    :material-plus: [geoip](#geoip)  
+    :material-plus: [ip_cidr](#ip_cidr)  
+    :material-plus: [ip_is_private](#ip_is_private)
+
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-plus: [rule_set](#rule_set)  
@@ -53,10 +59,19 @@ icon: material/alert-decagram
         "source_geoip": [
           "private"
         ],
+        "geoip": [
+          "cn"
+        ],
         "source_ip_cidr": [
-          "10.0.0.0/24"
+          "10.0.0.0/24",
+          "192.168.0.1"
         ],
         "source_ip_is_private": false,
+        "ip_cidr": [
+          "10.0.0.0/24",
+          "192.168.0.1"
+        ],
+        "ip_is_private": false,
         "source_port": [
           12345
         ],
@@ -307,6 +322,32 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 重写 DNS 回应中的 TTL。
 
+### 地址筛选字段
+
+仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
+
+!!! note ""
+
+    引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。
+
+#### geoip
+
+!!! question "自 sing-box 1.9.0 起"
+
+与查询响应匹配 GeoIP。
+
+#### ip_cidr
+
+!!! question "自 sing-box 1.9.0 起"
+
+与查询相应匹配 IP CIDR。
+
+#### ip_is_private
+
+!!! question "自 sing-box 1.9.0 起"
+
+与查询响应匹配非公开 IP。
+
 ### 逻辑字段
 
 #### type
@@ -319,4 +360,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 #### rules
 
-包括的规则。
+包括的规则。

+ 0 - 4
docs/configuration/experimental/cache-file.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 !!! question "Since sing-box 1.8.0"
 
 ### Structure

+ 0 - 4
docs/configuration/experimental/cache-file.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 !!! question "自 sing-box 1.8.0 起"
 
 ### 结构

+ 0 - 4
docs/configuration/experimental/clash-api.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "Changes in sing-box 1.8.0"
 
     :material-delete-alert: [store_mode](#store_mode)  

+ 0 - 4
docs/configuration/experimental/clash-api.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-delete-alert: [store_mode](#store_mode)  

+ 0 - 4
docs/configuration/experimental/index.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 # Experimental
 
 !!! quote "Changes in sing-box 1.8.0"

+ 0 - 4
docs/configuration/experimental/index.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 # 实验性
 
 !!! quote "sing-box 1.8.0 中的更改"

+ 0 - 4
docs/configuration/inbound/tun.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "Changes in sing-box 1.8.0"
 
     :material-plus: [gso](#gso)  

+ 0 - 4
docs/configuration/inbound/tun.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-plus: [gso](#gso)  

+ 0 - 4
docs/configuration/outbound/wireguard.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 !!! quote "Changes in sing-box 1.8.0"
     
     :material-plus: [gso](#gso)  

+ 0 - 4
docs/configuration/outbound/wireguard.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-plus: [gso](#gso)  

+ 0 - 4
docs/configuration/route/index.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 # Route
 
 !!! quote "Changes in sing-box 1.8.0"

+ 0 - 4
docs/configuration/route/index.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 # 路由
 
 !!! quote "sing-box 1.8.0 中的更改"

+ 0 - 4
docs/configuration/route/rule.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "Changes in sing-box 1.8.0"
 
     :material-plus: [rule_set](#rule_set)  

+ 0 - 4
docs/configuration/route/rule.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-plus: [rule_set](#rule_set)  

+ 0 - 4
docs/configuration/rule-set/headless-rule.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 ### Structure
 
 !!! question "Since sing-box 1.8.0"

+ 0 - 4
docs/configuration/rule-set/index.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 # Rule Set
 
 !!! question "Since sing-box 1.8.0"

+ 0 - 4
docs/configuration/rule-set/source-format.md

@@ -1,7 +1,3 @@
----
-icon: material/new-box
----
-
 # Source Format
 
 !!! question "Since sing-box 1.8.0"

+ 0 - 5
docs/configuration/shared/tls.md

@@ -1,8 +1,3 @@
----
-icon: material/alert-decagram
----
-
-
 !!! quote "Changes in sing-box 1.8.0"
 
     :material-alert-decagram: [utls](#utls)  

+ 0 - 4
docs/configuration/shared/tls.zh.md

@@ -1,7 +1,3 @@
----
-icon: material/alert-decagram
----
-
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-alert-decagram: [utls](#utls)  

+ 34 - 76
docs/manual/proxy/client.md

@@ -290,10 +290,6 @@ flowchart TB
 
 === ":material-dns: DNS rules"
 
-    !!! info
-    
-        DNS rules are optional if FakeIP is used.
-
     ```json
     {
       "dns": {
@@ -322,19 +318,29 @@ flowchart TB
             "server": "google"
           },
           {
-            "geosite": "geolocation-cn",
+            "rule_set": "geosite-geolocation-cn",
             "server": "local"
           }
         ]
+      },
+      "route": {
+        "rule_set": [
+          {
+            "type": "remote",
+            "tag": "geosite-geolocation-cn",
+            "format": "binary",
+            "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
+          }
+        ]
       }
     }
     ```
 
-=== ":material-dns: DNS rules (1.8.0+)"
+=== ":material-dns: DNS rules (1.9.0+)"
 
-    !!! info
-    
-        DNS rules are optional if FakeIP is used.
+    !!! warning "DNS leaks"
+
+        The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS if using this method.
 
     ```json
     {
@@ -346,7 +352,7 @@ flowchart TB
           },
           {
             "tag": "local",
-            "address": "223.5.5.5",
+            "address": "https://223.5.5.5/dns-query",
             "detour": "direct"
           }
         ],
@@ -366,6 +372,14 @@ flowchart TB
           {
             "rule_set": "geosite-geolocation-cn",
             "server": "local"
+          },
+          {
+            "clash_mode": "Default",
+            "server": "google"
+          },
+          {
+            "rule_set": "geoip-cn",
+            "server": "local"
           }
         ]
       },
@@ -376,80 +390,24 @@ flowchart TB
             "tag": "geosite-geolocation-cn",
             "format": "binary",
             "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
-          }
-        ]
-      }
-    }
-    ```
-
-=== ":material-router-network: Route rules"
-
-    ```json
-    {
-      "outbounds": [
-        {
-          "type": "direct",
-          "tag": "direct"
-        },
-        {
-          "type": "block",
-          "tag": "block"
-        }
-      ],
-      "route": {
-        "rules": [
-          {
-            "type": "logical",
-            "mode": "or",
-            "rules": [
-              {
-                "protocol": "dns"
-              },
-              {
-                "port": 53
-              }
-            ],
-            "outbound": "dns"
-          },
-          {
-            "geoip": "private",
-            "outbound": "direct"
           },
           {
-            "clash_mode": "Direct",
-            "outbound": "direct"
-          },
-          {
-            "clash_mode": "Global",
-            "outbound": "default"
-          },
-          {
-            "type": "logical",
-            "mode": "or",
-            "rules": [
-              {
-                "port": 853
-              },
-              {
-                "network": "udp",
-                "port": 443
-              },
-              {
-                "protocol": "stun"
-              }
-            ],
-            "outbound": "block"
-          },
-          {
-            "geosite": "geolocation-cn",
-            "outbound": "direct"
+            "type": "remote",
+            "tag": "geoip-cn",
+            "format": "binary",
+            "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
           }
         ]
+      },
+      "experimental": {
+        "clash_api": {
+          "default_mode": "Leak"
+        }
       }
     }
     ```
 
-=== ":material-router-network: Route rules (1.8.0+)"
+=== ":material-router-network: Route rules"
 
     ```json
     {

+ 2 - 2
go.mod

@@ -24,10 +24,10 @@ require (
 	github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1
 	github.com/sagernet/gomobile v0.1.3
 	github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e
-	github.com/sagernet/quic-go v0.40.1
+	github.com/sagernet/quic-go v0.43.0-beta.3
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
 	github.com/sagernet/sing v0.4.0-beta.20
-	github.com/sagernet/sing-dns v0.1.14
+	github.com/sagernet/sing-dns v0.2.0-beta.18
 	github.com/sagernet/sing-mux v0.2.0
 	github.com/sagernet/sing-quic v0.1.15
 	github.com/sagernet/sing-shadowsocks v0.2.6

+ 4 - 4
go.sum

@@ -101,15 +101,15 @@ github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dks
 github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
-github.com/sagernet/quic-go v0.40.1 h1:qLeTIJR0d0JWRmDWo346nLsVN6EWihd1kalJYPEd0TM=
-github.com/sagernet/quic-go v0.40.1/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4DwGbNl9UQrU=
+github.com/sagernet/quic-go v0.43.0-beta.3 h1:qclJbbpgZe76EH62Bdu3LfDSC2zmuxj7zXCpdQBbe7c=
+github.com/sagernet/quic-go v0.43.0-beta.3/go.mod h1:3EtxR1Yaa1FZu6jFPiBHpOAdhOxL4A3EPxmiVgjJvVM=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
 github.com/sagernet/sing v0.4.0-beta.20 h1:8rEepj4LMcR0Wd389fJIziv/jr3MBtX5qXBHsfxJ+dY=
 github.com/sagernet/sing v0.4.0-beta.20/go.mod h1:PFQKbElc2Pke7faBLv8oEba5ehtKO21Ho+TkYemTI3Y=
-github.com/sagernet/sing-dns v0.1.14 h1:kxE/Ik3jMXmD3sXsdt9MgrNzLFWt64mghV+MQqzyf40=
-github.com/sagernet/sing-dns v0.1.14/go.mod h1:AA+vZMNovuPN5i/sPnfF6756Nq94nzb5nXodMWbta5w=
+github.com/sagernet/sing-dns v0.2.0-beta.18 h1:6vzXZThRdA7YUzBOpSbUT48XRumtl/KIpIHFSOP0za8=
+github.com/sagernet/sing-dns v0.2.0-beta.18/go.mod h1:k/dmFcQpg6+m08gC1yQBy+13+QkuLqpKr4bIreq4U24=
 github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo=
 github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
 github.com/sagernet/sing-quic v0.1.15 h1:LGWPxQEeg89+68RHP7HtAV0RZeEWQikUqOfE9nYmr2A=

+ 3 - 0
option/rule_dns.go

@@ -77,6 +77,9 @@ type DefaultDNSRule struct {
 	DomainRegex       Listable[string]       `json:"domain_regex,omitempty"`
 	Geosite           Listable[string]       `json:"geosite,omitempty"`
 	SourceGeoIP       Listable[string]       `json:"source_geoip,omitempty"`
+	GeoIP             Listable[string]       `json:"geoip,omitempty"`
+	IPCIDR            Listable[string]       `json:"ip_cidr,omitempty"`
+	IPIsPrivate       bool                   `json:"ip_is_private,omitempty"`
 	SourceIPCIDR      Listable[string]       `json:"source_ip_cidr,omitempty"`
 	SourceIPIsPrivate bool                   `json:"source_ip_is_private,omitempty"`
 	SourcePort        Listable[uint16]       `json:"source_port,omitempty"`

+ 133 - 54
route/router_dns.go

@@ -2,13 +2,13 @@ package route
 
 import (
 	"context"
+	"errors"
 	"net/netip"
 	"strings"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common/cache"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -37,41 +37,51 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) {
 	return domain, loaded
 }
 
-func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool) (context.Context, dns.Transport, dns.DomainStrategy) {
+func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (context.Context, dns.Transport, dns.DomainStrategy, adapter.DNSRule, int) {
 	metadata := adapter.ContextFrom(ctx)
 	if metadata == nil {
 		panic("no context")
 	}
-	for i, rule := range r.dnsRules {
-		metadata.ResetRuleCache()
-		if rule.Match(metadata) {
-			detour := rule.Outbound()
-			transport, loaded := r.transportMap[detour]
-			if !loaded {
-				r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour)
-				continue
-			}
-			if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP {
-				continue
-			}
-			r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
-			if rule.DisableCache() {
-				ctx = dns.ContextWithDisableCache(ctx, true)
-			}
-			if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
-				ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
-			}
-			if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
-				return ctx, transport, domainStrategy
-			} else {
-				return ctx, transport, r.defaultDomainStrategy
+	if index < len(r.dnsRules) {
+		dnsRules := r.dnsRules
+		if index != -1 {
+			dnsRules = dnsRules[index+1:]
+		}
+		for ruleIndex, rule := range dnsRules {
+			metadata.ResetRuleCache()
+			if rule.Match(metadata) {
+				detour := rule.Outbound()
+				transport, loaded := r.transportMap[detour]
+				if !loaded {
+					r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour)
+					continue
+				}
+				if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP {
+					continue
+				}
+				displayRuleIndex := ruleIndex
+				if index != -1 {
+					displayRuleIndex += index + 1
+				}
+				r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", detour)
+				if rule.DisableCache() {
+					ctx = dns.ContextWithDisableCache(ctx, true)
+				}
+				if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
+					ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
+				}
+				if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
+					return ctx, transport, domainStrategy, rule, ruleIndex
+				} else {
+					return ctx, transport, r.defaultDomainStrategy, rule, ruleIndex
+				}
 			}
 		}
 	}
 	if domainStrategy, dsLoaded := r.transportDomainStrategy[r.defaultTransport]; dsLoaded {
-		return ctx, r.defaultTransport, domainStrategy
+		return ctx, r.defaultTransport, domainStrategy, nil, -1
 	} else {
-		return ctx, r.defaultTransport, r.defaultDomainStrategy
+		return ctx, r.defaultTransport, r.defaultDomainStrategy, nil, -1
 	}
 }
 
@@ -86,7 +96,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
 	)
 	response, cached = r.dnsClient.ExchangeCache(ctx, message)
 	if !cached {
-		ctx, metadata := adapter.AppendContext(ctx)
+		var metadata *adapter.InboundContext
+		ctx, metadata = adapter.AppendContext(ctx)
 		if len(message.Question) > 0 {
 			metadata.QueryType = message.Question[0].Qtype
 			switch metadata.QueryType {
@@ -97,17 +108,47 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
 			}
 			metadata.Domain = fqdnToDomain(message.Question[0].Name)
 		}
-		ctx, transport, strategy := r.matchDNS(ctx, true)
-		ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout)
-		defer cancel()
-		response, err = r.dnsClient.Exchange(ctx, transport, message, strategy)
-		if err != nil && len(message.Question) > 0 {
-			r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String())))
+		var (
+			transport dns.Transport
+			strategy  dns.DomainStrategy
+			rule      adapter.DNSRule
+			ruleIndex int
+		)
+		ruleIndex = -1
+		for {
+			var (
+				dnsCtx       context.Context
+				cancel       context.CancelFunc
+				addressLimit bool
+			)
+
+			dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex)
+			dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout)
+			if rule != nil && rule.WithAddressLimit() && isAddressQuery(message) {
+				addressLimit = true
+				response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool {
+					metadata.DestinationAddresses, _ = dns.MessageToAddresses(response)
+					return rule.MatchAddressLimit(metadata)
+				})
+			} else {
+				addressLimit = false
+				response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy)
+			}
+			cancel()
+			if err != nil {
+				if errors.Is(err, dns.ErrResponseRejected) {
+					r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String())))
+				} else if len(message.Question) > 0 {
+					r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String())))
+				} else {
+					r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
+				}
+			}
+			if !addressLimit || err == nil {
+				break
+			}
 		}
 	}
-	if len(message.Question) > 0 && response != nil {
-		LogDNSAnswers(r.dnsLogger, ctx, message.Question[0].Name, response.Answer)
-	}
 	if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
 		for _, answer := range response.Answer {
 			switch record := answer.(type) {
@@ -125,22 +166,57 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
 	r.dnsLogger.DebugContext(ctx, "lookup domain ", domain)
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Domain = domain
-	ctx, transport, transportStrategy := r.matchDNS(ctx, false)
-	if strategy == dns.DomainStrategyAsIS {
-		strategy = transportStrategy
+	var (
+		transport         dns.Transport
+		transportStrategy dns.DomainStrategy
+		rule              adapter.DNSRule
+		ruleIndex         int
+		resultAddrs       []netip.Addr
+		err               error
+	)
+	ruleIndex = -1
+	for {
+		var (
+			dnsCtx       context.Context
+			cancel       context.CancelFunc
+			addressLimit bool
+		)
+		metadata.ResetRuleCache()
+		metadata.DestinationAddresses = nil
+		dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex)
+		if strategy == dns.DomainStrategyAsIS {
+			strategy = transportStrategy
+		}
+		dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout)
+		if rule != nil && rule.WithAddressLimit() {
+			addressLimit = true
+			resultAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool {
+				metadata.DestinationAddresses = responseAddrs
+				return rule.MatchAddressLimit(metadata)
+			})
+		} else {
+			addressLimit = false
+			resultAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy)
+		}
+		cancel()
+		if err != nil {
+			if errors.Is(err, dns.ErrResponseRejected) {
+				r.dnsLogger.DebugContext(ctx, "response rejected for ", domain)
+			} else {
+				r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
+			}
+		} else if len(resultAddrs) == 0 {
+			r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
+			err = dns.RCodeNameError
+		}
+		if !addressLimit || err == nil {
+			break
+		}
 	}
-	ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout)
-	defer cancel()
-	addrs, err := r.dnsClient.Lookup(ctx, transport, domain, strategy)
-	if len(addrs) > 0 {
-		r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(addrs), " "))
-	} else if err != nil {
-		r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
-	} else {
-		r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
-		err = dns.RCodeNameError
+	if len(resultAddrs) > 0 {
+		r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(resultAddrs), " "))
 	}
-	return addrs, err
+	return resultAddrs, err
 }
 
 func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) {
@@ -154,10 +230,13 @@ func (r *Router) ClearDNSCache() {
 	}
 }
 
-func LogDNSAnswers(logger log.ContextLogger, ctx context.Context, domain string, answers []mDNS.RR) {
-	for _, answer := range answers {
-		logger.InfoContext(ctx, "exchanged ", domain, " ", mDNS.Type(answer.Header().Rrtype).String(), " ", formatQuestion(answer.String()))
+func isAddressQuery(message *mDNS.Msg) bool {
+	for _, question := range message.Question {
+		if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
+			return true
+		}
 	}
+	return false
 }
 
 func fqdnToDomain(fqdn string) string {

+ 5 - 1
route/router_rule.go

@@ -59,7 +59,7 @@ func isGeoIPRule(rule option.DefaultRule) bool {
 }
 
 func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
-	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
+	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
 }
 
 func isGeositeRule(rule option.DefaultRule) bool {
@@ -97,3 +97,7 @@ func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
 func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
 	return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
 }
+
+func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool {
+	return len(rule.IPCIDR) > 0 || rule.IPSet != nil
+}

+ 23 - 1
route/rule_abstract.go

@@ -15,6 +15,7 @@ type abstractDefaultRule struct {
 	sourceAddressItems      []RuleItem
 	sourcePortItems         []RuleItem
 	destinationAddressItems []RuleItem
+	destinationIPCIDRItems  []RuleItem
 	destinationPortItems    []RuleItem
 	allItems                []RuleItem
 	ruleSetItem             RuleItem
@@ -64,6 +65,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 	}
 
 	if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
+		metadata.DidMatch = true
 		for _, item := range r.sourceAddressItems {
 			if item.Match(metadata) {
 				metadata.SourceAddressMatch = true
@@ -73,6 +75,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 	}
 
 	if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
+		metadata.DidMatch = true
 		for _, item := range r.sourcePortItems {
 			if item.Match(metadata) {
 				metadata.SourcePortMatch = true
@@ -82,6 +85,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 	}
 
 	if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
+		metadata.DidMatch = true
 		for _, item := range r.destinationAddressItems {
 			if item.Match(metadata) {
 				metadata.DestinationAddressMatch = true
@@ -90,7 +94,18 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 		}
 	}
 
+	if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch {
+		metadata.DidMatch = true
+		for _, item := range r.destinationIPCIDRItems {
+			if item.Match(metadata) {
+				metadata.DestinationAddressMatch = true
+				break
+			}
+		}
+	}
+
 	if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
+		metadata.DidMatch = true
 		for _, item := range r.destinationPortItems {
 			if item.Match(metadata) {
 				metadata.DestinationPortMatch = true
@@ -100,6 +115,9 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 	}
 
 	for _, item := range r.items {
+		if _, isRuleSet := item.(*RuleSetItem); !isRuleSet {
+			metadata.DidMatch = true
+		}
 		if !item.Match(metadata) {
 			return r.invert
 		}
@@ -113,7 +131,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 		return r.invert
 	}
 
-	if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
+	if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch {
 		return r.invert
 	}
 
@@ -121,6 +139,10 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 		return r.invert
 	}
 
+	if !metadata.DidMatch {
+		return true
+	}
+
 	return !r.invert
 }
 

+ 3 - 3
route/rule_default.go

@@ -109,7 +109,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
 	}
 	if len(options.GeoIP) > 0 {
 		item := NewGeoIPItem(router, logger, false, options.GeoIP)
-		rule.destinationAddressItems = append(rule.destinationAddressItems, item)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.SourceIPCIDR) > 0 {
@@ -130,12 +130,12 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
 		if err != nil {
 			return nil, E.Cause(err, "ipcidr")
 		}
-		rule.destinationAddressItems = append(rule.destinationAddressItems, item)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
 	if options.IPIsPrivate {
 		item := NewIPIsPrivateItem(false)
-		rule.destinationAddressItems = append(rule.destinationAddressItems, item)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.SourcePort) > 0 {

+ 91 - 0
route/rule_dns.go

@@ -5,6 +5,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 
@@ -111,6 +112,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 		rule.sourceAddressItems = append(rule.sourceAddressItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
+	if len(options.GeoIP) > 0 {
+		item := NewGeoIPItem(router, logger, false, options.GeoIP)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if len(options.SourceIPCIDR) > 0 {
 		item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
 		if err != nil {
@@ -119,11 +125,24 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 		rule.sourceAddressItems = append(rule.sourceAddressItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
+	if len(options.IPCIDR) > 0 {
+		item, err := NewIPCIDRItem(false, options.IPCIDR)
+		if err != nil {
+			return nil, E.Cause(err, "ip_cidr")
+		}
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if options.SourceIPIsPrivate {
 		item := NewIPIsPrivateItem(true)
 		rule.sourceAddressItems = append(rule.sourceAddressItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
+	if options.IPIsPrivate {
+		item := NewIPIsPrivateItem(false)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if len(options.SourcePort) > 0 {
 		item := NewPortItem(true, options.SourcePort)
 		rule.sourcePortItems = append(rule.sourcePortItems, item)
@@ -211,6 +230,34 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 {
 	return r.rewriteTTL
 }
 
+func (r *DefaultDNSRule) WithAddressLimit() bool {
+	if len(r.destinationIPCIDRItems) > 0 {
+		return true
+	}
+	for _, rawRule := range r.items {
+		ruleSet, isRuleSet := rawRule.(*RuleSetItem)
+		if !isRuleSet {
+			continue
+		}
+		if ruleSet.ContainsIPCIDRRule() {
+			return true
+		}
+	}
+	return false
+}
+
+func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
+	metadata.IgnoreDestinationIPCIDRMatch = true
+	defer func() {
+		metadata.IgnoreDestinationIPCIDRMatch = false
+	}()
+	return r.abstractDefaultRule.Match(metadata)
+}
+
+func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
+	return r.abstractDefaultRule.Match(metadata)
+}
+
 var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
 
 type LogicalDNSRule struct {
@@ -254,3 +301,47 @@ func (r *LogicalDNSRule) DisableCache() bool {
 func (r *LogicalDNSRule) RewriteTTL() *uint32 {
 	return r.rewriteTTL
 }
+
+func (r *LogicalDNSRule) WithAddressLimit() bool {
+	for _, rawRule := range r.rules {
+		switch rule := rawRule.(type) {
+		case *DefaultDNSRule:
+			if rule.WithAddressLimit() {
+				return true
+			}
+		case *LogicalDNSRule:
+			if rule.WithAddressLimit() {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
+	if r.mode == C.LogicalTypeAnd {
+		return common.All(r.rules, func(it adapter.HeadlessRule) bool {
+			metadata.ResetRuleCache()
+			return it.(adapter.DNSRule).Match(metadata)
+		}) != r.invert
+	} else {
+		return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
+			metadata.ResetRuleCache()
+			return it.(adapter.DNSRule).Match(metadata)
+		}) != r.invert
+	}
+}
+
+func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
+	if r.mode == C.LogicalTypeAnd {
+		return common.All(r.rules, func(it adapter.HeadlessRule) bool {
+			metadata.ResetRuleCache()
+			return it.(adapter.DNSRule).MatchAddressLimit(metadata)
+		}) != r.invert
+	} else {
+		return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
+			metadata.ResetRuleCache()
+			return it.(adapter.DNSRule).MatchAddressLimit(metadata)
+		}) != r.invert
+	}
+}

+ 2 - 2
route/rule_headless.go

@@ -80,11 +80,11 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles
 		if err != nil {
 			return nil, E.Cause(err, "ipcidr")
 		}
-		rule.destinationAddressItems = append(rule.destinationAddressItems, item)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
 		rule.allItems = append(rule.allItems, item)
 	} else if options.IPSet != nil {
 		item := NewRawIPCIDRItem(false, options.IPSet)
-		rule.destinationAddressItems = append(rule.destinationAddressItems, item)
+		rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
 		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.SourcePort) > 0 {

+ 8 - 1
route/rule_item_rule_set.go

@@ -4,6 +4,7 @@ import (
 	"strings"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 )
@@ -13,7 +14,7 @@ var _ RuleItem = (*RuleSetItem)(nil)
 type RuleSetItem struct {
 	router            adapter.Router
 	tagList           []string
-	setList           []adapter.HeadlessRule
+	setList           []adapter.RuleSet
 	ipcidrMatchSource bool
 }
 
@@ -46,6 +47,12 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
 	return false
 }
 
+func (r *RuleSetItem) ContainsIPCIDRRule() bool {
+	return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool {
+		return ruleSet.Metadata().ContainsIPCIDRRule
+	})
+}
+
 func (r *RuleSetItem) String() string {
 	if len(r.tagList) == 1 {
 		return F.ToString("rule_set=", r.tagList[0])

+ 7 - 0
route/rule_set_local.go

@@ -3,12 +3,14 @@ package route
 import (
 	"context"
 	"os"
+	"strings"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/srs"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
 )
 
@@ -55,6 +57,7 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS
 	var metadata adapter.RuleSetMetadata
 	metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
 	metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
+	metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
 	return &LocalRuleSet{rules, metadata}, nil
 }
 
@@ -67,6 +70,10 @@ func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
 	return false
 }
 
+func (s *LocalRuleSet) String() string {
+	return strings.Join(F.MapToString(s.rules), " ")
+}
+
 func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
 	return nil
 }

+ 7 - 0
route/rule_set_remote.go

@@ -7,6 +7,7 @@ import (
 	"net"
 	"net/http"
 	"runtime"
+	"strings"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -14,6 +15,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
@@ -68,6 +70,10 @@ func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
 	return false
 }
 
+func (s *RemoteRuleSet) String() string {
+	return strings.Join(F.MapToString(s.rules), " ")
+}
+
 func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
 	var dialer N.Dialer
 	if s.options.RemoteOptions.DownloadDetour != "" {
@@ -150,6 +156,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
 	}
 	s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
 	s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
+	s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
 	s.rules = rules
 	return nil
 }