diff --git a/client/internal/engine.go b/client/internal/engine.go index bebf04f6c0e..a6dd3cebd3d 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -186,6 +186,9 @@ type Engine struct { dnsServer dns.Server + // lastDNSConfig stores the last applied DNS configuration + lastDNSConfig *nbdns.Config + // checks are the client-applied posture checks that need to be evaluated on the client checks []*mgmProto.Checks @@ -1060,8 +1063,13 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { protoDNSConfig = &mgmProto.DNSConfig{} } - if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil { - log.Errorf("failed to update dns server, err: %v", err) + newDNSConfig := toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network) + if !dnsConfigsEqual(e.lastDNSConfig, &newDNSConfig) { + if err := e.dnsServer.UpdateDNSServer(serial, newDNSConfig); err != nil { + log.Errorf("failed to update dns server, err: %v", err) + } else { + e.lastDNSConfig = &newDNSConfig + } } // apply routes first, route related actions might depend on routing being enabled @@ -1255,6 +1263,87 @@ func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network netip.Prefix) nbdns return dnsUpdate } +func dnsConfigsEqual(a, b *nbdns.Config) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + if a.ServiceEnable != b.ServiceEnable { + return false + } + if len(a.CustomZones) != len(b.CustomZones) { + return false + } + if len(a.NameServerGroups) != len(b.NameServerGroups) { + return false + } + + if !customZonesEqual(a.CustomZones, b.CustomZones) { + return false + } + + if !nameServerGroupsEqual(a.NameServerGroups, b.NameServerGroups) { + return false + } + + return true +} + +func customZonesEqual(a, b []nbdns.CustomZone) bool { + for i, zoneA := range a { + zoneB := b[i] + if zoneA.Domain != zoneB.Domain { + return false + } + if len(zoneA.Records) != len(zoneB.Records) { + return false + } + for j, recordA := range zoneA.Records { + recordB := zoneB.Records[j] + if recordA.Name != recordB.Name || + recordA.Type != recordB.Type || + recordA.Class != recordB.Class || + recordA.TTL != recordB.TTL || + recordA.RData != recordB.RData { + return false + } + } + } + return true +} + +func nameServerGroupsEqual(a, b []*nbdns.NameServerGroup) bool { + for i, nsGroupA := range a { + nsGroupB := b[i] + if nsGroupA.Primary != nsGroupB.Primary || + nsGroupA.SearchDomainsEnabled != nsGroupB.SearchDomainsEnabled { + return false + } + if len(nsGroupA.Domains) != len(nsGroupB.Domains) { + return false + } + for j, domainA := range nsGroupA.Domains { + if domainA != nsGroupB.Domains[j] { + return false + } + } + if len(nsGroupA.NameServers) != len(nsGroupB.NameServers) { + return false + } + for j, nsA := range nsGroupA.NameServers { + nsB := nsGroupB.NameServers[j] + if nsA.IP != nsB.IP || + nsA.NSType != nsB.NSType || + nsA.Port != nsB.Port { + return false + } + } + } + return true +} + func (e *Engine) updateOfflinePeers(offlinePeers []*mgmProto.RemotePeerConfig) { replacement := make([]peer.State, len(offlinePeers)) for i, offlinePeer := range offlinePeers { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 2f1098100ac..f453aa9eaec 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1624,3 +1624,462 @@ func getPeers(e *Engine) int { return len(e.peerStore.PeersPubKey()) } + +func TestDNSConfigsEqual(t *testing.T) { + tests := []struct { + name string + a *nbdns.Config + b *nbdns.Config + expected bool + }{ + { + name: "both nil", + a: nil, + b: nil, + expected: true, + }, + { + name: "one nil", + a: &nbdns.Config{}, + b: nil, + expected: false, + }, + { + name: "empty configs", + a: &nbdns.Config{}, + b: &nbdns.Config{}, + expected: true, + }, + { + name: "different ServiceEnable", + a: &nbdns.Config{ + ServiceEnable: true, + }, + b: &nbdns.Config{ + ServiceEnable: false, + }, + expected: false, + }, + { + name: "same simple config", + a: &nbdns.Config{ + ServiceEnable: true, + CustomZones: []nbdns.CustomZone{}, + }, + b: &nbdns.Config{ + ServiceEnable: true, + CustomZones: []nbdns.CustomZone{}, + }, + expected: true, + }, + { + name: "different number of custom zones", + a: &nbdns.Config{ + CustomZones: []nbdns.CustomZone{ + {Domain: "example.com"}, + }, + }, + b: &nbdns.Config{ + CustomZones: []nbdns.CustomZone{}, + }, + expected: false, + }, + { + name: "same custom zones", + a: &nbdns.Config{ + CustomZones: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test", Type: 1, Class: "IN", TTL: 300, RData: "1.2.3.4"}, + }, + }, + }, + }, + b: &nbdns.Config{ + CustomZones: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test", Type: 1, Class: "IN", TTL: 300, RData: "1.2.3.4"}, + }, + }, + }, + }, + expected: true, + }, + { + name: "different custom zone domains", + a: &nbdns.Config{ + CustomZones: []nbdns.CustomZone{ + {Domain: "example.com"}, + }, + }, + b: &nbdns.Config{ + CustomZones: []nbdns.CustomZone{ + {Domain: "example.org"}, + }, + }, + expected: false, + }, + { + name: "different number of nameserver groups", + a: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + {Primary: true}, + }, + }, + b: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{}, + }, + expected: false, + }, + { + name: "same nameserver groups", + a: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + { + Primary: true, + Domains: []string{"example.com"}, + SearchDomainsEnabled: true, + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + }, + }, + }, + }, + b: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + { + Primary: true, + Domains: []string{"example.com"}, + SearchDomainsEnabled: true, + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + }, + }, + }, + }, + expected: true, + }, + { + name: "different nameserver primary", + a: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + {Primary: true}, + }, + }, + b: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + {Primary: false}, + }, + }, + expected: false, + }, + { + name: "different nameserver IPs", + a: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + }, + }, + }, + }, + b: &nbdns.Config{ + NameServerGroups: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("1.1.1.1"), NSType: 1, Port: 53}, + }, + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := dnsConfigsEqual(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestCustomZonesEqual(t *testing.T) { + tests := []struct { + name string + a []nbdns.CustomZone + b []nbdns.CustomZone + expected bool + }{ + { + name: "both empty", + a: []nbdns.CustomZone{}, + b: []nbdns.CustomZone{}, + expected: true, + }, + { + name: "same single zone", + a: []nbdns.CustomZone{ + {Domain: "example.com"}, + }, + b: []nbdns.CustomZone{ + {Domain: "example.com"}, + }, + expected: true, + }, + { + name: "different domains", + a: []nbdns.CustomZone{ + {Domain: "example.com"}, + }, + b: []nbdns.CustomZone{ + {Domain: "example.org"}, + }, + expected: false, + }, + { + name: "different number of records", + a: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test1"}, + }, + }, + }, + b: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test1"}, + {Name: "test2"}, + }, + }, + }, + expected: false, + }, + { + name: "different record names", + a: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test1", Type: 1, Class: "IN", TTL: 300, RData: "1.2.3.4"}, + }, + }, + }, + b: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test2", Type: 1, Class: "IN", TTL: 300, RData: "1.2.3.4"}, + }, + }, + }, + expected: false, + }, + { + name: "different record types", + a: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test", Type: 1, Class: "IN", TTL: 300, RData: "1.2.3.4"}, + }, + }, + }, + b: []nbdns.CustomZone{ + { + Domain: "example.com", + Records: []nbdns.SimpleRecord{ + {Name: "test", Type: 5, Class: "IN", TTL: 300, RData: "1.2.3.4"}, + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := customZonesEqual(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNameServerGroupsEqual(t *testing.T) { + tests := []struct { + name string + a []*nbdns.NameServerGroup + b []*nbdns.NameServerGroup + expected bool + }{ + { + name: "both empty", + a: []*nbdns.NameServerGroup{}, + b: []*nbdns.NameServerGroup{}, + expected: true, + }, + { + name: "same single group", + a: []*nbdns.NameServerGroup{ + {Primary: true}, + }, + b: []*nbdns.NameServerGroup{ + {Primary: true}, + }, + expected: true, + }, + { + name: "different primary", + a: []*nbdns.NameServerGroup{ + {Primary: true}, + }, + b: []*nbdns.NameServerGroup{ + {Primary: false}, + }, + expected: false, + }, + { + name: "different SearchDomainsEnabled", + a: []*nbdns.NameServerGroup{ + {SearchDomainsEnabled: true}, + }, + b: []*nbdns.NameServerGroup{ + {SearchDomainsEnabled: false}, + }, + expected: false, + }, + { + name: "different number of domains", + a: []*nbdns.NameServerGroup{ + {Domains: []string{"example.com"}}, + }, + b: []*nbdns.NameServerGroup{ + {Domains: []string{"example.com", "example.org"}}, + }, + expected: false, + }, + { + name: "different domains", + a: []*nbdns.NameServerGroup{ + {Domains: []string{"example.com"}}, + }, + b: []*nbdns.NameServerGroup{ + {Domains: []string{"example.org"}}, + }, + expected: false, + }, + { + name: "different number of nameservers", + a: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8")}, + }, + }, + }, + b: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8")}, + {IP: netip.MustParseAddr("1.1.1.1")}, + }, + }, + }, + expected: false, + }, + { + name: "different nameserver IPs", + a: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + }, + }, + }, + b: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("1.1.1.1"), NSType: 1, Port: 53}, + }, + }, + }, + expected: false, + }, + { + name: "different nameserver types", + a: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + }, + }, + }, + b: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 2, Port: 53}, + }, + }, + }, + expected: false, + }, + { + name: "different nameserver ports", + a: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + }, + }, + }, + b: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 5353}, + }, + }, + }, + expected: false, + }, + { + name: "complete matching groups", + a: []*nbdns.NameServerGroup{ + { + Primary: true, + Domains: []string{"example.com", "example.org"}, + SearchDomainsEnabled: true, + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + {IP: netip.MustParseAddr("1.1.1.1"), NSType: 1, Port: 53}, + }, + }, + }, + b: []*nbdns.NameServerGroup{ + { + Primary: true, + Domains: []string{"example.com", "example.org"}, + SearchDomainsEnabled: true, + NameServers: []nbdns.NameServer{ + {IP: netip.MustParseAddr("8.8.8.8"), NSType: 1, Port: 53}, + {IP: netip.MustParseAddr("1.1.1.1"), NSType: 1, Port: 53}, + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := nameServerGroupsEqual(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +}