/*
Technitium DNS Server
Copyright (C) 2020 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.Zones;
using System.Collections.Generic;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.ZoneManagers
{
public sealed class CacheZoneManager : DnsCache
{
#region variables
const uint FAILURE_RECORD_TTL = 30u;
const uint NEGATIVE_RECORD_TTL = 300u;
const uint MINIMUM_RECORD_TTL = 10u;
const uint SERVE_STALE_TTL = 7 * 24 * 60 * 60; //7 days serve stale ttl as per draft-ietf-dnsop-serve-stale-04
readonly ZoneTree _root = new ZoneTree();
#endregion
#region constructor
public CacheZoneManager()
: base(FAILURE_RECORD_TTL, NEGATIVE_RECORD_TTL, MINIMUM_RECORD_TTL, SERVE_STALE_TTL)
{ }
#endregion
#region protected
protected override void CacheRecords(IReadOnlyList resourceRecords)
{
if (resourceRecords.Count == 1)
{
CacheZone zone = _root.GetOrAdd(resourceRecords[0].Name, delegate (string key)
{
return new CacheZone(resourceRecords[0].Name);
});
zone.SetRecords(resourceRecords[0].Type, resourceRecords);
}
else
{
Dictionary>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(resourceRecords);
//add grouped records
foreach (KeyValuePair>> groupedByTypeRecords in groupedByDomainRecords)
{
CacheZone zone = _root.GetOrAdd(groupedByTypeRecords.Key, delegate (string key)
{
return new CacheZone(groupedByTypeRecords.Key);
});
foreach (KeyValuePair> groupedRecords in groupedByTypeRecords.Value)
zone.SetRecords(groupedRecords.Key, groupedRecords.Value);
}
}
}
#endregion
#region private
private List GetAdditionalRecords(IReadOnlyCollection nsRecords, bool serveStale)
{
List additionalRecords = new List();
foreach (DnsResourceRecord nsRecord in nsRecords)
{
if (nsRecord.Type != DnsResourceRecordType.NS)
continue;
CacheZone cacheZone = _root.FindZone((nsRecord.RDATA as DnsNSRecord).NameServer, out _, out _, out _);
if (cacheZone != null)
{
{
IReadOnlyList records = cacheZone.QueryRecords(DnsResourceRecordType.A, serveStale);
if ((records.Count > 0) && (records[0].RDATA is DnsARecord))
additionalRecords.AddRange(records);
}
{
IReadOnlyList records = cacheZone.QueryRecords(DnsResourceRecordType.AAAA, serveStale);
if ((records.Count > 0) && (records[0].RDATA is DnsAAAARecord))
additionalRecords.AddRange(records);
}
}
}
return additionalRecords;
}
#endregion
#region public
public void DoMaintenance()
{
foreach (CacheZone zone in _root)
{
zone.RemoveExpiredRecords();
if (zone.IsEmpty)
_root.TryRemove(zone.Name, out _); //remove empty zone
}
}
public void Flush()
{
_root.Clear();
}
public bool DeleteZone(string domain)
{
return _root.TryRemove(domain, out _);
}
public List ListSubDomains(string domain)
{
return _root.ListSubDomains(domain);
}
public List ListAllRecords(string domain)
{
if (_root.TryGet(domain, out CacheZone zone))
return zone.ListAllRecords();
return new List(0);
}
public DnsDatagram QueryClosestDelegation(DnsDatagram request)
{
_ = _root.FindZone(request.Question[0].Name, out CacheZone delegation, out _, out _);
if (delegation == null)
{
//no cached delegation found
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.Refused, request.Question);
}
//return closest name servers in delegation
IReadOnlyList authority = delegation.QueryRecords(DnsResourceRecordType.NS, false);
List additional = GetAdditionalRecords(authority, false);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional);
}
public override DnsDatagram Query(DnsDatagram request, bool serveStale = false)
{
CacheZone zone = _root.FindZone(request.Question[0].Name, out CacheZone delegation, out _, out _);
if (zone == null)
{
//zone not found
if (delegation == null)
{
//no cached delegation found
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.Refused, request.Question);
}
//return closest name servers in delegation
IReadOnlyList authority = delegation.QueryRecords(DnsResourceRecordType.NS, serveStale);
List additional = GetAdditionalRecords(authority, serveStale);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional);
}
//zone found
IReadOnlyList answers = zone.QueryRecords(request.Question[0].Type, serveStale);
if (answers.Count > 0)
{
if (answers[0].RDATA is DnsEmptyRecord)
{
DnsResourceRecord[] authority = null;
DnsResourceRecord soaRecord = (answers[0].RDATA as DnsEmptyRecord).Authority;
if (soaRecord != null)
authority = new DnsResourceRecord[] { soaRecord };
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, authority);
}
if (answers[0].RDATA is DnsNXRecord)
{
DnsResourceRecord[] authority = null;
DnsResourceRecord soaRecord = (answers[0].RDATA as DnsNXRecord).Authority;
if (soaRecord != null)
authority = new DnsResourceRecord[] { soaRecord };
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NameError, request.Question, null, authority);
}
if (answers[0].RDATA is DnsANYRecord)
{
DnsANYRecord anyRR = answers[0].RDATA as DnsANYRecord;
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, anyRR.Records);
}
if (answers[0].RDATA is DnsFailureRecord)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, (answers[0].RDATA as DnsFailureRecord).RCODE, request.Question);
IReadOnlyList additional = null;
if (request.Question[0].Type == DnsResourceRecordType.NS)
additional = GetAdditionalRecords(answers, serveStale);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, answers, null, additional);
}
//found nothing in cache
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.Refused, request.Question);
}
#endregion
}
}