From 1cef517a97f3154a78d81365f63fa8f5992c242a Mon Sep 17 00:00:00 2001 From: pascal Date: Fri, 21 Aug 2026 16:05:51 +0200 Subject: [PATCH] include NonAuthoritative zone in tests --- client/internal/dns_test.go | 87 +++++++++++++++++++ .../network_map_db/network_map_data.sql | 6 ++ .../network_map_data_golden.json | 26 ++++++ .../testdata/cases/dns-config/case.json | 2 +- .../cases/dns-config/golden/peer-a.json | 21 +++++ .../cases/dns-config/golden/peer-c.json | 13 +++ .../testdata/cases/dns-config/nmdata.json | 22 +++++ 7 files changed, 176 insertions(+), 1 deletion(-) diff --git a/client/internal/dns_test.go b/client/internal/dns_test.go index e15cc8fb7..031431efe 100644 --- a/client/internal/dns_test.go +++ b/client/internal/dns_test.go @@ -8,7 +8,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/client/iface/wgaddr" nbdns "github.com/netbirdio/netbird/dns" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) func TestCreatePTRRecord_IPv4(t *testing.T) { @@ -136,3 +138,88 @@ func TestAddReverseZone_IPv6(t *testing.T) { assert.Len(t, reverseZone.Records, 1) assert.Equal(t, int(dns.TypePTR), reverseZone.Records[0].Type) } + +// TestToDNSConfig_ZoneFlagsPreserved pins the per-zone NonAuthoritative flag +// through the legacy DNSConfig path. A non-authoritative zone is match-only: +// the local resolver falls through to the upstream for an in-zone name it does +// not define. The built-in peer zone is the authoritative one and must stay +// that way, so the flag has to travel per zone rather than be derived. +func TestToDNSConfig_ZoneFlagsPreserved(t *testing.T) { + config := toDNSConfig(&mgmProto.DNSConfig{ + ServiceEnable: true, + CustomZones: []*mgmProto.CustomZone{ + { + Domain: "netbird.cloud.", + Records: []*mgmProto.SimpleRecord{ + {Name: "peer1.netbird.cloud.", Type: int64(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "100.64.0.1"}, + }, + }, + { + Domain: "corp.internal.", + NonAuthoritative: true, + SearchDomainDisabled: true, + Records: []*mgmProto.SimpleRecord{ + {Name: "db.corp.internal.", Type: int64(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.10.0.5"}, + }, + }, + }, + }, wgaddr.Address{ + IP: netip.MustParseAddr("100.64.0.1"), + Network: netip.MustParsePrefix("100.64.0.0/16"), + }) + + zones := make(map[string]nbdns.CustomZone, len(config.CustomZones)) + for _, zone := range config.CustomZones { + zones[zone.Domain] = zone + } + + peerZone, ok := zones["netbird.cloud."] + require.True(t, ok, "peer zone must survive") + assert.False(t, peerZone.NonAuthoritative, "the built-in peer zone owns the account domain and stays authoritative") + + accountZone, ok := zones["corp.internal."] + require.True(t, ok, "account zone must survive") + assert.True(t, accountZone.NonAuthoritative, "an account zone stays match-only, else undefined in-zone names get black-holed") + assert.True(t, accountZone.SearchDomainDisabled) +} + +// TestToDNSConfig_SingleZoneForcedAuthoritative pins the compatibility clause +// in toDNSConfig: a config carrying exactly one zone is treated as +// authoritative no matter what the server said, because servers that predate +// the NonAuthoritative field send only the peer FQDN zone. +// +// The clause can only ever downgrade an explicit true to false, so a server +// that legitimately sends a single non-authoritative zone — an account whose +// only zone is a custom one, with no peer records to build the built-in zone +// from — gets that zone's whole apex black-holed on the client. Real accounts +// always carry the peer zone alongside, which is why this is latent. Narrowing +// it needs a way to tell "unset" from "false" on the wire, or the account +// domain passed down here; until then this test states the contract so a +// change to it is deliberate. +func TestToDNSConfig_SingleZoneForcedAuthoritative(t *testing.T) { + config := toDNSConfig(&mgmProto.DNSConfig{ + ServiceEnable: true, + CustomZones: []*mgmProto.CustomZone{ + { + Domain: "corp.internal.", + NonAuthoritative: true, + Records: []*mgmProto.SimpleRecord{ + {Name: "db.corp.internal.", Type: int64(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.10.0.5"}, + }, + }, + }, + }, wgaddr.Address{ + IP: netip.MustParseAddr("100.64.0.1"), + Network: netip.MustParsePrefix("100.64.0.0/16"), + }) + + require.NotEmpty(t, config.CustomZones) + assert.Equal(t, "corp.internal.", config.CustomZones[0].Domain) + assert.False(t, config.CustomZones[0].NonAuthoritative, + "a lone zone is forced authoritative for pre-NonAuthoritative servers") + + // The reverse zone the config gains afterwards must not feed back into the + // decision: the compat gate counts the zones the server sent. + require.Len(t, config.CustomZones, 2, "a reverse zone is appended for the overlay prefix") + assert.Equal(t, "64.100.in-addr.arpa.", config.CustomZones[1].Domain) +} diff --git a/integration_tests/management/network_map_db/network_map_data.sql b/integration_tests/management/network_map_db/network_map_data.sql index 9f8e1bdb8..d94e2f4aa 100644 --- a/integration_tests/management/network_map_db/network_map_data.sql +++ b/integration_tests/management/network_map_db/network_map_data.sql @@ -47,8 +47,14 @@ insert into zones (id, account_id, domain, enabled, enable_search_domain, distri VALUES('zone-331','account-33','test-331.com',true,true,'["33-group-one-resource-id"]'); insert into zones (id, account_id, domain, enabled, enable_search_domain, distribution_groups) VALUES('zone-332','account-33','disabled-331.com',false,true,'["33-group-one-resource-id"]'); +insert into zones (id, account_id, domain, enabled, enable_search_domain, distribution_groups) + VALUES('zone-333','account-33','search-off-331.com',true,false,'["33-group-two-resources-id"]'); insert into records (id, account_id, zone_id, name, type, ttl, content) VALUES('record-333','account-33','zone-332','test.disabled-331.com','A',1800,'1.1.1.9'); +insert into records (id, account_id, zone_id, name, type, ttl, content) + VALUES('record-334','account-33','zone-333','test.search-off-331.com','A',1800,'1.1.1.3'); +insert into records (id, account_id, zone_id, name, type, ttl, content) + VALUES('record-335','account-33','zone-333','alias.search-off-331.com','CNAME',1800,'test.search-off-331.com'); insert into records (id, account_id, zone_id, name, type, ttl, content) VALUES('record-331','account-33','zone-331','test.test-331.com','A',1800,'1.1.1.1'); insert into records (id, account_id, zone_id, name, type, ttl, content) diff --git a/integration_tests/management/network_map_db/network_map_data_golden.json b/integration_tests/management/network_map_db/network_map_data_golden.json index cff044d89..bb0ccd30b 100644 --- a/integration_tests/management/network_map_db/network_map_data_golden.json +++ b/integration_tests/management/network_map_db/network_map_data_golden.json @@ -513,6 +513,32 @@ "SearchDomainDisabled": false, "NonAuthoritative": true } + }, + { + "DistributionGroups": [ + "33-group-two-resources-id" + ], + "Zone": { + "Domain": "search-off-331.com.", + "Records": [ + { + "Name": "test.search-off-331.com.", + "Type": 1, + "Class": "IN", + "TTL": 1800, + "RData": "1.1.1.3" + }, + { + "Name": "alias.search-off-331.com.", + "Type": 5, + "Class": "IN", + "TTL": 1800, + "RData": "test.search-off-331.com." + } + ], + "SearchDomainDisabled": true, + "NonAuthoritative": true + } } ], "PrivateServiceCandidates": null, diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/case.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/case.json index 4844b02f2..e4c46c63d 100644 --- a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/case.json +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/case.json @@ -1,5 +1,5 @@ { - "description": "Nameserver group and applied custom zone distributed to grp-dev; peer-a (with an extra DNS label) receives them, peer-c outside the group receives neither.", + "description": "Nameserver group and applied custom zones distributed to grp-dev; peer-a (with an extra DNS label) receives them, peer-c is outside that group and receives only the zone distributed to grp-ops. Zone flags travel per zone: both grp-dev zones are match-only (NonAuthoritative), only search-off.internal. disables the search domain, and the built-in peer zone stays authoritative.", "peers": [ "peer-a", "peer-c" diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-a.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-a.json index a06f3c42d..f06a19d9d 100644 --- a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-a.json +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-a.json @@ -45,6 +45,27 @@ } ] }, + { + "Domain": "search-off.internal.", + "SearchDomainDisabled": true, + "NonAuthoritative": true, + "Records": [ + { + "Name": "alias.search-off.internal.", + "Type": "5", + "Class": "IN", + "TTL": "300", + "RData": "app.search-off.internal." + }, + { + "Name": "app.search-off.internal.", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "10.10.0.6" + } + ] + }, { "Domain": "netbird.test.", "Records": [ diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-c.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-c.json index cdb77d464..7e04dca40 100644 --- a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-c.json +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/golden/peer-c.json @@ -22,6 +22,19 @@ "RData": "100.64.0.3" } ] + }, + { + "Domain": "ops-only.internal.", + "NonAuthoritative": true, + "Records": [ + { + "Name": "tool.ops-only.internal.", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "10.10.0.7" + } + ] } ], "ForwarderPort": "22054" diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/nmdata.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/nmdata.json index 6efb3835b..b9741ef16 100644 --- a/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/nmdata.json +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/nmdata.json @@ -47,6 +47,28 @@ {"Name": "db.corp.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.5"} ] } + }, + { + "DistributionGroups": ["grp-dev"], + "Zone": { + "Domain": "search-off.internal.", + "NonAuthoritative": true, + "SearchDomainDisabled": true, + "Records": [ + {"Name": "app.search-off.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.6"}, + {"Name": "alias.search-off.internal.", "Type": 5, "Class": "IN", "TTL": 300, "RData": "app.search-off.internal."} + ] + } + }, + { + "DistributionGroups": ["grp-ops"], + "Zone": { + "Domain": "ops-only.internal.", + "NonAuthoritative": true, + "Records": [ + {"Name": "tool.ops-only.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.7"} + ] + } } ] } \ No newline at end of file