diff --git a/DnsServerCore/Dns/DnsServer.cs b/DnsServerCore/Dns/DnsServer.cs index ea74e468..ac4baa62 100644 --- a/DnsServerCore/Dns/DnsServer.cs +++ b/DnsServerCore/Dns/DnsServer.cs @@ -42,6 +42,7 @@ 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; @@ -64,7 +65,7 @@ namespace DnsServerCore.Dns Deny = 0, Allow = 1, AllowOnlyForPrivateNetworks = 2, - UseSpecifiedNetworks = 3 + UseSpecifiedNetworkACL = 3 } public enum DnsServerBlockingType : byte @@ -93,10 +94,11 @@ namespace DnsServerCore.Dns 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[] { new DnsARecordData(IPAddress.Any) }; - static readonly IReadOnlyCollection _aaaaRecords = new DnsAAAARecordData[] { new DnsAAAARecordData(IPAddress.IPv6Any) }; + 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; @@ -161,6 +163,7 @@ namespace DnsServerCore.Dns bool _enableDnsOverHttp; bool _enableDnsOverTls; bool _enableDnsOverHttps; + bool _enableDnsOverHttp3; bool _enableDnsOverQuic; int _dnsOverUdpProxyPort = 538; int _dnsOverTcpProxyPort = 538; @@ -175,8 +178,7 @@ namespace DnsServerCore.Dns IReadOnlyDictionary _tsigKeys; DnsServerRecursion _recursion; - IReadOnlyCollection _recursionDeniedNetworks; - IReadOnlyCollection _recursionAllowedNetworks; + IReadOnlyCollection _recursionNetworkACL; bool _randomizeName; bool _qnameMinimization; @@ -184,6 +186,7 @@ namespace DnsServerCore.Dns int _resolverRetries = 2; int _resolverTimeout = 1500; + int _resolverConcurrency = 2; int _resolverMaxStackCount = 16; bool _serveStale = true; @@ -202,6 +205,7 @@ namespace DnsServerCore.Dns NetProxy _proxy; IReadOnlyList _forwarders; + bool _concurrentForwarding = true; int _forwarderRetries = 3; int _forwarderTimeout = 2000; int _forwarderConcurrency = 2; @@ -462,7 +466,7 @@ namespace DnsServerCore.Dns try { - DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); + DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { _stats.QueueUpdate(null, remoteEP, protocol, null, false); @@ -609,11 +613,14 @@ namespace DnsServerCore.Dns SslStream tlsStream = new SslStream(new NetworkStream(socket)); string serverName = null; - await tlsStream.AuthenticateAsServerAsync(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) + await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { - serverName = clientHelloInfo.ServerName; - return ValueTask.FromResult(_sslServerAuthenticationOptions); - }, null, default).WithTimeout(_tcpReceiveTimeout); + return tlsStream.AuthenticateAsServerAsync(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) + { + serverName = clientHelloInfo.ServerName; + return ValueTask.FromResult(_sslServerAuthenticationOptions); + }, null, cancellationToken1); + }, _tcpReceiveTimeout); NameServerAddress dnsEP; @@ -633,7 +640,11 @@ namespace DnsServerCore.Dns return; } - ProxyProtocolStream proxyStream = await ProxyProtocolStream.CreateAsServerAsync(new NetworkStream(socket)).WithTimeout(_tcpReceiveTimeout); + 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); @@ -643,6 +654,10 @@ namespace DnsServerCore.Dns throw new InvalidOperationException(); } } + catch (AuthenticationException) + { + //ignore TLS auth exception + } catch (TimeoutException) { //ignore timeout exception on TLS auth @@ -719,7 +734,7 @@ namespace DnsServerCore.Dns { try { - DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); + DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { await stream.DisposeAsync(); @@ -859,7 +874,7 @@ namespace DnsServerCore.Dns } //process request async - DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address)); + DnsDatagram response = await ProcessRequestAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address)); if (response is null) { _stats.QueueUpdate(null, remoteEP, DnsTransportProtocol.Quic, null, false); @@ -1000,7 +1015,7 @@ namespace DnsServerCore.Dns throw new InvalidOperationException(); } - DnsDatagram dnsResponse = await PreProcessQueryAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address)); + DnsDatagram dnsResponse = await ProcessRequestAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address)); if (dnsResponse is null) { //drop request @@ -1027,6 +1042,10 @@ namespace DnsServerCore.Dns _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) @@ -1054,36 +1073,15 @@ namespace DnsServerCore.Dns return false; } - case DnsServerRecursion.UseSpecifiedNetworks: - if (_recursionDeniedNetworks is not null) - { - foreach (NetworkAddress deniedNetworkAddress in _recursionDeniedNetworks) - { - if (deniedNetworkAddress.Contains(remoteIP)) - return false; - } - } - - if (_recursionAllowedNetworks is not null) - { - foreach (NetworkAddress allowedNetworkAddress in _recursionAllowedNetworks) - { - if (allowedNetworkAddress.Contains(remoteIP)) - return true; - } - } - - if (IPAddress.IsLoopback(remoteIP)) - return true; - - return false; + case DnsServerRecursion.UseSpecifiedNetworkACL: + return NetworkAccessControl.IsAddressAllowed(remoteIP, _recursionNetworkACL, true); default: return false; } } - private async Task PreProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) + private async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { foreach (IDnsRequestController requestController in _dnsApplicationManager.DnsRequestControllers) { @@ -1256,6 +1254,15 @@ namespace DnsServerCore.Dns //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); @@ -1276,8 +1283,8 @@ namespace DnsServerCore.Dns private async Task ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { - AuthZoneInfo authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); - if ((authZoneInfo is null) || (authZoneInfo.Type != AuthZoneType.Secondary) || authZoneInfo.Disabled) + 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) @@ -1291,9 +1298,22 @@ namespace DnsServerCore.Dns } } - IReadOnlyList primaryNameServers = await authZoneInfo.GetPrimaryNameServerAddressesAsync(this); + IReadOnlyList primaryNameServerAddresses; - foreach (NameServerAddress primaryNameServer in primaryNameServers) + SecondaryCatalogZone secondaryCatalogZone = null; + if (zoneInfo.CatalogZoneName is not null) + { + AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.CatalogZoneName, zoneInfo.CatalogZoneName); + if (authZone is SecondaryCatalogZone catalogZone) + secondaryCatalogZone = catalogZone; + } + + 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; @@ -1304,16 +1324,16 @@ namespace DnsServerCore.Dns 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _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 = authZoneInfo.GetApexRecords(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)) { @@ -1322,7 +1342,7 @@ namespace DnsServerCore.Dns } } - authZoneInfo.TriggerRefresh(); + zoneInfo.TriggerRefresh(); return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } @@ -1334,16 +1354,16 @@ namespace DnsServerCore.Dns if (request.Question[0].Class != DnsClass.IN) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; - AuthZoneInfo authZoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name); - if ((authZoneInfo is null) || authZoneInfo.Disabled) + 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _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 authZoneInfo.GetSecondaryNameServerAddressesAsync(this); + IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { @@ -1354,27 +1374,11 @@ namespace DnsServerCore.Dns return false; } - bool IsSpecifiedIpAddressAllowed() - { - IPAddress remoteAddress = remoteEP.Address; - IReadOnlyCollection specifiedIpAddresses = authZoneInfo.UpdateIpAddresses; - if (specifiedIpAddresses is not null) - { - foreach (NetworkAddress networkAddress in specifiedIpAddresses) - { - if (networkAddress.Contains(remoteAddress)) - return true; - } - } - - return false; - } - async Task IsUpdatePermittedAsync() { bool isUpdateAllowed; - switch (authZoneInfo.Update) + switch (zoneInfo.Update) { case AuthZoneUpdate.Allow: isUpdateAllowed = true; @@ -1384,12 +1388,12 @@ namespace DnsServerCore.Dns isUpdateAllowed = await IsZoneNameServerAllowedAsync(); break; - case AuthZoneUpdate.AllowOnlySpecifiedIpAddresses: - isUpdateAllowed = IsSpecifiedIpAddressAllowed(); + case AuthZoneUpdate.UseSpecifiedNetworkACL: + isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL); break; - case AuthZoneUpdate.AllowBothZoneNameServersAndSpecifiedIpAddresses: - isUpdateAllowed = IsSpecifiedIpAddressAllowed() || await IsZoneNameServerAllowedAsync(); + case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: + isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL) || await IsZoneNameServerAllowedAsync(); break; case AuthZoneUpdate.Deny: @@ -1400,17 +1404,17 @@ namespace DnsServerCore.Dns if (!isUpdateAllowed) { - _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _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 ((authZoneInfo.UpdateSecurityPolicies is not null) && (authZoneInfo.UpdateSecurityPolicies.Count > 0)) + if ((zoneInfo.UpdateSecurityPolicies is not null) && (zoneInfo.UpdateSecurityPolicies.Count > 0)) { - if ((tsigAuthenticatedKeyName is null) || !authZoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLowerInvariant(), out IReadOnlyDictionary> policyMap)) + 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: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _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; } @@ -1443,7 +1447,7 @@ namespace DnsServerCore.Dns if (!isPermitted) { - _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request [" + uRecord.Name.ToLowerInvariant() + " " + uRecord.Type.ToString() + " " + uRecord.Class.ToString() + "] due to Dynamic Updates Security Policy for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _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; } @@ -1453,7 +1457,7 @@ namespace DnsServerCore.Dns return true; } - switch (authZoneInfo.Type) + switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: @@ -1469,7 +1473,7 @@ namespace DnsServerCore.Dns return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(prRecord.Name); - if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) + 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) @@ -1480,13 +1484,13 @@ namespace DnsServerCore.Dns if (prRecord.Type == DnsResourceRecordType.ANY) { //check if name is in use - if (!_authZoneManager.NameExists(authZoneInfo.Name, prRecord.Name)) + 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(authZoneInfo.Name, prRecord.Name, prRecord.Type); + 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 }; } @@ -1499,13 +1503,13 @@ namespace DnsServerCore.Dns if (prRecord.Type == DnsResourceRecordType.ANY) { //check if name is not in use - if (_authZoneManager.NameExists(authZoneInfo.Name, prRecord.Name)) + 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(authZoneInfo.Name, prRecord.Name, prRecord.Type); + 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 }; } @@ -1543,7 +1547,7 @@ namespace DnsServerCore.Dns foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { IReadOnlyList prRRSet = rrsetEntry.Value; - IReadOnlyList rrset = _authZoneManager.GetRecords(authZoneInfo.Name, zoneEntry.Key, rrsetEntry.Key); + IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, zoneEntry.Key, rrsetEntry.Key); //check if RRSet exists (value dependent) //compare RRSets @@ -1587,7 +1591,7 @@ namespace DnsServerCore.Dns foreach (DnsResourceRecord uRecord in request.Authority) { AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(uRecord.Name); - if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) + 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) @@ -1661,43 +1665,43 @@ namespace DnsServerCore.Dns //Add to an RRset if (uRecord.Type == DnsResourceRecordType.CNAME) { - if (_authZoneManager.NameExists(authZoneInfo.Name, uRecord.Name) && (_authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count == 0)) + 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(authZoneInfo.Name, uRecord.Name, uRecord.Type); + IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); - _authZoneManager.SetRecord(authZoneInfo.Name, uRecord); + _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.DNAME) { - IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); + IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); - _authZoneManager.SetRecord(authZoneInfo.Name, uRecord); + _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.SOA) { - if (!uRecord.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) + if (!uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //can add SOA only to apex - IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); + IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); - _authZoneManager.SetRecord(authZoneInfo.Name, uRecord); + _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else { - if (_authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count > 0) + 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(authZoneInfo.Name, uRecord.Name, uRecord.Type); + 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(authZoneInfo.Name, uRecord); + _authZoneManager.AddRecord(zoneInfo.Name, uRecord); } } else if (uRecord.Class == DnsClass.ANY) @@ -1705,9 +1709,9 @@ namespace DnsServerCore.Dns if (uRecord.Type == DnsResourceRecordType.ANY) { //Delete all RRsets from a name - IReadOnlyDictionary> existingRRSets = _authZoneManager.GetAllRecords(authZoneInfo.Name, uRecord.Name); + IReadOnlyDictionary> existingRRSets = _authZoneManager.GetEntriesFor(zoneInfo.Name, uRecord.Name); - if (uRecord.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) + if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> existingRRSet in existingRRSets) { @@ -1725,7 +1729,7 @@ namespace DnsServerCore.Dns AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value); - _authZoneManager.DeleteRecords(authZoneInfo.Name, uRecord.Name, existingRRSet.Key); + _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key); } } else @@ -1744,14 +1748,14 @@ namespace DnsServerCore.Dns AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value); - _authZoneManager.DeleteRecords(authZoneInfo.Name, uRecord.Name, existingRRSet.Key); + _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key); } } } else { //Delete an RRset - if (uRecord.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) + if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) { switch (uRecord.Type) { @@ -1766,10 +1770,10 @@ namespace DnsServerCore.Dns } } - IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); + IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); - _authZoneManager.DeleteRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); + _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); } } else if (uRecord.Class == DnsClass.NONE) @@ -1787,14 +1791,14 @@ namespace DnsServerCore.Dns continue; //no SOA can be deleted; skip DNSSEC rrsets } - IReadOnlyList existingRRSet = _authZoneManager.GetRecords(authZoneInfo.Name, uRecord.Name, uRecord.Type); + IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); - if ((uRecord.Type == DnsResourceRecordType.NS) && (existingRRSet.Count == 1) && uRecord.Name.Equals(authZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) + 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(authZoneInfo.Name, uRecord.Name, uRecord.Type, uRecord.RDATA); + _authZoneManager.DeleteRecord(zoneInfo.Name, uRecord.Name, uRecord.Type, uRecord.RDATA); } } } @@ -1806,9 +1810,9 @@ namespace DnsServerCore.Dns foreach (KeyValuePair> originalRRSet in originalRRSetEntries.Value) { if (originalRRSet.Value.Count == 0) - _authZoneManager.DeleteRecords(authZoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key); + _authZoneManager.DeleteRecords(zoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key); else - _authZoneManager.SetRecords(authZoneInfo.Name, originalRRSet.Value); + _authZoneManager.SetRecords(zoneInfo.Name, originalRRSet.Value); } } @@ -1816,15 +1820,16 @@ namespace DnsServerCore.Dns } } - _authZoneManager.SaveZoneFile(authZoneInfo.Name); + _authZoneManager.SaveZoneFile(zoneInfo.Name); - _log?.Write(remoteEP, protocol, "DNS Server successfully processed a zone UPDATE request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.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 @@ -1832,28 +1837,45 @@ namespace DnsServerCore.Dns 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 primaryNameServers = await authZoneInfo.GetPrimaryNameServerAddressesAsync(this); + IReadOnlyList primaryNameServerAddresses; + DnsTransportProtocol primaryZoneTransferProtocol; - DnsResourceRecord soaRecord = authZoneInfo.GetApexRecords(DnsResourceRecordType.SOA)[0]; - SOARecordInfo recordInfo = soaRecord.GetAuthSOARecordInfo(); + SecondaryCatalogZone secondaryCatalogZone = null; + if (zoneInfo.CatalogZoneName is not null) + { + AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.CatalogZoneName, zoneInfo.CatalogZoneName); + if (authZone is SecondaryCatalogZone catalogZone) + secondaryCatalogZone = catalogZone; + } - switch (recordInfo.ZoneTransferProtocol) + 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(primaryNameServers.Count); + List updatedNameServers = new List(primaryNameServerAddresses.Count); - foreach (NameServerAddress primaryNameServer in primaryNameServers) + foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { - if (primaryNameServer.Protocol == recordInfo.ZoneTransferProtocol) + if (primaryNameServer.Protocol == primaryZoneTransferProtocol) updatedNameServers.Add(primaryNameServer); else - updatedNameServers.Add(primaryNameServer.ChangeProtocol(recordInfo.ZoneTransferProtocol)); + updatedNameServers.Add(primaryNameServer.ChangeProtocol(primaryZoneTransferProtocol)); } - primaryNameServers = updatedNameServers; + primaryNameServerAddresses = updatedNameServers; } break; @@ -1861,9 +1883,9 @@ namespace DnsServerCore.Dns if (protocol == DnsTransportProtocol.Tcp) { //change name server protocol to TCP - List updatedNameServers = new List(primaryNameServers.Count); + List updatedNameServers = new List(primaryNameServerAddresses.Count); - foreach (NameServerAddress primaryNameServer in primaryNameServers) + foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) updatedNameServers.Add(primaryNameServer); @@ -1871,7 +1893,7 @@ namespace DnsServerCore.Dns updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); } - primaryNameServers = updatedNameServers; + primaryNameServerAddresses = updatedNameServers; } break; } @@ -1879,9 +1901,9 @@ namespace DnsServerCore.Dns TsigKey key = null; if (!string.IsNullOrEmpty(tsigAuthenticatedKeyName) && ((_tsigKeys is null) || !_tsigKeys.TryGetValue(tsigAuthenticatedKeyName, out key))) - throw new DnsServerException("DNS Server does not have TSIG key '" + tsigAuthenticatedKeyName + "' configured to authenticate dynamic updates for secondary zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + 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(primaryNameServers); + DnsClient dnsClient = new DnsClient(primaryNameServerAddresses); dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; @@ -1912,9 +1934,9 @@ namespace DnsServerCore.Dns private async Task ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName) { AuthZoneInfo authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); - if ((authZoneInfo is null) || !authZoneInfo.IsActive) + if ((authZoneInfo is null) || !authZoneInfo.ApexZone.IsActive) { - _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request as the zone was not found or was inactive, for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request as the zone was not found or was inactive, for zone: " + authZoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } @@ -1923,18 +1945,20 @@ namespace DnsServerCore.Dns { case AuthZoneType.Primary: case AuthZoneType.Secondary: + case AuthZoneType.Forwarder: + case AuthZoneType.Catalog: break; default: - _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the DNS server is not authoritative for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the DNS server is not authoritative for zone: " + authZoneInfo.DisplayName); 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() + async Task IsZoneNameServerAllowedAsync(ApexZone apexZone) { IPAddress remoteAddress = remoteEP.Address; - IReadOnlyList secondaryNameServers = await authZoneInfo.GetSecondaryNameServerAddressesAsync(this); + IReadOnlyList secondaryNameServers = await apexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { @@ -1945,18 +1969,35 @@ namespace DnsServerCore.Dns return false; } - bool IsSpecifiedNameServerAllowed() + async Task IsZoneTransferAllowed(ApexZone apexZone) { - IPAddress remoteAddress = remoteEP.Address; - IReadOnlyCollection specifiedNameServers = authZoneInfo.ZoneTransferNameServers; - if (specifiedNameServers is not null) + switch (apexZone.ZoneTransfer) { - foreach (NetworkAddress specifiedNetwork in specifiedNameServers) - { - if (specifiedNetwork.Contains(remoteAddress)) - return true; - } + 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; } @@ -1979,49 +2020,27 @@ namespace DnsServerCore.Dns if (!isInZoneTransferAllowedList) { - bool isZoneTransferAllowed = false; + ApexZone apexZone = authZoneInfo.ApexZone; - switch (authZoneInfo.ZoneTransfer) + if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogZoneTransfer) + apexZone = apexZone.CatalogZone; //use catalog zone transfer options + + if (!await IsZoneTransferAllowed(apexZone)) { - case AuthZoneTransfer.Deny: - break; - - case AuthZoneTransfer.Allow: - isZoneTransferAllowed = true; - break; - - case AuthZoneTransfer.AllowOnlyZoneNameServers: - isZoneTransferAllowed = await IsZoneNameServerAllowedAsync(); - break; - - case AuthZoneTransfer.AllowOnlySpecifiedNameServers: - isZoneTransferAllowed = IsSpecifiedNameServerAllowed(); - break; - - case AuthZoneTransfer.AllowBothZoneAndSpecifiedNameServers: - isZoneTransferAllowed = IsSpecifiedNameServerAllowed() || await IsZoneNameServerAllowedAsync(); - break; - } - - if (!isZoneTransferAllowed) - { - _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + authZoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } - if ((authZoneInfo.ZoneTransferTsigKeyNames is not null) && (authZoneInfo.ZoneTransferTsigKeyNames.Count > 0)) + if (!IsTsigAuthenticated(apexZone)) { - if ((tsigAuthenticatedKeyName is null) || !authZoneInfo.ZoneTransferTsigKeyNames.ContainsKey(tsigAuthenticatedKeyName.ToLowerInvariant())) - { - _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + authZoneInfo.DisplayName); - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; - } + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } - _log?.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + authZoneInfo.DisplayName); IReadOnlyList xfrRecords; @@ -2049,7 +2068,7 @@ namespace DnsServerCore.Dns if (response is null) return null; - bool reprocessResponse; + bool reprocessResponse; //to allow resolving CNAME/ANAME in response do { reprocessResponse = false; @@ -2066,11 +2085,11 @@ namespace DnsServerCore.Dns switch (lastRR.Type) { case DnsResourceRecordType.CNAME: - return await ProcessCNAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol, false, skipDnsAppAuthoritativeRequestHandlers); + return await ProcessCNAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, false, skipDnsAppAuthoritativeRequestHandlers); case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: - return await ProcessANAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol, skipDnsAppAuthoritativeRequestHandlers); + return await ProcessANAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); } } } @@ -2083,17 +2102,20 @@ namespace DnsServerCore.Dns if (request.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache - return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, Array.Empty(), _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); + response = await RecursiveResolveAsync(request, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); + reprocessResponse = true; } break; case DnsResourceRecordType.FWD: //do conditional forwarding - return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); + response = await RecursiveResolveAsync(request, remoteEP, response.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); + reprocessResponse = true; + break; case DnsResourceRecordType.APP: - response = await ProcessAPPAsync(request, remoteEP, response, isRecursionAllowed, protocol); + response = await ProcessAPPAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); reprocessResponse = true; break; } @@ -2107,7 +2129,11 @@ namespace DnsServerCore.Dns private async Task AuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { - DnsDatagram response = _authZoneManager.Query(request, isRecursionAllowed); + 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; @@ -2140,7 +2166,7 @@ namespace DnsServerCore.Dns return null; } - private async Task ProcessAPPAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol) + 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; @@ -2159,9 +2185,7 @@ namespace DnsServerCore.Dns if (zoneInfo.Type == AuthZoneType.Forwarder) { - //return FWD response - rcode = DnsResponseCode.NoError; - + //process FWD record if exists if (!zoneInfo.Name.Equals(appResourceRecord.Name, StringComparison.OrdinalIgnoreCase)) { AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.Name, appResourceRecord.Name); @@ -2171,6 +2195,11 @@ namespace DnsServerCore.Dns 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 { @@ -2180,7 +2209,7 @@ namespace DnsServerCore.Dns else rcode = DnsResponseCode.NxDomain; - authority = zoneInfo.GetApexRecords(DnsResourceRecordType.SOA); + 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 }; @@ -2206,13 +2235,13 @@ namespace DnsServerCore.Dns //return server failure response with SOA { AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name); - IReadOnlyList authority = zoneInfo.GetApexRecords(DnsResourceRecordType.SOA); + 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, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) + 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); @@ -2282,7 +2311,7 @@ namespace DnsServerCore.Dns } else if ((newResponse.Answer.Count > 0) && (newResponse.GetLastAnswerRecord() is DnsResourceRecord lastAnswer) && ((lastAnswer.Type == DnsResourceRecordType.ANAME) || (lastAnswer.Type == DnsResourceRecordType.ALIAS))) { - newResponse = await ProcessANAMEAsync(request, remoteEP, newResponse, isRecursionAllowed, protocol, skipDnsAppAuthoritativeRequestHandlers); + newResponse = await ProcessANAMEAsync(request, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { @@ -2294,7 +2323,7 @@ namespace DnsServerCore.Dns if (newRequest.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolveDnsCache - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, Array.Empty(), _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } @@ -2307,7 +2336,7 @@ namespace DnsServerCore.Dns break; case DnsResourceRecordType.APP: - newResponse = await ProcessAPPAsync(newRequest, remoteEP, newResponse, isRecursionAllowed, protocol); + newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); break; } } @@ -2393,7 +2422,7 @@ namespace DnsServerCore.Dns return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, newAnswer, authority, additional) { Tag = response.Tag }; } - private async Task ProcessANAMEAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, bool skipDnsAppAuthoritativeRequestHandlers) + private async Task ProcessANAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { EDnsOption[] eDnsClientSubnetOption = null; @@ -2431,7 +2460,7 @@ namespace DnsServerCore.Dns { case DnsResourceRecordType.NS: //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, Array.Empty(), _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); break; case DnsResourceRecordType.FWD: @@ -2440,7 +2469,7 @@ namespace DnsServerCore.Dns break; case DnsResourceRecordType.APP: - newResponse = await ProcessAPPAsync(newRequest, remoteEP, newResponse, isRecursionAllowed, protocol); + newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); break; } } @@ -2679,6 +2708,17 @@ namespace DnsServerCore.Dns 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) @@ -2772,7 +2812,7 @@ namespace DnsServerCore.Dns DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY)) - response = await ProcessCNAMEAsync(request, remoteEP, response, true, protocol, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); + response = await ProcessCNAMEAsync(request, response, remoteEP, protocol, true, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); if (!isAllowed) { @@ -2964,7 +3004,7 @@ namespace DnsServerCore.Dns //got new resolver task added so question is not being resolved; do recursive resolution in another task on resolver thread pool _ = Task.Factory.StartNew(delegate () { - return RecursiveResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource); + return RecursiveResolverBackgroundTaskAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _resolverTaskScheduler); } @@ -3029,11 +3069,11 @@ namespace DnsServerCore.Dns } //no response available; respond with ServerFailure - EDnsOption[] options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Waiting for resolver")) }; + EDnsOption[] options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Waiting for resolver"))]; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); } - private async Task RecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource taskCompletionSource) + private async Task RecursiveResolverBackgroundTaskAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource taskCompletionSource) { try { @@ -3047,155 +3087,28 @@ namespace DnsServerCore.Dns else dnsCache = _dnsCache; - //check for this-server - if ((conditionalForwarders is not null) && (conditionalForwarders.Count == 1) && (conditionalForwarders[0].RDATA is DnsForwarderRecordData fwd) && fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) - { - //resolve directly with DNSSEC validation preference - conditionalForwarders = null; - dnssecValidation = fwd.DnssecValidation; - } - DnsDatagram response; - if ((conditionalForwarders is not null) && (conditionalForwarders.Count > 0)) + if (conditionalForwarders is not null) { - //check for forwarder name server resolution - foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) + if (conditionalForwarders.Count > 0) { - if (conditionalForwarder.Type != DnsResourceRecordType.FWD) - continue; - - DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; - - if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) - continue; - - NetProxy proxy = forwarder.GetProxy(_proxy); - if (proxy is null) - { - //recursive resolve name server when proxy is null else let proxy resolve it - if (forwarder.NameServer.IsIPEndPointStale) //refresh forwarder IPEndPoint if stale - await forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout); - } - } - - if (conditionalForwarders.Count == 1) - { - DnsResourceRecord conditionalForwarder = conditionalForwarders[0]; - response = await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name); + //do priority based conditional forwarding + response = await PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders); } else { - using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) + //do force recursive resolution + response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { - CancellationToken cancellationToken = cancellationTokenSource.Token; - List> tasks = new List>(conditionalForwarders.Count); - - //start worker tasks - foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) - { - if (conditionalForwarder.Type != DnsResourceRecordType.FWD) - continue; - - DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; - - if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) - continue; - - tasks.Add(Task.Factory.StartNew(delegate () - { - return ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, forwarder, conditionalForwarder.Name, cancellationToken); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current).Unwrap()); - } - - //wait for first positive response, or for all tasks to fault - response = null; - DnsDatagram lastResponse = null; - Exception lastException = null; - - while (tasks.Count > 0) - { - Task completedTask = await Task.WhenAny(tasks); - - if (completedTask.Status == TaskStatus.RanToCompletion) - { - //resolver task complete - DnsDatagram taskResponse = await completedTask; //await to get response - bool foundResponse = false; - - switch (taskResponse.RCODE) - { - case DnsResponseCode.NoError: - case DnsResponseCode.NxDomain: - case DnsResponseCode.YXDomain: - cancellationTokenSource.Cancel(); //to stop other resolver tasks - response = taskResponse; - foundResponse = true; - break; - - default: - //keep response - lastResponse = taskResponse; - break; - } - - if (foundResponse) - break; - } - - tasks.Remove(completedTask); - lastException = completedTask.Exception; - - if (lastException is AggregateException) - lastException = lastException.InnerException; - } - - if (response is null) - { - if (lastResponse is not null) - response = lastResponse; - else if (lastException is not null) - ExceptionDispatchInfo.Capture(lastException).Throw(); - else - throw new InvalidOperationException(); - } - } + 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 if ((conditionalForwarders is null) && (_forwarders is not null) && (_forwarders.Count > 0)) - { - //use forwarders - if (_proxy is null) - { - //recursive resolve name server when proxy is null else let proxy resolve it - foreach (NameServerAddress nameServerAddress in _forwarders) - { - if (nameServerAddress.IsIPEndPointStale) //refresh forwarder IPEndPoint if stale - await nameServerAddress.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout); - } - } - - //query forwarders and update cache - DnsClient dnsClient = new DnsClient(_forwarders); - - dnsClient.Cache = dnsCache; - dnsClient.Proxy = _proxy; - dnsClient.PreferIPv6 = _preferIPv6; - dnsClient.RandomizeName = _randomizeName; - dnsClient.Retries = _forwarderRetries; - dnsClient.Timeout = _forwarderTimeout; - dnsClient.Concurrency = _forwarderConcurrency; - dnsClient.UdpPayloadSize = _udpPayloadSize; - dnsClient.DnssecValidation = dnssecValidation; - dnsClient.EDnsClientSubnet = eDnsClientSubnet; - dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them - - response = await dnsClient.ResolveAsync(question); - } else { - //do recursive resolution - response = await DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, _nsRevalidation, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverMaxStackCount, true, true); + //do default recursive resolution + response = await DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, dnssecValidation, skipDnsAppAuthoritativeRequestHandlers); } switch (response.RCODE) @@ -3310,16 +3223,16 @@ namespace DnsServerCore.Dns { IReadOnlyList options; - if (ex.InnerException is SocketException ex3a) + if (ex3.InnerException is SocketException ex3a) { if (ex3a.SocketErrorCode == SocketError.TimedOut) - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out")) }; + options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))]; else - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex3a.SocketErrorCode.ToString())) }; + options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex3a.SocketErrorCode.ToString()))]; } else { - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "No response from name servers for " + question.ToString())) }; + 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); @@ -3331,9 +3244,9 @@ namespace DnsServerCore.Dns IReadOnlyList options; if (ex4.SocketErrorCode == SocketError.TimedOut) - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out")) }; + options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))]; else - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex4.SocketErrorCode.ToString())) }; + 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); @@ -3346,13 +3259,13 @@ namespace DnsServerCore.Dns if (ex5.InnerException is SocketException ex5a) { if (ex5a.SocketErrorCode == SocketError.TimedOut) - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out")) }; + options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NoReachableAuthority, "Request timed out"))]; else - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex5a.SocketErrorCode.ToString())) }; + options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "Socket error: " + ex5a.SocketErrorCode.ToString()))]; } else { - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.NetworkError, "IO error: " + ex5.Message)) }; + 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); @@ -3361,7 +3274,7 @@ namespace DnsServerCore.Dns } else { - IReadOnlyList options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Resolver exception")) }; + 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)); @@ -3373,24 +3286,328 @@ namespace DnsServerCore.Dns } } - private Task ConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, DnsForwarderRecordData forwarder, string conditionalForwardingZoneCut, CancellationToken cancellationToken = default) + private async Task DefaultRecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IDnsCache dnsCache, bool dnssecValidation, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { - DnsClient dnsClient = new DnsClient(forwarder.NameServer); + if ((_forwarders is not null) && (_forwarders.Count > 0)) + { + //use forwarders + if (_concurrentForwarding) + { + //recursive resolve forwarder + foreach (NameServerAddress forwarder in _forwarders) + { + if (forwarder.IsIPEndPointStale) + { + //refresh forwarder IPEndPoint if stale + await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) + { + return forwarder.RecursiveResolveIPAddressAsync(dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); + }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken); + } + } - 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; + //query forwarders and update cache + DnsClient dnsClient = new DnsClient(_forwarders); - return dnsClient.ResolveAsync(question, cancellationToken); + 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) + { + //recursive resolve forwarder + if (forwarder.IsIPEndPointStale) + { + //refresh forwarder IPEndPoint if stale + await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) + { + return forwarder.RecursiveResolveIPAddressAsync(dnsCache, _proxy, _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 + + //recursive resolve name server + if (forwarder.NameServer.IsIPEndPointStale) + { + //refresh forwarder IPEndPoint if stale + resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) + { + return forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, forwarder.GetProxy(_proxy), _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) @@ -4038,7 +4255,13 @@ namespace DnsServerCore.Dns { try { - _stats.GetLatestClientSubnetStats(_qpmLimitSampleMinutes, _qpmLimitIPv4PrefixLength, _qpmLimitIPv6PrefixLength, out _qpmLimitClientSubnetStats, out _qpmLimitErrorClientSubnetStats); + _stats.GetLatestClientSubnetStats(_qpmLimitSampleMinutes, _qpmLimitIPv4PrefixLength, _qpmLimitIPv6PrefixLength, out IReadOnlyDictionary qpmLimitClientSubnetStats, out IReadOnlyDictionary qpmLimitErrorClientSubnetStats); + + WriteClientSubnetRateLimitLog(_qpmLimitErrorClientSubnetStats, qpmLimitErrorClientSubnetStats, _qpmLimitErrors, "errors"); + WriteClientSubnetRateLimitLog(_qpmLimitClientSubnetStats, qpmLimitClientSubnetStats, _qpmLimitRequests, "requests"); + + _qpmLimitClientSubnetStats = qpmLimitClientSubnetStats; + _qpmLimitErrorClientSubnetStats = qpmLimitErrorClientSubnetStats; } catch (Exception ex) { @@ -4053,6 +4276,45 @@ namespace DnsServerCore.Dns } } + 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)) @@ -4139,7 +4401,7 @@ namespace DnsServerCore.Dns { serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions) { - listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; + listenOptions.Protocols = _enableDnsOverHttp3 ? HttpProtocols.Http1AndHttp2AndHttp3 : HttpProtocols.Http1AndHttp2; listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { return ValueTask.FromResult(_sslServerAuthenticationOptions); @@ -4341,10 +4603,29 @@ namespace DnsServerCore.Dns #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; - udpListener.Bind(localEP); + 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); @@ -4599,19 +4880,6 @@ namespace DnsServerCore.Dns UdpClientConnection.CreateSocketPool(_preferIPv6); }); } - - //update root servers list by priming query async - _ = Task.Run(async delegate () - { - try - { - await DnsClient.UpdateRootServersAsync(_proxy, _preferIPv6, _resolverRetries, _resolverTimeout); - } - catch (Exception ex) - { - _log.Write("Error while updating root servers list by priming query: " + ex.ToString()); - } - }); } public async Task StopAsync() @@ -4694,7 +4962,10 @@ namespace DnsServerCore.Dns public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false) { - return ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, null).WithTimeout(timeout); + 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) @@ -4915,8 +5186,14 @@ namespace DnsServerCore.Dns get { return _eDnsClientSubnetIpv4Override; } set { - if ((value is not null) && (value.AddressFamily != AddressFamily.InterNetwork)) - throw new ArgumentException("EDNS Client Subnet IPv4 Override must be an IPv4 network address.", nameof(EDnsClientSubnetIpv4Override)); + 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; } @@ -4927,8 +5204,14 @@ namespace DnsServerCore.Dns get { return _eDnsClientSubnetIpv6Override; } set { - if ((value is not null) && (value.AddressFamily != AddressFamily.InterNetworkV6)) - throw new ArgumentException("EDNS Client Subnet IPv6 Override must be an IPv6 network address.", nameof(EDnsClientSubnetIpv6Override)); + 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; } @@ -5124,6 +5407,12 @@ namespace DnsServerCore.Dns set { _enableDnsOverHttps = value; } } + public bool EnableDnsOverHttp3 + { + get { return _enableDnsOverHttp3; } + set { _enableDnsOverHttp3 = value; } + } + public bool EnableDnsOverQuic { get { return _enableDnsOverQuic; } @@ -5237,27 +5526,15 @@ namespace DnsServerCore.Dns } } - public IReadOnlyCollection RecursionDeniedNetworks + public IReadOnlyCollection RecursionNetworkACL { - get { return _recursionDeniedNetworks; } + get { return _recursionNetworkACL; } set { if ((value is not null) && (value.Count > byte.MaxValue)) - throw new ArgumentOutOfRangeException(nameof(RecursionDeniedNetworks), "Networks cannot be more than 255."); + throw new ArgumentOutOfRangeException(nameof(RecursionNetworkACL), "Network Access Control List cannot have more than 255 entries."); - _recursionDeniedNetworks = value; - } - } - - public IReadOnlyCollection RecursionAllowedNetworks - { - get { return _recursionAllowedNetworks; } - set - { - if ((value is not null) && (value.Count > byte.MaxValue)) - throw new ArgumentOutOfRangeException(nameof(RecursionAllowedNetworks), "Networks cannot be more than 255."); - - _recursionAllowedNetworks = value; + _recursionNetworkACL = value; } } @@ -5303,6 +5580,18 @@ namespace DnsServerCore.Dns } } + 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; } @@ -5458,6 +5747,12 @@ namespace DnsServerCore.Dns set { _forwarders = value; } } + public bool ConcurrentForwarding + { + get { return _concurrentForwarding; } + set { _concurrentForwarding = value; } + } + public int ForwarderRetries { get { return _forwarderRetries; }