Add address filter support for DNS rules

This commit is contained in:
世界 2024-02-03 17:45:27 +08:00
parent 830ea46932
commit 0517ceef76
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
37 changed files with 436 additions and 248 deletions

View file

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

View file

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

View file

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

View file

@ -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" !!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set) :material-plus: [rule_set](#rule_set)
@ -53,11 +59,19 @@ icon: material/alert-decagram
"source_geoip": [ "source_geoip": [
"private" "private"
], ],
"geoip": [
"cn"
],
"source_ip_cidr": [ "source_ip_cidr": [
"10.0.0.0/24", "10.0.0.0/24",
"192.168.0.1" "192.168.0.1"
], ],
"source_ip_is_private": false, "source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [ "source_port": [
12345 12345
], ],
@ -312,6 +326,32 @@ Disable cache and save cache in this query.
Rewrite TTL in DNS responses. 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 ### Logical Fields
#### type #### type

View file

@ -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 中的更改" !!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set) :material-plus: [rule_set](#rule_set)
@ -53,10 +59,19 @@ icon: material/alert-decagram
"source_geoip": [ "source_geoip": [
"private" "private"
], ],
"geoip": [
"cn"
],
"source_ip_cidr": [ "source_ip_cidr": [
"10.0.0.0/24" "10.0.0.0/24",
"192.168.0.1"
], ],
"source_ip_is_private": false, "source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [ "source_port": [
12345 12345
], ],
@ -307,6 +322,32 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
重写 DNS 回应中的 TTL。 重写 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 #### type
@ -319,4 +360,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
#### rules #### rules
包括的规则。 包括的规则。

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -290,52 +290,6 @@ flowchart TB
=== ":material-dns: DNS rules" === ":material-dns: DNS rules"
!!! info
DNS rules are optional if FakeIP is used.
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"geosite": "geolocation-cn",
"server": "local"
}
]
}
}
```
=== ":material-dns: DNS rules (1.8.0+)"
!!! info
DNS rules are optional if FakeIP is used.
```json ```json
{ {
"dns": { "dns": {
@ -382,74 +336,78 @@ flowchart TB
} }
``` ```
=== ":material-router-network: Route rules" === ":material-dns: DNS rules (1.9.0+)"
!!! 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 ```json
{ {
"outbounds": [ "dns": {
{ "servers": [
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{ {
"type": "logical", "tag": "google",
"mode": "or", "address": "tls://8.8.8.8"
"rules": [
{
"protocol": "dns"
},
{
"port": 53
}
],
"outbound": "dns"
}, },
{ {
"geoip": "private", "tag": "local",
"outbound": "direct" "address": "https://223.5.5.5/dns-query",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
}, },
{ {
"clash_mode": "Direct", "clash_mode": "Direct",
"outbound": "direct" "server": "local"
}, },
{ {
"clash_mode": "Global", "clash_mode": "Global",
"outbound": "default" "server": "google"
}, },
{ {
"type": "logical", "rule_set": "geosite-geolocation-cn",
"mode": "or", "server": "local"
"rules": [
{
"port": 853
},
{
"network": "udp",
"port": 443
},
{
"protocol": "stun"
}
],
"outbound": "block"
}, },
{ {
"geosite": "geolocation-cn", "clash_mode": "Default",
"outbound": "direct" "server": "google"
},
{
"rule_set": "geoip-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"
},
{
"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 ```json
{ {

4
go.mod
View file

@ -24,10 +24,10 @@ require (
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1
github.com/sagernet/gomobile v0.1.3 github.com/sagernet/gomobile v0.1.3
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e 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/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.4.0-beta.20 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-mux v0.2.0
github.com/sagernet/sing-quic v0.1.15 github.com/sagernet/sing-quic v0.1.15
github.com/sagernet/sing-shadowsocks v0.2.6 github.com/sagernet/sing-shadowsocks v0.2.6

8
go.sum
View file

@ -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/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 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= 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.43.0-beta.3 h1:qclJbbpgZe76EH62Bdu3LfDSC2zmuxj7zXCpdQBbe7c=
github.com/sagernet/quic-go v0.40.1/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4DwGbNl9UQrU= 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 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= 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.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 h1:8rEepj4LMcR0Wd389fJIziv/jr3MBtX5qXBHsfxJ+dY=
github.com/sagernet/sing v0.4.0-beta.20/go.mod h1:PFQKbElc2Pke7faBLv8oEba5ehtKO21Ho+TkYemTI3Y= 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.2.0-beta.18 h1:6vzXZThRdA7YUzBOpSbUT48XRumtl/KIpIHFSOP0za8=
github.com/sagernet/sing-dns v0.1.14/go.mod h1:AA+vZMNovuPN5i/sPnfF6756Nq94nzb5nXodMWbta5w= 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 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo=
github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= 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= github.com/sagernet/sing-quic v0.1.15 h1:LGWPxQEeg89+68RHP7HtAV0RZeEWQikUqOfE9nYmr2A=

View file

@ -77,6 +77,9 @@ type DefaultDNSRule struct {
DomainRegex Listable[string] `json:"domain_regex,omitempty"` DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"` Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,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"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"`

View file

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

View file

@ -59,7 +59,7 @@ func isGeoIPRule(rule option.DefaultRule) bool {
} }
func isGeoIPDNSRule(rule option.DefaultDNSRule) 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 { func isGeositeRule(rule option.DefaultRule) bool {
@ -97,3 +97,7 @@ func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
} }
func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.IPCIDR) > 0 || rule.IPSet != nil
}

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" 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.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.SourceIPCIDR) > 0 {
item, err := NewIPCIDRItem(true, options.SourceIPCIDR) item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
if err != nil { if err != nil {
@ -119,11 +125,24 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, 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 { if options.SourceIPIsPrivate {
item := NewIPIsPrivateItem(true) item := NewIPIsPrivateItem(true)
rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort) item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item) rule.sourcePortItems = append(rule.sourcePortItems, item)
@ -211,6 +230,34 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 {
return r.rewriteTTL 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) var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
type LogicalDNSRule struct { type LogicalDNSRule struct {
@ -254,3 +301,47 @@ func (r *LogicalDNSRule) DisableCache() bool {
func (r *LogicalDNSRule) RewriteTTL() *uint32 { func (r *LogicalDNSRule) RewriteTTL() *uint32 {
return r.rewriteTTL 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
}
}

View file

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

View file

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

View file

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

View file

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