diff --git a/dns/_repro_test.go b/dns/_repro_test.go new file mode 100644 index 000000000..467d0cb24 --- /dev/null +++ b/dns/_repro_test.go @@ -0,0 +1,121 @@ +package dns + +import ( + "context" + "errors" + "net/netip" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json/badoption" + mDNS "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestReproLookupWithRulesIgnoresRouteStrategy(t *testing.T) { + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected", Strategy: C.DomainStrategyIPv4Only}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + if transport.Tag() == "default" { + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + } + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + case mDNS.TypeAAAA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil + default: + return nil, errors.New("unexpected qtype") + } + }, + }) + + addrs, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addrs) +} + +func TestReproLogicalMatchResponseIPCIDR(t *testing.T) { + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, errors.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{Domain: badoption.Listable[string]{"example.com"}}, + DNSRuleAction: option.DNSRuleAction{Action: C.RuleActionTypeEvaluate, RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}}, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{MatchResponse: true, IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}}, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}}, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}}, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} diff --git a/dns/router_test.go b/dns/router_test.go index f4bbd9a39..c0def7bf2 100644 --- a/dns/router_test.go +++ b/dns/router_test.go @@ -196,6 +196,65 @@ func TestExchangeNewModeEvaluateMatchResponseRoute(t *testing.T) { require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) } +func TestExchangeNewModeEvaluateMatchResponseRouteIgnoresTTL(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 30), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, errors.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. 60 IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + func TestLookupNewModeAllowsPartialSuccess(t *testing.T) { t.Parallel() diff --git a/option/dns_record.go b/option/dns_record.go index c51341f1d..b2d73fa00 100644 --- a/option/dns_record.go +++ b/option/dns_record.go @@ -2,7 +2,6 @@ package option import ( "encoding/base64" - "strings" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" @@ -52,7 +51,6 @@ func (r *DNSRCode) Build() int { type DNSRecordOptions struct { dns.RR fromBase64 bool - hasTTL bool } func (o DNSRecordOptions) MarshalJSON() ([]byte, error) { @@ -78,24 +76,17 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error { if err == nil { return o.unmarshalBase64(binary) } - parser := dns.NewZoneParser(strings.NewReader(stringValue+"\n"), "", "") - record, ok := parser.Next() - if !ok { - err = parser.Err() - if err == nil { - err = E.New("empty DNS record") - } - return err - } - err = parser.Err() + record, err := dns.NewRR(stringValue) if err != nil { return err } + if record == nil { + return E.New("empty DNS record") + } if a, isA := record.(*dns.A); isA { a.A = M.AddrFromIP(a.A).Unmap().AsSlice() } o.RR = record - o.hasTTL = record.Header().Ttl != 0 return nil } @@ -106,7 +97,6 @@ func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error { } o.RR = record o.fromBase64 = true - o.hasTTL = true return nil } @@ -118,8 +108,5 @@ func (o DNSRecordOptions) Match(record dns.RR) bool { if o.RR == nil || record == nil { return false } - if o.hasTTL { - return o.RR.String() == record.String() - } return dns.IsDuplicate(o.RR, record) } diff --git a/option/dns_record_test.go b/option/dns_record_test.go new file mode 100644 index 000000000..f30f6a682 --- /dev/null +++ b/option/dns_record_test.go @@ -0,0 +1,38 @@ +package option + +import ( + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func mustRecordOptions(t *testing.T, record string) DNSRecordOptions { + t.Helper() + var value DNSRecordOptions + require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`))) + return value +} + +func TestDNSRecordOptionsUnmarshalJSONAcceptsRelativeOwnerNames(t *testing.T) { + t.Parallel() + + for _, record := range []string{ + "example.com A 1.1.1.1", + "@ IN A 1.1.1.1", + "www IN CNAME @", + } { + value := mustRecordOptions(t, record) + require.NotNil(t, value.RR) + } +} + +func TestDNSRecordOptionsMatchIgnoresTTL(t *testing.T) { + t.Parallel() + + expected := mustRecordOptions(t, "example.com. 600 IN A 1.1.1.1") + record, err := dns.NewRR("example.com. 60 IN A 1.1.1.1") + require.NoError(t, err) + + require.True(t, expected.Match(record)) +}