Fix DNS record parsing and matching regressions

This commit is contained in:
世界
2026-03-24 18:21:42 +08:00
parent 62247578f4
commit 9b3415f7fc
4 changed files with 222 additions and 17 deletions

121
dns/_repro_test.go Normal file
View File

@@ -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))
}

View File

@@ -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()

View File

@@ -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)
}

38
option/dns_record_test.go Normal file
View File

@@ -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))
}