package dns import ( "context" "net" "net/netip" "strings" "sync" "testing" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" rulepkg "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-tun" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badoption" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" "github.com/stretchr/testify/require" "go4.org/netipx" ) type fakeDNSTransport struct { tag string transportType string } func (t *fakeDNSTransport) Start(adapter.StartStage) error { return nil } func (t *fakeDNSTransport) Close() error { return nil } func (t *fakeDNSTransport) Type() string { return t.transportType } func (t *fakeDNSTransport) Tag() string { return t.tag } func (t *fakeDNSTransport) Dependencies() []string { return nil } func (t *fakeDNSTransport) Reset() {} func (t *fakeDNSTransport) Exchange(context.Context, *mDNS.Msg) (*mDNS.Msg, error) { return nil, E.New("unused transport exchange") } type fakeDNSTransportManager struct { defaultTransport adapter.DNSTransport transports map[string]adapter.DNSTransport } func (m *fakeDNSTransportManager) Start(adapter.StartStage) error { return nil } func (m *fakeDNSTransportManager) Close() error { return nil } func (m *fakeDNSTransportManager) Transports() []adapter.DNSTransport { transports := make([]adapter.DNSTransport, 0, len(m.transports)) for _, transport := range m.transports { transports = append(transports, transport) } return transports } func (m *fakeDNSTransportManager) Transport(tag string) (adapter.DNSTransport, bool) { transport, loaded := m.transports[tag] return transport, loaded } func (m *fakeDNSTransportManager) Default() adapter.DNSTransport { return m.defaultTransport } func (m *fakeDNSTransportManager) FakeIP() adapter.FakeIPTransport { return nil } func (m *fakeDNSTransportManager) Remove(string) error { return nil } func (m *fakeDNSTransportManager) Create(context.Context, log.ContextLogger, string, string, any) error { return E.New("unsupported") } type fakeDNSClient struct { beforeExchange func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg) exchange func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) lookupWithCtx func(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) lookup func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) } type fakeDeprecatedManager struct { features []deprecated.Note } type fakeRouter struct { access sync.RWMutex ruleSets map[string]adapter.RuleSet } func (r *fakeRouter) Start(adapter.StartStage) error { return nil } func (r *fakeRouter) Close() error { return nil } func (r *fakeRouter) PreMatch(metadata adapter.InboundContext, _ tun.DirectRouteContext, _ time.Duration, _ bool) (tun.DirectRouteDestination, error) { return nil, nil } func (r *fakeRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error { return nil } func (r *fakeRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error { return nil } func (r *fakeRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) { } func (r *fakeRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) { } func (r *fakeRouter) RuleSet(tag string) (adapter.RuleSet, bool) { r.access.RLock() defer r.access.RUnlock() ruleSet, loaded := r.ruleSets[tag] return ruleSet, loaded } func (r *fakeRouter) setRuleSet(tag string, ruleSet adapter.RuleSet) { r.access.Lock() defer r.access.Unlock() if r.ruleSets == nil { r.ruleSets = make(map[string]adapter.RuleSet) } r.ruleSets[tag] = ruleSet } func (r *fakeRouter) Rules() []adapter.Rule { return nil } func (r *fakeRouter) NeedFindProcess() bool { return false } func (r *fakeRouter) NeedFindNeighbor() bool { return false } func (r *fakeRouter) NeighborResolver() adapter.NeighborResolver { return nil } func (r *fakeRouter) AppendTracker(adapter.ConnectionTracker) {} func (r *fakeRouter) ResetNetwork() {} type fakeRuleSet struct { access sync.Mutex metadata adapter.RuleSetMetadata metadataRead func(adapter.RuleSetMetadata) adapter.RuleSetMetadata match func(*adapter.InboundContext) bool callbacks list.List[adapter.RuleSetUpdateCallback] refs int afterIncrementReference func() beforeDecrementReference func() } func (s *fakeRuleSet) Name() string { return "fake-rule-set" } func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil } func (s *fakeRuleSet) PostStart() error { return nil } func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata { s.access.Lock() metadata := s.metadata metadataRead := s.metadataRead s.access.Unlock() if metadataRead != nil { return metadataRead(metadata) } return metadata } func (s *fakeRuleSet) ExtractIPSet() []*netipx.IPSet { return nil } func (s *fakeRuleSet) IncRef() { s.access.Lock() s.refs++ afterIncrementReference := s.afterIncrementReference s.access.Unlock() if afterIncrementReference != nil { afterIncrementReference() } } func (s *fakeRuleSet) DecRef() { s.access.Lock() beforeDecrementReference := s.beforeDecrementReference s.access.Unlock() if beforeDecrementReference != nil { beforeDecrementReference() } s.access.Lock() defer s.access.Unlock() s.refs-- if s.refs < 0 { panic("rule-set: negative refs") } } func (s *fakeRuleSet) Cleanup() {} func (s *fakeRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { s.access.Lock() defer s.access.Unlock() return s.callbacks.PushBack(callback) } func (s *fakeRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { s.access.Lock() defer s.access.Unlock() s.callbacks.Remove(element) } func (s *fakeRuleSet) Close() error { return nil } func (s *fakeRuleSet) Match(metadata *adapter.InboundContext) bool { s.access.Lock() match := s.match s.access.Unlock() if match != nil { return match(metadata) } return true } func (s *fakeRuleSet) String() string { return "fake-rule-set" } func (s *fakeRuleSet) updateMetadata(metadata adapter.RuleSetMetadata) { s.access.Lock() s.metadata = metadata callbacks := s.callbacks.Array() s.access.Unlock() for _, callback := range callbacks { callback(s) } } func (s *fakeRuleSet) snapshotCallbacks() []adapter.RuleSetUpdateCallback { s.access.Lock() defer s.access.Unlock() return s.callbacks.Array() } func (s *fakeRuleSet) refCount() int { s.access.Lock() defer s.access.Unlock() return s.refs } func (m *fakeDeprecatedManager) ReportDeprecated(feature deprecated.Note) { m.features = append(m.features, feature) } func (c *fakeDNSClient) Start() {} func (c *fakeDNSClient) Exchange(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, _ adapter.DNSQueryOptions, _ func(*mDNS.Msg) bool) (*mDNS.Msg, error) { if c.beforeExchange != nil { c.beforeExchange(ctx, transport, message) } if c.exchange == nil { if len(message.Question) != 1 { return nil, E.New("unused client exchange") } var ( addresses []netip.Addr response *mDNS.Msg err error ) if c.lookupWithCtx != nil { addresses, response, err = c.lookupWithCtx(ctx, transport, FqdnToDomain(message.Question[0].Name), adapter.DNSQueryOptions{}) } else if c.lookup != nil { addresses, response, err = c.lookup(transport, FqdnToDomain(message.Question[0].Name), adapter.DNSQueryOptions{}) } else { return nil, E.New("unused client exchange") } if err != nil { return nil, err } if response != nil { return response, nil } return FixedResponse(0, message.Question[0], addresses, 60), nil } return c.exchange(transport, message) } func (c *fakeDNSClient) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(*mDNS.Msg) bool) ([]netip.Addr, error) { if c.lookup == nil && c.lookupWithCtx == nil { return nil, E.New("unused client lookup") } var ( addresses []netip.Addr response *mDNS.Msg err error ) if c.lookupWithCtx != nil { addresses, response, err = c.lookupWithCtx(ctx, transport, domain, options) } else { addresses, response, err = c.lookup(transport, domain, options) } if err != nil { return nil, err } if response == nil { response = FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), addresses, 60) } if responseChecker != nil && !responseChecker(response) { return nil, ErrResponseRejected } if addresses != nil { return addresses, nil } return MessageToAddresses(response), nil } func (c *fakeDNSClient) ClearCache() {} func newTestRouter(t *testing.T, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router { router := newTestRouterWithContext(t, context.Background(), rules, transportManager, client) t.Cleanup(func() { router.Close() }) return router } func newTestRouterWithContext(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router { return newTestRouterWithContextAndLogger(t, ctx, rules, transportManager, client, log.NewNOPFactory().NewLogger("dns")) } func newTestRouterWithContextAndLogger(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient, dnsLogger log.ContextLogger) *Router { t.Helper() router := &Router{ ctx: ctx, logger: dnsLogger, transport: transportManager, client: client, rawRules: make([]option.DNSRule, 0, len(rules)), rules: make([]adapter.DNSRule, 0, len(rules)), defaultDomainStrategy: C.DomainStrategyAsIS, } if rules != nil { err := router.Initialize(rules) require.NoError(t, err) err = router.Start(adapter.StartStateStart) require.NoError(t, err) } return router } func waitForLogMessageContaining(t *testing.T, entries <-chan log.Entry, done <-chan struct{}, substring string) log.Entry { t.Helper() timeout := time.After(time.Second) for { select { case entry, ok := <-entries: if !ok { t.Fatal("log subscription closed") } if strings.Contains(entry.Message, substring) { return entry } case <-done: t.Fatal("log subscription closed") case <-timeout: t.Fatalf("timed out waiting for log message containing %q", substring) } } } func fixedQuestion(name string, qType uint16) mDNS.Question { return mDNS.Question{ Name: mDNS.Fqdn(name), Qtype: qType, Qclass: mDNS.ClassINET, } } func mustRecord(t *testing.T, record string) option.DNSRecordOptions { t.Helper() var value option.DNSRecordOptions require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`))) return value } func TestInitializeRejectsDirectLegacyRuleWhenRuleSetForcesNew(t *testing.T) { t.Parallel() ctx := context.Background() ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ Type: C.RuleSetTypeInline, Tag: "query-set", InlineOptions: option.PlainRuleSet{ Rules: []option.HeadlessRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)}, }, }}, }, }) require.NoError(t, err) ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "query-set": ruleSet, }, }) router := &Router{ ctx: ctx, logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 2), defaultDomainStrategy: C.DomainStrategyAsIS, } err = router.Initialize([]option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"query-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ IPIsPrivate: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "private"}, }, }, }, }) require.ErrorContains(t, err, "Response Match Fields") require.ErrorContains(t, err, "require match_response") } func TestLookupLegacyDNSModeDefersRuleSetDestinationIPMatch(t *testing.T) { t.Parallel() ctx := context.Background() ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ Type: C.RuleSetTypeInline, Tag: "legacy-ipcidr-set", InlineOptions: option.PlainRuleSet{ Rules: []option.HeadlessRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ IPCIDR: badoption.Listable[string]{"10.0.0.0/8"}, }, }}, }, }) require.NoError(t, err) ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "legacy-ipcidr-set": ruleSet, }, }) defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "private"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "private": privateTransport, }, }, &fakeDNSClient{ lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { require.Equal(t, "example.com", domain) require.Equal(t, "private", transport.Tag()) response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60) return MessageToAddresses(response), response, nil }, }) require.True(t, router.legacyDNSMode) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ LookupStrategy: C.DomainStrategyIPv4Only, }) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses) } func TestRuleSetUpdateReleasesOldRuleSetRefs(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{} ctx := service.ContextWith[adapter.Router](context.Background(), &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, }) defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{}) require.Equal(t, 1, fakeSet.refCount()) fakeSet.updateMetadata(adapter.RuleSetMetadata{}) require.Equal(t, 1, fakeSet.refCount()) fakeSet.updateMetadata(adapter.RuleSetMetadata{}) require.Equal(t, 1, fakeSet.refCount()) require.NoError(t, router.Close()) require.Zero(t, fakeSet.refCount()) } func TestValidateRuleSetMetadataUpdateRejectsRuleSetThatWouldDisableLegacyDNSMode(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{ metadata: adapter.RuleSetMetadata{ ContainsIPCIDRRule: true, }, } routerService := &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, } ctx := service.ContextWith[adapter.Router](context.Background(), routerService) router := newTestRouterWithContext(t, ctx, []option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ IPIsPrivate: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil }, }) require.True(t, router.legacyDNSMode) err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ ContainsDNSQueryTypeRule: true, }) require.ErrorContains(t, err, "require match_response") } func TestValidateRuleSetMetadataUpdateRejectsRuleSetOnlyLegacyModeSwitchToNew(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{ metadata: adapter.RuleSetMetadata{ ContainsIPCIDRRule: true, }, } routerService := &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, } ctx := service.ContextWith[adapter.Router](context.Background(), routerService) router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil }, }) require.True(t, router.legacyDNSMode) err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ ContainsIPCIDRRule: true, ContainsDNSQueryTypeRule: true, }) require.ErrorContains(t, err, "Address Filter Fields") } func TestValidateRuleSetMetadataUpdateBeforeStartUsesStartupValidation(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{} routerService := &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, } ctx := service.ContextWith[adapter.Router](context.Background(), routerService) router := &Router{ ctx: ctx, logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 2), rules: make([]adapter.DNSRule, 0, 2), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ IPIsPrivate: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }, }) require.NoError(t, err) require.False(t, router.started) err = router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ ContainsDNSQueryTypeRule: true, }) require.ErrorContains(t, err, "require match_response") } func TestValidateRuleSetMetadataUpdateRejectsRuleSetThatWouldRequireLegacyDNSMode(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{} routerService := &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, } ctx := service.ContextWith[adapter.Router](context.Background(), routerService) router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil, nil }, }) require.False(t, router.legacyDNSMode) err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ ContainsIPCIDRRule: true, }) require.ErrorContains(t, err, "Address Filter Fields") } func TestValidateRuleSetMetadataUpdateAllowsRuleSetThatKeepsNonLegacyDNSMode(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{} routerService := &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, } ctx := service.ContextWith[adapter.Router](context.Background(), routerService) router := newTestRouterWithContext(t, ctx, []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{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{}) require.False(t, router.legacyDNSMode) err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ ContainsIPCIDRRule: true, }) require.NoError(t, err) } func TestValidateRuleSetMetadataUpdateAllowsRelaxingLegacyRequirement(t *testing.T) { t.Parallel() fakeSet := &fakeRuleSet{ metadata: adapter.RuleSetMetadata{ ContainsIPCIDRRule: true, }, } routerService := &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "dynamic-set": fakeSet, }, } ctx := service.ContextWith[adapter.Router](context.Background(), routerService) router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"dynamic-set"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil }, }) require.True(t, router.legacyDNSMode) err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{}) require.NoError(t, err) } func TestCloseWaitsForInFlightLookupUntilContextCancellation(t *testing.T) { t.Parallel() defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} selectedTransport := &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP} lookupStarted := make(chan struct{}) var lookupStartedOnce sync.Once 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.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "selected": selectedTransport, }, }, &fakeDNSClient{ lookupWithCtx: func(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { require.Equal(t, "selected", transport.Tag()) require.Equal(t, "example.com", domain) lookupStartedOnce.Do(func() { close(lookupStarted) }) <-ctx.Done() return nil, nil, ctx.Err() }, }) lookupCtx, cancelLookup := context.WithCancel(context.Background()) defer cancelLookup() var ( lookupErr error closeErr error ) lookupDone := make(chan struct{}) go func() { _, lookupErr = router.Lookup(lookupCtx, "example.com", adapter.DNSQueryOptions{}) close(lookupDone) }() select { case <-lookupStarted: case <-time.After(time.Second): t.Fatal("lookup did not reach DNS client") } closeDone := make(chan struct{}) go func() { closeErr = router.Close() close(closeDone) }() select { case <-closeDone: t.Fatal("close finished before lookup context cancellation") default: } cancelLookup() select { case <-lookupDone: case <-time.After(time.Second): t.Fatal("lookup did not finish after cancellation") } select { case <-closeDone: case <-time.After(time.Second): t.Fatal("close did not finish after lookup cancellation") } require.ErrorIs(t, lookupErr, context.Canceled) require.NoError(t, closeErr) } func TestLookupLegacyDNSModeDefersDirectDestinationIPMatch(t *testing.T) { t.Parallel() defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} client := &fakeDNSClient{ lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { require.Equal(t, "example.com", domain) require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) switch transport.Tag() { case "private": response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60) return MessageToAddresses(response), response, nil case "default": t.Fatal("default transport should not be used when legacy rule matches after response") } return nil, nil, E.New("unexpected transport") }, } router := newTestRouter(t, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ IPIsPrivate: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "private"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "private": privateTransport, }, }, client) require.True(t, router.legacyDNSMode) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ LookupStrategy: C.DomainStrategyIPv4Only, }) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses) } func TestLookupLegacyDNSModeFallsBackAfterRejectedAddressLimitResponse(t *testing.T) { t.Parallel() defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} var lookupAccess sync.Mutex var lookupTags []string recordLookup := func(tag string) { lookupAccess.Lock() lookupTags = append(lookupTags, tag) lookupAccess.Unlock() } currentLookupTags := func() []string { lookupAccess.Lock() defer lookupAccess.Unlock() return append([]string(nil), lookupTags...) } client := &fakeDNSClient{ lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { require.Equal(t, "example.com", domain) require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) recordLookup(transport.Tag()) switch transport.Tag() { case "private": response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60) return MessageToAddresses(response), response, nil case "default": response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60) return MessageToAddresses(response), response, nil } return nil, nil, E.New("unexpected transport") }, } router := newTestRouter(t, []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ IPIsPrivate: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "private"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "private": privateTransport, }, }, client) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ LookupStrategy: C.DomainStrategyIPv4Only, }) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses) require.Equal(t, []string{"private", "default"}, currentLookupTags()) } func TestLookupLegacyDNSModeRuleSetAcceptEmptyDoesNotTreatMismatchAsEmpty(t *testing.T) { t.Parallel() ctx := context.Background() ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ Type: C.RuleSetTypeInline, Tag: "legacy-ipcidr-set", InlineOptions: option.PlainRuleSet{ Rules: []option.HeadlessRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ IPCIDR: badoption.Listable[string]{"10.0.0.0/8"}, }, }}, }, }) require.NoError(t, err) ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ ruleSets: map[string]adapter.RuleSet{ "legacy-ipcidr-set": ruleSet, }, }) defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} var lookupAccess sync.Mutex var lookupTags []string recordLookup := func(tag string) { lookupAccess.Lock() lookupTags = append(lookupTags, tag) lookupAccess.Unlock() } currentLookupTags := func() []string { lookupAccess.Lock() defer lookupAccess.Unlock() return append([]string(nil), lookupTags...) } router := newTestRouterWithContext(t, ctx, []option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"}, RuleSetIPCIDRAcceptEmpty: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "private"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "private": privateTransport, }, }, &fakeDNSClient{ lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { require.Equal(t, "example.com", domain) require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) recordLookup(transport.Tag()) switch transport.Tag() { case "private": response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60) return MessageToAddresses(response), response, nil case "default": response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60) return MessageToAddresses(response), response, nil } return nil, nil, E.New("unexpected transport") }, }) require.True(t, router.legacyDNSMode) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ LookupStrategy: C.DomainStrategyIPv4Only, }) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses) require.Equal(t, []string{"private", "default"}, currentLookupTags()) } func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRoute(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")}, 60), nil case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil default: return nil, E.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. 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 TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRcodeRoute(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 &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Response: true, Rcode: mDNS.RcodeNameError, }, Question: []mDNS.Question{message.Question[0]}, }, nil case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil default: return nil, E.New("unexpected transport") } }, } rcode := option.DNSRCode(mDNS.RcodeNameError) 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, ResponseRcode: &rcode, }, 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 TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseNsRoute(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}, }, } nsRecord := mustRecord(t, "example.com. IN NS ns1.example.com.") client := &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { switch transport.Tag() { case "upstream": return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Response: true, Rcode: mDNS.RcodeSuccess, }, Question: []mDNS.Question{message.Question[0]}, Ns: []mDNS.RR{nsRecord.Build()}, }, nil case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil default: return nil, E.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, ResponseNs: badoption.Listable[option.DNSRecordOptions]{nsRecord}, }, 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 TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseExtraRoute(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}, }, } extraRecord := mustRecord(t, "ns1.example.com. IN A 192.0.2.53") client := &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { switch transport.Tag() { case "upstream": return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Response: true, Rcode: mDNS.RcodeSuccess, }, Question: []mDNS.Question{message.Question[0]}, Extra: []mDNS.RR{extraRecord.Build()}, }, nil case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil default: return nil, E.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, ResponseExtra: badoption.Listable[option.DNSRecordOptions]{extraRecord}, }, 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 TestExchangeLegacyDNSModeDisabledEvaluateDoesNotLeakAddressesToNextQuery(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}, }, } var inspectedSelected bool client := &fakeDNSClient{ beforeExchange: func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg) { if transport.Tag() != "selected" { return } inspectedSelected = true metadata := adapter.ContextFrom(ctx) require.NotNil(t, metadata) require.Empty(t, metadata.DestinationAddresses) require.NotNil(t, metadata.DNSResponse) }, 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, E.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. 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.True(t, inspectedSelected) require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) } func TestExchangeLegacyDNSModeDisabledEvaluateRouteResolutionFailureClearsResponse(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")}, 60), nil case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil case "default": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil default: return nil, E.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{ Domain: badoption.Listable[string]{"example.com"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeEvaluate, RouteOptions: option.DNSRouteActionOptions{Server: "missing"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. 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("4.4.4.4")}, MessageToAddresses(response)) } func TestExchangeLegacyDNSModeDisabledSecondEvaluateOverwritesFirstResponse(t *testing.T) { t.Parallel() transportManager := &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "first-upstream": &fakeDNSTransport{tag: "first-upstream", transportType: C.DNSTypeUDP}, "second-upstream": &fakeDNSTransport{tag: "second-upstream", transportType: C.DNSTypeUDP}, "first-match": &fakeDNSTransport{tag: "first-match", transportType: C.DNSTypeUDP}, "second-match": &fakeDNSTransport{tag: "second-match", transportType: C.DNSTypeUDP}, }, } client := &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { switch transport.Tag() { case "first-upstream": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil case "second-upstream": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil case "first-match": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("7.7.7.7")}, 60), nil case "second-match": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil case "default": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil default: return nil, E.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: "first-upstream"}, }, }, }, { 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: "second-upstream"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "first-match"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 2.2.2.2")}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "second-match"}, }, }, }, } 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 TestExchangeLegacyDNSModeDisabledEvaluateExchangeFailureUsesMatchResponseBooleanSemantics(t *testing.T) { t.Parallel() testCases := []struct { name string invert bool expectedAddr netip.Addr }{ { name: "plain match_response rule stays false", expectedAddr: netip.MustParseAddr("4.4.4.4"), }, { name: "invert match_response rule becomes true", invert: true, expectedAddr: netip.MustParseAddr("8.8.8.8"), }, } for _, testCase := range testCases { t.Run(testCase.name, func(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 nil, E.New("upstream exchange failed") case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil case "default": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil default: return nil, E.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, Invert: testCase.invert, }, 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{testCase.expectedAddr}, MessageToAddresses(response)) }) } } func TestExchangeLegacyDNSModeDisabledRespondReturnsEvaluatedResponse(t *testing.T) { t.Parallel() var exchanges []string 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: "upstream"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRespond, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { exchanges = append(exchanges, transport.Tag()) require.Equal(t, "upstream", transport.Tag()) return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil }, }) require.False(t, router.legacyDNSMode) 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, []string{"upstream"}, exchanges) require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, MessageToAddresses(response)) } func TestLookupLegacyDNSModeDisabledRespondReturnsEvaluatedResponse(t *testing.T) { t.Parallel() 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: "upstream"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRespond, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { require.Equal(t, "upstream", transport.Tag()) switch message.Question[0].Qtype { case mDNS.TypeA: return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil case mDNS.TypeAAAA: return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil default: return nil, E.New("unexpected qtype") } }, }) require.False(t, router.legacyDNSMode) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) require.NoError(t, err) require.Equal(t, []netip.Addr{ netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2001:db8::1"), }, addresses) } func TestExchangeLegacyDNSModeDisabledRespondWithoutEvaluatedResponseReturnsError(t *testing.T) { t.Parallel() 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: "upstream"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRespond, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, _ *mDNS.Msg) (*mDNS.Msg, error) { require.Equal(t, "upstream", transport.Tag()) return nil, E.New("upstream exchange failed") }, }) require.False(t, router.legacyDNSMode) response, err := router.Exchange(context.Background(), &mDNS.Msg{ Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, }, adapter.DNSQueryOptions{}) require.Nil(t, response) require.ErrorContains(t, err, dnsRespondMissingResponseMessage) } func TestLookupLegacyDNSModeDisabledAllowsPartialSuccessForExchangeFailure(t *testing.T) { t.Parallel() defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} router := newTestRouter(t, nil, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { require.Equal(t, "default", transport.Tag()) switch message.Question[0].Qtype { case mDNS.TypeA: return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil case mDNS.TypeAAAA: return nil, E.New("ipv6 failed") default: return nil, E.New("unexpected qtype") } }, }) router.legacyDNSMode = false addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) } func TestLookupLegacyDNSModeDisabledAllowsPartialSuccessForRcodeError(t *testing.T) { t.Parallel() defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} router := newTestRouter(t, nil, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { require.Equal(t, "default", transport.Tag()) switch message.Question[0].Qtype { case mDNS.TypeA: return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil case mDNS.TypeAAAA: return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Response: true, Rcode: mDNS.RcodeNameError, }, Question: []mDNS.Question{message.Question[0]}, }, nil default: return nil, E.New("unexpected qtype") } }, }) router.legacyDNSMode = false addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) } func TestLookupLegacyDNSModeDisabledSkipsFakeIPRule(t *testing.T) { t.Parallel() 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.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, "fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { require.Equal(t, "default", transport.Tag()) if message.Question[0].Qtype == mDNS.TypeA { return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil } return FixedResponse(0, message.Question[0], nil, 60), nil }, }) router.legacyDNSMode = false addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) require.NoError(t, err) require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses) } func TestExchangeLegacyDNSModeDisabledAllowsRouteFakeIPRule(t *testing.T) { t.Parallel() fakeTransport := &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP} 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.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, }, }, }}, &fakeDNSTransportManager{ defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, transports: map[string]adapter.DNSTransport{ "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, "fake": fakeTransport, }, }, &fakeDNSClient{ exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { require.Same(t, fakeTransport, transport) return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("198.18.0.1")}, 60), nil }, }) 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("198.18.0.1")}, MessageToAddresses(response)) } func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByEvaluate(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]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", Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), }, }, }, }}) require.ErrorContains(t, err, "strategy") require.ErrorContains(t, err, "deprecated") } func TestInitializeRejectsEvaluateFakeIPServerInDefaultRule(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{transports: map[string]adapter.DNSTransport{"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}}}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]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: "fake"}, }, }, }}) require.ErrorContains(t, err, "evaluate action cannot use fakeip server") require.ErrorContains(t, err, "fake") } func TestInitializeRejectsEvaluateFakeIPServerInLogicalRule(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{transports: map[string]adapter.DNSTransport{"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}}}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalDNSRule{ RawLogicalDNSRule: option.RawLogicalDNSRule{ Mode: C.LogicalTypeOr, 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: "fake"}, }, }, }}) require.ErrorContains(t, err, "evaluate action cannot use fakeip server") require.ErrorContains(t, err, "fake") } func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByMatchResponse(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRouteOptions, RouteOptionsOptions: option.DNSRouteOptionsActionOptions{ Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), }, }, }, }}) require.ErrorContains(t, err, "strategy") require.ErrorContains(t, err, "deprecated") } func TestInitializeRejectsDNSMatchResponseWithoutPrecedingEvaluate(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }}) require.ErrorContains(t, err, "preceding evaluate action") } func TestInitializeRejectsDNSRespondWithoutPrecedingEvaluate(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRespond, }, }, }}) require.ErrorContains(t, err, "preceding evaluate action") } func TestInitializeRejectsLogicalDNSRespondWithoutPrecedingEvaluate(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalDNSRule{ RawLogicalDNSRule: option.RawLogicalDNSRule{ Mode: C.LogicalTypeOr, Rules: []option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, }, }}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRespond, }, }, }}) require.ErrorContains(t, err, "preceding evaluate action") } func TestInitializeRejectsEvaluateRuleWithResponseMatchWithoutPrecedingEvaluate(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 1), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalDNSRule{ RawLogicalDNSRule: option.RawLogicalDNSRule{ Mode: C.LogicalTypeOr, Rules: []option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, }, }, }, }, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeEvaluate, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }}) require.ErrorContains(t, err, "preceding evaluate action") } func TestInitializeAllowsEvaluateRuleWithResponseMatchAfterPrecedingEvaluate(t *testing.T) { t.Parallel() router := &Router{ ctx: context.Background(), logger: log.NewNOPFactory().NewLogger("dns"), transport: &fakeDNSTransportManager{}, client: &fakeDNSClient{}, rawRules: make([]option.DNSRule, 0, 2), defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"bootstrap.example"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeEvaluate, RouteOptions: option.DNSRouteActionOptions{Server: "bootstrap"}, }, }, }, { Type: C.RuleTypeLogical, LogicalOptions: option.LogicalDNSRule{ RawLogicalDNSRule: option.RawLogicalDNSRule{ Mode: C.LogicalTypeOr, Rules: []option.DNSRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ MatchResponse: true, ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, }, }, }, }, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeEvaluate, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }, }) require.NoError(t, err) } func TestLookupLegacyDNSModeDisabledReturnsRejectedErrorForRejectAction(t *testing.T) { t.Parallel() 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.RuleActionTypeReject, RejectOptions: option.RejectActionOptions{ Method: C.RuleActionRejectMethodDefault, }, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{}) require.False(t, router.legacyDNSMode) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) require.Nil(t, addresses) require.Error(t, err) require.True(t, rulepkg.IsRejected(err)) } func TestExchangeLegacyDNSModeDisabledReturnsRefusedResponseForRejectAction(t *testing.T) { t.Parallel() 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.RuleActionTypeReject, RejectOptions: option.RejectActionOptions{ Method: C.RuleActionRejectMethodDefault, }, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{}) require.False(t, router.legacyDNSMode) 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, mDNS.RcodeRefused, response.Rcode) require.Equal(t, []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, response.Question) } func TestExchangeLegacyDNSModeDisabledReturnsDropErrorForRejectDropAction(t *testing.T) { t.Parallel() 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.RuleActionTypeReject, RejectOptions: option.RejectActionOptions{ Method: C.RuleActionRejectMethodDrop, }, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{}) require.False(t, router.legacyDNSMode) response, err := router.Exchange(context.Background(), &mDNS.Msg{ Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, }, adapter.DNSQueryOptions{}) require.Nil(t, response) require.ErrorIs(t, err, tun.ErrDrop) } func TestLookupLegacyDNSModeDisabledFiltersPerQueryTypeAddressesBeforeMerging(t *testing.T) { t.Parallel() 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.RuleActionTypePredefined, PredefinedOptions: option.DNSRouteActionPredefined{ Answer: badoption.Listable[option.DNSRecordOptions]{ mustRecord(t, "example.com. IN A 1.1.1.1"), mustRecord(t, "example.com. IN AAAA 2001:db8::1"), }, }, }, }, }, }, &fakeDNSTransportManager{ defaultTransport: defaultTransport, transports: map[string]adapter.DNSTransport{ "default": defaultTransport, }, }, &fakeDNSClient{}) require.False(t, router.legacyDNSMode) addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) require.NoError(t, err) require.Equal(t, []netip.Addr{ netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2001:db8::1"), }, addresses) } func TestExchangeLegacyDNSModeDisabledLogicalMatchResponseIPCIDRFallsThrough(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("9.9.9.9")}, 60), nil case "selected": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil case "default": return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil default: return nil, E.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("4.4.4.4")}, MessageToAddresses(response)) } func TestLegacyDNSModeReportsLegacyAddressFilterDeprecation(t *testing.T) { t.Parallel() manager := &fakeDeprecatedManager{} ctx := service.ContextWith[deprecated.Manager](context.Background(), manager) router := &Router{ ctx: ctx, logger: log.NewNOPFactory().NewLogger("dns"), client: &fakeDNSClient{}, defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{Server: "default"}, }, }, }}) require.NoError(t, err) err = router.Start(adapter.StartStateStart) require.NoError(t, err) require.Len(t, manager.features, 1) require.Equal(t, deprecated.OptionLegacyDNSAddressFilter.Name, manager.features[0].Name) } func TestLegacyDNSModeReportsDNSRuleStrategyDeprecation(t *testing.T) { t.Parallel() manager := &fakeDeprecatedManager{} ctx := service.ContextWith[deprecated.Manager](context.Background(), manager) router := &Router{ ctx: ctx, logger: log.NewNOPFactory().NewLogger("dns"), client: &fakeDNSClient{}, defaultDomainStrategy: C.DomainStrategyAsIS, } err := router.Initialize([]option.DNSRule{{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ RawDefaultDNSRule: option.RawDefaultDNSRule{ Domain: badoption.Listable[string]{"example.com"}, }, DNSRuleAction: option.DNSRuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.DNSRouteActionOptions{ Server: "default", Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), }, }, }, }}) require.NoError(t, err) err = router.Start(adapter.StartStateStart) require.NoError(t, err) require.Len(t, manager.features, 1) require.Equal(t, deprecated.OptionLegacyDNSRuleStrategy.Name, manager.features[0].Name) }