/*
Technitium DNS Server
Copyright (C) 2025 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.ApplicationCommon;
using DnsServerCore.Dns.Applications;
using DnsServerCore.Dns.ResourceRecords;
using DnsServerCore.Dns.Trees;
using DnsServerCore.Dns.ZoneManagers;
using DnsServerCore.Dns.Zones;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Mail;
using System.Net.Quic;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.ExceptionServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ClientConnection;
using TechnitiumLibrary.Net.Dns.EDnsOptions;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
using TechnitiumLibrary.Net.Proxy;
using TechnitiumLibrary.Net.ProxyProtocol;
namespace DnsServerCore.Dns
{
#pragma warning disable CA2252 // This API requires opting into preview features
#pragma warning disable CA1416 // Validate platform compatibility
public enum DnsServerRecursion : byte
{
Deny = 0,
Allow = 1,
AllowOnlyForPrivateNetworks = 2,
UseSpecifiedNetworkACL = 3
}
public enum DnsServerBlockingType : byte
{
AnyAddress = 0,
NxDomain = 1,
CustomAddress = 2
}
public sealed class DnsServer : IAsyncDisposable, IDisposable, IDnsClient
{
#region enum
enum ServiceState
{
Stopped = 0,
Starting = 1,
Running = 2,
Stopping = 3
}
#endregion
#region variables
internal const int MAX_CNAME_HOPS = 16;
internal const int SERVE_STALE_MAX_WAIT_TIME = 1800; //max time to wait before serve stale [RFC 8767]
const int SERVE_STALE_TIME_DIFFERENCE = 200; //200ms before client timeout [RFC 8767]
internal const int RECURSIVE_RESOLUTION_TIMEOUT = 60000; //max time that can be spent per recursive resolution task
static readonly IPEndPoint IPENDPOINT_ANY_0 = new IPEndPoint(IPAddress.Any, 0);
static readonly IReadOnlyCollection _aRecords = [new DnsARecordData(IPAddress.Any)];
static readonly IReadOnlyCollection _aaaaRecords = [new DnsAAAARecordData(IPAddress.IPv6Any)];
static readonly List _doqApplicationProtocols = new List() { new SslApplicationProtocol("doq") };
string _serverDomain;
readonly string _configFolder;
readonly string _dohwwwFolder;
IReadOnlyList _localEndPoints;
LogManager _log;
MailAddress _responsiblePerson;
MailAddress _defaultResponsiblePerson;
NameServerAddress _thisServer;
readonly List _udpListeners = new List();
readonly List _udpProxyListeners = new List();
readonly List _tcpListeners = new List();
readonly List _tcpProxyListeners = new List();
readonly List _tlsListeners = new List();
readonly List _quicListeners = new List();
WebApplication _dohWebService;
readonly AuthZoneManager _authZoneManager;
readonly AllowedZoneManager _allowedZoneManager;
readonly BlockedZoneManager _blockedZoneManager;
readonly BlockListZoneManager _blockListZoneManager;
readonly CacheZoneManager _cacheZoneManager;
readonly DnsApplicationManager _dnsApplicationManager;
readonly ResolverDnsCache _dnsCache;
readonly ResolverDnsCache _dnsCacheSkipDnsApps; //to prevent request reaching apps again
readonly StatsManager _stats;
IReadOnlyCollection _zoneTransferAllowedNetworks;
IReadOnlyCollection _notifyAllowedNetworks;
bool _preferIPv6;
ushort _udpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE;
bool _dnssecValidation = true;
bool _eDnsClientSubnet;
byte _eDnsClientSubnetIPv4PrefixLength = 24;
byte _eDnsClientSubnetIPv6PrefixLength = 56;
NetworkAddress _eDnsClientSubnetIpv4Override;
NetworkAddress _eDnsClientSubnetIpv6Override;
int _qpmLimitRequests = 6000; //100qps
int _qpmLimitErrors = 600; //10qps
int _qpmLimitSampleMinutes = 5;
int _qpmLimitIPv4PrefixLength = 24;
int _qpmLimitIPv6PrefixLength = 56;
IReadOnlyCollection _qpmLimitBypassList;
int _clientTimeout = 2000;
int _tcpSendTimeout = 10000;
int _tcpReceiveTimeout = 10000;
int _quicIdleTimeout = 60000;
int _quicMaxInboundStreams = 100;
int _listenBacklog = 100;
bool _enableDnsOverUdpProxy;
bool _enableDnsOverTcpProxy;
bool _enableDnsOverHttp;
bool _enableDnsOverTls;
bool _enableDnsOverHttps;
bool _enableDnsOverHttp3;
bool _enableDnsOverQuic;
IReadOnlyCollection _reverseProxyNetworkACL;
int _dnsOverUdpProxyPort = 538;
int _dnsOverTcpProxyPort = 538;
int _dnsOverHttpPort = 80;
int _dnsOverTlsPort = 853;
int _dnsOverHttpsPort = 443;
int _dnsOverQuicPort = 853;
X509Certificate2Collection _certificateCollection;
SslServerAuthenticationOptions _dotSslServerAuthenticationOptions;
SslServerAuthenticationOptions _doqSslServerAuthenticationOptions;
SslServerAuthenticationOptions _dohSslServerAuthenticationOptions;
string _dnsOverHttpRealIpHeader = "X-Real-IP";
IReadOnlyDictionary _tsigKeys;
DnsServerRecursion _recursion;
IReadOnlyCollection _recursionNetworkACL;
bool _randomizeName;
bool _qnameMinimization;
bool _nsRevalidation;
int _resolverRetries = 2;
int _resolverTimeout = 1500;
int _resolverConcurrency = 2;
int _resolverMaxStackCount = 16;
bool _serveStale = true;
int _serveStaleMaxWaitTime = SERVE_STALE_MAX_WAIT_TIME;
int _cachePrefetchEligibility = 2;
int _cachePrefetchTrigger = 9;
int _cachePrefetchSampleIntervalInMinutes = 5;
int _cachePrefetchSampleEligibilityHitsPerHour = 30;
bool _enableBlocking = true;
bool _allowTxtBlockingReport = true;
IReadOnlyCollection _blockingBypassList;
DnsServerBlockingType _blockingType = DnsServerBlockingType.NxDomain;
uint _blockingAnswerTtl = 30;
IReadOnlyCollection _customBlockingARecords = Array.Empty();
IReadOnlyCollection _customBlockingAAAARecords = Array.Empty();
NetProxy _proxy;
IReadOnlyList _forwarders;
bool _concurrentForwarding = true;
int _forwarderRetries = 3;
int _forwarderTimeout = 2000;
int _forwarderConcurrency = 2;
LogManager _resolverLog;
LogManager _queryLog;
Timer _cachePrefetchSamplingTimer;
readonly object _cachePrefetchSamplingTimerLock = new object();
const int CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL = 5000;
Timer _cachePrefetchRefreshTimer;
readonly object _cachePrefetchRefreshTimerLock = new object();
const int CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL = 10000;
DateTime _cachePrefetchSamplingTimerTriggersOn;
IList _cacheRefreshSampleList;
Timer _cacheMaintenanceTimer;
readonly object _cacheMaintenanceTimerLock = new object();
const int CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL = 5 * 60 * 1000;
const int CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL = 5 * 60 * 1000;
Timer _qpmLimitSamplingTimer;
readonly object _qpmLimitSamplingTimerLock = new object();
const int QPM_LIMIT_SAMPLING_TIMER_INTERVAL = 10000;
IReadOnlyDictionary _qpmLimitClientSubnetStats;
IReadOnlyDictionary _qpmLimitErrorClientSubnetStats;
readonly IndependentTaskScheduler _queryTaskScheduler = new IndependentTaskScheduler(threadName: "QueryThreadPool");
TaskPool _resolverTaskPool;
readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(ThreadPriority.AboveNormal, "ResolverThreadPool");
readonly ConcurrentDictionary> _resolverTasks = new ConcurrentDictionary>(-1, 1000);
volatile ServiceState _state = ServiceState.Stopped;
#endregion
#region constructor
static DnsServer()
{
//set min threads since the default value is too small
{
ThreadPool.GetMinThreads(out int minWorker, out int minIOC);
int minThreads = Environment.ProcessorCount * 16;
if (minWorker < minThreads)
minWorker = minThreads;
if (minIOC < minThreads)
minIOC = minThreads;
ThreadPool.SetMinThreads(minWorker, minIOC);
}
}
public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, LogManager log = null)
: this(serverDomain, configFolder, dohwwwFolder, new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) }, log)
{ }
public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, IPEndPoint localEndPoint, LogManager log = null)
: this(serverDomain, configFolder, dohwwwFolder, new IPEndPoint[] { localEndPoint }, log)
{ }
public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, IReadOnlyList localEndPoints, LogManager log = null)
{
_serverDomain = serverDomain;
_configFolder = configFolder;
_dohwwwFolder = dohwwwFolder;
_localEndPoints = localEndPoints;
_log = log;
ReconfigureResolverTaskPool(100);
_authZoneManager = new AuthZoneManager(this);
_allowedZoneManager = new AllowedZoneManager(this);
_blockedZoneManager = new BlockedZoneManager(this);
_blockListZoneManager = new BlockListZoneManager(this);
_cacheZoneManager = new CacheZoneManager(this);
_dnsApplicationManager = new DnsApplicationManager(this);
_dnsCache = new ResolverDnsCache(this, false);
_dnsCacheSkipDnsApps = new ResolverDnsCache(this, true); //to prevent request reaching apps again
//init stats
_stats = new StatsManager(this);
}
#endregion
#region IDisposable
bool _disposed;
public async ValueTask DisposeAsync()
{
if (_disposed)
return;
await StopAsync();
_authZoneManager?.Dispose();
_allowedZoneManager?.Dispose();
_blockedZoneManager?.Dispose();
_dnsApplicationManager?.Dispose();
_stats?.Dispose();
_resolverTaskPool?.Dispose();
_disposed = true;
}
public void Dispose()
{
DisposeAsync().Sync();
}
#endregion
#region private
private async Task ReadUdpRequestAsync(Socket udpListener, DnsTransportProtocol protocol)
{
byte[] recvBuffer;
if (protocol == DnsTransportProtocol.UdpProxy)
recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE + 256];
else
recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE];
using MemoryStream recvBufferStream = new MemoryStream(recvBuffer);
try
{
int localPort = (udpListener.LocalEndPoint as IPEndPoint).Port;
EndPoint epAny;
switch (udpListener.AddressFamily)
{
case AddressFamily.InterNetwork:
epAny = new IPEndPoint(IPAddress.Any, 0);
break;
case AddressFamily.InterNetworkV6:
epAny = new IPEndPoint(IPAddress.IPv6Any, 0);
break;
default:
throw new NotSupportedException("AddressFamily not supported.");
}
SocketReceiveMessageFromResult result;
while (true)
{
recvBufferStream.SetLength(DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE); //resetting length before using buffer
try
{
result = await udpListener.ReceiveMessageFromAsync(recvBuffer, SocketFlags.None, epAny);
}
catch (SocketException ex)
{
switch (ex.SocketErrorCode)
{
case SocketError.ConnectionReset:
case SocketError.HostUnreachable:
case SocketError.MessageSize:
case SocketError.NetworkReset:
result = default;
break;
default:
throw;
}
}
if (result.ReceivedBytes > 0)
{
if (result.RemoteEndPoint is not IPEndPoint remoteEP)
continue;
try
{
recvBufferStream.Position = 0;
recvBufferStream.SetLength(result.ReceivedBytes);
IPEndPoint returnEP = remoteEP;
if (protocol == DnsTransportProtocol.UdpProxy)
{
if (!NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL))
{
//this feature is intended to be used with a reverse proxy or load balancer on private network
continue;
}
ProxyProtocolStream proxyStream = await ProxyProtocolStream.CreateAsServerAsync(recvBufferStream);
remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort);
recvBufferStream.Position = proxyStream.DataOffset;
}
if (IsQpmLimitCrossed(remoteEP.Address))
{
_stats.QueueUpdate(null, remoteEP, protocol, null, true);
continue;
}
DnsDatagram request = DnsDatagram.ReadFrom(recvBufferStream);
request.SetMetadata(new NameServerAddress(new IPEndPoint(result.PacketInformation.Address, localPort), DnsTransportProtocol.Udp));
_ = ProcessUdpRequestAsync(udpListener, remoteEP, returnEP, protocol, request);
}
catch (EndOfStreamException)
{
//ignore incomplete udp datagrams
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
}
}
catch (ObjectDisposedException)
{
//server stopping
}
catch (SocketException ex)
{
switch (ex.SocketErrorCode)
{
case SocketError.OperationAborted:
case SocketError.Interrupted:
break; //server stopping
default:
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
_log?.Write(ex);
break;
}
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
_log?.Write(ex);
}
}
private async Task ProcessUdpRequestAsync(Socket udpListener, IPEndPoint remoteEP, IPEndPoint returnEP, DnsTransportProtocol protocol, DnsDatagram request)
{
byte[] sendBuffer = null;
try
{
DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address));
if (response is null)
{
_stats.QueueUpdate(null, remoteEP, protocol, null, false);
return; //drop request
}
//send response
int sendBufferSize;
if (request.EDNS is null)
sendBufferSize = 512;
else if (request.EDNS.UdpPayloadSize > _udpPayloadSize)
sendBufferSize = _udpPayloadSize;
else
sendBufferSize = request.EDNS.UdpPayloadSize;
sendBuffer = ArrayPool.Shared.Rent(sendBufferSize);
using (MemoryStream sendBufferStream = new MemoryStream(sendBuffer, 0, sendBufferSize))
{
try
{
response.WriteTo(sendBufferStream);
}
catch (NotSupportedException)
{
if (response.IsSigned)
{
//rfc8945 section 5.3
response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, null, null, new DnsResourceRecord[] { response.Additional[response.Additional.Count - 1] }, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative };
}
else
{
switch (response.Question[0].Type)
{
case DnsResourceRecordType.MX:
case DnsResourceRecordType.SRV:
case DnsResourceRecordType.SVCB:
case DnsResourceRecordType.HTTPS:
//removing glue records and trying again since some mail servers fail to fallback to TCP on truncation
//removing glue records to prevent truncation for SRV/SVCB/HTTPS
response = response.CloneWithoutGlueRecords();
sendBufferStream.Position = 0;
try
{
response.WriteTo(sendBufferStream);
}
catch (NotSupportedException)
{
//send TC since response is still big even after removing glue records
response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative };
}
break;
case DnsResourceRecordType.IXFR:
response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, false, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, new DnsResourceRecord[] { response.Answer[0] }, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; //truncate response
break;
default:
response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative };
break;
}
}
sendBufferStream.Position = 0;
response.WriteTo(sendBufferStream);
}
//send dns datagram async
await udpListener.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, returnEP);
}
_queryLog?.Write(remoteEP, protocol, request, response);
_stats.QueueUpdate(request, remoteEP, protocol, response, false);
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
_queryLog?.Write(remoteEP, protocol, request, null);
_log?.Write(remoteEP, protocol, ex);
}
finally
{
if (sendBuffer is not null)
ArrayPool.Shared.Return(sendBuffer);
}
}
private async Task AcceptConnectionAsync(Socket tcpListener, DnsTransportProtocol protocol)
{
IPEndPoint localEP = tcpListener.LocalEndPoint as IPEndPoint;
try
{
tcpListener.SendTimeout = _tcpSendTimeout;
tcpListener.ReceiveTimeout = _tcpReceiveTimeout;
tcpListener.NoDelay = true;
while (true)
{
Socket socket = await tcpListener.AcceptAsync();
_ = ProcessConnectionAsync(socket, protocol);
}
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.OperationAborted)
return; //server stopping
_log?.Write(localEP, protocol, ex);
}
catch (ObjectDisposedException)
{
//server stopped
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
_log?.Write(localEP, protocol, ex);
}
}
private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol)
{
IPEndPoint remoteEP = null;
try
{
remoteEP = socket.RemoteEndPoint as IPEndPoint;
switch (protocol)
{
case DnsTransportProtocol.Tcp:
await ReadStreamRequestAsync(new NetworkStream(socket), remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol);
break;
case DnsTransportProtocol.Tls:
SslStream tlsStream = new SslStream(new NetworkStream(socket));
string serverName = null;
await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return tlsStream.AuthenticateAsServerAsync(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
{
serverName = clientHelloInfo.ServerName;
return ValueTask.FromResult(_dotSslServerAuthenticationOptions);
}, null, cancellationToken1);
}, _tcpReceiveTimeout);
NameServerAddress dnsEP;
if (string.IsNullOrEmpty(serverName))
dnsEP = new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tls);
else
dnsEP = new NameServerAddress(serverName, socket.LocalEndPoint as IPEndPoint, DnsTransportProtocol.Tls);
await ReadStreamRequestAsync(tlsStream, remoteEP, dnsEP, protocol);
break;
case DnsTransportProtocol.TcpProxy:
if (!NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL))
{
//this feature is intended to be used with a reverse proxy or load balancer on private network
return;
}
ProxyProtocolStream proxyStream = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return ProxyProtocolStream.CreateAsServerAsync(new NetworkStream(socket), cancellationToken1);
}, _tcpReceiveTimeout);
remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort);
await ReadStreamRequestAsync(proxyStream, remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol);
break;
default:
throw new InvalidOperationException();
}
}
catch (AuthenticationException)
{
//ignore TLS auth exception
}
catch (TimeoutException)
{
//ignore timeout exception on TLS auth
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
finally
{
socket.Dispose();
}
}
private async Task ReadStreamRequestAsync(Stream stream, IPEndPoint remoteEP, NameServerAddress dnsEP, DnsTransportProtocol protocol)
{
try
{
using MemoryStream readBuffer = new MemoryStream(64);
using MemoryStream writeBuffer = new MemoryStream(2048);
using SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1);
while (true)
{
if (IsQpmLimitCrossed(remoteEP.Address))
{
_stats.QueueUpdate(null, remoteEP, protocol, null, true);
break;
}
DnsDatagram request;
//read dns datagram with timeout
using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())
{
Task task = DnsDatagram.ReadFromTcpAsync(stream, readBuffer, cancellationTokenSource.Token);
if (await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task)
{
//read timed out
await stream.DisposeAsync();
return;
}
cancellationTokenSource.Cancel(); //cancel delay task
request = await task;
request.SetMetadata(dnsEP);
}
//process request async
_ = ProcessStreamRequestAsync(stream, writeBuffer, writeSemaphore, remoteEP, request, protocol);
}
}
catch (ObjectDisposedException)
{
//ignore
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
private async Task ProcessStreamRequestAsync(Stream stream, MemoryStream writeBuffer, SemaphoreSlim writeSemaphore, IPEndPoint remoteEP, DnsDatagram request, DnsTransportProtocol protocol)
{
try
{
DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address));
if (response is null)
{
await stream.DisposeAsync();
_stats.QueueUpdate(null, remoteEP, protocol, null, false);
return; //drop request
}
//send response
await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)
{
await writeSemaphore.WaitAsync(cancellationToken1);
try
{
//send dns datagram
await response.WriteToTcpAsync(stream, writeBuffer, cancellationToken1);
await stream.FlushAsync(cancellationToken1);
}
finally
{
writeSemaphore.Release();
}
}, _tcpSendTimeout);
_queryLog?.Write(remoteEP, protocol, request, response);
_stats.QueueUpdate(request, remoteEP, protocol, response, false);
}
catch (ObjectDisposedException)
{
//ignore
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
if (request is not null)
_queryLog.Write(remoteEP, protocol, request, null);
_log?.Write(remoteEP, protocol, ex);
}
}
private async Task AcceptQuicConnectionAsync(QuicListener quicListener)
{
try
{
while (true)
{
try
{
QuicConnection quicConnection = await quicListener.AcceptConnectionAsync();
_ = ProcessQuicConnectionAsync(quicConnection);
}
catch (QuicException ex)
{
if (ex.InnerException is OperationCanceledException)
continue;
throw;
}
}
}
catch (ObjectDisposedException)
{
//server stopped
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
_log?.Write(quicListener.LocalEndPoint, DnsTransportProtocol.Quic, ex);
}
}
private async Task ProcessQuicConnectionAsync(QuicConnection quicConnection)
{
try
{
NameServerAddress dnsEP;
if (string.IsNullOrEmpty(quicConnection.TargetHostName))
dnsEP = new NameServerAddress(quicConnection.LocalEndPoint, DnsTransportProtocol.Quic);
else
dnsEP = new NameServerAddress(quicConnection.TargetHostName, quicConnection.LocalEndPoint, DnsTransportProtocol.Quic);
while (true)
{
if (IsQpmLimitCrossed(quicConnection.RemoteEndPoint.Address))
{
_stats.QueueUpdate(null, quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, null, true);
break;
}
QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync();
_ = ProcessQuicStreamRequestAsync(quicStream, quicConnection.RemoteEndPoint, dnsEP);
}
}
catch (QuicException ex)
{
switch (ex.QuicError)
{
case QuicError.ConnectionIdle:
case QuicError.ConnectionAborted:
case QuicError.ConnectionTimeout:
break;
default:
_log?.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex);
break;
}
}
catch (Exception ex)
{
_log?.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex);
}
finally
{
await quicConnection.DisposeAsync();
}
}
private async Task ProcessQuicStreamRequestAsync(QuicStream quicStream, IPEndPoint remoteEP, NameServerAddress dnsEP)
{
MemoryStream sharedBuffer = new MemoryStream(512);
DnsDatagram request = null;
try
{
//read dns datagram with timeout
using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())
{
Task task = DnsDatagram.ReadFromTcpAsync(quicStream, sharedBuffer, cancellationTokenSource.Token);
if (await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task)
{
//read timed out
quicStream.Abort(QuicAbortDirection.Both, (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR);
return;
}
cancellationTokenSource.Cancel(); //cancel delay task
request = await task;
request.SetMetadata(dnsEP);
}
//process request async
DnsDatagram response = await ProcessRequestAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address));
if (response is null)
{
_stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Quic, null, false);
return; //drop request
}
//send response
await response.WriteToTcpAsync(quicStream, sharedBuffer);
_queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, response);
_stats.QueueUpdate(request, remoteEP, DnsTransportProtocol.Quic, response, false);
}
catch (IOException)
{
//ignore QuicException / IOException
}
catch (Exception ex)
{
if (request is not null)
_queryLog.Write(remoteEP, DnsTransportProtocol.Quic, request, null);
_log?.Write(remoteEP, DnsTransportProtocol.Quic, ex);
}
finally
{
await sharedBuffer.DisposeAsync();
await quicStream.DisposeAsync();
}
}
private async Task ProcessDoHRequestAsync(HttpContext context)
{
IPEndPoint remoteEP = context.GetRemoteEndPoint(); //get the socket connection remote EP
DnsDatagram dnsRequest = null;
try
{
HttpRequest request = context.Request;
HttpResponse response = context.Response;
if (NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL))
{
//try to get client's actual IP from X-Real-IP header, if any
if (!string.IsNullOrEmpty(_dnsOverHttpRealIpHeader))
{
string xRealIp = context.Request.Headers[_dnsOverHttpRealIpHeader];
if (IPAddress.TryParse(xRealIp, out IPAddress address))
remoteEP = new IPEndPoint(address, 0);
}
}
else
{
if (!request.IsHttps)
{
//DNS-over-HTTP insecure protocol is intended to be used with an SSL terminated reverse proxy like nginx on private network
response.StatusCode = 403;
await response.WriteAsync("DNS-over-HTTPS (DoH) queries are supported only on HTTPS.");
return;
}
}
if (IsQpmLimitCrossed(remoteEP.Address))
{
_stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, true);
response.StatusCode = 429;
await response.WriteAsync("Too Many Requests");
return;
}
switch (request.Method)
{
case "GET":
bool acceptsDoH = false;
string requestAccept = request.Headers.Accept;
if (string.IsNullOrEmpty(requestAccept))
{
acceptsDoH = true;
}
else
{
foreach (string mediaType in requestAccept.Split(','))
{
if (mediaType.Equals("application/dns-message", StringComparison.OrdinalIgnoreCase))
{
acceptsDoH = true;
break;
}
}
}
if (!acceptsDoH)
{
response.Redirect((request.IsHttps ? "https://" : "http://") + request.Headers.Host);
return;
}
string dnsRequestBase64Url = request.Query["dns"];
if (string.IsNullOrEmpty(dnsRequestBase64Url))
{
response.StatusCode = 400;
await response.WriteAsync("Bad Request");
return;
}
//convert from base64url to base64
dnsRequestBase64Url = dnsRequestBase64Url.Replace('-', '+');
dnsRequestBase64Url = dnsRequestBase64Url.Replace('_', '/');
//add padding
int x = dnsRequestBase64Url.Length % 4;
if (x > 0)
dnsRequestBase64Url = dnsRequestBase64Url.PadRight(dnsRequestBase64Url.Length - x + 4, '=');
using (MemoryStream mS = new MemoryStream(Convert.FromBase64String(dnsRequestBase64Url)))
{
dnsRequest = DnsDatagram.ReadFrom(mS);
dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress()));
}
break;
case "POST":
if (!string.Equals(request.Headers.ContentType, "application/dns-message", StringComparison.OrdinalIgnoreCase))
{
response.StatusCode = 415;
await response.WriteAsync("Unsupported Media Type");
return;
}
using (MemoryStream mS = new MemoryStream(32))
{
await request.Body.CopyToAsync(mS, 32);
mS.Position = 0;
dnsRequest = DnsDatagram.ReadFrom(mS);
dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress()));
}
break;
default:
throw new InvalidOperationException();
}
DnsDatagram dnsResponse = await ProcessRequestAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address));
if (dnsResponse is null)
{
//drop request
context.Connection.RequestClose();
_stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, false);
return;
}
using (MemoryStream mS = new MemoryStream(512))
{
dnsResponse.WriteTo(mS);
mS.Position = 0;
response.ContentType = "application/dns-message";
response.ContentLength = mS.Length;
await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)
{
await using (Stream s = response.Body)
{
await mS.CopyToAsync(s, 512, cancellationToken1);
}
}, _tcpSendTimeout);
}
_queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, dnsResponse);
_stats.QueueUpdate(dnsRequest, remoteEP, DnsTransportProtocol.Https, dnsResponse, false);
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
if (dnsRequest is not null)
_queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, null);
_log?.Write(remoteEP, DnsTransportProtocol.Https, ex);
}
}
private bool IsRecursionAllowed(IPAddress remoteIP)
{
switch (_recursion)
{
case DnsServerRecursion.Allow:
return true;
case DnsServerRecursion.AllowOnlyForPrivateNetworks:
switch (remoteIP.AddressFamily)
{
case AddressFamily.InterNetwork:
case AddressFamily.InterNetworkV6:
return NetUtilities.IsPrivateIP(remoteIP);
default:
return false;
}
case DnsServerRecursion.UseSpecifiedNetworkACL:
return NetworkAccessControl.IsAddressAllowed(remoteIP, _recursionNetworkACL, true);
default:
return false;
}
}
private async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)
{
foreach (IDnsRequestController requestController in _dnsApplicationManager.DnsRequestControllers)
{
try
{
DnsRequestControllerAction action = await requestController.GetRequestActionAsync(request, remoteEP, protocol);
switch (action)
{
case DnsRequestControllerAction.DropSilently:
return null; //drop request
case DnsRequestControllerAction.DropWithRefused:
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; //drop request with refused
}
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
if (request.ParsingException is not null)
{
//format error
if (request.ParsingException is not IOException)
_log?.Write(remoteEP, protocol, request.ParsingException);
//format error response
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative };
}
if (request.IsSigned)
{
if (!request.VerifySignedRequest(_tsigKeys, out DnsDatagram unsignedRequest, out DnsDatagram errorResponse))
{
_log?.Write(remoteEP, protocol, "DNS Server received a request that failed TSIG signature verification (RCODE: " + errorResponse.RCODE + "; TSIG Error: " + errorResponse.TsigError + ")");
errorResponse.Tag = DnsServerResponseType.Authoritative;
return errorResponse;
}
DnsDatagram unsignedResponse = await ProcessQueryAsync(unsignedRequest, remoteEP, protocol, isRecursionAllowed, false, request.TsigKeyName);
if (unsignedResponse is null)
return null;
unsignedResponse = await PostProcessQueryAsync(request, remoteEP, protocol, unsignedResponse);
if (unsignedResponse is null)
return null;
return unsignedResponse.SignResponse(request, _tsigKeys);
}
if (request.EDNS is not null)
{
if (request.EDNS.Version != 0)
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.BADVERS, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative };
}
DnsDatagram response = await ProcessQueryAsync(request, remoteEP, protocol, isRecursionAllowed, false, null);
if (response is null)
return null;
return await PostProcessQueryAsync(request, remoteEP, protocol, response);
}
private async Task PostProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
{
foreach (IDnsPostProcessor postProcessor in _dnsApplicationManager.DnsPostProcessors)
{
try
{
response = await postProcessor.PostProcessAsync(request, remoteEP, protocol, response);
if (response is null)
return null;
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
if (request.EDNS is null)
{
if (response.EDNS is not null)
response = response.CloneWithoutEDns();
return response;
}
if (response.EDNS is not null)
return response;
IReadOnlyList options = null;
EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true);
if (requestECS is not null)
options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address);
if (response.Additional.Count == 0)
return response.Clone(null, null, new DnsResourceRecord[] { DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options) });
if (response.IsSigned)
return response;
DnsResourceRecord[] newAdditional = new DnsResourceRecord[response.Additional.Count + 1];
for (int i = 0; i < response.Additional.Count; i++)
newAdditional[i] = response.Additional[i];
newAdditional[response.Additional.Count] = DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
return response.Clone(null, null, newAdditional);
}
private async Task ProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, string tsigAuthenticatedKeyName)
{
if (request.IsResponse)
return null; //drop response datagram to avoid loops in rare scenarios
switch (request.OPCODE)
{
case DnsOpcode.StandardQuery:
if (request.Question.Count != 1)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
if (request.Question[0].Class != DnsClass.IN)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
try
{
DnsQuestionRecord question = request.Question[0];
switch (question.Type)
{
case DnsResourceRecordType.AXFR:
if (protocol == DnsTransportProtocol.Udp)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName);
case DnsResourceRecordType.IXFR:
return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName);
case DnsResourceRecordType.FWD:
case DnsResourceRecordType.APP:
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
//query authoritative zone
DnsDatagram response = await ProcessAuthoritativeQueryAsync(request, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (response is not null)
{
if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, response.RCODE, request.Question) { Tag = DnsServerResponseType.Authoritative };
return response;
}
if (!request.RecursionDesired || !isRecursionAllowed)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
//do recursive query
if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };
return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers);
}
catch (InvalidDomainNameException)
{
//format error response
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
catch (TimeoutException ex)
{
DnsDatagram response = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative };
_log?.Write(remoteEP, protocol, request, response);
_log?.Write(remoteEP, protocol, ex);
return response;
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
case DnsOpcode.Notify:
return await ProcessNotifyQueryAsync(request, remoteEP, protocol);
case DnsOpcode.Update:
return await ProcessUpdateQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName);
default:
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NotImplemented, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
private async Task ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)
{
AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name);
if ((zoneInfo is null) || ((zoneInfo.Type != AuthZoneType.Secondary) && (zoneInfo.Type != AuthZoneType.SecondaryForwarder) && (zoneInfo.Type != AuthZoneType.SecondaryCatalog)) || zoneInfo.Disabled)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
async Task RemoteVerifiedAsync(IPAddress remoteAddress)
{
if (_notifyAllowedNetworks is not null)
{
foreach (NetworkAddress notifyAllowedNetwork in _notifyAllowedNetworks)
{
if (notifyAllowedNetwork.Contains(remoteAddress))
return true;
}
}
IReadOnlyList primaryNameServerAddresses;
SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone;
if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers)
primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses);
else
primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync();
foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses)
{
if (primaryNameServer.IPEndPoint.Address.Equals(remoteAddress))
return true;
}
return false;
}
if (!await RemoteVerifiedAsync(remoteEP.Address))
{
_log?.Write(remoteEP, protocol, "DNS Server refused a NOTIFY request since the request IP address was not recognized by the secondary zone: " + zoneInfo.DisplayName);
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
_log?.Write(remoteEP, protocol, "DNS Server received a NOTIFY request for secondary zone: " + zoneInfo.DisplayName);
if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA))
{
IReadOnlyList localSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);
if (!DnsSOARecordData.IsZoneUpdateAvailable((localSoaRecords[0].RDATA as DnsSOARecordData).Serial, (request.Answer[0].RDATA as DnsSOARecordData).Serial))
{
//no update was available
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
zoneInfo.TriggerRefresh();
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
private async Task ProcessUpdateQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName)
{
if ((request.Question.Count != 1) || (request.Question[0].Type != DnsResourceRecordType.SOA))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
if (request.Question[0].Class != DnsClass.IN)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative };
AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name);
if ((zoneInfo is null) || zoneInfo.Disabled)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative };
_log?.Write(remoteEP, protocol, "DNS Server received a zone UPDATE request for zone: " + zoneInfo.DisplayName);
async Task IsZoneNameServerAllowedAsync()
{
IPAddress remoteAddress = remoteEP.Address;
IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync();
foreach (NameServerAddress secondaryNameServer in secondaryNameServers)
{
if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))
return true;
}
return false;
}
async Task IsUpdatePermittedAsync()
{
bool isUpdateAllowed;
switch (zoneInfo.Update)
{
case AuthZoneUpdate.Allow:
isUpdateAllowed = true;
break;
case AuthZoneUpdate.AllowOnlyZoneNameServers:
isUpdateAllowed = await IsZoneNameServerAllowedAsync();
break;
case AuthZoneUpdate.UseSpecifiedNetworkACL:
isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL);
break;
case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL:
isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL) || await IsZoneNameServerAllowedAsync();
break;
case AuthZoneUpdate.Deny:
default:
isUpdateAllowed = false;
break;
}
if (!isUpdateAllowed)
{
_log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request IP address is not allowed by the zone: " + zoneInfo.DisplayName);
return false;
}
//check security policies
if ((zoneInfo.UpdateSecurityPolicies is not null) && (zoneInfo.UpdateSecurityPolicies.Count > 0))
{
if ((tsigAuthenticatedKeyName is null) || !zoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLowerInvariant(), out IReadOnlyDictionary> policyMap))
{
_log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request is missing TSIG auth required by the zone: " + zoneInfo.DisplayName);
return false;
}
//check policy
foreach (DnsResourceRecord uRecord in request.Authority)
{
bool isPermitted = false;
foreach (KeyValuePair> policy in policyMap)
{
if (
uRecord.Name.Equals(policy.Key, StringComparison.OrdinalIgnoreCase) ||
(policy.Key.StartsWith("*.") && uRecord.Name.EndsWith(policy.Key.Substring(1), StringComparison.OrdinalIgnoreCase))
)
{
foreach (DnsResourceRecordType allowedType in policy.Value)
{
if ((allowedType == DnsResourceRecordType.ANY) || (allowedType == uRecord.Type))
{
isPermitted = true;
break;
}
}
if (isPermitted)
break;
}
}
if (!isPermitted)
{
_log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request [" + uRecord.Name.ToLowerInvariant() + " " + uRecord.Type.ToString() + " " + uRecord.Class.ToString() + "] due to Dynamic Updates Security Policy for zone: " + zoneInfo.DisplayName);
return false;
}
}
}
return true;
}
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
case AuthZoneType.Forwarder:
//update
{
//process prerequisite section
{
Dictionary>> temp = new Dictionary>>();
foreach (DnsResourceRecord prRecord in request.Answer)
{
if (prRecord.TTL != 0)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(prRecord.Name);
if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative };
if (prRecord.Class == DnsClass.ANY)
{
if (prRecord.RDATA.RDLENGTH != 0)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
if (prRecord.Type == DnsResourceRecordType.ANY)
{
//check if name is in use
if (!_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
else
{
//check if RRSet exists (value independent)
IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type);
if (rrset.Count == 0)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
else if (prRecord.Class == DnsClass.NONE)
{
if (prRecord.RDATA.RDLENGTH != 0)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
if (prRecord.Type == DnsResourceRecordType.ANY)
{
//check if name is not in use
if (_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXDomain, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
else
{
//check if RRSet does not exists
IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type);
if (rrset.Count > 0)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
else if (prRecord.Class == request.Question[0].Class)
{
//check if RRSet exists (value dependent)
//add to temp for later comparison
string recordName = prRecord.Name.ToLowerInvariant();
if (!temp.TryGetValue(recordName, out Dictionary> rrsetEntry))
{
rrsetEntry = new Dictionary>();
temp.Add(recordName, rrsetEntry);
}
if (!rrsetEntry.TryGetValue(prRecord.Type, out List rrset))
{
rrset = new List();
rrsetEntry.Add(prRecord.Type, rrset);
}
rrset.Add(prRecord);
}
else
{
//FORMERR
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
//compare collected RRSets in temp
foreach (KeyValuePair>> zoneEntry in temp)
{
foreach (KeyValuePair> rrsetEntry in zoneEntry.Value)
{
List prRRSet = rrsetEntry.Value;
IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, zoneEntry.Key, rrsetEntry.Key);
//check if RRSet exists (value dependent)
//compare RRSets
if (prRRSet.Count != rrset.Count)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };
foreach (DnsResourceRecord prRecord in prRRSet)
{
bool found = false;
foreach (DnsResourceRecord record in rrset)
{
if (
prRecord.Name.Equals(record.Name, StringComparison.OrdinalIgnoreCase) &&
(prRecord.Class == record.Class) &&
(prRecord.Type == record.Type) &&
(prRecord.RDATA.RDLENGTH == record.RDATA.RDLENGTH) &&
prRecord.RDATA.Equals(record.RDATA)
)
{
found = true;
break;
}
}
if (!found)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
}
}
//check for permissions
if (!await IsUpdatePermittedAsync())
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
//process update section
{
//prescan
foreach (DnsResourceRecord uRecord in request.Authority)
{
AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(uRecord.Name);
if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative };
if (uRecord.Class == request.Question[0].Class)
{
switch (uRecord.Type)
{
case DnsResourceRecordType.ANY:
case DnsResourceRecordType.AXFR:
case DnsResourceRecordType.MAILA:
case DnsResourceRecordType.MAILB:
case DnsResourceRecordType.IXFR:
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
else if (uRecord.Class == DnsClass.ANY)
{
if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH != 0))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
switch (uRecord.Type)
{
case DnsResourceRecordType.AXFR:
case DnsResourceRecordType.MAILA:
case DnsResourceRecordType.MAILB:
case DnsResourceRecordType.IXFR:
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
else if (uRecord.Class == DnsClass.NONE)
{
if (uRecord.TTL != 0)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
switch (uRecord.Type)
{
case DnsResourceRecordType.ANY:
case DnsResourceRecordType.AXFR:
case DnsResourceRecordType.MAILA:
case DnsResourceRecordType.MAILB:
case DnsResourceRecordType.IXFR:
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
else
{
//FORMERR
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
//update
Dictionary>> originalRRSets = new Dictionary>>();
void AddToOriginalRRSets(string domain, DnsResourceRecordType type, IReadOnlyList existingRRSet)
{
if (!originalRRSets.TryGetValue(domain, out Dictionary> originalRRSetEntries))
{
originalRRSetEntries = new Dictionary>();
originalRRSets.Add(domain, originalRRSetEntries);
}
originalRRSetEntries.TryAdd(type, existingRRSet);
}
try
{
foreach (DnsResourceRecord uRecord in request.Authority)
{
if (uRecord.Class == request.Question[0].Class)
{
//Add to an RRset
if (uRecord.Type == DnsResourceRecordType.CNAME)
{
if (_authZoneManager.NameExists(zoneInfo.Name, uRecord.Name) && (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count == 0))
continue; //current name exists and has non-CNAME records so cannot add CNAME record
IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);
_authZoneManager.SetRecord(zoneInfo.Name, uRecord);
}
else if (uRecord.Type == DnsResourceRecordType.DNAME)
{
IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);
_authZoneManager.SetRecord(zoneInfo.Name, uRecord);
}
else if (uRecord.Type == DnsResourceRecordType.SOA)
{
if (!uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))
continue; //can add SOA only to apex
IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);
_authZoneManager.SetRecord(zoneInfo.Name, uRecord);
}
else
{
if (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count > 0)
continue; //current name contains CNAME so cannot add non-CNAME record
IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);
if (uRecord.Type == DnsResourceRecordType.NS)
uRecord.SyncGlueRecords(request.Additional);
_authZoneManager.AddRecord(zoneInfo.Name, uRecord);
}
}
else if (uRecord.Class == DnsClass.ANY)
{
if (uRecord.Type == DnsResourceRecordType.ANY)
{
//Delete all RRsets from a name
IReadOnlyDictionary> existingRRSets = _authZoneManager.GetEntriesFor(zoneInfo.Name, uRecord.Name);
if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))
{
foreach (KeyValuePair> existingRRSet in existingRRSets)
{
switch (existingRRSet.Key)
{
case DnsResourceRecordType.SOA:
case DnsResourceRecordType.NS:
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3PARAM:
case DnsResourceRecordType.NSEC3:
continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets
}
AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value);
_authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key);
}
}
else
{
foreach (KeyValuePair> existingRRSet in existingRRSets)
{
switch (existingRRSet.Key)
{
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3PARAM:
case DnsResourceRecordType.NSEC3:
continue; //skip DNSSEC rrsets
}
AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value);
_authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key);
}
}
}
else
{
//Delete an RRset
if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))
{
switch (uRecord.Type)
{
case DnsResourceRecordType.SOA:
case DnsResourceRecordType.NS:
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3PARAM:
case DnsResourceRecordType.NSEC3:
continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets
}
}
IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);
_authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
}
}
else if (uRecord.Class == DnsClass.NONE)
{
//Delete an RR from an RRset
switch (uRecord.Type)
{
case DnsResourceRecordType.SOA:
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3PARAM:
case DnsResourceRecordType.NSEC3:
continue; //no SOA can be deleted; skip DNSSEC rrsets
}
IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);
if ((uRecord.Type == DnsResourceRecordType.NS) && (existingRRSet.Count == 1) && uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))
continue; //no apex NS can be deleted if only 1 NS exists
AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);
_authZoneManager.DeleteRecord(zoneInfo.Name, uRecord.Name, uRecord.Type, uRecord.RDATA);
}
}
}
catch
{
//revert
foreach (KeyValuePair>> originalRRSetEntries in originalRRSets)
{
foreach (KeyValuePair> originalRRSet in originalRRSetEntries.Value)
{
if (originalRRSet.Value.Count == 0)
_authZoneManager.DeleteRecords(zoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key);
else
_authZoneManager.SetRecords(zoneInfo.Name, originalRRSet.Value);
}
}
throw;
}
}
_authZoneManager.SaveZoneFile(zoneInfo.Name);
_log?.Write(remoteEP, protocol, "DNS Server successfully processed a zone UPDATE request for zone: " + zoneInfo.DisplayName);
//NOERROR
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
case AuthZoneType.Secondary:
case AuthZoneType.SecondaryForwarder:
//forward
{
//check for permissions
if (!await IsUpdatePermittedAsync())
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
//forward to primary
IReadOnlyList primaryNameServerAddresses;
DnsTransportProtocol primaryZoneTransferProtocol;
SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone;
if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers)
{
primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses);
primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol;
}
else
{
primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync();
primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol;
}
switch (primaryZoneTransferProtocol)
{
case DnsTransportProtocol.Tls:
case DnsTransportProtocol.Quic:
{
//change name server protocol to TLS/QUIC
List updatedNameServers = new List(primaryNameServerAddresses.Count);
foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses)
{
if (primaryNameServer.Protocol == primaryZoneTransferProtocol)
updatedNameServers.Add(primaryNameServer);
else
updatedNameServers.Add(primaryNameServer.ChangeProtocol(primaryZoneTransferProtocol));
}
primaryNameServerAddresses = updatedNameServers;
}
break;
default:
if (protocol == DnsTransportProtocol.Tcp)
{
//change name server protocol to TCP
List updatedNameServers = new List(primaryNameServerAddresses.Count);
foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses)
{
if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp)
updatedNameServers.Add(primaryNameServer);
else
updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp));
}
primaryNameServerAddresses = updatedNameServers;
}
break;
}
TsigKey key = null;
if (!string.IsNullOrEmpty(tsigAuthenticatedKeyName) && ((_tsigKeys is null) || !_tsigKeys.TryGetValue(tsigAuthenticatedKeyName, out key)))
throw new DnsServerException("DNS Server does not have TSIG key '" + tsigAuthenticatedKeyName + "' configured to authenticate dynamic updates for " + zoneInfo.TypeName + " zone: " + zoneInfo.DisplayName);
DnsClient dnsClient = new DnsClient(primaryNameServerAddresses);
dnsClient.Proxy = _proxy;
dnsClient.PreferIPv6 = _preferIPv6;
dnsClient.Retries = _forwarderRetries;
dnsClient.Timeout = _forwarderTimeout;
dnsClient.Concurrency = 1;
DnsDatagram newRequest = request.Clone();
newRequest.SetRandomIdentifier();
DnsDatagram newResponse;
if (key is null)
newResponse = await dnsClient.RawResolveAsync(newRequest);
else
newResponse = await dnsClient.TsigResolveAsync(newRequest, key);
newResponse.SetIdentifier(request.Identifier);
return newResponse;
}
default:
return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
private async Task ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName)
{
AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name);
if ((zoneInfo is null) || !zoneInfo.ApexZone.IsActive)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
case AuthZoneType.Secondary:
case AuthZoneType.Forwarder:
case AuthZoneType.Catalog:
break;
default:
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
async Task IsZoneNameServerAllowedAsync(ApexZone apexZone)
{
IPAddress remoteAddress = remoteEP.Address;
IReadOnlyList secondaryNameServers = await apexZone.GetResolvedSecondaryNameServerAddressesAsync();
foreach (NameServerAddress secondaryNameServer in secondaryNameServers)
{
if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))
return true;
}
return false;
}
async Task IsZoneTransferAllowed(ApexZone apexZone)
{
switch (apexZone.ZoneTransfer)
{
case AuthZoneTransfer.Allow:
return true;
case AuthZoneTransfer.AllowOnlyZoneNameServers:
return await IsZoneNameServerAllowedAsync(apexZone);
case AuthZoneTransfer.UseSpecifiedNetworkACL:
return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL);
case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL:
return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL) || await IsZoneNameServerAllowedAsync(apexZone);
case AuthZoneTransfer.Deny:
default:
return false;
}
}
bool IsTsigAuthenticated(ApexZone apexZone)
{
if ((apexZone.ZoneTransferTsigKeyNames is null) || (apexZone.ZoneTransferTsigKeyNames.Count < 1))
return true; //no auth needed
if ((tsigAuthenticatedKeyName is not null) && apexZone.ZoneTransferTsigKeyNames.ContainsKey(tsigAuthenticatedKeyName.ToLowerInvariant()))
return true; //key matches
return false;
}
bool isInZoneTransferAllowedList = false;
if (_zoneTransferAllowedNetworks is not null)
{
IPAddress remoteAddress = remoteEP.Address;
foreach (NetworkAddress networkAddress in _zoneTransferAllowedNetworks)
{
if (networkAddress.Contains(remoteAddress))
{
isInZoneTransferAllowedList = true;
break;
}
}
}
if (!isInZoneTransferAllowedList)
{
ApexZone apexZone = zoneInfo.ApexZone;
CatalogZone catalogZone = apexZone.CatalogZone;
if (catalogZone is not null)
{
if (!apexZone.OverrideCatalogZoneTransfer)
apexZone = catalogZone; //use catalog zone transfer options
}
else
{
SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone;
if (secondaryCatalogZone is not null)
{
if (!apexZone.OverrideCatalogZoneTransfer)
apexZone = secondaryCatalogZone; //use secondary zone transfer options
}
}
if (!await IsZoneTransferAllowed(apexZone))
{
_log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + zoneInfo.DisplayName);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
if (!IsTsigAuthenticated(apexZone))
{
_log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + zoneInfo.DisplayName);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
}
_log?.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + zoneInfo.DisplayName);
IReadOnlyList xfrRecords;
if (request.Question[0].Type == DnsResourceRecordType.IXFR)
{
if ((request.Authority.Count == 1) && (request.Authority[0].Type == DnsResourceRecordType.SOA))
xfrRecords = _authZoneManager.QueryIncrementalZoneTransferRecords(request.Question[0].Name, request.Authority[0]);
else
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
else
{
xfrRecords = _authZoneManager.QueryZoneTransferRecords(request.Question[0].Name);
}
DnsDatagram xfrResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, xfrRecords) { Tag = DnsServerResponseType.Authoritative };
xfrResponse = xfrResponse.Split();
//update notify failed list
NameServerAddress allowedZoneNameServer = null;
switch (zoneInfo.Notify)
{
case AuthZoneNotify.ZoneNameServers:
case AuthZoneNotify.BothZoneAndSpecifiedNameServers:
IPAddress remoteAddress = remoteEP.Address;
IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync();
foreach (NameServerAddress secondaryNameServer in secondaryNameServers)
{
if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))
{
allowedZoneNameServer = secondaryNameServer;
break;
}
}
break;
}
zoneInfo.ApexZone.RemoveFromNotifyFailedList(allowedZoneNameServer, remoteEP.Address);
return xfrResponse;
}
private async Task ProcessAuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers)
{
DnsDatagram response = await AuthoritativeQueryAsync(request, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (response is null)
return null;
bool reprocessResponse; //to allow resolving CNAME/ANAME in response
do
{
reprocessResponse = false;
if (response.RCODE == DnsResponseCode.NoError)
{
if (response.Answer.Count > 0)
{
DnsResourceRecordType questionType = request.Question[0].Type;
DnsResourceRecord lastRR = response.GetLastAnswerRecord();
if ((lastRR.Type != questionType) && (questionType != DnsResourceRecordType.ANY))
{
switch (lastRR.Type)
{
case DnsResourceRecordType.CNAME:
return await ProcessCNAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, false, skipDnsAppAuthoritativeRequestHandlers);
case DnsResourceRecordType.ANAME:
case DnsResourceRecordType.ALIAS:
return await ProcessANAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
}
}
}
else if (response.Authority.Count > 0)
{
DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord();
switch (firstAuthority.Type)
{
case DnsResourceRecordType.NS:
if (request.RecursionDesired && isRecursionAllowed)
{
//do forced recursive resolution (with blocking support) using empty conditional forwarders; name servers will be provided via ResolverDnsCache
return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, [], _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers);
}
break;
case DnsResourceRecordType.FWD:
//do conditional forwarding (with blocking support)
return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers);
case DnsResourceRecordType.APP:
response = await ProcessAPPAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (response is null)
return null; //drop request
reprocessResponse = true;
break;
}
}
}
}
while (reprocessResponse);
return response;
}
private async Task AuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers)
{
DnsDatagram response = await _authZoneManager.QueryAsync(request, remoteEP.Address, isRecursionAllowed);
if (response is not null)
{
response.Tag = DnsServerResponseType.Authoritative;
return response;
}
if (!skipDnsAppAuthoritativeRequestHandlers)
{
foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers)
{
try
{
DnsDatagram appResponse = await requestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed);
if (appResponse is not null)
{
if (appResponse.Tag is null)
appResponse.Tag = DnsServerResponseType.Authoritative;
return appResponse;
}
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
}
return null;
}
private async Task ProcessAPPAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers)
{
DnsResourceRecord appResourceRecord = response.Authority[0];
DnsApplicationRecordData appRecord = appResourceRecord.RDATA as DnsApplicationRecordData;
if (_dnsApplicationManager.Applications.TryGetValue(appRecord.AppName, out DnsApplication application))
{
if (application.DnsAppRecordRequestHandlers.TryGetValue(appRecord.ClassPath, out IDnsAppRecordRequestHandler appRecordRequestHandler))
{
AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(appResourceRecord.Name);
DnsDatagram appResponse = await appRecordRequestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed, zoneInfo.Name, appResourceRecord.Name, appResourceRecord.TTL, appRecord.Data);
if (appResponse is null)
{
DnsResponseCode rcode;
IReadOnlyList authority = null;
if ((zoneInfo.Type == AuthZoneType.Forwarder) || (zoneInfo.Type == AuthZoneType.SecondaryForwarder))
{
//process FWD record if exists
if (!zoneInfo.Name.Equals(appResourceRecord.Name, StringComparison.OrdinalIgnoreCase))
{
AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.Name, appResourceRecord.Name);
if (authZone is not null)
authority = authZone.QueryRecords(DnsResourceRecordType.FWD, false);
}
if ((authority is null) || (authority.Count == 0))
authority = zoneInfo.ApexZone.QueryRecords(DnsResourceRecordType.FWD, false);
if (authority.Count > 0)
return await RecursiveResolveAsync(request, remoteEP, authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
rcode = DnsResponseCode.NoError;
}
else
{
//return NODATA/NXDOMAIN response
if ((request.Question[0].Name.Length == appResourceRecord.Name.Length) || appResourceRecord.Name.StartsWith('*'))
rcode = DnsResponseCode.NoError;
else
rcode = DnsResponseCode.NxDomain;
authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);
}
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative };
}
else
{
if (appResponse.AuthoritativeAnswer)
appResponse.Tag = DnsServerResponseType.Authoritative;
return appResponse; //return app response
}
}
else
{
_log?.Write(remoteEP, protocol, "DNS request handler '" + appRecord.ClassPath + "' was not found in the application '" + appRecord.AppName + "': " + appResourceRecord.Name);
}
}
else
{
_log?.Write(remoteEP, protocol, "DNS application '" + appRecord.AppName + "' was not found: " + appResourceRecord.Name);
}
//return server failure response with SOA
{
AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name);
IReadOnlyList authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative };
}
}
private async Task ProcessCNAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers)
{
List newAnswer = new List(response.Answer.Count + 4);
newAnswer.AddRange(response.Answer);
//copying NSEC/NSEC3 for for wildcard answers
List newAuthority = new List(2);
foreach (DnsResourceRecord record in response.Authority)
{
switch (record.Type)
{
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3:
newAuthority.Add(record);
break;
case DnsResourceRecordType.RRSIG:
switch ((record.RDATA as DnsRRSIGRecordData).TypeCovered)
{
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3:
newAuthority.Add(record);
break;
}
break;
}
}
DnsDatagram lastResponse = response;
bool isAuthoritativeAnswer = response.AuthoritativeAnswer;
DnsResourceRecord lastRR = response.GetLastAnswerRecord();
EDnsOption[] eDnsClientSubnetOption = null;
DnsDatagram newResponse = null;
double responseRtt = 0.0;
if (response.Metadata is not null)
responseRtt = response.Metadata.RoundTripTime;
if (_eDnsClientSubnet)
{
EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();
if (requestECS is not null)
eDnsClientSubnetOption = [new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS)];
}
int queryCount = 0;
do
{
string cnameDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain;
if (lastRR.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase))
break; //loop detected
DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(cnameDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption);
//query authoritative zone first
newResponse = await AuthoritativeQueryAsync(newRequest, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
{
//not found in auth zone
if (newRequest.RecursionDesired && isRecursionAllowed)
{
//do recursion
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
isAuthoritativeAnswer = false;
}
else
{
//break since no recursion allowed/desired
break;
}
}
else if ((newResponse.Answer.Count > 0) && (newResponse.GetLastAnswerRecord() is DnsResourceRecord lastAnswer) && ((lastAnswer.Type == DnsResourceRecordType.ANAME) || (lastAnswer.Type == DnsResourceRecordType.ALIAS)))
{
newResponse = await ProcessANAMEAsync(request, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
}
else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0))
{
//found delegated/forwarded zone
DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord();
switch (firstAuthority.Type)
{
case DnsResourceRecordType.NS:
if (newRequest.RecursionDesired && isRecursionAllowed)
{
//do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolveDnsCache
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
isAuthoritativeAnswer = false;
}
break;
case DnsResourceRecordType.FWD:
//do conditional forwarding
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
isAuthoritativeAnswer = false;
break;
case DnsResourceRecordType.APP:
newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
break;
}
}
if (newResponse.Metadata is not null)
responseRtt += newResponse.Metadata.RoundTripTime;
//check last response
if (newResponse.Answer.Count == 0)
break; //cannot proceed to resolve further
lastRR = newResponse.GetLastAnswerRecord();
if (lastRR.Type != DnsResourceRecordType.CNAME)
{
newAnswer.AddRange(newResponse.Answer);
break; //cname was resolved
}
bool foundRepeat = false;
foreach (DnsResourceRecord newResponseAnswerRecord in newResponse.Answer)
{
if ((newResponseAnswerRecord.Type == DnsResourceRecordType.CNAME) || (newResponseAnswerRecord.Type == DnsResourceRecordType.DNAME))
{
foreach (DnsResourceRecord answerRecord in newAnswer)
{
if (newResponseAnswerRecord.Equals(answerRecord))
{
foundRepeat = true;
break;
}
}
if (foundRepeat)
break;
}
newAnswer.Add(newResponseAnswerRecord);
}
if (foundRepeat)
break; //loop detected
lastResponse = newResponse;
}
while (++queryCount < MAX_CNAME_HOPS);
DnsResponseCode rcode;
IReadOnlyList authority;
IReadOnlyList additional;
if (newResponse is null)
{
//no recursion available
rcode = DnsResponseCode.NoError;
if (newAuthority.Count == 0)
{
authority = lastResponse.Authority;
}
else
{
newAuthority.AddRange(lastResponse.Authority);
authority = newAuthority;
}
additional = lastResponse.Additional;
}
else
{
rcode = newResponse.RCODE;
if (newAuthority.Count == 0)
{
authority = newResponse.Authority;
}
else
{
newAuthority.AddRange(newResponse.Authority);
authority = newAuthority;
}
additional = newResponse.Additional;
}
DnsDatagram finalResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, newAnswer, authority, additional) { Tag = response.Tag };
finalResponse.SetMetadata(null, responseRtt);
return finalResponse;
}
private async Task ProcessANAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers)
{
EDnsOption[] eDnsClientSubnetOption = null;
if (_eDnsClientSubnet)
{
EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();
if (requestECS is not null)
eDnsClientSubnetOption = [new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS)];
}
Queue>> resolveQueue = new Queue>>();
async Task> ResolveANAMEAsync(DnsResourceRecord anameRR, int queryCount = 0)
{
string lastDomain = (anameRR.RDATA as DnsANAMERecordData).Domain;
if (anameRR.Name.Equals(lastDomain, StringComparison.OrdinalIgnoreCase))
return null; //loop detected
do
{
DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(lastDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption);
//query authoritative zone first
DnsDatagram newResponse = await AuthoritativeQueryAsync(newRequest, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
{
//not found in auth zone; do recursion
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
}
else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0))
{
//found delegated/forwarded zone
DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord();
switch (firstAuthority.Type)
{
case DnsResourceRecordType.NS:
//do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
break;
case DnsResourceRecordType.FWD:
//do conditional forwarding
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
break;
case DnsResourceRecordType.APP:
newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
if (newResponse is null)
return null; //drop request
break;
}
}
//check new response
if (newResponse.RCODE != DnsResponseCode.NoError)
return null; //cannot proceed to resolve further
if (newResponse.Answer.Count == 0)
return Array.Empty(); //NO DATA
DnsResourceRecordType questionType = request.Question[0].Type;
DnsResourceRecord lastRR = newResponse.GetLastAnswerRecord();
if (lastRR.Type == questionType)
{
//found final answer
List answers = new List();
foreach (DnsResourceRecord answer in newResponse.Answer)
{
if (answer.Type != questionType)
continue;
if (anameRR.TTL < answer.TTL)
answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TTL, answer.RDATA));
else
answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TTL, answer.RDATA));
}
return answers;
}
switch (lastRR.Type)
{
case DnsResourceRecordType.ANAME:
case DnsResourceRecordType.ALIAS:
if (newResponse.Answer.Count == 1)
{
lastDomain = (lastRR.RDATA as DnsANAMERecordData).Domain;
}
else
{
//resolve multiple ANAME records async
queryCount++; //increment since one query was done already
foreach (DnsResourceRecord newAnswer in newResponse.Answer)
resolveQueue.Enqueue(ResolveANAMEAsync(newAnswer, queryCount));
return Array.Empty();
}
break;
case DnsResourceRecordType.CNAME:
lastDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain;
break;
default:
//aname/cname was resolved, but no answer found
return Array.Empty();
}
}
while (++queryCount < MAX_CNAME_HOPS);
//max hops limit crossed
return null;
}
List responseAnswer = new List();
foreach (DnsResourceRecord answer in response.Answer)
{
switch (answer.Type)
{
case DnsResourceRecordType.ANAME:
case DnsResourceRecordType.ALIAS:
resolveQueue.Enqueue(ResolveANAMEAsync(answer));
break;
default:
if (resolveQueue.Count == 0)
responseAnswer.Add(answer);
break;
}
}
bool foundErrors = false;
while (resolveQueue.Count > 0)
{
IReadOnlyList records = await resolveQueue.Dequeue();
if (records is null)
foundErrors = true;
else if (records.Count > 0)
responseAnswer.AddRange(records);
}
DnsResponseCode rcode = DnsResponseCode.NoError;
IReadOnlyList authority = null;
if (responseAnswer.Count == 0)
{
if (foundErrors)
{
rcode = DnsResponseCode.ServerFailure;
}
else
{
authority = response.Authority;
//update last used on
DateTime utcNow = DateTime.UtcNow;
foreach (DnsResourceRecord record in authority)
record.GetAuthGenericRecordInfo().LastUsedOn = utcNow;
}
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, responseAnswer, authority, null) { Tag = response.Tag };
}
private DnsDatagram ProcessBlocked(DnsDatagram request)
{
DnsDatagram response = _blockedZoneManager.Query(request);
if (response is null)
{
//domain not blocked in blocked zone
response = _blockListZoneManager.Query(request); //check in block list zone
if (response is null)
return null; //domain not blocked in block list zone
//domain is blocked in block list zone
response.Tag = DnsServerResponseType.Blocked;
return response;
}
else
{
//domain is blocked in blocked zone
DnsQuestionRecord question = request.Question[0];
string GetBlockedDomain()
{
DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord();
if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA))
return firstAuthority.Name;
else
return question.Name;
}
if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT))
{
//return meta data
string blockedDomain = GetBlockedDomain();
IReadOnlyList answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _blockingAnswerTtl, new DnsTXTRecordData("source=blocked-zone; domain=" + blockedDomain))];
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked };
}
else
{
string blockedDomain = null;
EDnsOption[] options = null;
if (_allowTxtBlockingReport && (request.EDNS is not null))
{
blockedDomain = GetBlockedDomain();
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=blocked-zone; domain=" + blockedDomain))];
}
IReadOnlyCollection aRecords;
IReadOnlyCollection aaaaRecords;
switch (_blockingType)
{
case DnsServerBlockingType.AnyAddress:
aRecords = _aRecords;
aaaaRecords = _aaaaRecords;
break;
case DnsServerBlockingType.CustomAddress:
aRecords = _customBlockingARecords;
aaaaRecords = _customBlockingAAAARecords;
break;
case DnsServerBlockingType.NxDomain:
if (blockedDomain is null)
blockedDomain = GetBlockedDomain();
string parentDomain = AuthZoneManager.GetParentZone(blockedDomain);
if (parentDomain is null)
parentDomain = string.Empty;
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _blockedZoneManager.DnsSOARecord)], null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked };
default:
throw new InvalidOperationException();
}
IReadOnlyList answer;
IReadOnlyList authority = null;
switch (question.Type)
{
case DnsResourceRecordType.A:
{
if (aRecords.Count > 0)
{
DnsResourceRecord[] rrList = new DnsResourceRecord[aRecords.Count];
int i = 0;
foreach (DnsARecordData record in aRecords)
rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _blockingAnswerTtl, record);
answer = rrList;
}
else
{
answer = null;
authority = response.Authority;
}
}
break;
case DnsResourceRecordType.AAAA:
{
if (aaaaRecords.Count > 0)
{
DnsResourceRecord[] rrList = new DnsResourceRecord[aaaaRecords.Count];
int i = 0;
foreach (DnsAAAARecordData record in aaaaRecords)
rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _blockingAnswerTtl, record);
answer = rrList;
}
else
{
answer = null;
authority = response.Authority;
}
}
break;
default:
answer = response.Answer;
authority = response.Authority;
break;
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked };
}
}
}
private async Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)
{
if (request.Question.Count > 0)
{
DnsQuestionRecord question = request.Question[0];
if (question.Type == DnsResourceRecordType.DS)
{
//DS is at parent zone which causes IsAllowed() to return null; change QTYPE to A to fix this issue that causes allowed domains to fail DNSSEC validation at downstream
DnsQuestionRecord newQuestion = new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN);
request = new DnsDatagram(request.Identifier, request.IsResponse, request.OPCODE, request.AuthoritativeAnswer, request.Truncation, request.RecursionDesired, request.RecursionAvailable, request.AuthenticData, request.CheckingDisabled, request.RCODE, [newQuestion], request.Answer, request.Authority, request.Additional);
}
}
if (_enableBlocking)
{
if (_blockingBypassList is not null)
{
IPAddress remoteIP = remoteEP.Address;
foreach (NetworkAddress network in _blockingBypassList)
{
if (network.Contains(remoteIP))
return true;
}
}
if (_allowedZoneManager.IsAllowed(request) || _blockListZoneManager.IsAllowed(request))
return true;
}
foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers)
{
try
{
if (await blockingHandler.IsAllowedAsync(request, remoteEP))
return true;
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
return false;
}
private async Task ProcessBlockedQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)
{
if (_enableBlocking)
{
DnsDatagram blockedResponse = ProcessBlocked(request);
if (blockedResponse is not null)
return blockedResponse;
}
foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers)
{
try
{
DnsDatagram appBlockedResponse = await blockingHandler.ProcessRequestAsync(request, remoteEP);
if (appBlockedResponse is not null)
{
if (appBlockedResponse.Tag is null)
appBlockedResponse.Tag = DnsServerResponseType.Blocked;
return appBlockedResponse;
}
}
catch (Exception ex)
{
_log?.Write(remoteEP, protocol, ex);
}
}
return null;
}
private async Task ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers)
{
bool isAllowed;
if (cacheRefreshOperation)
{
//cache refresh operation should be able to refresh all the records in cache
//this is since a blocked CNAME record could still be used by an allowed domain name and so must resolve
isAllowed = true;
}
else
{
isAllowed = await IsAllowedAsync(request, remoteEP, protocol);
if (!isAllowed)
{
DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(request, remoteEP, protocol);
if (blockedResponse is not null)
return blockedResponse;
}
}
DnsDatagram response = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers);
if (response is null)
return null; //drop request
if (response.Answer.Count > 0)
{
DnsResourceRecordType questionType = request.Question[0].Type;
DnsResourceRecord lastRR = response.GetLastAnswerRecord();
if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY))
{
response = await ProcessCNAMEAsync(request, response, remoteEP, protocol, true, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers);
if (response is null)
return null; //drop request
}
if (!isAllowed)
{
//check for CNAME cloaking
for (int i = 0; i < response.Answer.Count; i++)
{
DnsResourceRecord record = response.Answer[i];
if (record.Type != DnsResourceRecordType.CNAME)
break; //no further CNAME records exists
DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord((record.RDATA as DnsCNAMERecordData).Domain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize);
if (request.Metadata is not null)
newRequest.SetMetadata(request.Metadata.NameServer);
//check allowed zone
isAllowed = await IsAllowedAsync(newRequest, remoteEP, protocol);
if (isAllowed)
break; //CNAME is in allowed zone
//check blocked zone and block list zone
DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(newRequest, remoteEP, protocol);
if (blockedResponse is not null)
{
//found cname cloaking
List answer = new List();
//copy current and previous CNAME records
for (int j = 0; j <= i; j++)
answer.Add(response.Answer[j]);
//copy last response answers
answer.AddRange(blockedResponse.Answer);
//include blocked response additional section to pass on Extended DNS Errors
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, blockedResponse.RCODE, request.Question, answer, blockedResponse.Authority, blockedResponse.Additional) { Tag = blockedResponse.Tag };
}
}
}
}
if (response.Tag is null)
{
if (response.IsBlockedResponse())
response.Tag = DnsServerResponseType.UpstreamBlocked;
}
else if ((DnsServerResponseType)response.Tag == DnsServerResponseType.Cached)
{
if (response.IsBlockedResponse())
response.Tag = DnsServerResponseType.CacheBlocked;
}
return response;
}
private async Task RecursiveResolveAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers)
{
DnsQuestionRecord question = request.Question[0];
NetworkAddress eDnsClientSubnet = null;
bool advancedForwardingClientSubnet = false; //this feature is used by Advanced Forwarding app to cache response per network group
if (_eDnsClientSubnet)
{
EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();
if (requestECS is null)
{
if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork))
{
//set ipv4 override shadow ECS option
eDnsClientSubnet = _eDnsClientSubnetIpv4Override;
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
}
else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6))
{
//set ipv6 override shadow ECS option
eDnsClientSubnet = _eDnsClientSubnetIpv6Override;
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
}
else if (!NetUtilities.IsPrivateIP(remoteEP.Address))
{
//set shadow ECS option
switch (remoteEP.AddressFamily)
{
case AddressFamily.InterNetwork:
eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv4PrefixLength);
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
break;
case AddressFamily.InterNetworkV6:
eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv6PrefixLength);
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
break;
default:
request.ShadowHideEDnsClientSubnetOption();
break;
}
}
}
else if ((requestECS.Family != EDnsClientSubnetAddressFamily.IPv4) && (requestECS.Family != EDnsClientSubnetAddressFamily.IPv6))
{
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };
}
else if (requestECS.AdvancedForwardingClientSubnet)
{
//request from Advanced Forwarding app
advancedForwardingClientSubnet = true;
eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength);
}
else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.Address))
{
//disable ECS option
request.ShadowHideEDnsClientSubnetOption();
}
else if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork))
{
//set ipv4 override shadow ECS option
eDnsClientSubnet = _eDnsClientSubnetIpv4Override;
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
}
else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6))
{
//set ipv6 override shadow ECS option
eDnsClientSubnet = _eDnsClientSubnetIpv6Override;
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
}
else
{
//use ECS from client request
switch (requestECS.Family)
{
case EDnsClientSubnetAddressFamily.IPv4:
eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv4PrefixLength));
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
break;
case EDnsClientSubnetAddressFamily.IPv6:
eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv6PrefixLength));
request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);
break;
}
}
}
else
{
//ECS feature disabled
EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();
if (requestECS is not null)
{
advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet;
if (advancedForwardingClientSubnet)
eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); //request from Advanced Forwarding app
else
request.ShadowHideEDnsClientSubnetOption(); //hide ECS option
}
}
if (!cachePrefetchOperation && !cacheRefreshOperation)
{
//query cache zone to see if answer available
DnsDatagram cacheResponse = await QueryCacheAsync(request, false, false);
if (cacheResponse is not null)
{
if (_cachePrefetchTrigger > 0)
{
//inspect response TTL values to decide if prefetch trigger is needed
foreach (DnsResourceRecord answer in cacheResponse.Answer)
{
if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= _cachePrefetchTrigger))
{
//trigger prefetch async
_ = PrefetchCacheAsync(request, remoteEP, conditionalForwarders);
break;
}
}
}
return cacheResponse;
}
}
//recursion with locking
TaskCompletionSource resolverTaskCompletionSource = new TaskCompletionSource();
Task resolverTask = _resolverTasks.GetOrAdd(GetResolverQueryKey(question, eDnsClientSubnet), resolverTaskCompletionSource.Task);
if (resolverTask.Equals(resolverTaskCompletionSource.Task))
{
//got new resolver task added so question is not being resolved; do recursive resolution in another task on resolver thread pool
if (!_resolverTaskPool.TryQueueTask(delegate (object state)
{
return RecursiveResolverBackgroundTaskAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource);
})
)
{
//resolver queue full
if (!_resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _)) //remove recursion lock entry
throw new InvalidOperationException();
return null; //drop request
}
}
//request is being recursively resolved by another thread
if (cachePrefetchOperation)
return null; //return null as prefetch worker thread does not need valid response and thus does not need to wait
if (_serveStale)
{
int waitTimeout = Math.Min(_serveStaleMaxWaitTime, _clientTimeout - SERVE_STALE_TIME_DIFFERENCE); //200ms before client timeout or max 1800ms [RFC 8767]
//wait till short timeout for response
if ((waitTimeout > 0) && (await Task.WhenAny(resolverTask, Task.Delay(waitTimeout)) == resolverTask))
{
//resolver signaled
RecursiveResolveResponse response = await resolverTask;
if (response is not null)
return PrepareRecursiveResolveResponse(request, response);
//resolver had exception
}
else
{
//wait timed out
//query cache zone to return stale answer (if available) as per RFC 8767
DnsDatagram staleResponse = await QueryCacheAsync(request, true, false);
if (staleResponse is not null)
return staleResponse;
//no stale record was found
//wait till full timeout before responding as ServerFailure
int timeout = _clientTimeout - waitTimeout;
if (await Task.WhenAny(resolverTask, Task.Delay(timeout)) == resolverTask)
{
//resolver signaled
RecursiveResolveResponse response = await resolverTask;
if (response is not null)
return PrepareRecursiveResolveResponse(request, response);
//resolver had exception
}
}
}
else
{
//wait till full client timeout for response
if (await Task.WhenAny(resolverTask, Task.Delay(_clientTimeout)) == resolverTask)
{
//resolver signaled
RecursiveResolveResponse response = await resolverTask;
if (response is not null)
return PrepareRecursiveResolveResponse(request, response);
//resolver had exception
}
}
//no response available; respond with ServerFailure
EDnsOption[] options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Waiting for resolver. Please try again."))];
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
}
private async Task RecursiveResolverBackgroundTaskAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource taskCompletionSource)
{
try
{
//recursive resolve and update cache
IDnsCache dnsCache;
if (cachePrefetchOperation || cacheRefreshOperation)
dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question);
else if (skipDnsAppAuthoritativeRequestHandlers || advancedForwardingClientSubnet)
dnsCache = _dnsCacheSkipDnsApps; //to prevent request reaching apps again
else
dnsCache = _dnsCache;
DnsDatagram response;
if (conditionalForwarders is not null)
{
if (conditionalForwarders.Count > 0)
{
//do priority based conditional forwarding
response = await PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders);
}
else
{
//do force recursive resolution
response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, _nsRevalidation, true, cancellationToken: cancellationToken1);
}, RECURSIVE_RESOLUTION_TIMEOUT);
}
}
else
{
//do default recursive resolution
response = await DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, dnssecValidation, skipDnsAppAuthoritativeRequestHandlers);
}
switch (response.RCODE)
{
case DnsResponseCode.NoError:
case DnsResponseCode.NxDomain:
case DnsResponseCode.YXDomain:
taskCompletionSource.SetResult(new RecursiveResolveResponse(response, response));
break;
default:
throw new DnsServerException("DNS Server received a response for '" + question.ToString() + "' with RCODE=" + response.RCODE.ToString() + " from: " + (response.Metadata is null ? "unknown" : response.Metadata.NameServer));
}
}
catch (Exception ex)
{
if (_resolverLog is not null)
{
string strForwarders = null;
if ((conditionalForwarders is not null) && (conditionalForwarders.Count > 0))
{
foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)
{
NameServerAddress nameServer = (conditionalForwarder.RDATA as DnsForwarderRecordData).NameServer;
if (strForwarders is null)
strForwarders = nameServer.ToString();
else
strForwarders += ", " + nameServer.ToString();
}
}
else if ((_forwarders is not null) && (_forwarders.Count > 0))
{
foreach (NameServerAddress nameServer in _forwarders)
{
if (strForwarders is null)
strForwarders = nameServer.ToString();
else
strForwarders += ", " + nameServer.ToString();
}
}
_resolverLog.Write("DNS Server failed to resolve the request '" + question.ToString() + "'" + (strForwarders is null ? "" : " using forwarders: " + strForwarders) + ".\r\n" + ex.ToString());
}
//fetch failure/stale response to signal; reset stale records
DnsDatagram cacheRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, dnssecValidation, DnsResponseCode.NoError, [question], null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(eDnsClientSubnet));
DnsDatagram cacheResponse = await QueryCacheAsync(cacheRequest, _serveStale, _serveStale);
if (cacheResponse is not null)
{
//signal failure/stale response
if (!dnssecValidation || cacheResponse.AuthenticData)
{
//no dnssec validation enabled OR cache response is validated data
taskCompletionSource.SetResult(new RecursiveResolveResponse(cacheResponse, cacheResponse));
}
else
{
//dnssec validation enabled; cache response may be a bogus/failure response
static bool HasBogusRecords(IReadOnlyList records)
{
foreach (DnsResourceRecord record in records)
{
switch (record.DnssecStatus)
{
case DnssecStatus.Disabled:
case DnssecStatus.Secure:
case DnssecStatus.Insecure:
case DnssecStatus.Indeterminate:
break;
default:
return true;
}
}
return false;
}
bool isFailureResponse = false;
switch (cacheResponse.RCODE)
{
case DnsResponseCode.NoError:
case DnsResponseCode.NxDomain:
case DnsResponseCode.YXDomain:
isFailureResponse = HasBogusRecords(cacheResponse.Answer);
if (!isFailureResponse)
isFailureResponse = HasBogusRecords(cacheResponse.Authority);
break;
default:
isFailureResponse = true;
break;
}
if (isFailureResponse)
{
//return failure response
List options;
if ((cacheResponse.EDNS is not null) && (cacheResponse.EDNS.Options.Count > 0))
{
options = new List(cacheResponse.EDNS.Options.Count);
foreach (EDnsOption option in cacheResponse.EDNS.Options)
{
if (option.Code == EDnsOptionCode.EXTENDED_DNS_ERROR)
options.Add(option);
}
}
else
{
options = null;
}
DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, [question], null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, cacheResponse));
}
else
{
//return cached stale answer
taskCompletionSource.SetResult(new RecursiveResolveResponse(cacheResponse, cacheResponse));
}
}
}
else
{
IReadOnlyList options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Resolver exception"))];
DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, [question], null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse));
}
}
finally
{
_resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _);
}
}
private async Task DefaultRecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IDnsCache dnsCache, bool dnssecValidation, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default)
{
if ((_forwarders is not null) && (_forwarders.Count > 0))
{
//use forwarders
if (_concurrentForwarding)
{
if (_proxy is null)
{
//recursive resolve forwarders only when proxy is null else let proxy resolve it to allow using .onion or private domains
foreach (NameServerAddress forwarder in _forwarders)
{
if (forwarder.IsIPEndPointStale)
{
//refresh forwarder IPEndPoint if stale
await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1);
}, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken);
}
}
}
//query forwarders and update cache
DnsClient dnsClient = new DnsClient(_forwarders);
dnsClient.Cache = dnsCache;
dnsClient.Proxy = _proxy;
dnsClient.PreferIPv6 = _preferIPv6;
dnsClient.RandomizeName = _randomizeName;
dnsClient.Retries = _forwarderRetries;
dnsClient.Timeout = _forwarderTimeout;
dnsClient.Concurrency = _forwarderConcurrency;
dnsClient.UdpPayloadSize = _udpPayloadSize;
dnsClient.DnssecValidation = dnssecValidation;
dnsClient.EDnsClientSubnet = eDnsClientSubnet;
dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them
return await dnsClient.ResolveAsync(question, cancellationToken);
}
else
{
//do sequentially ordered forwarding
Exception lastException = null;
foreach (NameServerAddress forwarder in _forwarders)
{
if (_proxy is null)
{
//recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains
if (forwarder.IsIPEndPointStale)
{
//refresh forwarder IPEndPoint if stale
await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1);
}, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken);
}
}
//query forwarder and update cache
DnsClient dnsClient = new DnsClient(forwarder);
dnsClient.Cache = dnsCache;
dnsClient.Proxy = _proxy;
dnsClient.PreferIPv6 = _preferIPv6;
dnsClient.RandomizeName = _randomizeName;
dnsClient.Retries = _forwarderRetries;
dnsClient.Timeout = _forwarderTimeout;
dnsClient.Concurrency = _forwarderConcurrency;
dnsClient.UdpPayloadSize = _udpPayloadSize;
dnsClient.DnssecValidation = dnssecValidation;
dnsClient.EDnsClientSubnet = eDnsClientSubnet;
dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them
try
{
return await dnsClient.ResolveAsync(question, cancellationToken);
}
catch (Exception ex)
{
lastException = ex;
}
if (dnsCache is not ResolverPrefetchDnsCache)
dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache
}
ExceptionDispatchInfo.Capture(lastException).Throw();
throw lastException;
}
}
else
{
//do recursive resolution
return await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, _nsRevalidation, true, null, cancellationToken1);
}, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken);
}
}
internal async Task PriorityConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, bool skipDnsAppAuthoritativeRequestHandlers, IReadOnlyList conditionalForwarders)
{
if (conditionalForwarders.Count == 1)
{
DnsResourceRecord conditionalForwarder = conditionalForwarders[0];
return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers);
}
//group by priority and check for forwarder name server resolution
Dictionary> conditionalForwarderGroups = new Dictionary>(conditionalForwarders.Count);
{
List resolveTasks = new List();
foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)
{
if (conditionalForwarder.Type != DnsResourceRecordType.FWD)
continue;
DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData;
if (conditionalForwarderGroups.TryGetValue(forwarder.Priority, out List conditionalForwardersEntry))
{
conditionalForwardersEntry.Add(conditionalForwarder);
}
else
{
conditionalForwardersEntry = new List(2)
{
conditionalForwarder
};
conditionalForwarderGroups[forwarder.Priority] = conditionalForwardersEntry;
}
if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
continue; //skip resolving
NetProxy proxy = forwarder.GetProxy(_proxy);
if (proxy is null)
{
//recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains
if (forwarder.NameServer.IsIPEndPointStale)
{
//refresh forwarder IPEndPoint if stale
resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1);
}, RECURSIVE_RESOLUTION_TIMEOUT));
}
}
}
if (resolveTasks.Count > 0)
await Task.WhenAll(resolveTasks);
}
if (conditionalForwarderGroups.Count == 1)
{
foreach (KeyValuePair> conditionalForwardersEntry in conditionalForwarderGroups)
return await ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry.Value, skipDnsAppAuthoritativeRequestHandlers);
}
List priorities = new List(conditionalForwarderGroups.Keys);
priorities.Sort();
using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())
{
CancellationToken currentCancellationToken = cancellationTokenSource.Token;
DnsDatagram lastResponse = null;
Exception lastException = null;
foreach (byte priority in priorities)
{
if (!conditionalForwarderGroups.TryGetValue(priority, out List conditionalForwardersEntry))
continue;
Task priorityTask = ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken);
try
{
DnsDatagram priorityTaskResponse = await priorityTask; //await to get response
switch (priorityTaskResponse.RCODE)
{
case DnsResponseCode.NoError:
case DnsResponseCode.NxDomain:
case DnsResponseCode.YXDomain:
cancellationTokenSource.Cancel(); //to stop other priority resolver tasks
return priorityTaskResponse;
default:
//keep response
lastResponse = priorityTaskResponse;
break;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
lastException = ex;
if (lastException is AggregateException)
lastException = lastException.InnerException;
}
if (dnsCache is not ResolverPrefetchDnsCache)
dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache
}
if (lastResponse is not null)
return lastResponse;
if (lastException is not null)
ExceptionDispatchInfo.Capture(lastException).Throw();
throw new InvalidOperationException();
}
}
private async Task ConcurrentConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, List conditionalForwarders, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default)
{
if (conditionalForwarders.Count == 1)
{
DnsResourceRecord conditionalForwarder = conditionalForwarders[0];
return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, cancellationToken);
}
using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())
{
using CancellationTokenRegistration r = cancellationToken.Register(cancellationTokenSource.Cancel);
CancellationToken currentCancellationToken = cancellationTokenSource.Token;
List> tasks = new List>(conditionalForwarders.Count);
//start worker tasks
foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)
{
if (conditionalForwarder.Type != DnsResourceRecordType.FWD)
continue;
DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData;
tasks.Add(Task.Factory.StartNew(delegate ()
{
return ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, forwarder, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current).Unwrap());
}
//wait for first positive response, or for all tasks to fault
DnsDatagram lastResponse = null;
Exception lastException = null;
while (tasks.Count > 0)
{
Task completedTask = await Task.WhenAny(tasks);
try
{
DnsDatagram taskResponse = await completedTask; //await to get response
switch (taskResponse.RCODE)
{
case DnsResponseCode.NoError:
case DnsResponseCode.NxDomain:
case DnsResponseCode.YXDomain:
cancellationTokenSource.Cancel(); //to stop other resolver tasks
return taskResponse;
default:
//keep response
lastResponse = taskResponse;
break;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
lastException = ex;
if (lastException is AggregateException)
lastException = lastException.InnerException;
}
tasks.Remove(completedTask);
}
if (lastResponse is not null)
return lastResponse;
if (lastException is not null)
ExceptionDispatchInfo.Capture(lastException).Throw();
throw new InvalidOperationException();
}
}
private Task ConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, DnsForwarderRecordData forwarder, string conditionalForwardingZoneCut, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default)
{
if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
{
//resolve via default recursive resolver with DNSSEC validation preference
return DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, forwarder.DnssecValidation, skipDnsAppAuthoritativeRequestHandlers, cancellationToken);
}
else
{
//resolve via conditional forwarder
DnsClient dnsClient = new DnsClient(forwarder.NameServer);
dnsClient.Cache = dnsCache;
dnsClient.Proxy = forwarder.GetProxy(_proxy);
dnsClient.PreferIPv6 = _preferIPv6;
dnsClient.RandomizeName = _randomizeName;
dnsClient.Retries = _forwarderRetries;
dnsClient.Timeout = _forwarderTimeout;
dnsClient.Concurrency = _forwarderConcurrency;
dnsClient.UdpPayloadSize = _udpPayloadSize;
dnsClient.DnssecValidation = forwarder.DnssecValidation;
dnsClient.EDnsClientSubnet = eDnsClientSubnet;
dnsClient.AdvancedForwardingClientSubnet = advancedForwardingClientSubnet;
dnsClient.ConditionalForwardingZoneCut = conditionalForwardingZoneCut;
return dnsClient.ResolveAsync(question, cancellationToken);
}
}
private DnsDatagram PrepareRecursiveResolveResponse(DnsDatagram request, RecursiveResolveResponse resolveResponse)
{
//get a tailored response for the request
bool dnssecOk = request.DnssecOk;
if (dnssecOk && request.CheckingDisabled)
{
DnsDatagram cdResponse = resolveResponse.CheckingDisabledResponse;
bool authenticData = false;
if (dnssecOk)
{
if (cdResponse.Answer.Count > 0)
{
authenticData = true;
foreach (DnsResourceRecord record in cdResponse.Answer)
{
if (record.DnssecStatus != DnssecStatus.Secure)
{
authenticData = false;
break;
}
}
}
else if (cdResponse.Authority.Count > 0)
{
authenticData = true;
foreach (DnsResourceRecord record in cdResponse.Authority)
{
if (record.DnssecStatus != DnssecStatus.Secure)
{
authenticData = false;
break;
}
}
}
}
DnsDatagram finalCdResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, true, cdResponse.RCODE, request.Question, cdResponse.Answer, cdResponse.Authority, RemoveOPTFromAdditional(cdResponse.Additional, true), _udpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, cdResponse.EDNS?.Options);
DnsDatagramMetadata metadata = cdResponse.Metadata;
if (metadata is not null)
finalCdResponse.SetMetadata(metadata.NameServer, metadata.RoundTripTime);
return finalCdResponse;
}
DnsDatagram response = resolveResponse.Response;
IReadOnlyList answer = response.Answer;
IReadOnlyList authority = response.Authority;
IReadOnlyList additional = response.Additional;
//answer section checks
if (!dnssecOk && (answer.Count > 0) && (response.Question[0].Type != DnsResourceRecordType.ANY))
{
//remove RRSIGs from answer
bool foundRRSIG = false;
foreach (DnsResourceRecord record in answer)
{
if (record.Type == DnsResourceRecordType.RRSIG)
{
foundRRSIG = true;
break;
}
}
if (foundRRSIG)
{
List newAnswer = new List(answer.Count);
foreach (DnsResourceRecord record in answer)
{
if (record.Type == DnsResourceRecordType.RRSIG)
continue;
newAnswer.Add(record);
}
answer = newAnswer;
}
}
//authority section checks
if (!dnssecOk && (authority.Count > 0))
{
//remove DNSSEC records
bool foundDnssecRecords = false;
bool foundOther = false;
foreach (DnsResourceRecord record in authority)
{
switch (record.Type)
{
case DnsResourceRecordType.DS:
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3:
foundDnssecRecords = true;
break;
default:
foundOther = true;
break;
}
}
if (foundDnssecRecords)
{
if (foundOther)
{
List newAuthority = new List(2);
foreach (DnsResourceRecord record in authority)
{
switch (record.Type)
{
case DnsResourceRecordType.DS:
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3:
break;
default:
newAuthority.Add(record);
break;
}
}
authority = newAuthority;
}
else
{
authority = Array.Empty();
}
}
}
//additional section checks
if (additional.Count > 0)
{
if ((request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0)))
{
//copy options as new OPT and keep other records
List newAdditional = new List(additional.Count);
foreach (DnsResourceRecord record in additional)
{
switch (record.Type)
{
case DnsResourceRecordType.OPT:
continue;
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.DNSKEY:
if (dnssecOk)
break;
continue;
}
newAdditional.Add(record);
}
IReadOnlyList options;
if (response.GetEDnsClientSubnetOption(true) is not null)
{
//response contains ECS
if (request.GetEDnsClientSubnetOption(true) is not null)
{
//request has ECS and type is supported; keep ECS in response
options = response.EDNS.Options;
}
else
{
//cache does not support the qtype so remove ECS from response
if (response.EDNS.Options.Count == 1)
{
options = Array.Empty();
}
else
{
List newOptions = new List(response.EDNS.Options.Count);
foreach (EDnsOption option in response.EDNS.Options)
{
if (option.Code != EDnsOptionCode.EDNS_CLIENT_SUBNET)
newOptions.Add(option);
}
options = newOptions;
}
}
}
else
{
options = response.EDNS.Options;
}
if (response.DnsClientExtendedErrors.Count > 0)
{
//add dns client extended errors
List newOptions = new List(options.Count + response.DnsClientExtendedErrors.Count);
newOptions.AddRange(options);
foreach (EDnsExtendedDnsErrorOptionData ee in response.DnsClientExtendedErrors)
newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, ee));
options = newOptions;
}
newAdditional.Add(DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options));
additional = newAdditional;
}
else if (response.EDNS is not null)
{
//remove OPT from additional
additional = RemoveOPTFromAdditional(additional, dnssecOk);
}
}
{
bool authenticData = false;
if (dnssecOk)
{
if (answer.Count > 0)
{
authenticData = true;
foreach (DnsResourceRecord record in answer)
{
if (record.DnssecStatus != DnssecStatus.Secure)
{
authenticData = false;
break;
}
}
}
else if (authority.Count > 0)
{
authenticData = true;
foreach (DnsResourceRecord record in authority)
{
if (record.DnssecStatus != DnssecStatus.Secure)
{
authenticData = false;
break;
}
}
}
}
DnsDatagram finalResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, request.CheckingDisabled, response.RCODE, request.Question, answer, authority, additional);
DnsDatagramMetadata metadata = response.Metadata;
if (metadata is not null)
finalResponse.SetMetadata(metadata.NameServer, metadata.RoundTripTime);
return finalResponse;
}
}
private static IReadOnlyList RemoveOPTFromAdditional(IReadOnlyList additional, bool dnssecOk)
{
if (additional.Count == 0)
return additional;
if ((additional.Count == 1) && (additional[0].Type == DnsResourceRecordType.OPT))
return Array.Empty();
List newAdditional = new List(additional.Count - 1);
foreach (DnsResourceRecord record in additional)
{
switch (record.Type)
{
case DnsResourceRecordType.OPT:
continue;
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.DNSKEY:
if (dnssecOk)
break;
continue;
}
newAdditional.Add(record);
}
return newAdditional;
}
private static string GetResolverQueryKey(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet)
{
if (eDnsClientSubnet is null)
return question.ToString();
return question.ToString() + " " + eDnsClientSubnet.ToString();
}
private async Task QueryCacheAsync(DnsDatagram request, bool serveStale, bool resetExpiry)
{
DnsDatagram cacheResponse = await _cacheZoneManager.QueryAsync(request, serveStale, false, resetExpiry);
if (cacheResponse is not null)
{
if ((cacheResponse.RCODE != DnsResponseCode.NoError) || (cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || cacheResponse.IsFirstAuthoritySOA())
{
cacheResponse.Tag = DnsServerResponseType.Cached;
return cacheResponse;
}
}
return null;
}
private async Task PrefetchCacheAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders)
{
try
{
_ = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, _dnssecValidation, true, false, false);
}
catch (Exception ex)
{
_resolverLog?.Write(ex);
}
}
private async Task RefreshCacheAsync(IList cacheRefreshSampleList, CacheRefreshSample sample, int sampleQuestionIndex)
{
try
{
//refresh cache
bool addBackToSampleList = false;
DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { sample.SampleQuestion });
DnsDatagram response = await ProcessRecursiveQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Udp, sample.ConditionalForwarders, _dnssecValidation, true, false);
if (response is null)
{
addBackToSampleList = true;
}
else
{
DateTime utcNow = DateTime.UtcNow;
foreach (DnsResourceRecord answer in response.Answer)
{
if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (utcNow.AddSeconds(answer.TTL) < _cachePrefetchSamplingTimerTriggersOn))
{
//answer expires before next sampling so add back to the list to allow refreshing it
addBackToSampleList = true;
break;
}
}
}
if (addBackToSampleList)
cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again
}
catch (Exception ex)
{
_resolverLog?.Write(ex);
cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again
}
}
private async Task GetCacheRefreshNeededQueryAsync(DnsQuestionRecord question, int trigger)
{
int queryCount = 0;
while (true)
{
DnsDatagram cacheResponse = await QueryCacheAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), false, false);
if (cacheResponse is null)
return question; //cache expired so refresh question
if (cacheResponse.Answer.Count == 0)
return null; //dont refresh empty responses
//inspect response TTL values to decide if refresh is needed
foreach (DnsResourceRecord answer in cacheResponse.Answer)
{
if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= trigger))
return question; //TTL eligible and less than trigger so refresh question
}
DnsResourceRecord lastRR = cacheResponse.GetLastAnswerRecord();
if (lastRR.Type == question.Type)
return null; //answer was resolved
if (lastRR.Type != DnsResourceRecordType.CNAME)
return null; //invalid response so ignore question
queryCount++;
if (queryCount >= MAX_CNAME_HOPS)
return null; //too many hops so ignore question
//follow CNAME chain to inspect TTL further
question = new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, question.Type, question.Class);
}
}
private async Task IsCacheRefreshNeededAsync(DnsQuestionRecord question, int trigger)
{
DnsDatagram cacheResponse = await QueryCacheAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), false, false);
if (cacheResponse is null)
return true; //cache expired so refresh needed
if (cacheResponse.Answer.Count == 0)
return false; //dont refresh empty responses
//inspect response TTL values to decide if refresh is needed
foreach (DnsResourceRecord answer in cacheResponse.Answer)
{
if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= trigger))
return true; //TTL eligible less than trigger so refresh
}
return false; //no need to refresh for this query
}
private async void CachePrefetchSamplingTimerCallback(object state)
{
try
{
List> eligibleQueries = _stats.GetLastHourEligibleQueries(_cachePrefetchSampleEligibilityHitsPerHour);
List cacheRefreshSampleList = new List(eligibleQueries.Count);
int cacheRefreshTrigger = (_cachePrefetchSampleIntervalInMinutes + 1) * 60;
foreach (KeyValuePair eligibleQuery in eligibleQueries)
{
DnsQuestionRecord eligibleQuerySample = eligibleQuery.Key;
if (eligibleQuerySample.Type == DnsResourceRecordType.ANY)
continue; //dont refresh type ANY queries
DnsQuestionRecord refreshQuery = null;
IReadOnlyList conditionalForwarders = null;
//query auth zone for refresh query
int queryCount = 0;
bool reQueryAuthZone;
do
{
reQueryAuthZone = false;
DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { eligibleQuerySample });
DnsDatagram response = await AuthoritativeQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, false);
if (response is null)
{
//zone not hosted; do refresh
refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger);
}
else
{
//zone is hosted; check further
if (response.Answer.Count > 0)
{
DnsResourceRecord lastRR = response.GetLastAnswerRecord();
if ((lastRR.Type == DnsResourceRecordType.CNAME) && (eligibleQuerySample.Type != DnsResourceRecordType.CNAME))
{
eligibleQuerySample = new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, eligibleQuerySample.Type, eligibleQuerySample.Class);
reQueryAuthZone = true;
}
}
else if (response.Authority.Count > 0)
{
DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord();
switch (firstAuthority.Type)
{
case DnsResourceRecordType.NS: //zone is delegated
refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger);
conditionalForwarders = Array.Empty(); //do forced recursive resolution using empty conditional forwarders
break;
case DnsResourceRecordType.FWD: //zone is conditional forwarder
refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger);
conditionalForwarders = response.Authority; //do conditional forwarding
break;
}
}
}
}
while (reQueryAuthZone && (++queryCount < MAX_CNAME_HOPS));
if (refreshQuery is not null)
cacheRefreshSampleList.Add(new CacheRefreshSample(refreshQuery, conditionalForwarders));
}
_cacheRefreshSampleList = cacheRefreshSampleList;
}
catch (Exception ex)
{
_log?.Write(ex);
}
finally
{
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer is not null)
{
_cachePrefetchSamplingTimer.Change(_cachePrefetchSampleIntervalInMinutes * 60 * 1000, Timeout.Infinite);
_cachePrefetchSamplingTimerTriggersOn = DateTime.UtcNow.AddMinutes(_cachePrefetchSampleIntervalInMinutes);
}
}
}
}
private async void CachePrefetchRefreshTimerCallback(object state)
{
try
{
IList cacheRefreshSampleList = _cacheRefreshSampleList;
if (cacheRefreshSampleList is not null)
{
for (int i = 0; i < cacheRefreshSampleList.Count; i++)
{
CacheRefreshSample sample = cacheRefreshSampleList[i];
if (sample is null)
continue;
if (!await IsCacheRefreshNeededAsync(sample.SampleQuestion, _cachePrefetchTrigger + 1))
continue;
//run in resolver thread pool
if (_resolverTaskPool.TryQueueTask(delegate (object state)
{
return RefreshCacheAsync(cacheRefreshSampleList, sample, (int)state);
}, i)
)
{
//refresh cache task was queued
cacheRefreshSampleList[i] = null; //remove from sample list to avoid concurrent refresh attempt
}
}
}
}
catch (Exception ex)
{
_log?.Write(ex);
}
finally
{
lock (_cachePrefetchRefreshTimerLock)
{
_cachePrefetchRefreshTimer?.Change((_cachePrefetchTrigger + 1) * 1000, Timeout.Infinite);
}
}
}
private void CacheMaintenanceTimerCallback(object state)
{
try
{
_cacheZoneManager.RemoveExpiredRecords();
//force GC collection to remove old cache data from memory quickly
GC.Collect();
}
catch (Exception ex)
{
_log?.Write(ex);
}
finally
{
lock (_cacheMaintenanceTimerLock)
{
_cacheMaintenanceTimer?.Change(CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);
}
}
}
private void ResetPrefetchTimers()
{
if ((_cachePrefetchTrigger == 0) || (_recursion == DnsServerRecursion.Deny))
{
lock (_cachePrefetchSamplingTimerLock)
{
_cachePrefetchSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
lock (_cachePrefetchRefreshTimerLock)
{
_cachePrefetchRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
}
else if (_state == ServiceState.Running)
{
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer is not null)
{
_cachePrefetchSamplingTimer.Change(CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL, Timeout.Infinite);
_cachePrefetchSamplingTimerTriggersOn = DateTime.UtcNow.AddMilliseconds(CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL);
}
}
lock (_cachePrefetchRefreshTimerLock)
{
_cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL, Timeout.Infinite);
}
}
}
internal bool IsQpmLimitCrossed(IPAddress remoteIP)
{
if ((_qpmLimitRequests < 1) && (_qpmLimitErrors < 1))
return false;
if (IPAddress.IsLoopback(remoteIP))
return false;
if (_qpmLimitBypassList is not null)
{
foreach (NetworkAddress networkAddress in _qpmLimitBypassList)
{
if (networkAddress.Contains(remoteIP))
return false;
}
}
IPAddress remoteSubnet;
switch (remoteIP.AddressFamily)
{
case AddressFamily.InterNetwork:
remoteSubnet = remoteIP.GetNetworkAddress(_qpmLimitIPv4PrefixLength);
break;
case AddressFamily.InterNetworkV6:
remoteSubnet = remoteIP.GetNetworkAddress(_qpmLimitIPv6PrefixLength);
break;
default:
throw new NotSupportedException("AddressFamily not supported.");
}
if ((_qpmLimitErrors > 0) && (_qpmLimitErrorClientSubnetStats is not null) && _qpmLimitErrorClientSubnetStats.TryGetValue(remoteSubnet, out long errorCountPerSample))
{
long averageErrorCountPerMinute = errorCountPerSample / _qpmLimitSampleMinutes;
if (averageErrorCountPerMinute >= _qpmLimitErrors)
return true;
}
if ((_qpmLimitRequests > 0) && (_qpmLimitClientSubnetStats is not null) && _qpmLimitClientSubnetStats.TryGetValue(remoteSubnet, out long countPerSample))
{
long averageCountPerMinute = countPerSample / _qpmLimitSampleMinutes;
if (averageCountPerMinute >= _qpmLimitRequests)
return true;
}
return false;
}
private void QpmLimitSamplingTimerCallback(object state)
{
try
{
_stats.GetLatestClientSubnetStats(_qpmLimitSampleMinutes, _qpmLimitIPv4PrefixLength, _qpmLimitIPv6PrefixLength, out IReadOnlyDictionary qpmLimitClientSubnetStats, out IReadOnlyDictionary qpmLimitErrorClientSubnetStats);
if (_qpmLimitErrors > 0)
WriteClientSubnetRateLimitLog(_qpmLimitErrorClientSubnetStats, qpmLimitErrorClientSubnetStats, _qpmLimitErrors, "errors");
if (_qpmLimitRequests > 0)
WriteClientSubnetRateLimitLog(_qpmLimitClientSubnetStats, qpmLimitClientSubnetStats, _qpmLimitRequests, "requests");
_qpmLimitClientSubnetStats = qpmLimitClientSubnetStats;
_qpmLimitErrorClientSubnetStats = qpmLimitErrorClientSubnetStats;
}
catch (Exception ex)
{
_log?.Write(ex);
}
finally
{
lock (_qpmLimitSamplingTimerLock)
{
_qpmLimitSamplingTimer?.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite);
}
}
}
private void WriteClientSubnetRateLimitLog(IReadOnlyDictionary oldQpmLimitClientSubnetStats, IReadOnlyDictionary newQpmLimitClientSubnetStats, long qpmLimit, string limitType)
{
if (oldQpmLimitClientSubnetStats is not null)
{
foreach (KeyValuePair sampleEntry in oldQpmLimitClientSubnetStats)
{
long oldAverageCountPerMinute = sampleEntry.Value / _qpmLimitSampleMinutes;
if (oldAverageCountPerMinute >= qpmLimit)
{
//previously over limit
long averageCountPerMinute = 0;
if (newQpmLimitClientSubnetStats.TryGetValue(sampleEntry.Key, out long newCountPerSample))
averageCountPerMinute = newCountPerSample / _qpmLimitSampleMinutes;
if (averageCountPerMinute < qpmLimit) //currently under limit
_log?.Write("Client subnet '" + sampleEntry.Key + "/" + (sampleEntry.Key.AddressFamily == AddressFamily.InterNetwork ? _qpmLimitIPv4PrefixLength : _qpmLimitIPv6PrefixLength) + "' is no longer being rate limited (" + averageCountPerMinute + " qpm for " + limitType + ").");
}
}
}
foreach (KeyValuePair sampleEntry in newQpmLimitClientSubnetStats)
{
long averageCountPerMinute = sampleEntry.Value / _qpmLimitSampleMinutes;
if (averageCountPerMinute >= qpmLimit)
{
//currently over limit
if ((oldQpmLimitClientSubnetStats is not null) && oldQpmLimitClientSubnetStats.TryGetValue(sampleEntry.Key, out long oldCountPerSample))
{
long oldAverageCountPerMinute = oldCountPerSample / _qpmLimitSampleMinutes;
if (oldAverageCountPerMinute >= qpmLimit)
continue; //previously over limit too
}
_log?.Write("Client subnet '" + sampleEntry.Key + "/" + (sampleEntry.Key.AddressFamily == AddressFamily.InterNetwork ? _qpmLimitIPv4PrefixLength : _qpmLimitIPv6PrefixLength) + "' is being rate limited till the query rate limit (" + averageCountPerMinute + " qpm for " + limitType + ") falls below " + qpmLimit + " qpm.");
}
}
}
private void ResetQpsLimitTimer()
{
if ((_qpmLimitRequests < 1) && (_qpmLimitErrors < 1))
{
lock (_qpmLimitSamplingTimerLock)
{
_qpmLimitSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_qpmLimitClientSubnetStats = null;
_qpmLimitErrorClientSubnetStats = null;
}
}
else if (_state == ServiceState.Running)
{
lock (_qpmLimitSamplingTimerLock)
{
_qpmLimitSamplingTimer?.Change(0, Timeout.Infinite);
}
}
}
private void UpdateThisServer()
{
if ((_localEndPoints is null) || (_localEndPoints.Count == 0))
{
_thisServer = new NameServerAddress(_serverDomain, IPAddress.Loopback);
}
else
{
foreach (IPEndPoint localEndPoint in _localEndPoints)
{
if (localEndPoint.Address.Equals(IPAddress.Any))
{
_thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.Loopback, localEndPoint.Port));
return;
}
if (localEndPoint.Address.Equals(IPAddress.IPv6Any))
{
_thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.IPv6Loopback, localEndPoint.Port));
return;
}
}
_thisServer = new NameServerAddress(_serverDomain, _localEndPoints[0]);
}
}
#endregion
#region resolver task pool
internal bool TryQueueResolverTask(Func