/*
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
}
}