@@ -3,6 +3,7 @@ package dnsfwd
3
3
import (
4
4
"context"
5
5
"fmt"
6
+ "net"
6
7
"net/netip"
7
8
"strings"
8
9
"testing"
@@ -16,8 +17,8 @@ import (
16
17
firewall "github.com/netbirdio/netbird/client/firewall/manager"
17
18
"github.com/netbirdio/netbird/client/internal/dns/test"
18
19
"github.com/netbirdio/netbird/client/internal/peer"
19
- "github.com/netbirdio/netbird/shared/management/domain"
20
20
"github.com/netbirdio/netbird/route"
21
+ "github.com/netbirdio/netbird/shared/management/domain"
21
22
)
22
23
23
24
func Test_getMatchingEntries (t * testing.T ) {
@@ -708,6 +709,131 @@ func TestDNSForwarder_MultipleOverlappingPatterns(t *testing.T) {
708
709
assert .Len (t , matches , 3 , "Should match 3 patterns" )
709
710
}
710
711
712
+ // TestDNSForwarder_NodataVsNxdomain tests that the forwarder correctly distinguishes
713
+ // between NXDOMAIN (domain doesn't exist) and NODATA (domain exists but no records of that type)
714
+ func TestDNSForwarder_NodataVsNxdomain (t * testing.T ) {
715
+ mockFirewall := & MockFirewall {}
716
+ mockResolver := & MockResolver {}
717
+
718
+ forwarder := NewDNSForwarder ("127.0.0.1:0" , 300 , mockFirewall , & peer.Status {})
719
+ forwarder .resolver = mockResolver
720
+
721
+ d , err := domain .FromString ("example.com" )
722
+ require .NoError (t , err )
723
+
724
+ set := firewall .NewDomainSet ([]domain.Domain {d })
725
+ entries := []* ForwarderEntry {{Domain : d , ResID : "test-res" , Set : set }}
726
+ forwarder .UpdateDomains (entries )
727
+
728
+ tests := []struct {
729
+ name string
730
+ queryType uint16
731
+ setupMocks func ()
732
+ expectedCode int
733
+ expectNoAnswer bool // true if we expect NOERROR with empty answer (NODATA case)
734
+ description string
735
+ }{
736
+ {
737
+ name : "domain exists but no AAAA records (NODATA)" ,
738
+ queryType : dns .TypeAAAA ,
739
+ setupMocks : func () {
740
+ // First query for AAAA returns not found
741
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip6" , "example.com." ).
742
+ Return ([]netip.Addr {}, & net.DNSError {IsNotFound : true , Name : "example.com" }).Once ()
743
+ // Check query for A records succeeds (domain exists)
744
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip4" , "example.com." ).
745
+ Return ([]netip.Addr {netip .MustParseAddr ("1.2.3.4" )}, nil ).Once ()
746
+ },
747
+ expectedCode : dns .RcodeSuccess ,
748
+ expectNoAnswer : true ,
749
+ description : "Should return NOERROR when domain exists but has no records of requested type" ,
750
+ },
751
+ {
752
+ name : "domain exists but no A records (NODATA)" ,
753
+ queryType : dns .TypeA ,
754
+ setupMocks : func () {
755
+ // First query for A returns not found
756
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip4" , "example.com." ).
757
+ Return ([]netip.Addr {}, & net.DNSError {IsNotFound : true , Name : "example.com" }).Once ()
758
+ // Check query for AAAA records succeeds (domain exists)
759
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip6" , "example.com." ).
760
+ Return ([]netip.Addr {netip .MustParseAddr ("2001:db8::1" )}, nil ).Once ()
761
+ },
762
+ expectedCode : dns .RcodeSuccess ,
763
+ expectNoAnswer : true ,
764
+ description : "Should return NOERROR when domain exists but has no A records" ,
765
+ },
766
+ {
767
+ name : "domain doesn't exist (NXDOMAIN)" ,
768
+ queryType : dns .TypeA ,
769
+ setupMocks : func () {
770
+ // First query for A returns not found
771
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip4" , "example.com." ).
772
+ Return ([]netip.Addr {}, & net.DNSError {IsNotFound : true , Name : "example.com" }).Once ()
773
+ // Check query for AAAA also returns not found (domain doesn't exist)
774
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip6" , "example.com." ).
775
+ Return ([]netip.Addr {}, & net.DNSError {IsNotFound : true , Name : "example.com" }).Once ()
776
+ },
777
+ expectedCode : dns .RcodeNameError ,
778
+ expectNoAnswer : true ,
779
+ description : "Should return NXDOMAIN when domain doesn't exist at all" ,
780
+ },
781
+ {
782
+ name : "domain exists with records (normal success)" ,
783
+ queryType : dns .TypeA ,
784
+ setupMocks : func () {
785
+ mockResolver .On ("LookupNetIP" , mock .Anything , "ip4" , "example.com." ).
786
+ Return ([]netip.Addr {netip .MustParseAddr ("1.2.3.4" )}, nil ).Once ()
787
+ // Expect firewall update for successful resolution
788
+ expectedPrefix := netip .PrefixFrom (netip .MustParseAddr ("1.2.3.4" ), 32 )
789
+ mockFirewall .On ("UpdateSet" , set , []netip.Prefix {expectedPrefix }).Return (nil ).Once ()
790
+ },
791
+ expectedCode : dns .RcodeSuccess ,
792
+ expectNoAnswer : false ,
793
+ description : "Should return NOERROR with answer when records exist" ,
794
+ },
795
+ }
796
+
797
+ for _ , tt := range tests {
798
+ t .Run (tt .name , func (t * testing.T ) {
799
+ // Reset mock expectations
800
+ mockResolver .ExpectedCalls = nil
801
+ mockResolver .Calls = nil
802
+ mockFirewall .ExpectedCalls = nil
803
+ mockFirewall .Calls = nil
804
+
805
+ tt .setupMocks ()
806
+
807
+ query := & dns.Msg {}
808
+ query .SetQuestion (dns .Fqdn ("example.com" ), tt .queryType )
809
+
810
+ var writtenResp * dns.Msg
811
+ mockWriter := & test.MockResponseWriter {
812
+ WriteMsgFunc : func (m * dns.Msg ) error {
813
+ writtenResp = m
814
+ return nil
815
+ },
816
+ }
817
+
818
+ resp := forwarder .handleDNSQuery (mockWriter , query )
819
+
820
+ // If a response was returned, it means it should be written (happens in wrapper functions)
821
+ if resp != nil && writtenResp == nil {
822
+ writtenResp = resp
823
+ }
824
+
825
+ require .NotNil (t , writtenResp , "Expected response to be written" )
826
+ assert .Equal (t , tt .expectedCode , writtenResp .Rcode , tt .description )
827
+
828
+ if tt .expectNoAnswer {
829
+ assert .Empty (t , writtenResp .Answer , "Response should have no answer records" )
830
+ }
831
+
832
+ mockResolver .AssertExpectations (t )
833
+ })
834
+ }
835
+ }
836
+
711
837
func TestDNSForwarder_EmptyQuery (t * testing.T ) {
712
838
// Test handling of malformed query with no questions
713
839
forwarder := NewDNSForwarder ("127.0.0.1:0" , 300 , nil , & peer.Status {})
0 commit comments