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