diff --git a/DnsServerCore/Dns/Zones/PrimaryZone.cs b/DnsServerCore/Dns/Zones/PrimaryZone.cs index 81eea677..cce1e8ed 100644 --- a/DnsServerCore/Dns/Zones/PrimaryZone.cs +++ b/DnsServerCore/Dns/Zones/PrimaryZone.cs @@ -62,7 +62,7 @@ namespace DnsServerCore.Dns.Zones const int DNSSEC_TIMER_INITIAL_INTERVAL = 30000; const int DNSSEC_TIMER_PERIODIC_INTERVAL = 300000; DateTime _lastSignatureRefreshCheckedOn; - readonly object _dnssecNSecUpdateLock = new object(); + readonly object _dnssecUpdateLock = new object(); #endregion @@ -633,7 +633,7 @@ namespace DnsServerCore.Dns.Zones throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); if (saltLength > 32) - throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 salt length valid range is 0-32"); + throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); //generate private keys DnssecAlgorithm algorithm; @@ -694,7 +694,7 @@ namespace DnsServerCore.Dns.Zones throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); if (saltLength > 32) - throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 salt length valid range is 0-32"); + throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); //generate private keys DnssecAlgorithm algorithm; @@ -773,8 +773,6 @@ namespace DnsServerCore.Dns.Zones } } - CommitAndIncrementSerial(deletedRecords, addedRecords); - if (useNSec3) { EnableNSec3(zones, iterations, saltLength); @@ -786,8 +784,6 @@ namespace DnsServerCore.Dns.Zones _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC; } - TriggerNotify(); - //update private key state foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { @@ -802,6 +798,9 @@ namespace DnsServerCore.Dns.Zones } _dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite); + + CommitAndIncrementSerial(deletedRecords, addedRecords); + TriggerNotify(); } public void UnsignZone() @@ -825,9 +824,6 @@ namespace DnsServerCore.Dns.Zones } } - CommitAndIncrementSerial(deletedRecords); - TriggerNotify(); - Timer dnssecTimer = _dnssecTimer; if (dnssecTimer is not null) { @@ -840,6 +836,9 @@ namespace DnsServerCore.Dns.Zones _dnssecPrivateKeys = null; _dnssecStatus = AuthZoneDnssecStatus.Unsigned; + + CommitAndIncrementSerial(deletedRecords); + TriggerNotify(); } public void ConvertToNSec() @@ -847,25 +846,22 @@ namespace DnsServerCore.Dns.Zones if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3) throw new DnsServerException("Cannot convert to NSEC: the zone must be signed with NSEC3 for conversion."); - lock (_dnssecNSecUpdateLock) + lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name); - List nsec3Zones = new List(zones.Count / 2); - List nonNSec3Zones = new List(zones.Count / 2); + DisableNSec3(zones); - nsec3Zones.Add(zones[0]); //apex zone + //since zones were removed when disabling NSEC3; get updated non empty zones list + List nonEmptyZones = new List(zones.Count); foreach (AuthZone zone in zones) { - if (zone.HasOnlyNSec3Records()) - nsec3Zones.Add(zone); - else - nonNSec3Zones.Add(zone); + if (!zone.IsEmpty) + nonEmptyZones.Add(zone); } - EnableNSec(nonNSec3Zones); - DisableNSec3(nsec3Zones); + EnableNSec(nonEmptyZones); _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC; } @@ -876,12 +872,18 @@ namespace DnsServerCore.Dns.Zones if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC) throw new DnsServerException("Cannot convert to NSEC3: the zone must be signed with NSEC for conversion."); - lock (_dnssecNSecUpdateLock) + if (iterations > 50) + throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); + + if (saltLength > 32) + throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); + + lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name); - EnableNSec3(zones, iterations, saltLength); DisableNSec(zones); + EnableNSec3(zones, iterations, saltLength); _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3; } @@ -892,12 +894,28 @@ namespace DnsServerCore.Dns.Zones if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3) throw new DnsServerException("Cannot update NSEC3 parameters: the zone must be signed with NSEC3 first."); - lock (_dnssecNSecUpdateLock) + if (iterations > 50) + throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); + + if (saltLength > 32) + throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); + + lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name); DisableNSec3(zones); - EnableNSec3(zones, iterations, saltLength); + + //since zones were removed when disabling NSEC3; get updated non empty zones list + List nonEmptyZones = new List(zones.Count); + + foreach (AuthZone zone in zones) + { + if (!zone.IsEmpty) + nonEmptyZones.Add(zone); + } + + EnableNSec3(nonEmptyZones, iterations, saltLength); } } @@ -968,28 +986,23 @@ namespace DnsServerCore.Dns.Zones //list all partial NSEC3 records foreach (AuthZone zone in zones) { - if (zone.IsEmpty) - continue; //zone was removed when disabling NSEC3 - partialNSec3Records.Add(zone.GetPartialNSec3Record(_name, ttl, iterations, salt)); - if (zone.Name.Length > _name.Length) + int zoneLabelCount = DnsRRSIGRecord.GetLabelCount(zone.Name); + if ((zoneLabelCount - apexLabelCount) > 1) { - //find empty non-terminal (ENT) - string currentOwnerName = AuthZoneManager.GetParentZone(zone.Name); - int labelCount; + //empty non-terminal (ENT) may exists + string currentOwnerName = zone.Name; - while (currentOwnerName.Length > _name.Length) + while (true) { - labelCount = DnsRRSIGRecord.GetLabelCount(currentOwnerName); - if ((labelCount - apexLabelCount) <= 1) - break; //ENT not found + currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName); + if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase)) + break; //add partial NSEC3 record for ENT - DnsNSEC3Record newNSec3Record = new DnsNSEC3Record(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty(), Array.Empty()); - partialNSec3Records.Add(new DnsResourceRecord(newNSec3Record.ComputeHashedOwnerName(currentOwnerName) + "." + _name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newNSec3Record)); - - currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName); + AuthZone entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object + partialNSec3Records.Add(entZone.GetPartialNSec3Record(_name, ttl, iterations, salt)); } } } @@ -1028,9 +1041,11 @@ namespace DnsServerCore.Dns.Zones uniqueTypes.Add(type); } + uniqueTypes.Sort(); + //update the next nsec3 record and continue - DnsNSEC3Record mergedNSec3 = new DnsNSEC3Record(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty(), uniqueTypes); - partialNSec3Records[i + 1] = new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, mergedNSec3); + DnsNSEC3Record mergedPartialNSec3 = new DnsNSEC3Record(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty(), uniqueTypes); + partialNSec3Records[i + 1] = new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, mergedPartialNSec3); continue; } } @@ -1708,9 +1723,7 @@ namespace DnsServerCore.Dns.Zones return privateKeys; //zone is root DnsQuestionRecord dsQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN); - ResolverPrefetchDnsCache dnsCache = new ResolverPrefetchDnsCache(_dnsServer.DnsApplicationManager, _dnsServer.AuthZoneManager, _dnsServer.CacheZoneManager, dsQuestion); - - DnsDatagram dsResponse = await DnsClient.RecursiveResolveAsync(dsQuestion, dnsCache, _dnsServer.Proxy, _dnsServer.PreferIPv6, _dnsServer.UdpPayloadSize, _dnsServer.RandomizeName, _dnsServer.QnameMinimization, false, true); + DnsDatagram dsResponse = await DnsClient.RecursiveResolveAsync(dsQuestion, _dnsServer.DnsCache, _dnsServer.Proxy, _dnsServer.PreferIPv6, _dnsServer.UdpPayloadSize, _dnsServer.RandomizeName, false, false, false); IReadOnlyList dsRecords = DnsClient.ParseResponseDS(dsResponse); List activePrivateKeys = new List(dsRecords.Count); @@ -1825,7 +1838,7 @@ namespace DnsServerCore.Dns.Zones internal void UpdateDnssecRecordsFor(AuthZone zone, DnsResourceRecordType type) { //lock to sync this call since to prevent inconsistent NSEC/NSEC3 updates - lock (_dnssecNSecUpdateLock) + lock (_dnssecUpdateLock) { IReadOnlyList records = zone.GetRecords(type); if (records.Count > 0) @@ -1862,9 +1875,36 @@ namespace DnsServerCore.Dns.Zones } if (_dnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC) + { UpdateNSecRRSetFor(zone); + } else + { UpdateNSec3RRSetFor(zone); + + int apexLabelCount = DnsRRSIGRecord.GetLabelCount(_name); + int zoneLabelCount = DnsRRSIGRecord.GetLabelCount(zone.Name); + + if ((zoneLabelCount - apexLabelCount) > 1) + { + //empty non-terminal (ENT) may exists + string currentOwnerName = zone.Name; + + while (true) + { + currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName); + if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase)) + break; + + //update NSEC3 rrset for current owner name + AuthZone entZone = _dnsServer.AuthZoneManager.GetAuthZone(currentOwnerName, _name); + if (entZone is null) + entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object + + UpdateNSec3RRSetFor(entZone); + } + } + } } } @@ -1922,29 +1962,38 @@ namespace DnsServerCore.Dns.Zones private void UpdateNSec3RRSetFor(AuthZone zone) { uint ttl = GetZoneSoaMinimum(); + bool noSubDomainExistsForEmptyZone = (zone.IsEmpty || zone.HasOnlyNSec3Records()) && !_dnsServer.AuthZoneManager.SubDomainExists(zone.Name, _name); - IReadOnlyList newNSec3Records = GetUpdatedNSec3RRSetFor(zone, ttl); + IReadOnlyList newNSec3Records = GetUpdatedNSec3RRSetFor(zone, ttl, noSubDomainExistsForEmptyZone); if (newNSec3Records.Count > 0) { DnsResourceRecord newNSec3Record = newNSec3Records[0]; - DnsNSEC3Record newNSec3 = newNSec3Record.RDATA as DnsNSEC3Record; AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, newNSec3Record.Name); if (nsec3Zone is null) throw new InvalidOperationException(); - if (newNSec3.Types.Count == 1) + if (noSubDomainExistsForEmptyZone) { - //only RRSIG exists so remove NSEC3 + //no records exists in real zone and no sub domain exists, so remove NSEC3 IReadOnlyList deletedNSec3Records = nsec3Zone.RemoveNSec3RecordsWithRRSig(); if (deletedNSec3Records.Count > 0) CommitAndIncrementSerial(deletedNSec3Records); - //remove nsec3 sub domain zone since it wont get removed otherwise - if (nsec3Zone is SubDomainZone subDomainZone) + //remove nsec3 sub domain zone if empty since it wont get removed otherwise + if (nsec3Zone is SubDomainZone nsec3SubDomainZone) { if (nsec3Zone.IsEmpty) _dnsServer.AuthZoneManager.RemoveSubDomainZone(nsec3Zone.Name); //remove empty sub zone + else + nsec3SubDomainZone.AutoUpdateState(); + } + + //remove the real zone if empty so that any of the ENT that exists can also be removed later + if (zone is SubDomainZone subDomainZone) + { + if (zone.IsEmpty) + _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone else subDomainZone.AutoUpdateState(); } @@ -1986,14 +2035,14 @@ namespace DnsServerCore.Dns.Zones private IReadOnlyList GetUpdatedNSecRRSetFor(AuthZone zone, uint ttl) { - AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(zone.Name); + AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(zone.Name, _name); if (nextZone is null) nextZone = this; return zone.GetUpdatedNSecRRSet(nextZone.Name, ttl); } - private IReadOnlyList GetUpdatedNSec3RRSetFor(AuthZone zone, uint ttl) + private IReadOnlyList GetUpdatedNSec3RRSetFor(AuthZone zone, uint ttl, bool forceGetNewRRSet) { if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3PARAM, out IReadOnlyList nsec3ParamRecords)) throw new InvalidOperationException(); @@ -2009,7 +2058,7 @@ namespace DnsServerCore.Dns.Zones while (true) { - AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(currentOwnerName); + AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(currentOwnerName, _name); if (nextZone is null) break; @@ -2030,7 +2079,7 @@ namespace DnsServerCore.Dns.Zones while (true) { - AuthZone previousZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(currentOwnerName); + AuthZone previousZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(currentOwnerName, _name); if (previousZone is null) break; @@ -2048,12 +2097,21 @@ namespace DnsServerCore.Dns.Zones if (nextHashedOwnerName is null) nextHashedOwnerName = DnsNSEC3Record.GetHashedOwnerNameFrom(hashedOwnerName); //only 1 NSEC3 record in zone - return zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.SaltValue); + IReadOnlyList newNSec3Records = zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.SaltValue); + + if (forceGetNewRRSet) + return newNSec3Records; + + AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(hashedOwnerName, _name); + if (nsec3Zone is null) + return newNSec3Records; + + return nsec3Zone.GetUpdatedNSec3RRSet(newNSec3Records); } private void RelinkPreviousNSecRRSetFor(DnsResourceRecord currentNSecRecord, uint ttl, bool wasRemoved) { - AuthZone previousNsecZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(currentNSecRecord.Name); + AuthZone previousNsecZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(currentNSecRecord.Name, _name); if (previousNsecZone is null) return; //current zone is apex @@ -2099,7 +2157,7 @@ namespace DnsServerCore.Dns.Zones while (true) { - previousNSec3Zone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(currentOwnerName); + previousNSec3Zone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(currentOwnerName, _name); if (previousNSec3Zone is null) break; @@ -2115,7 +2173,7 @@ namespace DnsServerCore.Dns.Zones if (previousNSec3Record is null) { - //didnt find previous NSEC3 since removed NSEC3 was the first; find the last NSEC3 to update + //didnt find previous NSEC3; find the last NSEC3 to update if (wasRemoved) currentOwnerName = currentNSec3.NextHashedOwnerName + "." + _name; else @@ -2123,7 +2181,7 @@ namespace DnsServerCore.Dns.Zones while (true) { - AuthZone nextNSec3Zone = _dnsServer.AuthZoneManager.FindZone(currentOwnerName); + AuthZone nextNSec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(currentOwnerName, _name); if (nextNSec3Zone is null) break; @@ -2270,8 +2328,20 @@ namespace DnsServerCore.Dns.Zones oldSoaRecord.Tag = null; //remove RR info from old SOA to allow creating new RR info for it during SetDeletedOn() } + DnsResourceRecord[] newSoaRecords = new DnsResourceRecord[] { newSoaRecord }; + //update SOA - _entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { newSoaRecord }; + _entries[DnsResourceRecordType.SOA] = newSoaRecords; + + IReadOnlyList newRRSigRecords = null; + IReadOnlyList deletedRRSigRecords = null; + + if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) + { + //sign SOA and update RRSig + newRRSigRecords = SignRRSet(newSoaRecords); + AddOrUpdateRRSigRecords(newRRSigRecords, out deletedRRSigRecords); + } //start commit oldSoaRecord.SetDeletedOn(DateTime.UtcNow); @@ -2293,6 +2363,9 @@ namespace DnsServerCore.Dns.Zones } } + if (deletedRRSigRecords is not null) + _history.AddRange(deletedRRSigRecords); + //write added _history.Add(newSoaRecord); @@ -2310,6 +2383,9 @@ namespace DnsServerCore.Dns.Zones } } + if (newRRSigRecords is not null) + _history.AddRange(newRRSigRecords); + //end commit CleanupHistory(_history); @@ -2340,7 +2416,8 @@ namespace DnsServerCore.Dns.Zones switch (type) { case DnsResourceRecordType.CNAME: - throw new InvalidOperationException("Cannot set CNAME record to zone root."); + case DnsResourceRecordType.DS: + throw new InvalidOperationException("Cannot set " + type.ToString() + " record at zone apex."); case DnsResourceRecordType.SOA: if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) @@ -2394,6 +2471,9 @@ namespace DnsServerCore.Dns.Zones case DnsResourceRecordType.APP: throw new InvalidOperationException("Cannot add record: use SetRecords() for " + record.Type.ToString() + " record"); + case DnsResourceRecordType.DS: + throw new InvalidOperationException("Cannot set DS record at zone apex."); + case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: