mirror of
https://github.com/fergalmoran/DnsServer.git
synced 2026-02-14 11:53:57 +00:00
984 lines
41 KiB
C#
984 lines
41 KiB
C#
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
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;
|
|
using TechnitiumLibrary.Net.Dns;
|
|
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
|
|
|
|
readonly object _refreshTimerLock = new object();
|
|
Timer _refreshTimer;
|
|
bool _refreshTimerTriggered;
|
|
const int REFRESH_TIMER_INTERVAL = 5000;
|
|
|
|
const int REFRESH_SOA_TIMEOUT = 10000;
|
|
const int REFRESH_XFR_TIMEOUT = 120000;
|
|
const int REFRESH_RETRIES = 5;
|
|
|
|
const int REFRESH_TSIG_FUDGE = 300;
|
|
|
|
bool _overrideCatalogPrimaryNameServers;
|
|
|
|
IReadOnlyList<NameServerAddress> _primaryNameServerAddresses;
|
|
DnsTransportProtocol _primaryZoneTransferProtocol;
|
|
string _primaryZoneTransferTsigKeyName;
|
|
|
|
DateTime _expiry;
|
|
bool _isExpired;
|
|
|
|
bool _validateZone;
|
|
bool _validationFailed;
|
|
|
|
bool _resync;
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
public SecondaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)
|
|
: base(dnsServer, zoneInfo)
|
|
{
|
|
_overrideCatalogPrimaryNameServers = zoneInfo.OverrideCatalogPrimaryNameServers;
|
|
|
|
_primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses;
|
|
_primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol;
|
|
_primaryZoneTransferTsigKeyName = zoneInfo.PrimaryZoneTransferTsigKeyName;
|
|
|
|
_expiry = zoneInfo.Expiry;
|
|
_isExpired = DateTime.UtcNow > _expiry;
|
|
|
|
_validateZone = zoneInfo.ValidateZone;
|
|
_validationFailed = zoneInfo.ValidationFailed;
|
|
|
|
_refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
|
|
|
|
InitNotify();
|
|
}
|
|
|
|
protected SecondaryZone(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol, string primaryZoneTransferTsigKeyName, bool validateZone)
|
|
: base(dnsServer, name)
|
|
{
|
|
PrimaryZoneTransferProtocol = primaryZoneTransferProtocol;
|
|
|
|
PrimaryNameServerAddresses = primaryNameServerAddresses?.Convert(delegate (NameServerAddress nameServer)
|
|
{
|
|
if (nameServer.Protocol != primaryZoneTransferProtocol)
|
|
nameServer = nameServer.ChangeProtocol(primaryZoneTransferProtocol);
|
|
|
|
return nameServer;
|
|
});
|
|
|
|
PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName;
|
|
_validateZone = validateZone;
|
|
|
|
_isExpired = true; //new secondary zone is considered expired till it refreshes
|
|
|
|
_refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
|
|
|
|
InitNotify();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region static
|
|
|
|
public static async Task<SecondaryZone> CreateAsync(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false)
|
|
{
|
|
SecondaryZone secondaryZone = new SecondaryZone(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone);
|
|
|
|
try
|
|
{
|
|
DnsDatagram soaResponse;
|
|
|
|
try
|
|
{
|
|
DnsQuestionRecord soaQuestion = new DnsQuestionRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN);
|
|
|
|
if (secondaryZone.PrimaryNameServerAddresses is null)
|
|
{
|
|
soaResponse = await secondaryZone._dnsServer.DirectQueryAsync(soaQuestion);
|
|
}
|
|
else
|
|
{
|
|
DnsClient dnsClient = new DnsClient(secondaryZone.PrimaryNameServerAddresses);
|
|
|
|
foreach (NameServerAddress nameServerAddress in dnsClient.Servers)
|
|
{
|
|
if (nameServerAddress.IsIPEndPointStale)
|
|
await nameServerAddress.ResolveIPAddressAsync(secondaryZone._dnsServer, secondaryZone._dnsServer.PreferIPv6);
|
|
}
|
|
|
|
dnsClient.Proxy = secondaryZone._dnsServer.Proxy;
|
|
dnsClient.PreferIPv6 = secondaryZone._dnsServer.PreferIPv6;
|
|
|
|
DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [soaQuestion], null, null, null, secondaryZone._dnsServer.UdpPayloadSize);
|
|
|
|
if (string.IsNullOrEmpty(primaryZoneTransferTsigKeyName))
|
|
soaResponse = await dnsClient.RawResolveAsync(soaRequest);
|
|
else if ((secondaryZone._dnsServer.TsigKeys is not null) && secondaryZone._dnsServer.TsigKeys.TryGetValue(primaryZoneTransferTsigKeyName, out TsigKey key))
|
|
soaResponse = await dnsClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
|
|
else
|
|
throw new DnsServerException("No such TSIG key was found configured: " + primaryZoneTransferTsigKeyName);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new DnsServerException("DNS Server failed to find SOA record for: " + secondaryZone.ToString(), ex);
|
|
}
|
|
|
|
if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA))
|
|
throw new DnsServerException("DNS Server did not receive SOA record in response from any of the primary name servers for: " + secondaryZone.ToString());
|
|
|
|
DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];
|
|
DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;
|
|
|
|
DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum);
|
|
DnsResourceRecord soaRecord = new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, receivedSoaRecord.OriginalTtlValue, soa);
|
|
|
|
secondaryZone._entries[DnsResourceRecordType.SOA] = [soaRecord];
|
|
}
|
|
catch
|
|
{
|
|
if (!ignoreSoaFailure)
|
|
throw;
|
|
|
|
//continue with dummy SOA
|
|
DnsSOARecordData soa = new DnsSOARecordData(secondaryZone._dnsServer.ServerDomain, "invalid", 0, 300, 60, 604800, 900);
|
|
DnsResourceRecord soaRecord = new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);
|
|
soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;
|
|
|
|
secondaryZone._entries[DnsResourceRecordType.SOA] = [soaRecord];
|
|
}
|
|
|
|
return secondaryZone;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
|
|
bool _disposed;
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
try
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
if (disposing)
|
|
{
|
|
lock (_refreshTimerLock)
|
|
{
|
|
if (_refreshTimer != null)
|
|
{
|
|
_refreshTimer.Dispose();
|
|
_refreshTimer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
finally
|
|
{
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region private
|
|
|
|
private async void RefreshTimerCallback(object state)
|
|
{
|
|
try
|
|
{
|
|
if (Disabled && !_resync)
|
|
return;
|
|
|
|
_isExpired = DateTime.UtcNow > _expiry;
|
|
|
|
//get primary name server addresses
|
|
IReadOnlyList<NameServerAddress> primaryNameServerAddresses;
|
|
DnsTransportProtocol primaryZoneTransferProtocol;
|
|
string primaryZoneTransferTsigKeyName;
|
|
|
|
SecondaryCatalogZone secondaryCatalogZone = SecondaryCatalogZone;
|
|
|
|
if ((secondaryCatalogZone is not null) && !_overrideCatalogPrimaryNameServers)
|
|
{
|
|
primaryNameServerAddresses = await GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses);
|
|
primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol;
|
|
primaryZoneTransferTsigKeyName = secondaryCatalogZone.PrimaryZoneTransferTsigKeyName;
|
|
}
|
|
else
|
|
{
|
|
primaryNameServerAddresses = await GetResolvedPrimaryNameServerAddressesAsync();
|
|
primaryZoneTransferProtocol = _primaryZoneTransferProtocol;
|
|
primaryZoneTransferTsigKeyName = _primaryZoneTransferTsigKeyName;
|
|
}
|
|
|
|
DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];
|
|
DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;
|
|
|
|
if (primaryNameServerAddresses.Count == 0)
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server could not find primary name server IP addresses for " + GetZoneTypeName() + " zone: " + ToString());
|
|
|
|
//set timer for retry
|
|
ResetRefreshTimer(currentSoa.Retry * 1000);
|
|
_syncFailed = true;
|
|
return;
|
|
}
|
|
|
|
TsigKey key = null;
|
|
|
|
if (!string.IsNullOrEmpty(primaryZoneTransferTsigKeyName) && ((_dnsServer.TsigKeys is null) || !_dnsServer.TsigKeys.TryGetValue(primaryZoneTransferTsigKeyName, out key)))
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server does not have TSIG key '" + primaryZoneTransferTsigKeyName + "' configured for refreshing " + GetZoneTypeName() + " zone: " + ToString());
|
|
|
|
//set timer for retry
|
|
ResetRefreshTimer(currentSoa.Retry * 1000);
|
|
_syncFailed = true;
|
|
return;
|
|
}
|
|
|
|
//refresh zone
|
|
if (await RefreshZoneAsync(primaryNameServerAddresses, primaryZoneTransferProtocol, key, _validateZone))
|
|
{
|
|
DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
|
|
|
|
_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;
|
|
}
|
|
|
|
//no response from any of the name servers; set timer for retry
|
|
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
|
|
ResetRefreshTimer(soa.Retry * 1000);
|
|
_syncFailed = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.LogManager?.Write(ex);
|
|
|
|
//set timer for retry
|
|
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
|
|
ResetRefreshTimer(soa.Retry * 1000);
|
|
_syncFailed = true;
|
|
}
|
|
finally
|
|
{
|
|
_refreshTimerTriggered = false;
|
|
}
|
|
}
|
|
|
|
private void ResetRefreshTimer(long dueTime)
|
|
{
|
|
lock (_refreshTimerLock)
|
|
{
|
|
_refreshTimer?.Change(dueTime, Timeout.Infinite);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> RefreshZoneAsync(IReadOnlyList<NameServerAddress> primaryNameServers, DnsTransportProtocol zoneTransferProtocol, TsigKey key, bool validateZone)
|
|
{
|
|
try
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server has started zone refresh for " + GetZoneTypeName() + " zone: " + ToString());
|
|
|
|
//get nameservers list with correct zone tranfer protocol
|
|
List<NameServerAddress> updatedNameServers = new List<NameServerAddress>(primaryNameServers.Count);
|
|
{
|
|
switch (zoneTransferProtocol)
|
|
{
|
|
case DnsTransportProtocol.Tls:
|
|
case DnsTransportProtocol.Quic:
|
|
//change name server protocol to TLS/QUIC
|
|
foreach (NameServerAddress primaryNameServer in primaryNameServers)
|
|
{
|
|
if (primaryNameServer.Protocol == zoneTransferProtocol)
|
|
updatedNameServers.Add(primaryNameServer);
|
|
else
|
|
updatedNameServers.Add(primaryNameServer.ChangeProtocol(zoneTransferProtocol));
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
//change name server protocol to TCP
|
|
foreach (NameServerAddress primaryNameServer in primaryNameServers)
|
|
{
|
|
if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp)
|
|
updatedNameServers.Add(primaryNameServer);
|
|
else
|
|
updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
//init XFR DNS Client
|
|
DnsClient xfrClient = new DnsClient(updatedNameServers);
|
|
xfrClient.Proxy = _dnsServer.Proxy;
|
|
xfrClient.PreferIPv6 = _dnsServer.PreferIPv6;
|
|
xfrClient.Retries = REFRESH_RETRIES;
|
|
xfrClient.Concurrency = 1;
|
|
|
|
DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];
|
|
DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;
|
|
|
|
if (!_resync && (this is not SecondaryForwarderZone)) //skip SOA probe for Secondary Forwarder/Catalog since Forwarder/Catalog is not authoritative for SOA
|
|
{
|
|
//check for update
|
|
xfrClient.Timeout = REFRESH_SOA_TIMEOUT;
|
|
|
|
DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN)], null, null, null, _dnsServer.UdpPayloadSize);
|
|
DnsDatagram soaResponse;
|
|
|
|
if (key is null)
|
|
soaResponse = await xfrClient.RawResolveAsync(soaRequest);
|
|
else
|
|
soaResponse = await xfrClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
|
|
|
|
if (soaResponse.RCODE != DnsResponseCode.NoError)
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + ToString() + "' " + GetZoneTypeName() + " 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))
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server received an empty response for SOA query for '" + ToString() + "' " + GetZoneTypeName() + " zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
|
|
return false;
|
|
}
|
|
|
|
DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];
|
|
DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;
|
|
|
|
//compare using sequence space arithmetic
|
|
if (!currentSoa.IsZoneUpdateAvailable(receivedSoa))
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server successfully checked for '" + ToString() + "' " + GetZoneTypeName() + " zone update from: " + soaResponse.Metadata.NameServer.ToString());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//update available; do zone transfer
|
|
xfrClient.Timeout = REFRESH_XFR_TIMEOUT;
|
|
|
|
bool doIXFR = !_isExpired && !_resync;
|
|
|
|
while (true)
|
|
{
|
|
DnsQuestionRecord xfrQuestion;
|
|
IReadOnlyList<DnsResourceRecord> xfrAuthority;
|
|
|
|
if (doIXFR)
|
|
{
|
|
xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.IXFR, DnsClass.IN);
|
|
xfrAuthority = [currentSoaRecord];
|
|
}
|
|
else
|
|
{
|
|
xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.AXFR, DnsClass.IN);
|
|
xfrAuthority = null;
|
|
}
|
|
|
|
DnsDatagram xfrRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [xfrQuestion], null, xfrAuthority);
|
|
DnsDatagram xfrResponse;
|
|
|
|
if (key is null)
|
|
xfrResponse = await xfrClient.RawResolveAsync(xfrRequest);
|
|
else
|
|
xfrResponse = await xfrClient.TsigResolveAsync(xfrRequest, key, REFRESH_TSIG_FUDGE);
|
|
|
|
if (doIXFR && ((xfrResponse.RCODE == DnsResponseCode.NotImplemented) || (xfrResponse.RCODE == DnsResponseCode.Refused)))
|
|
{
|
|
doIXFR = false;
|
|
continue;
|
|
}
|
|
|
|
if (xfrResponse.RCODE != DnsResponseCode.NoError)
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString());
|
|
return false;
|
|
}
|
|
|
|
if (xfrResponse.Answer.Count < 1)
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server received an empty response for zone transfer query for '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString());
|
|
return false;
|
|
}
|
|
|
|
if (!_name.Equals(xfrResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase) || (xfrResponse.Answer[0].RDATA is not DnsSOARecordData xfrSoa))
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server received invalid response for zone transfer query for '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString());
|
|
return false;
|
|
}
|
|
|
|
if (_resync || currentSoa.IsZoneUpdateAvailable(xfrSoa))
|
|
{
|
|
xfrResponse = xfrResponse.Join(); //join multi message response
|
|
|
|
if (doIXFR)
|
|
{
|
|
IReadOnlyList<DnsResourceRecord> historyRecords = _dnsServer.AuthZoneManager.SyncIncrementalZoneTransferRecords(_name, xfrResponse.Answer);
|
|
if (historyRecords.Count > 0)
|
|
await FinalizeIncrementalZoneTransferAsync(historyRecords);
|
|
else
|
|
await FinalizeZoneTransferAsync(); //AXFR response was received
|
|
}
|
|
else
|
|
{
|
|
_dnsServer.AuthZoneManager.SyncZoneTransferRecords(_name, xfrResponse.Answer);
|
|
await FinalizeZoneTransferAsync();
|
|
}
|
|
|
|
_lastModified = DateTime.UtcNow;
|
|
|
|
if (validateZone)
|
|
await ValidateZoneAsync();
|
|
else
|
|
_validationFailed = false;
|
|
|
|
if (_validationFailed)
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server refreshed '" + ToString() + "' " + GetZoneTypeName() + " zone with validation failure from: " + xfrResponse.Metadata.NameServer.ToString());
|
|
}
|
|
else
|
|
{
|
|
//trigger notify
|
|
TriggerNotify();
|
|
|
|
_dnsServer.LogManager?.Write("DNS Server successfully refreshed '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server successfully checked for '" + ToString() + "' " + GetZoneTypeName() + " zone update from: " + xfrResponse.Metadata.NameServer.ToString());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.LogManager?.Write("DNS Server failed to refresh '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + primaryNameServers.Join() + "\r\n" + ex.ToString());
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task ValidateZoneAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
DnsClientInternal dnsClient = new DnsClientInternal(_dnsServer);
|
|
|
|
dnsClient.DnssecValidation = true;
|
|
dnsClient.Timeout = 10000;
|
|
|
|
IReadOnlyList<DnsZONEMDRecordData> 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 " + GetZoneTypeName() + " zone '" + ToString() + "': 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 " + GetZoneTypeName() + " zone '" + ToString() + "': 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 " + GetZoneTypeName() + " zone '" + ToString() + "': 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.ListEquals(zoneMd.Digest))
|
|
{
|
|
//validation successfull
|
|
_validationFailed = false;
|
|
_dnsServer.LogManager?.Write("ZONEMD validation was completed successfully for the " + GetZoneTypeName() + " zone: " + ToString());
|
|
return;
|
|
}
|
|
}
|
|
|
|
//validation failed
|
|
_validationFailed = true;
|
|
_dnsServer.LogManager?.Write("ZONEMD validation failed for the " + GetZoneTypeName() + " zone '" + ToString() + "': 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 " + GetZoneTypeName() + " zone '" + ToString() + "':\r\n" + ex.ToString());
|
|
}
|
|
}
|
|
|
|
private void SerializeZoneTo(MemoryStream hashStream)
|
|
{
|
|
//list zone records for ZONEMD Simple scheme
|
|
List<DnsResourceRecord> records;
|
|
{
|
|
List<DnsResourceRecord> allZoneRecords = new List<DnsResourceRecord>();
|
|
|
|
_dnsServer.AuthZoneManager.ListAllZoneRecords(_name, allZoneRecords);
|
|
|
|
records = new List<DnsResourceRecord>(allZoneRecords.Count);
|
|
|
|
foreach (DnsResourceRecord record in allZoneRecords)
|
|
{
|
|
switch (record.Type)
|
|
{
|
|
case DnsResourceRecordType.NS:
|
|
records.Add(record);
|
|
|
|
IReadOnlyList<DnsResourceRecord> 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<KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>>> zones = new List<KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>>>(DnsResourceRecord.GroupRecords(records, true));
|
|
|
|
//sort zones by canonical DNS name
|
|
zones.Sort(delegate (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> x, KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> y)
|
|
{
|
|
return DnsNSECRecordData.CanonicalComparison(x.Key, y.Key);
|
|
});
|
|
|
|
//start serialization, zone by zone
|
|
using MemoryStream rrBuffer = new MemoryStream(512);
|
|
|
|
foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> zone in zones)
|
|
{
|
|
//list all RRSets for current zone owner name
|
|
List<KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>>> rrSets = new List<KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>>>(zone.Value);
|
|
|
|
//RRsets having the same owner name MUST be numerically ordered, in ascending order, by their numeric RR TYPE
|
|
rrSets.Sort(delegate (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> x, KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> y)
|
|
{
|
|
return x.Key.CompareTo(y.Key);
|
|
});
|
|
|
|
//serialize records
|
|
List<CanonicallySerializedResourceRecord> rrList = new List<CanonicallySerializedResourceRecord>(rrSets.Count * 4);
|
|
|
|
foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrSet in rrSets)
|
|
{
|
|
//serialize current RRSet records
|
|
List<CanonicallySerializedResourceRecord> serializedResourceRecords = new List<CanonicallySerializedResourceRecord>(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual Task FinalizeZoneTransferAsync()
|
|
{
|
|
ClearZoneHistory();
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
protected virtual Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList<DnsResourceRecord> historyRecords)
|
|
{
|
|
CommitZoneHistory(historyRecords);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public
|
|
|
|
public override string GetZoneTypeName()
|
|
{
|
|
return "Secondary";
|
|
}
|
|
|
|
public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL)
|
|
{
|
|
if (Disabled)
|
|
return;
|
|
|
|
if (_refreshTimerTriggered)
|
|
return;
|
|
|
|
_refreshTimerTriggered = true;
|
|
ResetRefreshTimer(refreshInterval);
|
|
}
|
|
|
|
public void TriggerResync()
|
|
{
|
|
if (_refreshTimerTriggered)
|
|
return;
|
|
|
|
_resync = true;
|
|
|
|
_refreshTimerTriggered = true;
|
|
ResetRefreshTimer(0);
|
|
}
|
|
|
|
public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)
|
|
{
|
|
throw new InvalidOperationException("Cannot set records in " + GetZoneTypeName() + " zone.");
|
|
}
|
|
|
|
public override void AddRecord(DnsResourceRecord record)
|
|
{
|
|
throw new InvalidOperationException("Cannot add record in " + GetZoneTypeName() + " zone.");
|
|
}
|
|
|
|
public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)
|
|
{
|
|
throw new InvalidOperationException("Cannot delete record in " + GetZoneTypeName() + " zone.");
|
|
}
|
|
|
|
public override bool DeleteRecords(DnsResourceRecordType type)
|
|
{
|
|
throw new InvalidOperationException("Cannot delete records in " + GetZoneTypeName() + " zone.");
|
|
}
|
|
|
|
public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)
|
|
{
|
|
throw new InvalidOperationException("Cannot update record in " + GetZoneTypeName() + " zone.");
|
|
}
|
|
|
|
#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 notify
|
|
|
|
if (value)
|
|
{
|
|
DisableNotifyTimer();
|
|
ResetRefreshTimer(Timeout.Infinite);
|
|
}
|
|
else
|
|
{
|
|
TriggerNotify();
|
|
TriggerRefresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool OverrideCatalogNotify
|
|
{
|
|
get { throw new InvalidOperationException(); }
|
|
set { throw new InvalidOperationException(); }
|
|
}
|
|
|
|
public virtual bool OverrideCatalogPrimaryNameServers
|
|
{
|
|
get { return _overrideCatalogPrimaryNameServers; }
|
|
set { _overrideCatalogPrimaryNameServers = value; }
|
|
}
|
|
|
|
public override AuthZoneNotify Notify
|
|
{
|
|
get { return base.Notify; }
|
|
set
|
|
{
|
|
switch (value)
|
|
{
|
|
case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones:
|
|
throw new ArgumentException("The Notify option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Notify));
|
|
}
|
|
|
|
base.Notify = value;
|
|
}
|
|
}
|
|
|
|
public override AuthZoneUpdate Update
|
|
{
|
|
get { return base.Update; }
|
|
set
|
|
{
|
|
switch (value)
|
|
{
|
|
case AuthZoneUpdate.AllowOnlyZoneNameServers:
|
|
case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL:
|
|
throw new ArgumentException("The Dynamic Updates option is invalid for Secondary zones: " + value.ToString(), nameof(Update));
|
|
}
|
|
|
|
base.Update = value;
|
|
}
|
|
}
|
|
|
|
public virtual IReadOnlyList<NameServerAddress> PrimaryNameServerAddresses
|
|
{
|
|
get { return _primaryNameServerAddresses; }
|
|
set
|
|
{
|
|
if ((value is null) || (value.Count == 0))
|
|
_primaryNameServerAddresses = null;
|
|
else if (value.Count > byte.MaxValue)
|
|
throw new ArgumentOutOfRangeException(nameof(PrimaryNameServerAddresses), "Name server addresses cannot have more than 255 entries.");
|
|
else
|
|
_primaryNameServerAddresses = value;
|
|
}
|
|
}
|
|
|
|
public DnsTransportProtocol PrimaryZoneTransferProtocol
|
|
{
|
|
get { return _primaryZoneTransferProtocol; }
|
|
set
|
|
{
|
|
switch (value)
|
|
{
|
|
case DnsTransportProtocol.Tcp:
|
|
case DnsTransportProtocol.Tls:
|
|
case DnsTransportProtocol.Quic:
|
|
_primaryZoneTransferProtocol = value;
|
|
break;
|
|
|
|
default:
|
|
throw new NotSupportedException("Zone transfer protocol is not supported: XFR-over-" + value.ToString().ToUpper());
|
|
}
|
|
}
|
|
}
|
|
|
|
public string PrimaryZoneTransferTsigKeyName
|
|
{
|
|
get { return _primaryZoneTransferTsigKeyName; }
|
|
set
|
|
{
|
|
if (value is null)
|
|
_primaryZoneTransferTsigKeyName = string.Empty;
|
|
else
|
|
_primaryZoneTransferTsigKeyName = value;
|
|
}
|
|
}
|
|
|
|
public DateTime Expiry
|
|
{ get { return _expiry; } }
|
|
|
|
public bool IsExpired
|
|
{ get { return _isExpired; } }
|
|
|
|
public virtual bool ValidateZone
|
|
{
|
|
get { return _validateZone; }
|
|
set { _validateZone = value; }
|
|
}
|
|
|
|
public bool ValidationFailed
|
|
{ get { return _validationFailed; } }
|
|
|
|
public override bool IsActive
|
|
{
|
|
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<DnsDatagram> InternalResolveAsync(DnsDatagram request, Func<DnsDatagram, CancellationToken, Task<DnsDatagram>> getValidatedResponseAsync = null, bool doNotReorderNameServers = false, CancellationToken cancellationToken = default)
|
|
{
|
|
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
|
|
}
|
|
}
|
|
}
|