/* Technitium DNS Server Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; using DnsServerCore.Dns.Applications; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using System; using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Mail; using System.Net.Quic; using System.Net.Security; using System.Net.Sockets; using System.Runtime.ExceptionServices; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ClientConnection; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Proxy; using TechnitiumLibrary.Net.ProxyProtocol; namespace DnsServerCore.Dns { #pragma warning disable CA2252 // This API requires opting into preview features #pragma warning disable CA1416 // Validate platform compatibility public enum DnsServerRecursion : byte { Deny = 0, Allow = 1, AllowOnlyForPrivateNetworks = 2, UseSpecifiedNetworkACL = 3 } public enum DnsServerBlockingType : byte { AnyAddress = 0, NxDomain = 1, CustomAddress = 2 } public sealed class DnsServer : IAsyncDisposable, IDisposable, IDnsClient { #region enum enum ServiceState { Stopped = 0, Starting = 1, Running = 2, Stopping = 3 } #endregion #region variables internal const int MAX_CNAME_HOPS = 16; internal const int SERVE_STALE_MAX_WAIT_TIME = 1800; //max time to wait before serve stale [RFC 8767] const int SERVE_STALE_TIME_DIFFERENCE = 200; //200ms before client timeout [RFC 8767] internal const int RECURSIVE_RESOLUTION_TIMEOUT = 60000; //max time that can be spent per recursive resolution task static readonly IPEndPoint IPENDPOINT_ANY_0 = new IPEndPoint(IPAddress.Any, 0); static readonly IReadOnlyCollection _aRecords = [new DnsARecordData(IPAddress.Any)]; static readonly IReadOnlyCollection _aaaaRecords = [new DnsAAAARecordData(IPAddress.IPv6Any)]; static readonly List quicApplicationProtocols = new List() { new SslApplicationProtocol("doq") }; string _serverDomain; readonly string _configFolder; readonly string _dohwwwFolder; IReadOnlyList _localEndPoints; LogManager _log; MailAddress _responsiblePerson; MailAddress _defaultResponsiblePerson; NameServerAddress _thisServer; readonly List _udpListeners = new List(); readonly List _udpProxyListeners = new List(); readonly List _tcpListeners = new List(); readonly List _tcpProxyListeners = new List(); readonly List _tlsListeners = new List(); readonly List _quicListeners = new List(); WebApplication _dohWebService; readonly AuthZoneManager _authZoneManager; readonly AllowedZoneManager _allowedZoneManager; readonly BlockedZoneManager _blockedZoneManager; readonly BlockListZoneManager _blockListZoneManager; readonly CacheZoneManager _cacheZoneManager; readonly DnsApplicationManager _dnsApplicationManager; readonly ResolverDnsCache _dnsCache; readonly ResolverDnsCache _dnsCacheSkipDnsApps; //to prevent request reaching apps again readonly StatsManager _stats; IReadOnlyCollection _zoneTransferAllowedNetworks; IReadOnlyCollection _notifyAllowedNetworks; bool _preferIPv6; ushort _udpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; bool _dnssecValidation = true; bool _eDnsClientSubnet; byte _eDnsClientSubnetIPv4PrefixLength = 24; byte _eDnsClientSubnetIPv6PrefixLength = 56; NetworkAddress _eDnsClientSubnetIpv4Override; NetworkAddress _eDnsClientSubnetIpv6Override; int _qpmLimitRequests = 6000; //100qps int _qpmLimitErrors = 600; //10qps int _qpmLimitSampleMinutes = 5; int _qpmLimitIPv4PrefixLength = 24; int _qpmLimitIPv6PrefixLength = 56; IReadOnlyCollection _qpmLimitBypassList; int _clientTimeout = 2000; int _tcpSendTimeout = 10000; int _tcpReceiveTimeout = 10000; int _quicIdleTimeout = 60000; int _quicMaxInboundStreams = 100; int _listenBacklog = 100; bool _enableDnsOverUdpProxy; bool _enableDnsOverTcpProxy; bool _enableDnsOverHttp; bool _enableDnsOverTls; bool _enableDnsOverHttps; bool _enableDnsOverHttp3; bool _enableDnsOverQuic; int _dnsOverUdpProxyPort = 538; int _dnsOverTcpProxyPort = 538; int _dnsOverHttpPort = 80; int _dnsOverTlsPort = 853; int _dnsOverHttpsPort = 443; int _dnsOverQuicPort = 853; X509Certificate2Collection _certificateCollection; SslServerAuthenticationOptions _sslServerAuthenticationOptions; SslServerAuthenticationOptions _quicSslServerAuthenticationOptions; string _dnsOverHttpRealIpHeader = "X-Real-IP"; IReadOnlyDictionary _tsigKeys; DnsServerRecursion _recursion; IReadOnlyCollection _recursionNetworkACL; bool _randomizeName; bool _qnameMinimization; bool _nsRevalidation; int _resolverRetries = 2; int _resolverTimeout = 1500; int _resolverConcurrency = 2; int _resolverMaxStackCount = 16; bool _serveStale = true; int _serveStaleMaxWaitTime = SERVE_STALE_MAX_WAIT_TIME; int _cachePrefetchEligibility = 2; int _cachePrefetchTrigger = 9; int _cachePrefetchSampleIntervalInMinutes = 5; int _cachePrefetchSampleEligibilityHitsPerHour = 30; bool _enableBlocking = true; bool _allowTxtBlockingReport = true; IReadOnlyCollection _blockingBypassList; DnsServerBlockingType _blockingType = DnsServerBlockingType.NxDomain; uint _blockingAnswerTtl = 30; IReadOnlyCollection _customBlockingARecords = Array.Empty(); IReadOnlyCollection _customBlockingAAAARecords = Array.Empty(); NetProxy _proxy; IReadOnlyList _forwarders; bool _concurrentForwarding = true; int _forwarderRetries = 3; int _forwarderTimeout = 2000; int _forwarderConcurrency = 2; LogManager _resolverLog; LogManager _queryLog; Timer _cachePrefetchSamplingTimer; readonly object _cachePrefetchSamplingTimerLock = new object(); const int CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL = 5000; Timer _cachePrefetchRefreshTimer; readonly object _cachePrefetchRefreshTimerLock = new object(); const int CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL = 10000; DateTime _cachePrefetchSamplingTimerTriggersOn; IList _cacheRefreshSampleList; Timer _cacheMaintenanceTimer; readonly object _cacheMaintenanceTimerLock = new object(); const int CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL = 5 * 60 * 1000; const int CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL = 5 * 60 * 1000; Timer _qpmLimitSamplingTimer; readonly object _qpmLimitSamplingTimerLock = new object(); const int QPM_LIMIT_SAMPLING_TIMER_INTERVAL = 10000; IReadOnlyDictionary _qpmLimitClientSubnetStats; IReadOnlyDictionary _qpmLimitErrorClientSubnetStats; readonly IndependentTaskScheduler _queryTaskScheduler = new IndependentTaskScheduler(); readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(ThreadPriority.AboveNormal); readonly ConcurrentDictionary> _resolverTasks = new ConcurrentDictionary>(); volatile ServiceState _state = ServiceState.Stopped; #endregion #region constructor static DnsServer() { //set min threads since the default value is too small { ThreadPool.GetMinThreads(out int minWorker, out int minIOC); int minThreads = Environment.ProcessorCount * 16; if (minWorker < minThreads) minWorker = minThreads; if (minIOC < minThreads) minIOC = minThreads; ThreadPool.SetMinThreads(minWorker, minIOC); } } public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, LogManager log = null) : this(serverDomain, configFolder, dohwwwFolder, new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) }, log) { } public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, IPEndPoint localEndPoint, LogManager log = null) : this(serverDomain, configFolder, dohwwwFolder, new IPEndPoint[] { localEndPoint }, log) { } public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, IReadOnlyList localEndPoints, LogManager log = null) { _serverDomain = serverDomain; _configFolder = configFolder; _dohwwwFolder = dohwwwFolder; _localEndPoints = localEndPoints; _log = log; _authZoneManager = new AuthZoneManager(this); _allowedZoneManager = new AllowedZoneManager(this); _blockedZoneManager = new BlockedZoneManager(this); _blockListZoneManager = new BlockListZoneManager(this); _cacheZoneManager = new CacheZoneManager(this); _dnsApplicationManager = new DnsApplicationManager(this); _dnsCache = new ResolverDnsCache(this, false); _dnsCacheSkipDnsApps = new ResolverDnsCache(this, true); //to prevent request reaching apps again //init stats _stats = new StatsManager(this); } #endregion #region IDisposable bool _disposed; public async ValueTask DisposeAsync() { if (_disposed) return; await StopAsync(); _authZoneManager?.Dispose(); _allowedZoneManager?.Dispose(); _blockedZoneManager?.Dispose(); _dnsApplicationManager?.Dispose(); _stats?.Dispose(); _disposed = true; } public void Dispose() { DisposeAsync().Sync(); } #endregion #region private private async Task ReadUdpRequestAsync(Socket udpListener, DnsTransportProtocol protocol) { byte[] recvBuffer; if (protocol == DnsTransportProtocol.UdpProxy) recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE + 256]; else recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE]; using MemoryStream recvBufferStream = new MemoryStream(recvBuffer); try { int localPort = (udpListener.LocalEndPoint as IPEndPoint).Port; EndPoint epAny; switch (udpListener.AddressFamily) { case AddressFamily.InterNetwork: epAny = new IPEndPoint(IPAddress.Any, 0); break; case AddressFamily.InterNetworkV6: epAny = new IPEndPoint(IPAddress.IPv6Any, 0); break; default: throw new NotSupportedException("AddressFamily not supported."); } SocketReceiveMessageFromResult result; while (true) { recvBufferStream.SetLength(DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE); //resetting length before using buffer try { result = await udpListener.ReceiveMessageFromAsync(recvBuffer, SocketFlags.None, epAny); } catch (SocketException ex) { switch (ex.SocketErrorCode) { case SocketError.ConnectionReset: case SocketError.HostUnreachable: case SocketError.MessageSize: case SocketError.NetworkReset: result = default; break; default: throw; } } if (result.ReceivedBytes > 0) { if (result.RemoteEndPoint is not IPEndPoint remoteEP) continue; try { recvBufferStream.Position = 0; recvBufferStream.SetLength(result.ReceivedBytes); IPEndPoint returnEP = remoteEP; if (protocol == DnsTransportProtocol.UdpProxy) { if (!NetUtilities.IsPrivateIP(remoteEP.Address)) { //intentionally blocking public IP addresses from using DNS-over-UDP-PROXY //this feature is intended to be used with a reverse proxy or load balancer on private network continue; } ProxyProtocolStream proxyStream = await ProxyProtocolStream.CreateAsServerAsync(recvBufferStream); remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort); recvBufferStream.Position = proxyStream.DataOffset; } if (IsQpmLimitCrossed(remoteEP.Address)) { _stats.QueueUpdate(null, remoteEP, protocol, null, true); continue; } DnsDatagram request = DnsDatagram.ReadFrom(recvBufferStream); request.SetMetadata(new NameServerAddress(new IPEndPoint(result.PacketInformation.Address, localPort), DnsTransportProtocol.Udp)); _ = ProcessUdpRequestAsync(udpListener, remoteEP, returnEP, protocol, request); } catch (EndOfStreamException) { //ignore incomplete udp datagrams } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } } } catch (ObjectDisposedException) { //server stopping } catch (SocketException ex) { switch (ex.SocketErrorCode) { case SocketError.OperationAborted: case SocketError.Interrupted: break; //server stopping default: if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log?.Write(ex); break; } } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log?.Write(ex); } } private async Task ProcessUdpRequestAsync(Socket udpListener, IPEndPoint remoteEP, IPEndPoint returnEP, DnsTransportProtocol protocol, DnsDatagram request) { byte[] sendBuffer = null; try { DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { _stats.QueueUpdate(null, remoteEP, protocol, null, false); return; //drop request } //send response int sendBufferSize; if (request.EDNS is null) sendBufferSize = 512; else if (request.EDNS.UdpPayloadSize > _udpPayloadSize) sendBufferSize = _udpPayloadSize; else sendBufferSize = request.EDNS.UdpPayloadSize; sendBuffer = ArrayPool.Shared.Rent(sendBufferSize); using (MemoryStream sendBufferStream = new MemoryStream(sendBuffer, 0, sendBufferSize)) { try { response.WriteTo(sendBufferStream); } catch (NotSupportedException) { if (response.IsSigned) { //rfc8945 section 5.3 response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, null, null, new DnsResourceRecord[] { response.Additional[response.Additional.Count - 1] }, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; } else { switch (response.Question[0].Type) { case DnsResourceRecordType.MX: case DnsResourceRecordType.SRV: case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: //removing glue records and trying again since some mail servers fail to fallback to TCP on truncation //removing glue records to prevent truncation for SRV/SVCB/HTTPS response = response.CloneWithoutGlueRecords(); sendBufferStream.Position = 0; try { response.WriteTo(sendBufferStream); } catch (NotSupportedException) { //send TC since response is still big even after removing glue records response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; } break; case DnsResourceRecordType.IXFR: response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, false, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, new DnsResourceRecord[] { response.Answer[0] }, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; //truncate response break; default: response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; break; } } sendBufferStream.Position = 0; response.WriteTo(sendBufferStream); } //send dns datagram async await udpListener.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, returnEP); } _queryLog?.Write(remoteEP, protocol, request, response); _stats.QueueUpdate(request, remoteEP, protocol, response, false); } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _queryLog?.Write(remoteEP, protocol, request, null); _log?.Write(remoteEP, protocol, ex); } finally { if (sendBuffer is not null) ArrayPool.Shared.Return(sendBuffer); } } private async Task AcceptConnectionAsync(Socket tcpListener, DnsTransportProtocol protocol) { IPEndPoint localEP = tcpListener.LocalEndPoint as IPEndPoint; try { tcpListener.SendTimeout = _tcpSendTimeout; tcpListener.ReceiveTimeout = _tcpReceiveTimeout; tcpListener.NoDelay = true; while (true) { Socket socket = await tcpListener.AcceptAsync(); _ = ProcessConnectionAsync(socket, protocol); } } catch (SocketException ex) { if (ex.SocketErrorCode == SocketError.OperationAborted) return; //server stopping _log?.Write(localEP, protocol, ex); } catch (ObjectDisposedException) { //server stopped } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log?.Write(localEP, protocol, ex); } } private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol) { IPEndPoint remoteEP = null; try { remoteEP = socket.RemoteEndPoint as IPEndPoint; switch (protocol) { case DnsTransportProtocol.Tcp: await ReadStreamRequestAsync(new NetworkStream(socket), remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol); break; case DnsTransportProtocol.Tls: SslStream tlsStream = new SslStream(new NetworkStream(socket)); string serverName = null; await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return tlsStream.AuthenticateAsServerAsync(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { serverName = clientHelloInfo.ServerName; return ValueTask.FromResult(_sslServerAuthenticationOptions); }, null, cancellationToken1); }, _tcpReceiveTimeout); NameServerAddress dnsEP; if (string.IsNullOrEmpty(serverName)) dnsEP = new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tls); else dnsEP = new NameServerAddress(serverName, socket.LocalEndPoint as IPEndPoint, DnsTransportProtocol.Tls); await ReadStreamRequestAsync(tlsStream, remoteEP, dnsEP, protocol); break; case DnsTransportProtocol.TcpProxy: if (!NetUtilities.IsPrivateIP(remoteEP.Address)) { //intentionally blocking public IP addresses from using DNS-over-TCP-PROXY //this feature is intended to be used with a reverse proxy or load balancer on private network return; } ProxyProtocolStream proxyStream = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return ProxyProtocolStream.CreateAsServerAsync(new NetworkStream(socket), cancellationToken1); }, _tcpReceiveTimeout); remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort); await ReadStreamRequestAsync(proxyStream, remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol); break; default: throw new InvalidOperationException(); } } catch (AuthenticationException) { //ignore TLS auth exception } catch (TimeoutException) { //ignore timeout exception on TLS auth } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } finally { socket.Dispose(); } } private async Task ReadStreamRequestAsync(Stream stream, IPEndPoint remoteEP, NameServerAddress dnsEP, DnsTransportProtocol protocol) { try { using MemoryStream readBuffer = new MemoryStream(64); using MemoryStream writeBuffer = new MemoryStream(2048); using SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1); while (true) { if (IsQpmLimitCrossed(remoteEP.Address)) { _stats.QueueUpdate(null, remoteEP, protocol, null, true); break; } DnsDatagram request; //read dns datagram with timeout using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { Task task = DnsDatagram.ReadFromTcpAsync(stream, readBuffer, cancellationTokenSource.Token); if (await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) { //read timed out await stream.DisposeAsync(); return; } cancellationTokenSource.Cancel(); //cancel delay task request = await task; request.SetMetadata(dnsEP); } //process request async _ = ProcessStreamRequestAsync(stream, writeBuffer, writeSemaphore, remoteEP, request, protocol); } } catch (ObjectDisposedException) { //ignore } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } private async Task ProcessStreamRequestAsync(Stream stream, MemoryStream writeBuffer, SemaphoreSlim writeSemaphore, IPEndPoint remoteEP, DnsDatagram request, DnsTransportProtocol protocol) { try { DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { await stream.DisposeAsync(); _stats.QueueUpdate(null, remoteEP, protocol, null, false); return; //drop request } //send response await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1) { await writeSemaphore.WaitAsync(cancellationToken1); try { //send dns datagram await response.WriteToTcpAsync(stream, writeBuffer, cancellationToken1); await stream.FlushAsync(cancellationToken1); } finally { writeSemaphore.Release(); } }, _tcpSendTimeout); _queryLog?.Write(remoteEP, protocol, request, response); _stats.QueueUpdate(request, remoteEP, protocol, response, false); } catch (ObjectDisposedException) { //ignore } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { if (request is not null) _queryLog.Write(remoteEP, protocol, request, null); _log?.Write(remoteEP, protocol, ex); } } private async Task AcceptQuicConnectionAsync(QuicListener quicListener) { try { while (true) { QuicConnection quicConnection = await quicListener.AcceptConnectionAsync(); _ = ProcessQuicConnectionAsync(quicConnection); } } catch (ObjectDisposedException) { //server stopped } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log?.Write(quicListener.LocalEndPoint, DnsTransportProtocol.Quic, ex); } } private async Task ProcessQuicConnectionAsync(QuicConnection quicConnection) { try { NameServerAddress dnsEP; if (string.IsNullOrEmpty(quicConnection.TargetHostName)) dnsEP = new NameServerAddress(quicConnection.LocalEndPoint, DnsTransportProtocol.Quic); else dnsEP = new NameServerAddress(quicConnection.TargetHostName, quicConnection.LocalEndPoint, DnsTransportProtocol.Quic); while (true) { if (IsQpmLimitCrossed(quicConnection.RemoteEndPoint.Address)) { _stats.QueueUpdate(null, quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, null, true); break; } QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync(); _ = ProcessQuicStreamRequestAsync(quicStream, quicConnection.RemoteEndPoint, dnsEP); } } catch (QuicException ex) { switch (ex.QuicError) { case QuicError.ConnectionIdle: case QuicError.ConnectionAborted: case QuicError.ConnectionTimeout: break; default: _log?.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex); break; } } catch (Exception ex) { _log?.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex); } finally { await quicConnection.DisposeAsync(); } } private async Task ProcessQuicStreamRequestAsync(QuicStream quicStream, IPEndPoint remoteEP, NameServerAddress dnsEP) { MemoryStream sharedBuffer = new MemoryStream(512); DnsDatagram request = null; try { //read dns datagram with timeout using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { Task task = DnsDatagram.ReadFromTcpAsync(quicStream, sharedBuffer, cancellationTokenSource.Token); if (await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) { //read timed out quicStream.Abort(QuicAbortDirection.Both, (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR); return; } cancellationTokenSource.Cancel(); //cancel delay task request = await task; request.SetMetadata(dnsEP); } //process request async DnsDatagram response = await ProcessRequestAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address)); if (response is null) { _stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Quic, null, false); return; //drop request } //send response await response.WriteToTcpAsync(quicStream, sharedBuffer); _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, response); _stats.QueueUpdate(request, remoteEP, DnsTransportProtocol.Quic, response, false); } catch (IOException) { //ignore QuicException / IOException } catch (Exception ex) { if (request is not null) _queryLog.Write(remoteEP, DnsTransportProtocol.Quic, request, null); _log?.Write(remoteEP, DnsTransportProtocol.Quic, ex); } finally { await sharedBuffer.DisposeAsync(); await quicStream.DisposeAsync(); } } private async Task ProcessDoHRequestAsync(HttpContext context) { IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsOverHttpRealIpHeader); DnsDatagram dnsRequest = null; try { HttpRequest request = context.Request; HttpResponse response = context.Response; if (IsQpmLimitCrossed(remoteEP.Address)) { _stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, true); response.StatusCode = 429; await response.WriteAsync("Too Many Requests"); return; } if (!request.IsHttps) { //get the actual connection remote EP IPEndPoint connectionEp = context.GetRemoteEndPoint(null); if (!NetUtilities.IsPrivateIP(connectionEp.Address)) { //intentionally blocking public IP addresses from using DNS-over-HTTP (without TLS) //this feature is intended to be used with an SSL terminated reverse proxy like nginx on private network response.StatusCode = 403; await response.WriteAsync("DNS-over-HTTPS (DoH) queries are supported only on HTTPS."); return; } } switch (request.Method) { case "GET": bool acceptsDoH = false; string requestAccept = request.Headers.Accept; if (string.IsNullOrEmpty(requestAccept)) { acceptsDoH = true; } else { foreach (string mediaType in requestAccept.Split(',')) { if (mediaType.Equals("application/dns-message", StringComparison.OrdinalIgnoreCase)) { acceptsDoH = true; break; } } } if (!acceptsDoH) { response.Redirect((request.IsHttps ? "https://" : "http://") + request.Headers.Host); return; } string dnsRequestBase64Url = request.Query["dns"]; if (string.IsNullOrEmpty(dnsRequestBase64Url)) { response.StatusCode = 400; await response.WriteAsync("Bad Request"); return; } //convert from base64url to base64 dnsRequestBase64Url = dnsRequestBase64Url.Replace('-', '+'); dnsRequestBase64Url = dnsRequestBase64Url.Replace('_', '/'); //add padding int x = dnsRequestBase64Url.Length % 4; if (x > 0) dnsRequestBase64Url = dnsRequestBase64Url.PadRight(dnsRequestBase64Url.Length - x + 4, '='); using (MemoryStream mS = new MemoryStream(Convert.FromBase64String(dnsRequestBase64Url))) { dnsRequest = DnsDatagram.ReadFrom(mS); dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress())); } break; case "POST": if (!string.Equals(request.Headers.ContentType, "application/dns-message", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = 415; await response.WriteAsync("Unsupported Media Type"); return; } using (MemoryStream mS = new MemoryStream(32)) { await request.Body.CopyToAsync(mS, 32); mS.Position = 0; dnsRequest = DnsDatagram.ReadFrom(mS); dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress())); } break; default: throw new InvalidOperationException(); } DnsDatagram dnsResponse = await ProcessRequestAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address)); if (dnsResponse is null) { //drop request context.Connection.RequestClose(); _stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, false); return; } using (MemoryStream mS = new MemoryStream(512)) { dnsResponse.WriteTo(mS); mS.Position = 0; response.ContentType = "application/dns-message"; response.ContentLength = mS.Length; await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1) { await using (Stream s = response.Body) { await mS.CopyToAsync(s, 512, cancellationToken1); } }, _tcpSendTimeout); } _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, dnsResponse); _stats.QueueUpdate(dnsRequest, remoteEP, DnsTransportProtocol.Https, dnsResponse, false); } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { if (dnsRequest is not null) _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, null); _log?.Write(remoteEP, DnsTransportProtocol.Https, ex); } } private bool IsRecursionAllowed(IPAddress remoteIP) { switch (_recursion) { case DnsServerRecursion.Allow: return true; case DnsServerRecursion.AllowOnlyForPrivateNetworks: switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: case AddressFamily.InterNetworkV6: return NetUtilities.IsPrivateIP(remoteIP); default: return false; } case DnsServerRecursion.UseSpecifiedNetworkACL: return NetworkAccessControl.IsAddressAllowed(remoteIP, _recursionNetworkACL, true); default: return false; } } private async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { foreach (IDnsRequestController requestController in _dnsApplicationManager.DnsRequestControllers) { try { DnsRequestControllerAction action = await requestController.GetRequestActionAsync(request, remoteEP, protocol); switch (action) { case DnsRequestControllerAction.DropSilently: return null; //drop request case DnsRequestControllerAction.DropWithRefused: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; //drop request with refused } } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } if (request.ParsingException is not null) { //format error if (request.ParsingException is not IOException) _log?.Write(remoteEP, protocol, request.ParsingException); //format error response return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; } if (request.IsSigned) { if (!request.VerifySignedRequest(_tsigKeys, out DnsDatagram unsignedRequest, out DnsDatagram errorResponse)) { _log?.Write(remoteEP, protocol, "DNS Server received a request that failed TSIG signature verification (RCODE: " + errorResponse.RCODE + "; TSIG Error: " + errorResponse.TsigError + ")"); errorResponse.Tag = DnsServerResponseType.Authoritative; return errorResponse; } DnsDatagram unsignedResponse = await ProcessQueryAsync(unsignedRequest, remoteEP, protocol, isRecursionAllowed, false, request.TsigKeyName); if (unsignedResponse is null) return null; unsignedResponse = await PostProcessQueryAsync(request, remoteEP, protocol, unsignedResponse); if (unsignedResponse is null) return null; return unsignedResponse.SignResponse(request, _tsigKeys); } if (request.EDNS is not null) { if (request.EDNS.Version != 0) return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.BADVERS, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; } DnsDatagram response = await ProcessQueryAsync(request, remoteEP, protocol, isRecursionAllowed, false, null); if (response is null) return null; return await PostProcessQueryAsync(request, remoteEP, protocol, response); } private async Task PostProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { foreach (IDnsPostProcessor postProcessor in _dnsApplicationManager.DnsPostProcessors) { try { response = await postProcessor.PostProcessAsync(request, remoteEP, protocol, response); if (response is null) return null; } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } if (request.EDNS is null) { if (response.EDNS is not null) response = response.CloneWithoutEDns(); return response; } if (response.EDNS is not null) return response; IReadOnlyList options = null; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true); if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); if (response.Additional.Count == 0) return response.Clone(null, null, new DnsResourceRecord[] { DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options) }); if (response.IsSigned) return response; DnsResourceRecord[] newAdditional = new DnsResourceRecord[response.Additional.Count + 1]; for (int i = 0; i < response.Additional.Count; i++) newAdditional[i] = response.Additional[i]; newAdditional[response.Additional.Count] = DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); return response.Clone(null, null, newAdditional); } private async Task ProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, string tsigAuthenticatedKeyName) { if (request.IsResponse) return null; //drop response datagram to avoid loops in rare scenarios switch (request.OPCODE) { case DnsOpcode.StandardQuery: if (request.Question.Count != 1) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (request.Question[0].Class != DnsClass.IN) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; try { DnsQuestionRecord question = request.Question[0]; switch (question.Type) { case DnsResourceRecordType.AXFR: if (protocol == DnsTransportProtocol.Udp) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName); case DnsResourceRecordType.IXFR: return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName); case DnsResourceRecordType.FWD: case DnsResourceRecordType.APP: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } //query authoritative zone DnsDatagram response = await ProcessAuthoritativeQueryAsync(request, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); if (response is not null) { if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, response.RCODE, request.Question) { Tag = DnsServerResponseType.Authoritative }; return response; } if (!request.RecursionDesired || !isRecursionAllowed) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; //do recursive query if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } catch (InvalidDomainNameException) { //format error response return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } catch (TimeoutException ex) { DnsDatagram response = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative }; _log?.Write(remoteEP, protocol, request, response); _log?.Write(remoteEP, protocol, ex); return response; } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative }; } case DnsOpcode.Notify: return await ProcessNotifyQueryAsync(request, remoteEP, protocol); case DnsOpcode.Update: return await ProcessUpdateQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName); default: return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NotImplemented, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((zoneInfo is null) || ((zoneInfo.Type != AuthZoneType.Secondary) && (zoneInfo.Type != AuthZoneType.SecondaryForwarder) && (zoneInfo.Type != AuthZoneType.SecondaryCatalog)) || zoneInfo.Disabled) return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; async Task RemoteVerifiedAsync(IPAddress remoteAddress) { if (_notifyAllowedNetworks is not null) { foreach (NetworkAddress notifyAllowedNetwork in _notifyAllowedNetworks) { if (notifyAllowedNetwork.Contains(remoteAddress)) return true; } } IReadOnlyList primaryNameServerAddresses; SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone; if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers) primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses); else primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync(); foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } if (!await RemoteVerifiedAsync(remoteEP.Address)) { _log?.Write(remoteEP, protocol, "DNS Server refused a NOTIFY request since the request IP address was not recognized by the secondary zone: " + zoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } _log?.Write(remoteEP, protocol, "DNS Server received a NOTIFY request for secondary zone: " + zoneInfo.DisplayName); if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA)) { IReadOnlyList localSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (!DnsSOARecordData.IsZoneUpdateAvailable((localSoaRecords[0].RDATA as DnsSOARecordData).Serial, (request.Answer[0].RDATA as DnsSOARecordData).Serial)) { //no update was available return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } zoneInfo.TriggerRefresh(); return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } private async Task ProcessUpdateQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName) { if ((request.Question.Count != 1) || (request.Question[0].Type != DnsResourceRecordType.SOA)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (request.Question[0].Class != DnsClass.IN) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name); if ((zoneInfo is null) || zoneInfo.Disabled) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; _log?.Write(remoteEP, protocol, "DNS Server received a zone UPDATE request for zone: " + zoneInfo.DisplayName); async Task IsZoneNameServerAllowedAsync() { IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } async Task IsUpdatePermittedAsync() { bool isUpdateAllowed; switch (zoneInfo.Update) { case AuthZoneUpdate.Allow: isUpdateAllowed = true; break; case AuthZoneUpdate.AllowOnlyZoneNameServers: isUpdateAllowed = await IsZoneNameServerAllowedAsync(); break; case AuthZoneUpdate.UseSpecifiedNetworkACL: isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL); break; case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL) || await IsZoneNameServerAllowedAsync(); break; case AuthZoneUpdate.Deny: default: isUpdateAllowed = false; break; } if (!isUpdateAllowed) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request IP address is not allowed by the zone: " + zoneInfo.DisplayName); return false; } //check security policies if ((zoneInfo.UpdateSecurityPolicies is not null) && (zoneInfo.UpdateSecurityPolicies.Count > 0)) { if ((tsigAuthenticatedKeyName is null) || !zoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLowerInvariant(), out IReadOnlyDictionary> policyMap)) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request is missing TSIG auth required by the zone: " + zoneInfo.DisplayName); return false; } //check policy foreach (DnsResourceRecord uRecord in request.Authority) { bool isPermitted = false; foreach (KeyValuePair> policy in policyMap) { if ( uRecord.Name.Equals(policy.Key, StringComparison.OrdinalIgnoreCase) || (policy.Key.StartsWith("*.") && uRecord.Name.EndsWith(policy.Key.Substring(1), StringComparison.OrdinalIgnoreCase)) ) { foreach (DnsResourceRecordType allowedType in policy.Value) { if ((allowedType == DnsResourceRecordType.ANY) || (allowedType == uRecord.Type)) { isPermitted = true; break; } } if (isPermitted) break; } } if (!isPermitted) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request [" + uRecord.Name.ToLowerInvariant() + " " + uRecord.Type.ToString() + " " + uRecord.Class.ToString() + "] due to Dynamic Updates Security Policy for zone: " + zoneInfo.DisplayName); return false; } } } return true; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: //update { //process prerequisite section { Dictionary>> temp = new Dictionary>>(); foreach (DnsResourceRecord prRecord in request.Answer) { if (prRecord.TTL != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(prRecord.Name); if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (prRecord.Class == DnsClass.ANY) { if (prRecord.RDATA.RDLENGTH != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (prRecord.Type == DnsResourceRecordType.ANY) { //check if name is in use if (!_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else { //check if RRSet exists (value independent) IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type); if (rrset.Count == 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (prRecord.Class == DnsClass.NONE) { if (prRecord.RDATA.RDLENGTH != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (prRecord.Type == DnsResourceRecordType.ANY) { //check if name is not in use if (_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXDomain, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else { //check if RRSet does not exists IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type); if (rrset.Count > 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (prRecord.Class == request.Question[0].Class) { //check if RRSet exists (value dependent) //add to temp for later comparison string recordName = prRecord.Name.ToLowerInvariant(); if (!temp.TryGetValue(recordName, out Dictionary> rrsetEntry)) { rrsetEntry = new Dictionary>(); temp.Add(recordName, rrsetEntry); } if (!rrsetEntry.TryGetValue(prRecord.Type, out List rrset)) { rrset = new List(); rrsetEntry.Add(prRecord.Type, rrset); } rrset.Add(prRecord); } else { //FORMERR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } //compare collected RRSets in temp foreach (KeyValuePair>> zoneEntry in temp) { foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { IReadOnlyList prRRSet = rrsetEntry.Value; IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, zoneEntry.Key, rrsetEntry.Key); //check if RRSet exists (value dependent) //compare RRSets if (prRRSet.Count != rrset.Count) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; foreach (DnsResourceRecord prRecord in prRRSet) { bool found = false; foreach (DnsResourceRecord record in rrset) { if ( prRecord.Name.Equals(record.Name, StringComparison.OrdinalIgnoreCase) && (prRecord.Class == record.Class) && (prRecord.Type == record.Type) && (prRecord.RDATA.RDLENGTH == record.RDATA.RDLENGTH) && prRecord.RDATA.Equals(record.RDATA) ) { found = true; break; } } if (!found) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } } } //check for permissions if (!await IsUpdatePermittedAsync()) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; //process update section { //prescan foreach (DnsResourceRecord uRecord in request.Authority) { AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(uRecord.Name); if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (uRecord.Class == request.Question[0].Class) { switch (uRecord.Type) { case DnsResourceRecordType.ANY: case DnsResourceRecordType.AXFR: case DnsResourceRecordType.MAILA: case DnsResourceRecordType.MAILB: case DnsResourceRecordType.IXFR: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (uRecord.Class == DnsClass.ANY) { if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH != 0)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) { case DnsResourceRecordType.AXFR: case DnsResourceRecordType.MAILA: case DnsResourceRecordType.MAILB: case DnsResourceRecordType.IXFR: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (uRecord.Class == DnsClass.NONE) { if (uRecord.TTL != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) { case DnsResourceRecordType.ANY: case DnsResourceRecordType.AXFR: case DnsResourceRecordType.MAILA: case DnsResourceRecordType.MAILB: case DnsResourceRecordType.IXFR: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else { //FORMERR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } //update Dictionary>> originalRRSets = new Dictionary>>(); void AddToOriginalRRSets(string domain, DnsResourceRecordType type, IReadOnlyList existingRRSet) { if (!originalRRSets.TryGetValue(domain, out Dictionary> originalRRSetEntries)) { originalRRSetEntries = new Dictionary>(); originalRRSets.Add(domain, originalRRSetEntries); } originalRRSetEntries.TryAdd(type, existingRRSet); } try { foreach (DnsResourceRecord uRecord in request.Authority) { if (uRecord.Class == request.Question[0].Class) { //Add to an RRset if (uRecord.Type == DnsResourceRecordType.CNAME) { if (_authZoneManager.NameExists(zoneInfo.Name, uRecord.Name) && (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count == 0)) continue; //current name exists and has non-CNAME records so cannot add CNAME record IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.DNAME) { IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.SOA) { if (!uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //can add SOA only to apex IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else { if (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count > 0) continue; //current name contains CNAME so cannot add non-CNAME record IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); if (uRecord.Type == DnsResourceRecordType.NS) uRecord.SyncGlueRecords(request.Additional); _authZoneManager.AddRecord(zoneInfo.Name, uRecord); } } else if (uRecord.Class == DnsClass.ANY) { if (uRecord.Type == DnsResourceRecordType.ANY) { //Delete all RRsets from a name IReadOnlyDictionary> existingRRSets = _authZoneManager.GetEntriesFor(zoneInfo.Name, uRecord.Name); if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> existingRRSet in existingRRSets) { switch (existingRRSet.Key) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets } AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value); _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key); } } else { foreach (KeyValuePair> existingRRSet in existingRRSets) { switch (existingRRSet.Key) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //skip DNSSEC rrsets } AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value); _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key); } } } else { //Delete an RRset if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) { switch (uRecord.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets } } IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); } } else if (uRecord.Class == DnsClass.NONE) { //Delete an RR from an RRset switch (uRecord.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //no SOA can be deleted; skip DNSSEC rrsets } IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); if ((uRecord.Type == DnsResourceRecordType.NS) && (existingRRSet.Count == 1) && uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //no apex NS can be deleted if only 1 NS exists AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.DeleteRecord(zoneInfo.Name, uRecord.Name, uRecord.Type, uRecord.RDATA); } } } catch { //revert foreach (KeyValuePair>> originalRRSetEntries in originalRRSets) { foreach (KeyValuePair> originalRRSet in originalRRSetEntries.Value) { if (originalRRSet.Value.Count == 0) _authZoneManager.DeleteRecords(zoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key); else _authZoneManager.SetRecords(zoneInfo.Name, originalRRSet.Value); } } throw; } } _authZoneManager.SaveZoneFile(zoneInfo.Name); _log?.Write(remoteEP, protocol, "DNS Server successfully processed a zone UPDATE request for zone: " + zoneInfo.DisplayName); //NOERROR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: //forward { //check for permissions if (!await IsUpdatePermittedAsync()) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; //forward to primary IReadOnlyList primaryNameServerAddresses; DnsTransportProtocol primaryZoneTransferProtocol; SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone; if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers) { primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses); primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol; } else { primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync(); primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol; } switch (primaryZoneTransferProtocol) { case DnsTransportProtocol.Tls: case DnsTransportProtocol.Quic: { //change name server protocol to TLS/QUIC List updatedNameServers = new List(primaryNameServerAddresses.Count); foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.Protocol == primaryZoneTransferProtocol) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.ChangeProtocol(primaryZoneTransferProtocol)); } primaryNameServerAddresses = updatedNameServers; } break; default: if (protocol == DnsTransportProtocol.Tcp) { //change name server protocol to TCP List updatedNameServers = new List(primaryNameServerAddresses.Count); foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); } primaryNameServerAddresses = updatedNameServers; } break; } TsigKey key = null; if (!string.IsNullOrEmpty(tsigAuthenticatedKeyName) && ((_tsigKeys is null) || !_tsigKeys.TryGetValue(tsigAuthenticatedKeyName, out key))) throw new DnsServerException("DNS Server does not have TSIG key '" + tsigAuthenticatedKeyName + "' configured to authenticate dynamic updates for " + zoneInfo.TypeName + " zone: " + zoneInfo.DisplayName); DnsClient dnsClient = new DnsClient(primaryNameServerAddresses); dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = 1; DnsDatagram newRequest = request.Clone(); newRequest.SetRandomIdentifier(); DnsDatagram newResponse; if (key is null) newResponse = await dnsClient.RawResolveAsync(newRequest); else newResponse = await dnsClient.TsigResolveAsync(newRequest, key); newResponse.SetIdentifier(request.Identifier); return newResponse; } default: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName) { AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((zoneInfo is null) || !zoneInfo.ApexZone.IsActive) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: break; default: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } async Task IsZoneNameServerAllowedAsync(ApexZone apexZone) { IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await apexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } async Task IsZoneTransferAllowed(ApexZone apexZone) { switch (apexZone.ZoneTransfer) { case AuthZoneTransfer.Allow: return true; case AuthZoneTransfer.AllowOnlyZoneNameServers: return await IsZoneNameServerAllowedAsync(apexZone); case AuthZoneTransfer.UseSpecifiedNetworkACL: return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL); case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL) || await IsZoneNameServerAllowedAsync(apexZone); case AuthZoneTransfer.Deny: default: return false; } } bool IsTsigAuthenticated(ApexZone apexZone) { if ((apexZone.ZoneTransferTsigKeyNames is null) || (apexZone.ZoneTransferTsigKeyNames.Count < 1)) return true; //no auth needed if ((tsigAuthenticatedKeyName is not null) && apexZone.ZoneTransferTsigKeyNames.ContainsKey(tsigAuthenticatedKeyName.ToLowerInvariant())) return true; //key matches return false; } bool isInZoneTransferAllowedList = false; if (_zoneTransferAllowedNetworks is not null) { IPAddress remoteAddress = remoteEP.Address; foreach (NetworkAddress networkAddress in _zoneTransferAllowedNetworks) { if (networkAddress.Contains(remoteAddress)) { isInZoneTransferAllowedList = true; break; } } } if (!isInZoneTransferAllowedList) { ApexZone apexZone = zoneInfo.ApexZone; CatalogZone catalogZone = apexZone.CatalogZone; if (catalogZone is not null) { if (!apexZone.OverrideCatalogZoneTransfer) apexZone = catalogZone; //use catalog zone transfer options } else { SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone; if (secondaryCatalogZone is not null) { if (!apexZone.OverrideCatalogZoneTransfer) apexZone = secondaryCatalogZone; //use secondary zone transfer options } } if (!await IsZoneTransferAllowed(apexZone)) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + zoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } if (!IsTsigAuthenticated(apexZone)) { _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + zoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } _log?.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + zoneInfo.DisplayName); IReadOnlyList xfrRecords; if (request.Question[0].Type == DnsResourceRecordType.IXFR) { if ((request.Authority.Count == 1) && (request.Authority[0].Type == DnsResourceRecordType.SOA)) xfrRecords = _authZoneManager.QueryIncrementalZoneTransferRecords(request.Question[0].Name, request.Authority[0]); else return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else { xfrRecords = _authZoneManager.QueryZoneTransferRecords(request.Question[0].Name); } DnsDatagram xfrResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, xfrRecords) { Tag = DnsServerResponseType.Authoritative }; xfrResponse = xfrResponse.Split(); //update notify failed list NameServerAddress allowedZoneNameServer = null; switch (zoneInfo.Notify) { case AuthZoneNotify.ZoneNameServers: case AuthZoneNotify.BothZoneAndSpecifiedNameServers: IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) { allowedZoneNameServer = secondaryNameServer; break; } } break; } zoneInfo.ApexZone.RemoveFromNotifyFailedList(allowedZoneNameServer, remoteEP.Address); return xfrResponse; } private async Task ProcessAuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { DnsDatagram response = await AuthoritativeQueryAsync(request, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); if (response is null) return null; bool reprocessResponse; //to allow resolving CNAME/ANAME in response do { reprocessResponse = false; if (response.RCODE == DnsResponseCode.NoError) { if (response.Answer.Count > 0) { DnsResourceRecordType questionType = request.Question[0].Type; DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type != questionType) && (questionType != DnsResourceRecordType.ANY)) { switch (lastRR.Type) { case DnsResourceRecordType.CNAME: return await ProcessCNAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, false, skipDnsAppAuthoritativeRequestHandlers); case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: return await ProcessANAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); } } } else if (response.Authority.Count > 0) { DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: if (request.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution (with blocking support) using empty conditional forwarders; name servers will be provided via ResolverDnsCache return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, [], _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } break; case DnsResourceRecordType.FWD: //do conditional forwarding (with blocking support) return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); case DnsResourceRecordType.APP: response = await ProcessAPPAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); reprocessResponse = true; break; } } } } while (reprocessResponse); return response; } private async Task AuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { DnsDatagram response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return _authZoneManager.QueryAsync(request, remoteEP.Address, isRecursionAllowed, cancellationToken1); }, _clientTimeout); if (response is not null) { response.Tag = DnsServerResponseType.Authoritative; return response; } if (!skipDnsAppAuthoritativeRequestHandlers) { foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers) { try { DnsDatagram appResponse = await requestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed); if (appResponse is not null) { if (appResponse.Tag is null) appResponse.Tag = DnsServerResponseType.Authoritative; return appResponse; } } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } } return null; } private async Task ProcessAPPAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { DnsResourceRecord appResourceRecord = response.Authority[0]; DnsApplicationRecordData appRecord = appResourceRecord.RDATA as DnsApplicationRecordData; if (_dnsApplicationManager.Applications.TryGetValue(appRecord.AppName, out DnsApplication application)) { if (application.DnsAppRecordRequestHandlers.TryGetValue(appRecord.ClassPath, out IDnsAppRecordRequestHandler appRecordRequestHandler)) { AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(appResourceRecord.Name); DnsDatagram appResponse = await appRecordRequestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed, zoneInfo.Name, appResourceRecord.Name, appResourceRecord.TTL, appRecord.Data); if (appResponse is null) { DnsResponseCode rcode; IReadOnlyList authority = null; if (zoneInfo.Type == AuthZoneType.Forwarder) { //process FWD record if exists if (!zoneInfo.Name.Equals(appResourceRecord.Name, StringComparison.OrdinalIgnoreCase)) { AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.Name, appResourceRecord.Name); if (authZone is not null) authority = authZone.QueryRecords(DnsResourceRecordType.FWD, false); } if ((authority is null) || (authority.Count == 0)) authority = zoneInfo.ApexZone.QueryRecords(DnsResourceRecordType.FWD, false); if (authority.Count > 0) return await RecursiveResolveAsync(request, remoteEP, authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); rcode = DnsResponseCode.NoError; } else { //return NODATA/NXDOMAIN response if ((request.Question[0].Name.Length == appResourceRecord.Name.Length) || appResourceRecord.Name.StartsWith('*')) rcode = DnsResponseCode.NoError; else rcode = DnsResponseCode.NxDomain; authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); } return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; } else { if (appResponse.AuthoritativeAnswer) appResponse.Tag = DnsServerResponseType.Authoritative; return appResponse; //return app response } } else { _log?.Write(remoteEP, protocol, "DNS request handler '" + appRecord.ClassPath + "' was not found in the application '" + appRecord.AppName + "': " + appResourceRecord.Name); } } else { _log?.Write(remoteEP, protocol, "DNS application '" + appRecord.AppName + "' was not found: " + appResourceRecord.Name); } //return server failure response with SOA { AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name); IReadOnlyList authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessCNAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { List newAnswer = new List(response.Answer.Count + 4); newAnswer.AddRange(response.Answer); //copying NSEC/NSEC3 for for wildcard answers List newAuthority = new List(2); foreach (DnsResourceRecord record in response.Authority) { switch (record.Type) { case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: newAuthority.Add(record); break; case DnsResourceRecordType.RRSIG: switch ((record.RDATA as DnsRRSIGRecordData).TypeCovered) { case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: newAuthority.Add(record); break; } break; } } DnsDatagram lastResponse = response; bool isAuthoritativeAnswer = response.AuthoritativeAnswer; DnsResourceRecord lastRR = response.GetLastAnswerRecord(); EDnsOption[] eDnsClientSubnetOption = null; DnsDatagram newResponse = null; if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) eDnsClientSubnetOption = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS) }; } int queryCount = 0; do { string cnameDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain; if (lastRR.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase)) break; //loop detected DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(cnameDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption); //query authoritative zone first newResponse = await AuthoritativeQueryAsync(newRequest, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); if (newResponse is null) { //not found in auth zone if (newRequest.RecursionDesired && isRecursionAllowed) { //do recursion newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } else { //break since no recursion allowed/desired break; } } else if ((newResponse.Answer.Count > 0) && (newResponse.GetLastAnswerRecord() is DnsResourceRecord lastAnswer) && ((lastAnswer.Type == DnsResourceRecordType.ANAME) || (lastAnswer.Type == DnsResourceRecordType.ALIAS))) { newResponse = await ProcessANAMEAsync(request, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { //found delegated/forwarded zone DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: if (newRequest.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolveDnsCache newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } break; case DnsResourceRecordType.FWD: //do conditional forwarding newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; break; case DnsResourceRecordType.APP: newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); break; } } //check last response if (newResponse.Answer.Count == 0) break; //cannot proceed to resolve further lastRR = newResponse.GetLastAnswerRecord(); if (lastRR.Type != DnsResourceRecordType.CNAME) { newAnswer.AddRange(newResponse.Answer); break; //cname was resolved } bool foundRepeat = false; foreach (DnsResourceRecord newResponseAnswerRecord in newResponse.Answer) { if ((newResponseAnswerRecord.Type == DnsResourceRecordType.CNAME) || (newResponseAnswerRecord.Type == DnsResourceRecordType.DNAME)) { foreach (DnsResourceRecord answerRecord in newAnswer) { if (newResponseAnswerRecord.Equals(answerRecord)) { foundRepeat = true; break; } } if (foundRepeat) break; } newAnswer.Add(newResponseAnswerRecord); } if (foundRepeat) break; //loop detected lastResponse = newResponse; } while (++queryCount < MAX_CNAME_HOPS); DnsResponseCode rcode; IReadOnlyList authority; IReadOnlyList additional; if (newResponse is null) { //no recursion available rcode = DnsResponseCode.NoError; if (newAuthority.Count == 0) { authority = lastResponse.Authority; } else { newAuthority.AddRange(lastResponse.Authority); authority = newAuthority; } additional = lastResponse.Additional; } else { rcode = newResponse.RCODE; if (newAuthority.Count == 0) { authority = newResponse.Authority; } else { newAuthority.AddRange(newResponse.Authority); authority = newAuthority; } additional = newResponse.Additional; } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, newAnswer, authority, additional) { Tag = response.Tag }; } private async Task ProcessANAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { EDnsOption[] eDnsClientSubnetOption = null; if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) eDnsClientSubnetOption = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS) }; } Queue>> resolveQueue = new Queue>>(); async Task> ResolveANAMEAsync(DnsResourceRecord anameRR, int queryCount = 0) { string lastDomain = (anameRR.RDATA as DnsANAMERecordData).Domain; if (anameRR.Name.Equals(lastDomain, StringComparison.OrdinalIgnoreCase)) return null; //loop detected do { DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(lastDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption); //query authoritative zone first DnsDatagram newResponse = await AuthoritativeQueryAsync(newRequest, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); if (newResponse is null) { //not found in auth zone; do recursion newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { //found delegated/forwarded zone DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); break; case DnsResourceRecordType.FWD: //do conditional forwarding newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); break; case DnsResourceRecordType.APP: newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); break; } } //check new response if (newResponse.RCODE != DnsResponseCode.NoError) return null; //cannot proceed to resolve further if (newResponse.Answer.Count == 0) return Array.Empty(); //NO DATA DnsResourceRecordType questionType = request.Question[0].Type; DnsResourceRecord lastRR = newResponse.GetLastAnswerRecord(); if (lastRR.Type == questionType) { //found final answer List answers = new List(); foreach (DnsResourceRecord answer in newResponse.Answer) { if (answer.Type != questionType) continue; if (anameRR.TTL < answer.TTL) answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TTL, answer.RDATA)); else answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TTL, answer.RDATA)); } return answers; } switch (lastRR.Type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: if (newResponse.Answer.Count == 1) { lastDomain = (lastRR.RDATA as DnsANAMERecordData).Domain; } else { //resolve multiple ANAME records async queryCount++; //increment since one query was done already foreach (DnsResourceRecord newAnswer in newResponse.Answer) resolveQueue.Enqueue(ResolveANAMEAsync(newAnswer, queryCount)); return Array.Empty(); } break; case DnsResourceRecordType.CNAME: lastDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain; break; default: //aname/cname was resolved, but no answer found return Array.Empty(); } } while (++queryCount < MAX_CNAME_HOPS); //max hops limit crossed return null; } List responseAnswer = new List(); foreach (DnsResourceRecord answer in response.Answer) { switch (answer.Type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: resolveQueue.Enqueue(ResolveANAMEAsync(answer)); break; default: if (resolveQueue.Count == 0) responseAnswer.Add(answer); break; } } bool foundErrors = false; while (resolveQueue.Count > 0) { IReadOnlyList records = await resolveQueue.Dequeue(); if (records is null) foundErrors = true; else if (records.Count > 0) responseAnswer.AddRange(records); } DnsResponseCode rcode = DnsResponseCode.NoError; IReadOnlyList authority = null; if (responseAnswer.Count == 0) { if (foundErrors) { rcode = DnsResponseCode.ServerFailure; } else { authority = response.Authority; //update last used on DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in authority) record.GetAuthGenericRecordInfo().LastUsedOn = utcNow; } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, responseAnswer, authority, null) { Tag = response.Tag }; } private DnsDatagram ProcessBlocked(DnsDatagram request) { DnsDatagram response = _blockedZoneManager.Query(request); if (response is null) { //domain not blocked in blocked zone response = _blockListZoneManager.Query(request); //check in block list zone if (response is null) return null; //domain not blocked in block list zone //domain is blocked in block list zone response.Tag = DnsServerResponseType.Blocked; return response; } else { //domain is blocked in blocked zone DnsQuestionRecord question = request.Question[0]; string GetBlockedDomain() { DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA)) return firstAuthority.Name; else return question.Name; } if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) { //return meta data string blockedDomain = GetBlockedDomain(); IReadOnlyList answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _blockingAnswerTtl, new DnsTXTRecordData("source=blocked-zone; domain=" + blockedDomain))]; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }; } else { string blockedDomain = null; EDnsOption[] options = null; if (_allowTxtBlockingReport && (request.EDNS is not null)) { blockedDomain = GetBlockedDomain(); options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=blocked-zone; domain=" + blockedDomain))]; } IReadOnlyCollection aRecords; IReadOnlyCollection aaaaRecords; switch (_blockingType) { case DnsServerBlockingType.AnyAddress: aRecords = _aRecords; aaaaRecords = _aaaaRecords; break; case DnsServerBlockingType.CustomAddress: aRecords = _customBlockingARecords; aaaaRecords = _customBlockingAAAARecords; break; case DnsServerBlockingType.NxDomain: if (blockedDomain is null) blockedDomain = GetBlockedDomain(); string parentDomain = AuthZoneManager.GetParentZone(blockedDomain); if (parentDomain is null) parentDomain = string.Empty; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _blockedZoneManager.DnsSOARecord)], null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; default: throw new InvalidOperationException(); } IReadOnlyList answer; IReadOnlyList authority = null; switch (question.Type) { case DnsResourceRecordType.A: { if (aRecords.Count > 0) { DnsResourceRecord[] rrList = new DnsResourceRecord[aRecords.Count]; int i = 0; foreach (DnsARecordData record in aRecords) rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _blockingAnswerTtl, record); answer = rrList; } else { answer = null; authority = response.Authority; } } break; case DnsResourceRecordType.AAAA: { if (aaaaRecords.Count > 0) { DnsResourceRecord[] rrList = new DnsResourceRecord[aaaaRecords.Count]; int i = 0; foreach (DnsAAAARecordData record in aaaaRecords) rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _blockingAnswerTtl, record); answer = rrList; } else { answer = null; authority = response.Authority; } } break; default: answer = response.Answer; authority = response.Authority; break; } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; } } } private async Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { if (request.Question.Count > 0) { DnsQuestionRecord question = request.Question[0]; if (question.Type == DnsResourceRecordType.DS) { //DS is at parent zone which causes IsAllowed() to return null; change QTYPE to A to fix this issue that causes allowed domains to fail DNSSEC validation at downstream DnsQuestionRecord newQuestion = new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN); request = new DnsDatagram(request.Identifier, request.IsResponse, request.OPCODE, request.AuthoritativeAnswer, request.Truncation, request.RecursionDesired, request.RecursionAvailable, request.AuthenticData, request.CheckingDisabled, request.RCODE, [newQuestion], request.Answer, request.Authority, request.Additional); } } if (_enableBlocking) { if (_blockingBypassList is not null) { IPAddress remoteIP = remoteEP.Address; foreach (NetworkAddress network in _blockingBypassList) { if (network.Contains(remoteIP)) return true; } } if (_allowedZoneManager.IsAllowed(request) || _blockListZoneManager.IsAllowed(request)) return true; } foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers) { try { if (await blockingHandler.IsAllowedAsync(request, remoteEP)) return true; } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } return false; } private async Task ProcessBlockedQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { if (_enableBlocking) { DnsDatagram blockedResponse = ProcessBlocked(request); if (blockedResponse is not null) return blockedResponse; } foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers) { try { DnsDatagram appBlockedResponse = await blockingHandler.ProcessRequestAsync(request, remoteEP); if (appBlockedResponse is not null) { if (appBlockedResponse.Tag is null) appBlockedResponse.Tag = DnsServerResponseType.Blocked; return appBlockedResponse; } } catch (Exception ex) { _log?.Write(remoteEP, protocol, ex); } } return null; } private async Task ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { bool isAllowed; if (cacheRefreshOperation) { //cache refresh operation should be able to refresh all the records in cache //this is since a blocked CNAME record could still be used by an allowed domain name and so must resolve isAllowed = true; } else { isAllowed = await IsAllowedAsync(request, remoteEP, protocol); if (!isAllowed) { DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(request, remoteEP, protocol); if (blockedResponse is not null) return blockedResponse; } } DnsDatagram response = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); if (response.Answer.Count > 0) { DnsResourceRecordType questionType = request.Question[0].Type; DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY)) response = await ProcessCNAMEAsync(request, response, remoteEP, protocol, true, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); if (!isAllowed) { //check for CNAME cloaking for (int i = 0; i < response.Answer.Count; i++) { DnsResourceRecord record = response.Answer[i]; if (record.Type != DnsResourceRecordType.CNAME) break; //no further CNAME records exists DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord((record.RDATA as DnsCNAMERecordData).Domain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize); if (request.Metadata is not null) newRequest.SetMetadata(request.Metadata.NameServer); //check allowed zone isAllowed = await IsAllowedAsync(newRequest, remoteEP, protocol); if (isAllowed) break; //CNAME is in allowed zone //check blocked zone and block list zone DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(newRequest, remoteEP, protocol); if (blockedResponse is not null) { //found cname cloaking List answer = new List(); //copy current and previous CNAME records for (int j = 0; j <= i; j++) answer.Add(response.Answer[j]); //copy last response answers answer.AddRange(blockedResponse.Answer); //include blocked response additional section to pass on Extended DNS Errors return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, blockedResponse.RCODE, request.Question, answer, blockedResponse.Authority, blockedResponse.Additional) { Tag = blockedResponse.Tag }; } } } } if (response.Tag is null) { if (response.IsBlockedResponse()) response.Tag = DnsServerResponseType.UpstreamBlocked; } else if ((DnsServerResponseType)response.Tag == DnsServerResponseType.Cached) { if (response.IsBlockedResponse()) response.Tag = DnsServerResponseType.CacheBlocked; } return response; } private async Task RecursiveResolveAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { DnsQuestionRecord question = request.Question[0]; NetworkAddress eDnsClientSubnet = null; bool advancedForwardingClientSubnet = false; //this feature is used by Advanced Forwarding app to cache response per network group if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is null) { if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork)) { //set ipv4 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv4Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6)) { //set ipv6 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv6Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else if (!NetUtilities.IsPrivateIP(remoteEP.Address)) { //set shadow ECS option switch (remoteEP.AddressFamily) { case AddressFamily.InterNetwork: eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv4PrefixLength); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; case AddressFamily.InterNetworkV6: eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv6PrefixLength); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; default: request.ShadowHideEDnsClientSubnetOption(); break; } } } else if ((requestECS.Family != EDnsClientSubnetAddressFamily.IPv4) && (requestECS.Family != EDnsClientSubnetAddressFamily.IPv6)) { return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else if (requestECS.AdvancedForwardingClientSubnet) { //request from Advanced Forwarding app advancedForwardingClientSubnet = true; eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); } else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.Address)) { //disable ECS option request.ShadowHideEDnsClientSubnetOption(); } else if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork)) { //set ipv4 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv4Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6)) { //set ipv6 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv6Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else { //use ECS from client request switch (requestECS.Family) { case EDnsClientSubnetAddressFamily.IPv4: eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv4PrefixLength)); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; case EDnsClientSubnetAddressFamily.IPv6: eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv6PrefixLength)); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; } } } else { //ECS feature disabled EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet; if (advancedForwardingClientSubnet) eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); //request from Advanced Forwarding app else request.ShadowHideEDnsClientSubnetOption(); //hide ECS option } } if (!cachePrefetchOperation && !cacheRefreshOperation) { //query cache zone to see if answer available DnsDatagram cacheResponse = QueryCache(request, false, false); if (cacheResponse is not null) { if (_cachePrefetchTrigger > 0) { //inspect response TTL values to decide if prefetch trigger is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= _cachePrefetchTrigger)) { //trigger prefetch async _ = PrefetchCacheAsync(request, remoteEP, conditionalForwarders); break; } } } return cacheResponse; } } //recursion with locking TaskCompletionSource resolverTaskCompletionSource = new TaskCompletionSource(); Task resolverTask = _resolverTasks.GetOrAdd(GetResolverQueryKey(question, eDnsClientSubnet), resolverTaskCompletionSource.Task); if (resolverTask.Equals(resolverTaskCompletionSource.Task)) { //got new resolver task added so question is not being resolved; do recursive resolution in another task on resolver thread pool _ = Task.Factory.StartNew(delegate () { return RecursiveResolverBackgroundTaskAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _resolverTaskScheduler); } //request is being recursively resolved by another thread if (cachePrefetchOperation) return null; //return null as prefetch worker thread does not need valid response and thus does not need to wait if (_serveStale) { int waitTimeout = Math.Min(_serveStaleMaxWaitTime, _clientTimeout - SERVE_STALE_TIME_DIFFERENCE); //200ms before client timeout or max 1800ms [RFC 8767] //wait till short timeout for response if ((waitTimeout > 0) && (await Task.WhenAny(resolverTask, Task.Delay(waitTimeout)) == resolverTask)) { //resolver signaled RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception } else { //wait timed out //query cache zone to return stale answer (if available) as per RFC 8767 DnsDatagram staleResponse = QueryCache(request, true, false); if (staleResponse is not null) return staleResponse; //no stale record was found //wait till full timeout before responding as ServerFailure int timeout = _clientTimeout - waitTimeout; if (await Task.WhenAny(resolverTask, Task.Delay(timeout)) == resolverTask) { //resolver signaled RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception } } } else { //wait till full client timeout for response if (await Task.WhenAny(resolverTask, Task.Delay(_clientTimeout)) == resolverTask) { //resolver signaled RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception } } //no response available; respond with ServerFailure EDnsOption[] options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Waiting for resolver. Please try again."))]; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); } private async Task RecursiveResolverBackgroundTaskAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource taskCompletionSource) { try { //recursive resolve and update cache IDnsCache dnsCache; if (cachePrefetchOperation || cacheRefreshOperation) dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); else if (skipDnsAppAuthoritativeRequestHandlers || advancedForwardingClientSubnet) dnsCache = _dnsCacheSkipDnsApps; //to prevent request reaching apps again else dnsCache = _dnsCache; DnsDatagram response; if (conditionalForwarders is not null) { if (conditionalForwarders.Count > 0) { //do priority based conditional forwarding response = await PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders); } else { //do force recursive resolution response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, _nsRevalidation, true, cancellationToken: cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT); } } else { //do default recursive resolution response = await DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, dnssecValidation, skipDnsAppAuthoritativeRequestHandlers); } switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: taskCompletionSource.SetResult(new RecursiveResolveResponse(response, response)); break; default: throw new DnsServerException("DNS Server received a response for '" + question.ToString() + "' with RCODE=" + response.RCODE.ToString() + " from: " + (response.Metadata is null ? "unknown" : response.Metadata.NameServer)); } } catch (Exception ex) { if (_resolverLog is not null) { string strForwarders = null; if ((conditionalForwarders is not null) && (conditionalForwarders.Count > 0)) { foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { NameServerAddress nameServer = (conditionalForwarder.RDATA as DnsForwarderRecordData).NameServer; if (strForwarders is null) strForwarders = nameServer.ToString(); else strForwarders += ", " + nameServer.ToString(); } } else if ((_forwarders is not null) && (_forwarders.Count > 0)) { foreach (NameServerAddress nameServer in _forwarders) { if (strForwarders is null) strForwarders = nameServer.ToString(); else strForwarders += ", " + nameServer.ToString(); } } _resolverLog.Write("DNS Server failed to resolve the request '" + question.ToString() + "'" + (strForwarders is null ? "" : " using forwarders: " + strForwarders) + ".\r\n" + ex.ToString()); } if (_serveStale) { //fetch and reset stale records DnsDatagram cacheRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, dnssecValidation, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(eDnsClientSubnet)); DnsDatagram staleResponse = QueryCache(cacheRequest, true, true); if (staleResponse is not null) { //signal stale response if (!dnssecValidation || staleResponse.AuthenticData) { taskCompletionSource.SetResult(new RecursiveResolveResponse(staleResponse, staleResponse)); } else { List options; if ((staleResponse.EDNS is not null) && (staleResponse.EDNS.Options.Count > 0)) { options = new List(staleResponse.EDNS.Options.Count); foreach (EDnsOption option in staleResponse.EDNS.Options) { if (option.Code == EDnsOptionCode.EXTENDED_DNS_ERROR) options.Add(option); } } else { options = null; } DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, staleResponse)); } return; } } //signal failure response to release waiting tasks if (ex is DnsClientResponseDnssecValidationException ex2) { List options; if (ex2.Response.DnsClientExtendedErrors.Count > 0) { options = new List(ex2.Response.DnsClientExtendedErrors.Count); foreach (EDnsExtendedDnsErrorOptionData dnsError in ex2.Response.DnsClientExtendedErrors) options.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, dnsError)); } else { options = null; } DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, options); if ((ex2.Response.Question.Count > 0) && ex2.Response.Question[0].Equals(question)) taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, ex2.Response)); else taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse)); } else if (ex is DnsClientNoResponseException ex3) { IReadOnlyList options; if (ex3.InnerException is SocketException ex3a) { if (ex3a.SocketErrorCode == SocketError.TimedOut) options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))]; else options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex3a.SocketErrorCode.ToString()))]; } else { options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "No response from name servers for " + question.ToString()))]; } DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse)); } else if (ex is SocketException ex4) { IReadOnlyList options; if (ex4.SocketErrorCode == SocketError.TimedOut) options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))]; else options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex4.SocketErrorCode.ToString()))]; DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse)); } else if (ex is IOException ex5) { IReadOnlyList options; if (ex5.InnerException is SocketException ex5a) { if (ex5a.SocketErrorCode == SocketError.TimedOut) options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))]; else options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex5a.SocketErrorCode.ToString()))]; } else { options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "IO error: " + ex5.Message))]; } DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse)); } else { IReadOnlyList options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Resolver exception"))]; DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, new DnsQuestionRecord[] { question }, null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse)); } } finally { _resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _); } } private async Task DefaultRecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IDnsCache dnsCache, bool dnssecValidation, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { if ((_forwarders is not null) && (_forwarders.Count > 0)) { //use forwarders if (_concurrentForwarding) { if (_proxy is null) { //recursive resolve forwarders only when proxy is null else let proxy resolve it to allow using .onion or private domains foreach (NameServerAddress forwarder in _forwarders) { if (forwarder.IsIPEndPointStale) { //refresh forwarder IPEndPoint if stale await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken); } } } //query forwarders and update cache DnsClient dnsClient = new DnsClient(_forwarders); dnsClient.Cache = dnsCache; dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; dnsClient.RandomizeName = _randomizeName; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = _forwarderConcurrency; dnsClient.UdpPayloadSize = _udpPayloadSize; dnsClient.DnssecValidation = dnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them return await dnsClient.ResolveAsync(question, cancellationToken); } else { //do sequentially ordered forwarding Exception lastException = null; foreach (NameServerAddress forwarder in _forwarders) { if (_proxy is null) { //recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains if (forwarder.IsIPEndPointStale) { //refresh forwarder IPEndPoint if stale await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken); } } //query forwarder and update cache DnsClient dnsClient = new DnsClient(forwarder); dnsClient.Cache = dnsCache; dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; dnsClient.RandomizeName = _randomizeName; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = _forwarderConcurrency; dnsClient.UdpPayloadSize = _udpPayloadSize; dnsClient.DnssecValidation = dnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them try { return await dnsClient.ResolveAsync(question, cancellationToken); } catch (Exception ex) { lastException = ex; } if (dnsCache is not ResolverPrefetchDnsCache) dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache } ExceptionDispatchInfo.Capture(lastException).Throw(); throw lastException; } } else { //do recursive resolution return await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, _nsRevalidation, true, null, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken); } } private async Task PriorityConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, bool skipDnsAppAuthoritativeRequestHandlers, IReadOnlyList conditionalForwarders) { if (conditionalForwarders.Count == 1) { DnsResourceRecord conditionalForwarder = conditionalForwarders[0]; return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers); } //group by priority and check for forwarder name server resolution Dictionary> conditionalForwarderGroups = new Dictionary>(conditionalForwarders.Count); { List resolveTasks = new List(); foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; if (conditionalForwarderGroups.TryGetValue(forwarder.Priority, out List conditionalForwardersEntry)) { conditionalForwardersEntry.Add(conditionalForwarder); } else { conditionalForwardersEntry = new List(2) { conditionalForwarder }; conditionalForwarderGroups[forwarder.Priority] = conditionalForwardersEntry; } if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) continue; //skip resolving NetProxy proxy = forwarder.GetProxy(_proxy); if (proxy is null) { //recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains if (forwarder.NameServer.IsIPEndPointStale) { //refresh forwarder IPEndPoint if stale resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT)); } } } if (resolveTasks.Count > 0) await Task.WhenAll(resolveTasks); } if (conditionalForwarderGroups.Count == 1) { foreach (KeyValuePair> conditionalForwardersEntry in conditionalForwarderGroups) return await ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry.Value, skipDnsAppAuthoritativeRequestHandlers); } List priorities = new List(conditionalForwarderGroups.Keys); priorities.Sort(); using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { CancellationToken currentCancellationToken = cancellationTokenSource.Token; DnsDatagram lastResponse = null; Exception lastException = null; foreach (byte priority in priorities) { if (!conditionalForwarderGroups.TryGetValue(priority, out List conditionalForwardersEntry)) continue; Task priorityTask = ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken); try { DnsDatagram priorityTaskResponse = await priorityTask; //await to get response switch (priorityTaskResponse.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: cancellationTokenSource.Cancel(); //to stop other priority resolver tasks return priorityTaskResponse; default: //keep response lastResponse = priorityTaskResponse; break; } } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastException = ex; if (lastException is AggregateException) lastException = lastException.InnerException; } if (dnsCache is not ResolverPrefetchDnsCache) dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache } if (lastResponse is not null) return lastResponse; if (lastException is not null) ExceptionDispatchInfo.Capture(lastException).Throw(); throw new InvalidOperationException(); } } private async Task ConcurrentConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, IReadOnlyList conditionalForwarders, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { if (conditionalForwarders.Count == 1) { DnsResourceRecord conditionalForwarder = conditionalForwarders[0]; return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, cancellationToken); } using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { using CancellationTokenRegistration r = cancellationToken.Register(cancellationTokenSource.Cancel); CancellationToken currentCancellationToken = cancellationTokenSource.Token; List> tasks = new List>(conditionalForwarders.Count); //start worker tasks foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; tasks.Add(Task.Factory.StartNew(delegate () { return ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, forwarder, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current).Unwrap()); } //wait for first positive response, or for all tasks to fault DnsDatagram lastResponse = null; Exception lastException = null; while (tasks.Count > 0) { Task completedTask = await Task.WhenAny(tasks); try { DnsDatagram taskResponse = await completedTask; //await to get response switch (taskResponse.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: cancellationTokenSource.Cancel(); //to stop other resolver tasks return taskResponse; default: //keep response lastResponse = taskResponse; break; } } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastException = ex; if (lastException is AggregateException) lastException = lastException.InnerException; } tasks.Remove(completedTask); } if (lastResponse is not null) return lastResponse; if (lastException is not null) ExceptionDispatchInfo.Capture(lastException).Throw(); throw new InvalidOperationException(); } } private Task ConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, DnsForwarderRecordData forwarder, string conditionalForwardingZoneCut, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { //resolve via default recursive resolver with DNSSEC validation preference return DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, forwarder.DnssecValidation, skipDnsAppAuthoritativeRequestHandlers, cancellationToken); } else { //resolve via conditional forwarder DnsClient dnsClient = new DnsClient(forwarder.NameServer); dnsClient.Cache = dnsCache; dnsClient.Proxy = forwarder.GetProxy(_proxy); dnsClient.PreferIPv6 = _preferIPv6; dnsClient.RandomizeName = _randomizeName; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = _forwarderConcurrency; dnsClient.UdpPayloadSize = _udpPayloadSize; dnsClient.DnssecValidation = forwarder.DnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.AdvancedForwardingClientSubnet = advancedForwardingClientSubnet; dnsClient.ConditionalForwardingZoneCut = conditionalForwardingZoneCut; return dnsClient.ResolveAsync(question, cancellationToken); } } private DnsDatagram PrepareRecursiveResolveResponse(DnsDatagram request, RecursiveResolveResponse resolveResponse) { //get a tailored response for the request bool dnssecOk = request.DnssecOk; if (dnssecOk && request.CheckingDisabled) { bool authenticData = false; if (dnssecOk) { if (resolveResponse.CheckingDisabledResponse.Answer.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in resolveResponse.CheckingDisabledResponse.Answer) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } else if (resolveResponse.CheckingDisabledResponse.Authority.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in resolveResponse.CheckingDisabledResponse.Authority) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, true, resolveResponse.CheckingDisabledResponse.RCODE, request.Question, resolveResponse.CheckingDisabledResponse.Answer, resolveResponse.CheckingDisabledResponse.Authority, RemoveOPTFromAdditional(resolveResponse.CheckingDisabledResponse.Additional, true), _udpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, resolveResponse.CheckingDisabledResponse.EDNS?.Options); } DnsDatagram response = resolveResponse.Response; IReadOnlyList answer = response.Answer; IReadOnlyList authority = response.Authority; IReadOnlyList additional = response.Additional; //answer section checks if (!dnssecOk && (answer.Count > 0) && (response.Question[0].Type != DnsResourceRecordType.ANY)) { //remove RRSIGs from answer bool foundRRSIG = false; foreach (DnsResourceRecord record in answer) { if (record.Type == DnsResourceRecordType.RRSIG) { foundRRSIG = true; break; } } if (foundRRSIG) { List newAnswer = new List(answer.Count); foreach (DnsResourceRecord record in answer) { if (record.Type == DnsResourceRecordType.RRSIG) continue; newAnswer.Add(record); } answer = newAnswer; } } //authority section checks if (!dnssecOk && (authority.Count > 0)) { //remove DNSSEC records bool foundDnssecRecords = false; bool foundOther = false; foreach (DnsResourceRecord record in authority) { switch (record.Type) { case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: foundDnssecRecords = true; break; default: foundOther = true; break; } } if (foundDnssecRecords) { if (foundOther) { List newAuthority = new List(2); foreach (DnsResourceRecord record in authority) { switch (record.Type) { case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: break; default: newAuthority.Add(record); break; } } authority = newAuthority; } else { authority = Array.Empty(); } } } //additional section checks if (additional.Count > 0) { if ((response.RCODE != DnsResponseCode.NoError) && (request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0))) { //only responses with RCODE!=NoError gets cached as a special cache record to preserve EDNS options //copy options as new OPT and keep other records List newAdditional = new List(additional.Count); foreach (DnsResourceRecord record in additional) { switch (record.Type) { case DnsResourceRecordType.OPT: continue; case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.DNSKEY: if (dnssecOk) break; continue; } newAdditional.Add(record); } IReadOnlyList options; if (response.GetEDnsClientSubnetOption(true) is not null) { //response contains ECS if (request.GetEDnsClientSubnetOption(true) is not null) { //request has ECS and type is supported; keep ECS in response options = response.EDNS.Options; } else { //cache does not support the qtype so remove ECS from response if (response.EDNS.Options.Count == 1) { options = Array.Empty(); } else { List newOptions = new List(response.EDNS.Options.Count); foreach (EDnsOption option in response.EDNS.Options) { if (option.Code != EDnsOptionCode.EDNS_CLIENT_SUBNET) newOptions.Add(option); } options = newOptions; } } } else { options = response.EDNS.Options; } if (response.DnsClientExtendedErrors.Count > 0) { //add dns client extended errors List newOptions = new List(options.Count + response.DnsClientExtendedErrors.Count); newOptions.AddRange(options); foreach (EDnsExtendedDnsErrorOptionData ee in response.DnsClientExtendedErrors) newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, ee)); options = newOptions; } newAdditional.Add(DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options)); additional = newAdditional; } else if (response.EDNS is not null) { //remove OPT from additional additional = RemoveOPTFromAdditional(additional, dnssecOk); } } { bool authenticData = false; if (dnssecOk) { if (answer.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in answer) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } else if (authority.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in authority) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, request.CheckingDisabled, response.RCODE, request.Question, answer, authority, additional); } } private static IReadOnlyList RemoveOPTFromAdditional(IReadOnlyList additional, bool dnssecOk) { if (additional.Count == 0) return additional; if ((additional.Count == 1) && (additional[0].Type == DnsResourceRecordType.OPT)) return Array.Empty(); List newAdditional = new List(additional.Count - 1); foreach (DnsResourceRecord record in additional) { switch (record.Type) { case DnsResourceRecordType.OPT: continue; case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.DNSKEY: if (dnssecOk) break; continue; } newAdditional.Add(record); } return newAdditional; } private static string GetResolverQueryKey(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet) { if (eDnsClientSubnet is null) return question.ToString(); return question.ToString() + " " + eDnsClientSubnet.ToString(); } private DnsDatagram QueryCache(DnsDatagram request, bool serveStale, bool resetExpiry) { DnsDatagram cacheResponse = _cacheZoneManager.Query(request, serveStale, false, resetExpiry); if (cacheResponse is not null) { if ((cacheResponse.RCODE != DnsResponseCode.NoError) || (cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || cacheResponse.IsFirstAuthoritySOA()) { cacheResponse.Tag = DnsServerResponseType.Cached; return cacheResponse; } } return null; } private async Task PrefetchCacheAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders) { try { await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, _dnssecValidation, true, false, false); } catch (Exception ex) { _resolverLog?.Write(ex); } } private async Task RefreshCacheAsync(IList cacheRefreshSampleList, CacheRefreshSample sample, int sampleQuestionIndex) { try { //refresh cache DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { sample.SampleQuestion }); DnsDatagram response = await ProcessRecursiveQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Udp, sample.ConditionalForwarders, _dnssecValidation, true, false); bool addBackToSampleList = false; DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord answer in response.Answer) { if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (utcNow.AddSeconds(answer.TTL) < _cachePrefetchSamplingTimerTriggersOn)) { //answer expires before next sampling so add back to the list to allow refreshing it addBackToSampleList = true; break; } } if (addBackToSampleList) cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again } catch (Exception ex) { _resolverLog?.Write(ex); cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again } } private DnsQuestionRecord GetCacheRefreshNeededQuery(DnsQuestionRecord question, int trigger) { int queryCount = 0; while (true) { DnsDatagram cacheResponse = QueryCache(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), false, false); if (cacheResponse is null) return question; //cache expired so refresh question if (cacheResponse.Answer.Count == 0) return null; //dont refresh empty responses //inspect response TTL values to decide if refresh is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= trigger)) return question; //TTL eligible and less than trigger so refresh question } DnsResourceRecord lastRR = cacheResponse.GetLastAnswerRecord(); if (lastRR.Type == question.Type) return null; //answer was resolved if (lastRR.Type != DnsResourceRecordType.CNAME) return null; //invalid response so ignore question queryCount++; if (queryCount >= MAX_CNAME_HOPS) return null; //too many hops so ignore question //follow CNAME chain to inspect TTL further question = new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, question.Type, question.Class); } } private bool IsCacheRefreshNeeded(DnsQuestionRecord question, int trigger) { DnsDatagram cacheResponse = QueryCache(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), false, false); if (cacheResponse is null) return true; //cache expired so refresh needed if (cacheResponse.Answer.Count == 0) return false; //dont refresh empty responses //inspect response TTL values to decide if refresh is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= trigger)) return true; //TTL eligible less than trigger so refresh } return false; //no need to refresh for this query } private async void CachePrefetchSamplingTimerCallback(object state) { try { List> eligibleQueries = _stats.GetLastHourEligibleQueries(_cachePrefetchSampleEligibilityHitsPerHour); List cacheRefreshSampleList = new List(eligibleQueries.Count); int cacheRefreshTrigger = (_cachePrefetchSampleIntervalInMinutes + 1) * 60; foreach (KeyValuePair eligibleQuery in eligibleQueries) { DnsQuestionRecord eligibleQuerySample = eligibleQuery.Key; if (eligibleQuerySample.Type == DnsResourceRecordType.ANY) continue; //dont refresh type ANY queries DnsQuestionRecord refreshQuery = null; IReadOnlyList conditionalForwarders = null; //query auth zone for refresh query int queryCount = 0; bool reQueryAuthZone; do { reQueryAuthZone = false; DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { eligibleQuerySample }); DnsDatagram response = await AuthoritativeQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, false); if (response is null) { //zone not hosted; do refresh refreshQuery = GetCacheRefreshNeededQuery(eligibleQuerySample, cacheRefreshTrigger); } else { //zone is hosted; check further if (response.Answer.Count > 0) { DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type == DnsResourceRecordType.CNAME) && (eligibleQuerySample.Type != DnsResourceRecordType.CNAME)) { eligibleQuerySample = new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, eligibleQuerySample.Type, eligibleQuerySample.Class); reQueryAuthZone = true; } } else if (response.Authority.Count > 0) { DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: //zone is delegated refreshQuery = GetCacheRefreshNeededQuery(eligibleQuerySample, cacheRefreshTrigger); conditionalForwarders = Array.Empty(); //do forced recursive resolution using empty conditional forwarders break; case DnsResourceRecordType.FWD: //zone is conditional forwarder refreshQuery = GetCacheRefreshNeededQuery(eligibleQuerySample, cacheRefreshTrigger); conditionalForwarders = response.Authority; //do conditional forwarding break; } } } } while (reQueryAuthZone && (++queryCount < MAX_CNAME_HOPS)); if (refreshQuery is not null) cacheRefreshSampleList.Add(new CacheRefreshSample(refreshQuery, conditionalForwarders)); } _cacheRefreshSampleList = cacheRefreshSampleList; } catch (Exception ex) { _log?.Write(ex); } finally { lock (_cachePrefetchSamplingTimerLock) { if (_cachePrefetchSamplingTimer is not null) { _cachePrefetchSamplingTimer.Change(_cachePrefetchSampleIntervalInMinutes * 60 * 1000, Timeout.Infinite); _cachePrefetchSamplingTimerTriggersOn = DateTime.UtcNow.AddMinutes(_cachePrefetchSampleIntervalInMinutes); } } } } private void CachePrefetchRefreshTimerCallback(object state) { try { IList cacheRefreshSampleList = _cacheRefreshSampleList; if (cacheRefreshSampleList is not null) { for (int i = 0; i < cacheRefreshSampleList.Count; i++) { CacheRefreshSample sample = cacheRefreshSampleList[i]; if (sample is null) continue; if (!IsCacheRefreshNeeded(sample.SampleQuestion, _cachePrefetchTrigger + 1)) continue; cacheRefreshSampleList[i] = null; //remove from sample list to avoid concurrent refresh attempt int sampleQuestionIndex = i; _ = Task.Run(delegate () { return RefreshCacheAsync(cacheRefreshSampleList, sample, sampleQuestionIndex); }); //run task in threadpool since its long running } } } catch (Exception ex) { _log?.Write(ex); } finally { lock (_cachePrefetchRefreshTimerLock) { _cachePrefetchRefreshTimer?.Change((_cachePrefetchTrigger + 1) * 1000, Timeout.Infinite); } } } private void CacheMaintenanceTimerCallback(object state) { try { _cacheZoneManager.RemoveExpiredRecords(); //force GC collection to remove old cache data from memory quickly GC.Collect(); } catch (Exception ex) { _log?.Write(ex); } finally { lock (_cacheMaintenanceTimerLock) { _cacheMaintenanceTimer?.Change(CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } } } private void ResetPrefetchTimers() { if ((_cachePrefetchTrigger == 0) || (_recursion == DnsServerRecursion.Deny)) { lock (_cachePrefetchSamplingTimerLock) { _cachePrefetchSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite); } lock (_cachePrefetchRefreshTimerLock) { _cachePrefetchRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } else if (_state == ServiceState.Running) { lock (_cachePrefetchSamplingTimerLock) { if (_cachePrefetchSamplingTimer is not null) { _cachePrefetchSamplingTimer.Change(CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL, Timeout.Infinite); _cachePrefetchSamplingTimerTriggersOn = DateTime.UtcNow.AddMilliseconds(CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL); } } lock (_cachePrefetchRefreshTimerLock) { _cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL, Timeout.Infinite); } } } internal bool IsQpmLimitCrossed(IPAddress remoteIP) { if ((_qpmLimitRequests < 1) && (_qpmLimitErrors < 1)) return false; if (IPAddress.IsLoopback(remoteIP)) return false; if (_qpmLimitBypassList is not null) { foreach (NetworkAddress networkAddress in _qpmLimitBypassList) { if (networkAddress.Contains(remoteIP)) return false; } } IPAddress remoteSubnet; switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: remoteSubnet = remoteIP.GetNetworkAddress(_qpmLimitIPv4PrefixLength); break; case AddressFamily.InterNetworkV6: remoteSubnet = remoteIP.GetNetworkAddress(_qpmLimitIPv6PrefixLength); break; default: throw new NotSupportedException("AddressFamily not supported."); } if ((_qpmLimitErrors > 0) && (_qpmLimitErrorClientSubnetStats is not null) && _qpmLimitErrorClientSubnetStats.TryGetValue(remoteSubnet, out long errorCountPerSample)) { long averageErrorCountPerMinute = errorCountPerSample / _qpmLimitSampleMinutes; if (averageErrorCountPerMinute >= _qpmLimitErrors) return true; } if ((_qpmLimitRequests > 0) && (_qpmLimitClientSubnetStats is not null) && _qpmLimitClientSubnetStats.TryGetValue(remoteSubnet, out long countPerSample)) { long averageCountPerMinute = countPerSample / _qpmLimitSampleMinutes; if (averageCountPerMinute >= _qpmLimitRequests) return true; } return false; } private void QpmLimitSamplingTimerCallback(object state) { try { _stats.GetLatestClientSubnetStats(_qpmLimitSampleMinutes, _qpmLimitIPv4PrefixLength, _qpmLimitIPv6PrefixLength, out IReadOnlyDictionary qpmLimitClientSubnetStats, out IReadOnlyDictionary qpmLimitErrorClientSubnetStats); if (_qpmLimitErrors > 0) WriteClientSubnetRateLimitLog(_qpmLimitErrorClientSubnetStats, qpmLimitErrorClientSubnetStats, _qpmLimitErrors, "errors"); if (_qpmLimitRequests > 0) WriteClientSubnetRateLimitLog(_qpmLimitClientSubnetStats, qpmLimitClientSubnetStats, _qpmLimitRequests, "requests"); _qpmLimitClientSubnetStats = qpmLimitClientSubnetStats; _qpmLimitErrorClientSubnetStats = qpmLimitErrorClientSubnetStats; } catch (Exception ex) { _log?.Write(ex); } finally { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite); } } } private void WriteClientSubnetRateLimitLog(IReadOnlyDictionary oldQpmLimitClientSubnetStats, IReadOnlyDictionary newQpmLimitClientSubnetStats, long qpmLimit, string limitType) { if (oldQpmLimitClientSubnetStats is not null) { foreach (KeyValuePair sampleEntry in oldQpmLimitClientSubnetStats) { long oldAverageCountPerMinute = sampleEntry.Value / _qpmLimitSampleMinutes; if (oldAverageCountPerMinute >= qpmLimit) { //previously over limit long averageCountPerMinute = 0; if (newQpmLimitClientSubnetStats.TryGetValue(sampleEntry.Key, out long newCountPerSample)) averageCountPerMinute = newCountPerSample / _qpmLimitSampleMinutes; if (averageCountPerMinute < qpmLimit) //currently under limit _log?.Write("Client subnet '" + sampleEntry.Key + "/" + (sampleEntry.Key.AddressFamily == AddressFamily.InterNetwork ? _qpmLimitIPv4PrefixLength : _qpmLimitIPv6PrefixLength) + "' is no longer being rate limited (" + averageCountPerMinute + " qpm for " + limitType + ")."); } } } foreach (KeyValuePair sampleEntry in newQpmLimitClientSubnetStats) { long averageCountPerMinute = sampleEntry.Value / _qpmLimitSampleMinutes; if (averageCountPerMinute >= qpmLimit) { //currently over limit if ((oldQpmLimitClientSubnetStats is not null) && oldQpmLimitClientSubnetStats.TryGetValue(sampleEntry.Key, out long oldCountPerSample)) { long oldAverageCountPerMinute = oldCountPerSample / _qpmLimitSampleMinutes; if (oldAverageCountPerMinute >= qpmLimit) continue; //previously over limit too } _log?.Write("Client subnet '" + sampleEntry.Key + "/" + (sampleEntry.Key.AddressFamily == AddressFamily.InterNetwork ? _qpmLimitIPv4PrefixLength : _qpmLimitIPv6PrefixLength) + "' is being rate limited till the query rate limit (" + averageCountPerMinute + " qpm for " + limitType + ") falls below " + qpmLimit + " qpm."); } } } private void ResetQpsLimitTimer() { if ((_qpmLimitRequests < 1) && (_qpmLimitErrors < 1)) { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite); _qpmLimitClientSubnetStats = null; _qpmLimitErrorClientSubnetStats = null; } } else if (_state == ServiceState.Running) { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(0, Timeout.Infinite); } } } private void UpdateThisServer() { if ((_localEndPoints is null) || (_localEndPoints.Count == 0)) { _thisServer = new NameServerAddress(_serverDomain, IPAddress.Loopback); } else { foreach (IPEndPoint localEndPoint in _localEndPoints) { if (localEndPoint.Address.Equals(IPAddress.Any)) { _thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.Loopback, localEndPoint.Port)); return; } if (localEndPoint.Address.Equals(IPAddress.IPv6Any)) { _thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.IPv6Loopback, localEndPoint.Port)); return; } } _thisServer = new NameServerAddress(_serverDomain, _localEndPoints[0]); } } #endregion #region doh web service private async Task StartDoHAsync() { IReadOnlyList localAddresses = GetValidKestralLocalAddresses(_localEndPoints.Convert(delegate (IPEndPoint ep) { return ep.Address; })); try { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(Path.GetDirectoryName(_dohwwwFolder)) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_dohwwwFolder) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions) { //bind to http port if (_enableDnsOverHttp) { foreach (IPAddress localAddress in localAddresses) serverOptions.Listen(localAddress, _dnsOverHttpPort); } //bind to https port if (_enableDnsOverHttps && (_certificateCollection is not null)) { foreach (IPAddress localAddress in localAddresses) { serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions) { listenOptions.Protocols = _enableDnsOverHttp3 ? HttpProtocols.Http1AndHttp2AndHttp3 : HttpProtocols.Http1AndHttp2; listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { return ValueTask.FromResult(_sslServerAuthenticationOptions); }, null); }); } } serverOptions.AddServerHeader = false; serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout); serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout); serverOptions.Limits.MaxRequestHeadersTotalSize = 4096; serverOptions.Limits.MaxRequestLineSize = serverOptions.Limits.MaxRequestHeadersTotalSize; serverOptions.Limits.MaxRequestBufferSize = serverOptions.Limits.MaxRequestLineSize; serverOptions.Limits.MaxRequestBodySize = 64 * 1024; serverOptions.Limits.MaxResponseBufferSize = 4096; }); builder.Logging.ClearProviders(); _dohWebService = builder.Build(); _dohWebService.UseDefaultFiles(); _dohWebService.UseStaticFiles(new StaticFileOptions() { OnPrepareResponse = delegate (StaticFileResponseContext ctx) { ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex, nofollow"; ctx.Context.Response.Headers.CacheControl = "private, max-age=300"; }, ServeUnknownFileTypes = true }); _dohWebService.UseRouting(); _dohWebService.MapGet("/dns-query", ProcessDoHRequestAsync); _dohWebService.MapPost("/dns-query", ProcessDoHRequestAsync); await _dohWebService.StartAsync(); if (_log is not null) { foreach (IPAddress localAddress in localAddresses) { if (_enableDnsOverHttp) _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server was bound successfully."); if (_enableDnsOverHttps && (_certificateCollection is not null)) _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server was bound successfully."); } } } catch (Exception ex) { await StopDoHAsync(); if (_log is not null) { foreach (IPAddress localAddress in localAddresses) { if (_enableDnsOverHttp) _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server failed to bind."); if (_enableDnsOverHttps && (_certificateCollection is not null)) _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server failed to bind."); } _log?.Write(ex); } } } private async Task StopDoHAsync() { if (_dohWebService is not null) { try { await _dohWebService.DisposeAsync(); } catch (Exception ex) { _log?.Write(ex); } _dohWebService = null; } } internal static IReadOnlyList GetValidKestralLocalAddresses(IReadOnlyList localAddresses) { List supportedLocalAddresses = new List(localAddresses.Count); foreach (IPAddress localAddress in localAddresses) { switch (localAddress.AddressFamily) { case AddressFamily.InterNetwork: if (Socket.OSSupportsIPv4) { if (!supportedLocalAddresses.Contains(localAddress)) supportedLocalAddresses.Add(localAddress); } break; case AddressFamily.InterNetworkV6: if (Socket.OSSupportsIPv6) { if (!supportedLocalAddresses.Contains(localAddress)) supportedLocalAddresses.Add(localAddress); } break; } } bool containsUnicastAddress = false; foreach (IPAddress localAddress in supportedLocalAddresses) { if (!localAddress.Equals(IPAddress.Any) && !localAddress.Equals(IPAddress.IPv6Any)) { containsUnicastAddress = true; break; } } List newLocalAddresses = new List(supportedLocalAddresses.Count); if (containsUnicastAddress) { //replace any with loopback address foreach (IPAddress localAddress in supportedLocalAddresses) { if (localAddress.Equals(IPAddress.Any)) { if (!newLocalAddresses.Contains(IPAddress.Loopback)) newLocalAddresses.Add(IPAddress.Loopback); } else if (localAddress.Equals(IPAddress.IPv6Any)) { if (!newLocalAddresses.Contains(IPAddress.IPv6Loopback)) newLocalAddresses.Add(IPAddress.IPv6Loopback); } else { if (!newLocalAddresses.Contains(localAddress)) newLocalAddresses.Add(localAddress); } } } else { //remove "0.0.0.0" if [::] exists foreach (IPAddress localAddress in supportedLocalAddresses) { if (localAddress.Equals(IPAddress.Any)) { if (!supportedLocalAddresses.Contains(IPAddress.IPv6Any)) newLocalAddresses.Add(localAddress); } else { newLocalAddresses.Add(localAddress); } } } return newLocalAddresses; } #endregion #region public public async Task StartAsync() { if (_disposed) ObjectDisposedException.ThrowIf(_disposed, this); if (_state != ServiceState.Stopped) throw new InvalidOperationException("DNS Server is already running."); _state = ServiceState.Starting; //bind on all local end points foreach (IPEndPoint localEP in _localEndPoints) { Socket udpListener = null; try { udpListener = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp); #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom() if (Environment.OSVersion.Platform == PlatformID.Win32NT) { const uint IOC_IN = 0x80000000; const uint IOC_VENDOR = 0x18000000; const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); } #endregion if (Environment.OSVersion.Platform == PlatformID.Unix) udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses udpListener.ReceiveBufferSize = 512 * 1024; udpListener.SendBufferSize = 512 * 1024; try { udpListener.Bind(localEP); } catch (SocketException ex1) { switch (ex1.ErrorCode) { case 99: //SocketException (99): Cannot assign requested address await Task.Delay(5000); //wait for address to be available before retrying udpListener.Bind(localEP); break; default: throw; } } _udpListeners.Add(udpListener); _log?.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully."); } catch (Exception ex) { _log?.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString()); udpListener?.Dispose(); } if (_enableDnsOverUdpProxy) { IPEndPoint udpProxyEP = new IPEndPoint(localEP.Address, _dnsOverUdpProxyPort); Socket udpProxyListener = null; try { udpProxyListener = new Socket(udpProxyEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp); #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom() if (Environment.OSVersion.Platform == PlatformID.Win32NT) { const uint IOC_IN = 0x80000000; const uint IOC_VENDOR = 0x18000000; const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; udpProxyListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); } #endregion udpProxyListener.ReceiveBufferSize = 512 * 1024; udpProxyListener.SendBufferSize = 512 * 1024; udpProxyListener.Bind(udpProxyEP); _udpProxyListeners.Add(udpProxyListener); _log?.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, "DNS Server was bound successfully."); } catch (Exception ex) { _log?.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, "DNS Server failed to bind.\r\n" + ex.ToString()); udpProxyListener?.Dispose(); } } Socket tcpListener = null; try { tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); tcpListener.Bind(localEP); tcpListener.Listen(_listenBacklog); _tcpListeners.Add(tcpListener); _log?.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully."); } catch (Exception ex) { _log?.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString()); tcpListener?.Dispose(); } if (_enableDnsOverTcpProxy) { IPEndPoint tcpProxyEP = new IPEndPoint(localEP.Address, _dnsOverTcpProxyPort); Socket tcpProxyListner = null; try { tcpProxyListner = new Socket(tcpProxyEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); tcpProxyListner.Bind(tcpProxyEP); tcpProxyListner.Listen(_listenBacklog); _tcpProxyListeners.Add(tcpProxyListner); _log?.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, "DNS Server was bound successfully."); } catch (Exception ex) { _log?.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, "DNS Server failed to bind.\r\n" + ex.ToString()); tcpProxyListner?.Dispose(); } } if (_enableDnsOverTls && (_certificateCollection is not null)) { IPEndPoint tlsEP = new IPEndPoint(localEP.Address, _dnsOverTlsPort); Socket tlsListener = null; try { tlsListener = new Socket(tlsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); tlsListener.Bind(tlsEP); tlsListener.Listen(_listenBacklog); _tlsListeners.Add(tlsListener); _log?.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server was bound successfully."); } catch (Exception ex) { _log?.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server failed to bind.\r\n" + ex.ToString()); tlsListener?.Dispose(); } } if (_enableDnsOverQuic && (_certificateCollection is not null)) { IPEndPoint quicEP = new IPEndPoint(localEP.Address, _dnsOverQuicPort); QuicListener quicListener = null; try { QuicListenerOptions listenerOptions = new QuicListenerOptions() { ListenEndPoint = quicEP, ListenBacklog = _listenBacklog, ApplicationProtocols = quicApplicationProtocols, ConnectionOptionsCallback = delegate (QuicConnection quicConnection, SslClientHelloInfo sslClientHello, CancellationToken cancellationToken) { QuicServerConnectionOptions serverConnectionOptions = new QuicServerConnectionOptions() { DefaultCloseErrorCode = (long)DnsOverQuicErrorCodes.DOQ_NO_ERROR, DefaultStreamErrorCode = (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR, MaxInboundUnidirectionalStreams = 0, MaxInboundBidirectionalStreams = _quicMaxInboundStreams, IdleTimeout = TimeSpan.FromMilliseconds(_quicIdleTimeout), ServerAuthenticationOptions = _quicSslServerAuthenticationOptions }; return ValueTask.FromResult(serverConnectionOptions); } }; quicListener = await QuicListener.ListenAsync(listenerOptions); _quicListeners.Add(quicListener); _log?.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server was bound successfully."); } catch (Exception ex) { _log?.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server failed to bind.\r\n" + ex.ToString()); if (quicListener is not null) await quicListener.DisposeAsync(); } } } //start reading query packets int listenerTaskCount = Math.Max(1, Environment.ProcessorCount); foreach (Socket udpListener in _udpListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return ReadUdpRequestAsync(udpListener, DnsTransportProtocol.Udp); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket udpProxyListener in _udpProxyListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return ReadUdpRequestAsync(udpProxyListener, DnsTransportProtocol.UdpProxy); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket tcpListener in _tcpListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptConnectionAsync(tcpListener, DnsTransportProtocol.Tcp); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket tcpProxyListener in _tcpProxyListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptConnectionAsync(tcpProxyListener, DnsTransportProtocol.TcpProxy); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket tlsListener in _tlsListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptConnectionAsync(tlsListener, DnsTransportProtocol.Tls); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (QuicListener quicListener in _quicListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptQuicConnectionAsync(quicListener); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } if (_enableDnsOverHttp || (_enableDnsOverHttps && (_certificateCollection is not null))) await StartDoHAsync(); _cachePrefetchSamplingTimer = new Timer(CachePrefetchSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _cachePrefetchRefreshTimer = new Timer(CachePrefetchRefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _cacheMaintenanceTimer = new Timer(CacheMaintenanceTimerCallback, null, CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL, Timeout.Infinite); _qpmLimitSamplingTimer = new Timer(QpmLimitSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _state = ServiceState.Running; UpdateThisServer(); ResetPrefetchTimers(); ResetQpsLimitTimer(); if (Environment.OSVersion.Platform == PlatformID.Win32NT) { //init udp socket pool async for port randomization ThreadPool.QueueUserWorkItem(delegate (object state) { UdpClientConnection.CreateSocketPool(_preferIPv6); }); } } public async Task StopAsync() { if (_state != ServiceState.Running) return; _state = ServiceState.Stopping; lock (_cachePrefetchSamplingTimerLock) { if (_cachePrefetchSamplingTimer is not null) { _cachePrefetchSamplingTimer.Dispose(); _cachePrefetchSamplingTimer = null; } } lock (_cachePrefetchRefreshTimerLock) { if (_cachePrefetchRefreshTimer is not null) { _cachePrefetchRefreshTimer.Dispose(); _cachePrefetchRefreshTimer = null; } } lock (_cacheMaintenanceTimerLock) { if (_cacheMaintenanceTimer is not null) { _cacheMaintenanceTimer.Dispose(); _cacheMaintenanceTimer = null; } } lock (_qpmLimitSamplingTimerLock) { if (_qpmLimitSamplingTimer is not null) { _qpmLimitSamplingTimer.Dispose(); _qpmLimitSamplingTimer = null; } } foreach (Socket udpListener in _udpListeners) { try { udpListener.Dispose(); } catch (Exception ex) { _log?.Write(ex); } } foreach (Socket udpProxyListener in _udpProxyListeners) { try { udpProxyListener.Dispose(); } catch (Exception ex) { _log?.Write(ex); } } foreach (Socket tcpListener in _tcpListeners) { try { tcpListener.Dispose(); } catch (Exception ex) { _log?.Write(ex); } } foreach (Socket tcpProxyListener in _tcpProxyListeners) { try { tcpProxyListener.Dispose(); } catch (Exception ex) { _log?.Write(ex); } } foreach (Socket tlsListener in _tlsListeners) { try { tlsListener.Dispose(); } catch (Exception ex) { _log?.Write(ex); } } foreach (QuicListener quicListener in _quicListeners) { try { await quicListener.DisposeAsync(); } catch (Exception ex) { _log?.Write(ex); } } _udpListeners.Clear(); _udpProxyListeners.Clear(); _tcpListeners.Clear(); _tcpProxyListeners.Clear(); _tlsListeners.Clear(); _quicListeners.Clear(); await StopDoHAsync(); _state = ServiceState.Stopped; } public Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false) { return DirectQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), timeout, skipDnsAppAuthoritativeRequestHandlers); } public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false) { return TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, null); }, timeout); } Task IDnsClient.ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken) { return DirectQueryAsync(question); } #endregion #region properties public string ServerDomain { get { return _serverDomain; } set { if (!_serverDomain.Equals(value)) { if (DnsClient.IsDomainNameUnicode(value)) value = DnsClient.ConvertDomainNameToAscii(value); DnsClient.IsDomainNameValid(value, true); if (IPAddress.TryParse(value, out _)) throw new DnsServerException("Invalid domain name [" + value + "]: IP address cannot be used for DNS server domain name."); _serverDomain = value.ToLowerInvariant(); _defaultResponsiblePerson = new MailAddress("hostadmin@" + _serverDomain); _authZoneManager.UpdateServerDomain(); _allowedZoneManager.UpdateServerDomain(); _blockedZoneManager.UpdateServerDomain(); _blockListZoneManager.UpdateServerDomain(); UpdateThisServer(); } } } public string ConfigFolder { get { return _configFolder; } } public IReadOnlyList LocalEndPoints { get { return _localEndPoints; } set { _localEndPoints = value; } } public LogManager LogManager { get { return _log; } set { _log = value; } } internal MailAddress ResponsiblePersonInternal { get { return _responsiblePerson; } set { _responsiblePerson = value; } } public MailAddress ResponsiblePerson { get { if (_responsiblePerson is not null) return _responsiblePerson; if (_defaultResponsiblePerson is null) _defaultResponsiblePerson = new MailAddress("hostadmin@" + _serverDomain); return _defaultResponsiblePerson; } } public NameServerAddress ThisServer { get { return _thisServer; } } public AuthZoneManager AuthZoneManager { get { return _authZoneManager; } } public AllowedZoneManager AllowedZoneManager { get { return _allowedZoneManager; } } public BlockedZoneManager BlockedZoneManager { get { return _blockedZoneManager; } } public BlockListZoneManager BlockListZoneManager { get { return _blockListZoneManager; } } public CacheZoneManager CacheZoneManager { get { return _cacheZoneManager; } } public DnsApplicationManager DnsApplicationManager { get { return _dnsApplicationManager; } } public IDnsCache DnsCache { get { return _dnsCache; } } public StatsManager StatsManager { get { return _stats; } } public IReadOnlyCollection ZoneTransferAllowedNetworks { get { return _zoneTransferAllowedNetworks; } set { if ((value is null) || (value.Count == 0)) _zoneTransferAllowedNetworks = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ZoneTransferAllowedNetworks), "Networks cannot have more than 255 entries."); else _zoneTransferAllowedNetworks = value; } } public IReadOnlyCollection NotifyAllowedNetworks { get { return _notifyAllowedNetworks; } set { if ((value is null) || (value.Count == 0)) _notifyAllowedNetworks = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(NotifyAllowedNetworks), "Networks cannot have more than 255 entries."); else _notifyAllowedNetworks = value; } } public bool PreferIPv6 { get { return _preferIPv6; } set { if (_preferIPv6 != value) { _preferIPv6 = value; //init udp socket pool async for port randomization ThreadPool.QueueUserWorkItem(delegate (object state) { if (Environment.OSVersion.Platform == PlatformID.Win32NT) UdpClientConnection.CreateSocketPool(_preferIPv6); }); } } } public ushort UdpPayloadSize { get { return _udpPayloadSize; } set { if ((value < 512) || (value > 4096)) throw new ArgumentOutOfRangeException(nameof(UdpPayloadSize), "Invalid EDNS UDP payload size: valid range is 512-4096 bytes."); _udpPayloadSize = value; } } public bool DnssecValidation { get { return _dnssecValidation; } set { if (_dnssecValidation != value) { if (!_dnssecValidation) _cacheZoneManager.Flush(); //flush cache to remove non validated data _dnssecValidation = value; } } } public bool EDnsClientSubnet { get { return _eDnsClientSubnet; } set { if (_eDnsClientSubnet != value) { _eDnsClientSubnet = value; if (!_eDnsClientSubnet) { ThreadPool.QueueUserWorkItem(delegate (object state) { _cacheZoneManager.DeleteEDnsClientSubnetData(); }); } } } } public byte EDnsClientSubnetIPv4PrefixLength { get { return _eDnsClientSubnetIPv4PrefixLength; } set { if (value > 32) throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv4PrefixLength), "EDNS Client Subnet IPv4 prefix length cannot be greater than 32."); _eDnsClientSubnetIPv4PrefixLength = value; } } public byte EDnsClientSubnetIPv6PrefixLength { get { return _eDnsClientSubnetIPv6PrefixLength; } set { if (value > 64) throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv6PrefixLength), "EDNS Client Subnet IPv6 prefix length cannot be greater than 64."); _eDnsClientSubnetIPv6PrefixLength = value; } } public NetworkAddress EDnsClientSubnetIpv4Override { get { return _eDnsClientSubnetIpv4Override; } set { if (value is not null) { if (value.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("EDNS Client Subnet IPv4 Override must be an IPv4 network address.", nameof(EDnsClientSubnetIpv4Override)); if (value.IsHostAddress) value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv4PrefixLength); } _eDnsClientSubnetIpv4Override = value; } } public NetworkAddress EDnsClientSubnetIpv6Override { get { return _eDnsClientSubnetIpv6Override; } set { if (value is not null) { if (value.AddressFamily != AddressFamily.InterNetworkV6) throw new ArgumentException("EDNS Client Subnet IPv6 Override must be an IPv6 network address.", nameof(EDnsClientSubnetIpv6Override)); if (value.IsHostAddress) value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv6PrefixLength); } _eDnsClientSubnetIpv6Override = value; } } public int QpmLimitRequests { get { return _qpmLimitRequests; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(QpmLimitRequests), "Value cannot be less than 0."); if (_qpmLimitRequests != value) { if ((_qpmLimitRequests == 0) || (value == 0)) { _qpmLimitRequests = value; ResetQpsLimitTimer(); } else { _qpmLimitRequests = value; } } } } public int QpmLimitErrors { get { return _qpmLimitErrors; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(QpmLimitErrors), "Value cannot be less than 0."); if (_qpmLimitErrors != value) { if ((_qpmLimitErrors == 0) || (value == 0)) { _qpmLimitErrors = value; ResetQpsLimitTimer(); } else { _qpmLimitErrors = value; } } } } public int QpmLimitSampleMinutes { get { return _qpmLimitSampleMinutes; } set { if ((value < 1) || (value > 60)) throw new ArgumentOutOfRangeException(nameof(QpmLimitSampleMinutes), "Valid range is between 1 and 60 minutes."); _qpmLimitSampleMinutes = value; } } public int QpmLimitIPv4PrefixLength { get { return _qpmLimitIPv4PrefixLength; } set { if ((value < 0) || (value > 32)) throw new ArgumentOutOfRangeException(nameof(QpmLimitIPv4PrefixLength), "Valid range is between 0 and 32."); _qpmLimitIPv4PrefixLength = value; } } public int QpmLimitIPv6PrefixLength { get { return _qpmLimitIPv6PrefixLength; } set { if ((value < 0) || (value > 64)) throw new ArgumentOutOfRangeException(nameof(QpmLimitIPv6PrefixLength), "Valid range is between 0 and 64."); _qpmLimitIPv6PrefixLength = value; } } public IReadOnlyCollection QpmLimitBypassList { get { return _qpmLimitBypassList; } set { if ((value is null) || (value.Count == 0)) _qpmLimitBypassList = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(QpmLimitBypassList), "Networks cannot have more than 255 entries."); else _qpmLimitBypassList = value; } } public int ClientTimeout { get { return _clientTimeout; } set { if ((value < 1000) || (value > 10000)) throw new ArgumentOutOfRangeException(nameof(ClientTimeout), "Valid range is from 1000 to 10000."); _clientTimeout = value; } } public int TcpSendTimeout { get { return _tcpSendTimeout; } set { if ((value < 1000) || (value > 90000)) throw new ArgumentOutOfRangeException(nameof(TcpSendTimeout), "Valid range is from 1000 to 90000."); _tcpSendTimeout = value; } } public int TcpReceiveTimeout { get { return _tcpReceiveTimeout; } set { if ((value < 1000) || (value > 90000)) throw new ArgumentOutOfRangeException(nameof(TcpReceiveTimeout), "Valid range is from 1000 to 90000."); _tcpReceiveTimeout = value; } } public int QuicIdleTimeout { get { return _quicIdleTimeout; } set { if ((value < 1000) || (value > 90000)) throw new ArgumentOutOfRangeException(nameof(QuicIdleTimeout), "Valid range is from 1000 to 90000."); _quicIdleTimeout = value; } } public int QuicMaxInboundStreams { get { return _quicMaxInboundStreams; } set { if ((value < 0) || (value > 1000)) throw new ArgumentOutOfRangeException(nameof(QuicMaxInboundStreams), "Valid range is from 1 to 1000."); _quicMaxInboundStreams = value; } } public int ListenBacklog { get { return _listenBacklog; } set { _listenBacklog = value; } } public bool EnableDnsOverUdpProxy { get { return _enableDnsOverUdpProxy; } set { _enableDnsOverUdpProxy = value; } } public bool EnableDnsOverTcpProxy { get { return _enableDnsOverTcpProxy; } set { _enableDnsOverTcpProxy = value; } } public bool EnableDnsOverHttp { get { return _enableDnsOverHttp; } set { _enableDnsOverHttp = value; } } public bool EnableDnsOverTls { get { return _enableDnsOverTls; } set { _enableDnsOverTls = value; } } public bool EnableDnsOverHttps { get { return _enableDnsOverHttps; } set { _enableDnsOverHttps = value; } } public bool EnableDnsOverHttp3 { get { return _enableDnsOverHttp3; } set { _enableDnsOverHttp3 = value; } } public bool EnableDnsOverQuic { get { return _enableDnsOverQuic; } set { _enableDnsOverQuic = value; } } public int DnsOverUdpProxyPort { get { return _dnsOverUdpProxyPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverUdpProxyPort), "Port number valid range is from 0 to 65535."); _dnsOverUdpProxyPort = value; } } public int DnsOverTcpProxyPort { get { return _dnsOverTcpProxyPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverTcpProxyPort), "Port number valid range is from 0 to 65535."); _dnsOverTcpProxyPort = value; } } public int DnsOverHttpPort { get { return _dnsOverHttpPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), "Port number valid range is from 0 to 65535."); _dnsOverHttpPort = value; } } public int DnsOverTlsPort { get { return _dnsOverTlsPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverTlsPort), "Port number valid range is from 0 to 65535."); _dnsOverTlsPort = value; } } public int DnsOverHttpsPort { get { return _dnsOverHttpsPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), "Port number valid range is from 0 to 65535."); _dnsOverHttpsPort = value; } } public int DnsOverQuicPort { get { return _dnsOverQuicPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverQuicPort), "Port number valid range is from 0 to 65535."); _dnsOverQuicPort = value; } } public X509Certificate2Collection CertificateCollection { get { return _certificateCollection; } set { if (value is null) { _certificateCollection = null; _sslServerAuthenticationOptions = null; _quicSslServerAuthenticationOptions = null; } else { X509Certificate2 serverCertificate = null; foreach (X509Certificate2 certificate in value) { if (certificate.HasPrivateKey) { serverCertificate = certificate; break; } } if (serverCertificate is null) throw new ArgumentException("DNS Server TLS certificate file must contain a certificate with private key."); _certificateCollection = value; SslStreamCertificateContext certificateContext = SslStreamCertificateContext.Create(serverCertificate, _certificateCollection, false); _sslServerAuthenticationOptions = new SslServerAuthenticationOptions() { ServerCertificateContext = certificateContext }; _quicSslServerAuthenticationOptions = new SslServerAuthenticationOptions() { ApplicationProtocols = quicApplicationProtocols, ServerCertificateContext = certificateContext }; } } } public string DnsOverHttpRealIpHeader { get { return _dnsOverHttpRealIpHeader; } set { if (string.IsNullOrEmpty(value)) _dnsOverHttpRealIpHeader = "X-Real-IP"; else if (value.Length > 255) throw new ArgumentException("DNS-over-HTTP Real IP header name cannot exceed 255 characters.", nameof(DnsOverHttpRealIpHeader)); else if (value.Contains(' ')) throw new ArgumentException("DNS-over-HTTP Real IP header name cannot contain invalid characters.", nameof(DnsOverHttpRealIpHeader)); else _dnsOverHttpRealIpHeader = value; } } public IReadOnlyDictionary TsigKeys { get { return _tsigKeys; } set { if ((value is null) || (value.Count == 0)) _tsigKeys = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(TsigKeys), "TSIG keys cannot have more than 255 entries."); else _tsigKeys = value; } } public DnsServerRecursion Recursion { get { return _recursion; } set { if (_recursion != value) { if ((_recursion == DnsServerRecursion.Deny) || (value == DnsServerRecursion.Deny)) { _recursion = value; ResetPrefetchTimers(); } else { _recursion = value; } } } } public IReadOnlyCollection RecursionNetworkACL { get { return _recursionNetworkACL; } set { if ((value is null) || (value.Count == 0)) _recursionNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(RecursionNetworkACL), "Network Access Control List cannot have more than 255 entries."); else _recursionNetworkACL = value; } } public bool RandomizeName { get { return _randomizeName; } set { _randomizeName = value; } } public bool QnameMinimization { get { return _qnameMinimization; } set { _qnameMinimization = value; } } public bool NsRevalidation { get { return _nsRevalidation; } set { _nsRevalidation = value; } } public int ResolverRetries { get { return _resolverRetries; } set { if ((value < 1) || (value > 10)) throw new ArgumentOutOfRangeException(nameof(ResolverRetries), "Valid range is from 1 to 10."); _resolverRetries = value; } } public int ResolverTimeout { get { return _resolverTimeout; } set { if ((value < 1000) || (value > 10000)) throw new ArgumentOutOfRangeException(nameof(ResolverTimeout), "Valid range is from 1000 to 10000."); _resolverTimeout = value; } } public int ResolverConcurrency { get { return _resolverConcurrency; } set { if ((value < 1) || (value > 4)) throw new ArgumentOutOfRangeException(nameof(ResolverConcurrency), "Valid range is from 1 to 4."); _resolverConcurrency = value; } } public int ResolverMaxStackCount { get { return _resolverMaxStackCount; } set { if ((value < 10) || (value > 30)) throw new ArgumentOutOfRangeException(nameof(ResolverMaxStackCount), "Valid range is from 10 to 30."); _resolverMaxStackCount = value; } } public bool ServeStale { get { return _serveStale; } set { _serveStale = value; } } public int ServeStaleMaxWaitTime { get { return _serveStaleMaxWaitTime; } set { if ((value < 0) || (value > 1800)) throw new ArgumentOutOfRangeException(nameof(ServeStaleMaxWaitTime), "Serve stale max wait time valid range is 0 to 1800 milliseconds. Default value is 1800 milliseconds."); _serveStaleMaxWaitTime = value; } } public int CachePrefetchEligibility { get { return _cachePrefetchEligibility; } set { if (value < 2) throw new ArgumentOutOfRangeException(nameof(CachePrefetchEligibility), "Valid value is greater that or equal to 2."); _cachePrefetchEligibility = value; } } public int CachePrefetchTrigger { get { return _cachePrefetchTrigger; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(CachePrefetchTrigger), "Valid value is greater that or equal to 0."); if (_cachePrefetchTrigger != value) { if ((_cachePrefetchTrigger == 0) || (value == 0)) { _cachePrefetchTrigger = value; ResetPrefetchTimers(); } else { _cachePrefetchTrigger = value; } } } } public int CachePrefetchSampleIntervalInMinutes { get { return _cachePrefetchSampleIntervalInMinutes; } set { if ((value < 1) || (value > 60)) throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleIntervalInMinutes), "Valid range is between 1 and 60 minutes."); _cachePrefetchSampleIntervalInMinutes = value; } } public int CachePrefetchSampleEligibilityHitsPerHour { get { return _cachePrefetchSampleEligibilityHitsPerHour; } set { if (value < 1) throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleEligibilityHitsPerHour), "Valid value is greater than or equal to 1."); _cachePrefetchSampleEligibilityHitsPerHour = value; } } public bool EnableBlocking { get { return _enableBlocking; } set { _enableBlocking = value; } } public bool AllowTxtBlockingReport { get { return _allowTxtBlockingReport; } set { _allowTxtBlockingReport = value; } } public IReadOnlyCollection BlockingBypassList { get { return _blockingBypassList; } set { if ((value is null) || (value.Count == 0)) _blockingBypassList = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(BlockingBypassList), "Networks cannot have more than 255 entries."); else _blockingBypassList = value; } } public DnsServerBlockingType BlockingType { get { return _blockingType; } set { _blockingType = value; } } public uint BlockingAnswerTtl { get { return _blockingAnswerTtl; } set { if (_blockingAnswerTtl != value) { _blockingAnswerTtl = value; //update SOA MINIMUM values _blockedZoneManager.UpdateServerDomain(); _blockListZoneManager.UpdateServerDomain(); } } } public IReadOnlyCollection CustomBlockingARecords { get { return _customBlockingARecords; } set { if (value is null) value = Array.Empty(); _customBlockingARecords = value; } } public IReadOnlyCollection CustomBlockingAAAARecords { get { return _customBlockingAAAARecords; } set { if (value is null) value = Array.Empty(); _customBlockingAAAARecords = value; } } public NetProxy Proxy { get { return _proxy; } set { _proxy = value; } } public IReadOnlyList Forwarders { get { return _forwarders; } set { _forwarders = value; } } public bool ConcurrentForwarding { get { return _concurrentForwarding; } set { _concurrentForwarding = value; } } public int ForwarderRetries { get { return _forwarderRetries; } set { if ((value < 1) || (value > 10)) throw new ArgumentOutOfRangeException(nameof(ForwarderRetries), "Valid range is from 1 to 10."); _forwarderRetries = value; } } public int ForwarderTimeout { get { return _forwarderTimeout; } set { if ((value < 1000) || (value > 10000)) throw new ArgumentOutOfRangeException(nameof(ForwarderTimeout), "Valid range is from 1000 to 10000."); _forwarderTimeout = value; } } public int ForwarderConcurrency { get { return _forwarderConcurrency; } set { if ((value < 1) || (value > 10)) throw new ArgumentOutOfRangeException(nameof(ForwarderConcurrency), "Valid range is from 1 to 10."); _forwarderConcurrency = value; } } public LogManager ResolverLogManager { get { return _resolverLog; } set { _resolverLog = value; } } public LogManager QueryLogManager { get { return _queryLog; } set { _queryLog = value; } } #endregion class CacheRefreshSample { public CacheRefreshSample(DnsQuestionRecord sampleQuestion, IReadOnlyList conditionalForwarders) { SampleQuestion = sampleQuestion; ConditionalForwarders = conditionalForwarders; } public DnsQuestionRecord SampleQuestion { get; } public IReadOnlyList ConditionalForwarders { get; } } class RecursiveResolveResponse { public RecursiveResolveResponse(DnsDatagram response, DnsDatagram checkingDisabledResponse) { Response = response; CheckingDisabledResponse = checkingDisabledResponse; } public DnsDatagram Response { get; } public DnsDatagram CheckingDisabledResponse { get; } } } #pragma warning restore CA2252 // This API requires opting into preview features #pragma warning restore CA1416 // Validate platform compatibility }