EonaCat.DnsTester/EonaCat.DnsTester/Helpers/DnsHelper.cs

401 lines
14 KiB
C#

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<string> OnLog;
public static async Task<DnsResponse> SendDnsQueryPacket(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.ReceiveTimeout = DnsReceiveTimeout;
await client.SendAsync(queryBytes, queryBytes.Length, endPoint);
var responseResult = await client.ReceiveAsync();
DnsResponse response = ParseDnsResponsePacket(dnsId, startTime, server, responseResult.Buffer);
return response;
}
}
public static int DnsReceiveTimeout { get; set; } = 5;
public static byte[] CreateDnsQueryPacket(string domainName, DnsRecordType recordType)
{
Random random = new Random();
// DNS header
ushort id = (ushort)random.Next(0, 65536);
ushort flags = (ushort)0x0100; // recursion desired
ushort qdcount = 1;
ushort ancount = 0;
ushort nscount = 0;
ushort arcount = 0;
byte[] 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
string[] labels = domainName.Split('.');
byte[] qnameBytes = new byte[domainName.Length + 2];
int qnameIndex = 0;
foreach (string label in labels)
{
qnameBytes[qnameIndex++] = (byte)label.Length;
foreach (char c in label)
{
qnameBytes[qnameIndex++] = (byte)c;
}
}
qnameBytes[qnameIndex++] = 0;
byte[] qtypeBytes = new byte[] { (byte)((ushort)recordType >> 8), (byte)((ushort)recordType & 0xff) };
byte[] qclassBytes = new byte[] { 0, 1 }; // internet class
byte[] 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
byte[] 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
string name = ParseDnsName(queryBytes, ref offset);
if (name == null)
{
return null;
}
// Parse the DNS type and class
ushort type = (ushort)((queryBytes[offset] << 8) | queryBytes[offset + 1]);
ushort qclass = (ushort)((queryBytes[offset + 2] << 8) | queryBytes[offset + 3]);
offset += 4;
return new DnsQuestion
{
Name = name,
Type = (DnsRecordType)type,
Class = (DnsRecordClass)qclass,
};
}
static DnsResponse ParseDnsResponsePacket(string dnsId, long startTime, string server, byte[] responseBytes)
{
// Parse the DNS header
ushort id = (ushort)((responseBytes[0] << 8) | responseBytes[1]);
ushort flags = (ushort)((responseBytes[2] << 8) | responseBytes[3]);
ushort qdcount = (ushort)((responseBytes[4] << 8) | responseBytes[5]);
ushort ancount = (ushort)((responseBytes[6] << 8) | responseBytes[7]);
ushort nscount = (ushort)((responseBytes[8] << 8) | responseBytes[9]);
ushort arcount = (ushort)((responseBytes[10] << 8) | responseBytes[11]);
List<DnsQuestion> questions = new List<DnsQuestion>();
//for (int i = 0; i < qdcount; i++)
//{
// DnsQuestion question = ParseDnsQuestionRecord(responseBytes, ref offset);
// if (question != null)
// {
// questions.Add(question);
// }
//}
int offset = 12;
// Parse the DNS answer records
List<ResourceRecord> answers = new List<ResourceRecord>();
for (int i = 0; i < ancount; i++)
{
try
{
ResourceRecord 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
List<ResourceRecord> authorities = new List<ResourceRecord>();
for (int i = 0; i < nscount; i++)
{
try
{
ResourceRecord 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
List<ResourceRecord> additionals = new List<ResourceRecord>();
for (int i = 0; i < arcount; i++)
{
try
{
ResourceRecord 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
string name = ParseDnsName(responseBytes, ref offset);
if (name == null)
{
return null;
}
// Parse the DNS type, class, ttl, and data length
DnsRecordType type = (DnsRecordType)((responseBytes[offset++] << 8) | responseBytes[offset++]);
DnsRecordClass klass = (DnsRecordClass)((responseBytes[offset++] << 8) | responseBytes[offset++]);
uint ttl = (uint)((responseBytes[offset++] << 24) | (responseBytes[offset++] << 16) | (responseBytes[offset++] << 8) | responseBytes[offset++]);
ushort rdlen = (ushort)((responseBytes[responseBytes.Length - 5]));
offset += 10;
// Parse the DNS data
string data = null;
switch ((DnsRecordType)type)
{
case DnsRecordType.A:
if (rdlen != 4)
{
return null;
}
data = new IPAddress(new byte[]
{
responseBytes[offset], responseBytes[offset + 1], responseBytes[offset + 2],
responseBytes[offset + 3]
}).ToString();
offset += rdlen;
break;
case DnsRecordType.CNAME:
case DnsRecordType.NS:
data = ParseDnsName(responseBytes, ref offset);
if (data == null)
{
return null;
}
break;
case DnsRecordType.MX:
ushort preference = (ushort)((responseBytes[offset] << 8) | responseBytes[offset + 1]);
offset += 2;
string exchange = ParseDnsName(responseBytes, ref offset);
if (exchange == null)
{
return null;
}
data = $"{preference} {exchange}";
break;
case DnsRecordType.TXT:
data = Encoding.ASCII.GetString(responseBytes, offset, rdlen);
offset += rdlen;
break;
default:
offset += rdlen;
break;
}
return new ResourceRecord
{
Name = name,
Type = type,
Class = klass,
Ttl = TimeSpan.FromSeconds(ttl),
Data = data,
};
}
static string ParseDnsName(byte[] responseBytes, ref int offset)
{
StringBuilder name = new StringBuilder();
int originalOffset = offset;
while (true)
{
byte len = responseBytes[offset++];
if (len == 0)
{
break;
}
if ((len & 0xc0) == 0xc0)
{
ushort pointer = (ushort)(((len & 0x3f) << 8) | responseBytes[offset++]);
int pointerOffset = pointer & 0x3fff;
if (pointerOffset >= responseBytes.Length)
{
return null;
}
int pointerEnd = offset;
offset = pointerOffset;
if (offset < originalOffset)
{
return null;
}
name.Append(ParseDnsName(responseBytes, ref offset));
offset = pointerEnd;
break;
}
if (len > 63)
{
return null;
}
name.Append(Encoding.ASCII.GetString(responseBytes, offset, len));
offset += len;
if (responseBytes[offset - 1] != 0)
{
name.Append(".");
}
}
if (originalOffset == offset)
{
return null;
}
return name.ToString();
}
}
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<ResourceRecord> Answers { get; set; }
public long StartTime { get; set; }
public string Resolver { get; set; }
public string DnsId { get; set; }
public List<DnsQuestion> Questions { get; set; }
public List<ResourceRecord> Authorities { get; set; }
public List<ResourceRecord> Additionals { get; set; }
}
public enum DnsRecordType : ushort
{
A = 1,
NS = 2,
CNAME = 5,
MX = 15,
TXT = 16,
AAAA = 28,
}
public enum DnsRecordClass : ushort
{
/// <summary>
/// The Internet.
/// </summary>
Internet = 1,
/// <summary>
/// The CSNET class (Obsolete - used only for examples insome obsolete RFCs).
/// </summary>
CS = 2,
/// <summary>
/// The CHAOS class.
/// </summary>
CH = 3,
/// <summary>
/// Hesiod[Dyer 87].
/// </summary>
HS = 4,
/// <summary>
/// Used in UPDATE message to signify no class.
/// </summary>
None = 254,
/// <summary>
/// Only used in QCLASS.
/// </summary>
/// <seealso cref="Question.Class"/>
ANY = 255
}
}