diff --git a/DnsServerCore/Dns/Zones/SecondaryZone.cs b/DnsServerCore/Dns/Zones/SecondaryZone.cs
index f3bd3773..0886b501 100644
--- a/DnsServerCore/Dns/Zones/SecondaryZone.cs
+++ b/DnsServerCore/Dns/Zones/SecondaryZone.cs
@@ -20,6 +20,8 @@ along with this program. If not, see .
using DnsServerCore.Dns.ResourceRecords;
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary;
@@ -28,6 +30,9 @@ using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.Zones
{
+ //Message Digest for DNS Zones
+ //https://datatracker.ietf.org/doc/rfc8976/
+
class SecondaryZone : ApexZone
{
#region variables
@@ -48,6 +53,8 @@ namespace DnsServerCore.Dns.Zones
DateTime _expiry;
bool _isExpired;
+ bool _validationFailed;
+
bool _resync;
#endregion
@@ -60,6 +67,7 @@ namespace DnsServerCore.Dns.Zones
_dnsServer = dnsServer;
_expiry = zoneInfo.Expiry;
+ _validationFailed = zoneInfo.ValidationFailed;
_isExpired = DateTime.UtcNow > _expiry;
_refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
@@ -83,7 +91,7 @@ namespace DnsServerCore.Dns.Zones
#region static
- public static async Task CreateAsync(DnsServer dnsServer, string name, string primaryNameServerAddresses = null, DnsTransportProtocol zoneTransferProtocol = DnsTransportProtocol.Tcp, string tsigKeyName = null)
+ public static async Task CreateAsync(DnsServer dnsServer, string name, string primaryNameServerAddresses = null, DnsTransportProtocol zoneTransferProtocol = DnsTransportProtocol.Tcp, string tsigKeyName = null, bool validateZone = false)
{
switch (zoneTransferProtocol)
{
@@ -134,9 +142,9 @@ namespace DnsServerCore.Dns.Zones
DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { soaQuestion }, null, null, null, dnsServer.UdpPayloadSize);
if (string.IsNullOrEmpty(tsigKeyName))
- soaResponse = await dnsClient.ResolveAsync(soaRequest);
+ soaResponse = await dnsClient.RawResolveAsync(soaRequest);
else if ((dnsServer.TsigKeys is not null) && dnsServer.TsigKeys.TryGetValue(tsigKeyName, out TsigKey key))
- soaResponse = await dnsClient.ResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
+ soaResponse = await dnsClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
else
throw new DnsServerException("No such TSIG key was found configured: " + tsigKeyName);
}
@@ -159,6 +167,7 @@ namespace DnsServerCore.Dns.Zones
authRecordInfo.PrimaryNameServers = primaryNameServers;
authRecordInfo.ZoneTransferProtocol = zoneTransferProtocol;
authRecordInfo.TsigKeyName = tsigKeyName;
+ authRecordInfo.ValidateZone = validateZone;
secondaryZone._entries[DnsResourceRecordType.SOA] = soaRR;
@@ -222,9 +231,7 @@ namespace DnsServerCore.Dns.Zones
if (primaryNameServers.Count == 0)
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server could not find primary name server IP addresses for secondary zone: " + (_name == "" ? "" : _name));
+ _dnsServer.LogManager?.Write("DNS Server could not find primary name server IP addresses for secondary zone: " + (_name == "" ? "" : _name));
//set timer for retry
ResetRefreshTimer(currentSoa.Retry * 1000);
@@ -237,9 +244,7 @@ namespace DnsServerCore.Dns.Zones
if (!string.IsNullOrEmpty(recordInfo.TsigKeyName) && ((_dnsServer.TsigKeys is null) || !_dnsServer.TsigKeys.TryGetValue(recordInfo.TsigKeyName, out key)))
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server does not have TSIG key '" + recordInfo.TsigKeyName + "' configured for refreshing secondary zone: " + (_name == "" ? "" : _name));
+ _dnsServer.LogManager?.Write("DNS Server does not have TSIG key '" + recordInfo.TsigKeyName + "' configured for refreshing secondary zone: " + (_name == "" ? "" : _name));
//set timer for retry
ResetRefreshTimer(currentSoa.Retry * 1000);
@@ -248,16 +253,21 @@ namespace DnsServerCore.Dns.Zones
}
//refresh zone
- if (await RefreshZoneAsync(primaryNameServers, recordInfo.ZoneTransferProtocol, key))
+ if (await RefreshZoneAsync(primaryNameServers, recordInfo.ZoneTransferProtocol, key, recordInfo.ValidateZone))
{
- //zone refreshed; set timer for refresh
DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
- ResetRefreshTimer(latestSoa.Refresh * 1000);
+
_syncFailed = false;
_expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire);
_isExpired = false;
_resync = false;
_dnsServer.AuthZoneManager.SaveZoneFile(_name);
+
+ if (_validationFailed)
+ ResetRefreshTimer(latestSoa.Retry * 1000); //zone validation failed, set timer for retry
+ else
+ ResetRefreshTimer(latestSoa.Refresh * 1000); //zone refreshed; set timer for refresh
+
return;
}
@@ -268,9 +278,7 @@ namespace DnsServerCore.Dns.Zones
}
catch (Exception ex)
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write(ex);
+ _dnsServer.LogManager?.Write(ex);
//set timer for retry
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
@@ -287,20 +295,15 @@ namespace DnsServerCore.Dns.Zones
{
lock (_refreshTimerLock)
{
- if (_refreshTimer != null)
- _refreshTimer.Change(dueTime, Timeout.Infinite);
+ _refreshTimer?.Change(dueTime, Timeout.Infinite);
}
}
- private async Task RefreshZoneAsync(IReadOnlyList primaryNameServers, DnsTransportProtocol zoneTransferProtocol, TsigKey key)
+ private async Task RefreshZoneAsync(IReadOnlyList primaryNameServers, DnsTransportProtocol zoneTransferProtocol, TsigKey key, bool validateZone)
{
try
{
- {
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server has started zone refresh for secondary zone: " + (_name == "" ? "" : _name));
- }
+ _dnsServer.LogManager?.Write("DNS Server has started zone refresh for secondary zone: " + (_name == "" ? "" : _name));
DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];
DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;
@@ -330,25 +333,19 @@ namespace DnsServerCore.Dns.Zones
DnsDatagram soaResponse;
if (key is null)
- soaResponse = await client.ResolveAsync(soaRequest);
+ soaResponse = await client.RawResolveAsync(soaRequest);
else
- soaResponse = await client.ResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
+ soaResponse = await client.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
if (soaResponse.RCODE != DnsResponseCode.NoError)
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
-
+ _dnsServer.LogManager?.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
return false;
}
if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase))
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
-
+ _dnsServer.LogManager?.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
return false;
}
@@ -358,10 +355,7 @@ namespace DnsServerCore.Dns.Zones
//compare using sequence space arithmetic
if (!currentSoa.IsZoneUpdateAvailable(receivedSoa))
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + soaResponse.Metadata.NameServer.ToString());
-
+ _dnsServer.LogManager?.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + soaResponse.Metadata.NameServer.ToString());
return true;
}
}
@@ -427,9 +421,9 @@ namespace DnsServerCore.Dns.Zones
DnsDatagram xfrResponse;
if (key is null)
- xfrResponse = await xfrClient.ResolveAsync(xfrRequest);
+ xfrResponse = await xfrClient.RawResolveAsync(xfrRequest);
else
- xfrResponse = await xfrClient.ResolveAsync(xfrRequest, key, REFRESH_TSIG_FUDGE);
+ xfrResponse = await xfrClient.TsigResolveAsync(xfrRequest, key, REFRESH_TSIG_FUDGE);
if (doIXFR && ((xfrResponse.RCODE == DnsResponseCode.NotImplemented) || (xfrResponse.RCODE == DnsResponseCode.Refused)))
{
@@ -439,28 +433,19 @@ namespace DnsServerCore.Dns.Zones
if (xfrResponse.RCODE != DnsResponseCode.NoError)
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
-
+ _dnsServer.LogManager?.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
return false;
}
if (xfrResponse.Answer.Count < 1)
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server received an empty response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
-
+ _dnsServer.LogManager?.Write("DNS Server received an empty response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
return false;
}
if (!_name.Equals(xfrResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase) || (xfrResponse.Answer[0].Type != DnsResourceRecordType.SOA) || (xfrResponse.Answer[0].RDATA is not DnsSOARecordData xfrSoa))
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server received invalid response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
-
+ _dnsServer.LogManager?.Write("DNS Server received invalid response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
return false;
}
@@ -484,18 +469,26 @@ namespace DnsServerCore.Dns.Zones
_lastModified = DateTime.UtcNow;
- //trigger notify
- TriggerNotify();
+ if (validateZone)
+ await ValidateZoneAsync();
+ else
+ _validationFailed = false;
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
+ if (_validationFailed)
+ {
+ _dnsServer.LogManager?.Write("DNS Server refreshed '" + (_name == "" ? "" : _name) + "' secondary zone with validation failure from: " + xfrResponse.Metadata.NameServer.ToString());
+ }
+ else
+ {
+ //trigger notify
+ TriggerNotify();
+
+ _dnsServer.LogManager?.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
+ }
}
else
{
- LogManager log = _dnsServer.LogManager;
- if (log != null)
- log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + xfrResponse.Metadata.NameServer.ToString());
+ _dnsServer.LogManager?.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + xfrResponse.Metadata.NameServer.ToString());
}
return true;
@@ -504,7 +497,7 @@ namespace DnsServerCore.Dns.Zones
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
- if (log != null)
+ if (log is not null)
{
string strNameServers = null;
@@ -523,6 +516,219 @@ namespace DnsServerCore.Dns.Zones
}
}
+ private async Task ValidateZoneAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ DnsClientInternal dnsClient = new DnsClientInternal(_dnsServer);
+
+ dnsClient.DnssecValidation = true;
+ dnsClient.Timeout = 10000;
+
+ IReadOnlyList zoneMdList = DnsClient.ParseResponseZONEMD(await dnsClient.ResolveAsync(_name, DnsResourceRecordType.ZONEMD, cancellationToken));
+ if (zoneMdList.Count == 0)
+ {
+ //ZONEMD RRSet does not exists; digest verification cannot occur
+ _validationFailed = false;
+ _dnsServer.LogManager?.Write("ZONEMD validation cannot occur for the secondary zone '" + (_name.Length == 0 ? "" : _name) + "': ZONEMD RRset does not exists in the zone.");
+ return;
+ }
+
+ for (int i = 0; i < zoneMdList.Count; i++)
+ {
+ for (int j = 0; j < zoneMdList.Count; j++)
+ {
+ if (i == j)
+ continue; //skip comparing self
+
+ DnsZONEMDRecordData zoneMd = zoneMdList[i];
+ DnsZONEMDRecordData checkZoneMd = zoneMdList[j];
+
+ if ((checkZoneMd.Scheme == zoneMd.Scheme) && (checkZoneMd.HashAlgorithm == zoneMd.HashAlgorithm))
+ {
+ _validationFailed = true;
+ _dnsServer.LogManager?.Write("ZONEMD validation failed for the secondary zone '" + (_name.Length == 0 ? "" : _name) + "': ZONEMD RRset contains more than one RR with the same Scheme and Hash Algorithm.");
+ return;
+ }
+ }
+ }
+
+ DnsSOARecordData soa = DnsClient.ParseResponseSOA(await dnsClient.ResolveAsync(_name, DnsResourceRecordType.SOA, cancellationToken));
+ if (soa is null)
+ {
+ _validationFailed = true;
+ _dnsServer.LogManager?.Write("ZONEMD validation failed for the secondary zone '" + (_name.Length == 0 ? "" : _name) + "': failed to find SOA record.");
+ return;
+ }
+
+ using MemoryStream hashStream = new MemoryStream(4096);
+ byte[] computedDigestSHA384 = null;
+ byte[] computedDigestSHA512 = null;
+ bool zoneSerialized = false;
+
+ foreach (DnsZONEMDRecordData zoneMd in zoneMdList)
+ {
+ if (soa.Serial != zoneMd.Serial)
+ continue;
+
+ if (zoneMd.Scheme != ZoneMdScheme.Simple)
+ continue;
+
+ byte[] computedDigest;
+
+ switch (zoneMd.HashAlgorithm)
+ {
+ case ZoneMdHashAlgorithm.SHA384:
+ if (zoneMd.Digest.Length != 48)
+ continue;
+
+ if (computedDigestSHA384 is null)
+ {
+ if (!zoneSerialized)
+ {
+ SerializeZoneTo(hashStream);
+ zoneSerialized = true;
+ }
+
+ hashStream.Position = 0;
+ computedDigestSHA384 = SHA384.HashData(hashStream);
+ }
+
+ computedDigest = computedDigestSHA384;
+ break;
+
+ case ZoneMdHashAlgorithm.SHA512:
+ if (zoneMd.Digest.Length != 64)
+ continue;
+
+ if (computedDigestSHA512 is null)
+ {
+ if (!zoneSerialized)
+ {
+ SerializeZoneTo(hashStream);
+ zoneSerialized = true;
+ }
+
+ hashStream.Position = 0;
+ computedDigestSHA512 = SHA512.HashData(hashStream);
+ }
+
+ computedDigest = computedDigestSHA512;
+ break;
+
+ default:
+ continue;
+ }
+
+ if (computedDigest.Equals(zoneMd.Digest))
+ {
+ //validation successfull
+ _validationFailed = false;
+ _dnsServer.LogManager?.Write("ZONEMD validation was completed successfully for the secondary zone: " + (_name.Length == 0 ? "" : _name));
+ return;
+ }
+ }
+
+ //validation failed
+ _validationFailed = true;
+ _dnsServer.LogManager?.Write("ZONEMD validation failed for the secondary zone '" + (_name.Length == 0 ? "" : _name) + "': none of the ZONEMD records could successfully validate the zone.");
+ }
+ catch (Exception ex)
+ {
+ //validation failed
+ _validationFailed = true;
+ _dnsServer.LogManager?.Write("ZONEMD validation failed for the secondary zone '" + (_name.Length == 0 ? "" : _name) + "':\r\n" + ex.ToString());
+ }
+ }
+
+ private void SerializeZoneTo(MemoryStream hashStream)
+ {
+ //list zone records for ZONEMD Simple scheme
+ List records;
+ {
+ List allZoneRecords = new List();
+
+ _dnsServer.AuthZoneManager.ListAllZoneRecords(_name, allZoneRecords);
+
+ records = new List(allZoneRecords.Count);
+
+ foreach (DnsResourceRecord record in allZoneRecords)
+ {
+ switch (record.Type)
+ {
+ case DnsResourceRecordType.NS:
+ records.Add(record);
+
+ IReadOnlyList glueRecords = record.GetAuthNSRecordInfo().GlueRecords;
+ if (glueRecords is not null)
+ records.AddRange(glueRecords);
+
+ break;
+
+ case DnsResourceRecordType.RRSIG:
+ if (record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase) && (record.RDATA is DnsRRSIGRecordData rdata) && (rdata.TypeCovered == DnsResourceRecordType.ZONEMD))
+ break; //skip RRSIG covering the apex ZONEMD
+
+ records.Add(record);
+ break;
+
+ case DnsResourceRecordType.ZONEMD:
+ if (record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase))
+ break; //skip apex ZONEMD
+
+ records.Add(record);
+ break;
+
+ default:
+ records.Add(record);
+ break;
+ }
+ }
+ }
+
+ //group records into zones by DNS name
+ List>>> zones = new List>>>(DnsResourceRecord.GroupRecords(records, true));
+
+ //sort zones by canonical DNS name
+ zones.Sort(delegate (KeyValuePair>> x, KeyValuePair>> y)
+ {
+ return DnsNSECRecordData.CanonicalComparison(x.Key, y.Key);
+ });
+
+ //start serialization, zone by zone
+ using MemoryStream rrBuffer = new MemoryStream(512);
+
+ foreach (KeyValuePair>> zone in zones)
+ {
+ //list all RRSets for current zone owner name
+ List>> rrSets = new List>>(zone.Value);
+
+ //RRsets having the same owner name MUST be numerically ordered, in ascending order, by their numeric RR TYPE
+ rrSets.Sort(delegate (KeyValuePair> x, KeyValuePair> y)
+ {
+ return x.Key.CompareTo(y.Key);
+ });
+
+ //serialize records
+ List rrList = new List(rrSets.Count * 4);
+
+ foreach (KeyValuePair> rrSet in rrSets)
+ {
+ //serialize current RRSet records
+ List serializedResourceRecords = new List(rrSet.Value.Count);
+
+ foreach (DnsResourceRecord record in rrSet.Value)
+ serializedResourceRecords.Add(CanonicallySerializedResourceRecord.Create(record.Name, record.Type, record.Class, record.OriginalTtlValue, record.RDATA, rrBuffer));
+
+ //Canonical RR Ordering by sorting RDATA portion of the canonical form of each RR
+ serializedResourceRecords.Sort();
+
+ foreach (CanonicallySerializedResourceRecord serializedResourceRecord in serializedResourceRecords)
+ serializedResourceRecord.WriteTo(hashStream);
+ }
+ }
+ }
+
private void CommitZoneHistory(IReadOnlyList historyRecords)
{
lock (_zoneHistory)
@@ -636,6 +842,9 @@ namespace DnsServerCore.Dns.Zones
public bool IsExpired
{ get { return _isExpired; } }
+ public bool ValidationFailed
+ { get { return _validationFailed; } }
+
public override bool Disabled
{
get { return _disabled; }
@@ -661,9 +870,56 @@ namespace DnsServerCore.Dns.Zones
public override bool IsActive
{
- get { return !_disabled && !_isExpired; }
+ get { return !_disabled && !_isExpired && !_validationFailed; }
}
#endregion
+
+ class DnsClientInternal : DnsClient, IDnsCache
+ {
+ #region variables
+
+ readonly DnsServer _dnsServer;
+
+ #endregion
+
+ #region constructor
+
+ public DnsClientInternal(DnsServer dnsServer)
+ {
+ _dnsServer = dnsServer;
+ Cache = this; //set dummy cache to avoid DnsCache from overwriting DnsResourceRecord.Tag properties which currently has GenericRecordInfo objects
+ }
+
+ #endregion
+
+ #region protected
+
+ protected override Task InternalResolveAsync(DnsDatagram request, CancellationToken cancellationToken)
+ {
+ return _dnsServer.DirectQueryAsync(request, Timeout);
+ }
+
+ #endregion
+
+ #region public
+
+ public DnsDatagram QueryClosestDelegation(DnsDatagram request)
+ {
+ return null; //no cache available
+ }
+
+ public DnsDatagram Query(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false)
+ {
+ return null; //no cache available
+ }
+
+ public void CacheResponse(DnsDatagram response, bool isDnssecBadCache = false, string zoneCut = null)
+ {
+ //do nothing to prevent caching
+ }
+
+ #endregion
+ }
}
}