/* Technitium DNS Server Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using DnsServerCore.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { public enum AuthZoneQueryAccess : byte { Deny = 0, Allow = 1, AllowOnlyPrivateNetworks = 2, AllowOnlyZoneNameServers = 3, UseSpecifiedNetworkACL = 4, AllowZoneNameServersAndUseSpecifiedNetworkACL = 5 } public enum AuthZoneTransfer : byte { Deny = 0, Allow = 1, AllowOnlyZoneNameServers = 2, UseSpecifiedNetworkACL = 3, AllowZoneNameServersAndUseSpecifiedNetworkACL = 4 } public enum AuthZoneNotify : byte { None = 0, ZoneNameServers = 1, SpecifiedNameServers = 2, BothZoneAndSpecifiedNameServers = 3, SeparateNameServersForCatalogAndMemberZones = 4 } public enum AuthZoneUpdate : byte { Deny = 0, Allow = 1, AllowOnlyZoneNameServers = 2, UseSpecifiedNetworkACL = 3, AllowZoneNameServersAndUseSpecifiedNetworkACL = 4 } abstract class ApexZone : AuthZone, IDisposable { #region variables protected readonly DnsServer _dnsServer; protected DateTime _lastModified; string _catalogZoneName; bool _overrideCatalogQueryAccess; bool _overrideCatalogZoneTransfer; bool _overrideCatalogNotify; protected AuthZoneQueryAccess _queryAccess; IReadOnlyCollection _queryAccessNetworkACL; protected AuthZoneTransfer _zoneTransfer; IReadOnlyCollection _zoneTransferNetworkACL; IReadOnlyDictionary _zoneTransferTsigKeyNames; readonly List _zoneHistory; //for IXFR support AuthZoneNotify _notify; IReadOnlyCollection _notifyNameServers; IReadOnlyCollection _notifySecondaryCatalogNameServers; AuthZoneUpdate _update; IReadOnlyCollection _updateNetworkACL; IReadOnlyDictionary>> _updateSecurityPolicies; protected AuthZoneDnssecStatus _dnssecStatus; Timer _notifyTimer; bool _notifyTimerTriggered; const int NOTIFY_TIMER_INTERVAL = 10000; List _notifyList; List _notifyFailed; const int NOTIFY_TIMEOUT = 10000; const int NOTIFY_RETRIES = 5; protected bool _syncFailed; Timer _recordExpiryTimer; readonly object _recordExpiryTimerLock = new object(); DateTime _recordExpiryTimerStartedOn; uint _recordExpiryTimerTtl; bool _recordExpiryTimerRunning; CatalogZone _catalogZone; SecondaryCatalogZone _secondaryCatalogZone; #endregion #region constructor protected ApexZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(zoneInfo) { _dnsServer = dnsServer; _catalogZoneName = zoneInfo.CatalogZoneName; _overrideCatalogQueryAccess = zoneInfo.OverrideCatalogQueryAccess; _overrideCatalogZoneTransfer = zoneInfo.OverrideCatalogZoneTransfer; _overrideCatalogNotify = zoneInfo.OverrideCatalogNotify; _queryAccess = zoneInfo.QueryAccess; _queryAccessNetworkACL = zoneInfo.QueryAccessNetworkACL; _zoneTransfer = zoneInfo.ZoneTransfer; _zoneTransferNetworkACL = zoneInfo.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = zoneInfo.ZoneTransferTsigKeyNames; if (zoneInfo.ZoneHistory is null) _zoneHistory = new List(); else _zoneHistory = new List(zoneInfo.ZoneHistory); _notify = zoneInfo.Notify; _notifyNameServers = zoneInfo.NotifyNameServers; _notifySecondaryCatalogNameServers = zoneInfo.NotifySecondaryCatalogNameServers; _update = zoneInfo.Update; _updateNetworkACL = zoneInfo.UpdateNetworkACL; _updateSecurityPolicies = zoneInfo.UpdateSecurityPolicies; _lastModified = zoneInfo.LastModified; } protected ApexZone(DnsServer dnsServer, string name) : base(name) { _dnsServer = dnsServer; _queryAccess = AuthZoneQueryAccess.Allow; _zoneHistory = new List(); _lastModified = DateTime.UtcNow; } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _notifyTimer?.Dispose(); lock (_recordExpiryTimerLock) { if (_recordExpiryTimer is not null) { _recordExpiryTimer.Dispose(); _recordExpiryTimer = null; } } } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region notify protected void InitNotify() { _notifyTimer = new Timer(NotifyTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _notifyList = new List(); _notifyFailed = new List(); } protected void DisableNotifyTimer() { if (_notifyTimer is not null) _notifyTimer.Change(Timeout.Infinite, Timeout.Infinite); } private async void NotifyTimerCallback(object state) { ApexZone apexZone = this; if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogNotify) apexZone = apexZone.CatalogZone; List notifiedNameServers = new List(); async Task NotifyZoneNameServersAsync(bool onlyFailedNameServers) { string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer; IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords //notify all secondary name servers List tasks = new List(); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; string nameServerHost = (nsRecord.RDATA as DnsNSRecordData).NameServer; if (primaryNameServer.Equals(nameServerHost, StringComparison.OrdinalIgnoreCase)) continue; //skip primary name server if (onlyFailedNameServers) { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) continue; } } notifiedNameServers.Add(nameServerHost); List nameServers = new List(2); await ResolveNameServerAddressesAsync(nsRecord, nameServers); if (nameServers.Count > 0) { tasks.Add(NotifyNameServerAsync(nameServerHost, nameServers)); } else { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) _notifyFailed.Add(nameServerHost); } _dnsServer.LogManager?.Write("DNS Server failed to notify name server '" + nameServerHost + "' due to failure in resolving its IP address for zone: " + ToString()); } } await Task.WhenAll(tasks); } Task NotifySpecifiedNameServersAsync(bool onlyFailedNameServers) { IReadOnlyCollection specifiedNameServers = apexZone._notifyNameServers; if (specifiedNameServers is not null) return NotifyNameServersAsync(specifiedNameServers, onlyFailedNameServers); return Task.CompletedTask; } Task NotifySecondaryCatalogNameServersAsync(bool onlyFailedNameServers) { IReadOnlyCollection secondaryCatalogNameServers = apexZone._notifySecondaryCatalogNameServers; if (secondaryCatalogNameServers is not null) return NotifyNameServersAsync(secondaryCatalogNameServers, onlyFailedNameServers); return Task.CompletedTask; } async Task NotifyNameServersAsync(IReadOnlyCollection nameServerIpAddresses, bool onlyFailedNameServers) { List tasks = new List(); foreach (IPAddress nameServerIpAddress in nameServerIpAddresses) { string nameServerHost = nameServerIpAddress.ToString(); if (onlyFailedNameServers) { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) continue; } } notifiedNameServers.Add(nameServerHost); tasks.Add(NotifyNameServerAsync(nameServerHost, [new NameServerAddress(nameServerIpAddress)])); } await Task.WhenAll(tasks); } try { switch (apexZone._notify) { case AuthZoneNotify.ZoneNameServers: await NotifyZoneNameServersAsync(!_notifyTimerTriggered); break; case AuthZoneNotify.SpecifiedNameServers: await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered); break; case AuthZoneNotify.BothZoneAndSpecifiedNameServers: Task t1 = NotifyZoneNameServersAsync(!_notifyTimerTriggered); Task t2 = NotifySpecifiedNameServersAsync(!_notifyTimerTriggered); await Task.WhenAll(t1, t2); break; case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones: if (this is CatalogZone) await NotifySecondaryCatalogNameServersAsync(!_notifyTimerTriggered); else await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered); break; } //remove non-existent name servers from notify failed list lock (_notifyFailed) { if (_notifyFailed.Count > 0) { List toRemove = new List(); foreach (string failedNameServer in _notifyFailed) { if (!notifiedNameServers.Contains(failedNameServer)) toRemove.Add(failedNameServer); } foreach (string failedNameServer in toRemove) _notifyFailed.Remove(failedNameServer); if (_notifyFailed.Count > 0) { //set timer to notify failed name servers again int retryInterval = (int)((_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Retry * 1000); _notifyTimer.Change(retryInterval, Timeout.Infinite); } } } } catch (Exception ex) { _dnsServer.LogManager?.Write(ex); } finally { _notifyTimerTriggered = false; } } private async Task NotifyNameServerAsync(string nameServerHost, IReadOnlyList nameServers) { //use notify list to prevent multiple threads from notifying the same name server lock (_notifyList) { if (_notifyList.Contains(nameServerHost)) return; //already notifying the name server in another thread _notifyList.Add(nameServerHost); } try { DnsClient client = new DnsClient(nameServers); client.Proxy = _dnsServer.Proxy; client.Timeout = NOTIFY_TIMEOUT; client.Retries = NOTIFY_RETRIES; DnsDatagram notifyRequest = new DnsDatagram(0, false, DnsOpcode.Notify, true, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN) }, _entries[DnsResourceRecordType.SOA]); DnsDatagram response = await client.RawResolveAsync(notifyRequest); switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NotImplemented: { //transaction complete lock (_notifyFailed) { _notifyFailed.Remove(nameServerHost); } _dnsServer.LogManager?.Write("DNS Server successfully notified name server '" + nameServerHost + "' for zone: " + ToString()); } break; default: { //transaction failed lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) _notifyFailed.Add(nameServerHost); } _dnsServer.LogManager?.Write("DNS Server failed to notify name server '" + nameServerHost + "' (RCODE=" + response.RCODE.ToString() + ") for zone: " + ToString()); } break; } } catch (Exception ex) { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) _notifyFailed.Add(nameServerHost); } _dnsServer.LogManager?.Write("DNS Server failed to notify name server '" + nameServerHost + "' for zone: " + ToString() + "\r\n" + ex.ToString()); } finally { lock (_notifyList) { _notifyList.Remove(nameServerHost); } } } internal void RemoveFromNotifyFailedList(NameServerAddress allowedZoneNameServer, IPAddress allowedIPAddress) { if (_notifyFailed is null) return; lock (_notifyFailed) { if (_notifyFailed.Count == 0) return; if ((allowedZoneNameServer is not null) && (allowedZoneNameServer.DomainEndPoint is not null)) _notifyFailed.Remove(allowedZoneNameServer.DomainEndPoint.Address); _notifyFailed.Remove(allowedIPAddress.ToString()); } } public void TriggerNotify() { if (Disabled) return; ApexZone apexZone = this; if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogNotify) apexZone = apexZone.CatalogZone; if (apexZone._notify == AuthZoneNotify.None) { if (_notifyFailed is not null) { lock (_notifyFailed) { _notifyFailed.Clear(); } } return; } if (_notifyTimerTriggered) return; if (_disposed) return; if (_notifyTimer is null) return; _notifyTimer.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite); _notifyTimerTriggered = true; } #endregion #region record expiry protected void InitRecordExpiry() { _recordExpiryTimer = new Timer(RecordExpiryTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } private uint GetMinRecordExpiryTtl(uint minExpiryTtl) { if (!_recordExpiryTimerRunning) return Math.Min(minExpiryTtl, uint.MaxValue / 1000); uint elapsedSeconds = Convert.ToUInt32((DateTime.UtcNow - _recordExpiryTimerStartedOn).TotalSeconds); if (elapsedSeconds >= _recordExpiryTimerTtl) return 0u; uint pendingExpiryTtl = _recordExpiryTimerTtl - elapsedSeconds; return Math.Min(Math.Min(pendingExpiryTtl, minExpiryTtl), uint.MaxValue / 1000); } public void StartRecordExpiryTimer(uint minExpiryTtl) { lock (_recordExpiryTimerLock) { if (_recordExpiryTimer is not null) { uint minTtl = GetMinRecordExpiryTtl(minExpiryTtl); _recordExpiryTimer.Change(minTtl * 1000, Timeout.Infinite); _recordExpiryTimerStartedOn = DateTime.UtcNow; _recordExpiryTimerTtl = minTtl; _recordExpiryTimerRunning = true; } } } private void RecordExpiryTimerCallback(object state) { _recordExpiryTimerRunning = false; uint minExpiryTtl = 0u; try { IReadOnlyList authZones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); bool recordsDeleted = false; foreach (AuthZone authZone in authZones) { foreach (KeyValuePair> entry in authZone.Entries) { foreach (DnsResourceRecord record in entry.Value) { GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); if (recordInfo.ExpiryTtl > 0u) { uint pendingExpiryTtl = recordInfo.GetPendingExpiryTtl(); if (pendingExpiryTtl == 0u) { if (_dnsServer.AuthZoneManager.DeleteRecord(_name, record)) recordsDeleted = true; } else { if (minExpiryTtl == 0u) minExpiryTtl = pendingExpiryTtl; else minExpiryTtl = Math.Min(minExpiryTtl, pendingExpiryTtl); } } } } } if (recordsDeleted) _dnsServer.AuthZoneManager.SaveZoneFile(_name); } catch (Exception ex) { _dnsServer.LogManager?.Write(ex); } finally { if (minExpiryTtl > 0u) StartRecordExpiryTimer(minExpiryTtl); } } #endregion #region internal internal virtual void UpdateDnssecStatus() { if (!_entries.ContainsKey(DnsResourceRecordType.DNSKEY)) _dnssecStatus = AuthZoneDnssecStatus.Unsigned; else if (_entries.ContainsKey(DnsResourceRecordType.NSEC3PARAM)) _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3; else _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC; } #endregion #region versioning internal virtual void CommitAndIncrementSerial(IReadOnlyList deletedRecords = null, IReadOnlyList addedRecords = null) { _lastModified = DateTime.UtcNow; if (addedRecords is not null) { uint minExpiryTtl = 0u; foreach (DnsResourceRecord addedRecord in addedRecords) { uint expiryTtl = addedRecord.GetAuthGenericRecordInfo().ExpiryTtl; if (expiryTtl > 0u) { if (minExpiryTtl == 0u) minExpiryTtl = expiryTtl; else minExpiryTtl = Math.Min(minExpiryTtl, expiryTtl); } } if (minExpiryTtl > 0u) StartRecordExpiryTimer(minExpiryTtl); } lock (_zoneHistory) { DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsResourceRecord newSoaRecord; { DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData; if ((addedRecords is not null) && (addedRecords.Count == 1) && (addedRecords[0].Type == DnsResourceRecordType.SOA)) { DnsResourceRecord addSoaRecord = addedRecords[0]; DnsSOARecordData addSoa = addSoaRecord.RDATA as DnsSOARecordData; uint serial = GetNewSerial(oldSoa.Serial, addSoa.Serial, addSoaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme); newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, addSoaRecord.TTL, new DnsSOARecordData(addSoa.PrimaryNameServer, addSoa.ResponsiblePerson, serial, addSoa.Refresh, addSoa.Retry, addSoa.Expire, addSoa.Minimum)) { Tag = addSoaRecord.Tag }; addedRecords = null; } else { uint serial = GetNewSerial(oldSoa.Serial, 0, oldSoaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme); newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TTL, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, serial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag }; } } DnsResourceRecord[] newSoaRecords = [newSoaRecord]; //update SOA _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); } //remove RR info from old SOA to allow creating new history RR info for setting DeletedOn oldSoaRecord.Tag = null; //start commit oldSoaRecord.GetAuthHistoryRecordInfo().DeletedOn = DateTime.UtcNow; //write removed _zoneHistory.Add(oldSoaRecord); if (deletedRecords is not null) { foreach (DnsResourceRecord deletedRecord in deletedRecords) { if (deletedRecord.GetAuthGenericRecordInfo().Disabled) continue; _zoneHistory.Add(deletedRecord); if (deletedRecord.Type == DnsResourceRecordType.NS) { IReadOnlyList glueRecords = deletedRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) _zoneHistory.AddRange(glueRecords); } } } if (deletedRRSigRecords is not null) _zoneHistory.AddRange(deletedRRSigRecords); //write added _zoneHistory.Add(newSoaRecord); if (addedRecords is not null) { foreach (DnsResourceRecord addedRecord in addedRecords) { if (addedRecord.GetAuthGenericRecordInfo().Disabled) continue; _zoneHistory.Add(addedRecord); if (addedRecord.Type == DnsResourceRecordType.NS) { IReadOnlyList glueRecords = addedRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) _zoneHistory.AddRange(glueRecords); } } } if (newRRSigRecords is not null) _zoneHistory.AddRange(newRRSigRecords); //end commit CleanupHistory(); } } protected static uint GetNewSerial(uint oldSerial, uint updateSerial, bool useSoaSerialDateScheme) { if (useSoaSerialDateScheme) { string strOldSerial = oldSerial.ToString(); string strOldSerialDate = null; byte counter = 0; if (strOldSerial.Length == 10) { //parse old serial strOldSerialDate = strOldSerial.Substring(0, 8); counter = byte.Parse(strOldSerial.Substring(8)); } string strSerialDate = DateTime.UtcNow.ToString("yyyyMMdd"); if (strOldSerialDate is null) { //transitioning to date scheme return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')); } else if (strSerialDate.Equals(strOldSerialDate)) { //same date if (counter < 99) { counter++; return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')); } else { //more than 100 increments return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')) + 1; } } else if (uint.Parse(strSerialDate) > uint.Parse(strOldSerialDate)) { //later date return uint.Parse(strSerialDate + "00"); } } //default uint serial = oldSerial; if (updateSerial > serial) serial = updateSerial; else if (serial < uint.MaxValue) serial++; else serial = 1; return serial; } internal void SetSoaSerial(uint newSerial) { lock (_zoneHistory) { DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData; DnsResourceRecord newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TTL, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, newSerial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag }; DnsResourceRecord[] newSoaRecords = [newSoaRecord]; //update SOA _entries[DnsResourceRecordType.SOA] = newSoaRecords; //clear history _zoneHistory.Clear(); } } public IReadOnlyList GetZoneHistory() { lock (_zoneHistory) { return _zoneHistory.ToArray(); } } protected void CleanupHistory() { DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData; DateTime expiry = DateTime.UtcNow.AddSeconds(-soa.Expire); int index = 0; while (index < _zoneHistory.Count) { //check difference sequence if (_zoneHistory[index].GetAuthHistoryRecordInfo().DeletedOn > expiry) break; //found record to keep //skip to next difference sequence index++; int soaCount = 1; while (index < _zoneHistory.Count) { if (_zoneHistory[index].Type == DnsResourceRecordType.SOA) { soaCount++; if (soaCount == 3) break; } index++; } } if (index == _zoneHistory.Count) { //delete entire history _zoneHistory.Clear(); return; } //remove expired records _zoneHistory.RemoveRange(0, index); } protected void CommitZoneHistory(IReadOnlyList historyRecords) { lock (_zoneHistory) { historyRecords[0].GetAuthHistoryRecordInfo().DeletedOn = DateTime.UtcNow; //write history _zoneHistory.AddRange(historyRecords); CleanupHistory(); } } protected void ClearZoneHistory() { lock (_zoneHistory) { _zoneHistory.Clear(); } } #endregion #region catalog zone private IReadOnlyCollection GetQueryAccessACL() { switch (_queryAccess) { case AuthZoneQueryAccess.Allow: return [ new NetworkAccessControl(IPAddress.Any, 0), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; case AuthZoneQueryAccess.AllowOnlyPrivateNetworks: return [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("10.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("100.64.0.0"), 10), new NetworkAccessControl(IPAddress.Parse("169.254.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("172.16.0.0"), 12), new NetworkAccessControl(IPAddress.Parse("192.168.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("2000::"), 3, true), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; case AuthZoneQueryAccess.AllowOnlyZoneNameServers: return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; case AuthZoneQueryAccess.UseSpecifiedNetworkACL: return _queryAccessNetworkACL; case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: if (_queryAccessNetworkACL is null) { return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; } return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32), .._queryAccessNetworkACL ]; case AuthZoneQueryAccess.Deny: default: return [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("::1"), 128) ]; } } private IReadOnlyCollection GetZoneTranferACL() { switch (_zoneTransfer) { case AuthZoneTransfer.Allow: return [ new NetworkAccessControl(IPAddress.Any, 0), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; case AuthZoneTransfer.AllowOnlyZoneNameServers: return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; case AuthZoneTransfer.UseSpecifiedNetworkACL: return _zoneTransferNetworkACL; case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: if (_zoneTransferNetworkACL is null) { return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; } return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32), .._zoneTransferNetworkACL ]; case AuthZoneTransfer.Deny: default: return [ new NetworkAccessControl(IPAddress.Any, 0, true), new NetworkAccessControl(IPAddress.IPv6Any, 0, true) ]; } } #endregion #region public public uint GetZoneSoaMinimum() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Minimum; } public uint GetZoneSoaExpire() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Expire; } public uint GetZoneSoaSerial() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Serial; } public abstract string GetZoneTypeName(); public override string ToString() { return _name.Length == 0 ? "" : _name; } #endregion #region name server address resolution public async Task> GetResolvedPrimaryNameServerAddressesAsync() { IReadOnlyList primaryNameServers; if (this is SecondaryZone secondary) primaryNameServers = secondary.PrimaryNameServerAddresses; else if (this is StubZone stub) primaryNameServers = stub.PrimaryNameServerAddresses; else primaryNameServers = null; if (primaryNameServers is not null) return await GetResolvedNameServerAddressesAsync(primaryNameServers); DnsResourceRecord soaRecord = _entries[DnsResourceRecordType.SOA][0]; string primaryNameServer = (soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer; IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords List nameServers = new List(nsRecords.Count * 2); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase)) { //found primary NS await ResolveNameServerAddressesAsync(nsRecord, nameServers); break; } } if (nameServers.Count < 1) await ResolveNameServerAddressesAsync(primaryNameServer, 53, DnsTransportProtocol.Udp, nameServers); return nameServers; } public async Task> GetResolvedSecondaryNameServerAddressesAsync() { string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer; IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords List nameServers = new List(nsRecords.Count * 2); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase)) continue; //skip primary name server await ResolveNameServerAddressesAsync(nsRecord, nameServers); } return nameServers; } public async Task> GetAllResolvedNameServerAddressesAsync() { IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords List nameServers = new List(nsRecords.Count * 2); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; await ResolveNameServerAddressesAsync(nsRecord, nameServers); } return nameServers; } public async Task> GetResolvedNameServerAddressesAsync(IReadOnlyList nameServers) { List resolvedNameServers = new List(nameServers.Count * 2); foreach (NameServerAddress nameServer in nameServers) { if (nameServer.IsIPEndPointStale) await ResolveNameServerAddressesAsync(nameServer.Host, nameServer.Port, nameServer.Protocol, resolvedNameServers); else resolvedNameServers.Add(nameServer); } return resolvedNameServers; } private async Task ResolveNameServerAddressesAsync(string nsDomain, int port, DnsTransportProtocol protocol, List outNameServers) { try { DnsDatagram response = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.A, DnsClass.IN)); if (response.Answer.Count > 0) { IReadOnlyList addresses = DnsClient.ParseResponseA(response); foreach (IPAddress address in addresses) outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol)); } } catch { } if (_dnsServer.PreferIPv6) { try { DnsDatagram response = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.AAAA, DnsClass.IN)); if (response.Answer.Count > 0) { IReadOnlyList addresses = DnsClient.ParseResponseAAAA(response); foreach (IPAddress address in addresses) outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol)); } } catch { } } } private Task ResolveNameServerAddressesAsync(DnsResourceRecord nsRecord, List outNameServers) { string nsDomain = (nsRecord.RDATA as DnsNSRecordData).NameServer; IReadOnlyList glueRecords = nsRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) { foreach (DnsResourceRecord glueRecord in glueRecords) { switch (glueRecord.Type) { case DnsResourceRecordType.A: outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsARecordData).Address)); break; case DnsResourceRecordType.AAAA: if (_dnsServer.PreferIPv6) outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsAAAARecordData).Address)); break; } } return Task.CompletedTask; } else { return ResolveNameServerAddressesAsync(nsDomain, 53, DnsTransportProtocol.Udp, outNameServers); } } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { if (base.Disabled == value) return; base.Disabled = value; //set value early to be able to use it for setting catalog properties CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (value) { //remove catalog zone membership without removing it from zone's options catalogZone.RemoveMemberZone(_name); _dnsServer.AuthZoneManager.SaveZoneFile(catalogZone._name); } else { //add catalog zone membership _dnsServer.AuthZoneManager.AddCatalogMemberZone(_catalogZoneName, new AuthZoneInfo(this), true); } } } } public DateTime LastModified { get { return _lastModified; } } public virtual string CatalogZoneName { get { return _catalogZoneName; } set { if (string.IsNullOrEmpty(value)) _catalogZoneName = null; else _catalogZoneName = value; //reset _catalogZone = null; _secondaryCatalogZone = null; } } public virtual bool OverrideCatalogQueryAccess { get { return _overrideCatalogQueryAccess; } set { _overrideCatalogQueryAccess = value; } } public virtual bool OverrideCatalogZoneTransfer { get { return _overrideCatalogZoneTransfer; } set { _overrideCatalogZoneTransfer = value; } } public virtual bool OverrideCatalogNotify { get { return _overrideCatalogNotify; } set { _overrideCatalogNotify = value; } } public virtual AuthZoneQueryAccess QueryAccess { get { return _queryAccess; } set { _queryAccess = value; //update catalog zone property if (this is CatalogZone thisCatalogZone) { //update global custom property thisCatalogZone.SetAllowQueryProperty(GetQueryAccessACL()); } else if (!Disabled && ((this is PrimaryZone) || (this is StubZone) || (this is ForwarderZone))) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_overrideCatalogQueryAccess) catalogZone.SetAllowQueryProperty(GetQueryAccessACL(), _name); //update member zone custom property else catalogZone.SetAllowQueryProperty(null, _name); //remove member zone custom property } } } } public IReadOnlyCollection QueryAccessNetworkACL { get { return _queryAccessNetworkACL; } set { if ((value is null) || (value.Count == 0)) _queryAccessNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(QueryAccessNetworkACL), "Network ACL cannot have more than 255 entries."); else _queryAccessNetworkACL = value; } } public virtual AuthZoneTransfer ZoneTransfer { get { return _zoneTransfer; } set { _zoneTransfer = value; //update catalog zone property if (this is CatalogZone thisCatalogZone) { //update global custom property thisCatalogZone.SetAllowTransferProperty(GetZoneTranferACL()); } else if (!Disabled && (this is PrimaryZone)) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_overrideCatalogZoneTransfer) catalogZone.SetAllowTransferProperty(GetZoneTranferACL(), _name); //update member zone custom property else catalogZone.SetAllowTransferProperty(null, _name); //remove member zone custom property } } } } public IReadOnlyCollection ZoneTransferNetworkACL { get { return _zoneTransferNetworkACL; } set { if ((value is null) || (value.Count == 0)) _zoneTransferNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ZoneTransferNetworkACL), "Network ACL cannot have more than 255 entries."); else _zoneTransferNetworkACL = value; } } public IReadOnlyDictionary ZoneTransferTsigKeyNames { get { return _zoneTransferTsigKeyNames; } set { if ((value is null) || (value.Count == 0)) _zoneTransferTsigKeyNames = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ZoneTransferTsigKeyNames), "Zone transfer TSIG key names cannot have more than 255 entries."); else _zoneTransferTsigKeyNames = value; //update catalog zone property if (this is CatalogZone thisCatalogZone) { //update global custom property thisCatalogZone.SetZoneTransferTsigKeyNamesProperty(_zoneTransferTsigKeyNames); } else if (!Disabled && (this is PrimaryZone)) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_overrideCatalogZoneTransfer) catalogZone.SetZoneTransferTsigKeyNamesProperty(_zoneTransferTsigKeyNames, _name); //update member zone custom property else catalogZone.SetZoneTransferTsigKeyNamesProperty(null, _name); //remove member zone custom property } } } } public virtual AuthZoneNotify Notify { get { return _notify; } set { _notify = value; lock (_notifyFailed) { _notifyFailed.Clear(); } } } public IReadOnlyCollection NotifyNameServers { get { return _notifyNameServers; } set { if ((value is null) || (value.Count == 0)) _notifyNameServers = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(NotifyNameServers), "Name server addresses cannot have more than 255 entries."); else _notifyNameServers = value; lock (_notifyFailed) { _notifyFailed.Clear(); } } } public IReadOnlyCollection NotifySecondaryCatalogNameServers { get { return _notifySecondaryCatalogNameServers; } set { if ((value is null) || (value.Count == 0)) _notifySecondaryCatalogNameServers = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(NotifySecondaryCatalogNameServers), "Secondary Catalog name server addresses cannot have more than 255 entries."); else _notifySecondaryCatalogNameServers = value; lock (_notifyFailed) { _notifyFailed.Clear(); } } } public virtual AuthZoneUpdate Update { get { return _update; } set { _update = value; } } public IReadOnlyCollection UpdateNetworkACL { get { return _updateNetworkACL; } set { if ((value is null) || (value.Count == 0)) _updateNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(UpdateNetworkACL), "Network ACL cannot have more than 255 entries."); else _updateNetworkACL = value; } } public IReadOnlyDictionary>> UpdateSecurityPolicies { get { return _updateSecurityPolicies; } set { _updateSecurityPolicies = value; } } public AuthZoneDnssecStatus DnssecStatus { get { return _dnssecStatus; } } public string[] NotifyFailed { get { if (_notifyFailed is null) return Array.Empty(); lock (_notifyFailed) { if (_notifyFailed.Count > 0) return _notifyFailed.ToArray(); return Array.Empty(); } } } public bool SyncFailed { get { return _syncFailed; } } public CatalogZone CatalogZone { get { if (_catalogZoneName is null) return null; if (_secondaryCatalogZone is not null) return null; if (_catalogZone is null) { if ((this is PrimaryZone) || (this is ForwarderZone)) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is CatalogZone catalogZone) _catalogZone = catalogZone; } else if (this is StubZone) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is CatalogZone catalogZone) _catalogZone = catalogZone; else if (apexZone is SecondaryCatalogZone secondaryCatalogZone) _secondaryCatalogZone = secondaryCatalogZone; } } return _catalogZone; } } public SecondaryCatalogZone SecondaryCatalogZone { get { if (_catalogZoneName is null) return null; if (_catalogZone is not null) return null; if (_secondaryCatalogZone is null) { if (this is SecondaryZone) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is SecondaryCatalogZone secondaryCatalogZone) _secondaryCatalogZone = secondaryCatalogZone; } else if (this is StubZone) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is SecondaryCatalogZone secondaryCatalogZone) _secondaryCatalogZone = secondaryCatalogZone; else if (apexZone is CatalogZone catalogZone) _catalogZone = catalogZone; } } return _secondaryCatalogZone; } } #endregion } }