/*
Technitium DNS Server
Copyright (C) 2023 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.Text;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
using TechnitiumLibrary.Net.Proxy;
namespace DnsServerCore.Dns.ZoneManagers
{
public sealed class AuthZoneManager : IDisposable
{
#region variables
readonly DnsServer _dnsServer;
string _serverDomain;
readonly AuthZoneTree _root = new AuthZoneTree();
readonly List _zoneIndex = new List(10);
readonly ReaderWriterLockSlim _zoneIndexLock = new ReaderWriterLockSlim();
#endregion
#region constructor
public AuthZoneManager(DnsServer dnsServer)
{
_dnsServer = dnsServer;
_serverDomain = _dnsServer.ServerDomain;
}
#endregion
#region IDisposable
bool _disposed;
private void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
foreach (AuthZoneNode zoneNode in _root)
zoneNode.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
}
#endregion
#region private
private void UpdateServerDomain(string serverDomain)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
//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.GetApexRecords(DnsResourceRecordType.SOA)[0];
DnsSOARecordData soa = record.RDATA as DnsSOARecordData;
if (soa.PrimaryNameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase))
{
string responsiblePerson = soa.ResponsiblePerson;
if (responsiblePerson.EndsWith(_serverDomain))
responsiblePerson = responsiblePerson.Replace(_serverDomain, serverDomain);
SetRecords(zone.Name, record.Name, record.Type, record.TTL, new DnsResourceRecordData[] { new DnsSOARecordData(serverDomain, responsiblePerson, soa.Serial, soa.Refresh, soa.Retry, soa.Expire, soa.Minimum) });
//update NS records
IReadOnlyList nsResourceRecords = zone.GetApexRecords(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
try
{
SaveZoneFile(zone.Name);
}
catch (Exception ex)
{
_dnsServer.LogManager?.Write(ex);
}
}
}
}
catch (Exception ex)
{
_dnsServer.LogManager?.Write(ex);
}
//update server domain
_serverDomain = serverDomain;
});
}
private ApexZone CreateEmptyZone(AuthZoneInfo zoneInfo)
{
ApexZone zone;
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
zone = new PrimaryZone(_dnsServer, zoneInfo);
break;
case AuthZoneType.Secondary:
zone = new SecondaryZone(_dnsServer, zoneInfo);
break;
case AuthZoneType.Stub:
zone = new StubZone(_dnsServer, zoneInfo);
break;
case AuthZoneType.Forwarder:
zone = new ForwarderZone(zoneInfo);
break;
default:
throw new InvalidDataException("DNS zone type not supported.");
}
if (_root.TryAdd(zone))
return zone;
throw new DnsServerException("Zone already exists: " + zoneInfo.Name);
}
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 SecondaryZone secondaryZone)
return new SecondarySubDomainZone(secondaryZone, domain);
else if (apexZone is ForwarderZone forwarderZone)
return new ForwarderSubDomainZone(forwarderZone, domain);
throw new DnsServerException("Zone cannot have sub domains.");
});
}
internal IReadOnlyList GetZoneWithSubDomainZones(string zoneName)
{
return _root.GetZoneWithSubDomainZones(zoneName);
}
internal AuthZone GetAuthZone(string zoneName, string domain)
{
return _root.GetAuthZone(zoneName, domain);
}
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 SubDomainExists(string zoneName, string domain)
{
return _root.SubDomainExists(zoneName, domain);
}
internal void RemoveSubDomainZone(string domain)
{
_root.TryRemove(domain, out SubDomainZone _);
}
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 does not belong to the zone: " + domain);
}
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 IReadOnlyList 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.GetAuthRecordInfo().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) && _root.TryGet(domain, out AuthZoneNode zoneNode) && zoneNode.IsActive)
{
if (((refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) && ((refRecord.RDATA as DnsSVCBRecordData).SvcPriority == 0))
{
//resolve SVCB/HTTPS for Alias mode refRecord
IReadOnlyList records = zoneNode.QueryRecords(refRecord.Type, dnssecOk);
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 = zoneNode.QueryRecords(DnsResourceRecordType.A, dnssecOk);
if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.A))
additionalRecords.AddRange(records);
}
if (!hasAAAA)
{
IReadOnlyList records = zoneNode.QueryRecords(DnsResourceRecordType.AAAA, dnssecOk);
if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.AAAA))
additionalRecords.AddRange(records);
}
break;
}
}
private DnsDatagram GetReferralResponse(DnsDatagram request, bool dnssecOk, AuthZone delegationZone, ApexZone apexZone)
{
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.GetAuthRecordInfo().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);
else
nsecRecords = _root.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, AuthZone closestZone, ApexZone forwarderZone)
{
IReadOnlyList authority = null;
if (zone is not null)
{
if (zone.ContainsNameServerRecords())
return GetReferralResponse(request, false, zone, forwarderZone);
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);
authority = closestZone.QueryRecords(DnsResourceRecordType.FWD, false);
}
if ((authority is null) || (authority.Count == 0))
{
if (forwarderZone.ContainsNameServerRecords())
return GetReferralResponse(request, false, forwarderZone, forwarderZone);
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();
}
finally
{
_zoneIndexLock.ExitWriteLock();
}
}
private static IReadOnlyList 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.Contains(record))
addedGlueRecords.Remove(record);
else
deletedGlueRecords.Add(record);
break;
default:
if (addedRecords.Contains(record))
addedRecords.Remove(record);
else
deletedRecords.Add(record);
break;
}
}
else
{
if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase))
{
if (addedRecords.Contains(record))
addedRecords.Remove(record);
else
deletedRecords.Add(record);
}
else
{
switch (record.Type)
{
case DnsResourceRecordType.A:
case DnsResourceRecordType.AAAA:
if (addedGlueRecords.Contains(record))
addedGlueRecords.Remove(record);
else
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.Contains(record))
deletedGlueRecords.Remove(record);
else
addedGlueRecords.Add(record);
break;
default:
if (deletedRecords.Contains(record))
deletedRecords.Remove(record);
else
addedRecords.Add(record);
break;
}
}
else
{
if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase))
{
if (deletedRecords.Contains(record))
deletedRecords.Remove(record);
else
addedRecords.Add(record);
}
else
{
switch (record.Type)
{
case DnsResourceRecordType.A:
case DnsResourceRecordType.AAAA:
if (deletedGlueRecords.Contains(record))
deletedGlueRecords.Remove(record);
else
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;
}
#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 = new string[] { "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
{
{
CreatePrimaryZone("localhost", _dnsServer.ServerDomain, true);
SetRecords("localhost", "localhost", DnsResourceRecordType.A, 3600, new DnsResourceRecordData[] { new DnsARecordData(IPAddress.Loopback) });
SetRecords("localhost", "localhost", DnsResourceRecordType.AAAA, 3600, new DnsResourceRecordData[] { new DnsAAAARecordData(IPAddress.IPv6Loopback) });
}
{
string prtDomain = "0.in-addr.arpa";
CreatePrimaryZone(prtDomain, _dnsServer.ServerDomain, true);
}
{
string prtDomain = "255.in-addr.arpa";
CreatePrimaryZone(prtDomain, _dnsServer.ServerDomain, true);
}
{
string ptrZoneName = "127.in-addr.arpa";
CreatePrimaryZone(ptrZoneName, _dnsServer.ServerDomain, true);
SetRecords(ptrZoneName, "1.0.0.127.in-addr.arpa", DnsResourceRecordType.PTR, 3600, new DnsResourceRecordData[] { new DnsPTRRecordData("localhost") });
}
{
string ptrZoneName = IPAddress.IPv6Loopback.GetReverseDomain();
CreatePrimaryZone(ptrZoneName, _dnsServer.ServerDomain, true);
SetRecords(ptrZoneName, ptrZoneName, DnsResourceRecordType.PTR, 3600, new DnsResourceRecordData[] { 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);
_zoneIndex.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();
}
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();
}
}
public AuthZoneInfo CreatePrimaryZone(string zoneName, string primaryNameServer, bool @internal)
{
PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, primaryNameServer, @internal);
_zoneIndexLock.EnterWriteLock();
try
{
if (_root.TryAdd(apexZone))
{
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
_zoneIndex.Add(zoneInfo);
_zoneIndex.Sort();
return zoneInfo;
}
}
finally
{
_zoneIndexLock.ExitWriteLock();
}
return null;
}
public async Task CreateSecondaryZoneAsync(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol zoneTransferProtocol = DnsTransportProtocol.Tcp, string tsigKeyName = null)
{
SecondaryZone apexZone = await SecondaryZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, zoneTransferProtocol, tsigKeyName);
_zoneIndexLock.EnterWriteLock();
try
{
if (_root.TryAdd(apexZone))
{
apexZone.TriggerRefresh(0);
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
_zoneIndex.Add(zoneInfo);
_zoneIndex.Sort();
return zoneInfo;
}
}
finally
{
_zoneIndexLock.ExitWriteLock();
}
return null;
}
public async Task CreateStubZoneAsync(string zoneName, string primaryNameServerAddresses = null)
{
StubZone apexZone = await StubZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses);
_zoneIndexLock.EnterWriteLock();
try
{
if (_root.TryAdd(apexZone))
{
apexZone.TriggerRefresh(0);
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
_zoneIndex.Add(zoneInfo);
_zoneIndex.Sort();
return zoneInfo;
}
}
finally
{
_zoneIndexLock.ExitWriteLock();
}
return null;
}
public AuthZoneInfo CreateForwarderZone(string zoneName, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, NetProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments)
{
ForwarderZone apexZone = new ForwarderZone(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();
return zoneInfo;
}
}
finally
{
_zoneIndexLock.ExitWriteLock();
}
return null;
}
public void ConvertZoneType(string zoneName, AuthZoneType type)
{
AuthZoneInfo currentZoneInfo = GetAuthZoneInfo(zoneName, false);
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 '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type.ToString() + " zone: the zone is already of the same type.");
switch (currentZoneInfo.Type)
{
case AuthZoneType.Primary:
switch (type)
{
case AuthZoneType.Forwarder:
if (currentZoneInfo.DnssecStatus != AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("Cannot convert the zone '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type + " zone: converting the zone will cause lose of DNSSEC private keys.");
break;
default:
throw new DnsServerException("Cannot convert the zone '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type + " zone: not supported.");
}
break;
case AuthZoneType.Secondary:
switch (type)
{
case AuthZoneType.Primary:
case AuthZoneType.Forwarder:
break;
default:
throw new DnsServerException("Cannot convert the zone '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type + " zone: not supported.");
}
break;
case AuthZoneType.Forwarder:
switch (type)
{
case AuthZoneType.Primary:
break;
default:
throw new DnsServerException("Cannot convert the zone '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type + " zone: not supported.");
}
break;
default:
throw new DnsServerException("Cannot convert the zone '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type.ToString() + " zone: not supported.");
}
//read all current records
List allRecords = new List();
ListAllZoneRecords(zoneName, allRecords);
try
{
//delete current zone
DeleteZone(zoneName);
//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:
{
AuthRecordInfo recordInfo = record.GetAuthRecordInfo();
record.Tag = null;
AuthRecordInfo newRecordInfo = record.GetAuthRecordInfo();
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:
{
//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(zoneName, _dnsServer.ServerDomain, false);
break;
case AuthZoneType.Forwarder:
switch (currentZoneInfo.Type)
{
case AuthZoneType.Primary:
{
//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:
continue;
}
updateRecords.Add(record);
}
allRecords = updateRecords;
}
break;
}
newZoneInfo = CreateForwarderZone(zoneName, DnsTransportProtocol.Udp, "this-server", _dnsServer.DnssecValidation, NetProxyType.None, null, 0, null, null, null);
break;
default:
throw new InvalidOperationException();
}
//load records
LoadRecords(newZoneInfo.ApexZone, allRecords);
}
catch (Exception ex)
{
_dnsServer.LogManager?.Write("DNS Server failed to convert the zone '" + (zoneName.Length == 0 ? "." : zoneName) + "' from " + currentZoneInfo.Type.ToString() + " to " + type.ToString() + " zone.\r\n" + ex.ToString());
//delete the zone if it was created
DeleteZone(zoneName);
//reload old zone file
string zoneFile = Path.Combine(_dnsServer.ConfigFolder, "zones", zoneName + ".zone");
_zoneIndexLock.EnterWriteLock();
try
{
using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read))
{
AuthZoneInfo zoneInfo = LoadZoneFrom(fS);
_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);
}
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);
}
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);
}
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);
}
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();
}
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();
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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();
}
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);
}
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);
}
public bool DeleteZone(string zoneName)
{
_zoneIndexLock.EnterWriteLock();
try
{
if (_root.TryRemove(zoneName, out ApexZone apexZone))
{
apexZone.Dispose();
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
if (!_zoneIndex.Remove(zoneInfo))
throw new InvalidOperationException("Zone deleted from tree but failed to remove from zone index.");
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 zone in _root.GetZoneWithSubDomainZones(zoneName))
zone.ListAllRecords(records);
}
public void ListAllRecords(string zoneName, string domain, List records)
{
ValidateZoneNameFor(zoneName, domain);
if (_root.TryGet(zoneName, domain, out AuthZone authZone))
authZone.ListAllRecords(records);
}
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> GetAllRecords(string zoneName, string domain)
{
ValidateZoneNameFor(zoneName, domain);
if (_root.TryGet(zoneName, domain, out AuthZone authZone))
return authZone.GetAllRecords();
return new Dictionary>(1);
}
public IReadOnlyList QueryZoneTransferRecords(string zoneName)
{
AuthZoneInfo authZone = GetAuthZoneInfo(zoneName, false);
if (authZone is null)
throw new InvalidOperationException("Zone was not found: " + zoneName);
//only primary and secondary zones support zone transfer
IReadOnlyList soaRecords = authZone.GetApexRecords(DnsResourceRecordType.SOA);
if (soaRecords.Count != 1)
throw new InvalidOperationException("Zone must be a primary or secondary 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)
{
AuthRecordInfo authRecordInfo = record.GetAuthRecordInfo();
if (authRecordInfo.Disabled)
continue;
switch (record.Type)
{
case DnsResourceRecordType.SOA:
break; //skip record
case DnsResourceRecordType.NS:
xfrRecords.Add(record);
IReadOnlyList glueRecords = authRecordInfo.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);
//only primary and secondary zones support zone transfer
IReadOnlyList soaRecords = authZone.GetApexRecords(DnsResourceRecordType.SOA);
if (soaRecords.Count != 1)
throw new InvalidOperationException("Zone must be a primary or secondary zone.");
DnsResourceRecord currentSoaRecord = soaRecords[0];
uint clientSerial = (clientSoaRecord.RDATA as DnsSOARecordData).Serial;
if (clientSerial == (currentSoaRecord.RDATA as DnsSOARecordData).Serial)
{
//zone not modified
return new DnsResourceRecord[] { 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))
apexZone.UpdateDnssecStatus();
}
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 (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.GetZoneWithSubDomainZones(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();
//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)
{
_ = _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: " + zoneName);
foreach (KeyValuePair>> zoneEntry in DnsResourceRecord.GroupRecords(records))
{
if (zoneName.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase))
{
foreach (KeyValuePair> rrsetEntry in zoneEntry.Value)
{
if (rrsetEntry.Key == DnsResourceRecordType.RRSIG)
{
//RRSIG records in response are not complete RRSet
foreach (DnsResourceRecord record in rrsetEntry.Value)
apexZone.AddRecord(record);
}
else
{
apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);
}
}
}
else
{
ValidateZoneNameFor(zoneName, zoneEntry.Key);
AuthZone authZone = GetOrAddSubDomainZone(zoneName, zoneEntry.Key);
foreach (KeyValuePair> rrsetEntry in zoneEntry.Value)
{
if (rrsetEntry.Key == DnsResourceRecordType.RRSIG)
{
//RRSIG records in response are not complete RRSet
foreach (DnsResourceRecord record in rrsetEntry.Value)
authZone.AddRecord(record);
}
else
{
authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);
}
}
if (authZone is SubDomainZone subDomainZone)
subDomainZone.AutoUpdateState();
}
}
apexZone.UpdateDnssecStatus();
}
internal void LoadRecords(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, string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData[] records)
{
ValidateZoneNameFor(zoneName, domain);
DnsResourceRecord[] resourceRecords = new DnsResourceRecord[records.Length];
for (int i = 0; i < records.Length; i++)
resourceRecords[i] = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, records[i]);
AuthZone authZone = GetOrAddSubDomainZone(zoneName, domain);
authZone.SetRecords(type, resourceRecords);
if (authZone is SubDomainZone subDomainZone)
subDomainZone.AutoUpdateState();
}
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, string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData record)
{
ValidateZoneNameFor(zoneName, domain);
AuthZone authZone = GetOrAddSubDomainZone(zoneName, domain);
authZone.AddRecord(new DnsResourceRecord(authZone.Name, type, DnsClass.IN, ttl, 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 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, string domain, DnsResourceRecordType type, DnsResourceRecordData record)
{
ValidateZoneNameFor(zoneName, domain);
if (_root.TryGet(zoneName, domain, out AuthZone authZone))
{
if (authZone.DeleteRecord(type, record))
{
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 new List(_zoneIndex);
}
finally
{
_zoneIndexLock.ExitReadLock();
}
}
public ZonesPage GetZonesPage(int pageNumber, int zonesPerPage)
{
_zoneIndexLock.EnterReadLock();
try
{
int totalZones = _zoneIndex.Count;
if (totalZones < 1)
return new ZonesPage(0, 0, 0, Array.Empty());
if (pageNumber == 0)
pageNumber = 1;
int totalPages = (totalZones / zonesPerPage) + (totalZones % zonesPerPage > 0 ? 1 : 0);
if ((pageNumber > totalPages) || (pageNumber < 0))
pageNumber = totalPages;
int start = (pageNumber - 1) * zonesPerPage;
int end = Math.Min(start + zonesPerPage, totalZones);
List zones = new List(end - start);
for (int i = start; i < end; i++)
zones.Add(_zoneIndex[i]);
return new ZonesPage(pageNumber, totalPages, totalZones, zones);
}
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);
}
//no delegation found
return null;
}
public DnsDatagram Query(DnsDatagram request, bool isRecursionAllowed)
{
DnsQuestionRecord question = request.Question[0];
AuthZone zone = _root.FindZone(question.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
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);
if (apexZone is StubZone)
return GetReferralResponse(request, false, apexZone, apexZone);
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)
return GetForwarderResponse(request, null, closest, apexZone); //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);
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 (zone.Equals(delegation))
{
//zone is delegation
return GetReferralResponse(request, dnssecOk, delegation, apexZone);
}
IReadOnlyList authority = null;
IReadOnlyList additional;
IReadOnlyList answers = zone.QueryRecords(question.Type, dnssecOk);
if (answers.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);
if (apexZone is StubZone)
return GetReferralResponse(request, false, apexZone, apexZone);
}
authority = zone.QueryRecords(DnsResourceRecordType.APP, false);
if (authority.Count == 0)
{
if (apexZone is ForwarderZone)
return GetForwarderResponse(request, zone, closest, apexZone); //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);
else
nsecRecords = _root.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.Contains('*') && !zone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase))
{
//wildcard zone; generate new answer records
DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answers.Count];
for (int i = 0; i < answers.Count; i++)
wildcardAnswers[i] = new DnsResourceRecord(question.Name, answers[i].Type, answers[i].Class, answers[i].TTL, answers[i].RDATA) { Tag = answers[i].Tag };
answers = 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);
else
nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, true);
if (nsecRecords.Count > 0)
authority = nsecRecords;
}
}
DnsResourceRecord lastRR = answers[answers.Count - 1];
if ((lastRR.Type != question.Type) && (question.Type != DnsResourceRecordType.ANY))
{
switch (lastRR.Type)
{
case DnsResourceRecordType.CNAME:
List newAnswers = new List(answers.Count + 1);
newAnswers.AddRange(answers);
ResolveCNAME(question, dnssecOk, lastRR, newAnswers);
answers = newAnswers;
break;
case DnsResourceRecordType.ANAME:
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(answers, dnssecOk);
break;
default:
additional = null;
break;
}
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, authority, additional);
}
}
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.DnssecStatus != AuthZoneDnssecStatus.Unsigned))
{
IReadOnlyList dnsKeyRecords = zoneInfo.GetApexRecords(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)
{
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 = CreateEmptyZone(zoneInfo);
try
{
//load records
LoadRecords(apexZone, records);
}
catch
{
DeleteZone(zoneInfo.Name);
throw;
}
//init zone
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
(apexZone as PrimaryZone).TriggerNotify();
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 = new AuthRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA);
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 = CreateEmptyZone(zoneInfo);
try
{
//load records
LoadRecords(apexZone, records);
}
catch
{
DeleteZone(zoneInfo.Name);
throw;
}
//init zone
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
(apexZone as PrimaryZone).TriggerNotify();
break;
}
return new AuthZoneInfo(apexZone);
}
case 4:
{
//read zone info
AuthZoneInfo zoneInfo = new AuthZoneInfo(bR);
//create zone
ApexZone apexZone = CreateEmptyZone(zoneInfo);
//read all zone records
DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()];
if (records.Length > 0)
{
for (int i = 0; i < records.Length; i++)
{
records[i] = new DnsResourceRecord(s);
records[i].Tag = new AuthRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA);
}
try
{
//load records
LoadRecords(apexZone, records);
}
catch
{
DeleteZone(zoneInfo.Name);
throw;
}
//init zone
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
(apexZone as PrimaryZone).TriggerNotify();
break;
case AuthZoneType.Secondary:
SecondaryZone secondary = apexZone as SecondaryZone;
secondary.TriggerNotify();
secondary.TriggerRefresh();
break;
case AuthZoneType.Stub:
(apexZone as StubZone).TriggerRefresh();
break;
}
}
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)
throw new InvalidOperationException("Zone was not found: " + zoneName);
//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(zoneName, records);
bW.Write(records.Count);
foreach (DnsResourceRecord record in records)
{
record.WriteTo(s);
if (record.Tag is not AuthRecordInfo rrInfo)
rrInfo = AuthRecordInfo.Default; //default info
rrInfo.WriteTo(bW);
}
}
public void SaveZoneFile(string zoneName)
{
zoneName = zoneName.ToLower();
using (MemoryStream mS = new MemoryStream())
{
//serialize zone
WriteZoneTo(zoneName, mS);
//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));
}
public void DeleteZoneFile(string zoneName)
{
zoneName = zoneName.ToLower();
File.Delete(Path.Combine(_dnsServer.ConfigFolder, "zones", zoneName + ".zone"));
_dnsServer.LogManager?.Write("Deleted zone file for domain: " + zoneName);
}
#endregion
#region properties
public string ServerDomain
{
get { return _serverDomain; }
set { UpdateServerDomain(value); }
}
public int TotalZones
{ get { return _zoneIndex.Count; } }
#endregion
public class ZonesPage
{
#region variables
readonly long _pageNumber;
readonly long _totalPages;
readonly long _totalZones;
readonly IReadOnlyList _zones;
#endregion
#region constructor
public ZonesPage(long pageNumber, long totalPages, long totalZones, IReadOnlyList zones)
{
_pageNumber = pageNumber;
_totalPages = totalPages;
_totalZones = totalZones;
_zones = zones;
}
#endregion
#region properties
public long PageNumber
{ get { return _pageNumber; } }
public long TotalPages
{ get { return _totalPages; } }
public long TotalZones
{ get { return _totalZones; } }
public IReadOnlyList Zones
{ get { return _zones; } }
#endregion
}
}
}