using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace EonaCat.DnsTester.Helpers { class DnsHelper { public static event EventHandler OnLog; public static async Task SendDnsQueryPacketAsync(string dnsId, string server, int port, byte[] queryBytes) { // Start the clock var startTime = DateTime.Now.Ticks; var endPoint = new IPEndPoint(IPAddress.Parse(server), port); using (var client = new UdpClient(endPoint.AddressFamily)) { client.DontFragment = true; client.EnableBroadcast = false; client.Client.SendTimeout = DnsSendTimeout; client.Client.ReceiveTimeout = DnsReceiveTimeout; byte[] responseBytes = null; if (FakeResponse) { responseBytes = DnsHelper.GetExampleResponse(); } else { await client.SendAsync(queryBytes, queryBytes.Length, endPoint).ConfigureAwait(false); var responseResult = await client.ReceiveAsync().ConfigureAwait(false); responseBytes = responseResult.Buffer; } var response = ParseDnsResponsePacket(dnsId, startTime, server, responseBytes); return response; } } // For testing purposes public static bool FakeResponse { get; set; } public static int DnsSendTimeout { get; set; } = 5; public static int DnsReceiveTimeout { get; set; } = 5; public static byte[] CreateDnsQueryPacket(string domainName, DnsRecordType recordType) { var random = new Random(); // DNS header var id = (ushort)random.Next(0, 65536); var flags = (ushort)0x0100; // recursion desired ushort qdcount = 1; ushort ancount = 0; ushort nscount = 0; ushort arcount = 0; var headerBytes = new byte[] { (byte)(id >> 8), (byte)(id & 0xff), (byte)(flags >> 8), (byte)(flags & 0xff), (byte)(qdcount >> 8), (byte)(qdcount & 0xff), (byte)(ancount >> 8), (byte)(ancount & 0xff), (byte)(nscount >> 8), (byte)(nscount & 0xff), (byte)(arcount >> 8), (byte)(arcount & 0xff), }; // DNS question var labels = domainName.Split('.'); var qnameBytes = new byte[domainName.Length + 2]; var qnameIndex = 0; foreach (var label in labels) { qnameBytes[qnameIndex++] = (byte)label.Length; foreach (var c in label) { qnameBytes[qnameIndex++] = (byte)c; } } qnameBytes[qnameIndex++] = 0; var qtypeBytes = new byte[] { (byte)((ushort)recordType >> 8), (byte)((ushort)recordType & 0xff) }; var qclassBytes = new byte[] { 0, 1 }; // internet class var questionBytes = new byte[qnameBytes.Length + qtypeBytes.Length + qclassBytes.Length]; qnameBytes.CopyTo(questionBytes, 0); qtypeBytes.CopyTo(questionBytes, qnameBytes.Length); qclassBytes.CopyTo(questionBytes, qnameBytes.Length + qtypeBytes.Length); // Combine the header and question to form the DNS query packet var queryBytes = new byte[headerBytes.Length + questionBytes.Length]; headerBytes.CopyTo(queryBytes, 0); questionBytes.CopyTo(queryBytes, headerBytes.Length); return queryBytes; } static DnsQuestion ParseDnsQuestionRecord(byte[] queryBytes, ref int offset) { // Parse the DNS name var name = DnsNameParser.ParseName(queryBytes, ref offset); if (name == null) { return null; } // Parse the DNS type and class var type = (ushort)((queryBytes[offset] << 8) | queryBytes[offset + 1]); var qclass = (ushort)((queryBytes[offset + 2] << 8) | queryBytes[offset + 3]); offset += 4; return new DnsQuestion { Name = name, Type = (DnsRecordType)type, Class = (DnsRecordClass)qclass, }; } public static byte[] GetExampleResponse() { // Example response bytes for the A record of google.com byte[] response = { 0x9d, 0xa9, // Query ID 0x81, 0x80, // Flags 0x00, 0x01, // Questions: 1 0x00, 0x01, // Answer RRs: 1 0x00, 0x00, // Authority RRs: 0 0x00, 0x00, // Additional RRs: 0 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, // Query: google 0x03, 0x63, 0x6f, 0x6d, // Query: com 0x00, 0x00, 0x01, // Record type: A 0x00, 0x01, // Record class: IN 0xc0, 0x0c, // Name pointer to google.com 0x00, 0x01, // Record type: A 0x00, 0x01, // Record class: IN 0x00, 0x00, 0x00, 0x3d, // TTL: 61 seconds 0x00, 0x04, // Data length: 4 bytes 0xac, 0xd9, 0x03, 0x3d // Data: 172.217.3.61 }; return response; } static DnsResponse ParseDnsResponsePacket(string dnsId, long startTime, string server, byte[] responseBytes) { Console.WriteLine("new byte[] { " + responseBytes + " }"); // Check if response is valid if (responseBytes.Length < 12) { throw new Exception("Invalid DNS response"); } // Set the offset to the start var offset = 0; // Parse the DNS header var id = (ushort)((responseBytes[0] << 8) | responseBytes[1]); var flags = (ushort)((responseBytes[2] << 8) | responseBytes[3]); var isResponse = (flags & 0x8000) != 0; var qdcount = (ushort)((responseBytes[4] << 8) | responseBytes[5]); var ancount = (ushort)((responseBytes[6] << 8) | responseBytes[7]); if (!isResponse) { throw new Exception("Invalid DNS response"); } var nscount = (ushort)((responseBytes[8] << 8) | responseBytes[9]); var arcount = (ushort)((responseBytes[10] << 8) | responseBytes[11]); // We parsed the header set the offset past the header offset = 12; var questions = new List(); for (var i = 0; i < qdcount; i++) { var question = ParseDnsQuestionRecord(responseBytes, ref offset); if (question != null) { questions.Add(question); } } // Parse the DNS answer records var answers = new List(); for (var i = 0; i < ancount; i++) { try { var answer = ParseDnsAnswerRecord(responseBytes, ref offset); if (answer != null) { answers.Add(answer); } } catch (Exception exception) { OnLog?.Invoke(null, $"Answer exception: " + exception.Message); } } // Parse the DNS authority records var authorities = new List(); for (var i = 0; i < nscount; i++) { try { var authority = ParseDnsAnswerRecord(responseBytes, ref offset); if (authority != null) { authorities.Add(authority); } } catch (Exception exception) { OnLog?.Invoke(null, $"Authority answer exception: " + exception.Message); } } // Parse the DNS additional records var additionals = new List(); for (var i = 0; i < arcount; i++) { try { var additional = ParseDnsAnswerRecord(responseBytes, ref offset); if (additional != null) { additionals.Add(additional); } } catch (Exception exception) { OnLog?.Invoke(null, $"Additional answer exception: " + exception.Message); } } return new DnsResponse { Id = id, StartTime = startTime, Resolver = server, Flags = flags, Class = (DnsRecordClass)((flags >> 3) & 0x0f), DnsId = dnsId, Questions = questions, Answers = answers, Authorities = authorities, Additionals = additionals, }; } static ResourceRecord ParseDnsAnswerRecord(byte[] responseBytes, ref int offset) { // Parse the DNS name var name = DnsNameParser.ExtractDomainName(responseBytes, ref offset); if (name == null) { return null; } // Parse the DNS type, class, ttl, and data length var type = (DnsRecordType)((responseBytes[offset++] << 8) + responseBytes[offset++]); var klass = (DnsRecordClass)((responseBytes[offset++] << 8) + responseBytes[offset++]); var ttl = (responseBytes[offset++] << 24) + (responseBytes[offset++] << 16) + (responseBytes[offset++] << 8) + responseBytes[offset++]; // Extract record data length var dataLength = (responseBytes[offset] << 8) + responseBytes[offset + 1]; offset += 2; // Extract record data var recordData = new byte[dataLength]; Buffer.BlockCopy(responseBytes, offset, recordData, 0, dataLength); string recordDataAsString = null; switch ((DnsRecordType)type) { case DnsRecordType.A: if (dataLength != 4) { return null; } recordDataAsString = new IPAddress(recordData).ToString(); offset += recordData.Length; break; case DnsRecordType.CNAME: recordDataAsString = DnsNameParser.ExtractDomainName(responseBytes, ref offset); break; case DnsRecordType.NS: recordDataAsString = DnsNameParser.ExtractDomainName(responseBytes, ref offset); break; case DnsRecordType.MX: var preference = (responseBytes[0] << 8) + responseBytes[1]; offset += 2; var exchange = DnsNameParser.ExtractDomainName(responseBytes, ref offset); recordDataAsString = $"{preference} {exchange}"; break; case DnsRecordType.TXT: recordDataAsString = Encoding.ASCII.GetString(recordData); break; default: offset += dataLength; break; } return new ResourceRecord { Name = name, Type = type, Class = klass, Ttl = TimeSpan.FromSeconds(ttl), Data = recordDataAsString, }; } static string GetString(byte[] bytes, ref int index, int length) { var str = ""; for (var i = 0; i < length; i++) { str += (char)bytes[index++]; } return str; } } public class DnsQuestion { public string Name { get; set; } public DnsRecordType Type { get; set; } public DnsRecordClass Class { get; set; } } public class ResourceRecord { public string Name { get; set; } public DnsRecordType Type { get; set; } public string Data { get; set; } public DnsRecordClass Class { get; set; } public TimeSpan Ttl { get; set; } public ushort DataLength { get; set; } } public class DnsResponse { public ushort Id { get; set; } public ushort Flags { get; set; } public DnsRecordClass Class { get; set; } public List Answers { get; set; } public long StartTime { get; set; } public string Resolver { get; set; } public string DnsId { get; set; } public List Questions { get; set; } public List Authorities { get; set; } public List Additionals { get; set; } } public enum DnsRecordType : ushort { A = 1, NS = 2, CNAME = 6, MX = 15, TXT = 16, AAAA = 28, } public enum DnsRecordClass : ushort { /// /// The Internet. /// Internet = 1, /// /// The CSNET class (Obsolete - used only for examples insome obsolete RFCs). /// CS = 2, /// /// The CHAOS class. /// CH = 3, /// /// Hesiod[Dyer 87]. /// HS = 4, /// /// Used in UPDATE message to signify no class. /// None = 254, /// /// Only used in QCLASS. /// /// ANY = 255 } }