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; } }