From b3a45e083964d23257b2b1dbc282e04a21b00667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 13 Aug 2026 13:31:05 +0800 Subject: [PATCH] Add query_client_subnet and query_dnssec DNS rule items and remove_client_subnet DNS route action option --- adapter/dns.go | 1 + adapter/inbound.go | 2 + dns/client.go | 18 ++++++--- dns/extension_edns0_subnet.go | 30 ++++++++++++++- dns/router.go | 20 ++++++++++ docs/configuration/dns/rule.md | 23 +++++++++++ docs/configuration/dns/rule.zh.md | 23 +++++++++++ docs/configuration/dns/rule_action.md | 28 ++++++++++++-- docs/configuration/dns/rule_action.zh.md | 28 ++++++++++++-- docs/schema.json | 41 ++++++++++++++++++++ option/rule_action.go | 1 + option/rule_dns.go | 2 + route/rule/rule_action.go | 10 +++++ route/rule/rule_dns.go | 22 +++++++++++ route/rule/rule_item_query_client_subnet.go | 42 +++++++++++++++++++++ route/rule/rule_item_query_dnssec.go | 21 +++++++++++ 16 files changed, 297 insertions(+), 15 deletions(-) create mode 100644 route/rule/rule_item_query_client_subnet.go create mode 100644 route/rule/rule_item_query_dnssec.go diff --git a/adapter/dns.go b/adapter/dns.go index 977cf11d..cf949e3e 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -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) { diff --git a/adapter/inbound.go b/adapter/inbound.go index d33fbea2..e5c3fd9c 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -94,6 +94,8 @@ type InboundContext struct { SourceMACAddress net.HardwareAddr SourceHostname string QueryType uint16 + QueryClientSubnet netip.Prefix + QueryDNSSEC bool FakeIP bool PreMatch bool diff --git a/dns/client.go b/dns/client.go index 99cb0ce1..60a2e46e 100644 --- a/dns/client.go +++ b/dns/client.go @@ -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 diff --git a/dns/extension_edns0_subnet.go b/dns/extension_edns0_subnet.go index 77258097..ba262b75 100644 --- a/dns/extension_edns0_subnet.go +++ b/dns/extension_edns0_subnet.go @@ -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 diff --git a/dns/router.go b/dns/router.go index 431212fd..af37ca29 100644 --- a/dns/router.go +++ b/dns/router.go @@ -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, diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index f1c5cdaf..ab5ca0f3 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -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`. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index ba9ccbbb..a72101f6 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -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`。 diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index c9f9e16d..0476588d 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -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 } ``` diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index c5d1f1b4..e01a6e2c 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -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 } ``` diff --git a/docs/schema.json b/docs/schema.json index 21e60632..a831eafc 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -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": [ { diff --git a/option/rule_action.go b/option/rule_action.go index df007773..478dcab1 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -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 diff --git a/option/rule_dns.go b/option/rule_dns.go index 0225bc96..53a6cf26 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -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"` diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 49947343..cb656118 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -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, ","), ")") } diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 66ad779b..4fc4797d 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -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) diff --git a/route/rule/rule_item_query_client_subnet.go b/route/rule/rule_item_query_client_subnet.go new file mode 100644 index 00000000..311680ac --- /dev/null +++ b/route/rule/rule_item_query_client_subnet.go @@ -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), " ") + "]" +} diff --git a/route/rule/rule_item_query_dnssec.go b/route/rule/rule_item_query_dnssec.go new file mode 100644 index 00000000..ddacf1f6 --- /dev/null +++ b/route/rule/rule_item_query_dnssec.go @@ -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" +}