Add query_client_subnet and query_dnssec DNS rule items and remove_client_subnet DNS route action option

This commit is contained in:
世界
2026-08-30 17:41:45 +08:00
parent 4e91d92c5f
commit b3a45e0839
16 changed files with 297 additions and 15 deletions
+1
View File
@@ -42,6 +42,7 @@ type DNSQueryOptions struct {
RewriteTTL *uint32
Timeout time.Duration
ClientSubnet netip.Prefix
RemoveClientSubnet bool
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) {
+2
View File
@@ -94,6 +94,8 @@ type InboundContext struct {
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
QueryClientSubnet netip.Prefix
QueryDNSSEC bool
FakeIP bool
PreMatch bool
+12 -6
View File
@@ -107,12 +107,15 @@ func (k dnsCacheKey) persistentName() string {
}
func (c *Client) newCacheKey(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions) dnsCacheKey {
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if !clientSubnet.IsValid() {
clientSubnet = clientSubnetFromMessage(message)
var clientSubnet netip.Prefix
if !options.RemoveClientSubnet {
clientSubnet = options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if !clientSubnet.IsValid() {
clientSubnet = clientSubnetFromMessage(message)
}
}
return dnsCacheKey{
Question: question,
@@ -679,6 +682,9 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, key dnsCac
}
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
if options.RemoveClientSubnet {
return removeClientSubnet(message)
}
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
+29 -1
View File
@@ -2,6 +2,9 @@ package dns
import (
"net/netip"
"slices"
"github.com/sagernet/sing/common"
"github.com/miekg/dns"
)
@@ -25,12 +28,37 @@ func clientSubnetFromMessage(message *dns.Msg) netip.Prefix {
if !addressLoaded {
return netip.Prefix{}
}
return netip.PrefixFrom(address, int(subnetOption.SourceNetmask))
return netip.PrefixFrom(address.Unmap(), int(subnetOption.SourceNetmask))
}
}
return netip.Prefix{}
}
func removeClientSubnet(message *dns.Msg) *dns.Msg {
if !slices.ContainsFunc(message.Extra, func(record dns.RR) bool {
optRecord, isOPTRecord := record.(*dns.OPT)
if !isOPTRecord {
return false
}
return slices.ContainsFunc(optRecord.Option, func(option dns.EDNS0) bool {
return option.Option() == dns.EDNS0SUBNET
})
}) {
return message
}
message = message.Copy()
for _, record := range message.Extra {
optRecord, isOPTRecord := record.(*dns.OPT)
if !isOPTRecord {
continue
}
optRecord.Option = common.Filter(optRecord.Option, func(option dns.EDNS0) bool {
return option.Option() != dns.EDNS0SUBNET
})
}
return message
}
func setClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, clone bool) *dns.Msg {
var (
optRecord *dns.OPT
+20
View File
@@ -326,6 +326,11 @@ func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFak
}
if action.ClientSubnet.IsValid() {
options.ClientSubnet = action.ClientSubnet
options.RemoveClientSubnet = false
}
if action.RemoveClientSubnet {
options.ClientSubnet = netip.Prefix{}
options.RemoveClientSubnet = true
}
return transport, currentRule, currentRuleIndex
case *R.RuleActionDNSRouteOptions:
@@ -343,6 +348,11 @@ func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFak
}
if action.ClientSubnet.IsValid() {
options.ClientSubnet = action.ClientSubnet
options.RemoveClientSubnet = false
}
if action.RemoveClientSubnet {
options.ClientSubnet = netip.Prefix{}
options.RemoveClientSubnet = true
}
case *R.RuleActionReject:
return nil, currentRule, currentRuleIndex
@@ -373,6 +383,11 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt
}
if routeOptions.ClientSubnet.IsValid() {
options.ClientSubnet = routeOptions.ClientSubnet
options.RemoveClientSubnet = false
}
if routeOptions.RemoveClientSubnet {
options.ClientSubnet = netip.Prefix{}
options.RemoveClientSubnet = true
}
}
@@ -945,6 +960,8 @@ func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.Domain
func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.QueryType = qType
metadata.QueryClientSubnet = netip.Prefix{}
metadata.QueryDNSSEC = false
metadata.IPVersion = 0
switch qType {
case mDNS.TypeA:
@@ -1069,6 +1086,9 @@ func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsEx
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
metadata.QueryClientSubnet = clientSubnetFromMessage(message)
edns0Option := message.IsEdns0()
metadata.QueryDNSSEC = edns0Option != nil && edns0Option.Do()
return &dnsExchangeContext{
ctx: ctx,
rules: rules,
+23
View File
@@ -14,6 +14,8 @@ icon: material/alert-decagram
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-plus: [query_client_subnet](#query_client_subnet)
:material-plus: [query_dnssec](#query_dnssec)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
@@ -77,6 +79,11 @@ icon: material/alert-decagram
"HTTPS",
32768
],
"query_client_subnet": [
"10.0.0.0/24",
"192.168.0.1"
],
"query_dnssec": false,
"network": "tcp",
"auth_user": [
"usera",
@@ -292,6 +299,22 @@ Not limited if empty.
DNS query type. Values can be integers or type name strings.
#### query_client_subnet
!!! question "Since sing-box 1.14.0"
Match the `edns0-subnet` OPT extra record (EDNS Client Subnet) in the query.
A listed prefix matches when it is no more specific than the received client subnet and contains its address.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
#### query_dnssec
!!! question "Since sing-box 1.14.0"
Match queries with the DNSSEC OK (`DO`) bit set.
#### network
`tcp` or `udp`.
+23
View File
@@ -14,6 +14,8 @@ icon: material/alert-decagram
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-plus: [query_client_subnet](#query_client_subnet)
:material-plus: [query_dnssec](#query_dnssec)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
@@ -77,6 +79,11 @@ icon: material/alert-decagram
"HTTPS",
32768
],
"query_client_subnet": [
"10.0.0.0/24",
"192.168.0.1"
],
"query_dnssec": false,
"network": "tcp",
"auth_user": [
"usera",
@@ -284,6 +291,22 @@ icon: material/alert-decagram
DNS 查询类型。值可以为整数或者类型名称字符串。
#### query_client_subnet
!!! question "自 sing-box 1.14.0 起"
匹配查询中的 `edns0-subnet` OPT 附加记录(EDNS 客户端子网)。
列出的前缀在不比收到的客户端子网更具体、且包含其地址时匹配。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
#### query_dnssec
!!! question "自 sing-box 1.14.0 起"
匹配设置了 DNSSEC OK (`DO`) 位的查询。
#### network
`tcp``udp`
+24 -4
View File
@@ -10,7 +10,8 @@ icon: material/new-box
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
:material-plus: [timeout](#timeout)
:material-plus: [race](#race)
:material-plus: [speculative](#speculative)
:material-plus: [speculative](#speculative)
:material-plus: [remove_client_subnet](#remove_client_subnet)
!!! quote "Changes in sing-box 1.12.0"
@@ -68,7 +69,8 @@ matched. The result may therefore depend on server speed only among race rules.
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"timeout": "",
"client_subnet": null
"client_subnet": null,
"remove_client_subnet": false
}
```
@@ -135,6 +137,14 @@ If value is an IP address instead of prefix, `/32` or `/128` will be appended au
Will override `dns.client_subnet`.
#### remove_client_subnet
!!! question "Since sing-box 1.14.0"
Remove the `edns0-subnet` OPT extra record from the query, and suppress `dns.client_subnet`.
Conflict with `client_subnet`.
### evaluate
!!! question "Since sing-box 1.14.0"
@@ -149,7 +159,8 @@ Will override `dns.client_subnet`.
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"timeout": "",
"client_subnet": null
"client_subnet": null,
"remove_client_subnet": false
}
```
@@ -217,6 +228,14 @@ If value is an IP address instead of prefix, `/32` or `/128` will be appended au
Will override `dns.client_subnet`.
#### remove_client_subnet
!!! question "Since sing-box 1.14.0"
Remove the `edns0-subnet` OPT extra record from the query, and suppress `dns.client_subnet`.
Conflict with `client_subnet`.
### respond
!!! question "Since sing-box 1.14.0"
@@ -242,7 +261,8 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"timeout": "",
"client_subnet": null
"client_subnet": null,
"remove_client_subnet": false
}
```
+24 -4
View File
@@ -10,7 +10,8 @@ icon: material/new-box
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
:material-plus: [timeout](#timeout)
:material-plus: [race](#race)
:material-plus: [speculative](#speculative)
:material-plus: [speculative](#speculative)
:material-plus: [remove_client_subnet](#remove_client_subnet)
!!! quote "sing-box 1.12.0 中的更改"
@@ -61,7 +62,8 @@ icon: material/new-box
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"timeout": "",
"client_subnet": null
"client_subnet": null,
"remove_client_subnet": false
}
```
@@ -125,6 +127,14 @@ icon: material/new-box
将覆盖 `dns.client_subnet`.
#### remove_client_subnet
!!! question "自 sing-box 1.14.0 起"
移除查询中的 `edns0-subnet` OPT 附加记录,并抑制 `dns.client_subnet`
`client_subnet` 冲突。
### evaluate
!!! question "自 sing-box 1.14.0 起"
@@ -139,7 +149,8 @@ icon: material/new-box
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"timeout": "",
"client_subnet": null
"client_subnet": null,
"remove_client_subnet": false
}
```
@@ -203,6 +214,14 @@ icon: material/new-box
将覆盖 `dns.client_subnet`.
#### remove_client_subnet
!!! question "自 sing-box 1.14.0 起"
移除查询中的 `edns0-subnet` OPT 附加记录,并抑制 `dns.client_subnet`
`client_subnet` 冲突。
### respond
!!! question "自 sing-box 1.14.0 起"
@@ -228,7 +247,8 @@ icon: material/new-box
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"timeout": "",
"client_subnet": null
"client_subnet": null,
"remove_client_subnet": false
}
```
+41
View File
@@ -1082,6 +1082,22 @@
}
]
},
"query_client_subnet": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"query_dnssec": {
"type": "boolean"
},
"network": {
"anyOf": [
{
@@ -1680,6 +1696,9 @@
},
"client_subnet": {
"type": "string"
},
"remove_client_subnet": {
"type": "boolean"
}
}
},
@@ -1718,6 +1737,9 @@
},
"client_subnet": {
"type": "string"
},
"remove_client_subnet": {
"type": "boolean"
}
},
"required": [
@@ -1763,6 +1785,9 @@
},
"client_subnet": {
"type": "string"
},
"remove_client_subnet": {
"type": "boolean"
}
},
"required": [
@@ -8692,6 +8717,22 @@
}
]
},
"query_client_subnet": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"query_dnssec": {
"type": "boolean"
},
"network": {
"anyOf": [
{
+1
View File
@@ -230,6 +230,7 @@ type AbstractDNSRouteActionOptions struct {
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
RemoveClientSubnet bool `json:"remove_client_subnet,omitempty"`
}
type DNSRouteOptionsActionOptions AbstractDNSRouteActionOptions
+2
View File
@@ -138,6 +138,8 @@ type RawDefaultDNSRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty" reference:"inbound"`
IPVersion int `json:"ip_version,omitempty" enum:"4,6"`
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
QueryClientSubnet badoption.Listable[*badoption.Prefixable] `json:"query_client_subnet,omitempty"`
QueryDNSSEC bool `json:"query_dnssec,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty" enum:"tcp,udp"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty" enum:"tls,http,quic,dns,stun,bittorrent,dtls,ssh,rdp,ntp"`
+10
View File
@@ -139,6 +139,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache,
RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
RemoveClientSubnet: action.RouteOptions.RemoveClientSubnet,
},
}
case C.RuleActionTypeEvaluate:
@@ -153,6 +154,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
DisableOptimisticCache: action.EvaluateOptions.DisableOptimisticCache,
RewriteTTL: action.EvaluateOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.EvaluateOptions.ClientSubnet)),
RemoveClientSubnet: action.EvaluateOptions.RemoveClientSubnet,
},
}
case C.RuleActionTypeRespond:
@@ -165,6 +167,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache,
RewriteTTL: action.RouteOptionsOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
RemoveClientSubnet: action.RouteOptionsOptions.RemoveClientSubnet,
}
case C.RuleActionTypeReject:
return &RuleActionReject{
@@ -349,6 +352,9 @@ func formatDNSRouteAction(action string, server string, speculative bool, option
if options.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet))
}
if options.RemoveClientSubnet {
descriptions = append(descriptions, "remove-client-subnet")
}
return F.ToString(action, "(", strings.Join(descriptions, ","), ")")
}
@@ -359,6 +365,7 @@ type RuleActionDNSRouteOptions struct {
DisableOptimisticCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
RemoveClientSubnet bool
}
func (r *RuleActionDNSRouteOptions) Type() string {
@@ -382,6 +389,9 @@ func (r *RuleActionDNSRouteOptions) String() string {
if r.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
}
if r.RemoveClientSubnet {
descriptions = append(descriptions, "remove-client-subnet")
}
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
}
+22
View File
@@ -73,6 +73,18 @@ func validateDNSRuleAction(action option.DNSRuleAction) error {
if action.Action == C.RuleActionTypeReject && action.RejectOptions.Method == C.RuleActionRejectMethodReply {
return E.New("reject method `reply` is not supported for DNS rules")
}
var routeOptions option.AbstractDNSRouteActionOptions
switch action.Action {
case "", C.RuleActionTypeRoute:
routeOptions = action.RouteOptions.AbstractDNSRouteActionOptions
case C.RuleActionTypeEvaluate:
routeOptions = action.EvaluateOptions.AbstractDNSRouteActionOptions
case C.RuleActionTypeRouteOptions:
routeOptions = option.AbstractDNSRouteActionOptions(action.RouteOptionsOptions)
}
if routeOptions.RemoveClientSubnet && routeOptions.ClientSubnet != nil {
return E.New("`client_subnet` and `remove_client_subnet` are mutually exclusive")
}
if action.Race {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeRespond, C.RuleActionTypeReject, C.RuleActionTypePredefined:
@@ -127,6 +139,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.QueryClientSubnet) > 0 {
item := NewQueryClientSubnetItem(options.QueryClientSubnet)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.QueryDNSSEC {
item := NewQueryDNSSECItem()
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
@@ -0,0 +1,42 @@
package rule
import (
"net/netip"
"slices"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json/badoption"
)
var _ RuleItem = (*QueryClientSubnetItem)(nil)
type QueryClientSubnetItem struct {
prefixes []netip.Prefix
}
func NewQueryClientSubnetItem(prefixables badoption.Listable[*badoption.Prefixable]) *QueryClientSubnetItem {
return &QueryClientSubnetItem{
prefixes: common.Map(prefixables, func(it *badoption.Prefixable) netip.Prefix {
return it.Build(netip.Prefix{})
}),
}
}
func (r *QueryClientSubnetItem) Match(metadata *adapter.InboundContext) bool {
clientSubnet := metadata.QueryClientSubnet
if !clientSubnet.IsValid() {
return false
}
return slices.ContainsFunc(r.prefixes, func(prefix netip.Prefix) bool {
return clientSubnet.Bits() >= prefix.Bits() && prefix.Contains(clientSubnet.Addr())
})
}
func (r *QueryClientSubnetItem) String() string {
if len(r.prefixes) == 1 {
return "query_client_subnet=" + r.prefixes[0].String()
}
return "query_client_subnet=[" + strings.Join(common.Map(r.prefixes, netip.Prefix.String), " ") + "]"
}
+21
View File
@@ -0,0 +1,21 @@
package rule
import (
"github.com/sagernet/sing-box/adapter"
)
var _ RuleItem = (*QueryDNSSECItem)(nil)
type QueryDNSSECItem struct{}
func NewQueryDNSSECItem() *QueryDNSSECItem {
return &QueryDNSSECItem{}
}
func (r *QueryDNSSECItem) Match(metadata *adapter.InboundContext) bool {
return metadata.QueryDNSSEC
}
func (r *QueryDNSSECItem) String() string {
return "query_dnssec=true"
}