/*
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.ResourceRecords;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.Zones
{
public enum AuthZoneTransfer : byte
{
Deny = 0,
Allow = 1,
AllowOnlyZoneNameServers = 2,
AllowOnlySpecifiedNameServers = 3,
AllowBothZoneAndSpecifiedNameServers = 4
}
public enum AuthZoneNotify : byte
{
None = 0,
ZoneNameServers = 1,
SpecifiedNameServers = 2,
BothZoneAndSpecifiedNameServers = 3
}
public enum AuthZoneUpdate : byte
{
Deny = 0,
Allow = 1,
AllowOnlyZoneNameServers = 2,
AllowOnlySpecifiedIpAddresses = 3,
AllowBothZoneNameServersAndSpecifiedIpAddresses = 4
}
abstract class ApexZone : AuthZone, IDisposable
{
#region variables
protected AuthZoneTransfer _zoneTransfer;
protected IReadOnlyCollection _zoneTransferNameServers;
protected AuthZoneNotify _notify;
protected IReadOnlyCollection _notifyNameServers;
protected AuthZoneUpdate _update;
protected IReadOnlyCollection _updateIpAddresses;
protected List _zoneHistory; //for IXFR support
protected IReadOnlyDictionary _zoneTransferTsigKeyNames;
protected IReadOnlyDictionary>> _updateSecurityPolicies;
protected AuthZoneDnssecStatus _dnssecStatus;
Timer _notifyTimer;
bool _notifyTimerTriggered;
const int NOTIFY_TIMER_INTERVAL = 10000;
List _notifyList;
List _notifyFailed;
const int NOTIFY_TIMEOUT = 10000;
const int NOTIFY_RETRIES = 5;
protected bool _syncFailed;
#endregion
#region constructor
protected ApexZone(AuthZoneInfo zoneInfo)
: base(zoneInfo)
{
_zoneTransfer = zoneInfo.ZoneTransfer;
_zoneTransferNameServers = zoneInfo.ZoneTransferNameServers;
_notify = zoneInfo.Notify;
_notifyNameServers = zoneInfo.NotifyNameServers;
_update = zoneInfo.Update;
_updateIpAddresses = zoneInfo.UpdateIpAddresses;
if (zoneInfo.ZoneHistory is null)
_zoneHistory = new List();
else
_zoneHistory = new List(zoneInfo.ZoneHistory);
_zoneTransferTsigKeyNames = zoneInfo.ZoneTransferTsigKeyNames;
_updateSecurityPolicies = zoneInfo.UpdateSecurityPolicies;
}
protected ApexZone(string name)
: base(name)
{
_zoneHistory = new List();
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_notifyTimer is not null)
_notifyTimer.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
}
#endregion
#region protected
protected void CleanupHistory(List history)
{
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
DateTime expiry = DateTime.UtcNow.AddSeconds(-soa.Expire);
int index = 0;
while (index < history.Count)
{
//check difference sequence
if (history[index].GetAuthRecordInfo().DeletedOn > expiry)
break; //found record to keep
//skip to next difference sequence
index++;
int soaCount = 1;
while (index < history.Count)
{
if (history[index].Type == DnsResourceRecordType.SOA)
{
soaCount++;
if (soaCount == 3)
break;
}
index++;
}
}
if (index == history.Count)
{
//delete entire history
history.Clear();
return;
}
//remove expired records
history.RemoveRange(0, index);
}
protected void InitNotify(DnsServer dnsServer)
{
_notifyTimer = new Timer(NotifyTimerCallback, dnsServer, Timeout.Infinite, Timeout.Infinite);
_notifyList = new List();
_notifyFailed = new List();
}
protected void DisableNotifyTimer()
{
if (_notifyTimer is not null)
_notifyTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
#endregion
#region private
private async void NotifyTimerCallback(object state)
{
DnsServer dnsServer = state as DnsServer;
List notifiedNameServers = new List();
async Task NotifyZoneNameServersAsync(bool onlyFailedNameServers)
{
string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer;
IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords
//notify all secondary name servers
List tasks = new List();
foreach (DnsResourceRecord nsRecord in nsRecords)
{
if (nsRecord.GetAuthRecordInfo().Disabled)
continue;
string nameServerHost = (nsRecord.RDATA as DnsNSRecordData).NameServer;
if (primaryNameServer.Equals(nameServerHost, StringComparison.OrdinalIgnoreCase))
continue; //skip primary name server
if (onlyFailedNameServers)
{
lock (_notifyFailed)
{
if (!_notifyFailed.Contains(nameServerHost))
continue;
}
}
notifiedNameServers.Add(nameServerHost);
List nameServers = new List(2);
await ResolveNameServerAddressesAsync(dnsServer, nsRecord, nameServers);
if (nameServers.Count > 0)
{
tasks.Add(NotifyNameServerAsync(dnsServer, nameServerHost, nameServers));
}
else
{
lock (_notifyFailed)
{
if (!_notifyFailed.Contains(nameServerHost))
_notifyFailed.Add(nameServerHost);
}
dnsServer.LogManager?.Write("DNS Server failed to notify name server '" + nameServerHost + "' due to failure in resolving its IP address for zone: " + (_name == "" ? "" : _name));
}
}
await Task.WhenAll(tasks);
}
async Task NotifySpecifiedNameServersAsync(bool onlyFailedNameServers)
{
IReadOnlyCollection specifiedNameServers = _notifyNameServers;
if (specifiedNameServers is not null)
{
List tasks = new List();
foreach (IPAddress specifiedNameServer in specifiedNameServers)
{
string nameServerHost = specifiedNameServer.ToString();
if (onlyFailedNameServers)
{
lock (_notifyFailed)
{
if (!_notifyFailed.Contains(nameServerHost))
continue;
}
}
notifiedNameServers.Add(nameServerHost);
tasks.Add(NotifyNameServerAsync(dnsServer, nameServerHost, new NameServerAddress[] { new NameServerAddress(specifiedNameServer) }));
}
await Task.WhenAll(tasks);
}
}
try
{
switch (_notify)
{
case AuthZoneNotify.ZoneNameServers:
await NotifyZoneNameServersAsync(!_notifyTimerTriggered);
break;
case AuthZoneNotify.SpecifiedNameServers:
await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered);
break;
case AuthZoneNotify.BothZoneAndSpecifiedNameServers:
Task t1 = NotifyZoneNameServersAsync(!_notifyTimerTriggered);
Task t2 = NotifySpecifiedNameServersAsync(!_notifyTimerTriggered);
await Task.WhenAll(t1, t2);
break;
}
//remove non-existent name servers from notify failed list
lock (_notifyFailed)
{
if (_notifyFailed.Count > 0)
{
List toRemove = new List();
foreach (string failedNameServer in _notifyFailed)
{
if (!notifiedNameServers.Contains(failedNameServer))
toRemove.Add(failedNameServer);
}
foreach (string failedNameServer in toRemove)
_notifyFailed.Remove(failedNameServer);
if (_notifyFailed.Count > 0)
{
//set timer to notify failed name servers again
int retryInterval = (int)((_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Retry * 1000);
_notifyTimer.Change(retryInterval, Timeout.Infinite);
}
}
}
}
catch (Exception ex)
{
dnsServer.LogManager?.Write(ex);
}
finally
{
_notifyTimerTriggered = false;
}
}
private async Task NotifyNameServerAsync(DnsServer dnsServer, string nameServerHost, IReadOnlyList nameServers)
{
//use notify list to prevent multiple threads from notifying the same name server
lock (_notifyList)
{
if (_notifyList.Contains(nameServerHost))
return; //already notifying the name server in another thread
_notifyList.Add(nameServerHost);
}
try
{
DnsClient client = new DnsClient(nameServers);
client.Proxy = dnsServer.Proxy;
client.Timeout = NOTIFY_TIMEOUT;
client.Retries = NOTIFY_RETRIES;
DnsDatagram notifyRequest = new DnsDatagram(0, false, DnsOpcode.Notify, true, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN) }, _entries[DnsResourceRecordType.SOA]);
DnsDatagram response = await client.ResolveAsync(notifyRequest);
switch (response.RCODE)
{
case DnsResponseCode.NoError:
case DnsResponseCode.NotImplemented:
{
//transaction complete
lock (_notifyFailed)
{
_notifyFailed.Remove(nameServerHost);
}
LogManager log = dnsServer.LogManager;
if (log is not null)
log.Write("DNS Server successfully notified name server '" + nameServerHost + "' for zone: " + (_name == "" ? "" : _name));
}
break;
default:
{
//transaction failed
lock (_notifyFailed)
{
if (!_notifyFailed.Contains(nameServerHost))
_notifyFailed.Add(nameServerHost);
}
LogManager log = dnsServer.LogManager;
if (log is not null)
log.Write("DNS Server failed to notify name server '" + nameServerHost + "' (RCODE=" + response.RCODE.ToString() + ") for zone : " + (_name == "" ? "" : _name));
}
break;
}
}
catch (Exception ex)
{
lock (_notifyFailed)
{
if (!_notifyFailed.Contains(nameServerHost))
_notifyFailed.Add(nameServerHost);
}
dnsServer.LogManager?.Write("DNS Server failed to notify name server '" + nameServerHost + "' for zone: " + (_name == "" ? "" : _name) + "\r\n" + ex.ToString());
}
finally
{
lock (_notifyList)
{
_notifyList.Remove(nameServerHost);
}
}
}
private static async Task ResolveNameServerAddressesAsync(DnsServer dnsServer, string nsDomain, int port, DnsTransportProtocol protocol, List outNameServers)
{
try
{
DnsDatagram response = await dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.A, DnsClass.IN));
if (response.Answer.Count > 0)
{
IReadOnlyList addresses = DnsClient.ParseResponseA(response);
foreach (IPAddress address in addresses)
outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol));
}
}
catch
{ }
if (dnsServer.PreferIPv6)
{
try
{
DnsDatagram response = await dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.AAAA, DnsClass.IN));
if (response.Answer.Count > 0)
{
IReadOnlyList addresses = DnsClient.ParseResponseAAAA(response);
foreach (IPAddress address in addresses)
outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol));
}
}
catch
{ }
}
}
private static Task ResolveNameServerAddressesAsync(DnsServer dnsServer, DnsResourceRecord nsRecord, List outNameServers)
{
string nsDomain = (nsRecord.RDATA as DnsNSRecordData).NameServer;
IReadOnlyList glueRecords = nsRecord.GetAuthRecordInfo().GlueRecords;
if (glueRecords is not null)
{
foreach (DnsResourceRecord glueRecord in glueRecords)
{
switch (glueRecord.Type)
{
case DnsResourceRecordType.A:
outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsARecordData).Address));
break;
case DnsResourceRecordType.AAAA:
if (dnsServer.PreferIPv6)
outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsAAAARecordData).Address));
break;
}
}
return Task.CompletedTask;
}
else
{
return ResolveNameServerAddressesAsync(dnsServer, nsDomain, 53, DnsTransportProtocol.Udp, outNameServers);
}
}
internal virtual void UpdateDnssecStatus()
{
if (!_entries.ContainsKey(DnsResourceRecordType.DNSKEY))
_dnssecStatus = AuthZoneDnssecStatus.Unsigned;
else if (_entries.ContainsKey(DnsResourceRecordType.NSEC3PARAM))
_dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3;
else
_dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC;
}
#endregion
#region public
public IReadOnlyList GetZoneHistory()
{
lock (_zoneHistory)
{
return _zoneHistory.ToArray();
}
}
public void TriggerNotify()
{
if (_disabled)
return;
if (_notify == AuthZoneNotify.None)
{
if (_notifyFailed is not null)
{
lock (_notifyFailed)
{
_notifyFailed.Clear();
}
}
return;
}
if (_notifyTimerTriggered)
return;
if (_disposed)
return;
if (_notifyTimer is null)
return;
_notifyTimer.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite);
_notifyTimerTriggered = true;
}
public async Task> GetPrimaryNameServerAddressesAsync(DnsServer dnsServer)
{
DnsResourceRecord soaRecord = _entries[DnsResourceRecordType.SOA][0];
IReadOnlyList primaryNameServers = soaRecord.GetAuthRecordInfo().PrimaryNameServers;
if (primaryNameServers is not null)
{
List resolvedNameServers = new List(primaryNameServers.Count * 2);
foreach (NameServerAddress nameServer in primaryNameServers)
{
if (nameServer.IsIPEndPointStale)
await ResolveNameServerAddressesAsync(dnsServer, nameServer.Host, nameServer.Port, nameServer.Protocol, resolvedNameServers);
else
resolvedNameServers.Add(nameServer);
}
return resolvedNameServers;
}
string primaryNameServer = (soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer;
IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords
List nameServers = new List(nsRecords.Count * 2);
foreach (DnsResourceRecord nsRecord in nsRecords)
{
if (nsRecord.GetAuthRecordInfo().Disabled)
continue;
if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase))
{
//found primary NS
await ResolveNameServerAddressesAsync(dnsServer, nsRecord, nameServers);
break;
}
}
if (nameServers.Count < 1)
await ResolveNameServerAddressesAsync(dnsServer, primaryNameServer, 53, DnsTransportProtocol.Udp, nameServers);
return nameServers;
}
public async Task> GetSecondaryNameServerAddressesAsync(DnsServer dnsServer)
{
string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer;
IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords
List nameServers = new List(nsRecords.Count * 2);
foreach (DnsResourceRecord nsRecord in nsRecords)
{
if (nsRecord.GetAuthRecordInfo().Disabled)
continue;
if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase))
continue; //skip primary name server
await ResolveNameServerAddressesAsync(dnsServer, nsRecord, nameServers);
}
return nameServers;
}
#endregion
#region properties
public virtual AuthZoneTransfer ZoneTransfer
{
get { return _zoneTransfer; }
set { _zoneTransfer = value; }
}
public IReadOnlyCollection ZoneTransferNameServers
{
get { return _zoneTransferNameServers; }
set
{
if ((value is not null) && (value.Count > byte.MaxValue))
throw new ArgumentOutOfRangeException(nameof(ZoneTransferNameServers), "Name server addresses cannot be more than 255.");
_zoneTransferNameServers = value;
}
}
public virtual AuthZoneNotify Notify
{
get { return _notify; }
set
{
if (_notify != value)
{
_notify = value;
lock (_notifyFailed)
{
_notifyFailed.Clear();
}
}
}
}
public IReadOnlyCollection NotifyNameServers
{
get { return _notifyNameServers; }
set
{
if ((value is not null) && (value.Count > byte.MaxValue))
throw new ArgumentOutOfRangeException(nameof(NotifyNameServers), "Name server addresses cannot be more than 255.");
if (_notifyNameServers != value)
{
_notifyNameServers = value;
lock (_notifyFailed)
{
_notifyFailed.Clear();
}
}
}
}
public virtual AuthZoneUpdate Update
{
get { return _update; }
set { _update = value; }
}
public IReadOnlyCollection UpdateIpAddresses
{
get { return _updateIpAddresses; }
set
{
if ((value is not null) && (value.Count > byte.MaxValue))
throw new ArgumentOutOfRangeException(nameof(ZoneTransferNameServers), "IP addresses cannot be more than 255.");
_updateIpAddresses = value;
}
}
public IReadOnlyDictionary ZoneTransferTsigKeyNames
{
get { return _zoneTransferTsigKeyNames; }
set { _zoneTransferTsigKeyNames = value; }
}
public IReadOnlyDictionary>> UpdateSecurityPolicies
{
get { return _updateSecurityPolicies; }
set { _updateSecurityPolicies = value; }
}
public string[] NotifyFailed
{
get
{
if (_notifyFailed is null)
return Array.Empty();
lock (_notifyFailed)
{
if (_notifyFailed.Count > 0)
return _notifyFailed.ToArray();
return Array.Empty();
}
}
}
public bool SyncFailed
{ get { return _syncFailed; } }
public AuthZoneDnssecStatus DnssecStatus
{ get { return _dnssecStatus; } }
#endregion
}
}