From c622118d6b2ffedd35f44584e7e2ad8914d15c72 Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sat, 14 Sep 2024 17:52:56 +0530 Subject: [PATCH] ForwarderZone: updated implementation to support notify and zone transfer features. Added support for record auto expiry feature. Added pseudo SOA record support for zone transfer support. Updated code to prevent adding DNSSEC related records. Added support for zone versioning. Code refactoring done. --- DnsServerCore/Dns/Zones/ForwarderZone.cs | 265 +++++++++++++++++++---- 1 file changed, 221 insertions(+), 44 deletions(-) diff --git a/DnsServerCore/Dns/Zones/ForwarderZone.cs b/DnsServerCore/Dns/Zones/ForwarderZone.cs index 3c393200..5802b62d 100644 --- a/DnsServerCore/Dns/Zones/ForwarderZone.cs +++ b/DnsServerCore/Dns/Zones/ForwarderZone.cs @@ -29,46 +29,61 @@ namespace DnsServerCore.Dns.Zones { #region constructor - public ForwarderZone(AuthZoneInfo zoneInfo) - : base(zoneInfo) - { } - - public ForwarderZone(string name) - : base(name) + public ForwarderZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) + : base(dnsServer, zoneInfo) { - _zoneTransfer = AuthZoneTransfer.Deny; - _notify = AuthZoneNotify.None; - _update = AuthZoneUpdate.Deny; + InitNotify(); + InitRecordExpiry(); } - public ForwarderZone(string name, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments) - : base(name) + public ForwarderZone(DnsServer dnsServer, string name) + : base(dnsServer, name) { - _zoneTransfer = AuthZoneTransfer.Deny; - _notify = AuthZoneNotify.None; - _update = AuthZoneUpdate.Deny; + InitZone(); + InitNotify(); + InitRecordExpiry(); + } - DnsResourceRecord fwdRecord = new DnsResourceRecord(name, DnsResourceRecordType.FWD, DnsClass.IN, 0, new DnsForwarderRecordData(forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword)); + public ForwarderZone(DnsServer dnsServer, string name, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments) + : base(dnsServer, name) + { + DnsResourceRecord fwdRecord = new DnsResourceRecord(name, DnsResourceRecordType.FWD, DnsClass.IN, 0, new DnsForwarderRecordData(forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, 0)); if (!string.IsNullOrEmpty(fwdRecordComments)) fwdRecord.GetAuthGenericRecordInfo().Comments = fwdRecordComments; - _entries[DnsResourceRecordType.FWD] = new DnsResourceRecord[] { fwdRecord }; + fwdRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; + + _entries[DnsResourceRecordType.FWD] = [fwdRecord]; + + InitZone(); + InitNotify(); + InitRecordExpiry(); } #endregion #region internal - internal void UpdateLastModified() + internal virtual void InitZone() { - _lastModified = DateTime.UtcNow; + //init forwarder zone with dummy SOA record + DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, "invalid", 1, 900, 300, 604800, 900); + DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); + soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; + + _entries[DnsResourceRecordType.SOA] = [soaRecord]; } #endregion #region public + public override string GetZoneTypeName() + { + return "Conditional Forwarder"; + } + public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { switch (type) @@ -77,12 +92,72 @@ namespace DnsServerCore.Dns.Zones throw new InvalidOperationException("Cannot set CNAME record at zone apex."); case DnsResourceRecordType.SOA: + if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("Invalid SOA record."); + + DnsResourceRecord newSoaRecord = records[0]; + DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData; + + if (newSoaRecord.OriginalTtlValue > newSoa.Expire) + throw new DnsServerException("Failed to set records: TTL cannot be greater than SOA EXPIRE."); + + if (newSoa.Retry > newSoa.Refresh) + throw new DnsServerException("Failed to set records: SOA RETRY cannot be greater than SOA REFRESH."); + + if (newSoa.Refresh > newSoa.Expire) + throw new DnsServerException("Failed to set records: SOA REFRESH cannot be greater than SOA EXPIRE."); + + { + //reset fixed record values + DnsSOARecordData modifiedSoa = new DnsSOARecordData(newSoa.PrimaryNameServer, "invalid", newSoa.Serial, newSoa.Refresh, newSoa.Retry, newSoa.Expire, newSoa.Minimum); + newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, modifiedSoa) { Tag = newSoaRecord.Tag }; + records = [newSoaRecord]; + } + + //remove any record info except serial date scheme and comments + bool useSoaSerialDateScheme; + string comments; + { + SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo(); + + useSoaSerialDateScheme = recordInfo.UseSoaSerialDateScheme; + comments = recordInfo.Comments; + } + + newSoaRecord.Tag = null; //remove old record info + + { + SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo(); + + recordInfo.UseSoaSerialDateScheme = useSoaSerialDateScheme; + recordInfo.Comments = comments; + recordInfo.LastModified = DateTime.UtcNow; + } + + //setting new SOA + CommitAndIncrementSerial(null, records); + + TriggerNotify(); + break; + case DnsResourceRecordType.DS: - throw new DnsServerException("The record type is not supported by forwarder zones."); + case DnsResourceRecordType.DNSKEY: + case DnsResourceRecordType.RRSIG: + case DnsResourceRecordType.NSEC: + case DnsResourceRecordType.NSEC3PARAM: + case DnsResourceRecordType.NSEC3: + throw new InvalidOperationException("Cannot set DNSSEC records."); default: - base.SetRecords(type, records); - UpdateLastModified(); + if (records[0].OriginalTtlValue > GetZoneSoaExpire()) + throw new DnsServerException("Failed to set records: TTL cannot be greater than SOA EXPIRE."); + + if (!TrySetRecords(type, records, out IReadOnlyList deletedRecords)) + throw new DnsServerException("Failed to set records. Please try again."); + + CommitAndIncrementSerial(deletedRecords, records); + + TriggerNotify(); break; } } @@ -92,72 +167,174 @@ namespace DnsServerCore.Dns.Zones switch (record.Type) { case DnsResourceRecordType.DS: - throw new DnsServerException("The record type is not supported by forwarder zones."); + case DnsResourceRecordType.DNSKEY: + case DnsResourceRecordType.RRSIG: + case DnsResourceRecordType.NSEC: + case DnsResourceRecordType.NSEC3PARAM: + case DnsResourceRecordType.NSEC3: + throw new InvalidOperationException("Cannot set DNSSEC records."); default: - base.AddRecord(record); - UpdateLastModified(); + if (record.OriginalTtlValue > GetZoneSoaExpire()) + throw new DnsServerException("Failed to add record: TTL cannot be greater than SOA EXPIRE."); + + AddRecord(record, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); + + if (addedRecords.Count > 0) + { + CommitAndIncrementSerial(deletedRecords, addedRecords); + + TriggerNotify(); + } break; } } public override bool DeleteRecords(DnsResourceRecordType type) { - if (base.DeleteRecords(type)) + switch (type) { - UpdateLastModified(); - return true; - } + case DnsResourceRecordType.SOA: + throw new InvalidOperationException("Cannot delete SOA record."); - return false; + default: + if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) + { + CommitAndIncrementSerial(removedRecords); + + TriggerNotify(); + + return true; + } + + return false; + } } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata) { - if (base.DeleteRecord(type, rdata)) + switch (type) { - UpdateLastModified(); - return true; - } + case DnsResourceRecordType.SOA: + throw new InvalidOperationException("Cannot delete SOA record."); - return false; + default: + if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord)) + { + CommitAndIncrementSerial([deletedRecord]); + + TriggerNotify(); + + return true; + } + + return false; + } } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { - base.UpdateRecord(oldRecord, newRecord); - UpdateLastModified(); + switch (oldRecord.Type) + { + case DnsResourceRecordType.SOA: + throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record"); + + default: + if (oldRecord.Type != newRecord.Type) + throw new InvalidOperationException("Old and new record types do not match."); + + if (newRecord.OriginalTtlValue > GetZoneSoaExpire()) + throw new DnsServerException("Cannot update record: TTL cannot be greater than SOA EXPIRE."); + + if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord)) + throw new DnsServerException("Cannot update record: the record does not exists to be updated."); + + AddRecord(newRecord, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); + + List allDeletedRecords = new List(deletedRecords.Count + 1); + allDeletedRecords.Add(deletedRecord); + allDeletedRecords.AddRange(deletedRecords); + + CommitAndIncrementSerial(allDeletedRecords, addedRecords); + + TriggerNotify(); + break; + } + } + + public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) + { + if (type == DnsResourceRecordType.SOA) + return []; //forwarder zone is not authoritative and contains dummy SOA record + + return base.QueryRecords(type, dnssecOk); } #endregion #region properties + public override AuthZoneQueryAccess QueryAccess + { + get { return base.QueryAccess; } + set + { + switch (value) + { + case AuthZoneQueryAccess.AllowOnlyZoneNameServers: + case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: + throw new ArgumentException("The Query Access option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(QueryAccess)); + } + + base.QueryAccess = value; + } + } + public override AuthZoneTransfer ZoneTransfer { - get { return _zoneTransfer; } - set { throw new InvalidOperationException(); } + get { return base.ZoneTransfer; } + set + { + switch (value) + { + case AuthZoneTransfer.AllowOnlyZoneNameServers: + case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: + throw new ArgumentException("The Zone Transfer option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(ZoneTransfer)); + } + + base.ZoneTransfer = value; + } } public override AuthZoneNotify Notify { - get { return _notify; } - set { throw new InvalidOperationException(); } + get { return base.Notify; } + set + { + switch (value) + { + case AuthZoneNotify.ZoneNameServers: + case AuthZoneNotify.BothZoneAndSpecifiedNameServers: + throw new ArgumentException("The Notify option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Notify)); + } + + base.Notify = value; + } } public override AuthZoneUpdate Update { - get { return _update; } + get { return base.Update; } set { switch (value) { case AuthZoneUpdate.AllowOnlyZoneNameServers: - case AuthZoneUpdate.AllowBothZoneNameServersAndSpecifiedIpAddresses: - throw new ArgumentException("The Dynamic Updates option is invalid for Conditional Forwarder zones: " + value.ToString(), nameof(Update)); + case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: + throw new ArgumentException("The Dynamic Updates option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Update)); } - _update = value; + base.Update = value; } }