/* 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.Dnssec; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ZoneManagers { public sealed class AuthZoneManager : IDisposable { #region events public event EventHandler SecondaryCatalogZoneAdded; public event EventHandler SecondaryCatalogZoneRemoved; #endregion #region variables readonly DnsServer _dnsServer; string _serverDomain; bool _useSoaSerialDateScheme; readonly AuthZoneTree _root = new AuthZoneTree(); readonly List _zoneIndex = new List(10); readonly List _catalogZoneIndex = new List(2); readonly ReaderWriterLockSlim _zoneIndexLock = new ReaderWriterLockSlim(); readonly object _saveLock = new object(); readonly Dictionary _pendingSaveZones = new Dictionary(); readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 10000; #endregion #region constructor public AuthZoneManager(DnsServer dnsServer) { _dnsServer = dnsServer; _serverDomain = _dnsServer.ServerDomain; _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { List failedZones = new List(); foreach (KeyValuePair pendingSaveZone in _pendingSaveZones) { try { SaveZoneFileInternal(pendingSaveZone.Key); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); failedZones.Add(pendingSaveZone.Key); } } _pendingSaveZones.Clear(); foreach (string zoneName in failedZones) _pendingSaveZones.TryAdd(zoneName, null); if (_pendingSaveZones.Count > 0) _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } }); } #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { lock (_saveLock) { _saveTimer?.Dispose(); try { foreach (KeyValuePair pendingSaveZone in _pendingSaveZones) { try { SaveZoneFileInternal(pendingSaveZone.Key); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } } finally { _pendingSaveZones.Clear(); } } foreach (AuthZoneNode zoneNode in _root) zoneNode.Dispose(); _zoneIndexLock.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region private internal void UpdateServerDomain(bool useBlockingAnswerTtl = false) { ThreadPool.QueueUserWorkItem(delegate (object state) { string serverDomain = _dnsServer.ServerDomain; //update authoritative zone SOA and NS records try { IReadOnlyList zones = GetAllZones(); foreach (AuthZoneInfo zone in zones) { if (zone.Type != AuthZoneType.Primary) continue; DnsResourceRecord record = zone.ApexZone.GetRecords(DnsResourceRecordType.SOA)[0]; DnsSOARecordData soa = record.RDATA as DnsSOARecordData; uint ttl; uint minimum; if (useBlockingAnswerTtl) { ttl = _dnsServer.BlockingAnswerTtl; minimum = ttl; } else { ttl = record.TTL; minimum = soa.Minimum; } if (soa.PrimaryNameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) { SetRecord(zone.Name, new DnsResourceRecord(record.Name, record.Type, DnsClass.IN, ttl, new DnsSOARecordData(serverDomain, soa.ResponsiblePerson, soa.Serial, soa.Refresh, soa.Retry, soa.Expire, minimum))); //update NS records IReadOnlyList nsResourceRecords = zone.ApexZone.GetRecords(DnsResourceRecordType.NS); foreach (DnsResourceRecord nsResourceRecord in nsResourceRecords) { if ((nsResourceRecord.RDATA as DnsNSRecordData).NameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) { UpdateRecord(zone.Name, nsResourceRecord, new DnsResourceRecord(nsResourceRecord.Name, nsResourceRecord.Type, nsResourceRecord.Class, nsResourceRecord.TTL, new DnsNSRecordData(serverDomain)) { Tag = nsResourceRecord.Tag }); break; } } if (zone.Internal) continue; //dont save internal zones to disk SaveZoneFile(zone.Name); } } } catch (Exception ex) { _dnsServer.LogManager?.Write(ex); } //update server domain _serverDomain = serverDomain; }); } private ApexZone CreateEmptyApexZone(AuthZoneInfo zoneInfo) { ApexZone apexZone; switch (zoneInfo.Type) { case AuthZoneType.Primary: apexZone = new PrimaryZone(_dnsServer, zoneInfo); break; case AuthZoneType.Secondary: apexZone = new SecondaryZone(_dnsServer, zoneInfo); break; case AuthZoneType.Stub: apexZone = new StubZone(_dnsServer, zoneInfo); break; case AuthZoneType.Forwarder: apexZone = new ForwarderZone(_dnsServer, zoneInfo); break; case AuthZoneType.SecondaryForwarder: apexZone = new SecondaryForwarderZone(_dnsServer, zoneInfo); break; case AuthZoneType.Catalog: apexZone = new CatalogZone(_dnsServer, zoneInfo); break; case AuthZoneType.SecondaryCatalog: SecondaryCatalogZone secondaryCatalogZone = new SecondaryCatalogZone(_dnsServer, zoneInfo); secondaryCatalogZone.ZoneAdded += SecondaryCatalogZoneAdded; secondaryCatalogZone.ZoneRemoved += SecondaryCatalogZoneRemoved; apexZone = secondaryCatalogZone; break; default: throw new InvalidDataException("DNS zone type not supported."); } if (_root.TryAdd(apexZone)) return apexZone; throw new DnsServerException("Zone already exists: " + zoneInfo.DisplayName); } internal AuthZone GetOrAddSubDomainZone(string zoneName, string domain) { return _root.GetOrAddSubDomainZone(zoneName, domain, delegate () { if (!_root.TryGet(zoneName, out ApexZone apexZone)) throw new DnsServerException("Zone was not found for domain: " + domain); if (apexZone is PrimaryZone primaryZone) return new PrimarySubDomainZone(primaryZone, domain); else if (apexZone is SecondaryCatalogZone secondaryCatalogZone) return new SecondaryCatalogSubDomainZone(secondaryCatalogZone, domain); else if (apexZone is SecondaryZone secondaryZone) return new SecondarySubDomainZone(secondaryZone, domain); else if (apexZone is CatalogZone catalogZone) return new CatalogSubDomainZone(catalogZone, domain); else if (apexZone is ForwarderZone forwarderZone) return new ForwarderSubDomainZone(forwarderZone, domain); throw new DnsServerException("Zone cannot have sub domains."); }); } internal IReadOnlyList GetApexZoneWithSubDomainZones(string zoneName) { return _root.GetApexZoneWithSubDomainZones(zoneName); } internal AuthZone GetAuthZone(string zoneName, string domain) { return _root.GetAuthZone(zoneName, domain); } internal ApexZone GetApexZone(string zoneName) { return _root.GetApexZone(zoneName); } internal AuthZone FindPreviousSubDomainZone(string zoneName, string domain) { return _root.FindPreviousSubDomainZone(zoneName, domain); } internal AuthZone FindNextSubDomainZone(string zoneName, string domain) { return _root.FindNextSubDomainZone(zoneName, domain); } internal bool SubDomainExistsFor(string zoneName, string domain) { return _root.SubDomainExistsFor(zoneName, domain); } internal void RemoveSubDomainZone(string domain, bool removeAllSubDomains = false) { _root.TryRemove(domain, out SubDomainZone _, removeAllSubDomains); } internal static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private static void ValidateZoneNameFor(string zoneName, string domain) { if (domain.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || domain.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase) || (zoneName.Length == 0)) return; throw new DnsServerException("The domain name '" + domain + "' does not belong to the zone: " + zoneName); } private void ResolveCNAME(DnsQuestionRecord question, bool dnssecOk, DnsResourceRecord lastCNAME, List answerRecords) { int queryCount = 0; do { string cnameDomain = (lastCNAME.RDATA as DnsCNAMERecordData).Domain; if (lastCNAME.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase)) break; //loop detected if (!_root.TryGet(cnameDomain, out AuthZoneNode zoneNode)) break; IReadOnlyList records = zoneNode.QueryRecords(question.Type, dnssecOk); if (records.Count < 1) break; DnsResourceRecord lastRR = records[records.Count - 1]; if (lastRR.Type != DnsResourceRecordType.CNAME) { answerRecords.AddRange(records); break; } foreach (DnsResourceRecord answerRecord in answerRecords) { if (answerRecord.Type != DnsResourceRecordType.CNAME) continue; if (answerRecord.RDATA.Equals(lastRR.RDATA)) return; //loop detected } answerRecords.AddRange(records); lastCNAME = lastRR; } while (++queryCount < DnsServer.MAX_CNAME_HOPS); } private bool DoDNAMESubstitution(DnsQuestionRecord question, bool dnssecOk, IReadOnlyList answer, out IReadOnlyList newAnswer) { DnsResourceRecord dnameRR = answer[0]; string result = (dnameRR.RDATA as DnsDNAMERecordData).Substitute(question.Name, dnameRR.Name); if (DnsClient.IsDomainNameValid(result)) { DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result)); List list = new List(5); list.AddRange(answer); list.Add(cnameRR); ResolveCNAME(question, dnssecOk, cnameRR, list); newAnswer = list; return true; } else { newAnswer = answer; return false; } } private List GetAdditionalRecords(IReadOnlyList refRecords, bool dnssecOk) { List additionalRecords = new List(refRecords.Count); foreach (DnsResourceRecord refRecord in refRecords) { switch (refRecord.Type) { case DnsResourceRecordType.NS: IReadOnlyList glueRecords = refRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) { additionalRecords.AddRange(glueRecords); } else { ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsNSRecordData).NameServer, dnssecOk, additionalRecords); } break; case DnsResourceRecordType.MX: ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsMXRecordData).Exchange, dnssecOk, additionalRecords); break; case DnsResourceRecordType.SRV: ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsSRVRecordData).Target, dnssecOk, additionalRecords); break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: DnsSVCBRecordData svcb = refRecord.RDATA as DnsSVCBRecordData; string targetName = svcb.TargetName; if (svcb.SvcPriority == 0) { //For AliasMode SVCB RRs, a TargetName of "." indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12] if ((targetName.Length == 0) || targetName.Equals(refRecord.Name, StringComparison.OrdinalIgnoreCase)) break; } else { //For ServiceMode SVCB RRs, if TargetName has the value ".", then the owner name of this record MUST be used as the effective TargetName [draft-ietf-dnsop-svcb-https-12] if (targetName.Length == 0) targetName = refRecord.Name; } ResolveAdditionalRecords(refRecord, targetName, dnssecOk, additionalRecords); break; } } return additionalRecords; } private void ResolveAdditionalRecords(DnsResourceRecord refRecord, string domain, bool dnssecOk, List additionalRecords) { int count = 0; while (count++ < DnsServer.MAX_CNAME_HOPS) { AuthZone zone = _root.FindZone(domain, out _, out _, out _, out _); if ((zone is null) || !zone.IsActive) break; if (((refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) && ((refRecord.RDATA as DnsSVCBRecordData).SvcPriority == 0)) { //resolve SVCB/HTTPS for Alias mode refRecord IReadOnlyList records = zone.QueryRecordsWildcard(refRecord.Type, dnssecOk, domain); if ((records.Count > 0) && (records[0].Type == refRecord.Type) && (records[0].RDATA is DnsSVCBRecordData svcb)) { additionalRecords.AddRange(records); string targetName = svcb.TargetName; if (svcb.SvcPriority == 0) { //Alias mode if ((targetName.Length == 0) || targetName.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase)) break; //For AliasMode SVCB RRs, a TargetName of "." indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12] foreach (DnsResourceRecord additionalRecord in additionalRecords) { if (additionalRecord.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase)) return; //loop detected } //continue to resolve SVCB/HTTPS further domain = targetName; refRecord = records[0]; continue; } else { //Service mode if (targetName.Length > 0) { //continue to resolve A/AAAA for target name domain = targetName; refRecord = records[0]; continue; } //resolve A/AAAA below } } } bool hasA = false; bool hasAAAA = false; if ((refRecord.Type == DnsResourceRecordType.SRV) || (refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) { foreach (DnsResourceRecord additionalRecord in additionalRecords) { if (additionalRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) { switch (additionalRecord.Type) { case DnsResourceRecordType.A: hasA = true; break; case DnsResourceRecordType.AAAA: hasAAAA = true; break; } } if (hasA && hasAAAA) break; } } if (!hasA) { IReadOnlyList records = zone.QueryRecordsWildcard(DnsResourceRecordType.A, dnssecOk, domain); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.A)) additionalRecords.AddRange(records); } if (!hasAAAA) { IReadOnlyList records = zone.QueryRecordsWildcard(DnsResourceRecordType.AAAA, dnssecOk, domain); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.AAAA)) additionalRecords.AddRange(records); } break; } } private DnsDatagram GetReferralResponse(DnsDatagram request, bool dnssecOk, AuthZone delegationZone, ApexZone apexZone, CancellationToken cancellationToken) { IReadOnlyList authority; if (delegationZone is StubZone) { authority = delegationZone.GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant query //update last used on DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in authority) record.GetAuthGenericRecordInfo().LastUsedOn = utcNow; } else { authority = delegationZone.QueryRecords(DnsResourceRecordType.NS, false); if (dnssecOk) { IReadOnlyList dsRecords = delegationZone.QueryRecords(DnsResourceRecordType.DS, true); if (dsRecords.Count > 0) { List newAuthority = new List(authority.Count + dsRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(dsRecords); authority = newAuthority; } else { //add proof of non existence (NODATA) to prove DS record does not exists IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNoData(delegationZone, apexZone, cancellationToken); else nsecRecords = AuthZoneTree.FindNSecProofOfNonExistenceNoData(delegationZone); if (nsecRecords.Count > 0) { List newAuthority = new List(authority.Count + nsecRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(nsecRecords); authority = newAuthority; } } } } IReadOnlyList additional = GetAdditionalRecords(authority, dnssecOk); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional); } private DnsDatagram GetForwarderResponse(DnsDatagram request, AuthZone zone, SubDomainZone closestZone, ApexZone forwarderZone, CancellationToken cancellationToken) { IReadOnlyList authority = null; if (zone is not null) { if (zone.ContainsNameServerRecords()) return GetReferralResponse(request, false, zone, forwarderZone, cancellationToken); authority = zone.QueryRecords(DnsResourceRecordType.FWD, false); } if (((authority is null) || (authority.Count == 0)) && (closestZone is not null)) { if (closestZone.ContainsNameServerRecords()) return GetReferralResponse(request, false, closestZone, forwarderZone, cancellationToken); authority = closestZone.QueryRecords(DnsResourceRecordType.FWD, false); } if ((authority is null) || (authority.Count == 0)) { if (forwarderZone.ContainsNameServerRecords()) return GetReferralResponse(request, false, forwarderZone, forwarderZone, cancellationToken); authority = forwarderZone.QueryRecords(DnsResourceRecordType.FWD, false); } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority); } internal void Flush() { _zoneIndexLock.EnterWriteLock(); try { _root.Clear(); _zoneIndex.Clear(); _catalogZoneIndex.Clear(); } finally { _zoneIndexLock.ExitWriteLock(); } } private static List CondenseIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord currentSoaRecord, IReadOnlyList xfrRecords) { DnsResourceRecord firstSoaRecord = xfrRecords[0]; DnsResourceRecord lastSoaRecord = xfrRecords[xfrRecords.Count - 1]; DnsResourceRecord firstDeletedSoaRecord = null; DnsResourceRecord lastAddedSoaRecord = null; List deletedRecords = new List(); List deletedGlueRecords = new List(); List addedRecords = new List(); List addedGlueRecords = new List(); //read and apply difference sequences int index = 1; int count = xfrRecords.Count - 1; DnsSOARecordData currentSoa = (DnsSOARecordData)currentSoaRecord.RDATA; while (index < count) { //read deleted records DnsResourceRecord deletedSoaRecord = xfrRecords[index]; if ((deletedSoaRecord.Type != DnsResourceRecordType.SOA) || !deletedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); if (firstDeletedSoaRecord is null) firstDeletedSoaRecord = deletedSoaRecord; index++; while (index < count) { DnsResourceRecord record = xfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!addedGlueRecords.Remove(record)) deletedGlueRecords.Add(record); break; default: if (!addedRecords.Remove(record)) deletedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { if (!addedRecords.Remove(record)) deletedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!addedGlueRecords.Remove(record)) deletedGlueRecords.Add(record); break; } } } index++; } //read added records DnsResourceRecord addedSoaRecord = xfrRecords[index]; if (!addedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); lastAddedSoaRecord = addedSoaRecord; index++; while (index < count) { DnsResourceRecord record = xfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!deletedGlueRecords.Remove(record)) addedGlueRecords.Add(record); break; default: if (!deletedRecords.Remove(record)) addedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { if (!deletedRecords.Remove(record)) addedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!deletedGlueRecords.Remove(record)) addedGlueRecords.Add(record); break; } } } index++; } //check sequence soa serial DnsSOARecordData deletedSoa = deletedSoaRecord.RDATA as DnsSOARecordData; if (currentSoa.Serial != deletedSoa.Serial) throw new InvalidOperationException("Current SOA serial does not match with the IXFR difference sequence deleted SOA."); //check next difference sequence currentSoa = addedSoaRecord.RDATA as DnsSOARecordData; } //create condensed records List condensedRecords = new List(2 + 2 + deletedRecords.Count + deletedGlueRecords.Count + addedRecords.Count + addedGlueRecords.Count); condensedRecords.Add(firstSoaRecord); condensedRecords.Add(firstDeletedSoaRecord); condensedRecords.AddRange(deletedRecords); condensedRecords.AddRange(deletedGlueRecords); condensedRecords.Add(lastAddedSoaRecord); condensedRecords.AddRange(addedRecords); condensedRecords.AddRange(addedGlueRecords); condensedRecords.Add(lastSoaRecord); return condensedRecords; } private void SaveZoneFileInternal(string zoneName) { zoneName = zoneName.ToLowerInvariant(); using (MemoryStream mS = new MemoryStream()) { //serialize zone WriteZoneTo(zoneName, mS); if (mS.Position == 0) return; //zone was not found //write to zone file mS.Position = 0; using (FileStream fS = new FileStream(Path.Combine(_dnsServer.ConfigFolder, "zones", zoneName + ".zone"), FileMode.Create, FileAccess.Write)) { mS.CopyTo(fS); } } _dnsServer.LogManager?.Write("Saved zone file for domain: " + (zoneName == "" ? "" : zoneName)); } #endregion #region public public void LoadAllZoneFiles() { Flush(); string zonesFolder = Path.Combine(_dnsServer.ConfigFolder, "zones"); if (!Directory.Exists(zonesFolder)) Directory.CreateDirectory(zonesFolder); //move zone files to new folder { string[] oldZoneFiles = Directory.GetFiles(_dnsServer.ConfigFolder, "*.zone"); foreach (string oldZoneFile in oldZoneFiles) File.Move(oldZoneFile, Path.Combine(zonesFolder, Path.GetFileName(oldZoneFile))); } //remove old internal zones files { string[] oldZoneFiles = ["localhost.zone", "1.0.0.127.in-addr.arpa.zone", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.zone"]; foreach (string oldZoneFile in oldZoneFiles) { string filePath = Path.Combine(zonesFolder, oldZoneFile); if (File.Exists(filePath)) { try { File.Delete(filePath); } catch { } } } } //load system zones { { CreateInternalPrimaryZone("localhost"); SetRecord("localhost", new DnsResourceRecord("localhost", DnsResourceRecordType.A, DnsClass.IN, 3600, new DnsARecordData(IPAddress.Loopback))); SetRecord("localhost", new DnsResourceRecord("localhost", DnsResourceRecordType.AAAA, DnsClass.IN, 3600, new DnsAAAARecordData(IPAddress.IPv6Loopback))); } { string ptrDomain = "0.in-addr.arpa"; CreateInternalPrimaryZone(ptrDomain); } { string ptrDomain = "255.in-addr.arpa"; CreateInternalPrimaryZone(ptrDomain); } { string ptrZoneName = "127.in-addr.arpa"; CreateInternalPrimaryZone(ptrZoneName); SetRecord(ptrZoneName, new DnsResourceRecord("1.0.0.127.in-addr.arpa", DnsResourceRecordType.PTR, DnsClass.IN, 3600, new DnsPTRRecordData("localhost"))); } { string ptrZoneName = IPAddress.IPv6Loopback.GetReverseDomain(); CreateInternalPrimaryZone(ptrZoneName); SetRecord(ptrZoneName, new DnsResourceRecord(ptrZoneName, DnsResourceRecordType.PTR, DnsClass.IN, 3600, new DnsPTRRecordData("localhost"))); } } //load zone files _zoneIndexLock.EnterWriteLock(); try { string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone"); foreach (string zoneFile in zoneFiles) { try { using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) { AuthZoneInfo zoneInfo = LoadZoneFrom(fS, File.GetLastWriteTimeUtc(fS.SafeFileHandle)); _zoneIndex.Add(zoneInfo); if (zoneInfo.Type == AuthZoneType.Catalog) _catalogZoneIndex.Add(zoneInfo); } _dnsServer.LogManager?.Write("DNS Server successfully loaded zone file: " + zoneFile); } catch (Exception ex) { _dnsServer.LogManager?.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString()); } } _zoneIndex.Sort(); _catalogZoneIndex.Sort(); } finally { _zoneIndexLock.ExitWriteLock(); } } internal AuthZoneInfo CreateSpecialPrimaryZone(string zoneName, DnsSOARecordData soaRecord, DnsNSRecordData ns) { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } internal void LoadSpecialPrimaryZones(IReadOnlyList zoneNames, DnsSOARecordData soaRecord, DnsNSRecordData ns) { _zoneIndexLock.EnterWriteLock(); try { foreach (string zoneName in zoneNames) { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); } } _zoneIndex.Sort(); } finally { _zoneIndexLock.ExitWriteLock(); } } internal void LoadSpecialPrimaryZones(Func getZoneName, DnsSOARecordData soaRecord, DnsNSRecordData ns) { _zoneIndexLock.EnterWriteLock(); try { string zoneName; while (true) { zoneName = getZoneName(); if (zoneName is null) break; PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); } } _zoneIndex.Sort(); } finally { _zoneIndexLock.ExitWriteLock(); } } internal AuthZoneInfo CreateInternalPrimaryZone(string zoneName) { return CreatePrimaryZone(zoneName, true, _useSoaSerialDateScheme); } public AuthZoneInfo CreatePrimaryZone(string zoneName) { return CreatePrimaryZone(zoneName, false, _useSoaSerialDateScheme); } public AuthZoneInfo CreatePrimaryZone(string zoneName, bool useSoaSerialDateScheme) { return CreatePrimaryZone(zoneName, false, useSoaSerialDateScheme); } private AuthZoneInfo CreatePrimaryZone(string zoneName, bool @internal, bool useSoaSerialDateScheme) { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, @internal, useSoaSerialDateScheme); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); if (!@internal) SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public Task CreateSecondaryZoneAsync(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateSecondaryZoneAsync(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, ignoreSoaFailure); } public async Task CreateSecondaryZoneAsync(string zoneName, IReadOnlyList primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false) { SecondaryZone apexZone = await SecondaryZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, ignoreSoaFailure); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public Task CreateStubZoneAsync(string zoneName, string primaryNameServerAddresses = null, bool ignoreSoaFailure = false) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateStubZoneAsync(zoneName, primaryNameServers, ignoreSoaFailure); } public async Task CreateStubZoneAsync(string zoneName, IReadOnlyList primaryNameServerAddresses = null, bool ignoreSoaFailure = false) { StubZone apexZone = await StubZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, ignoreSoaFailure); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } private AuthZoneInfo CreateForwarderZone(string zoneName) { ForwarderZone apexZone = new ForwarderZone(_dnsServer, zoneName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateForwarderZone(string zoneName, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments) { ForwarderZone apexZone = new ForwarderZone(_dnsServer, zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, fwdRecordComments); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateSecondaryForwarderZone(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateSecondaryForwarderZone(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); } public AuthZoneInfo CreateSecondaryForwarderZone(string zoneName, IReadOnlyList primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { SecondaryForwarderZone apexZone = new SecondaryForwarderZone(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateCatalogZone(string zoneName) { CatalogZone apexZone = new CatalogZone(_dnsServer, zoneName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); _catalogZoneIndex.Add(zoneInfo); _catalogZoneIndex.Sort(); apexZone.InitZoneProperties(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateSecondaryCatalogZone(string zoneName, string primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateSecondaryCatalogZone(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); } public AuthZoneInfo CreateSecondaryCatalogZone(string zoneName, IReadOnlyList primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { SecondaryCatalogZone apexZone = new SecondaryCatalogZone(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.ZoneAdded += SecondaryCatalogZoneAdded; apexZone.ZoneRemoved += SecondaryCatalogZoneRemoved; apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public void AddCatalogMemberZone(string catalogZoneName, AuthZoneInfo memberZoneInfo, bool ignoreValidationErrors = false) { switch (memberZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: if (!ignoreValidationErrors) { string currentCatalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName; if (currentCatalogZoneName is not null) throw new DnsServerException("The zone '" + memberZoneInfo.DisplayName + "' is already a member of Catalog zone '" + currentCatalogZoneName + "'."); } ApexZone apexZone = _root.GetApexZone(catalogZoneName); if (apexZone is not CatalogZone catalogZone) { if (ignoreValidationErrors) return; throw new DnsServerException("No such Catalog zone was found: " + catalogZoneName); } catalogZone.AddMemberZone(memberZoneInfo.Name, memberZoneInfo.Type); memberZoneInfo.ApexZone.CatalogZoneName = catalogZone.Name; //update properties in catalog zone by settings member zone property values again switch (memberZoneInfo.Type) { case AuthZoneType.Primary: memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; memberZoneInfo.ZoneTransfer = memberZoneInfo.ZoneTransfer; memberZoneInfo.ZoneTransferTsigKeyNames = memberZoneInfo.ZoneTransferTsigKeyNames; break; case AuthZoneType.Stub: memberZoneInfo.PrimaryNameServerAddresses = memberZoneInfo.PrimaryNameServerAddresses; memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; break; case AuthZoneType.Forwarder: memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; break; } SaveZoneFile(catalogZoneName); break; default: throw new NotSupportedException(); } } public void RemoveCatalogMemberZone(AuthZoneInfo memberZoneInfo) { switch (memberZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: string catalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName; if (catalogZoneName is null) return; memberZoneInfo.ApexZone.CatalogZone?.RemoveMemberZone(memberZoneInfo.Name); memberZoneInfo.ApexZone.CatalogZoneName = null; SaveZoneFile(catalogZoneName); break; default: throw new NotSupportedException(); } } public void ChangeCatalogMemberZoneOwnership(AuthZoneInfo memberZoneInfo, string newCatalogZoneName) { switch (memberZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: string currentCatalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName; if (currentCatalogZoneName is null) throw new DnsServerException("The zone '" + memberZoneInfo.DisplayName + "' is not a member of any Catalog zone."); ApexZone apexZone = _root.GetApexZone(currentCatalogZoneName); if (apexZone is not CatalogZone currentCatalogZone) throw new DnsServerException("No such Catalog zone was found: " + currentCatalogZoneName); AddCatalogMemberZone(newCatalogZoneName, memberZoneInfo, true); currentCatalogZone.ChangeMemberZoneOwnership(memberZoneInfo.Name, newCatalogZoneName); SaveZoneFile(currentCatalogZoneName); break; default: throw new NotSupportedException(); } } public AuthZoneInfo CloneZone(string zoneName, string sourceZoneName) { AuthZoneInfo sourceZoneInfo = GetAuthZoneInfo(sourceZoneName); if (sourceZoneInfo is null) throw new DnsServerException("No such zone was found: " + (sourceZoneName.Length == 0 ? "." : sourceZoneName)); AuthZoneInfo zoneInfo; switch (sourceZoneInfo.Type) { case AuthZoneType.Primary: zoneInfo = CreatePrimaryZone(zoneName); break; case AuthZoneType.Forwarder: zoneInfo = CreateForwarderZone(zoneName); break; default: throw new DnsServerException("Cannot clone the zone: source zone must be a Primary or Conditional Forwarder zone."); } if (zoneInfo is null) throw new DnsServerException("Failed to clone the zone: zone already exists."); //copy zone options zoneInfo.Disabled = sourceZoneInfo.Disabled; if (zoneInfo.Type == AuthZoneType.Primary) { zoneInfo.ZoneTransfer = sourceZoneInfo.ZoneTransfer; zoneInfo.ZoneTransferNetworkACL = sourceZoneInfo.ZoneTransferNetworkACL; zoneInfo.ZoneTransferTsigKeyNames = sourceZoneInfo.ZoneTransferTsigKeyNames; zoneInfo.Notify = sourceZoneInfo.Notify; zoneInfo.NotifyNameServers = sourceZoneInfo.NotifyNameServers; zoneInfo.Update = sourceZoneInfo.Update; zoneInfo.UpdateNetworkACL = sourceZoneInfo.UpdateNetworkACL; if (sourceZoneInfo.UpdateSecurityPolicies is not null) { Dictionary>> updateSecurityPolicies = new Dictionary>>(sourceZoneInfo.UpdateSecurityPolicies.Count); foreach (KeyValuePair>> sourceSecurityPolicy in sourceZoneInfo.UpdateSecurityPolicies) { Dictionary> policyMap = new Dictionary>(); foreach (KeyValuePair> sourcePolicyMap in sourceSecurityPolicy.Value) policyMap.Add(string.Concat(sourcePolicyMap.Key.AsSpan(0, sourcePolicyMap.Key.Length - sourceZoneName.Length), zoneName), sourcePolicyMap.Value); updateSecurityPolicies.Add(sourceSecurityPolicy.Key, policyMap); } zoneInfo.UpdateSecurityPolicies = updateSecurityPolicies; } } //copy records List sourceRecords = new List(); ListAllZoneRecords(sourceZoneName, sourceRecords); List newRecords = new List(sourceRecords.Count); foreach (DnsResourceRecord sourceRecord in sourceRecords) { switch (sourceRecord.Type) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.DS: continue; //skip DNSSEC records default: DnsResourceRecord newRecord = new DnsResourceRecord(string.Concat(sourceRecord.Name.AsSpan(0, sourceRecord.Name.Length - sourceZoneName.Length), zoneName), sourceRecord.Type, sourceRecord.Class, sourceRecord.TTL, sourceRecord.RDATA); if (sourceRecord.Tag is NSRecordInfo nsInfo) { NSRecordInfo nrInfo = new NSRecordInfo(); nrInfo.Disabled = nsInfo.Disabled; nrInfo.Comments = nsInfo.Comments; nrInfo.GlueRecords = nsInfo.GlueRecords; newRecord.Tag = nrInfo; } else if (sourceRecord.Tag is SOARecordInfo soaInfo) { SOARecordInfo nrInfo = new SOARecordInfo(); nrInfo.Disabled = soaInfo.Disabled; nrInfo.Comments = soaInfo.Comments; nrInfo.UseSoaSerialDateScheme = soaInfo.UseSoaSerialDateScheme; newRecord.Tag = nrInfo; } else if (sourceRecord.Tag is SVCBRecordInfo svcbInfo) { SVCBRecordInfo nrInfo = new SVCBRecordInfo(); nrInfo.Disabled = svcbInfo.Disabled; nrInfo.Comments = svcbInfo.Comments; nrInfo.AutoIpv4Hint = svcbInfo.AutoIpv4Hint; nrInfo.AutoIpv6Hint = svcbInfo.AutoIpv6Hint; newRecord.Tag = nrInfo; } else if (sourceRecord.Tag is GenericRecordInfo srInfo) { GenericRecordInfo nrInfo = new GenericRecordInfo(); nrInfo.Disabled = srInfo.Disabled; nrInfo.Comments = srInfo.Comments; newRecord.Tag = nrInfo; } newRecords.Add(newRecord); break; } } LoadZoneRecords(zoneInfo.ApexZone, newRecords); SaveZoneFile(zoneInfo.Name); return zoneInfo; } public void ConvertZoneType(string zoneName, AuthZoneType type) { AuthZoneInfo currentZoneInfo = GetAuthZoneInfo(zoneName); if (currentZoneInfo is null) throw new DnsServerException("No such zone was found: " + (zoneName.Length == 0 ? "." : zoneName)); if (currentZoneInfo.Type == type) throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone: the zone is already of the same type."); switch (currentZoneInfo.Type) { case AuthZoneType.Primary: switch (type) { case AuthZoneType.Forwarder: if (currentZoneInfo.ApexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone: converting the zone will cause lose of DNSSEC private keys."); break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone: not supported."); } break; case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: switch (type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone: not supported."); } break; case AuthZoneType.Forwarder: switch (type) { case AuthZoneType.Primary: break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone: not supported."); } break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone: not supported."); } //read all current records List allRecords = new List(); ListAllZoneRecords(currentZoneInfo.Name, allRecords); try { //delete current zone DeleteZone(currentZoneInfo); //create new zone AuthZoneInfo newZoneInfo; switch (type) { case AuthZoneType.Primary: switch (currentZoneInfo.Type) { case AuthZoneType.Secondary: { //reset SOA metadata and remove DNSSEC records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { switch (record.Type) { case DnsResourceRecordType.SOA: { GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); record.Tag = null; GenericRecordInfo newRecordInfo = record.GetAuthGenericRecordInfo(); newRecordInfo.Comments = recordInfo.Comments; } break; case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: continue; } updateRecords.Add(record); } allRecords = updateRecords; } break; case AuthZoneType.Forwarder: case AuthZoneType.SecondaryForwarder: { //remove all FWD records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { if (record.Type == DnsResourceRecordType.FWD) continue; updateRecords.Add(record); } allRecords = updateRecords; } break; } newZoneInfo = CreatePrimaryZone(currentZoneInfo.Name); break; case AuthZoneType.Forwarder: switch (currentZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.SecondaryForwarder: { //remove SOA and NS records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { switch (record.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: continue; } updateRecords.Add(record); } allRecords = updateRecords; } break; case AuthZoneType.Secondary: { //remove SOA, NS and DNSSEC records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { switch (record.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.DS: continue; } updateRecords.Add(record); } allRecords = updateRecords; } break; } newZoneInfo = CreateForwarderZone(currentZoneInfo.Name); break; default: throw new InvalidOperationException(); } //load records LoadZoneRecords(newZoneInfo.ApexZone, allRecords); //save zone file SaveZoneFile(newZoneInfo.Name); } catch (Exception ex) { _dnsServer.LogManager?.Write("DNS Server failed to convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(type) + " zone.\r\n" + ex.ToString()); //delete the zone if it was created DeleteZone(currentZoneInfo); //reload old zone file string zoneFile = Path.Combine(_dnsServer.ConfigFolder, "zones", currentZoneInfo.Name + ".zone"); _zoneIndexLock.EnterWriteLock(); try { using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) { AuthZoneInfo zoneInfo = LoadZoneFrom(fS, File.GetLastWriteTimeUtc(fS.SafeFileHandle)); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); } _dnsServer.LogManager?.Write("DNS Server successfully loaded zone file: " + zoneFile); } catch (Exception ex2) { _dnsServer.LogManager?.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex2.ToString()); } finally { _zoneIndexLock.ExitWriteLock(); } throw; } } public void SignPrimaryZoneWithRsaNSEC(string zoneName, string hashAlgorithm, int kskKeySize, int zskKeySize, uint dnsKeyTtl, ushort zskRolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.SignZoneWithRsaNSec(hashAlgorithm, kskKeySize, zskKeySize, dnsKeyTtl, zskRolloverDays); SaveZoneFile(primaryZone.Name); } public void SignPrimaryZoneWithRsaNSEC3(string zoneName, string hashAlgorithm, int kskKeySize, int zskKeySize, ushort iterations, byte saltLength, uint dnsKeyTtl, ushort zskRolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.SignZoneWithRsaNSec3(hashAlgorithm, kskKeySize, zskKeySize, iterations, saltLength, dnsKeyTtl, zskRolloverDays); SaveZoneFile(primaryZone.Name); } public void SignPrimaryZoneWithEcdsaNSEC(string zoneName, string curve, uint dnsKeyTtl, ushort zskRolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.SignZoneWithEcdsaNSec(curve, dnsKeyTtl, zskRolloverDays); SaveZoneFile(primaryZone.Name); } public void SignPrimaryZoneWithEcdsaNSEC3(string zoneName, string curve, ushort iterations, byte saltLength, uint dnsKeyTtl, ushort zskRolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.SignZoneWithEcdsaNSec3(curve, iterations, saltLength, dnsKeyTtl, zskRolloverDays); SaveZoneFile(primaryZone.Name); } public void UnsignPrimaryZone(string zoneName) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UnsignZone(); SaveZoneFile(primaryZone.Name); } public void ConvertPrimaryZoneToNSEC(string zoneName) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.ConvertToNSec(); SaveZoneFile(primaryZone.Name); } public void ConvertPrimaryZoneToNSEC3(string zoneName, ushort iterations, byte saltLength) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.ConvertToNSec3(iterations, saltLength); SaveZoneFile(primaryZone.Name); } public void UpdatePrimaryZoneNSEC3Parameters(string zoneName, ushort iterations, byte saltLength) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UpdateNSec3Parameters(iterations, saltLength); SaveZoneFile(primaryZone.Name); } public void UpdatePrimaryZoneDnsKeyTtl(string zoneName, uint dnsKeyTtl) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UpdateDnsKeyTtl(dnsKeyTtl); SaveZoneFile(primaryZone.Name); } public void GenerateAndAddPrimaryZoneDnssecRsaPrivateKey(string zoneName, DnssecPrivateKeyType keyType, string hashAlgorithm, int keySize, ushort rolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.GenerateAndAddRsaKey(keyType, hashAlgorithm, keySize, rolloverDays); SaveZoneFile(primaryZone.Name); } public void GenerateAndAddPrimaryZoneDnssecEcdsaPrivateKey(string zoneName, DnssecPrivateKeyType keyType, string curve, ushort rolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.GenerateAndAddEcdsaKey(keyType, curve, rolloverDays); SaveZoneFile(primaryZone.Name); } public void UpdatePrimaryZoneDnssecPrivateKey(string zoneName, ushort keyTag, ushort rolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UpdatePrivateKey(keyTag, rolloverDays); SaveZoneFile(primaryZone.Name); } public void DeletePrimaryZoneDnssecPrivateKey(string zoneName, ushort keyTag) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.DeletePrivateKey(keyTag); SaveZoneFile(primaryZone.Name); } public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(string zoneName) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.PublishAllGeneratedKeys(); SaveZoneFile(primaryZone.Name); } public void RolloverPrimaryZoneDnsKey(string zoneName, ushort keyTag) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.RolloverDnsKey(keyTag); SaveZoneFile(primaryZone.Name); } public void RetirePrimaryZoneDnsKey(string zoneName, ushort keyTag) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.RetireDnsKey(keyTag); SaveZoneFile(primaryZone.Name); } public bool DeleteZone(string zoneName, bool deleteZoneFile = false) { AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName); if (zoneInfo is null) return false; return DeleteZone(zoneInfo, deleteZoneFile); } public bool DeleteZone(AuthZoneInfo zoneInfo, bool deleteZoneFile = false) { switch (zoneInfo.Type) { case AuthZoneType.Catalog: //update all zone memberships for catalog zone to be deleted foreach (string memberZoneName in (zoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames()) { AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; if (zoneInfo.Name.Equals(memberZoneInfo.CatalogZoneName, StringComparison.OrdinalIgnoreCase)) { memberZoneInfo.ApexZone.CatalogZoneName = null; SaveZoneFile(memberZoneInfo.Name); } } break; case AuthZoneType.SecondaryCatalog: //delete all member zones for secondary catalog zone to be deleted foreach (string memberZoneName in (zoneInfo.ApexZone as SecondaryCatalogZone).GetAllMemberZoneNames()) { AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; if (zoneInfo.Name.Equals(memberZoneInfo.CatalogZoneName, StringComparison.OrdinalIgnoreCase)) DeleteZone(memberZoneInfo, true); } break; } _zoneIndexLock.EnterWriteLock(); try { if (_root.TryRemove(zoneInfo.Name, out ApexZone removedApexZone)) { removedApexZone.Dispose(); _zoneIndex.Remove(zoneInfo); if (zoneInfo.Type == AuthZoneType.Catalog) _catalogZoneIndex.Remove(zoneInfo); if (zoneInfo.CatalogZoneName is not null) RemoveCatalogMemberZone(zoneInfo); //remove catalog zone membership if (deleteZoneFile) { File.Delete(Path.Combine(_dnsServer.ConfigFolder, "zones", zoneInfo.Name + ".zone")); _dnsServer.LogManager?.Write("Deleted zone file for domain: " + zoneInfo.DisplayName); } return true; } } finally { _zoneIndexLock.ExitWriteLock(); } return false; } public AuthZoneInfo GetAuthZoneInfo(string zoneName, bool loadHistory = false) { if (_root.TryGet(zoneName, out AuthZoneNode authZoneNode) && (authZoneNode.ApexZone is not null)) return new AuthZoneInfo(authZoneNode.ApexZone, loadHistory); return null; } public AuthZoneInfo FindAuthZoneInfo(string domain, bool loadHistory = false) { _ = _root.FindZone(domain, out _, out _, out ApexZone apexZone, out _); if (apexZone is null) return null; return new AuthZoneInfo(apexZone, loadHistory); } public bool NameExists(string zoneName, string domain) { ValidateZoneNameFor(zoneName, domain); return _root.TryGet(zoneName, domain, out _); } public void ListAllZoneRecords(string zoneName, List records) { foreach (AuthZone authZone in _root.GetApexZoneWithSubDomainZones(zoneName)) authZone.ListAllRecords(records); } public void ListAllZoneRecords(string zoneName, DnsResourceRecordType[] types, List records) { foreach (AuthZone authZone in _root.GetApexZoneWithSubDomainZones(zoneName)) { foreach (DnsResourceRecordType type in types) records.AddRange(authZone.GetRecords(type)); } } public void ListAllRecords(string zoneName, string domain, List records) { ValidateZoneNameFor(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) authZone.ListAllRecords(records); } public IEnumerable EnumerateAllRecords(string zoneName, string domain, bool includeAllSubDomainNames = false) { ValidateZoneNameFor(zoneName, domain); if (includeAllSubDomainNames) { foreach (AuthZone authZone in _root.GetSubDomainZoneWithSubDomainZones(domain)) { foreach (KeyValuePair> entry in authZone.Entries) { foreach (DnsResourceRecord record in entry.Value) yield return record; } } } else { if (_root.TryGet(zoneName, domain, out AuthZone authZone)) { foreach (KeyValuePair> entry in authZone.Entries) { foreach (DnsResourceRecord record in entry.Value) yield return record; } } } } public IReadOnlyList GetRecords(string zoneName, string domain, DnsResourceRecordType type) { ValidateZoneNameFor(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) return authZone.GetRecords(type); return Array.Empty(); } public IReadOnlyDictionary> GetEntriesFor(string zoneName, string domain) { ValidateZoneNameFor(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) return authZone.Entries; return new Dictionary>(1); } public IReadOnlyList QueryZoneTransferRecords(string zoneName) { AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new InvalidOperationException("Zone was not found: " + zoneName); //primary, secondary, and forwarder zones support zone transfer IReadOnlyList soaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count != 1) throw new InvalidOperationException("Zone must be a primary, secondary, or forwarder zone."); DnsResourceRecord soaRecord = soaRecords[0]; List records = new List(); ListAllZoneRecords(zoneName, records); List xfrRecords = new List(records.Count + 1); //start message xfrRecords.Add(soaRecord); foreach (DnsResourceRecord record in records) { GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo(); if (authRecordInfo.Disabled) continue; switch (record.Type) { case DnsResourceRecordType.SOA: break; //skip record case DnsResourceRecordType.NS: xfrRecords.Add(record); IReadOnlyList glueRecords = (authRecordInfo as NSRecordInfo).GlueRecords; if (glueRecords is not null) { foreach (DnsResourceRecord glueRecord in glueRecords) xfrRecords.Add(glueRecord); } break; default: xfrRecords.Add(record); break; } } //end message xfrRecords.Add(soaRecord); return xfrRecords; } public IReadOnlyList QueryIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord clientSoaRecord) { AuthZoneInfo authZone = GetAuthZoneInfo(zoneName, true); if (authZone is null) throw new InvalidOperationException("Zone was not found: " + zoneName); //primary, secondary, forwarder, and catalog zones support zone transfer IReadOnlyList soaRecords = authZone.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count != 1) throw new InvalidOperationException("No SOA record was found for IXFR."); DnsResourceRecord currentSoaRecord = soaRecords[0]; uint clientSerial = (clientSoaRecord.RDATA as DnsSOARecordData).Serial; if (clientSerial == (currentSoaRecord.RDATA as DnsSOARecordData).Serial) { //zone not modified return [currentSoaRecord]; } //find history record start from client serial IReadOnlyList zoneHistory = authZone.ZoneHistory; int index = 0; while (index < zoneHistory.Count) { //check difference sequence if ((zoneHistory[index].RDATA as DnsSOARecordData).Serial == clientSerial) break; //found history for client's serial //skip to next difference sequence index++; int soaCount = 1; while (index < zoneHistory.Count) { if (zoneHistory[index].Type == DnsResourceRecordType.SOA) { soaCount++; if (soaCount == 3) break; } index++; } } if (index == zoneHistory.Count) { //client's serial was not found in zone history //do full zone transfer return QueryZoneTransferRecords(zoneName); } List xfrRecords = new List(); //start incremental message xfrRecords.Add(currentSoaRecord); //write history for (int i = index; i < zoneHistory.Count; i++) xfrRecords.Add(zoneHistory[i]); //end incremental message xfrRecords.Add(currentSoaRecord); //condense return CondenseIncrementalZoneTransferRecords(zoneName, clientSoaRecord, xfrRecords); } public void SyncZoneTransferRecords(string zoneName, IReadOnlyList xfrRecords) { if ((xfrRecords.Count < 2) || (xfrRecords[0].Type != DnsResourceRecordType.SOA) || !xfrRecords[0].Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || !xfrRecords[xfrRecords.Count - 1].Equals(xfrRecords[0])) throw new DnsServerException("Invalid AXFR response was received."); List latestRecords = new List(xfrRecords.Count); List allGlueRecords = new List(4); if (zoneName.Length == 0) { //root zone case for (int i = 1; i < xfrRecords.Count; i++) { DnsResourceRecord record = xfrRecords[i]; switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!allGlueRecords.Contains(record)) allGlueRecords.Add(record); break; default: if (!latestRecords.Contains(record)) latestRecords.Add(record); break; } } } else { for (int i = 1; i < xfrRecords.Count; i++) { DnsResourceRecord record = xfrRecords[i]; if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { if (!latestRecords.Contains(record)) latestRecords.Add(record); } else if (!allGlueRecords.Contains(record)) { allGlueRecords.Add(record); } } } if (allGlueRecords.Count > 0) { foreach (DnsResourceRecord record in latestRecords) { if (record.Type == DnsResourceRecordType.NS) record.SyncGlueRecords(allGlueRecords); } } //sync records List currentRecords = new List(); ListAllZoneRecords(zoneName, currentRecords); Dictionary>> currentRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(currentRecords); Dictionary>> latestRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(latestRecords); //remove domains that do not exists in new records foreach (KeyValuePair>> currentDomain in currentRecordsGroupedByDomain) { if (!latestRecordsGroupedByDomain.ContainsKey(currentDomain.Key)) _root.TryRemove(currentDomain.Key, out SubDomainZone _); } //sync new records foreach (KeyValuePair>> latestEntries in latestRecordsGroupedByDomain) { AuthZone zone = GetOrAddSubDomainZone(zoneName, latestEntries.Key); if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(latestEntries.Value); else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(latestEntries.Value); } if (!_root.TryGet(zoneName, out ApexZone apexZone)) throw new InvalidOperationException(); apexZone.UpdateDnssecStatus(); SaveZoneFile(apexZone.Name); } public IReadOnlyList SyncIncrementalZoneTransferRecords(string zoneName, IReadOnlyList xfrRecords) { if ((xfrRecords.Count < 2) || (xfrRecords[0].Type != DnsResourceRecordType.SOA) || !xfrRecords[0].Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || !xfrRecords[xfrRecords.Count - 1].Equals(xfrRecords[0])) throw new DnsServerException("Invalid IXFR/AXFR response was received."); if ((xfrRecords.Count < 4) || (xfrRecords[1].Type != DnsResourceRecordType.SOA)) { //received AXFR response SyncZoneTransferRecords(zoneName, xfrRecords); return Array.Empty(); } if (!_root.TryGet(zoneName, out ApexZone apexZone)) throw new InvalidOperationException("No such zone was found: " + zoneName); IReadOnlyList soaRecords = apexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count != 1) throw new InvalidOperationException("No authoritative zone was found: " + zoneName); //process IXFR response DnsResourceRecord currentSoaRecord = soaRecords[0]; DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData; IReadOnlyList condensedXfrRecords = CondenseIncrementalZoneTransferRecords(zoneName, currentSoaRecord, xfrRecords); List deletedRecords = new List(); List deletedGlueRecords = new List(); List addedRecords = new List(); List addedGlueRecords = new List(); //read and apply difference sequences int index = 1; int count = condensedXfrRecords.Count - 1; while (index < count) { //read deleted records DnsResourceRecord deletedSoaRecord = condensedXfrRecords[index]; if ((deletedSoaRecord.Type != DnsResourceRecordType.SOA) || !deletedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); index++; while (index < count) { DnsResourceRecord record = condensedXfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: deletedGlueRecords.Add(record); break; default: deletedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { deletedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: deletedGlueRecords.Add(record); break; } } } index++; } //read added records DnsResourceRecord addedSoaRecord = condensedXfrRecords[index]; if (!addedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); index++; while (index < count) { DnsResourceRecord record = condensedXfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: addedGlueRecords.Add(record); break; default: addedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { addedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: addedGlueRecords.Add(record); break; } } } index++; } //check sequence soa serial DnsSOARecordData deletedSoa = deletedSoaRecord.RDATA as DnsSOARecordData; if (currentSoa.Serial != deletedSoa.Serial) throw new InvalidOperationException("Current SOA serial does not match with the IXFR difference sequence deleted SOA."); //sync difference sequence if (deletedRecords.Count > 0) { foreach (KeyValuePair>> deletedEntry in DnsResourceRecord.GroupRecords(deletedRecords)) { AuthZone zone = GetOrAddSubDomainZone(zoneName, deletedEntry.Key); if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { zone.SyncRecords(deletedEntry.Value, null); } else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { zone.SyncRecords(deletedEntry.Value, null); if (zone.IsEmpty) _root.TryRemove(deletedEntry.Key, out SubDomainZone _); //remove empty sub zone } } } if (addedRecords.Count > 0) { foreach (KeyValuePair>> addedEntry in DnsResourceRecord.GroupRecords(addedRecords)) { AuthZone zone = GetOrAddSubDomainZone(zoneName, addedEntry.Key); if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(null, addedEntry.Value); else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(null, addedEntry.Value); } } if ((deletedGlueRecords.Count > 0) || (addedGlueRecords.Count > 0)) { foreach (AuthZone zone in _root.GetApexZoneWithSubDomainZones(zoneName)) zone.SyncGlueRecords(deletedGlueRecords, addedGlueRecords); } { AuthZone zone = GetOrAddSubDomainZone(zoneName, zoneName); addedSoaRecord.CopyRecordInfoFrom(currentSoaRecord); zone.LoadRecords(DnsResourceRecordType.SOA, new DnsResourceRecord[] { addedSoaRecord }); } //check next difference sequence currentSoa = addedSoaRecord.RDATA as DnsSOARecordData; deletedRecords.Clear(); deletedGlueRecords.Clear(); addedRecords.Clear(); addedGlueRecords.Clear(); } apexZone.UpdateDnssecStatus(); SaveZoneFile(apexZone.Name); //return history List historyRecords = new List(xfrRecords.Count - 2); for (int i = 1; i < xfrRecords.Count - 1; i++) historyRecords.Add(xfrRecords[i]); return historyRecords; } internal void ImportRecords(string zoneName, IReadOnlyList records, bool overwrite, bool overwriteSoaSerial) { _ = _root.FindZone(zoneName, out _, out _, out ApexZone apexZone, out _); if ((apexZone is null) || !apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("No such zone was found: " + zoneName); if ((apexZone is not PrimaryZone) && (apexZone is not ForwarderZone)) throw new DnsServerException("Zone must be a primary or forwarder type: " + apexZone.ToString()); List soaRRSet = null; foreach (KeyValuePair>> zoneEntry in DnsResourceRecord.GroupRecords(records)) { if (zoneName.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { switch (rrsetEntry.Key) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); break; case DnsResourceRecordType.SOA: apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); soaRRSet = rrsetEntry.Value; break; default: if (overwrite) { apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); } else { foreach (DnsResourceRecord record in rrsetEntry.Value) apexZone.AddRecord(record); } break; } } } else { ValidateZoneNameFor(zoneName, zoneEntry.Key); AuthZone authZone = GetOrAddSubDomainZone(zoneName, zoneEntry.Key); foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { switch (rrsetEntry.Key) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: case DnsResourceRecordType.SOA: authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); break; default: if (overwrite) { authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); } else { foreach (DnsResourceRecord record in rrsetEntry.Value) authZone.AddRecord(record); } break; } } if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } } if (overwriteSoaSerial && (soaRRSet is not null) && ((apexZone is PrimaryZone) || (apexZone is ForwarderZone))) apexZone.SetSoaSerial((soaRRSet[0].RDATA as DnsSOARecordData).Serial); SaveZoneFile(apexZone.Name); } private void LoadZoneRecords(ApexZone apexZone, IReadOnlyList records) { foreach (KeyValuePair>> zoneEntry in DnsResourceRecord.GroupRecords(records)) { if (apexZone.Name.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) apexZone.LoadRecords(rrsetEntry.Key, rrsetEntry.Value); } else { ValidateZoneNameFor(apexZone.Name, zoneEntry.Key); AuthZone authZone = GetOrAddSubDomainZone(apexZone.Name, zoneEntry.Key); foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) authZone.LoadRecords(rrsetEntry.Key, rrsetEntry.Value); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } } apexZone.UpdateDnssecStatus(); } public void SetRecords(string zoneName, IReadOnlyList records) { for (int i = 1; i < records.Count; i++) { if (!records[i].Name.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); if (records[i].Type != records[0].Type) throw new InvalidOperationException(); if (records[i].Class != records[0].Class) throw new InvalidOperationException(); } AuthZone authZone = GetOrAddSubDomainZone(zoneName, records[0].Name); authZone.SetRecords(records[0].Type, records); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } public void SetRecord(string zoneName, DnsResourceRecord record) { ValidateZoneNameFor(zoneName, record.Name); AuthZone authZone = GetOrAddSubDomainZone(zoneName, record.Name); authZone.SetRecords(record.Type, new DnsResourceRecord[] { record }); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } public void AddRecord(string zoneName, DnsResourceRecord record) { ValidateZoneNameFor(zoneName, record.Name); AuthZone authZone = GetOrAddSubDomainZone(zoneName, record.Name); authZone.AddRecord(record); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } public void UpdateRecord(string zoneName, DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { ValidateZoneNameFor(zoneName, oldRecord.Name); ValidateZoneNameFor(zoneName, newRecord.Name); if (oldRecord.Type != newRecord.Type) throw new DnsServerException("Cannot update record: new record must be of same type."); if (oldRecord.Type == DnsResourceRecordType.SOA) throw new DnsServerException("Cannot update record: use SetRecords() for updating SOA record."); if (!_root.TryGet(zoneName, oldRecord.Name, out AuthZone authZone)) throw new DnsServerException("Cannot update record: zone '" + zoneName + "' does not exists."); switch (oldRecord.Type) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: case DnsResourceRecordType.APP: if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) { authZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } else { authZone.DeleteRecords(oldRecord.Type); if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(oldRecord.Name, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } AuthZone newZone = GetOrAddSubDomainZone(zoneName, newRecord.Name); newZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); if (newZone is SubDomainZone subDomainZone1) subDomainZone1.AutoUpdateState(); } break; default: if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) { authZone.UpdateRecord(oldRecord, newRecord); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } else { if (!authZone.DeleteRecord(oldRecord.Type, oldRecord.RDATA)) throw new DnsWebServiceException("Cannot update record: the old record does not exists."); if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(oldRecord.Name, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } AuthZone newZone = GetOrAddSubDomainZone(zoneName, newRecord.Name); newZone.AddRecord(newRecord); if (newZone is SubDomainZone subDomainZone1) subDomainZone1.AutoUpdateState(); } break; } } public bool DeleteRecord(string zoneName, DnsResourceRecord record) { return DeleteRecord(zoneName, record.Name, record.Type, record.RDATA); } public bool DeleteRecord(string zoneName, string domain, DnsResourceRecordType type, DnsResourceRecordData rdata) { ValidateZoneNameFor(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) { if (authZone.DeleteRecord(type, rdata)) { if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(domain, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } return true; } } return false; } public bool DeleteRecords(string zoneName, string domain, DnsResourceRecordType type) { ValidateZoneNameFor(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) { if (authZone.DeleteRecords(type)) { if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(domain, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } return true; } } return false; } public IReadOnlyList GetAllZones() { _zoneIndexLock.EnterReadLock(); try { return _zoneIndex.ToArray(); } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetZones(Func predicate) { _zoneIndexLock.EnterReadLock(); try { List zoneInfoList = new List(); foreach (AuthZoneInfo zoneInfo in _zoneIndex) { if (predicate(zoneInfo)) zoneInfoList.Add(zoneInfo); } return zoneInfoList; } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetAllCatalogZones() { _zoneIndexLock.EnterReadLock(); try { return _catalogZoneIndex.ToArray(); } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetCatalogZones(Func predicate) { _zoneIndexLock.EnterReadLock(); try { List catalogZoneInfoList = new List(); foreach (AuthZoneInfo zone in _catalogZoneIndex) { if (predicate(zone)) catalogZoneInfoList.Add(zone); } return catalogZoneInfoList; } finally { _zoneIndexLock.ExitReadLock(); } } public void ListSubDomains(string domain, List subDomains) { _root.ListSubDomains(domain, subDomains); } public DnsDatagram QueryClosestDelegation(DnsDatagram request) { _ = _root.FindZone(request.Question[0].Name, out _, out SubDomainZone delegation, out ApexZone apexZone, out _); if (delegation is not null) { bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned); return GetReferralResponse(request, dnssecOk, delegation, apexZone, CancellationToken.None); } //no delegation found return null; } public async Task QueryAsync(DnsDatagram request, IPAddress remoteIP, bool isRecursionAllowed, CancellationToken cancellationToken = default) { AuthZone zone = _root.FindZone(request.Question[0].Name, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone apexZone, out bool hasSubDomains); if ((apexZone is null) || !apexZone.IsActive) return null; //no authority for requested zone if (!await IsQueryAllowedAsync(apexZone, remoteIP)) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.Refused, request.Question); return InternalQuery(request, isRecursionAllowed, zone, closest, delegation, apexZone, hasSubDomains, cancellationToken); } public DnsDatagram Query(DnsDatagram request, bool isRecursionAllowed) { AuthZone zone = _root.FindZone(request.Question[0].Name, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone apexZone, out bool hasSubDomains); if ((apexZone is null) || !apexZone.IsActive) return null; //no authority for requested zone return InternalQuery(request, isRecursionAllowed, zone, closest, delegation, apexZone, hasSubDomains, CancellationToken.None); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private DnsDatagram InternalQuery(DnsDatagram request, bool isRecursionAllowed, AuthZone zone, SubDomainZone closest, SubDomainZone delegation, ApexZone apexZone, bool hasSubDomains, CancellationToken cancellationToken) { DnsQuestionRecord question = request.Question[0]; bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned); if ((zone is null) || !zone.IsActive) { //zone not found if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) return GetReferralResponse(request, dnssecOk, delegation, apexZone, cancellationToken); if (apexZone is StubZone) return GetReferralResponse(request, false, apexZone, apexZone, cancellationToken); DnsResponseCode rCode = DnsResponseCode.NoError; IReadOnlyList answer = null; IReadOnlyList authority = null; if (closest is not null) { answer = closest.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } else { answer = null; authority = closest.QueryRecords(DnsResourceRecordType.APP, false); } } if (((answer is null) || (answer.Count == 0)) && ((authority is null) || (authority.Count == 0))) { answer = apexZone.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } else { answer = null; authority = apexZone.QueryRecords(DnsResourceRecordType.APP, false); if (authority.Count == 0) { if ((apexZone is ForwarderZone) || (apexZone is SecondaryForwarderZone)) return GetForwarderResponse(request, null, closest, apexZone, cancellationToken); //no DNAME or APP record available so process FWD response if (!hasSubDomains) rCode = DnsResponseCode.NxDomain; authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk); if (dnssecOk) { //add proof of non existence (NXDOMAIN) to prove the qname does not exists IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNxDomain(question.Name, false, cancellationToken); else nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, false); if (nsecRecords.Count > 0) { List newAuthority = new List(authority.Count + nsecRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(nsecRecords); authority = newAuthority; } } } } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority); } else { //zone found if (question.Type == DnsResourceRecordType.DS) { if (zone is ApexZone) { if ((delegation is null) || !delegation.IsActive || (delegation.Name.Length > apexZone.Name.Length)) return null; //no authoritative parent side delegation zone available to answer for DS zone = delegation; //switch zone to parent side sub domain delegation zone for DS record } } else if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) { //zone is delegation return GetReferralResponse(request, dnssecOk, delegation, apexZone, cancellationToken); } DnsResponseCode rCode = DnsResponseCode.NoError; IReadOnlyList answer = null; IReadOnlyList authority = null; IReadOnlyList additional = null; if (closest is not null) { answer = closest.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } } if ((answer is null) || (answer.Count == 0)) { answer = apexZone.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } else { answer = zone.QueryRecords(question.Type, dnssecOk); if (answer.Count == 0) { //record type not found if (question.Type == DnsResourceRecordType.DS) { //check for correct auth zone if (apexZone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase)) { //current auth zone is child side; find parent side auth zone for DS string parentZone = GetParentZone(question.Name); if (parentZone is null) parentZone = string.Empty; _ = _root.FindZone(parentZone, out _, out _, out apexZone, out _); if ((apexZone is null) || !apexZone.IsActive) return null; //no authority for requested zone } } else { //check for delegation, stub & forwarder if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) return GetReferralResponse(request, dnssecOk, delegation, apexZone, cancellationToken); if (apexZone is StubZone) return GetReferralResponse(request, false, apexZone, apexZone, cancellationToken); } authority = zone.QueryRecords(DnsResourceRecordType.APP, false); if (authority.Count == 0) { if ((apexZone is ForwarderZone) || (apexZone is SecondaryForwarderZone)) return GetForwarderResponse(request, zone, closest, apexZone, cancellationToken); //no APP record available so process FWD response authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk); if (dnssecOk) { //add proof of non existence (NODATA) to prove that no such type or record exists IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNoData(zone, apexZone, cancellationToken); else nsecRecords = AuthZoneTree.FindNSecProofOfNonExistenceNoData(zone); if (nsecRecords.Count > 0) { List newAuthority = new List(authority.Count + nsecRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(nsecRecords); authority = newAuthority; } } } additional = null; } else { //record type found if (zone.Name.StartsWith('*') && !zone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase)) { //wildcard zone; generate new answer records DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answer.Count]; for (int i = 0; i < answer.Count; i++) wildcardAnswers[i] = new DnsResourceRecord(question.Name, answer[i].Type, answer[i].Class, answer[i].TTL, answer[i].RDATA) { Tag = answer[i].Tag }; answer = wildcardAnswers; //add proof of non existence (WILDCARD) to prove that the wildcard expansion was legit and the qname actually does not exists if (dnssecOk) { IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNxDomain(question.Name, true, cancellationToken); else nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, true); if (nsecRecords.Count > 0) authority = nsecRecords; } } DnsResourceRecord lastRR = answer[answer.Count - 1]; if ((lastRR.Type != question.Type) && (question.Type != DnsResourceRecordType.ANY)) { switch (lastRR.Type) { case DnsResourceRecordType.CNAME: List newAnswers = new List(answer.Count + 1); newAnswers.AddRange(answer); ResolveCNAME(question, dnssecOk, lastRR, newAnswers); answer = newAnswers; break; case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: authority = apexZone.GetRecords(DnsResourceRecordType.SOA); //adding SOA for use with NO DATA response break; } } switch (question.Type) { case DnsResourceRecordType.NS: case DnsResourceRecordType.MX: case DnsResourceRecordType.SRV: case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: additional = GetAdditionalRecords(answer, dnssecOk); break; default: additional = null; break; } } } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority, additional); } } private static async Task IsQueryAllowedAsync(ApexZone apexZone, IPAddress remoteIP) { async Task IsZoneNameServerAllowedAsync() { IReadOnlyList zoneNameServers = await apexZone.GetAllResolvedNameServerAddressesAsync(); foreach (NameServerAddress nameServer in zoneNameServers) { if (nameServer.IPEndPoint.Address.Equals(remoteIP)) return true; } return false; } CatalogZone catalogZone = apexZone.CatalogZone; if (catalogZone is not null) { if (!apexZone.OverrideCatalogQueryAccess) apexZone = catalogZone; //use catalog query access options } else { SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone; if (secondaryCatalogZone is not null) { if (!apexZone.OverrideCatalogQueryAccess) apexZone = secondaryCatalogZone; //use secondary query access options } } switch (apexZone.QueryAccess) { case AuthZoneQueryAccess.Allow: return true; case AuthZoneQueryAccess.AllowOnlyPrivateNetworks: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: case AddressFamily.InterNetworkV6: return NetUtilities.IsPrivateIP(remoteIP); default: return false; } case AuthZoneQueryAccess.AllowOnlyZoneNameServers: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return await IsZoneNameServerAllowedAsync(); case AuthZoneQueryAccess.UseSpecifiedNetworkACL: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return NetworkAccessControl.IsAddressAllowed(remoteIP, apexZone.QueryAccessNetworkACL); case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return NetworkAccessControl.IsAddressAllowed(remoteIP, apexZone.QueryAccessNetworkACL) || await IsZoneNameServerAllowedAsync(); default: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return false; } } public void LoadTrustAnchorsTo(DnsClient dnsClient, string domain, DnsResourceRecordType type) { if (type == DnsResourceRecordType.DS) { domain = GetParentZone(domain); if (domain is null) domain = ""; } AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(domain, false); if ((zoneInfo is not null) && (zoneInfo.ApexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)) { IReadOnlyList dnsKeyRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.DNSKEY); foreach (DnsResourceRecord dnsKeyRecord in dnsKeyRecords) { DnsDNSKEYRecordData dnsKey = dnsKeyRecord.RDATA as DnsDNSKEYRecordData; if (dnsKey.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint) && !dnsKey.Flags.HasFlag(DnsDnsKeyFlag.Revoke)) { DnsDSRecordData dsRecord = dnsKey.CreateDS(dnsKeyRecord.Name, DnssecDigestType.SHA256); dnsClient.AddTrustAnchor(zoneInfo.Name, dsRecord); } } } } public AuthZoneInfo LoadZoneFrom(Stream s, DateTime lastModified) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DZ") throw new InvalidDataException("DnsServer zone file format is invalid."); switch (bR.ReadByte()) { case 2: { DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; if (records.Length == 0) throw new InvalidDataException("Zone does not contain SOA record."); DnsResourceRecord soaRecord = null; for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); if (records[i].Type == DnsResourceRecordType.SOA) soaRecord = records[i]; } if (soaRecord == null) throw new InvalidDataException("Zone does not contain SOA record."); //make zone info AuthZoneType zoneType; if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) zoneType = AuthZoneType.Primary; else zoneType = AuthZoneType.Stub; AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false); //create zone ApexZone apexZone = CreateEmptyApexZone(zoneInfo); try { //load records LoadZoneRecords(apexZone, records); } catch { DeleteZone(zoneInfo); throw; } //init zone switch (zoneInfo.Type) { case AuthZoneType.Primary: apexZone.TriggerNotify(); break; case AuthZoneType.Stub: { StubZone stub = apexZone as StubZone; soaRecord = stub.GetRecords(DnsResourceRecordType.SOA)[0]; SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo(); if (soaInfo.Version == 1) stub.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers; stub.TriggerRefresh(); } break; } return new AuthZoneInfo(apexZone); } case 3: { bool zoneDisabled = bR.ReadBoolean(); DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; if (records.Length == 0) throw new InvalidDataException("Zone does not contain SOA record."); DnsResourceRecord soaRecord = null; for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); records[i].Tag = AuthRecordInfo.ReadGenericRecordInfoFrom(bR, records[i].Type); if (records[i].Type == DnsResourceRecordType.SOA) soaRecord = records[i]; } if (soaRecord == null) throw new InvalidDataException("Zone does not contain SOA record."); //make zone info AuthZoneType zoneType; if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) zoneType = AuthZoneType.Primary; else zoneType = AuthZoneType.Stub; AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled); //create zone ApexZone apexZone = CreateEmptyApexZone(zoneInfo); try { //load records LoadZoneRecords(apexZone, records); } catch { DeleteZone(zoneInfo); throw; } //init zone switch (zoneInfo.Type) { case AuthZoneType.Primary: apexZone.TriggerNotify(); break; case AuthZoneType.Stub: { StubZone stub = apexZone as StubZone; soaRecord = stub.GetRecords(DnsResourceRecordType.SOA)[0]; SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo(); if (soaInfo.Version == 1) stub.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers; stub.TriggerRefresh(); } break; } return new AuthZoneInfo(apexZone); } case 4: { //read zone info AuthZoneInfo zoneInfo = new AuthZoneInfo(bR, lastModified); //create zone ApexZone apexZone = CreateEmptyApexZone(zoneInfo); try { //read all zone records DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; if (records.Length < 1) throw new InvalidDataException("Failed to load DNS zone file: the zone file does not contain any records."); uint minExpiryTtl = 0u; for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); records[i].Tag = AuthRecordInfo.ReadGenericRecordInfoFrom(bR, records[i].Type); GenericRecordInfo recordInfo = records[i].GetAuthGenericRecordInfo(); if (recordInfo.ExpiryTtl > 0u) { uint pendingExpiryTtl = recordInfo.GetPendingExpiryTtl(); if (pendingExpiryTtl == 0) { //expired record found; set 10 sec ttl for timer to delete it minExpiryTtl = 10; } else { if (minExpiryTtl == 0u) minExpiryTtl = pendingExpiryTtl; else minExpiryTtl = Math.Min(minExpiryTtl, pendingExpiryTtl); } } } //load records LoadZoneRecords(apexZone, records); //init zone switch (zoneInfo.Type) { case AuthZoneType.Primary: apexZone.TriggerNotify(); if (minExpiryTtl > 0u) apexZone.StartRecordExpiryTimer(minExpiryTtl); break; case AuthZoneType.Secondary: { SecondaryZone secondary = apexZone as SecondaryZone; DnsResourceRecord soaRecord = secondary.GetRecords(DnsResourceRecordType.SOA)[0]; SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo(); if (soaInfo.Version == 1) { secondary.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers; secondary.PrimaryZoneTransferProtocol = soaInfo.ZoneTransferProtocol; secondary.PrimaryZoneTransferTsigKeyName = soaInfo.TsigKeyName; } secondary.TriggerNotify(); secondary.TriggerRefresh(); } break; case AuthZoneType.Stub: { StubZone stub = apexZone as StubZone; DnsResourceRecord soaRecord = stub.GetRecords(DnsResourceRecordType.SOA)[0]; SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo(); if (soaInfo.Version == 1) stub.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers; stub.TriggerRefresh(); } break; case AuthZoneType.Forwarder: { IReadOnlyList soaRecords = apexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count == 0) (apexZone as ForwarderZone).InitZone(); apexZone.TriggerNotify(); if (minExpiryTtl > 0u) apexZone.StartRecordExpiryTimer(minExpiryTtl); } break; case AuthZoneType.SecondaryForwarder: { (apexZone as SecondaryZone).TriggerRefresh(); } break; case AuthZoneType.Catalog: { (apexZone as CatalogZone).BuildMembersIndex(); apexZone.TriggerNotify(); if (minExpiryTtl > 0u) apexZone.StartRecordExpiryTimer(minExpiryTtl); } break; case AuthZoneType.SecondaryCatalog: { (apexZone as SecondaryZone).TriggerRefresh(); (apexZone as SecondaryCatalogZone).BuildMembersIndex(); } break; } } catch { DeleteZone(zoneInfo); throw; } return new AuthZoneInfo(apexZone); } default: throw new InvalidDataException("DNS Zone file version not supported."); } } public void WriteZoneTo(string zoneName, Stream s) { AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName, true); if (zoneInfo is null) return; //serialize zone BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("DZ")); //format bW.Write((byte)4); //version //write zone info if (zoneInfo.Internal) throw new InvalidOperationException("Cannot save zones marked as internal."); zoneInfo.WriteTo(bW); //write all zone records List records = new List(); ListAllZoneRecords(zoneInfo.Name, records); bW.Write(records.Count); foreach (DnsResourceRecord record in records) { record.WriteTo(s); record.GetAuthGenericRecordInfo().WriteTo(bW); } } public void SaveZoneFile(string zoneName) { zoneName = zoneName.ToLowerInvariant(); lock (_saveLock) { if (!_pendingSaveZones.TryAdd(zoneName, null)) return; if (_pendingSaveZones.Count == 1) _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } #endregion #region properties public bool UseSoaSerialDateScheme { get { return _useSoaSerialDateScheme; } set { _useSoaSerialDateScheme = value; } } public int TotalZones { get { return _zoneIndex.Count; } } #endregion } }