/*
Technitium DNS Server
Copyright (C) 2020 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.Dns.ZoneManagers;
using DnsServerCore.Dns.Zones;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary.IO;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
using TechnitiumLibrary.Net.Http;
using TechnitiumLibrary.Net.Proxy;
namespace DnsServerCore.Dns
{
public sealed class DnsServer : IDisposable
{
#region enum
enum ServiceState
{
Stopped = 0,
Starting = 1,
Running = 2,
Stopping = 3
}
#endregion
#region variables
const int MAX_CNAME_HOPS = 16;
const int SERVE_STALE_WAIT_TIME = 1800;
string _serverDomain;
readonly string _configFolder;
readonly string _dohwwwFolder;
IReadOnlyList _localEndPoints;
LogManager _log;
NameServerAddress _thisServer;
readonly List _udpListeners = new List();
readonly List _tcpListeners = new List();
readonly List _httpListeners = new List();
readonly List _tlsListeners = new List();
readonly List _httpsListeners = new List();
bool _enableDnsOverHttp;
bool _enableDnsOverTls;
bool _enableDnsOverHttps;
bool _isDnsOverHttpsEnabled;
X509Certificate2 _certificate;
readonly AuthZoneManager _authZoneManager;
readonly AllowedZoneManager _allowedZoneManager;
readonly BlockedZoneManager _blockedZoneManager;
readonly BlockListZoneManager _blockListZoneManager;
readonly CacheZoneManager _cacheZoneManager = new CacheZoneManager();
readonly ResolverDnsCache _dnsCache;
readonly DnsARecord _aRecord = new DnsARecord(IPAddress.Any);
readonly DnsAAAARecord _aaaaRecord = new DnsAAAARecord(IPAddress.IPv6Any);
bool _allowRecursion;
bool _allowRecursionOnlyForPrivateNetworks;
NetProxy _proxy;
IReadOnlyList _forwarders;
bool _preferIPv6;
int _forwarderRetries = 3;
int _resolverRetries = 5;
int _forwarderTimeout = 4000;
int _resolverTimeout = 4000;
int _clientTimeout = 4000;
int _forwarderConcurrency = 2;
int _resolverMaxStackCount = 10;
int _cachePrefetchEligibility = 2;
int _cachePrefetchTrigger = 9;
int _cachePrefetchSampleIntervalInMinutes = 5;
int _cachePrefetchSampleEligibilityHitsPerHour = 30;
LogManager _queryLog;
readonly StatsManager _stats;
int _tcpSendTimeout = 10000;
int _tcpReceiveTimeout = 10000;
Timer _cachePrefetchSamplingTimer;
readonly object _cachePrefetchSamplingTimerLock = new object();
Timer _cachePrefetchRefreshTimer;
readonly object _cachePrefetchRefreshTimerLock = new object();
const int CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL = 60000;
DateTime _cachePrefetchSamplingTimerTriggersOn;
IList _cachePrefetchSampleList;
Timer _cacheMaintenanceTimer;
const int CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL = 60 * 60 * 1000;
const int CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000;
readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(ThreadPriority.AboveNormal);
readonly DomainTree> _resolverTasks = new DomainTree>();
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 * 256;
if (minWorker < minThreads)
minWorker = minThreads;
if (minIOC < minThreads)
minIOC = minThreads;
ThreadPool.SetMinThreads(minWorker, minIOC);
}
if (ServicePointManager.DefaultConnectionLimit < 10)
ServicePointManager.DefaultConnectionLimit = 10; //concurrent http request limit required when using DNS-over-HTTPS forwarders
}
public DnsServer(string configFolder, string dohwwwFolder, LogManager log = null)
: this(Environment.MachineName.ToLower(), configFolder, dohwwwFolder, log)
{ }
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);
_dnsCache = new ResolverDnsCache(_authZoneManager, _cacheZoneManager);
//init stats
string statsFolder = Path.Combine(_configFolder, "stats");
if (!Directory.Exists(statsFolder))
Directory.CreateDirectory(statsFolder);
_stats = new StatsManager(statsFolder, _log);
}
#endregion
#region IDisposable
bool _disposed;
private void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
Stop();
if (_authZoneManager != null)
_authZoneManager.Dispose();
if (_stats != null)
_stats.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
}
#endregion
#region private
private void ReadUdpRequestAsync(object parameter)
{
Socket udpListener = parameter as Socket;
EndPoint remoteEP;
byte[] recvBuffer = new byte[512];
int bytesRecv;
if (udpListener.AddressFamily == AddressFamily.InterNetwork)
remoteEP = new IPEndPoint(IPAddress.Any, 0);
else
remoteEP = new IPEndPoint(IPAddress.IPv6Any, 0);
try
{
while (true)
{
try
{
bytesRecv = udpListener.ReceiveFrom(recvBuffer, ref remoteEP);
}
catch (SocketException ex)
{
switch (ex.SocketErrorCode)
{
case SocketError.ConnectionReset:
case SocketError.HostUnreachable:
case SocketError.MessageSize:
case SocketError.NetworkReset:
bytesRecv = 0;
break;
default:
throw;
}
}
if (bytesRecv > 0)
{
try
{
DnsDatagram request = DnsDatagram.ReadFromUdp(new MemoryStream(recvBuffer, 0, bytesRecv, false));
_ = ProcessUdpRequestAsync(udpListener, remoteEP as IPEndPoint, request);
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(remoteEP as IPEndPoint, DnsTransportProtocol.Udp, ex);
}
}
}
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
LogManager log = _log;
if (log != null)
log.Write(remoteEP as IPEndPoint, DnsTransportProtocol.Udp, ex);
throw;
}
}
private async Task ProcessUdpRequestAsync(Socket udpListener, IPEndPoint remoteEP, DnsDatagram request)
{
try
{
DnsDatagram response;
if (request.ParsingException == null)
{
response = await ProcessQueryAsync(request, remoteEP, IsRecursionAllowed(remoteEP), DnsTransportProtocol.Udp);
}
else
{
//format error
LogManager log = _log;
if (log != null)
log.Write(remoteEP, DnsTransportProtocol.Udp, request.ParsingException);
//format error response
response = new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, IsRecursionAllowed(remoteEP), false, false, DnsResponseCode.FormatError, request.Question);
}
//send response
if (response != null)
{
byte[] sendBuffer = new byte[512];
MemoryStream sendBufferStream = new MemoryStream(sendBuffer);
try
{
response.WriteToUdp(sendBufferStream);
}
catch (NotSupportedException)
{
response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question);
sendBufferStream.Position = 0;
response.WriteToUdp(sendBufferStream);
}
//send dns datagram async
await udpListener.SendToAsync(sendBuffer, 0, (int)sendBufferStream.Position, remoteEP);
LogManager queryLog = _queryLog;
if (queryLog != null)
queryLog.Write(remoteEP, DnsTransportProtocol.Udp, request, response);
StatsManager stats = _stats;
if (stats != null)
stats.Update(response, remoteEP.Address);
}
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
LogManager queryLog = _queryLog;
if (queryLog != null)
queryLog.Write(remoteEP, DnsTransportProtocol.Udp, request, null);
LogManager log = _log;
if (log != null)
log.Write(remoteEP, DnsTransportProtocol.Udp, ex);
}
}
private void AcceptConnectionAsync(object parameter)
{
object[] parameters = parameter as object[];
Socket tcpListener = parameters[0] as Socket;
DnsTransportProtocol protocol = (DnsTransportProtocol)parameters[1];
bool usingHttps = true;
if (parameters.Length > 2)
usingHttps = (bool)parameters[2];
IPEndPoint localEP = tcpListener.LocalEndPoint as IPEndPoint;
try
{
tcpListener.SendTimeout = _tcpSendTimeout;
tcpListener.ReceiveTimeout = _tcpReceiveTimeout;
tcpListener.SendBufferSize = 2048;
tcpListener.ReceiveBufferSize = 512;
tcpListener.NoDelay = true;
while (true)
{
Socket socket = tcpListener.Accept();
_ = ProcessConnectionAsync(socket, protocol, usingHttps);
}
}
catch (Exception ex)
{
if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
return; //server stopping
LogManager log = _log;
if (log != null)
log.Write(localEP, protocol, ex);
throw;
}
}
private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol, bool usingHttps)
{
IPEndPoint remoteEP = null;
try
{
remoteEP = socket.RemoteEndPoint as IPEndPoint;
switch (protocol)
{
case DnsTransportProtocol.Tcp:
await ReadStreamRequestAsync(new NetworkStream(socket), _tcpReceiveTimeout, remoteEP, protocol);
break;
case DnsTransportProtocol.Tls:
SslStream tlsStream = new SslStream(new NetworkStream(socket));
await tlsStream.AuthenticateAsServerAsync(_certificate);
await ReadStreamRequestAsync(tlsStream, _tcpReceiveTimeout, remoteEP, protocol);
break;
case DnsTransportProtocol.Https:
Stream stream = new NetworkStream(socket);
if (usingHttps)
{
SslStream httpsStream = new SslStream(stream);
await httpsStream.AuthenticateAsServerAsync(_certificate);
stream = httpsStream;
}
await ProcessDoHRequestAsync(stream, _tcpReceiveTimeout, remoteEP, usingHttps);
break;
}
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(remoteEP, protocol, ex);
}
finally
{
if (socket != null)
socket.Dispose();
}
}
private async Task ReadStreamRequestAsync(Stream stream, int receiveTimeout, IPEndPoint remoteEP, DnsTransportProtocol protocol)
{
try
{
MemoryStream readBuffer = new MemoryStream(64);
MemoryStream writeBuffer = new MemoryStream(64);
SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1);
while (true)
{
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(receiveTimeout, cancellationTokenSource.Token)) != task)
{
//read timed out
stream.Dispose();
return;
}
cancellationTokenSource.Cancel(); //cancel delay task
request = await task;
}
//process request async
_ = ProcessStreamRequestAsync(stream, writeBuffer, writeSemaphore, remoteEP, request, protocol);
}
}
catch (ObjectDisposedException)
{
//ignore
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(remoteEP, protocol, ex);
}
}
private async Task ProcessStreamRequestAsync(Stream stream, MemoryStream writeBuffer, SemaphoreSlim writeSemaphore, IPEndPoint remoteEP, DnsDatagram request, DnsTransportProtocol protocol)
{
try
{
DnsDatagram response;
if (request.ParsingException == null)
{
response = await ProcessQueryAsync(request, remoteEP, IsRecursionAllowed(remoteEP), protocol);
}
else
{
//format error
response = new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, IsRecursionAllowed(remoteEP), false, false, DnsResponseCode.FormatError, request.Question);
LogManager queryLog = _queryLog;
if (queryLog != null)
queryLog.Write(remoteEP, protocol, request, response);
LogManager log = _log;
if (log != null)
log.Write(remoteEP, protocol, request.ParsingException);
}
//send response
if (response != null)
{
await writeSemaphore.WaitAsync();
try
{
//send dns datagram
await response.WriteToTcpAsync(stream, writeBuffer);
await stream.FlushAsync();
}
finally
{
writeSemaphore.Release();
}
LogManager queryLog = _queryLog;
if (queryLog != null)
queryLog.Write(remoteEP, protocol, request, response);
StatsManager stats = _stats;
if (stats != null)
stats.Update(response, remoteEP.Address);
}
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
LogManager queryLog = _queryLog;
if ((queryLog != null) && (request != null))
queryLog.Write(remoteEP, protocol, request, null);
LogManager log = _log;
if (log != null)
log.Write(remoteEP, protocol, ex);
}
}
private async Task ProcessDoHRequestAsync(Stream stream, int receiveTimeout, IPEndPoint remoteEP, bool usingHttps)
{
DnsDatagram dnsRequest = null;
DnsTransportProtocol dnsProtocol = DnsTransportProtocol.Https;
try
{
while (true)
{
HttpRequest httpRequest = await HttpRequest.ReadRequestAsync(stream).WithTimeout(receiveTimeout);
if (httpRequest == null)
return; //connection closed gracefully by client
IPEndPoint socketRemoteEP = remoteEP;
if (!usingHttps && NetUtilities.IsPrivateIP(socketRemoteEP.Address))
{
string xRealIp = httpRequest.Headers["X-Real-IP"];
if (IPAddress.TryParse(xRealIp, out IPAddress address))
{
//get the real IP address of the requesting client from X-Real-IP header set in nginx proxy_pass block
remoteEP = new IPEndPoint(address, 0);
}
}
string requestConnection = httpRequest.Headers[HttpRequestHeader.Connection];
if (string.IsNullOrEmpty(requestConnection))
requestConnection = "close";
switch (httpRequest.RequestPath)
{
case "/dns-query":
if (!usingHttps && !NetUtilities.IsPrivateIP(socketRemoteEP.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
await SendErrorAsync(stream, 403, "DNS-over-HTTPS (DoH) queries are supported only on HTTPS.");
return;
}
DnsTransportProtocol protocol = DnsTransportProtocol.Udp;
string strRequestAcceptTypes = httpRequest.Headers[HttpRequestHeader.Accept];
if (!string.IsNullOrEmpty(strRequestAcceptTypes))
{
foreach (string acceptType in strRequestAcceptTypes.Split(','))
{
if (acceptType == "application/dns-message")
{
protocol = DnsTransportProtocol.Https;
break;
}
else if (acceptType == "application/dns-json")
{
protocol = DnsTransportProtocol.HttpsJson;
dnsProtocol = DnsTransportProtocol.HttpsJson;
break;
}
}
}
switch (protocol)
{
case DnsTransportProtocol.Https:
#region https wire format
{
switch (httpRequest.HttpMethod)
{
case "GET":
string strRequest = httpRequest.QueryString["dns"];
if (string.IsNullOrEmpty(strRequest))
throw new ArgumentNullException("dns");
//convert from base64url to base64
strRequest = strRequest.Replace('-', '+');
strRequest = strRequest.Replace('_', '/');
//add padding
int x = strRequest.Length % 4;
if (x > 0)
strRequest = strRequest.PadRight(strRequest.Length - x + 4, '=');
dnsRequest = DnsDatagram.ReadFromUdp(new MemoryStream(Convert.FromBase64String(strRequest)));
break;
case "POST":
string strContentType = httpRequest.Headers[HttpRequestHeader.ContentType];
if (string.IsNullOrEmpty(strContentType))
throw new DnsServerException("Missing Content-Type header.");
if (strContentType != "application/dns-message")
throw new NotSupportedException("DNS request type not supported: " + strContentType);
using (MemoryStream mS = new MemoryStream())
{
await httpRequest.InputStream.CopyToAsync(mS, 512);
mS.Position = 0;
dnsRequest = DnsDatagram.ReadFromUdp(mS);
}
break;
default:
throw new NotSupportedException("DoH request type not supported.");
}
DnsDatagram dnsResponse;
if (dnsRequest.ParsingException == null)
{
dnsResponse = await ProcessQueryAsync(dnsRequest, remoteEP, IsRecursionAllowed(remoteEP), protocol);
}
else
{
//format error
LogManager log = _log;
if (log != null)
log.Write(remoteEP, protocol, dnsRequest.ParsingException);
//format error response
dnsResponse = new DnsDatagram(dnsRequest.Identifier, true, dnsRequest.OPCODE, false, false, dnsRequest.RecursionDesired, IsRecursionAllowed(remoteEP), false, false, DnsResponseCode.FormatError, dnsRequest.Question);
}
if (dnsResponse != null)
{
using (MemoryStream mS = new MemoryStream())
{
dnsResponse.WriteToUdp(mS);
byte[] buffer = mS.ToArray();
await SendContentAsync(stream, "application/dns-message", buffer);
}
LogManager queryLog = _queryLog;
if (queryLog != null)
queryLog.Write(remoteEP, protocol, dnsRequest, dnsResponse);
StatsManager stats = _stats;
if (stats != null)
stats.Update(dnsResponse, remoteEP.Address);
}
}
#endregion
break;
case DnsTransportProtocol.HttpsJson:
#region https json format
{
string strName = httpRequest.QueryString["name"];
if (string.IsNullOrEmpty(strName))
throw new ArgumentNullException("name");
string strType = httpRequest.QueryString["type"];
if (string.IsNullOrEmpty(strType))
strType = "1";
dnsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(strName, (DnsResourceRecordType)int.Parse(strType), DnsClass.IN) });
DnsDatagram dnsResponse = await ProcessQueryAsync(dnsRequest, remoteEP, IsRecursionAllowed(remoteEP), protocol);
if (dnsResponse != null)
{
using (MemoryStream mS = new MemoryStream())
{
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
dnsResponse.WriteToJson(jsonWriter);
jsonWriter.Flush();
byte[] buffer = mS.ToArray();
await SendContentAsync(stream, "application/dns-json; charset=utf-8", buffer);
}
LogManager queryLog = _queryLog;
if (queryLog != null)
queryLog.Write(remoteEP, protocol, dnsRequest, dnsResponse);
StatsManager stats = _stats;
if (stats != null)
stats.Update(dnsResponse, remoteEP.Address);
}
}
#endregion
break;
default:
await SendErrorAsync(stream, 406, "Only application/dns-message and application/dns-json types are accepted.");
break;
}
if (requestConnection.Equals("close", StringComparison.OrdinalIgnoreCase))
return;
break;
default:
string path = httpRequest.RequestPath;
if (!path.StartsWith("/") || path.Contains("/../") || path.Contains("/.../"))
{
await SendErrorAsync(stream, 404);
break;
}
if (path == "/")
path = "/index.html";
path = Path.GetFullPath(_dohwwwFolder + path.Replace('/', Path.DirectorySeparatorChar));
if (!path.StartsWith(_dohwwwFolder) || !File.Exists(path))
{
await SendErrorAsync(stream, 404);
break;
}
await SendFileAsync(stream, path);
break;
}
}
}
catch (TimeoutException)
{
//ignore timeout exception
}
catch (IOException)
{
//ignore IO exceptions
}
catch (Exception ex)
{
LogManager queryLog = _queryLog;
if ((queryLog != null) && (dnsRequest != null))
queryLog.Write(remoteEP, dnsProtocol, dnsRequest, null);
LogManager log = _log;
if (log != null)
log.Write(remoteEP, dnsProtocol, ex);
await SendErrorAsync(stream, ex);
}
}
private static async Task SendContentAsync(Stream outputStream, string contentType, byte[] bufferContent)
{
byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: " + contentType + "\r\nContent-Length: " + bufferContent.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\n\r\n");
await outputStream.WriteAsync(bufferHeader, 0, bufferHeader.Length);
await outputStream.WriteAsync(bufferContent, 0, bufferContent.Length);
await outputStream.FlushAsync();
}
private static Task SendErrorAsync(Stream outputStream, Exception ex)
{
return SendErrorAsync(outputStream, 500, ex.ToString());
}
private static async Task SendErrorAsync(Stream outputStream, int statusCode, string message = null)
{
try
{
string statusString = statusCode + " " + GetHttpStatusString((HttpStatusCode)statusCode);
byte[] bufferContent = Encoding.UTF8.GetBytes("" + statusString + "" + statusString + "
" + (message == null ? "" : "" + message + "
") + "");
byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 " + statusString + "\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " + bufferContent.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\n\r\n");
await outputStream.WriteAsync(bufferHeader, 0, bufferHeader.Length);
await outputStream.WriteAsync(bufferContent, 0, bufferContent.Length);
await outputStream.FlushAsync();
}
catch
{ }
}
private static async Task SendFileAsync(Stream outputStream, string filePath)
{
using (FileStream fS = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: " + WebUtilities.GetContentType(filePath).MediaType + "\r\nContent-Length: " + fS.Length + "\r\nCache-Control: private, max-age=300\r\nX-Robots-Tag: noindex, nofollow\r\n\r\n");
await outputStream.WriteAsync(bufferHeader, 0, bufferHeader.Length);
await fS.CopyToAsync(outputStream);
await outputStream.FlushAsync();
}
}
internal static string GetHttpStatusString(HttpStatusCode statusCode)
{
StringBuilder sb = new StringBuilder();
foreach (char c in statusCode.ToString().ToCharArray())
{
if (char.IsUpper(c) && sb.Length > 0)
sb.Append(' ');
sb.Append(c);
}
return sb.ToString();
}
private bool IsRecursionAllowed(IPEndPoint remoteEP)
{
if (!_allowRecursion)
return false;
if (_allowRecursionOnlyForPrivateNetworks)
{
switch (remoteEP.AddressFamily)
{
case AddressFamily.InterNetwork:
case AddressFamily.InterNetworkV6:
return NetUtilities.IsPrivateIP(remoteEP.Address);
default:
return false;
}
}
return true;
}
private async Task ProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, bool isRecursionAllowed, DnsTransportProtocol protocol)
{
if (request.IsResponse)
return null;
switch (request.OPCODE)
{
case DnsOpcode.StandardQuery:
if ((request.Question.Count != 1) || (request.Question[0].Class != DnsClass.IN))
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.Refused, request.Question);
try
{
switch (request.Question[0].Type)
{
case DnsResourceRecordType.AXFR:
if (protocol == DnsTransportProtocol.Udp)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question);
return await ProcessZoneTransferQueryAsync(request, remoteEP);
case DnsResourceRecordType.IXFR:
return await ProcessZoneTransferQueryAsync(request, remoteEP);
case DnsResourceRecordType.MAILB:
case DnsResourceRecordType.MAILA:
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NotImplemented, request.Question);
default:
DnsDatagram response;
//check in allowed zone
bool inAllowedZone = _allowedZoneManager.Query(request).RCODE != DnsResponseCode.Refused;
if (!inAllowedZone)
{
//check in blocked zone and block list zone
response = ProcessBlockedQuery(request);
if (response != null)
return response;
}
//query authoritative zone
response = await ProcessAuthoritativeQueryAsync(request, inAllowedZone, isRecursionAllowed);
if ((response.RCODE != DnsResponseCode.Refused) || !request.RecursionDesired || !isRecursionAllowed)
return response;
//do recursive query
return await ProcessRecursiveQueryAsync(request, null, null, !inAllowedZone, false);
}
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(remoteEP, protocol, ex);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.ServerFailure, request.Question);
}
case DnsOpcode.Notify:
return await ProcessNotifyQueryAsync(request, remoteEP);
default:
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NotImplemented, request.Question);
}
}
private async Task ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP)
{
AuthZoneInfo authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name);
if ((authZoneInfo == null) || (authZoneInfo.Type != AuthZoneType.Secondary))
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = StatsResponseType.Authoritative };
IPAddress remoteAddress = remoteEP.Address;
bool remoteVerified = false;
IReadOnlyList primaryNameServers = await authZoneInfo.GetPrimaryNameServerAddressesAsync(this);
foreach (NameServerAddress primaryNameServer in primaryNameServers)
{
if (primaryNameServer.IPEndPoint.Address.Equals(remoteAddress))
{
remoteVerified = true;
break;
}
}
if (!remoteVerified)
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = StatsResponseType.Authoritative };
LogManager log = _log;
if (log != null)
log.Write(remoteEP, "DNS Server received NOTIFY for zone: " + authZoneInfo.Name);
if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA))
{
IReadOnlyList localSoaRecords = authZoneInfo.GetRecords(DnsResourceRecordType.SOA);
if (!DnsSOARecord.IsZoneUpdateAvailable((localSoaRecords[0].RDATA as DnsSOARecord).Serial, (request.Answer[0].RDATA as DnsSOARecord).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 = StatsResponseType.Authoritative };
}
}
authZoneInfo.RefreshZone();
return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = StatsResponseType.Authoritative };
}
private async Task ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP)
{
AuthZoneInfo authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name);
if ((authZoneInfo == null) || (authZoneInfo.Type != AuthZoneType.Primary))
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = StatsResponseType.Authoritative };
IPAddress remoteAddress = remoteEP.Address;
bool isAxfrAllowed = IPAddress.IsLoopback(remoteAddress);
if (!isAxfrAllowed)
{
IReadOnlyList secondaryNameServers = await authZoneInfo.GetSecondaryNameServerAddressesAsync(this);
foreach (NameServerAddress secondaryNameServer in secondaryNameServers)
{
if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))
{
isAxfrAllowed = true;
break;
}
}
}
if (!isAxfrAllowed)
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = StatsResponseType.Authoritative };
LogManager log = _log;
if (log != null)
log.Write(remoteEP, "DNS Server received zone transfer request for zone: " + authZoneInfo.Name);
IReadOnlyList axfrRecords = _authZoneManager.QueryZoneTransferRecords(request.Question[0].Name);
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, axfrRecords) { Tag = StatsResponseType.Authoritative };
}
private Task ProcessAuthoritativeQueryAsync(DnsDatagram request, bool inAllowedZone, bool isRecursionAllowed)
{
DnsDatagram response = _authZoneManager.Query(request);
response.Tag = StatsResponseType.Authoritative;
if (response.RCODE == DnsResponseCode.NoError)
{
if (response.Answer.Count > 0)
{
DnsResourceRecordType questionType = request.Question[0].Type;
DnsResourceRecord lastRR = response.Answer[response.Answer.Count - 1];
if ((lastRR.Type != questionType) && (questionType != DnsResourceRecordType.ANY))
{
switch (lastRR.Type)
{
case DnsResourceRecordType.CNAME:
return ProcessCNAMEAsync(request, response, isRecursionAllowed, false);
case DnsResourceRecordType.ANAME:
return ProcessANAMEAsync(request, response, isRecursionAllowed);
}
}
}
else if (response.Authority.Count > 0)
{
switch (response.Authority[0].Type)
{
case DnsResourceRecordType.NS:
if (request.RecursionDesired && isRecursionAllowed)
{
//do recursive resolution using response authority name servers
List nameServers = NameServerAddress.GetNameServersFromResponse(response, _preferIPv6, false);
return ProcessRecursiveQueryAsync(request, nameServers, null, !inAllowedZone, false);
}
break;
case DnsResourceRecordType.FWD:
if ((response.Authority.Count == 1) && (response.Authority[0].Type == DnsResourceRecordType.FWD) && (response.Authority[0].RDATA as DnsForwarderRecord).Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
{
//do conditional forwarding via "this-server"
return ProcessRecursiveQueryAsync(request, null, null, !inAllowedZone, false);
}
else
{
//do conditional forwarding
List forwarders = new List(response.Authority.Count);
foreach (DnsResourceRecord rr in response.Authority)
{
if (rr.Type == DnsResourceRecordType.FWD)
{
DnsForwarderRecord fwd = rr.RDATA as DnsForwarderRecord;
if (!fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
forwarders.Add(fwd.NameServer);
}
}
return ProcessRecursiveQueryAsync(request, null, forwarders, !inAllowedZone, false);
}
}
}
}
return Task.FromResult(response);
}
private async Task ProcessCNAMEAsync(DnsDatagram request, DnsDatagram response, bool isRecursionAllowed, bool cacheRefreshOperation)
{
List responseAnswer = new List();
responseAnswer.AddRange(response.Answer);
DnsDatagram lastResponse;
bool isAuthoritativeAnswer = response.AuthoritativeAnswer;
string lastDomain = (response.Answer[response.Answer.Count - 1].RDATA as DnsCNAMERecord).Domain;
int queryCount = 0;
do
{
DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(lastDomain, request.Question[0].Type, request.Question[0].Class) });
//query authoritative zone first
lastResponse = _authZoneManager.Query(newRequest);
if (lastResponse.RCODE == DnsResponseCode.Refused)
{
//not found in auth zone
if (newRequest.RecursionDesired && isRecursionAllowed)
{
//do recursion
lastResponse = await RecursiveResolveAsync(newRequest, null, null, false, cacheRefreshOperation);
isAuthoritativeAnswer = false;
}
else
{
//break since no recursion allowed/desired
break;
}
}
else if ((lastResponse.Answer.Count > 0) && (lastResponse.Answer[0].Type == DnsResourceRecordType.ANAME))
{
lastResponse = await ProcessANAMEAsync(request, lastResponse, isRecursionAllowed);
}
else if ((lastResponse.Answer.Count == 0) && (lastResponse.Authority.Count > 0))
{
//found delegated/forwarded zone
switch (lastResponse.Authority[0].Type)
{
case DnsResourceRecordType.NS:
if (newRequest.RecursionDesired && isRecursionAllowed)
{
//do recursive resolution using last response authority name servers
List nameServers = NameServerAddress.GetNameServersFromResponse(lastResponse, _preferIPv6, false);
lastResponse = await RecursiveResolveAsync(newRequest, nameServers, null, false, false);
isAuthoritativeAnswer = false;
}
break;
case DnsResourceRecordType.FWD:
if ((lastResponse.Authority.Count == 1) && (lastResponse.Authority[0].RDATA as DnsForwarderRecord).Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
{
//do conditional forwarding via "this-server"
lastResponse = await RecursiveResolveAsync(newRequest, null, null, false, false);
isAuthoritativeAnswer = false;
}
else
{
//do conditional forwarding
List forwarders = new List(lastResponse.Authority.Count);
foreach (DnsResourceRecord rr in lastResponse.Authority)
{
if (rr.Type == DnsResourceRecordType.FWD)
{
DnsForwarderRecord fwd = rr.RDATA as DnsForwarderRecord;
if (!fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
forwarders.Add(fwd.NameServer);
}
}
lastResponse = await RecursiveResolveAsync(newRequest, null, forwarders, false, false);
isAuthoritativeAnswer = false;
}
break;
}
}
//check last response
if (lastResponse.Answer.Count == 0)
break; //cannot proceed to resolve further
responseAnswer.AddRange(lastResponse.Answer);
DnsResourceRecord lastRR = lastResponse.Answer[lastResponse.Answer.Count - 1];
if (lastRR.Type != DnsResourceRecordType.CNAME)
break; //cname was resolved
lastDomain = (lastRR.RDATA as DnsCNAMERecord).Domain;
}
while (++queryCount < MAX_CNAME_HOPS);
DnsResponseCode rcode;
IReadOnlyList authority = null;
IReadOnlyList additional = null;
if ((lastResponse.RCODE == DnsResponseCode.Refused) && !(request.RecursionDesired && isRecursionAllowed))
{
rcode = DnsResponseCode.NoError;
}
else
{
rcode = lastResponse.RCODE;
if (isAuthoritativeAnswer)
{
authority = response.Authority;
additional = response.Additional;
}
else
{
if ((lastResponse.Authority.Count > 0) && (lastResponse.Authority[0].Type == DnsResourceRecordType.SOA))
authority = lastResponse.Authority;
}
}
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, responseAnswer, authority, additional) { Tag = response.Tag };
}
private async Task ProcessANAMEAsync(DnsDatagram request, DnsDatagram response, bool isRecursionAllowed)
{
List responseAnswer = new List();
for (int i = 0; i < response.Answer.Count - 1; i++)
responseAnswer.Add(response.Answer[i]);
DnsDatagram lastResponse;
DnsResourceRecord anameRR = response.Answer[response.Answer.Count - 1];
string lastDomain = (anameRR.RDATA as DnsANAMERecord).Domain;
int queryCount = 0;
do
{
DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(lastDomain, request.Question[0].Type, request.Question[0].Class) });
//query authoritative zone first
lastResponse = _authZoneManager.Query(newRequest);
if (lastResponse.RCODE == DnsResponseCode.Refused)
{
//not found in auth zone; do recursion
lastResponse = await RecursiveResolveAsync(newRequest, null, null, false, false);
}
else if ((lastResponse.Answer.Count == 0) && (lastResponse.Authority.Count > 0))
{
//found delegated/forwarded zone
switch (lastResponse.Authority[0].Type)
{
case DnsResourceRecordType.NS:
//do recursive resolution using last response authority name servers
List nameServers = NameServerAddress.GetNameServersFromResponse(lastResponse, _preferIPv6, false);
lastResponse = await RecursiveResolveAsync(newRequest, nameServers, null, false, false);
break;
case DnsResourceRecordType.FWD:
if ((lastResponse.Authority.Count == 1) && (lastResponse.Authority[0].RDATA as DnsForwarderRecord).Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
{
//do conditional forwarding via "this-server"
lastResponse = await RecursiveResolveAsync(newRequest, null, null, false, false);
}
else
{
//do conditional forwarding
List forwarders = new List(lastResponse.Authority.Count);
foreach (DnsResourceRecord rr in lastResponse.Authority)
{
if (rr.Type == DnsResourceRecordType.FWD)
{
DnsForwarderRecord fwd = rr.RDATA as DnsForwarderRecord;
if (!fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase))
forwarders.Add(fwd.NameServer);
}
}
lastResponse = await RecursiveResolveAsync(newRequest, null, forwarders, false, false);
}
break;
}
}
//check last response
if (lastResponse.Answer.Count == 0)
break; //cannot proceed to resolve further
DnsResourceRecord firstRR = lastResponse.Answer[0];
if (firstRR.Type == request.Question[0].Type)
{
foreach (DnsResourceRecord answer in lastResponse.Answer)
{
if (anameRR.TtlValue < answer.TtlValue)
responseAnswer.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TtlValue, answer.RDATA));
else
responseAnswer.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TtlValue, answer.RDATA));
}
break; //found final answer
}
DnsResourceRecord lastRR = lastResponse.Answer[lastResponse.Answer.Count - 1];
if (lastRR.Type == DnsResourceRecordType.ANAME)
lastDomain = (lastRR.RDATA as DnsANAMERecord).Domain;
else if (lastRR.Type == DnsResourceRecordType.CNAME)
lastDomain = (lastRR.RDATA as DnsCNAMERecord).Domain;
else
break; //aname/cname was resolved
}
while (++queryCount < MAX_CNAME_HOPS);
DnsResponseCode rcode = lastResponse.RCODE;
if (rcode == DnsResponseCode.NameError)
rcode = DnsResponseCode.NoError;
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, responseAnswer, response.Authority, response.Additional) { Tag = response.Tag };
}
private DnsDatagram ProcessBlockedQuery(DnsDatagram request)
{
DnsDatagram response = _blockedZoneManager.Query(request);
if (response.RCODE == DnsResponseCode.Refused)
{
//domain not blocked in blocked zone
response = _blockListZoneManager.Query(request); //check in block list zone
if (response == null)
return null;
}
else
{
//domain is blocked in blocked zone
IReadOnlyList answer = null;
IReadOnlyList authority = null;
switch (response.Question[0].Type)
{
case DnsResourceRecordType.A:
answer = new DnsResourceRecord[] { new DnsResourceRecord(response.Question[0].Name, DnsResourceRecordType.A, response.Question[0].Class, 60, _aRecord) };
break;
case DnsResourceRecordType.AAAA:
answer = new DnsResourceRecord[] { new DnsResourceRecord(response.Question[0].Name, DnsResourceRecordType.AAAA, response.Question[0].Class, 60, _aaaaRecord) };
break;
case DnsResourceRecordType.NS:
answer = response.Answer;
break;
case DnsResourceRecordType.TXT:
answer = new DnsResourceRecord[] { new DnsResourceRecord(response.Question[0].Name, DnsResourceRecordType.TXT, response.Question[0].Class, 60, new DnsTXTRecord("blockList=custom; domain=" + response.Question[0].Name)) };
break;
default:
authority = response.Authority;
break;
}
response = new DnsDatagram(response.Identifier, true, response.OPCODE, false, false, response.RecursionDesired, true, false, false, DnsResponseCode.NoError, response.Question, answer, authority);
}
response.Tag = StatsResponseType.Blocked;
return response;
}
private async Task ProcessRecursiveQueryAsync(DnsDatagram request, IReadOnlyList viaNameServers, IReadOnlyList viaForwarders, bool checkForCnameCloaking, bool cacheRefreshOperation)
{
DnsDatagram response = await RecursiveResolveAsync(request, viaNameServers, viaForwarders, false, cacheRefreshOperation);
if (response.Answer.Count > 0)
{
DnsResourceRecordType questionType = request.Question[0].Type;
DnsResourceRecord lastRR = response.Answer[response.Answer.Count - 1];
if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY))
response = await ProcessCNAMEAsync(request, response, true, cacheRefreshOperation);
if (checkForCnameCloaking)
{
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 DnsCNAMERecord).Domain, request.Question[0].Type, request.Question[0].Class) });
//check allowed zone
bool inAllowedZone = _allowedZoneManager.Query(newRequest).RCODE != DnsResponseCode.Refused;
if (inAllowedZone)
break; //CNAME is in allowed zone
//check blocked zone and block list zone
DnsDatagram lastResponse = ProcessBlockedQuery(newRequest);
if (lastResponse != 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(lastResponse.Answer);
IReadOnlyList authority = null;
if ((lastResponse.Authority.Count > 0) && (lastResponse.Authority[0].Type == DnsResourceRecordType.SOA))
authority = lastResponse.Authority;
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, DnsResponseCode.NoError, request.Question, answer, authority) { Tag = lastResponse.Tag };
}
}
}
}
//return response
{
IReadOnlyList authority = null;
if ((authority == null) && (response.Authority.Count > 0) && (response.Authority[0].Type == DnsResourceRecordType.SOA))
authority = response.Authority;
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, response.RCODE, request.Question, response.Answer, authority) { Tag = response.Tag };
}
}
private async Task RecursiveResolveAsync(DnsDatagram request, IReadOnlyList viaNameServers, IReadOnlyList viaForwarders, bool cachePrefetchOperation, bool cacheRefreshOperation)
{
if (!cachePrefetchOperation && !cacheRefreshOperation)
{
//query cache zone to see if answer available
DnsDatagram cacheResponse = QueryCache(request, false);
if (cacheResponse != 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.TtlValue < _cachePrefetchTrigger))
{
//trigger prefetch async
_ = PrefetchCacheAsync(request, viaNameServers, viaForwarders);
break;
}
}
}
return cacheResponse;
}
}
//recursion with locking
TaskCompletionSource resolverTaskCompletionSource = new TaskCompletionSource();
Task resolverTask = _resolverTasks.GetOrAdd(GetResolverQueryKey(request.Question[0]), 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 RecursiveResolveAsync(request, viaNameServers, viaForwarders, cachePrefetchOperation, cacheRefreshOperation, 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
DateTime resolverWaitStartTime = DateTime.UtcNow;
//wait till short timeout for response
if (await Task.WhenAny(resolverTask, Task.Delay(SERVE_STALE_WAIT_TIME)) == resolverTask) //1.8 sec wait as per draft-ietf-dnsop-serve-stale-04
{
//resolver signaled
DnsDatagram response = await resolverTask;
if (response != null)
return response;
//resolver had exception and no stale record was found
}
else
{
//wait timed out
//query cache zone to return stale answer (if available) as per draft-ietf-dnsop-serve-stale-04
DnsDatagram staleResponse = QueryCache(request, true);
if (staleResponse != null)
return staleResponse;
//wait till full timeout before responding as ServerFailure
int timeout = Convert.ToInt32(_clientTimeout - (DateTime.UtcNow - resolverWaitStartTime).TotalMilliseconds);
if (timeout > 0)
{
if (await Task.WhenAny(resolverTask, Task.Delay(timeout)) == resolverTask)
{
//resolver signaled
DnsDatagram response = await resolverTask;
if (response != null)
return response;
}
//no response available from resolver or resolver had exception and no stale record was found
}
}
//no response available; respond with ServerFailure
return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.ServerFailure, request.Question);
}
private async Task RecursiveResolveAsync(DnsDatagram request, IReadOnlyList viaNameServers, IReadOnlyList viaForwarders, bool cachePrefetchOperation, bool cacheRefreshOperation, TaskCompletionSource taskCompletionSource)
{
IReadOnlyList forwarders = _forwarders;
if (viaForwarders != null)
forwarders = viaForwarders; //use provided forwarders
try
{
if ((viaNameServers == null) && (forwarders != null))
{
//use forwarders
if (_proxy == null)
{
//recursive resolve name server when proxy is null else let proxy resolve it
foreach (NameServerAddress nameServerAddress in forwarders)
{
if (nameServerAddress.IsIPEndPointStale) //refresh forwarder IPEndPoint if stale
await nameServerAddress.RecursiveResolveIPAddressAsync(_dnsCache, null, _preferIPv6, _resolverRetries, _resolverTimeout);
}
}
//query forwarders and update cache
DnsClient dnsClient = new DnsClient(forwarders);
dnsClient.Proxy = _proxy;
dnsClient.PreferIPv6 = _preferIPv6;
dnsClient.Retries = _forwarderRetries;
dnsClient.Timeout = _forwarderTimeout;
dnsClient.Concurrency = _forwarderConcurrency;
DnsDatagram response = await dnsClient.ResolveAsync(request.Question[0]);
_cacheZoneManager.CacheResponse(response);
taskCompletionSource.SetResult(response);
}
else
{
//recursive resolve and update cache
IDnsCache dnsCache;
if (cachePrefetchOperation || cacheRefreshOperation)
dnsCache = new ResolverPrefetchDnsCache(_authZoneManager, _cacheZoneManager, request.Question[0]);
else
dnsCache = _dnsCache;
DnsDatagram response = await DnsClient.RecursiveResolveAsync(request.Question[0], viaNameServers, dnsCache, _proxy, _preferIPv6, _resolverRetries, _resolverTimeout, _resolverMaxStackCount);
taskCompletionSource.SetResult(response);
}
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
{
string nameServers = null;
if (viaNameServers != null)
{
foreach (NameServerAddress nameServer in viaNameServers)
{
if (nameServers == null)
nameServers = nameServer.ToString();
else
nameServers += ", " + nameServer.ToString();
}
}
else if (forwarders != null)
{
foreach (NameServerAddress nameServer in forwarders)
{
if (nameServers == null)
nameServers = nameServer.ToString();
else
nameServers += ", " + nameServer.ToString();
}
}
log.Write("DNS Server recursive resolution failed for QNAME: " + request.Question[0].Name + "; QTYPE: " + request.Question[0].Type.ToString() + "; QCLASS: " + request.Question[0].Class.ToString() + (nameServers == null ? "" : "; Name Servers: " + nameServers) + ";\r\n" + ex.ToString());
}
//fetch stale record
DnsDatagram staleResponse = QueryCache(request, true);
if (staleResponse == null)
{
//no stale record was found; signal null response to release waiting tasks
taskCompletionSource.SetResult(null);
}
else
{
//reset expiry for stale records
foreach (DnsResourceRecord record in staleResponse.Answer)
{
if (record.IsStale)
record.ResetExpiry(30); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per draft-ietf-dnsop-serve-stale-04
}
//signal stale response
taskCompletionSource.SetResult(staleResponse);
}
}
finally
{
_resolverTasks.TryRemove(GetResolverQueryKey(request.Question[0]), out _);
}
}
private static string GetResolverQueryKey(DnsQuestionRecord question)
{
if (string.IsNullOrEmpty(question.Name))
return question.Type + "." + question.Class;
return question.Name + "." + question.Type + "." + question.Class;
}
private DnsDatagram QueryCache(DnsDatagram request, bool serveStale)
{
DnsDatagram cacheResponse = _cacheZoneManager.Query(request, serveStale);
if (cacheResponse.RCODE != DnsResponseCode.Refused)
{
if ((cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || (cacheResponse.Authority[0].Type == DnsResourceRecordType.SOA))
{
cacheResponse.Tag = StatsResponseType.Cached;
return cacheResponse;
}
}
return null;
}
private async Task PrefetchCacheAsync(DnsDatagram request, IReadOnlyList viaNameServers, IReadOnlyList viaForwarders)
{
try
{
await RecursiveResolveAsync(request, viaNameServers, viaForwarders, true, false);
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(ex);
}
}
private async Task RefreshCacheAsync(IList cacheRefreshSampleList, DnsQuestionRecord sampleQuestion, int sampleQuestionIndex)
{
try
{
//refresh cache
DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { sampleQuestion });
DnsDatagram response = await ProcessRecursiveQueryAsync(request, null, null, false, true);
bool removeFromSampleList = true;
DateTime utcNow = DateTime.UtcNow;
foreach (DnsResourceRecord answer in response.Answer)
{
if ((answer.OriginalTtlValue > _cachePrefetchEligibility) && (utcNow.AddSeconds(answer.TtlValue) < _cachePrefetchSamplingTimerTriggersOn))
{
//answer expires before next sampling so dont remove from list to allow refreshing it
removeFromSampleList = false;
break;
}
}
if (removeFromSampleList)
cacheRefreshSampleList[sampleQuestionIndex] = null;
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(ex);
}
}
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);
if (cacheResponse == 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.TtlValue < trigger))
return question; //TTL eligible and less than trigger so refresh question
}
DnsResourceRecord lastRR = cacheResponse.Answer[cacheResponse.Answer.Count - 1];
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 DnsCNAMERecord).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);
if (cacheResponse == 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.TtlValue < trigger))
return true; //TTL eligible less than trigger so refresh
}
return false; //no need to refresh for this query
}
private void CachePrefetchSamplingTimerCallback(object state)
{
try
{
StatsManager stats = _stats;
if (stats != null)
{
List> eligibleQueries = stats.GetLastHourEligibleQueries(_cachePrefetchSampleEligibilityHitsPerHour);
List cacheRefreshSampleList = new List(eligibleQueries.Count);
int cacheRefreshTrigger = (_cachePrefetchSampleIntervalInMinutes + 1) * 60;
foreach (KeyValuePair query in eligibleQueries)
{
AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(query.Key.Name);
if (zoneInfo != null)
{
switch (zoneInfo.Type)
{
case AuthZoneType.Primary:
case AuthZoneType.Secondary:
//zone is hosted
if (!zoneInfo.Disabled)
continue; //no cache refresh for zone that is hosted and enabled
break;
}
}
if (query.Key.Type == DnsResourceRecordType.ANY)
continue; //dont refresh ANY queries
DnsQuestionRecord refreshQuery = GetCacheRefreshNeededQuery(query.Key, cacheRefreshTrigger);
if (refreshQuery != null)
cacheRefreshSampleList.Add(refreshQuery);
}
_cachePrefetchSampleList = cacheRefreshSampleList;
}
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(ex);
}
finally
{
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer != null)
{
_cachePrefetchSamplingTimer.Change(_cachePrefetchSampleIntervalInMinutes * 60 * 1000, System.Threading.Timeout.Infinite);
_cachePrefetchSamplingTimerTriggersOn = DateTime.UtcNow.AddMinutes(_cachePrefetchSampleIntervalInMinutes);
}
}
}
}
private void CachePrefetchRefreshTimerCallback(object state)
{
try
{
IList cacheRefreshSampleList = _cachePrefetchSampleList;
if (cacheRefreshSampleList != null)
{
for (int i = 0; i < cacheRefreshSampleList.Count; i++)
{
DnsQuestionRecord sampleQuestion = cacheRefreshSampleList[i];
if (sampleQuestion == null)
continue;
if (!IsCacheRefreshNeeded(sampleQuestion, _cachePrefetchTrigger + 2))
continue;
_ = RefreshCacheAsync(cacheRefreshSampleList, sampleQuestion, i);
}
}
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(ex);
}
finally
{
lock (_cachePrefetchRefreshTimerLock)
{
if (_cachePrefetchRefreshTimer != null)
_cachePrefetchRefreshTimer.Change((_cachePrefetchTrigger + 1) * 1000, Timeout.Infinite);
}
}
}
private void CacheMaintenanceTimerCallback(object state)
{
try
{
_cacheZoneManager.DoMaintenance();
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(ex);
}
}
private void ResetPrefetchTimers()
{
if (_cachePrefetchTrigger == 0)
{
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer != null)
_cachePrefetchSamplingTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
lock (_cachePrefetchRefreshTimerLock)
{
if (_cachePrefetchRefreshTimer != null)
_cachePrefetchRefreshTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
}
else if (_state == ServiceState.Running)
{
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer != null)
{
_cachePrefetchSamplingTimer.Change(_cachePrefetchSampleIntervalInMinutes * 60 * 1000, System.Threading.Timeout.Infinite);
_cachePrefetchSamplingTimerTriggersOn = DateTime.UtcNow.AddMinutes(_cachePrefetchSampleIntervalInMinutes);
}
}
lock (_cachePrefetchRefreshTimerLock)
{
if (_cachePrefetchRefreshTimer != null)
_cachePrefetchRefreshTimer.Change(CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL, System.Threading.Timeout.Infinite);
}
}
}
private void UpdateThisServer()
{
if (_thisServer == null)
{
if ((_localEndPoints == null) || (_localEndPoints.Count == 0))
_thisServer = new NameServerAddress(_serverDomain, IPAddress.Loopback);
else if (_localEndPoints[0].Address.Equals(IPAddress.Any))
_thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.Loopback, _localEndPoints[0].Port));
else if (_localEndPoints[0].Equals(IPAddress.IPv6Any))
_thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.IPv6Loopback, _localEndPoints[0].Port));
else
_thisServer = new NameServerAddress(_serverDomain, _localEndPoints[0]);
}
else
{
_thisServer = new NameServerAddress(_serverDomain, _thisServer.IPEndPoint);
}
}
#endregion
#region public
public void Start()
{
if (_disposed)
throw new ObjectDisposedException("DnsServer");
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
udpListener.ReceiveBufferSize = 64 * 1024;
udpListener.SendBufferSize = 64 * 1024;
udpListener.Bind(localEP);
_udpListeners.Add(udpListener);
LogManager log = _log;
if (log != null)
log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString());
if (udpListener != null)
udpListener.Dispose();
}
Socket tcpListener = null;
try
{
tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
tcpListener.Bind(localEP);
tcpListener.Listen(100);
_tcpListeners.Add(tcpListener);
LogManager log = _log;
if (log != null)
log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString());
if (tcpListener != null)
tcpListener.Dispose();
}
if (_enableDnsOverHttp)
{
IPEndPoint httpEP = new IPEndPoint(localEP.Address, 8053);
Socket httpListener = null;
try
{
httpListener = new Socket(httpEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
httpListener.Bind(httpEP);
httpListener.Listen(100);
_httpListeners.Add(httpListener);
_isDnsOverHttpsEnabled = true;
LogManager log = _log;
if (log != null)
log.Write(httpEP, "Http", "DNS Server was bound successfully.");
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(httpEP, "Http", "DNS Server failed to bind.\r\n" + ex.ToString());
if (httpListener != null)
httpListener.Dispose();
}
}
if (_enableDnsOverTls && (_certificate != null))
{
IPEndPoint tlsEP = new IPEndPoint(localEP.Address, 853);
Socket tlsListener = null;
try
{
tlsListener = new Socket(tlsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
tlsListener.Bind(tlsEP);
tlsListener.Listen(100);
_tlsListeners.Add(tlsListener);
LogManager log = _log;
if (log != null)
log.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server failed to bind.\r\n" + ex.ToString());
if (tlsListener != null)
tlsListener.Dispose();
}
}
if (_enableDnsOverHttps)
{
//bind to http port 80 for certbot webroot support
{
IPEndPoint httpEP = new IPEndPoint(localEP.Address, 80);
Socket httpListener = null;
try
{
httpListener = new Socket(httpEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
httpListener.Bind(httpEP);
httpListener.Listen(100);
_httpListeners.Add(httpListener);
LogManager log = _log;
if (log != null)
log.Write(httpEP, "Http", "DNS Server was bound successfully.");
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(httpEP, "Http", "DNS Server failed to bind.\r\n" + ex.ToString());
if (httpListener != null)
httpListener.Dispose();
}
}
//bind to https port 443
if (_certificate != null)
{
IPEndPoint httpsEP = new IPEndPoint(localEP.Address, 443);
Socket httpsListener = null;
try
{
httpsListener = new Socket(httpsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
httpsListener.Bind(httpsEP);
httpsListener.Listen(100);
_httpsListeners.Add(httpsListener);
_isDnsOverHttpsEnabled = true;
LogManager log = _log;
if (log != null)
log.Write(httpsEP, DnsTransportProtocol.Https, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(httpsEP, DnsTransportProtocol.Https, "DNS Server failed to bind.\r\n" + ex.ToString());
if (httpsListener != null)
httpsListener.Dispose();
}
}
}
}
//start reading query packets
int listenerThreadCount = Math.Max(1, Environment.ProcessorCount);
foreach (Socket udpListener in _udpListeners)
{
for (int i = 0; i < listenerThreadCount; i++)
{
Thread thread = new Thread(ReadUdpRequestAsync);
thread.Name = "DNS UDP Read Request [" + i + "]";
thread.IsBackground = true;
thread.Start(udpListener);
}
}
foreach (Socket tcpListener in _tcpListeners)
{
for (int i = 0; i < listenerThreadCount; i++)
{
Thread thread = new Thread(AcceptConnectionAsync);
thread.Name = "DNS TCP Read Request [" + i + "]";
thread.IsBackground = true;
thread.Start(new object[] { tcpListener, DnsTransportProtocol.Tcp });
}
}
foreach (Socket httpListener in _httpListeners)
{
for (int i = 0; i < listenerThreadCount; i++)
{
Thread thread = new Thread(AcceptConnectionAsync);
thread.Name = "DNS HTTP Read Request [" + i + "]";
thread.IsBackground = true;
thread.Start(new object[] { httpListener, DnsTransportProtocol.Https, false });
}
}
foreach (Socket tlsListener in _tlsListeners)
{
for (int i = 0; i < listenerThreadCount; i++)
{
Thread thread = new Thread(AcceptConnectionAsync);
thread.Name = "DNS TLS Read Request [" + i + "]";
thread.IsBackground = true;
thread.Start(new object[] { tlsListener, DnsTransportProtocol.Tls });
}
}
foreach (Socket httpsListener in _httpsListeners)
{
for (int i = 0; i < listenerThreadCount; i++)
{
Thread thread = new Thread(AcceptConnectionAsync);
thread.Name = "DNS HTTPS Read Request [" + i + "]";
thread.IsBackground = true;
thread.Start(new object[] { httpsListener, DnsTransportProtocol.Https });
}
}
_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, CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL);
_state = ServiceState.Running;
UpdateThisServer();
ResetPrefetchTimers();
}
public void Stop()
{
if (_state != ServiceState.Running)
return;
_state = ServiceState.Stopping;
lock (_cachePrefetchSamplingTimerLock)
{
if (_cachePrefetchSamplingTimer != null)
{
_cachePrefetchSamplingTimer.Dispose();
_cachePrefetchSamplingTimer = null;
}
}
lock (_cachePrefetchRefreshTimerLock)
{
if (_cachePrefetchRefreshTimer != null)
{
_cachePrefetchRefreshTimer.Dispose();
_cachePrefetchRefreshTimer = null;
}
}
if (_cacheMaintenanceTimer != null)
{
_cacheMaintenanceTimer.Dispose();
_cacheMaintenanceTimer = null;
}
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
foreach (Socket udpListener in _udpListeners)
udpListener.Dispose();
foreach (Socket tcpListener in _tcpListeners)
tcpListener.Dispose();
foreach (Socket httpListener in _httpListeners)
httpListener.Dispose();
foreach (Socket tlsListener in _tlsListeners)
tlsListener.Dispose();
foreach (Socket httpsListener in _httpsListeners)
httpsListener.Dispose();
}
else
{
//issue: https://github.com/dotnet/runtime/issues/37873
foreach (Socket udpListener in _udpListeners)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
udpListener.Dispose();
});
}
foreach (Socket tcpListener in _tcpListeners)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
tcpListener.Dispose();
});
}
foreach (Socket httpListener in _httpListeners)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
httpListener.Dispose();
});
}
foreach (Socket tlsListener in _tlsListeners)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
tlsListener.Dispose();
});
}
foreach (Socket httpsListener in _httpsListeners)
{
ThreadPool.QueueUserWorkItem(delegate (object state)
{
httpsListener.Dispose();
});
}
}
_udpListeners.Clear();
_tcpListeners.Clear();
_httpListeners.Clear();
_tlsListeners.Clear();
_httpsListeners.Clear();
_state = ServiceState.Stopped;
}
public async Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 2000)
{
try
{
Task task = ProcessQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), new IPEndPoint(IPAddress.Any, 0), true, DnsTransportProtocol.Tcp);
using (CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource())
{
if (await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)) != task)
return null;
timeoutCancellationTokenSource.Cancel(); //stop delay task
}
return await task;
}
catch (Exception ex)
{
LogManager log = _log;
if (log != null)
log.Write(ex);
return null;
}
}
#endregion
#region properties
public string ServerDomain
{
get { return _serverDomain; }
set
{
if (!_serverDomain.Equals(value))
{
_serverDomain = value.ToLower();
_authZoneManager.ServerDomain = _serverDomain;
_allowedZoneManager.ServerDomain = _serverDomain;
_blockedZoneManager.ServerDomain = _serverDomain;
_blockListZoneManager.ServerDomain = _serverDomain;
UpdateThisServer();
}
}
}
public string ConfigFolder
{ get { return _configFolder; } }
public IReadOnlyList LocalEndPoints
{
get { return _localEndPoints; }
set { _localEndPoints = value; }
}
public NameServerAddress ThisServer
{ get { return _thisServer; } }
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 IsDnsOverHttpsEnabled
{ get { return _isDnsOverHttpsEnabled; } }
public X509Certificate2 Certificate
{
get { return _certificate; }
set
{
if (!value.HasPrivateKey)
throw new ArgumentException("Tls certificate does not contain private key.");
_certificate = value;
}
}
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 IDnsCache DnsCache
{ get { return _dnsCache; } }
public bool AllowRecursion
{
get { return _allowRecursion; }
set { _allowRecursion = value; }
}
public bool AllowRecursionOnlyForPrivateNetworks
{
get { return _allowRecursionOnlyForPrivateNetworks; }
set { _allowRecursionOnlyForPrivateNetworks = value; }
}
public NetProxy Proxy
{
get { return _proxy; }
set { _proxy = value; }
}
public IReadOnlyList Forwarders
{
get { return _forwarders; }
set { _forwarders = value; }
}
public bool PreferIPv6
{
get { return _preferIPv6; }
set { _preferIPv6 = value; }
}
public int ForwarderRetries
{
get { return _forwarderRetries; }
set
{
if (value > 0)
_forwarderRetries = value;
}
}
public int ResolverRetries
{
get { return _resolverRetries; }
set
{
if (value > 0)
_resolverRetries = value;
}
}
public int ForwarderTimeout
{
get { return _forwarderTimeout; }
set
{
if (value >= 2000)
_forwarderTimeout = value;
}
}
public int ResolverTimeout
{
get { return _resolverTimeout; }
set
{
if (value >= 2000)
_resolverTimeout = value;
}
}
public int ClientTimeout
{
get { return _clientTimeout; }
set
{
if (value >= 2000)
_clientTimeout = value;
}
}
public int ForwarderConcurrency
{
get { return _forwarderConcurrency; }
set { _forwarderConcurrency = value; }
}
public int ResolverMaxStackCount
{
get { return _resolverMaxStackCount; }
set { _resolverMaxStackCount = value; }
}
public int CachePrefetchEligibility
{
get { return _cachePrefetchEligibility; }
set
{
if (value < 2)
throw new ArgumentOutOfRangeException("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("CachePrefetchTrigger", "Valid value is greater that or equal to 0.");
if (_cachePrefetchTrigger != value)
{
_cachePrefetchTrigger = value;
ResetPrefetchTimers();
}
}
}
public int CachePrefetchSampleIntervalInMinutes
{
get { return _cachePrefetchSampleIntervalInMinutes; }
set
{
if ((value < 1) || (value > 60))
throw new ArgumentOutOfRangeException("CacheRefreshSampleIntervalInMinutes", "Valid range is between 1 and 60 minutes.");
if (_cachePrefetchSampleIntervalInMinutes != value)
{
_cachePrefetchSampleIntervalInMinutes = value;
ResetPrefetchTimers();
}
}
}
public int CachePrefetchSampleEligibilityHitsPerHour
{
get { return _cachePrefetchSampleEligibilityHitsPerHour; }
set
{
if (value < 1)
throw new ArgumentOutOfRangeException("CachePrefetchSampleEligibilityHitsPerHour", "Valid value is greater than or equal to 1.");
_cachePrefetchSampleEligibilityHitsPerHour = value;
}
}
public LogManager LogManager
{
get { return _log; }
set { _log = value; }
}
public LogManager QueryLogManager
{
get { return _queryLog; }
set { _queryLog = value; }
}
public StatsManager StatsManager
{ get { return _stats; } }
public int TcpSendTimeout
{
get { return _tcpSendTimeout; }
set { _tcpSendTimeout = value; }
}
public int TcpReceiveTimeout
{
get { return _tcpReceiveTimeout; }
set { _tcpReceiveTimeout = value; }
}
#endregion
}
}