/*
Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.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 quicApplicationProtocols = 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;
int _dnsOverUdpProxyPort = 538;
int _dnsOverTcpProxyPort = 538;
int _dnsOverHttpPort = 80;
int _dnsOverTlsPort = 853;
int _dnsOverHttpsPort = 443;
int _dnsOverQuicPort = 853;
X509Certificate2Collection _certificateCollection;
SslServerAuthenticationOptions _sslServerAuthenticationOptions;
SslServerAuthenticationOptions _quicSslServerAuthenticationOptions;
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();
readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(ThreadPriority.AboveNormal);
readonly ConcurrentDictionary> _resolverTasks = new ConcurrentDictionary>();
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;
_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();
_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 (!NetUtilities.IsPrivateIP(remoteEP.Address))
{
//intentionally blocking public IP addresses from using DNS-over-UDP-PROXY
//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(_sslServerAuthenticationOptions);
}, 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 (!NetUtilities.IsPrivateIP(remoteEP.Address))
{
//intentionally blocking public IP addresses from using DNS-over-TCP-PROXY
//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)
{
QuicConnection quicConnection = await quicListener.AcceptConnectionAsync();
_ = ProcessQuicConnectionAsync(quicConnection);
}
}
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(_dnsOverHttpRealIpHeader);
DnsDatagram dnsRequest = null;
try
{
HttpRequest request = context.Request;
HttpResponse response = context.Response;
if (IsQpmLimitCrossed(remoteEP.Address))
{
_stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, true);
response.StatusCode = 429;
await response.WriteAsync("Too Many Requests");
return;
}
if (!request.IsHttps)
{
//get the actual connection remote EP
IPEndPoint connectionEp = context.GetRemoteEndPoint(null);
if (!NetUtilities.IsPrivateIP(connectionEp.Address))
{
//intentionally blocking public IP addresses from using DNS-over-HTTP (without TLS)
//this feature 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;
}
}
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)
{
IReadOnlyList 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);
reprocessResponse = true;
break;
}
}
}
}
while (reprocessResponse);
return response;
}
private async Task AuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers)
{
DnsDatagram response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return _authZoneManager.QueryAsync(request, remoteEP.Address, isRecursionAllowed, cancellationToken1);
}, _clientTimeout);
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)
{
//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;
if (_eDnsClientSubnet)
{
EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();
if (requestECS is not null)
eDnsClientSubnetOption = new EDnsOption[] { 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);
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);
}
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);
isAuthoritativeAnswer = false;
}
break;
case DnsResourceRecordType.FWD:
//do conditional forwarding
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
isAuthoritativeAnswer = false;
break;
case DnsResourceRecordType.APP:
newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
break;
}
}
//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;
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, newAnswer, authority, additional) { Tag = response.Tag };
}
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[] { 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);
}
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);
break;
case DnsResourceRecordType.FWD:
//do conditional forwarding
newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers);
break;
case DnsResourceRecordType.APP:
newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);
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.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 (!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 = QueryCache(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
_ = Task.Factory.StartNew(delegate ()
{
return RecursiveResolverBackgroundTaskAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _resolverTaskScheduler);
}
//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 = QueryCache(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());
}
if (_serveStale)
{
//fetch and reset stale records
DnsDatagram cacheRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, dnssecValidation, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(eDnsClientSubnet));
DnsDatagram staleResponse = QueryCache(cacheRequest, true, true);
if (staleResponse is not null)
{
//signal stale response
if (!dnssecValidation || staleResponse.AuthenticData)
{
taskCompletionSource.SetResult(new RecursiveResolveResponse(staleResponse, staleResponse));
}
else
{
List options;
if ((staleResponse.EDNS is not null) && (staleResponse.EDNS.Options.Count > 0))
{
options = new List(staleResponse.EDNS.Options.Count);
foreach (EDnsOption option in staleResponse.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, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, staleResponse));
}
return;
}
}
//signal failure response to release waiting tasks
if (ex is DnsClientResponseDnssecValidationException ex2)
{
List options;
if (ex2.Response.DnsClientExtendedErrors.Count > 0)
{
options = new List(ex2.Response.DnsClientExtendedErrors.Count);
foreach (EDnsExtendedDnsErrorOptionData dnsError in ex2.Response.DnsClientExtendedErrors)
options.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, dnsError));
}
else
{
options = null;
}
DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, options);
if ((ex2.Response.Question.Count > 0) && ex2.Response.Question[0].Equals(question))
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, ex2.Response));
else
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse));
}
else if (ex is DnsClientNoResponseException ex3)
{
IReadOnlyList options;
if (ex3.InnerException is SocketException ex3a)
{
if (ex3a.SocketErrorCode == SocketError.TimedOut)
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))];
else
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex3a.SocketErrorCode.ToString()))];
}
else
{
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "No response from name servers for " + question.ToString()))];
}
DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse));
}
else if (ex is SocketException ex4)
{
IReadOnlyList options;
if (ex4.SocketErrorCode == SocketError.TimedOut)
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))];
else
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex4.SocketErrorCode.ToString()))];
DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse));
}
else if (ex is IOException ex5)
{
IReadOnlyList options;
if (ex5.InnerException is SocketException ex5a)
{
if (ex5a.SocketErrorCode == SocketError.TimedOut)
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))];
else
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex5a.SocketErrorCode.ToString()))];
}
else
{
options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "IO error: " + ex5.Message))];
}
DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);
taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse));
}
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, new DnsQuestionRecord[] { 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);
}
}
private 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, IReadOnlyList 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)
{
bool authenticData = false;
if (dnssecOk)
{
if (resolveResponse.CheckingDisabledResponse.Answer.Count > 0)
{
authenticData = true;
foreach (DnsResourceRecord record in resolveResponse.CheckingDisabledResponse.Answer)
{
if (record.DnssecStatus != DnssecStatus.Secure)
{
authenticData = false;
break;
}
}
}
else if (resolveResponse.CheckingDisabledResponse.Authority.Count > 0)
{
authenticData = true;
foreach (DnsResourceRecord record in resolveResponse.CheckingDisabledResponse.Authority)
{
if (record.DnssecStatus != DnssecStatus.Secure)
{
authenticData = false;
break;
}
}
}
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, true, resolveResponse.CheckingDisabledResponse.RCODE, request.Question, resolveResponse.CheckingDisabledResponse.Answer, resolveResponse.CheckingDisabledResponse.Authority, RemoveOPTFromAdditional(resolveResponse.CheckingDisabledResponse.Additional, true), _udpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, resolveResponse.CheckingDisabledResponse.EDNS?.Options);
}
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 ((response.RCODE != DnsResponseCode.NoError) && (request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0)))
{
//only responses with RCODE!=NoError gets cached as a special cache record to preserve EDNS options
//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;
}
}
}
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, request.CheckingDisabled, response.RCODE, request.Question, answer, authority, additional);
}
}
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 DnsDatagram QueryCache(DnsDatagram request, bool serveStale, bool resetExpiry)
{
DnsDatagram cacheResponse = _cacheZoneManager.Query(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
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);
bool addBackToSampleList = false;
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 DnsQuestionRecord GetCacheRefreshNeededQuery(DnsQuestionRecord question, int trigger)
{
int queryCount = 0;
while (true)
{
DnsDatagram cacheResponse = QueryCache(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 bool IsCacheRefreshNeeded(DnsQuestionRecord question, int trigger)
{
DnsDatagram cacheResponse = QueryCache(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 = GetCacheRefreshNeededQuery(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 = GetCacheRefreshNeededQuery(eligibleQuerySample, cacheRefreshTrigger);
conditionalForwarders = Array.Empty(); //do forced recursive resolution using empty conditional forwarders
break;
case DnsResourceRecordType.FWD: //zone is conditional forwarder
refreshQuery = GetCacheRefreshNeededQuery(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 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 (!IsCacheRefreshNeeded(sample.SampleQuestion, _cachePrefetchTrigger + 1))
continue;
cacheRefreshSampleList[i] = null; //remove from sample list to avoid concurrent refresh attempt
int sampleQuestionIndex = i;
_ = Task.Run(delegate () { return RefreshCacheAsync(cacheRefreshSampleList, sample, sampleQuestionIndex); }); //run task in threadpool since its long running
}
}
}
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 doh web service
private async Task StartDoHAsync()
{
IReadOnlyList localAddresses = GetValidKestralLocalAddresses(_localEndPoints.Convert(delegate (IPEndPoint ep) { return ep.Address; }));
try
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(Path.GetDirectoryName(_dohwwwFolder))
{
UseActivePolling = true,
UsePollingFileWatcher = true
};
builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_dohwwwFolder)
{
UseActivePolling = true,
UsePollingFileWatcher = true
};
builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions)
{
//bind to http port
if (_enableDnsOverHttp)
{
foreach (IPAddress localAddress in localAddresses)
serverOptions.Listen(localAddress, _dnsOverHttpPort);
}
//bind to https port
if (_enableDnsOverHttps && (_certificateCollection is not null))
{
foreach (IPAddress localAddress in localAddresses)
{
serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions)
{
listenOptions.Protocols = _enableDnsOverHttp3 ? HttpProtocols.Http1AndHttp2AndHttp3 : HttpProtocols.Http1AndHttp2;
listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
{
return ValueTask.FromResult(_sslServerAuthenticationOptions);
}, null);
});
}
}
serverOptions.AddServerHeader = false;
serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout);
serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout);
serverOptions.Limits.MaxRequestHeadersTotalSize = 4096;
serverOptions.Limits.MaxRequestLineSize = serverOptions.Limits.MaxRequestHeadersTotalSize;
serverOptions.Limits.MaxRequestBufferSize = serverOptions.Limits.MaxRequestLineSize;
serverOptions.Limits.MaxRequestBodySize = 64 * 1024;
serverOptions.Limits.MaxResponseBufferSize = 4096;
});
builder.Logging.ClearProviders();
_dohWebService = builder.Build();
_dohWebService.UseDefaultFiles();
_dohWebService.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = delegate (StaticFileResponseContext ctx)
{
ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex, nofollow";
ctx.Context.Response.Headers.CacheControl = "private, max-age=300";
},
ServeUnknownFileTypes = true
});
_dohWebService.UseRouting();
_dohWebService.MapGet("/dns-query", ProcessDoHRequestAsync);
_dohWebService.MapPost("/dns-query", ProcessDoHRequestAsync);
await _dohWebService.StartAsync();
if (_log is not null)
{
foreach (IPAddress localAddress in localAddresses)
{
if (_enableDnsOverHttp)
_log?.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server was bound successfully.");
if (_enableDnsOverHttps && (_certificateCollection is not null))
_log?.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server was bound successfully.");
}
}
}
catch (Exception ex)
{
await StopDoHAsync();
if (_log is not null)
{
foreach (IPAddress localAddress in localAddresses)
{
if (_enableDnsOverHttp)
_log?.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server failed to bind.");
if (_enableDnsOverHttps && (_certificateCollection is not null))
_log?.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server failed to bind.");
}
_log?.Write(ex);
}
}
}
private async Task StopDoHAsync()
{
if (_dohWebService is not null)
{
try
{
await _dohWebService.DisposeAsync();
}
catch (Exception ex)
{
_log?.Write(ex);
}
_dohWebService = null;
}
}
internal static IReadOnlyList GetValidKestralLocalAddresses(IReadOnlyList localAddresses)
{
List supportedLocalAddresses = new List(localAddresses.Count);
foreach (IPAddress localAddress in localAddresses)
{
switch (localAddress.AddressFamily)
{
case AddressFamily.InterNetwork:
if (Socket.OSSupportsIPv4)
{
if (!supportedLocalAddresses.Contains(localAddress))
supportedLocalAddresses.Add(localAddress);
}
break;
case AddressFamily.InterNetworkV6:
if (Socket.OSSupportsIPv6)
{
if (!supportedLocalAddresses.Contains(localAddress))
supportedLocalAddresses.Add(localAddress);
}
break;
}
}
bool containsUnicastAddress = false;
foreach (IPAddress localAddress in supportedLocalAddresses)
{
if (!localAddress.Equals(IPAddress.Any) && !localAddress.Equals(IPAddress.IPv6Any))
{
containsUnicastAddress = true;
break;
}
}
List newLocalAddresses = new List(supportedLocalAddresses.Count);
if (containsUnicastAddress)
{
//replace any with loopback address
foreach (IPAddress localAddress in supportedLocalAddresses)
{
if (localAddress.Equals(IPAddress.Any))
{
if (!newLocalAddresses.Contains(IPAddress.Loopback))
newLocalAddresses.Add(IPAddress.Loopback);
}
else if (localAddress.Equals(IPAddress.IPv6Any))
{
if (!newLocalAddresses.Contains(IPAddress.IPv6Loopback))
newLocalAddresses.Add(IPAddress.IPv6Loopback);
}
else
{
if (!newLocalAddresses.Contains(localAddress))
newLocalAddresses.Add(localAddress);
}
}
}
else
{
//remove "0.0.0.0" if [::] exists
foreach (IPAddress localAddress in supportedLocalAddresses)
{
if (localAddress.Equals(IPAddress.Any))
{
if (!supportedLocalAddresses.Contains(IPAddress.IPv6Any))
newLocalAddresses.Add(localAddress);
}
else
{
newLocalAddresses.Add(localAddress);
}
}
}
return newLocalAddresses;
}
#endregion
#region public
public async Task StartAsync()
{
if (_disposed)
ObjectDisposedException.ThrowIf(_disposed, this);
if (_state != ServiceState.Stopped)
throw new InvalidOperationException("DNS Server is already running.");
_state = ServiceState.Starting;
//bind on all local end points
foreach (IPEndPoint localEP in _localEndPoints)
{
Socket udpListener = null;
try
{
udpListener = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
#region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
const uint IOC_IN = 0x80000000;
const uint IOC_VENDOR = 0x18000000;
const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
#endregion
if (Environment.OSVersion.Platform == PlatformID.Unix)
udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses
udpListener.ReceiveBufferSize = 512 * 1024;
udpListener.SendBufferSize = 512 * 1024;
try
{
udpListener.Bind(localEP);
}
catch (SocketException ex1)
{
switch (ex1.ErrorCode)
{
case 99: //SocketException (99): Cannot assign requested address
await Task.Delay(5000); //wait for address to be available before retrying
udpListener.Bind(localEP);
break;
default:
throw;
}
}
_udpListeners.Add(udpListener);
_log?.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log?.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString());
udpListener?.Dispose();
}
if (_enableDnsOverUdpProxy)
{
IPEndPoint udpProxyEP = new IPEndPoint(localEP.Address, _dnsOverUdpProxyPort);
Socket udpProxyListener = null;
try
{
udpProxyListener = new Socket(udpProxyEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
#region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
const uint IOC_IN = 0x80000000;
const uint IOC_VENDOR = 0x18000000;
const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
udpProxyListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
#endregion
udpProxyListener.ReceiveBufferSize = 512 * 1024;
udpProxyListener.SendBufferSize = 512 * 1024;
udpProxyListener.Bind(udpProxyEP);
_udpProxyListeners.Add(udpProxyListener);
_log?.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log?.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, "DNS Server failed to bind.\r\n" + ex.ToString());
udpProxyListener?.Dispose();
}
}
Socket tcpListener = null;
try
{
tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
tcpListener.Bind(localEP);
tcpListener.Listen(_listenBacklog);
_tcpListeners.Add(tcpListener);
_log?.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log?.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString());
tcpListener?.Dispose();
}
if (_enableDnsOverTcpProxy)
{
IPEndPoint tcpProxyEP = new IPEndPoint(localEP.Address, _dnsOverTcpProxyPort);
Socket tcpProxyListner = null;
try
{
tcpProxyListner = new Socket(tcpProxyEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
tcpProxyListner.Bind(tcpProxyEP);
tcpProxyListner.Listen(_listenBacklog);
_tcpProxyListeners.Add(tcpProxyListner);
_log?.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log?.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, "DNS Server failed to bind.\r\n" + ex.ToString());
tcpProxyListner?.Dispose();
}
}
if (_enableDnsOverTls && (_certificateCollection is not null))
{
IPEndPoint tlsEP = new IPEndPoint(localEP.Address, _dnsOverTlsPort);
Socket tlsListener = null;
try
{
tlsListener = new Socket(tlsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
tlsListener.Bind(tlsEP);
tlsListener.Listen(_listenBacklog);
_tlsListeners.Add(tlsListener);
_log?.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log?.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server failed to bind.\r\n" + ex.ToString());
tlsListener?.Dispose();
}
}
if (_enableDnsOverQuic && (_certificateCollection is not null))
{
IPEndPoint quicEP = new IPEndPoint(localEP.Address, _dnsOverQuicPort);
QuicListener quicListener = null;
try
{
QuicListenerOptions listenerOptions = new QuicListenerOptions()
{
ListenEndPoint = quicEP,
ListenBacklog = _listenBacklog,
ApplicationProtocols = quicApplicationProtocols,
ConnectionOptionsCallback = delegate (QuicConnection quicConnection, SslClientHelloInfo sslClientHello, CancellationToken cancellationToken)
{
QuicServerConnectionOptions serverConnectionOptions = new QuicServerConnectionOptions()
{
DefaultCloseErrorCode = (long)DnsOverQuicErrorCodes.DOQ_NO_ERROR,
DefaultStreamErrorCode = (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR,
MaxInboundUnidirectionalStreams = 0,
MaxInboundBidirectionalStreams = _quicMaxInboundStreams,
IdleTimeout = TimeSpan.FromMilliseconds(_quicIdleTimeout),
ServerAuthenticationOptions = _quicSslServerAuthenticationOptions
};
return ValueTask.FromResult(serverConnectionOptions);
}
};
quicListener = await QuicListener.ListenAsync(listenerOptions);
_quicListeners.Add(quicListener);
_log?.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log?.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server failed to bind.\r\n" + ex.ToString());
if (quicListener is not null)
await quicListener.DisposeAsync();
}
}
}
//start reading query packets
int listenerTaskCount = Math.Max(1, Environment.ProcessorCount);
foreach (Socket udpListener in _udpListeners)
{
for (int i = 0; i < listenerTaskCount; i++)
{
_ = Task.Factory.StartNew(delegate ()
{
return ReadUdpRequestAsync(udpListener, DnsTransportProtocol.Udp);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);
}
}
foreach (Socket udpProxyListener in _udpProxyListeners)
{
for (int i = 0; i < listenerTaskCount; i++)
{
_ = Task.Factory.StartNew(delegate ()
{
return ReadUdpRequestAsync(udpProxyListener, DnsTransportProtocol.UdpProxy);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);
}
}
foreach (Socket tcpListener in _tcpListeners)
{
for (int i = 0; i < listenerTaskCount; i++)
{
_ = Task.Factory.StartNew(delegate ()
{
return AcceptConnectionAsync(tcpListener, DnsTransportProtocol.Tcp);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);
}
}
foreach (Socket tcpProxyListener in _tcpProxyListeners)
{
for (int i = 0; i < listenerTaskCount; i++)
{
_ = Task.Factory.StartNew(delegate ()
{
return AcceptConnectionAsync(tcpProxyListener, DnsTransportProtocol.TcpProxy);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);
}
}
foreach (Socket tlsListener in _tlsListeners)
{
for (int i = 0; i < listenerTaskCount; i++)
{
_ = Task.Factory.StartNew(delegate ()
{
return AcceptConnectionAsync(tlsListener, DnsTransportProtocol.Tls);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);
}
}
foreach (QuicListener quicListener in _quicListeners)
{
for (int i = 0; i < listenerTaskCount; i++)
{
_ = Task.Factory.StartNew(delegate ()
{
return AcceptQuicConnectionAsync(quicListener);
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);
}
}
if (_enableDnsOverHttp || (_enableDnsOverHttps && (_certificateCollection is not null)))
await StartDoHAsync();
_cachePrefetchSamplingTimer = new Timer(CachePrefetchSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
_cachePrefetchRefreshTimer = new Timer(CachePrefetchRefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
_cacheMaintenanceTimer = new Timer(CacheMaintenanceTimerCallback, null, CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL, Timeout.Infinite);
_qpmLimitSamplingTimer = new Timer(QpmLimitSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
_state = ServiceState.Running;
UpdateThisServer();
ResetPrefetchTimers();
ResetQpsLimitTimer();
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
//init udp socket pool async for port randomization
ThreadPool.QueueUserWorkItem(delegate (object state)
{
UdpClientConnection.CreateSocketPool(_preferIPv6);
});
}
}
public async Task StopAsync()
{
if (_state != ServiceState.Running)
return;
_state = ServiceState.Stopping;
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer is not null)
{
_cachePrefetchSamplingTimer.Dispose();
_cachePrefetchSamplingTimer = null;
}
}
lock (_cachePrefetchRefreshTimerLock)
{
if (_cachePrefetchRefreshTimer is not null)
{
_cachePrefetchRefreshTimer.Dispose();
_cachePrefetchRefreshTimer = null;
}
}
lock (_cacheMaintenanceTimerLock)
{
if (_cacheMaintenanceTimer is not null)
{
_cacheMaintenanceTimer.Dispose();
_cacheMaintenanceTimer = null;
}
}
lock (_qpmLimitSamplingTimerLock)
{
if (_qpmLimitSamplingTimer is not null)
{
_qpmLimitSamplingTimer.Dispose();
_qpmLimitSamplingTimer = null;
}
}
foreach (Socket udpListener in _udpListeners)
{
try
{
udpListener.Dispose();
}
catch (Exception ex)
{
_log?.Write(ex);
}
}
foreach (Socket udpProxyListener in _udpProxyListeners)
{
try
{
udpProxyListener.Dispose();
}
catch (Exception ex)
{
_log?.Write(ex);
}
}
foreach (Socket tcpListener in _tcpListeners)
{
try
{
tcpListener.Dispose();
}
catch (Exception ex)
{
_log?.Write(ex);
}
}
foreach (Socket tcpProxyListener in _tcpProxyListeners)
{
try
{
tcpProxyListener.Dispose();
}
catch (Exception ex)
{
_log?.Write(ex);
}
}
foreach (Socket tlsListener in _tlsListeners)
{
try
{
tlsListener.Dispose();
}
catch (Exception ex)
{
_log?.Write(ex);
}
}
foreach (QuicListener quicListener in _quicListeners)
{
try
{
await quicListener.DisposeAsync();
}
catch (Exception ex)
{
_log?.Write(ex);
}
}
_udpListeners.Clear();
_udpProxyListeners.Clear();
_tcpListeners.Clear();
_tcpProxyListeners.Clear();
_tlsListeners.Clear();
_quicListeners.Clear();
await StopDoHAsync();
_state = ServiceState.Stopped;
}
public Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false)
{
return DirectQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), timeout, skipDnsAppAuthoritativeRequestHandlers);
}
public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false)
{
return TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
{
return ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, null);
}, timeout);
}
Task IDnsClient.ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken)
{
return DirectQueryAsync(question);
}
#endregion
#region properties
public string ServerDomain
{
get { return _serverDomain; }
set
{
if (!_serverDomain.Equals(value))
{
if (DnsClient.IsDomainNameUnicode(value))
value = DnsClient.ConvertDomainNameToAscii(value);
DnsClient.IsDomainNameValid(value, true);
if (IPAddress.TryParse(value, out _))
throw new DnsServerException("Invalid domain name [" + value + "]: IP address cannot be used for DNS server domain name.");
_serverDomain = value.ToLowerInvariant();
_defaultResponsiblePerson = new MailAddress("hostadmin@" + _serverDomain);
_authZoneManager.UpdateServerDomain();
_allowedZoneManager.UpdateServerDomain();
_blockedZoneManager.UpdateServerDomain();
_blockListZoneManager.UpdateServerDomain();
UpdateThisServer();
}
}
}
public string ConfigFolder
{ get { return _configFolder; } }
public IReadOnlyList LocalEndPoints
{
get { return _localEndPoints; }
set { _localEndPoints = value; }
}
public LogManager LogManager
{
get { return _log; }
set { _log = value; }
}
internal MailAddress ResponsiblePersonInternal
{
get { return _responsiblePerson; }
set { _responsiblePerson = value; }
}
public MailAddress ResponsiblePerson
{
get
{
if (_responsiblePerson is not null)
return _responsiblePerson;
if (_defaultResponsiblePerson is null)
_defaultResponsiblePerson = new MailAddress("hostadmin@" + _serverDomain);
return _defaultResponsiblePerson;
}
}
public NameServerAddress ThisServer
{ get { return _thisServer; } }
public AuthZoneManager AuthZoneManager
{ get { return _authZoneManager; } }
public AllowedZoneManager AllowedZoneManager
{ get { return _allowedZoneManager; } }
public BlockedZoneManager BlockedZoneManager
{ get { return _blockedZoneManager; } }
public BlockListZoneManager BlockListZoneManager
{ get { return _blockListZoneManager; } }
public CacheZoneManager CacheZoneManager
{ get { return _cacheZoneManager; } }
public DnsApplicationManager DnsApplicationManager
{ get { return _dnsApplicationManager; } }
public IDnsCache DnsCache
{ get { return _dnsCache; } }
public StatsManager StatsManager
{ get { return _stats; } }
public IReadOnlyCollection ZoneTransferAllowedNetworks
{
get { return _zoneTransferAllowedNetworks; }
set
{
if ((value is null) || (value.Count == 0))
_zoneTransferAllowedNetworks = null;
else if (value.Count > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(ZoneTransferAllowedNetworks), "Networks cannot have more than 255 entries.");
else
_zoneTransferAllowedNetworks = value;
}
}
public IReadOnlyCollection NotifyAllowedNetworks
{
get { return _notifyAllowedNetworks; }
set
{
if ((value is null) || (value.Count == 0))
_notifyAllowedNetworks = null;
else if (value.Count > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(NotifyAllowedNetworks), "Networks cannot have more than 255 entries.");
else
_notifyAllowedNetworks = value;
}
}
public bool PreferIPv6
{
get { return _preferIPv6; }
set
{
if (_preferIPv6 != value)
{
_preferIPv6 = value;
//init udp socket pool async for port randomization
ThreadPool.QueueUserWorkItem(delegate (object state)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
UdpClientConnection.CreateSocketPool(_preferIPv6);
});
}
}
}
public ushort UdpPayloadSize
{
get { return _udpPayloadSize; }
set
{
if ((value < 512) || (value > 4096))
throw new ArgumentOutOfRangeException(nameof(UdpPayloadSize), "Invalid EDNS UDP payload size: valid range is 512-4096 bytes.");
_udpPayloadSize = value;
}
}
public bool DnssecValidation
{
get { return _dnssecValidation; }
set
{
if (_dnssecValidation != value)
{
if (!_dnssecValidation)
_cacheZoneManager.Flush(); //flush cache to remove non validated data
_dnssecValidation = value;
}
}
}
public bool EDnsClientSubnet
{
get { return _eDnsClientSubnet; }
set
{
if (_eDnsClientSubnet != value)
{
_eDnsClientSubnet = value;
if (!_eDnsClientSubnet)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
_cacheZoneManager.DeleteEDnsClientSubnetData();
});
}
}
}
}
public byte EDnsClientSubnetIPv4PrefixLength
{
get { return _eDnsClientSubnetIPv4PrefixLength; }
set
{
if (value > 32)
throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv4PrefixLength), "EDNS Client Subnet IPv4 prefix length cannot be greater than 32.");
_eDnsClientSubnetIPv4PrefixLength = value;
}
}
public byte EDnsClientSubnetIPv6PrefixLength
{
get { return _eDnsClientSubnetIPv6PrefixLength; }
set
{
if (value > 64)
throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv6PrefixLength), "EDNS Client Subnet IPv6 prefix length cannot be greater than 64.");
_eDnsClientSubnetIPv6PrefixLength = value;
}
}
public NetworkAddress EDnsClientSubnetIpv4Override
{
get { return _eDnsClientSubnetIpv4Override; }
set
{
if (value is not null)
{
if (value.AddressFamily != AddressFamily.InterNetwork)
throw new ArgumentException("EDNS Client Subnet IPv4 Override must be an IPv4 network address.", nameof(EDnsClientSubnetIpv4Override));
if (value.IsHostAddress)
value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv4PrefixLength);
}
_eDnsClientSubnetIpv4Override = value;
}
}
public NetworkAddress EDnsClientSubnetIpv6Override
{
get { return _eDnsClientSubnetIpv6Override; }
set
{
if (value is not null)
{
if (value.AddressFamily != AddressFamily.InterNetworkV6)
throw new ArgumentException("EDNS Client Subnet IPv6 Override must be an IPv6 network address.", nameof(EDnsClientSubnetIpv6Override));
if (value.IsHostAddress)
value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv6PrefixLength);
}
_eDnsClientSubnetIpv6Override = value;
}
}
public int QpmLimitRequests
{
get { return _qpmLimitRequests; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(QpmLimitRequests), "Value cannot be less than 0.");
if (_qpmLimitRequests != value)
{
if ((_qpmLimitRequests == 0) || (value == 0))
{
_qpmLimitRequests = value;
ResetQpsLimitTimer();
}
else
{
_qpmLimitRequests = value;
}
}
}
}
public int QpmLimitErrors
{
get { return _qpmLimitErrors; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(QpmLimitErrors), "Value cannot be less than 0.");
if (_qpmLimitErrors != value)
{
if ((_qpmLimitErrors == 0) || (value == 0))
{
_qpmLimitErrors = value;
ResetQpsLimitTimer();
}
else
{
_qpmLimitErrors = value;
}
}
}
}
public int QpmLimitSampleMinutes
{
get { return _qpmLimitSampleMinutes; }
set
{
if ((value < 1) || (value > 60))
throw new ArgumentOutOfRangeException(nameof(QpmLimitSampleMinutes), "Valid range is between 1 and 60 minutes.");
_qpmLimitSampleMinutes = value;
}
}
public int QpmLimitIPv4PrefixLength
{
get { return _qpmLimitIPv4PrefixLength; }
set
{
if ((value < 0) || (value > 32))
throw new ArgumentOutOfRangeException(nameof(QpmLimitIPv4PrefixLength), "Valid range is between 0 and 32.");
_qpmLimitIPv4PrefixLength = value;
}
}
public int QpmLimitIPv6PrefixLength
{
get { return _qpmLimitIPv6PrefixLength; }
set
{
if ((value < 0) || (value > 64))
throw new ArgumentOutOfRangeException(nameof(QpmLimitIPv6PrefixLength), "Valid range is between 0 and 64.");
_qpmLimitIPv6PrefixLength = value;
}
}
public IReadOnlyCollection QpmLimitBypassList
{
get { return _qpmLimitBypassList; }
set
{
if ((value is null) || (value.Count == 0))
_qpmLimitBypassList = null;
else if (value.Count > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(QpmLimitBypassList), "Networks cannot have more than 255 entries.");
else
_qpmLimitBypassList = value;
}
}
public int ClientTimeout
{
get { return _clientTimeout; }
set
{
if ((value < 1000) || (value > 10000))
throw new ArgumentOutOfRangeException(nameof(ClientTimeout), "Valid range is from 1000 to 10000.");
_clientTimeout = value;
}
}
public int TcpSendTimeout
{
get { return _tcpSendTimeout; }
set
{
if ((value < 1000) || (value > 90000))
throw new ArgumentOutOfRangeException(nameof(TcpSendTimeout), "Valid range is from 1000 to 90000.");
_tcpSendTimeout = value;
}
}
public int TcpReceiveTimeout
{
get { return _tcpReceiveTimeout; }
set
{
if ((value < 1000) || (value > 90000))
throw new ArgumentOutOfRangeException(nameof(TcpReceiveTimeout), "Valid range is from 1000 to 90000.");
_tcpReceiveTimeout = value;
}
}
public int QuicIdleTimeout
{
get { return _quicIdleTimeout; }
set
{
if ((value < 1000) || (value > 90000))
throw new ArgumentOutOfRangeException(nameof(QuicIdleTimeout), "Valid range is from 1000 to 90000.");
_quicIdleTimeout = value;
}
}
public int QuicMaxInboundStreams
{
get { return _quicMaxInboundStreams; }
set
{
if ((value < 0) || (value > 1000))
throw new ArgumentOutOfRangeException(nameof(QuicMaxInboundStreams), "Valid range is from 1 to 1000.");
_quicMaxInboundStreams = value;
}
}
public int ListenBacklog
{
get { return _listenBacklog; }
set { _listenBacklog = value; }
}
public bool EnableDnsOverUdpProxy
{
get { return _enableDnsOverUdpProxy; }
set { _enableDnsOverUdpProxy = value; }
}
public bool EnableDnsOverTcpProxy
{
get { return _enableDnsOverTcpProxy; }
set { _enableDnsOverTcpProxy = value; }
}
public bool EnableDnsOverHttp
{
get { return _enableDnsOverHttp; }
set { _enableDnsOverHttp = value; }
}
public bool EnableDnsOverTls
{
get { return _enableDnsOverTls; }
set { _enableDnsOverTls = value; }
}
public bool EnableDnsOverHttps
{
get { return _enableDnsOverHttps; }
set { _enableDnsOverHttps = value; }
}
public bool EnableDnsOverHttp3
{
get { return _enableDnsOverHttp3; }
set { _enableDnsOverHttp3 = value; }
}
public bool EnableDnsOverQuic
{
get { return _enableDnsOverQuic; }
set { _enableDnsOverQuic = value; }
}
public int DnsOverUdpProxyPort
{
get { return _dnsOverUdpProxyPort; }
set
{
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
throw new ArgumentOutOfRangeException(nameof(DnsOverUdpProxyPort), "Port number valid range is from 0 to 65535.");
_dnsOverUdpProxyPort = value;
}
}
public int DnsOverTcpProxyPort
{
get { return _dnsOverTcpProxyPort; }
set
{
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
throw new ArgumentOutOfRangeException(nameof(DnsOverTcpProxyPort), "Port number valid range is from 0 to 65535.");
_dnsOverTcpProxyPort = value;
}
}
public int DnsOverHttpPort
{
get { return _dnsOverHttpPort; }
set
{
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), "Port number valid range is from 0 to 65535.");
_dnsOverHttpPort = value;
}
}
public int DnsOverTlsPort
{
get { return _dnsOverTlsPort; }
set
{
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
throw new ArgumentOutOfRangeException(nameof(DnsOverTlsPort), "Port number valid range is from 0 to 65535.");
_dnsOverTlsPort = value;
}
}
public int DnsOverHttpsPort
{
get { return _dnsOverHttpsPort; }
set
{
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), "Port number valid range is from 0 to 65535.");
_dnsOverHttpsPort = value;
}
}
public int DnsOverQuicPort
{
get { return _dnsOverQuicPort; }
set
{
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
throw new ArgumentOutOfRangeException(nameof(DnsOverQuicPort), "Port number valid range is from 0 to 65535.");
_dnsOverQuicPort = value;
}
}
public X509Certificate2Collection CertificateCollection
{
get { return _certificateCollection; }
set
{
if (value is null)
{
_certificateCollection = null;
_sslServerAuthenticationOptions = null;
_quicSslServerAuthenticationOptions = null;
}
else
{
X509Certificate2 serverCertificate = null;
foreach (X509Certificate2 certificate in value)
{
if (certificate.HasPrivateKey)
{
serverCertificate = certificate;
break;
}
}
if (serverCertificate is null)
throw new ArgumentException("DNS Server TLS certificate file must contain a certificate with private key.");
_certificateCollection = value;
SslStreamCertificateContext certificateContext = SslStreamCertificateContext.Create(serverCertificate, _certificateCollection, false);
_sslServerAuthenticationOptions = new SslServerAuthenticationOptions()
{
ServerCertificateContext = certificateContext
};
_quicSslServerAuthenticationOptions = new SslServerAuthenticationOptions()
{
ApplicationProtocols = quicApplicationProtocols,
ServerCertificateContext = certificateContext
};
}
}
}
public string DnsOverHttpRealIpHeader
{
get { return _dnsOverHttpRealIpHeader; }
set
{
if (string.IsNullOrEmpty(value))
_dnsOverHttpRealIpHeader = "X-Real-IP";
else if (value.Length > 255)
throw new ArgumentException("DNS-over-HTTP Real IP header name cannot exceed 255 characters.", nameof(DnsOverHttpRealIpHeader));
else if (value.Contains(' '))
throw new ArgumentException("DNS-over-HTTP Real IP header name cannot contain invalid characters.", nameof(DnsOverHttpRealIpHeader));
else
_dnsOverHttpRealIpHeader = value;
}
}
public IReadOnlyDictionary TsigKeys
{
get { return _tsigKeys; }
set
{
if ((value is null) || (value.Count == 0))
_tsigKeys = null;
else if (value.Count > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(TsigKeys), "TSIG keys cannot have more than 255 entries.");
else
_tsigKeys = value;
}
}
public DnsServerRecursion Recursion
{
get { return _recursion; }
set
{
if (_recursion != value)
{
if ((_recursion == DnsServerRecursion.Deny) || (value == DnsServerRecursion.Deny))
{
_recursion = value;
ResetPrefetchTimers();
}
else
{
_recursion = value;
}
}
}
}
public IReadOnlyCollection RecursionNetworkACL
{
get { return _recursionNetworkACL; }
set
{
if ((value is null) || (value.Count == 0))
_recursionNetworkACL = null;
else if (value.Count > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(RecursionNetworkACL), "Network Access Control List cannot have more than 255 entries.");
else
_recursionNetworkACL = value;
}
}
public bool RandomizeName
{
get { return _randomizeName; }
set { _randomizeName = value; }
}
public bool QnameMinimization
{
get { return _qnameMinimization; }
set { _qnameMinimization = value; }
}
public bool NsRevalidation
{
get { return _nsRevalidation; }
set { _nsRevalidation = value; }
}
public int ResolverRetries
{
get { return _resolverRetries; }
set
{
if ((value < 1) || (value > 10))
throw new ArgumentOutOfRangeException(nameof(ResolverRetries), "Valid range is from 1 to 10.");
_resolverRetries = value;
}
}
public int ResolverTimeout
{
get { return _resolverTimeout; }
set
{
if ((value < 1000) || (value > 10000))
throw new ArgumentOutOfRangeException(nameof(ResolverTimeout), "Valid range is from 1000 to 10000.");
_resolverTimeout = value;
}
}
public int ResolverConcurrency
{
get { return _resolverConcurrency; }
set
{
if ((value < 1) || (value > 4))
throw new ArgumentOutOfRangeException(nameof(ResolverConcurrency), "Valid range is from 1 to 4.");
_resolverConcurrency = value;
}
}
public int ResolverMaxStackCount
{
get { return _resolverMaxStackCount; }
set
{
if ((value < 10) || (value > 30))
throw new ArgumentOutOfRangeException(nameof(ResolverMaxStackCount), "Valid range is from 10 to 30.");
_resolverMaxStackCount = value;
}
}
public bool ServeStale
{
get { return _serveStale; }
set { _serveStale = value; }
}
public int ServeStaleMaxWaitTime
{
get { return _serveStaleMaxWaitTime; }
set
{
if ((value < 0) || (value > 1800))
throw new ArgumentOutOfRangeException(nameof(ServeStaleMaxWaitTime), "Serve stale max wait time valid range is 0 to 1800 milliseconds. Default value is 1800 milliseconds.");
_serveStaleMaxWaitTime = value;
}
}
public int CachePrefetchEligibility
{
get { return _cachePrefetchEligibility; }
set
{
if (value < 2)
throw new ArgumentOutOfRangeException(nameof(CachePrefetchEligibility), "Valid value is greater that or equal to 2.");
_cachePrefetchEligibility = value;
}
}
public int CachePrefetchTrigger
{
get { return _cachePrefetchTrigger; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(CachePrefetchTrigger), "Valid value is greater that or equal to 0.");
if (_cachePrefetchTrigger != value)
{
if ((_cachePrefetchTrigger == 0) || (value == 0))
{
_cachePrefetchTrigger = value;
ResetPrefetchTimers();
}
else
{
_cachePrefetchTrigger = value;
}
}
}
}
public int CachePrefetchSampleIntervalInMinutes
{
get { return _cachePrefetchSampleIntervalInMinutes; }
set
{
if ((value < 1) || (value > 60))
throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleIntervalInMinutes), "Valid range is between 1 and 60 minutes.");
_cachePrefetchSampleIntervalInMinutes = value;
}
}
public int CachePrefetchSampleEligibilityHitsPerHour
{
get { return _cachePrefetchSampleEligibilityHitsPerHour; }
set
{
if (value < 1)
throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleEligibilityHitsPerHour), "Valid value is greater than or equal to 1.");
_cachePrefetchSampleEligibilityHitsPerHour = value;
}
}
public bool EnableBlocking
{
get { return _enableBlocking; }
set { _enableBlocking = value; }
}
public bool AllowTxtBlockingReport
{
get { return _allowTxtBlockingReport; }
set { _allowTxtBlockingReport = value; }
}
public IReadOnlyCollection BlockingBypassList
{
get { return _blockingBypassList; }
set
{
if ((value is null) || (value.Count == 0))
_blockingBypassList = null;
else if (value.Count > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(BlockingBypassList), "Networks cannot have more than 255 entries.");
else
_blockingBypassList = value;
}
}
public DnsServerBlockingType BlockingType
{
get { return _blockingType; }
set { _blockingType = value; }
}
public uint BlockingAnswerTtl
{
get { return _blockingAnswerTtl; }
set
{
if (_blockingAnswerTtl != value)
{
_blockingAnswerTtl = value;
//update SOA MINIMUM values
_blockedZoneManager.UpdateServerDomain();
_blockListZoneManager.UpdateServerDomain();
}
}
}
public IReadOnlyCollection CustomBlockingARecords
{
get { return _customBlockingARecords; }
set
{
if (value is null)
value = Array.Empty();
_customBlockingARecords = value;
}
}
public IReadOnlyCollection CustomBlockingAAAARecords
{
get { return _customBlockingAAAARecords; }
set
{
if (value is null)
value = Array.Empty();
_customBlockingAAAARecords = value;
}
}
public NetProxy Proxy
{
get { return _proxy; }
set { _proxy = value; }
}
public IReadOnlyList Forwarders
{
get { return _forwarders; }
set { _forwarders = value; }
}
public bool ConcurrentForwarding
{
get { return _concurrentForwarding; }
set { _concurrentForwarding = value; }
}
public int ForwarderRetries
{
get { return _forwarderRetries; }
set
{
if ((value < 1) || (value > 10))
throw new ArgumentOutOfRangeException(nameof(ForwarderRetries), "Valid range is from 1 to 10.");
_forwarderRetries = value;
}
}
public int ForwarderTimeout
{
get { return _forwarderTimeout; }
set
{
if ((value < 1000) || (value > 10000))
throw new ArgumentOutOfRangeException(nameof(ForwarderTimeout), "Valid range is from 1000 to 10000.");
_forwarderTimeout = value;
}
}
public int ForwarderConcurrency
{
get { return _forwarderConcurrency; }
set
{
if ((value < 1) || (value > 10))
throw new ArgumentOutOfRangeException(nameof(ForwarderConcurrency), "Valid range is from 1 to 10.");
_forwarderConcurrency = value;
}
}
public LogManager ResolverLogManager
{
get { return _resolverLog; }
set { _resolverLog = value; }
}
public LogManager QueryLogManager
{
get { return _queryLog; }
set { _queryLog = value; }
}
#endregion
class CacheRefreshSample
{
public CacheRefreshSample(DnsQuestionRecord sampleQuestion, IReadOnlyList conditionalForwarders)
{
SampleQuestion = sampleQuestion;
ConditionalForwarders = conditionalForwarders;
}
public DnsQuestionRecord SampleQuestion { get; }
public IReadOnlyList ConditionalForwarders { get; }
}
class RecursiveResolveResponse
{
public RecursiveResolveResponse(DnsDatagram response, DnsDatagram checkingDisabledResponse)
{
Response = response;
CheckingDisabledResponse = checkingDisabledResponse;
}
public DnsDatagram Response { get; }
public DnsDatagram CheckingDisabledResponse { get; }
}
}
#pragma warning restore CA2252 // This API requires opting into preview features
#pragma warning restore CA1416 // Validate platform compatibility
}