/* Technitium DNS Server Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using DnsServerCore.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.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Quic; using System.Net.Security; using System.Net.Sockets; using System.Runtime.ExceptionServices; 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; 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, UseSpecifiedNetworks = 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; const int SERVE_STALE_WAIT_TIME = 1800; static readonly IPEndPoint IPENDPOINT_ANY_0 = new IPEndPoint(IPAddress.Any, 0); static readonly IReadOnlyCollection _aRecords = new DnsARecordData[] { new DnsARecordData(IPAddress.Any) }; static readonly IReadOnlyCollection _aaaaRecords = new DnsAAAARecordData[] { new DnsAAAARecordData(IPAddress.IPv6Any) }; 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 _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 StatsManager _stats; bool _preferIPv6; ushort _udpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; bool _dnssecValidation = true; bool _eDnsClientSubnet; byte _eDnsClientSubnetIPv4PrefixLength = 24; byte _eDnsClientSubnetIPv6PrefixLength = 56; int _qpmLimitRequests = 0; int _qpmLimitErrors = 0; int _qpmLimitSampleMinutes = 5; int _qpmLimitIPv4PrefixLength = 24; int _qpmLimitIPv6PrefixLength = 56; int _clientTimeout = 4000; int _tcpSendTimeout = 10000; int _tcpReceiveTimeout = 10000; int _quicIdleTimeout = 60000; int _quicMaxInboundStreams = 100; int _listenBacklog = 100; bool _enableDnsOverHttp; bool _enableDnsOverTls; bool _enableDnsOverHttps; bool _enableDnsOverQuic; int _dnsOverHttpPort = 80; int _dnsOverTlsPort = 853; int _dnsOverHttpsPort = 443; int _dnsOverQuicPort = 853; X509Certificate2 _certificate; IReadOnlyDictionary _tsigKeys; DnsServerRecursion _recursion; IReadOnlyCollection _recursionDeniedNetworks; IReadOnlyCollection _recursionAllowedNetworks; bool _randomizeName; bool _qnameMinimization; bool _nsRevalidation; int _resolverRetries = 2; int _resolverTimeout = 2000; int _resolverMaxStackCount = 16; bool _serveStale = true; int _cachePrefetchEligibility = 2; int _cachePrefetchTrigger = 9; int _cachePrefetchSampleIntervalInMinutes = 5; int _cachePrefetchSampleEligibilityHitsPerHour = 30; bool _enableBlocking = true; bool _allowTxtBlockingReport = true; DnsServerBlockingType _blockingType = DnsServerBlockingType.NxDomain; IReadOnlyCollection _customBlockingARecords = Array.Empty(); IReadOnlyCollection _customBlockingAAAARecords = Array.Empty(); NetProxy _proxy; IReadOnlyList _forwarders; int _forwarderRetries = 3; int _forwarderTimeout = 2000; int _forwarderConcurrency = 2; 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(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, false); //init stats _stats = new StatsManager(this); //init udp socket pool async for port randomization ThreadPool.QueueUserWorkItem(delegate (object state) { if (Environment.OSVersion.Platform == PlatformID.Win32NT) UdpClientConnection.CreateSocketPool(_preferIPv6); }); } #endregion #region IDisposable bool _disposed; public async ValueTask DisposeAsync() { if (_disposed) return; await StopAsync(); _authZoneManager?.Dispose(); _dnsApplicationManager?.Dispose(); _stats?.Dispose(); _disposed = true; } public void Dispose() { DisposeAsync().Sync(); } #endregion #region private private async Task ReadUdpRequestAsync(Socket udpListener) { byte[] recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE]; using MemoryStream recvBufferStream = new MemoryStream(recvBuffer); try { 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."); } SocketReceiveFromResult result; while (true) { recvBufferStream.SetLength(DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE); //resetting length before using buffer try { result = await udpListener.ReceiveFromAsync(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; if (IsQpmLimitCrossed(remoteEP.Address)) continue; try { recvBufferStream.Position = 0; recvBufferStream.SetLength(result.ReceivedBytes); DnsDatagram request = DnsDatagram.ReadFrom(recvBufferStream); _ = ProcessUdpRequestAsync(udpListener, remoteEP, request); } catch (EndOfStreamException) { //ignore incomplete udp datagrams } catch (Exception ex) { _log?.Write(remoteEP, DnsTransportProtocol.Udp, 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, DnsDatagram request) { try { DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, DnsTransportProtocol.Udp, IsRecursionAllowed(remoteEP.Address)); if (response is null) return; //drop request //send response byte[] sendBuffer; if (request.EDNS is null) sendBuffer = new byte[512]; else if (request.EDNS.UdpPayloadSize > _udpPayloadSize) sendBuffer = new byte[_udpPayloadSize]; else sendBuffer = new byte[request.EDNS.UdpPayloadSize]; using (MemoryStream sendBufferStream = new MemoryStream(sendBuffer)) { 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, remoteEP); } _queryLog?.Write(remoteEP, DnsTransportProtocol.Udp, request, response); _stats.QueueUpdate(request, remoteEP, DnsTransportProtocol.Udp, response); } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _queryLog?.Write(remoteEP, DnsTransportProtocol.Udp, request, null); _log?.Write(remoteEP, DnsTransportProtocol.Udp, ex); } } 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, protocol); break; case DnsTransportProtocol.Tls: SslStream tlsStream = new SslStream(new NetworkStream(socket)); await tlsStream.AuthenticateAsServerAsync(_certificate).WithTimeout(_tcpReceiveTimeout); await ReadStreamRequestAsync(tlsStream, remoteEP, protocol); break; default: throw new InvalidOperationException(); } } 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, 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)) 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; } //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 PreProcessQueryAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { await stream.DisposeAsync(); return; //drop request } //send response await writeSemaphore.WaitAsync(); try { //send dns datagram await response.WriteToTcpAsync(stream, writeBuffer); await stream.FlushAsync(); } finally { writeSemaphore.Release(); } _queryLog?.Write(remoteEP, protocol, request, response); _stats.QueueUpdate(request, remoteEP, protocol, response); } 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 { while (true) { if (IsQpmLimitCrossed(quicConnection.RemoteEndPoint.Address)) break; QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync(); _ = ProcessQuicStreamRequestAsync(quicStream, quicConnection.RemoteEndPoint); } } 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) { 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; } //process request async DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address)); if (response is null) return; //drop request //send response await response.WriteToTcpAsync(quicStream, sharedBuffer); _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, response); _stats.QueueUpdate(request, remoteEP, DnsTransportProtocol.Quic, response); } 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(); DnsDatagram dnsRequest = null; try { HttpRequest request = context.Request; HttpResponse response = context.Response; if (IsQpmLimitCrossed(remoteEP.Address)) { response.StatusCode = 429; await response.WriteAsync("Too Many Requests"); return; } if (!request.IsHttps) { //get the actual connection remote EP IPEndPoint connectionEp = context.GetRemoteEndPoint(true); 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); } break; case "POST": if (!string.Equals(request.Headers["Content-Type"], "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); } break; default: throw new InvalidOperationException(); } DnsDatagram dnsResponse = await PreProcessQueryAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address)); if (dnsResponse is null) { //drop request context.Connection.RequestClose(); return; } using (MemoryStream mS = new MemoryStream(512)) { dnsResponse.WriteTo(mS); mS.Position = 0; response.ContentType = "application/dns-message"; response.ContentLength = mS.Length; using (Stream s = response.Body) { await mS.CopyToAsync(s, 512); } } _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, dnsResponse); _stats.QueueUpdate(dnsRequest, remoteEP, DnsTransportProtocol.Https, dnsResponse); } 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.UseSpecifiedNetworks: if (_recursionDeniedNetworks is not null) { foreach (NetworkAddress deniedNetworkAddress in _recursionDeniedNetworks) { if (deniedNetworkAddress.Contains(remoteIP)) return false; } } if (_recursionAllowedNetworks is not null) { foreach (NetworkAddress allowedNetworkAddress in _recursionAllowedNetworks) { if (allowedNetworkAddress.Contains(remoteIP)) return true; } } if (IPAddress.IsLoopback(remoteIP)) return true; return false; default: return false; } } private async Task PreProcessQueryAsync(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, false, 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, false, 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 PostProcessQueryAsync(request, remoteEP, protocol, await ProcessQueryAsync(unsignedRequest, remoteEP, protocol, isRecursionAllowed, false, request.TsigKeyName)); 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, false, DnsResponseCode.BADVERS, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; } return await PostProcessQueryAsync(request, remoteEP, protocol, await ProcessQueryAsync(request, remoteEP, protocol, isRecursionAllowed, false, null)); } 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); } 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, false, 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, false, 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, false, 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, false, 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, false, 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, false, 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, false, 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, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, 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, false, DnsResponseCode.NotImplemented, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { AuthZoneInfo authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((authZoneInfo is null) || (authZoneInfo.Type != AuthZoneType.Secondary) || authZoneInfo.Disabled) return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.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) { _log?.Write(remoteEP, protocol, "DNS Server refused a NOTIFY request since the request IP address was not recognized by the secondary zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA)) { IReadOnlyList localSoaRecords = authZoneInfo.GetApexRecords(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 }; } } authZoneInfo.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 authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((authZoneInfo is null) || authZoneInfo.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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); async Task IsZoneNameServerAllowedAsync() { IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await authZoneInfo.GetSecondaryNameServerAddressesAsync(this); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } bool IsSpecifiedIpAddressAllowed() { IPAddress remoteAddress = remoteEP.Address; IReadOnlyCollection specifiedIpAddresses = authZoneInfo.UpdateIpAddresses; if (specifiedIpAddresses is not null) { foreach (IPAddress specifiedIpAddress in specifiedIpAddresses) { if (specifiedIpAddress.Equals(remoteAddress)) return true; } } return false; } async Task IsUpdatePermittedAsync() { bool isUpdateAllowed; switch (authZoneInfo.Update) { case AuthZoneUpdate.Allow: isUpdateAllowed = true; break; case AuthZoneUpdate.AllowOnlyZoneNameServers: isUpdateAllowed = await IsZoneNameServerAllowedAsync(); break; case AuthZoneUpdate.AllowOnlySpecifiedIpAddresses: isUpdateAllowed = IsSpecifiedIpAddressAllowed(); break; case AuthZoneUpdate.AllowBothZoneNameServersAndSpecifiedIpAddresses: isUpdateAllowed = IsSpecifiedIpAddressAllowed() || 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return false; } //check security policies if ((authZoneInfo.UpdateSecurityPolicies is not null) && (authZoneInfo.UpdateSecurityPolicies.Count > 0)) { if ((tsigAuthenticatedKeyName is null) || !authZoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLower(), 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return false; } } } return true; } switch (authZoneInfo.Type) { case AuthZoneType.Primary: //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(authZoneInfo.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(authZoneInfo.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(authZoneInfo.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(authZoneInfo.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(authZoneInfo.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.ToLower(); 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(authZoneInfo.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(authZoneInfo.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(authZoneInfo.Name, uRecord.Name) && (_authZoneManager.GetRecords(authZoneInfo.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(authZoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.SetRecord(authZoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.DNAME) { IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.SetRecord(authZoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.SOA) { if (!uRecord.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //can add SOA only to apex IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.SetRecord(authZoneInfo.Name, uRecord); } else { if (_authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count > 0) continue; //current name contains CNAME so cannot add non-CNAME record IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); if (uRecord.Type == DnsResourceRecordType.NS) uRecord.SyncGlueRecords(request.Additional); _authZoneManager.AddRecord(authZoneInfo.Name, uRecord); } } else if (uRecord.Class == DnsClass.ANY) { if (uRecord.Type == DnsResourceRecordType.ANY) { //Delete all RRsets from a name IReadOnlyDictionary> existingRRSets = _authZoneManager.GetAllRecords(authZoneInfo.Name, uRecord.Name); if (uRecord.Name.Equals(authZoneInfo.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(authZoneInfo.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(authZoneInfo.Name, uRecord.Name, existingRRSet.Key); } } } else { //Delete an RRset if (uRecord.Name.Equals(authZoneInfo.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(authZoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.DeleteRecords(authZoneInfo.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(authZoneInfo.Name, uRecord.Name, uRecord.Type); if ((uRecord.Type == DnsResourceRecordType.NS) && (existingRRSet.Count == 1) && uRecord.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //no apex NS can be deleted if only 1 NS exists AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.DeleteRecord(authZoneInfo.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(authZoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key); else _authZoneManager.SetRecords(authZoneInfo.Name, originalRRSet.Value); } } throw; } } _authZoneManager.SaveZoneFile(authZoneInfo.Name); _log?.Write(remoteEP, protocol, "DNS Server successfully processed a zone UPDATE request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); //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: //forward to primary { IReadOnlyList primaryNameServers = await authZoneInfo.GetPrimaryNameServerAddressesAsync(this); DnsResourceRecord soaRecord = authZoneInfo.GetApexRecords(DnsResourceRecordType.SOA)[0]; AuthRecordInfo recordInfo = soaRecord.GetAuthRecordInfo(); switch (recordInfo.ZoneTransferProtocol) { case DnsTransportProtocol.Tls: case DnsTransportProtocol.Quic: { //change name server protocol to TLS/QUIC List updatedNameServers = new List(primaryNameServers.Count); foreach (NameServerAddress primaryNameServer in primaryNameServers) { if (primaryNameServer.Protocol == recordInfo.ZoneTransferProtocol) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.ChangeProtocol(recordInfo.ZoneTransferProtocol)); } primaryNameServers = updatedNameServers; } break; default: if (protocol == DnsTransportProtocol.Tcp) { //change name server protocol to TCP List updatedNameServers = new List(primaryNameServers.Count); foreach (NameServerAddress primaryNameServer in primaryNameServers) { if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); } primaryNameServers = 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 secondary zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); DnsClient dnsClient = new DnsClient(primaryNameServers); 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.ResolveAsync(newRequest); else newResponse = await dnsClient.ResolveAsync(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 authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((authZoneInfo is null) || authZoneInfo.Disabled || authZoneInfo.IsExpired) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request due to zone not found, zone disabled, or zone expired reasons for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } switch (authZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: break; default: _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the DNS server is not authoritative for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); 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() { IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await authZoneInfo.GetSecondaryNameServerAddressesAsync(this); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } bool IsSpecifiedNameServerAllowed() { IPAddress remoteAddress = remoteEP.Address; IReadOnlyCollection specifiedNameServers = authZoneInfo.ZoneTransferNameServers; if (specifiedNameServers is not null) { foreach (IPAddress specifiedNameServer in specifiedNameServers) { if (specifiedNameServer.Equals(remoteAddress)) return true; } } return false; } bool isZoneTransferAllowed = false; switch (authZoneInfo.ZoneTransfer) { case AuthZoneTransfer.Deny: break; case AuthZoneTransfer.Allow: isZoneTransferAllowed = true; break; case AuthZoneTransfer.AllowOnlyZoneNameServers: isZoneTransferAllowed = await IsZoneNameServerAllowedAsync(); break; case AuthZoneTransfer.AllowOnlySpecifiedNameServers: isZoneTransferAllowed = IsSpecifiedNameServerAllowed(); break; case AuthZoneTransfer.AllowBothZoneAndSpecifiedNameServers: isZoneTransferAllowed = IsSpecifiedNameServerAllowed() || await IsZoneNameServerAllowedAsync(); break; } if (!isZoneTransferAllowed) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } if ((authZoneInfo.ZoneTransferTsigKeyNames is not null) && (authZoneInfo.ZoneTransferTsigKeyNames.Count > 0)) { if ((tsigAuthenticatedKeyName is null) || !authZoneInfo.ZoneTransferTsigKeyNames.ContainsKey(tsigAuthenticatedKeyName.ToLower())) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); 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(); 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; 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, remoteEP, response, isRecursionAllowed, protocol, false, skipDnsAppAuthoritativeRequestHandlers); case DnsResourceRecordType.ANAME: return await ProcessANAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol, 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 using empty conditional forwarders; name servers will be provided via ResolverDnsCache return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, Array.Empty(), _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } break; case DnsResourceRecordType.FWD: //do conditional forwarding return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); case DnsResourceRecordType.APP: response = await ProcessAPPAsync(request, remoteEP, response, isRecursionAllowed, protocol); reprocessResponse = true; break; } } } } while (reprocessResponse); return response; } private async Task AuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { DnsDatagram response = _authZoneManager.Query(request, isRecursionAllowed); if (response is not null) { response.Tag = DnsServerResponseType.Authoritative; return response; } if (!skipDnsAppAuthoritativeRequestHandlers) { foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers) { try { DnsDatagram appResponse = await requestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed); if (appResponse is not null) { if (appResponse.Tag is null) appResponse.Tag = DnsServerResponseType.Authoritative; return appResponse; } } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } } return null; } private async Task ProcessAPPAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol) { 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) { //return FWD response rcode = DnsResponseCode.NoError; 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); } else { //return NODATA/NXDOMAIN response if (request.Question[0].Name.Length > appResourceRecord.Name.Length) rcode = DnsResponseCode.NxDomain; else rcode = DnsResponseCode.NoError; authority = zoneInfo.GetApexRecords(DnsResourceRecordType.SOA); } return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, 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.GetApexRecords(DnsResourceRecordType.SOA); return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.ServerFailure, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessCNAMEAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, 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, false, 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().Type == DnsResourceRecordType.ANAME)) { newResponse = await ProcessANAMEAsync(request, remoteEP, newResponse, isRecursionAllowed, protocol, 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, Array.Empty(), _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, remoteEP, newResponse, isRecursionAllowed, protocol); 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 answerRecord in newAnswer) { if (answerRecord.Type != DnsResourceRecordType.CNAME) continue; if (answerRecord.RDATA.Equals(lastRR.RDATA)) { foundRepeat = true; break; } } if (foundRepeat) break; //loop detected newAnswer.AddRange(newResponse.Answer); 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, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, 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, false, 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, Array.Empty(), _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, remoteEP, newResponse, isRecursionAllowed, protocol); 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; } if (lastRR.Type == DnsResourceRecordType.ANAME) { 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(); } } else if (lastRR.Type == DnsResourceRecordType.CNAME) { lastDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain; } else { //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) { if (answer.Type == DnsResourceRecordType.ANAME) { resolveQueue.Enqueue(ResolveANAMEAsync(answer)); } else { if (resolveQueue.Count == 0) responseAnswer.Add(answer); } } 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.GetAuthRecordInfo().LastUsedOn = utcNow; } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, responseAnswer, authority, null) { Tag = response.Tag }; } private DnsDatagram ProcessBlockedQuery(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[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, 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[] { 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[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _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: { List rrList = new List(aRecords.Count); foreach (DnsARecordData record in aRecords) rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 60, record)); answer = rrList; } break; case DnsResourceRecordType.AAAA: { List rrList = new List(aaaaRecords.Count); foreach (DnsAAAARecordData record in aaaaRecords) rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 60, record)); answer = rrList; } 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 ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { bool inAllowedZone; 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 inAllowedZone = true; } else if (!_enableBlocking) { inAllowedZone = true; } else { inAllowedZone = _allowedZoneManager.IsAllowed(request) || _blockListZoneManager.IsAllowed(request); if (!inAllowedZone) { //check in blocked zone and block list zone DnsDatagram blockedResponse = ProcessBlockedQuery(request); 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, remoteEP, response, true, protocol, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); if (!inAllowedZone) { //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); //check allowed zone inAllowedZone = _allowedZoneManager.IsAllowed(newRequest) || _blockListZoneManager.IsAllowed(newRequest); if (inAllowedZone) break; //CNAME is in allowed zone //check blocked zone and block list zone DnsDatagram blockedResponse = ProcessBlockedQuery(newRequest); 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 conditionalForwardingClientSubnet = false; if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is null) { 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, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else if (requestECS.ConditionalForwardingClientSubnet) { conditionalForwardingClientSubnet = true; eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); } else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.Address)) { //disable ECS option request.ShadowHideEDnsClientSubnetOption(); } 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 { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { conditionalForwardingClientSubnet = requestECS.ConditionalForwardingClientSubnet; if (conditionalForwardingClientSubnet) eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); else request.ShadowHideEDnsClientSubnetOption(); //hide ECS option } } if (!cachePrefetchOperation && !cacheRefreshOperation) { //query cache zone to see if answer available DnsDatagram cacheResponse = QueryCache(request, 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 RecursiveResolveAsync(question, eDnsClientSubnet, conditionalForwardingClientSubnet, 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 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 RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception and no stale record was found } else { //wait timed out if (_serveStale) { //query cache zone to return stale answer (if available) as per draft-ietf-dnsop-serve-stale-04 DnsDatagram staleResponse = QueryCache(request, true); if (staleResponse is not 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 RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); } //no response available from resolver or resolver had exception and no stale record was found } } //no response available; respond with ServerFailure EDnsOption[] options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Waiting for resolver")) }; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.ServerFailure, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); } private async Task RecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool conditionalForwardingClientSubnet, 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(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, skipDnsAppAuthoritativeRequestHandlers, question); else if (skipDnsAppAuthoritativeRequestHandlers || conditionalForwardingClientSubnet) dnsCache = new ResolverDnsCache(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, true); //to prevent request reaching apps again else dnsCache = _dnsCache; //check for this-server if ((conditionalForwarders is not null) && (conditionalForwarders.Count == 1) && (conditionalForwarders[0].RDATA is DnsForwarderRecordData fwd) && fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { //resolve directly with DNSSEC validation preference conditionalForwarders = null; dnssecValidation = fwd.DnssecValidation; } DnsDatagram response; if ((conditionalForwarders is not null) && (conditionalForwarders.Count > 0)) { //check for forwarder name server resolution foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) continue; NetProxy proxy = forwarder.Proxy; if (proxy is null) proxy = _proxy; if (proxy is null) { //recursive resolve name server when proxy is null else let proxy resolve it if (forwarder.NameServer.IsIPEndPointStale) //refresh forwarder IPEndPoint if stale await forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout); } } if (conditionalForwarders.Count == 1) { DnsResourceRecord conditionalForwarder = conditionalForwarders[0]; response = await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, conditionalForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name); } else { using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { CancellationToken cancellationToken = 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; if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) continue; tasks.Add(Task.Factory.StartNew(delegate () { return ConditionalForwarderResolveAsync(question, eDnsClientSubnet, conditionalForwardingClientSubnet, dnsCache, forwarder, conditionalForwarder.Name, cancellationToken); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current).Unwrap()); } //wait for first positive response, or for all tasks to fault response = null; DnsDatagram lastResponse = null; Exception lastException = null; while (tasks.Count > 0) { Task completedTask = await Task.WhenAny(tasks); if (completedTask.Status == TaskStatus.RanToCompletion) { //resolver task complete DnsDatagram taskResponse = await completedTask; //await to get response bool foundResponse = false; switch (taskResponse.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: cancellationTokenSource.Cancel(); //to stop other resolver tasks response = taskResponse; foundResponse = true; break; default: //keep response lastResponse = taskResponse; break; } if (foundResponse) break; } tasks.Remove(completedTask); lastException = completedTask.Exception; if (lastException is AggregateException) lastException = lastException.InnerException; } if (response is null) { if (lastResponse is not null) response = lastResponse; else if (lastException is not null) ExceptionDispatchInfo.Capture(lastException).Throw(); else throw new InvalidOperationException(); } } } } else if ((conditionalForwarders is null) && (_forwarders is not null) && (_forwarders.Count > 0)) { //use forwarders if (_proxy is 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, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout); } } //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 response = await dnsClient.ResolveAsync(question); } else { //do recursive resolution response = await DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, _nsRevalidation, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverMaxStackCount, true, true); } 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 (_log 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(); } } _log.Write("DNS Server failed to resolve the request '" + question.ToString() + "'" + (strForwarders is null ? "" : " using forwarders: " + strForwarders) + ".\r\n" + ex.ToString()); } if (_serveStale) { //fetch stale record 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); 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 (ex.InnerException is SocketException ex3a) { if (ex3a.SocketErrorCode == SocketError.TimedOut) options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out")) }; else options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex3a.SocketErrorCode.ToString())) }; } else { options = new EDnsOption[] { 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[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out")) }; else options = new EDnsOption[] { 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[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out")) }; else options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex5a.SocketErrorCode.ToString())) }; } else { options = new EDnsOption[] { 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[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Server 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 Task ConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool conditionalForwardingClientSubnet, IDnsCache dnsCache, DnsForwarderRecordData forwarder, string conditionalForwardingZoneCut, CancellationToken cancellationToken = default) { NetProxy proxy = forwarder.Proxy; if (proxy is null) proxy = _proxy; DnsClient dnsClient = new DnsClient(forwarder.NameServer); 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 = forwarder.DnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.ConditionalForwardingClientSubnet = conditionalForwardingClientSubnet; 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) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, resolveResponse.CheckingDisabledResponse.AuthenticData, resolveResponse.CheckingDisabledResponse.CheckingDisabled, 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 ((request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0))) { //copy options as new OPT and keep other records List newAdditional = new List(additional.Count); foreach (DnsResourceRecord record in additional) { switch (record.Type) { case DnsResourceRecordType.OPT: continue; case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.DNSKEY: if (dnssecOk) break; continue; } newAdditional.Add(record); } IReadOnlyList options; if (response.GetEDnsClientSubnetOption(true) is not null) { //response contains ECS if (request.GetEDnsClientSubnetOption(true) is not null) { //request has ECS and type is supported; keep ECS in response options = response.EDNS.Options; } else { //cache does not support the qtype so remove ECS from response if (response.EDNS.Options.Count == 1) { options = Array.Empty(); } else { List newOptions = new List(response.EDNS.Options.Count); foreach (EDnsOption option in response.EDNS.Options) { if (option.Code != EDnsOptionCode.EDNS_CLIENT_SUBNET) newOptions.Add(option); } options = newOptions; } } } else { options = response.EDNS.Options; } if (response.DnsClientExtendedErrors.Count > 0) { //add dns client extended errors List newOptions = new List(options.Count + response.DnsClientExtendedErrors.Count); newOptions.AddRange(options); foreach (EDnsExtendedDnsErrorOptionData ee in response.DnsClientExtendedErrors) newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, ee)); options = newOptions; } newAdditional.Add(DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options)); additional = newAdditional; } else if (response.EDNS is not null) { //remove OPT from additional additional = RemoveOPTFromAdditional(additional, dnssecOk); } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, response.AuthenticData, response.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 serveStaleAndResetExpiry) { DnsDatagram cacheResponse = _cacheZoneManager.Query(request, serveStaleAndResetExpiry); 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) { _log?.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) { _log?.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); 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); 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); } } } private bool IsQpmLimitCrossed(IPAddress remoteIP) { if ((_qpmLimitRequests < 1) && (_qpmLimitErrors < 1)) return false; if (IPAddress.IsLoopback(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 _qpmLimitClientSubnetStats, out _qpmLimitErrorClientSubnetStats); } catch (Exception ex) { _log?.Write(ex); } finally { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite); } } } 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() { 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 }; IReadOnlyList localAddresses = GetValidKestralLocalAddresses(_localEndPoints.Convert(delegate (IPEndPoint ep) { return ep.Address; })); 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 && (_certificate is not null)) { serverOptions.ConfigureHttpsDefaults(delegate (HttpsConnectionAdapterOptions configureOptions) { configureOptions.ServerCertificateSelector = delegate (ConnectionContext context, string dnsName) { return _certificate; }; }); foreach (IPAddress localAddress in localAddresses) { serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions) { listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; listenOptions.UseHttps(); }); } } 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.Add("X-Robots-Tag", "noindex, nofollow"); ctx.Context.Response.Headers.Add("Cache-Control", "private, max-age=300"); }, ServeUnknownFileTypes = true }); _dohWebService.UseRouting(); _dohWebService.MapGet("/dns-query", ProcessDoHRequestAsync); _dohWebService.MapPost("/dns-query", ProcessDoHRequestAsync); try { 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 && (_certificate 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 && (_certificate 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) { await _dohWebService.DisposeAsync(); _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) 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 = 512 * 1024; udpListener.SendBufferSize = 512 * 1024; udpListener.Bind(localEP); _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(); } 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 (_enableDnsOverTls && (_certificate 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 && (_certificate is not null)) { IPEndPoint quicEP = new IPEndPoint(localEP.Address, _dnsOverQuicPort); QuicListener quicListener = null; try { QuicListenerOptions listenerOptions = new QuicListenerOptions() { ListenEndPoint = quicEP, ListenBacklog = _listenBacklog, ApplicationProtocols = new List() { new SslApplicationProtocol("doq") }, 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 = new SslServerAuthenticationOptions { ApplicationProtocols = new List() { new SslApplicationProtocol("doq") }, ServerCertificate = _certificate } }; 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); }, 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 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 && (_certificate 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(); } 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) udpListener.Dispose(); foreach (Socket tcpListener in _tcpListeners) tcpListener.Dispose(); foreach (Socket tlsListener in _tlsListeners) tlsListener.Dispose(); foreach (QuicListener quicListener in _quicListeners) await quicListener.DisposeAsync(); _udpListeners.Clear(); _tcpListeners.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 ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, null).WithTimeout(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.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 LogManager LogManager { get { return _log; } set { _log = value; } } 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 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 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 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 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 EnableDnsOverQuic { get { return _enableDnsOverQuic; } set { _enableDnsOverQuic = value; } } public int DnsOverHttpPort { get { return _dnsOverHttpPort; } set { _dnsOverHttpPort = value; } } public int DnsOverTlsPort { get { return _dnsOverTlsPort; } set { _dnsOverTlsPort = value; } } public int DnsOverHttpsPort { get { return _dnsOverHttpsPort; } set { _dnsOverHttpsPort = value; } } public int DnsOverQuicPort { get { return _dnsOverQuicPort; } set { _dnsOverQuicPort = value; } } public X509Certificate2 Certificate { get { return _certificate; } set { if ((value is not null) && !value.HasPrivateKey) throw new ArgumentException("Tls certificate does not contain private key."); _certificate = value; } } public IReadOnlyDictionary TsigKeys { get { return _tsigKeys; } set { _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 RecursionDeniedNetworks { get { return _recursionDeniedNetworks; } set { if ((value is not null) && (value.Count > byte.MaxValue)) throw new ArgumentOutOfRangeException(nameof(RecursionDeniedNetworks), "Networks cannot be more than 255."); _recursionDeniedNetworks = value; } } public IReadOnlyCollection RecursionAllowedNetworks { get { return _recursionAllowedNetworks; } set { if ((value is not null) && (value.Count > byte.MaxValue)) throw new ArgumentOutOfRangeException(nameof(RecursionAllowedNetworks), "Networks cannot be more than 255."); _recursionAllowedNetworks = 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 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 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 DnsServerBlockingType BlockingType { get { return _blockingType; } set { _blockingType = value; } } 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 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 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 }