diff --git a/DnsServer.sln b/DnsServer.sln index 7d3f1263..f2acce0e 100644 --- a/DnsServer.sln +++ b/DnsServer.sln @@ -3,7 +3,25 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DnsServerCore", "DnsServerCore\DnsServerCore.csproj", "{A561CF13-FE21-40A1-BF8D-BD242304187A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnitiumLibrary.Net", "..\TechnitiumLibrary\TechnitiumLibrary.Net\TechnitiumLibrary.Net.csproj", "{C8293A12-5A6A-4F53-BEBE-35A6D37BD891}" +EndProject Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A561CF13-FE21-40A1-BF8D-BD242304187A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A561CF13-FE21-40A1-BF8D-BD242304187A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A561CF13-FE21-40A1-BF8D-BD242304187A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A561CF13-FE21-40A1-BF8D-BD242304187A}.Release|Any CPU.Build.0 = Release|Any CPU + {C8293A12-5A6A-4F53-BEBE-35A6D37BD891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8293A12-5A6A-4F53-BEBE-35A6D37BD891}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8293A12-5A6A-4F53-BEBE-35A6D37BD891}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8293A12-5A6A-4F53-BEBE-35A6D37BD891}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection diff --git a/DnsServerCore/DnsServer.cs b/DnsServerCore/DnsServer.cs new file mode 100644 index 00000000..a346b2f3 --- /dev/null +++ b/DnsServerCore/DnsServer.cs @@ -0,0 +1,491 @@ +/* +Technitium Library +Copyright (C) 2017 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 System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using TechnitiumLibrary.IO; +using TechnitiumLibrary.Net; + +namespace DnsServerCore +{ + public class DnsServer : IDisposable + { + #region variables + + const int BUFFER_MAX_SIZE = 65535; + const int TCP_SOCKET_SEND_TIMEOUT = 30000; + const int TCP_SOCKET_RECV_TIMEOUT = 60000; + + Socket _udpListener; + Thread _udpListenerThread; + + Socket _tcpListener; + Thread _tcpListenerThread; + + Zone _authoritativeZoneRoot = new Zone(true); + Zone _cacheZoneRoot = new Zone(false); + + bool _allowRecursion; + NameServerAddress[] _forwarders; + bool _enableIPv6 = false; + + #endregion + + #region constructor + + public DnsServer() + : this(new IPEndPoint(IPAddress.IPv6Any, 53)) + { } + + public DnsServer(IPAddress localIP, int port = 53) + : this(new IPEndPoint(localIP, port)) + { } + + public DnsServer(IPEndPoint localEP) + { + _udpListener = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + _udpListener.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + _udpListener.Bind(localEP); + + _tcpListener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + _tcpListener.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + _tcpListener.Bind(localEP); + _tcpListener.Listen(10); + + //start reading query packets + _udpListenerThread = new Thread(ReadUdpQueryPacketsAsync); + _udpListenerThread.IsBackground = true; + _udpListenerThread.Start(_udpListener); + + _tcpListenerThread = new Thread(AcceptTcpConnectionAsync); + _tcpListenerThread.IsBackground = true; + _tcpListenerThread.Start(_tcpListener); + } + + #endregion + + #region IDisposable Support + + bool _disposed = false; + + ~DnsServer() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_udpListener != null) + _udpListener.Dispose(); + + if (_tcpListener != null) + _tcpListener.Dispose(); + } + + _disposed = true; + } + } + + #endregion + + #region private + + private void ReadUdpQueryPacketsAsync(object parameter) + { + Socket udpListener = parameter as Socket; + + EndPoint remoteEP; + FixMemoryStream recvBufferStream = new FixMemoryStream(BUFFER_MAX_SIZE); + FixMemoryStream sendBufferStream = new FixMemoryStream(BUFFER_MAX_SIZE); + int bytesRecv; + + if (udpListener.AddressFamily == AddressFamily.InterNetwork) + remoteEP = new IPEndPoint(IPAddress.Any, 0); + else + remoteEP = new IPEndPoint(IPAddress.IPv6Any, 0); + + while (true) + { + bytesRecv = udpListener.ReceiveFrom(recvBufferStream.Buffer, ref remoteEP); + + if (bytesRecv > 0) + { + recvBufferStream.Position = 0; + recvBufferStream.SetLength(bytesRecv); + + IPEndPoint remoteNodeEP = remoteEP as IPEndPoint; + + try + { + DnsDatagram response = ProcessQuery(recvBufferStream); + + //send response + if (response != null) + { + sendBufferStream.Position = 0; + response.WriteTo(sendBufferStream); + udpListener.SendTo(sendBufferStream.Buffer, 0, (int)sendBufferStream.Position, SocketFlags.None, remoteEP); + } + } + catch + { } + } + } + } + + private void AcceptTcpConnectionAsync(object parameter) + { + Socket tcpListener = parameter as Socket; + + while (true) + { + Socket socket = tcpListener.Accept(); + + socket.NoDelay = true; + socket.SendTimeout = TCP_SOCKET_SEND_TIMEOUT; + socket.ReceiveTimeout = TCP_SOCKET_RECV_TIMEOUT; + + ThreadPool.QueueUserWorkItem(ReadTcpQueryPacketsAsync, socket); + } + } + + private void ReadTcpQueryPacketsAsync(object parameter) + { + Socket tcpSocket = parameter as Socket; + + try + { + FixMemoryStream recvBufferStream = new FixMemoryStream(BUFFER_MAX_SIZE); + FixMemoryStream sendBufferStream = new FixMemoryStream(BUFFER_MAX_SIZE); + int bytesRecv; + + while (true) + { + //read dns datagram length + bytesRecv = tcpSocket.Receive(recvBufferStream.Buffer, 0, 2, SocketFlags.None); + if (bytesRecv < 1) + throw new SocketException(); + + Array.Reverse(recvBufferStream.Buffer, 0, 2); + short length = BitConverter.ToInt16(recvBufferStream.Buffer, 0); + + //read dns datagram + int offset = 0; + while (offset < length) + { + bytesRecv = tcpSocket.Receive(recvBufferStream.Buffer, offset, length, SocketFlags.None); + if (bytesRecv < 1) + throw new SocketException(); + + offset += bytesRecv; + } + + bytesRecv = length; + + if (bytesRecv > 0) + { + recvBufferStream.Position = 0; + recvBufferStream.SetLength(bytesRecv); + + DnsDatagram response = ProcessQuery(recvBufferStream); + + //send response + if (response != null) + { + //write dns datagram from 3rd position + sendBufferStream.Position = 2; + response.WriteTo(sendBufferStream); + + //write dns datagram length at beginning + byte[] lengthBytes = BitConverter.GetBytes(Convert.ToInt16(sendBufferStream.Position - 2)); + sendBufferStream.Buffer[0] = lengthBytes[1]; + sendBufferStream.Buffer[1] = lengthBytes[0]; + + //send dns datagram + tcpSocket.Send(sendBufferStream.Buffer, 0, (int)sendBufferStream.Position, SocketFlags.None); + } + } + } + } + catch + { } + finally + { + if (tcpSocket != null) + tcpSocket.Dispose(); + } + } + + private DnsDatagram ProcessQuery(Stream s) + { + DnsDatagram request; + + try + { + request = new DnsDatagram(s); + } + catch + { + return null; + } + + if (request.Header.IsResponse) + return null; + + switch (request.Header.OPCODE) + { + case DnsOpcode.StandardQuery: + try + { + DnsDatagram authoritativeResponse = Zone.Query(_authoritativeZoneRoot, request, _enableIPv6); + + if ((authoritativeResponse.Header.RCODE != DnsResponseCode.Refused) || !request.Header.RecursionDesired || !_allowRecursion) + return authoritativeResponse; + + return RecursiveQuery(request); + } + catch + { + return new DnsDatagram(new DnsHeader(request.Header.Identifier, true, DnsOpcode.StandardQuery, false, false, request.Header.RecursionDesired, _allowRecursion, false, false, DnsResponseCode.ServerFailure, request.Header.QDCOUNT, 0, 0, 0), request.Question, null, null, null); + } + + default: + return new DnsDatagram(new DnsHeader(request.Header.Identifier, true, request.Header.OPCODE, false, false, request.Header.RecursionDesired, _allowRecursion, false, false, DnsResponseCode.Refused, request.Header.QDCOUNT, 0, 0, 0), request.Question, null, null, null); + } + } + + public DnsDatagram RecursiveQuery(DnsDatagram request) + { + DnsDatagram originalRequest = request; + List responses = new List(1); + + while (true) + { + DnsDatagram response = Resolve(request); + responses.Add(response); + + if (response.Header.RCODE != DnsResponseCode.NoError) + break; + + if (response.Answer.Length == 0) + break; + + List newQuestions = new List(); + + foreach (DnsQuestionRecord question in request.Question) + { + for (int i = 0; i < response.Answer.Length; i++) + { + DnsResourceRecord answerRecord = response.Answer[i]; + + if ((answerRecord.Type == DnsResourceRecordType.CNAME) && question.Name.Equals(answerRecord.Name, StringComparison.CurrentCultureIgnoreCase)) + { + string cnameDomain = (answerRecord.RDATA as DnsCNAMERecord).CNAMEDomainName; + bool containsAnswer = false; + + for (int j = i + 1; j < response.Answer.Length; j++) + { + DnsResourceRecord answer = response.Answer[j]; + + if ((answer.Type == question.Type) && cnameDomain.Equals(answer.Name, StringComparison.CurrentCultureIgnoreCase)) + { + containsAnswer = true; + break; + } + } + + if (!containsAnswer) + newQuestions.Add(new DnsQuestionRecord((answerRecord.RDATA as DnsCNAMERecord).CNAMEDomainName, question.Type, question.Class)); + + break; + } + } + } + + if (newQuestions.Count == 0) + break; + + request = new DnsDatagram(new DnsHeader(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, Convert.ToUInt16(newQuestions.Count), 0, 0, 0), newQuestions.ToArray(), null, null, null); + } + + return MergeResponseAnswers(originalRequest, responses); + } + + private DnsDatagram Resolve(DnsDatagram request) + { + DnsDatagram cacheResponse = Zone.Query(_cacheZoneRoot, request, _enableIPv6); + + if (cacheResponse.Header.RCODE != DnsResponseCode.Refused) + return cacheResponse; + + List responses = new List(); + + foreach (DnsQuestionRecord questionRecord in request.Question) + { + NameServerAddress[] nameServers = NameServerAddress.GetNameServersFromResponse(cacheResponse, _enableIPv6); + + if (nameServers.Length == 0) + { + if (_enableIPv6) + nameServers = DnsClient.ROOT_NAME_SERVERS_IPv6; + else + nameServers = DnsClient.ROOT_NAME_SERVERS_IPv4; + } + + int hopCount = 0; + bool working = true; + + while (working && ((hopCount++) < 64)) + { + DnsClient client = new DnsClient(nameServers, _enableIPv6, false); + + DnsDatagram response = client.Resolve(questionRecord); + + Zone.CacheResponse(_cacheZoneRoot, response); + + switch (response.Header.RCODE) + { + case DnsResponseCode.NoError: + if ((response.Answer.Length > 0) || (response.Authority.Length == 0)) + { + responses.Add(response); + working = false; + } + else + { + nameServers = NameServerAddress.GetNameServersFromResponse(response, _enableIPv6); + + if (nameServers.Length == 0) + { + responses.Add(response); + working = false; + } + } + break; + + default: + responses.Add(response); + working = false; + break; + } + } + } + + return MergeResponseAnswers(request, responses); + } + + private DnsDatagram MergeResponseAnswers(DnsDatagram request, List responses) + { + switch (responses.Count) + { + case 0: + return null; + + case 1: + DnsDatagram responseReceived = responses[0]; + return new DnsDatagram(new DnsHeader(request.Header.Identifier, true, request.Header.OPCODE, false, false, true, true, false, false, responseReceived.Header.RCODE, request.Header.QDCOUNT, responseReceived.Header.ANCOUNT, responseReceived.Header.NSCOUNT, responseReceived.Header.ARCOUNT), request.Question, responseReceived.Answer, responseReceived.Authority, responseReceived.Additional); + + default: + List responseAnswer = new List(); + List responseAuthority = new List(); + List responseAdditional = new List(); + + foreach (DnsDatagram response in responses) + { + responseAnswer.AddRange(response.Answer); + + if (response.Authority != null) + responseAuthority.AddRange(response.Authority); + + if (response.Additional != null) + responseAuthority.AddRange(response.Additional); + } + + return new DnsDatagram(new DnsHeader(request.Header.Identifier, true, request.Header.OPCODE, false, false, true, true, false, false, responses[0].Header.RCODE, request.Header.QDCOUNT, Convert.ToUInt16(responseAnswer.Count), Convert.ToUInt16(responseAuthority.Count), Convert.ToUInt16(responseAdditional.Count)), request.Question, responseAnswer.ToArray(), responseAuthority.ToArray(), responseAdditional.ToArray()); + } + } + + #endregion + + #region properties + + public Zone AuthoritativeZoneRoot + { get { return _authoritativeZoneRoot; } } + + public Zone CacheZoneRoot + { get { return _cacheZoneRoot; } } + + public bool AllowRecursion + { + get { return _allowRecursion; } + set { _allowRecursion = value; } + } + + public NameServerAddress[] Forwarders + { + get { return _forwarders; } + set { _forwarders = value; } + } + + public bool EnableIPv6 + { + get { return _enableIPv6; } + set { _enableIPv6 = value; } + } + #endregion + } + + public class DnsServerException : Exception + { + #region constructors + + public DnsServerException() + : base() + { } + + public DnsServerException(string message) + : base(message) + { } + + public DnsServerException(string message, Exception innerException) + : base(message, innerException) + { } + + protected DnsServerException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { } + + #endregion + } + +} diff --git a/DnsServerCore/DnsServerCore.csproj b/DnsServerCore/DnsServerCore.csproj new file mode 100644 index 00000000..c152a008 --- /dev/null +++ b/DnsServerCore/DnsServerCore.csproj @@ -0,0 +1,61 @@ + + + + + Debug + AnyCPU + {A561CF13-FE21-40A1-BF8D-BD242304187A} + Library + Properties + DnsServerCore + DnsServerCore + v4.6.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + {e0ba5456-feaa-4380-92bb-6b1c4bc3dc70} + TechnitiumLibrary.IO + + + {c8293a12-5a6a-4f53-bebe-35a6d37bd891} + TechnitiumLibrary.Net + + + + + \ No newline at end of file diff --git a/DnsServerCore/Properties/AssemblyInfo.cs b/DnsServerCore/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..2783e0e1 --- /dev/null +++ b/DnsServerCore/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Technitium DNS Server")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Technitium")] +[assembly: AssemblyProduct("Technitium DNS Server")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a561cf13-fe21-40a1-bf8d-bd242304187a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DnsServerCore/Zone.cs b/DnsServerCore/Zone.cs new file mode 100644 index 00000000..d673e659 --- /dev/null +++ b/DnsServerCore/Zone.cs @@ -0,0 +1,654 @@ +/* +Technitium Library +Copyright (C) 2017 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 System; +using System.Collections.Generic; +using System.Threading; +using TechnitiumLibrary.Net; + +namespace DnsServerCore +{ + public class Zone + { + #region variables + + string _name; + bool _authoritativeZone; + + Dictionary _subZone = new Dictionary(); + ReaderWriterLockSlim _subZoneLock = new ReaderWriterLockSlim(); + + Dictionary> _zoneEntries = new Dictionary>(); + ReaderWriterLockSlim _zoneEntriesLock = new ReaderWriterLockSlim(); + + #endregion + + #region constructor + + public Zone(bool authoritativeZone) + { + _name = ""; + _authoritativeZone = authoritativeZone; + + if (!_authoritativeZone) + LoadRootHintsInCache(); + } + + private Zone(string name, bool authoritativeZone) + { + _name = name.ToLower(); + _authoritativeZone = authoritativeZone; + } + + #endregion + + #region private + + private void LoadRootHintsInCache() + { + //load root server records + DnsResourceRecordData[] nsRecords = new DnsResourceRecordData[DnsClient.ROOT_NAME_SERVERS_IPv4.Length]; + + for (int i = 0; i < DnsClient.ROOT_NAME_SERVERS_IPv4.Length; i++) + { + NameServerAddress rootServer = DnsClient.ROOT_NAME_SERVERS_IPv4[i]; + + nsRecords[i] = new DnsNSRecord(rootServer.Domain); + SetRecord(rootServer.Domain, DnsResourceRecordType.A, 172800, new DnsResourceRecordData[] { new DnsARecord(rootServer.EndPoint.Address) }); + } + + for (int i = 0; i < DnsClient.ROOT_NAME_SERVERS_IPv6.Length; i++) + { + NameServerAddress rootServer = DnsClient.ROOT_NAME_SERVERS_IPv6[i]; + + SetRecord(rootServer.Domain, DnsResourceRecordType.AAAA, 172800, new DnsResourceRecordData[] { new DnsAAAARecord(rootServer.EndPoint.Address) }); + } + + SetRecord("", DnsResourceRecordType.NS, 172800, nsRecords); + } + + private static string[] ConvertDomainToPath(string domainName) + { + if (domainName == null) + return new string[] { }; + + string[] path = domainName.ToLower().Split('.'); + Array.Reverse(path); + + return path; + } + + #endregion + + #region public static + + public static Zone CreateZone(Zone rootZone, string domain) + { + Zone currentZone = rootZone; + string[] path = ConvertDomainToPath(domain); + + for (int i = 0; i < path.Length; i++) + { + string nextZoneLabel = path[i]; + + ReaderWriterLockSlim currentSubZoneLock = currentZone._subZoneLock; + currentSubZoneLock.EnterWriteLock(); + try + { + if (currentZone._subZone.ContainsKey(nextZoneLabel)) + { + currentZone = currentZone._subZone[nextZoneLabel]; + } + else + { + string zoneName = nextZoneLabel; + + if (currentZone._name != "") + zoneName += "." + currentZone._name; + + Zone nextZone = new Zone(zoneName, currentZone._authoritativeZone); + currentZone._subZone.Add(nextZoneLabel, nextZone); + + currentZone = nextZone; + } + } + finally + { + currentSubZoneLock.ExitWriteLock(); + } + } + + return currentZone; + } + + public static Zone GetClosestZone(Zone rootZone, string domain) + { + Zone currentZone = rootZone; + string[] path = ConvertDomainToPath(domain); + + for (int i = 0; i < path.Length; i++) + { + string nextZoneLabel = path[i]; + + ReaderWriterLockSlim currentSubZoneLock = currentZone._subZoneLock; + currentSubZoneLock.EnterReadLock(); + try + { + if (currentZone._subZone.ContainsKey(nextZoneLabel)) + currentZone = currentZone._subZone[nextZoneLabel]; + else + return currentZone; + } + finally + { + currentSubZoneLock.ExitReadLock(); + } + } + + return currentZone; + } + + public static void DeleteZone(Zone rootZone, string domain) + { + Zone currentZone = rootZone; + string[] path = ConvertDomainToPath(domain); + + //find parent zone + for (int i = 0; i < path.Length - 1; i++) + { + string nextZoneLabel = path[i]; + + ReaderWriterLockSlim currentSubZoneLock = currentZone._subZoneLock; + currentSubZoneLock.EnterReadLock(); + try + { + if (currentZone._subZone.ContainsKey(nextZoneLabel)) + currentZone = currentZone._subZone[nextZoneLabel]; + else + return; + } + finally + { + currentSubZoneLock.ExitReadLock(); + } + } + + currentZone._subZoneLock.EnterWriteLock(); + try + { + currentZone._subZone.Remove(path[path.Length - 1]); + } + finally + { + currentZone._subZoneLock.ExitWriteLock(); + } + } + + private static DnsDatagram Query(Zone rootZone, string domain, DnsResourceRecordType type, bool enableIPv6) + { + Zone closestZone = GetClosestZone(rootZone, domain); + DnsResourceRecord[] soaAuthority = null; + + if (rootZone._authoritativeZone) + { + soaAuthority = closestZone.GetRecord(closestZone.Name, DnsResourceRecordType.SOA); + if (soaAuthority == null) + return null; //authoritative zone not found + } + + DnsResourceRecord[] answer = closestZone.GetRecord(domain, type); + DnsResourceRecord[] authority = null; + DnsResourceRecord[] additional = null; + + if (answer == null) + { + if (rootZone._authoritativeZone) + { + //domain name doesnt exists in authoritative zone + authority = soaAuthority; + } + else + { + //domain name doesnt exists in cache; return closest available authority NS records + string closestZoneName = closestZone.Name; + + while (true) + { + authority = closestZone.GetRecord(closestZoneName, DnsResourceRecordType.NS); + + if ((authority != null) && (authority[0].Type == DnsResourceRecordType.NS)) + break; + + int i = closestZoneName.IndexOf('.'); + if (i < 0) + closestZoneName = ""; + else + closestZoneName = closestZoneName.Substring(i + 1); + + closestZone = GetClosestZone(rootZone, closestZoneName); + } + } + } + else if (answer.Length == 0) + { + //no records available for requested type + authority = closestZone.GetRecord(domain, DnsResourceRecordType.NS); + + if (authority.Length == 0) + authority = soaAuthority; + } + else if (answer[0] == null) + { + //NameError entry found in cache + authority = closestZone.GetRecord(closestZone.Name, DnsResourceRecordType.SOA); + } + else if ((type != DnsResourceRecordType.NS) && (type != DnsResourceRecordType.ANY) && ((type == DnsResourceRecordType.CNAME) || (answer[0].Type != DnsResourceRecordType.CNAME))) + { + authority = closestZone.GetRecord(closestZone.Name, DnsResourceRecordType.NS); + } + + //fill in glue records for NS records in authority + if ((authority != null) && (authority[0].Type != DnsResourceRecordType.SOA)) + { + List additionalList = new List(); + Zone closestNSZone = null; + + foreach (DnsResourceRecord record in authority) + { + DnsNSRecord nsRecord = record.RDATA as DnsNSRecord; + + if ((closestNSZone == null) || !nsRecord.NSDomainName.EndsWith(closestNSZone._name)) + closestNSZone = GetClosestZone(rootZone, nsRecord.NSDomainName); + + DnsResourceRecord[] nsAnswersA = closestNSZone.GetRecord(nsRecord.NSDomainName, DnsResourceRecordType.A); + if (nsAnswersA != null) + additionalList.AddRange(nsAnswersA); + + if (enableIPv6) + { + DnsResourceRecord[] nsAnswersAAAA = closestNSZone.GetRecord(nsRecord.NSDomainName, DnsResourceRecordType.AAAA); + if (nsAnswersAAAA != null) + additionalList.AddRange(nsAnswersAAAA); + } + } + + additional = additionalList.ToArray(); + } + + return new DnsDatagram(null, null, answer, authority, additional); + } + + public static DnsDatagram Query(Zone rootZone, DnsDatagram request, bool enableIPv6) + { + bool authoritativeAnswer = false; + DnsResponseCode RCODE = DnsResponseCode.Refused; + List answerList = new List(); + List authorityList = new List(); + List additionalList = new List(); + + foreach (DnsQuestionRecord question in request.Question) + { + DnsDatagram response = Zone.Query(rootZone, question.Name, question.Type, enableIPv6); + + if (response != null) + { + #region zone found + + authoritativeAnswer = rootZone._authoritativeZone; + + if (response.Answer == null) + { + if (authoritativeAnswer) + RCODE = DnsResponseCode.NameError; //domain does not exists in authoritative zone + else + RCODE = DnsResponseCode.Refused; //domain does not exists in cache + } + else + { + #region domain exists + + RCODE = DnsResponseCode.NoError; + + if (response.Answer.Length > 0) + { + if (!authoritativeAnswer && (response.Answer[0] == null)) + { + //name error set in cache + RCODE = DnsResponseCode.NameError; + } + else + { + answerList.AddRange(response.Answer); + + if ((response.Answer[0].Type == DnsResourceRecordType.CNAME) && (question.Type != DnsResourceRecordType.CNAME)) + { + //resolve CNAME domain name + DnsCNAMERecord cnameRecord = response.Answer[0].RDATA as DnsCNAMERecord; + + DnsDatagram cnameResponse = Zone.Query(rootZone, cnameRecord.CNAMEDomainName, question.Type, enableIPv6); + if ((cnameResponse != null) && (cnameResponse.Answer != null)) + { + answerList.AddRange(cnameResponse.Answer); + + if (cnameResponse.Authority != null) + authorityList.AddRange(cnameResponse.Authority); + } + } + } + } + + #endregion + } + + if ((response.Authority != null) && (response.Authority.Length > 0)) + authorityList.AddRange(response.Authority); + + if ((response.Additional != null) && (response.Additional.Length > 0)) + additionalList.AddRange(response.Additional); + + #endregion + } + } + + return new DnsDatagram(new DnsHeader(request.Header.Identifier, true, DnsOpcode.StandardQuery, authoritativeAnswer, false, request.Header.RecursionDesired, !rootZone._authoritativeZone, false, false, RCODE, Convert.ToUInt16(request.Question.Length), Convert.ToUInt16(answerList.Count), Convert.ToUInt16(authorityList.Count), Convert.ToUInt16(additionalList.Count)), request.Question, answerList.ToArray(), authorityList.ToArray(), additionalList.ToArray()); + } + + public static void CacheResponse(Zone rootZone, DnsDatagram response) + { + if (rootZone._authoritativeZone) + throw new DnsServerException("Cannot cache response into authoritative zone."); + + if (!response.Header.IsResponse) + return; + + //combine all records in the response + List allRecords = new List(); + + switch (response.Header.RCODE) + { + case DnsResponseCode.NameError: + string authorityZone = null; + uint ttl = 60; + + if ((response.Authority.Length > 0) && (response.Authority[0].Type == DnsResourceRecordType.SOA)) + { + authorityZone = response.Authority[0].Name; + ttl = (response.Authority[0].RDATA as DnsSOARecord).Minimum; + } + + foreach (DnsQuestionRecord question in response.Question) + { + if (authorityZone == null) + authorityZone = question.Name; + + Zone zone = CreateZone(rootZone, authorityZone); + zone.SetRecord(new DnsResourceRecord[] { new DnsResourceRecord(question.Name, question.Type, DnsClass.Internet, ttl, null) }); + } + break; + + case DnsResponseCode.NoError: + allRecords.AddRange(response.Answer); + break; + + default: + return; //nothing to do + } + + allRecords.AddRange(response.Authority); + allRecords.AddRange(response.Additional); + + //group all records by domain and type + Dictionary>> cacheEntries = new Dictionary>>(); + + foreach (DnsResourceRecord record in allRecords) + { + Dictionary> cacheTypeEntries; + + if (cacheEntries.ContainsKey(record.Name)) + { + cacheTypeEntries = cacheEntries[record.Name]; + } + else + { + cacheTypeEntries = new Dictionary>(); + cacheEntries.Add(record.Name, cacheTypeEntries); + } + + List cacheRREntries; + + if (cacheTypeEntries.ContainsKey(record.Type)) + { + cacheRREntries = cacheTypeEntries[record.Type]; + } + else + { + cacheRREntries = new List(); + cacheTypeEntries.Add(record.Type, cacheRREntries); + } + + cacheRREntries.Add(record); + } + + //add grouped entries into cache zone + foreach (KeyValuePair>> cacheEntry in cacheEntries) + { + string domain = cacheEntry.Key; + Zone zone = CreateZone(rootZone, domain); + + foreach (KeyValuePair> cacheTypeEntry in cacheEntry.Value) + { + DnsResourceRecord[] records = cacheTypeEntry.Value.ToArray(); + + foreach (DnsResourceRecord record in records) + record.SetExpiry(); + + zone.SetRecord(records); + } + } + } + + #endregion + + #region public + + public void SetRecord(string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData[] records) + { + DnsResourceRecord[] resourceRecords = new DnsResourceRecord[records.Length]; + + for (int i = 0; i < resourceRecords.Length; i++) + resourceRecords[i] = new DnsResourceRecord(domain, type, DnsClass.Internet, ttl, records[i]); + + SetRecord(resourceRecords); + } + + public void SetRecord(DnsResourceRecord[] resourceRecords) + { + if (resourceRecords.Length < 1) + return; + + string domain = resourceRecords[0].Name; + DnsResourceRecordType type = resourceRecords[0].Type; + + _zoneEntriesLock.EnterWriteLock(); + try + { + Dictionary zoneTypeEntries; + + if (_zoneEntries.ContainsKey(domain)) + { + zoneTypeEntries = _zoneEntries[domain]; + } + else + { + zoneTypeEntries = new Dictionary(); + _zoneEntries.Add(domain, zoneTypeEntries); + } + + if (zoneTypeEntries.ContainsKey(type)) + zoneTypeEntries[type] = new ZoneEntry(resourceRecords); + else + zoneTypeEntries.Add(type, new ZoneEntry(resourceRecords)); + } + finally + { + _zoneEntriesLock.ExitWriteLock(); + } + } + + public DnsResourceRecord[] GetRecord(string domain, DnsResourceRecordType type) + { + _zoneEntriesLock.EnterReadLock(); + try + { + Dictionary zoneTypeEntries; + + if (_zoneEntries.ContainsKey(domain)) + zoneTypeEntries = _zoneEntries[domain]; + else + return null; + + if (zoneTypeEntries.ContainsKey(DnsResourceRecordType.CNAME)) + { + ZoneEntry zoneEntry = zoneTypeEntries[DnsResourceRecordType.CNAME]; + + if (!_authoritativeZone && zoneEntry.IsExpired()) + return null; //domain does not exists in cache since expired + else + return zoneEntry.ResourceRecords; //return CNAME record + } + else if (_authoritativeZone && (type == DnsResourceRecordType.ANY)) + { + List records = new List(5); + + foreach (KeyValuePair entry in zoneTypeEntries) + { + records.AddRange(entry.Value.ResourceRecords); + } + + return records.ToArray(); //all authoritative records + } + else if (zoneTypeEntries.ContainsKey(type)) + { + ZoneEntry zoneEntry = zoneTypeEntries[type]; + + if (_authoritativeZone || (_name == "")) + { + return zoneEntry.ResourceRecords; //records found in authoritative zone or root hints from cache + } + else + { + if (zoneEntry.IsExpired()) + return null; //domain does not exists in cache since expired + else if (zoneEntry.IsNameErrorEntry()) + return new DnsResourceRecord[] { null }; //name error set in cache + else + return zoneEntry.ResourceRecords; //records found in cache + } + } + else + { + if (_authoritativeZone) + return new DnsResourceRecord[] { }; //no records in authoritative zone + else + return null; //domain does not exists in cache + } + } + finally + { + _zoneEntriesLock.ExitReadLock(); + } + } + + public void DeleteRecord(string domain, DnsResourceRecordType type) + { + _zoneEntriesLock.EnterWriteLock(); + try + { + Dictionary zoneTypeEntries; + + if (_zoneEntries.ContainsKey(domain)) + { + zoneTypeEntries = _zoneEntries[domain]; + + zoneTypeEntries.Remove(type); + + if (zoneTypeEntries.Count < 1) + _zoneEntries.Remove(domain); + } + } + finally + { + _zoneEntriesLock.ExitWriteLock(); + } + } + + public override string ToString() + { + return _name; + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + #endregion + } + + public class ZoneEntry + { + #region variables + + DnsResourceRecord[] _resourceRecords; + + #endregion + + #region constructor + + public ZoneEntry(DnsResourceRecord[] resourceRecords) + { + _resourceRecords = resourceRecords; + } + + #endregion + + #region public + + public bool IsExpired() + { + return (_resourceRecords[0].TTLValue < 1); + } + + public bool IsNameErrorEntry() + { + return (_resourceRecords[0].RDATA == null); + } + + #endregion + + #region properties + + public DnsResourceRecord[] ResourceRecords + { get { return _resourceRecords; } } + + #endregion + } +}