diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index 66b74e3c55..49ed07038e 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 0a444ff0cf..99e1ec0edc 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 c4a778384f..336c3b389a 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 5db5429a9c..cdff388eeb 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 dbb78e5c5d..0000000000 --- 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/include/registry.go b/include/registry.go index 87aea5769b..9be1f2b4f6 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/option/dns.go b/option/dns.go index c4186240f2..64dbea3896 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 c76a76c6f0..fa72b61b73 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 00d3ae7a4d..7c05dce68a 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 fe5ce39c25..70363effc5 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, ","), ")") +}