diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index 66b74e3c..49ed0703 100644 --- a/common/dialer/resolve.go +++ b/common/dialer/resolve.go @@ -44,6 +44,20 @@ type resolveDialer struct { } func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer { + if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { + return &resolveParallelNetworkDialer{ + resolveDialer{ + transport: service.FromContext[adapter.DNSTransportManager](ctx), + router: service.FromContext[adapter.DNSRouter](ctx), + dialer: dialer, + parallel: parallel, + server: server, + queryOptions: queryOptions, + fallbackDelay: fallbackDelay, + }, + parallelDialer, + } + } return &resolveDialer{ transport: service.FromContext[adapter.DNSTransportManager](ctx), router: service.FromContext[adapter.DNSRouter](ctx), @@ -60,21 +74,6 @@ type resolveParallelNetworkDialer struct { dialer ParallelInterfaceDialer } -func NewResolveParallelInterfaceDialer(ctx context.Context, dialer ParallelInterfaceDialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ParallelInterfaceResolveDialer { - return &resolveParallelNetworkDialer{ - resolveDialer{ - transport: service.FromContext[adapter.DNSTransportManager](ctx), - router: service.FromContext[adapter.DNSRouter](ctx), - dialer: dialer, - parallel: parallel, - server: server, - queryOptions: queryOptions, - fallbackDelay: fallbackDelay, - }, - dialer, - } -} - func (d *resolveDialer) initialize() error { d.initOnce.Do(d.initServer) return d.initErr diff --git a/constant/dns.go b/constant/dns.go index 0a444ff0..99e1ec0e 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -15,19 +15,19 @@ const ( ) const ( - DNSTypeLegacy = "legacy" - DNSTypeUDP = "udp" - DNSTypeTCP = "tcp" - DNSTypeTLS = "tls" - DNSTypeHTTPS = "https" - DNSTypeQUIC = "quic" - DNSTypeHTTP3 = "h3" - DNSTypeHosts = "hosts" - DNSTypeLocal = "local" - DNSTypePreDefined = "predefined" - DNSTypeFakeIP = "fakeip" - DNSTypeDHCP = "dhcp" - DNSTypeTailscale = "tailscale" + DNSTypeLegacy = "legacy" + DNSTypeLegacyRcode = "legacy_rcode" + DNSTypeUDP = "udp" + DNSTypeTCP = "tcp" + DNSTypeTLS = "tls" + DNSTypeHTTPS = "https" + DNSTypeQUIC = "quic" + DNSTypeHTTP3 = "h3" + DNSTypeLocal = "local" + DNSTypeHosts = "hosts" + DNSTypeFakeIP = "fakeip" + DNSTypeDHCP = "dhcp" + DNSTypeTailscale = "tailscale" ) const ( diff --git a/constant/rule.go b/constant/rule.go index c4a77838..336c3b38 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -33,6 +33,7 @@ const ( RuleActionTypeHijackDNS = "hijack-dns" RuleActionTypeSniff = "sniff" RuleActionTypeResolve = "resolve" + RuleActionTypePredefined = "predefined" ) const ( diff --git a/dns/router.go b/dns/router.go index 5db5429a..cdff388e 100644 --- a/dns/router.go +++ b/dns/router.go @@ -190,6 +190,8 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, } case *R.RuleActionReject: return nil, currentRule, currentRuleIndex + case *R.RuleActionPredefined: + return nil, currentRule, currentRuleIndex } } } @@ -260,6 +262,21 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte case C.RuleActionRejectMethodDrop: return nil, tun.ErrDrop } + case *R.RuleActionPredefined: + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: action.Rcode, + }, + Question: message.Question, + Answer: action.Answer, + Ns: action.Ns, + Extra: action.Extra, + }, nil } } var responseCheck func(responseAddrs []netip.Addr) bool @@ -376,6 +393,20 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ case C.RuleActionRejectMethodDrop: return nil, tun.ErrDrop } + case *R.RuleActionPredefined: + if action.Rcode != mDNS.RcodeSuccess { + err = RcodeError(action.Rcode) + } else { + for _, answer := range action.Answer { + switch record := answer.(type) { + case *mDNS.A: + responseAddrs = append(responseAddrs, M.AddrFromIP(record.A)) + case *mDNS.AAAA: + responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA)) + } + } + } + goto response } } var responseCheck func(responseAddrs []netip.Addr) bool @@ -395,6 +426,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ printResult() } } +response: printResult() if len(responseAddrs) > 0 { r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) diff --git a/dns/transport/predefined.go b/dns/transport/predefined.go deleted file mode 100644 index dbb78e5c..00000000 --- a/dns/transport/predefined.go +++ /dev/null @@ -1,83 +0,0 @@ -package transport - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/dns" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" - - mDNS "github.com/miekg/dns" -) - -var _ adapter.DNSTransport = (*PredefinedTransport)(nil) - -func RegisterPredefined(registry *dns.TransportRegistry) { - dns.RegisterTransport[option.PredefinedDNSServerOptions](registry, C.DNSTypePreDefined, NewPredefined) -} - -type PredefinedTransport struct { - dns.TransportAdapter - responses []*predefinedResponse -} - -type predefinedResponse struct { - questions []mDNS.Question - answer *mDNS.Msg -} - -func NewPredefined(ctx context.Context, logger log.ContextLogger, tag string, options option.PredefinedDNSServerOptions) (adapter.DNSTransport, error) { - var responses []*predefinedResponse - for _, response := range options.Responses { - questions, msg, err := response.Build() - if err != nil { - return nil, err - } - responses = append(responses, &predefinedResponse{ - questions: questions, - answer: msg, - }) - } - if len(responses) == 0 { - return nil, E.New("empty predefined responses") - } - return &PredefinedTransport{ - TransportAdapter: dns.NewTransportAdapter(C.DNSTypePreDefined, tag, nil), - responses: responses, - }, nil -} - -func (t *PredefinedTransport) Reset() { -} - -func (t *PredefinedTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - for _, response := range t.responses { - for _, question := range response.questions { - if func() bool { - if question.Name == "" && question.Qtype == mDNS.TypeNone { - return true - } else if question.Name == "" { - return common.Any(message.Question, func(it mDNS.Question) bool { - return it.Qtype == question.Qtype - }) - } else if question.Qtype == mDNS.TypeNone { - return common.Any(message.Question, func(it mDNS.Question) bool { - return it.Name == question.Name - }) - } else { - return common.Contains(message.Question, question) - } - }() { - copyAnswer := *response.answer - copyAnswer.Id = message.Id - copyAnswer.Question = message.Question - return ©Answer, nil - } - } - } - return nil, dns.RcodeNameError -} diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index 33c283fa..62c81c32 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -4,7 +4,8 @@ icon: material/new-box !!! quote "Changes in sing-box 1.12.0" - :material-plus: [strategy](#strategy) + :material-plus: [strategy](#strategy) + :material-plus: [predefined](#predefined) !!! question "Since sing-box 1.11.0" @@ -31,6 +32,8 @@ Tag of target server. #### strategy +!!! question "Since sing-box 1.12.0" + Set domain strategy for this query. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. @@ -69,7 +72,7 @@ Will overrides `dns.client_subnet`. ```json { "action": "reject", - "method": "default", // default + "method": "", "no_drop": false } ``` @@ -81,8 +84,61 @@ Will overrides `dns.client_subnet`. - `default`: Reply with NXDOMAIN. - `drop`: Drop the request. +`default` will be used by default. + #### no_drop If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. Not available when `method` is set to drop. + +### predefined + +!!! question "Since sing-box 1.12.0" + +```json +{ + "action": "predefined", + "rcode": "", + "answer": [], + "ns": [], + "extra": [] +} +``` + +`predefined` responds with predefined DNS records. + +#### rcode + +The response code. + +| Value | Value in the legacy rcode server | Description | +|------------|----------------------------------|-----------------| +| `NOERROR` | `success` | Ok | +| `FORMERR` | `format_error` | Bad request | +| `SERVFAIL` | `server_failure` | Server failure | +| `NXDOMAIN` | `name_error` | Not found | +| `NOTIMP` | `not_implemented` | Not implemented | +| `REFUSED` | `refused` | Refused | + +`NOERROR` will be used by default. + +#### answer + +List of text DNS record to respond as answers. + +Examples: + +| Record Type | Example | +|-------------|-------------------------------| +| `A` | `localhost. IN A 127.0.0.1` | +| `AAAA` | `localhost. IN AAAA ::1` | +| `TXT` | `localhost. IN TXT \"Hello\"` | + +#### ns + +List of text DNS record to respond as name servers. + +#### extra + +List of text DNS record to respond as extra records. diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 03843ec5..7aa3da6d 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -4,7 +4,8 @@ icon: material/new-box !!! quote "sing-box 1.12.0 中的更改" - :material-plus: [strategy](#strategy) + :material-plus: [strategy](#strategy) + :material-plus: [predefined](#predefined) !!! question "自 sing-box 1.11.0 起" @@ -12,9 +13,9 @@ icon: material/new-box ```json { - "action": "route", // 默认 + "action": "route", + // 默认 "server": "", - "strategy": "", "disable_cache": false, "rewrite_ttl": null, @@ -32,6 +33,8 @@ icon: material/new-box #### strategy +!!! question "自 sing-box 1.12.0 起" + 为此查询设置域名策略。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 @@ -70,7 +73,7 @@ icon: material/new-box ```json { "action": "reject", - "method": "default", // default + "method": "", "no_drop": false } ``` @@ -82,8 +85,61 @@ icon: material/new-box - `default`: 返回 NXDOMAIN。 - `drop`: 丢弃请求。 +默认使用 `defualt`。 + #### no_drop 如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 当 `method` 设为 `drop` 时不可用。 + +### predefined + +!!! question "自 sing-box 1.12.0 起" + +```json +{ + "action": "predefined", + "rcode": "", + "answer": [], + "ns": [], + "extra": [] +} +``` + +`predefined` 以预定义的 DNS 记录响应。 + +#### rcode + +响应码。 + +| 值 | 旧 rcode DNS 服务器中的值 | 描述 | +|------------|--------------------|-----------------| +| `NOERROR` | `success` | Ok | +| `FORMERR` | `format_error` | Bad request | +| `SERVFAIL` | `server_failure` | Server failure | +| `NXDOMAIN` | `name_error` | Not found | +| `NOTIMP` | `not_implemented` | Not implemented | +| `REFUSED` | `refused` | Refused | + +默认使用 `NOERROR`。 + +#### answer + +用于作为回答响应的文本 DNS 记录列表。 + +例子: + +| 记录类型 | 例子 | +|--------|-------------------------------| +| `A` | `localhost. IN A 127.0.0.1` | +| `AAAA` | `localhost. IN AAAA ::1` | +| `TXT` | `localhost. IN TXT \"Hello\"` | + +#### ns + +用于作为名称服务器响应的文本 DNS 记录列表。 + +#### extra + +用于作为额外记录响应的文本 DNS 记录列表。 diff --git a/docs/configuration/dns/server/hosts.md b/docs/configuration/dns/server/hosts.md index 25c490ed..ce859cca 100644 --- a/docs/configuration/dns/server/hosts.md +++ b/docs/configuration/dns/server/hosts.md @@ -67,4 +67,30 @@ Example: ] } } -``` \ No newline at end of file +``` + +### Examples + +=== "Use hosts if available" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "hosts" + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/predefined.md b/docs/configuration/dns/server/predefined.md deleted file mode 100644 index ac75d6bb..00000000 --- a/docs/configuration/dns/server/predefined.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -icon: material/new-box ---- - -!!! question "Since sing-box 1.12.0" - -# Predefined - -### Structure - -```json -{ - "dns": { - "servers": [ - { - "type": "predefined", - "tag": "", - "responses": [] - } - ] - } -} -``` - -### Fields - -#### responses - -==Required== - -List of [Response](#response-structure). - -### Response Structure - -```json -{ - "query": [], - "query_type": [], - "rcode": "", - "answer": [], - "ns": [], - "extra": [] -} -``` - -!!! note "" - - You can ignore the JSON Array [] tag when the content is only one item - -### Response Fields - -#### query - -List of domain name to match. - -#### query_type - -List of query type to match. - -#### rcode - -The response code. - -| Value | Value in the legacy rcode server | Description | -|------------|----------------------------------|-----------------| -| `NOERROR` | `success` | Ok | -| `FORMERR` | `format_error` | Bad request | -| `SERVFAIL` | `server_failure` | Server failure | -| `NXDOMAIN` | `name_error` | Not found | -| `NOTIMP` | `not_implemented` | Not implemented | -| `REFUSED` | `refused` | Refused | - -`NOERROR` will be used by default. - -#### answer - -List of text DNS record to respond as answers. - -Examples: - -| Record Type | Example | -|-------------|-------------------------------| -| `A` | `localhost. IN A 127.0.0.1` | -| `AAAA` | `localhost. IN AAAA ::1` | -| `TXT` | `localhost. IN TXT \"Hello\"` | - -#### ns - -List of text DNS record to respond as name servers. - -#### extra - -List of text DNS record to respond as extra records. diff --git a/include/registry.go b/include/registry.go index 87aea576..9be1f2b4 100644 --- a/include/registry.go +++ b/include/registry.go @@ -107,7 +107,6 @@ func DNSTransportRegistry() *dns.TransportRegistry { transport.RegisterUDP(registry) transport.RegisterTLS(registry) transport.RegisterHTTPS(registry) - transport.RegisterPredefined(registry) hosts.RegisterTransport(registry) local.RegisterTransport(registry) fakeip.RegisterTransport(registry) diff --git a/mkdocs.yml b/mkdocs.yml index 1aaee8b1..8ee45c94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -91,7 +91,6 @@ nav: - QUIC: configuration/dns/server/quic.md - HTTPS: configuration/dns/server/https.md - HTTP3: configuration/dns/server/http3.md - - Predefined: configuration/dns/server/predefined.md - DHCP: configuration/dns/server/dhcp.md - FakeIP: configuration/dns/server/fakeip.md - Tailscale: configuration/dns/server/tailscale.md diff --git a/option/dns.go b/option/dns.go index c4186240..64dbea38 100644 --- a/option/dns.go +++ b/option/dns.go @@ -46,7 +46,46 @@ func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) e } legacyOptions := o.LegacyDNSOptions o.LegacyDNSOptions = LegacyDNSOptions{} - return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) + err = badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) + if err != nil { + return err + } + rcodeMap := make(map[string]int) + o.Servers = common.Filter(o.Servers, func(it NewDNSServerOptions) bool { + if it.Type == C.DNSTypeLegacyRcode { + rcodeMap[it.Tag] = it.Options.(int) + return false + } + return true + }) + if len(rcodeMap) > 0 { + for i := 0; i < len(o.Rules); i++ { + rewriteRcode(rcodeMap, &o.Rules[i]) + } + } + return nil +} + +func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) { + switch rule.Type { + case C.RuleTypeDefault: + rewriteRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction) + case C.RuleTypeLogical: + rewriteRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction) + } +} + +func rewriteRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) { + if ruleAction.Action != C.RuleActionTypeRoute { + return + } + rcode, loaded := rcodeMap[ruleAction.RouteOptions.Server] + if !loaded { + return + } + ruleAction.Action = C.RuleActionTypePredefined + ruleAction.PredefinedOptions.Rcode = common.Ptr(DNSRCode(rcode)) + return } type DNSClientOptions struct { @@ -243,14 +282,8 @@ func (o *NewDNSServerOptions) Upgrade(ctx context.Context) error { default: return E.New("unknown rcode: ", serverURL.Host) } - o.Type = C.DNSTypePreDefined - o.Options = &PredefinedDNSServerOptions{ - Responses: []DNSResponseOptions{ - { - RCode: common.Ptr(DNSRCode(rcode)), - }, - }, - } + o.Type = C.DNSTypeLegacyRcode + o.Options = rcode case C.DNSTypeDHCP: o.Type = C.DNSTypeDHCP dhcpOptions := DHCPDNSServerOptions{} diff --git a/option/dns_record.go b/option/dns_record.go index c76a76c6..fa72b61b 100644 --- a/option/dns_record.go +++ b/option/dns_record.go @@ -3,30 +3,14 @@ package option import ( "encoding/base64" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" - "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" "github.com/miekg/dns" ) -type PredefinedDNSServerOptions struct { - Responses []DNSResponseOptions `json:"responses,omitempty"` -} - -type DNSResponseOptions struct { - Query badoption.Listable[string] `json:"query,omitempty"` - QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` - - RCode *DNSRCode `json:"rcode,omitempty"` - Answer badoption.Listable[DNSRecordOptions] `json:"answer,omitempty"` - Ns badoption.Listable[DNSRecordOptions] `json:"ns,omitempty"` - Extra badoption.Listable[DNSRecordOptions] `json:"extra,omitempty"` -} - type DNSRCode int func (r DNSRCode) MarshalJSON() ([]byte, error) { @@ -64,49 +48,6 @@ func (r *DNSRCode) Build() int { return int(*r) } -func (o DNSResponseOptions) Build() ([]dns.Question, *dns.Msg, error) { - var questions []dns.Question - if len(o.Query) == 0 && len(o.QueryType) == 0 { - questions = []dns.Question{{Qclass: dns.ClassINET}} - } else if len(o.Query) == 0 { - for _, queryType := range o.QueryType { - questions = append(questions, dns.Question{ - Qtype: uint16(queryType), - Qclass: dns.ClassINET, - }) - } - } else if len(o.QueryType) == 0 { - for _, domain := range o.Query { - questions = append(questions, dns.Question{ - Name: dns.Fqdn(domain), - Qclass: dns.ClassINET, - }) - } - } else { - for _, queryType := range o.QueryType { - for _, domain := range o.Query { - questions = append(questions, dns.Question{ - Name: dns.Fqdn(domain), - Qtype: uint16(queryType), - Qclass: dns.ClassINET, - }) - } - } - } - return questions, &dns.Msg{ - MsgHdr: dns.MsgHdr{ - Response: true, - Rcode: o.RCode.Build(), - Authoritative: true, - RecursionDesired: true, - RecursionAvailable: true, - }, - Answer: common.Map(o.Answer, DNSRecordOptions.build), - Ns: common.Map(o.Ns, DNSRecordOptions.build), - Extra: common.Map(o.Extra, DNSRecordOptions.build), - }, nil -} - type DNSRecordOptions struct { dns.RR fromBase64 bool @@ -156,6 +97,6 @@ func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error { return nil } -func (o DNSRecordOptions) build() dns.RR { +func (o DNSRecordOptions) Build() dns.RR { return o.RR } diff --git a/option/rule_action.go b/option/rule_action.go index 00d3ae7a..7c05dce6 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -92,6 +92,7 @@ type _DNSRuleAction struct { RouteOptions DNSRouteActionOptions `json:"-"` RouteOptionsOptions DNSRouteOptionsActionOptions `json:"-"` RejectOptions RejectActionOptions `json:"-"` + PredefinedOptions DNSRouteActionPredefined `json:"-"` } type DNSRuleAction _DNSRuleAction @@ -109,6 +110,8 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) { v = r.RouteOptionsOptions case C.RuleActionTypeReject: v = r.RejectOptions + case C.RuleActionTypePredefined: + v = r.PredefinedOptions default: return nil, E.New("unknown DNS rule action: " + r.Action) } @@ -129,6 +132,8 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e v = &r.RouteOptionsOptions case C.RuleActionTypeReject: v = &r.RejectOptions + case C.RuleActionTypePredefined: + v = &r.PredefinedOptions default: return E.New("unknown DNS rule action: " + r.Action) } @@ -294,3 +299,10 @@ type RouteActionResolve struct { RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } + +type DNSRouteActionPredefined struct { + Rcode *DNSRCode `json:"rcode,omitempty"` + Answer badoption.Listable[DNSRecordOptions] `json:"answer,omitempty"` + Ns badoption.Listable[DNSRecordOptions] `json:"ns,omitempty"` + Extra badoption.Listable[DNSRecordOptions] `json:"extra,omitempty"` +} diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index fe5ce39c..70363eff 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -20,6 +20,8 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + + "github.com/miekg/dns" ) func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { @@ -126,6 +128,13 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) NoDrop: action.RejectOptions.NoDrop, logger: logger, } + case C.RuleActionTypePredefined: + return &RuleActionPredefined{ + Rcode: action.PredefinedOptions.Rcode.Build(), + Answer: common.Map(action.PredefinedOptions.Answer, option.DNSRecordOptions.Build), + Ns: common.Map(action.PredefinedOptions.Ns, option.DNSRecordOptions.Build), + Extra: common.Map(action.PredefinedOptions.Extra, option.DNSRecordOptions.Build), + } default: panic(F.ToString("unknown rule action: ", action.Action)) } @@ -413,3 +422,23 @@ func (r *RuleActionResolve) String() string { return F.ToString("resolve(", strings.Join(options, ","), ")") } } + +type RuleActionPredefined struct { + Rcode int + Answer []dns.RR + Ns []dns.RR + Extra []dns.RR +} + +func (r *RuleActionPredefined) Type() string { + return C.RuleActionTypePredefined +} + +func (r *RuleActionPredefined) String() string { + var options []string + options = append(options, dns.RcodeToString[r.Rcode]) + options = append(options, common.Map(r.Answer, dns.RR.String)...) + options = append(options, common.Map(r.Ns, dns.RR.String)...) + options = append(options, common.Map(r.Extra, dns.RR.String)...) + return F.ToString("predefined(", strings.Join(options, ","), ")") +}