/* 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.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 _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 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 CreateAsync(DnsServer dnsServer, string name, IReadOnlyList 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 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 RefreshZoneAsync(IReadOnlyList 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 updatedNameServers = new List(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 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 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 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 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); } } } protected virtual Task FinalizeZoneTransferAsync() { ClearZoneHistory(); return Task.CompletedTask; } protected virtual Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList 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 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 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 InternalResolveAsync(DnsDatagram request, Func> 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 } } }