This commit is contained in:
2019-12-04 18:57:18 +01:00
parent 4692422c9a
commit 6263791dff
225 changed files with 33065 additions and 2 deletions
+62
View File
@@ -0,0 +1,62 @@
namespace Swan.Net.Dns
{
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
/// <summary>
/// DnsClient public interfaces.
/// </summary>
internal partial class DnsClient
{
public interface IDnsMessage
{
IList<DnsQuestion> Questions { get; }
int Size { get; }
byte[] ToArray();
}
public interface IDnsMessageEntry
{
DnsDomain Name { get; }
DnsRecordType Type { get; }
DnsRecordClass Class { get; }
int Size { get; }
byte[] ToArray();
}
public interface IDnsResourceRecord : IDnsMessageEntry
{
TimeSpan TimeToLive { get; }
int DataLength { get; }
byte[] Data { get; }
}
public interface IDnsRequest : IDnsMessage
{
int Id { get; set; }
DnsOperationCode OperationCode { get; set; }
bool RecursionDesired { get; set; }
}
public interface IDnsResponse : IDnsMessage
{
int Id { get; set; }
IList<IDnsResourceRecord> AnswerRecords { get; }
IList<IDnsResourceRecord> AuthorityRecords { get; }
IList<IDnsResourceRecord> AdditionalRecords { get; }
bool IsRecursionAvailable { get; set; }
bool IsAuthorativeServer { get; set; }
bool IsTruncated { get; set; }
DnsOperationCode OperationCode { get; set; }
DnsResponseCode ResponseCode { get; set; }
}
public interface IDnsRequestResolver
{
Task<DnsClientResponse> Request(DnsClientRequest request);
}
}
}
+681
View File
@@ -0,0 +1,681 @@
namespace Swan.Net.Dns
{
using Formatters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
/// <summary>
/// DnsClient Request inner class.
/// </summary>
internal partial class DnsClient
{
public class DnsClientRequest : IDnsRequest
{
private readonly IDnsRequestResolver _resolver;
private readonly IDnsRequest _request;
public DnsClientRequest(IPEndPoint dns, IDnsRequest? request = null, IDnsRequestResolver? resolver = null)
{
Dns = dns;
_request = request == null ? new DnsRequest() : new DnsRequest(request);
_resolver = resolver ?? new DnsUdpRequestResolver();
}
public int Id
{
get => _request.Id;
set => _request.Id = value;
}
public DnsOperationCode OperationCode
{
get => _request.OperationCode;
set => _request.OperationCode = value;
}
public bool RecursionDesired
{
get => _request.RecursionDesired;
set => _request.RecursionDesired = value;
}
public IList<DnsQuestion> Questions => _request.Questions;
public int Size => _request.Size;
public IPEndPoint Dns { get; set; }
public byte[] ToArray() => _request.ToArray();
public override string ToString() => _request.ToString();
/// <summary>
/// Resolves this request into a response using the provided DNS information. The given
/// request strategy is used to retrieve the response.
/// </summary>
/// <exception cref="DnsQueryException">Throw if a malformed response is received from the server.</exception>
/// <exception cref="IOException">Thrown if a IO error occurs.</exception>
/// <exception cref="SocketException">Thrown if a the reading or writing to the socket fails.</exception>
/// <returns>The response received from server.</returns>
public async Task<DnsClientResponse> Resolve()
{
try
{
var response = await _resolver.Request(this).ConfigureAwait(false);
if (response.Id != Id)
{
throw new DnsQueryException(response, "Mismatching request/response IDs");
}
if (response.ResponseCode != DnsResponseCode.NoError)
{
throw new DnsQueryException(response);
}
return response;
}
catch (Exception e)
{
if (e is ArgumentException || e is SocketException)
throw new DnsQueryException("Invalid response", e);
throw;
}
}
}
public class DnsRequest : IDnsRequest
{
private static readonly Random Random = new Random();
private DnsHeader header;
public DnsRequest()
{
Questions = new List<DnsQuestion>();
header = new DnsHeader
{
OperationCode = DnsOperationCode.Query,
Response = false,
Id = Random.Next(ushort.MaxValue),
};
}
public DnsRequest(IDnsRequest request)
{
header = new DnsHeader();
Questions = new List<DnsQuestion>(request.Questions);
header.Response = false;
Id = request.Id;
OperationCode = request.OperationCode;
RecursionDesired = request.RecursionDesired;
}
public IList<DnsQuestion> Questions { get; }
public int Size => header.Size + Questions.Sum(q => q.Size);
public int Id
{
get => header.Id;
set => header.Id = value;
}
public DnsOperationCode OperationCode
{
get => header.OperationCode;
set => header.OperationCode = value;
}
public bool RecursionDesired
{
get => header.RecursionDesired;
set => header.RecursionDesired = value;
}
public byte[] ToArray()
{
UpdateHeader();
using var result = new MemoryStream(Size);
return result
.Append(header.ToArray())
.Append(Questions.Select(q => q.ToArray()))
.ToArray();
}
public override string ToString()
{
UpdateHeader();
return Json.Serialize(this, true);
}
private void UpdateHeader()
{
header.QuestionCount = Questions.Count;
}
}
public class DnsTcpRequestResolver : IDnsRequestResolver
{
public async Task<DnsClientResponse> Request(DnsClientRequest request)
{
var tcp = new TcpClient();
try
{
#if !NET461
await tcp.Client.ConnectAsync(request.Dns).ConfigureAwait(false);
#else
tcp.Client.Connect(request.Dns);
#endif
var stream = tcp.GetStream();
var buffer = request.ToArray();
var length = BitConverter.GetBytes((ushort)buffer.Length);
if (BitConverter.IsLittleEndian)
Array.Reverse(length);
await stream.WriteAsync(length, 0, length.Length).ConfigureAwait(false);
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
buffer = new byte[2];
await Read(stream, buffer).ConfigureAwait(false);
if (BitConverter.IsLittleEndian)
Array.Reverse(buffer);
buffer = new byte[BitConverter.ToUInt16(buffer, 0)];
await Read(stream, buffer).ConfigureAwait(false);
var response = DnsResponse.FromArray(buffer);
return new DnsClientResponse(request, response, buffer);
}
finally
{
#if NET461
tcp.Close();
#else
tcp.Dispose();
#endif
}
}
private static async Task Read(Stream stream, byte[] buffer)
{
var length = buffer.Length;
var offset = 0;
int size;
while (length > 0 && (size = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false)) > 0)
{
offset += size;
length -= size;
}
if (length > 0)
{
throw new IOException("Unexpected end of stream");
}
}
}
public class DnsUdpRequestResolver : IDnsRequestResolver
{
private readonly IDnsRequestResolver _fallback;
public DnsUdpRequestResolver(IDnsRequestResolver fallback)
{
_fallback = fallback;
}
public DnsUdpRequestResolver()
{
_fallback = new DnsNullRequestResolver();
}
public async Task<DnsClientResponse> Request(DnsClientRequest request)
{
var udp = new UdpClient();
var dns = request.Dns;
try
{
udp.Client.SendTimeout = 7000;
udp.Client.ReceiveTimeout = 7000;
#if !NET461
await udp.Client.ConnectAsync(dns).ConfigureAwait(false);
#else
udp.Client.Connect(dns);
#endif
await udp.SendAsync(request.ToArray(), request.Size).ConfigureAwait(false);
var bufferList = new List<byte>();
do
{
var tempBuffer = new byte[1024];
var receiveCount = udp.Client.Receive(tempBuffer);
bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount));
}
while (udp.Client.Available > 0 || bufferList.Count == 0);
var buffer = bufferList.ToArray();
var response = DnsResponse.FromArray(buffer);
return response.IsTruncated
? await _fallback.Request(request).ConfigureAwait(false)
: new DnsClientResponse(request, response, buffer);
}
finally
{
#if NET461
udp.Close();
#else
udp.Dispose();
#endif
}
}
}
public class DnsNullRequestResolver : IDnsRequestResolver
{
public Task<DnsClientResponse> Request(DnsClientRequest request) => throw new DnsQueryException("Request failed");
}
// 12 bytes message header
[StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DnsHeader
{
public const int SIZE = 12;
private ushort id;
private byte flag0;
private byte flag1;
// Question count: number of questions in the Question section
private ushort questionCount;
// Answer record count: number of records in the Answer section
private ushort answerCount;
// Authority record count: number of records in the Authority section
private ushort authorityCount;
// Additional record count: number of records in the Additional section
private ushort addtionalCount;
public int Id
{
get => id;
set => id = (ushort)value;
}
public int QuestionCount
{
get => questionCount;
set => questionCount = (ushort)value;
}
public int AnswerRecordCount
{
get => answerCount;
set => answerCount = (ushort)value;
}
public int AuthorityRecordCount
{
get => authorityCount;
set => authorityCount = (ushort)value;
}
public int AdditionalRecordCount
{
get => addtionalCount;
set => addtionalCount = (ushort)value;
}
public bool Response
{
get => Qr == 1;
set => Qr = Convert.ToByte(value);
}
public DnsOperationCode OperationCode
{
get => (DnsOperationCode)Opcode;
set => Opcode = (byte)value;
}
public bool AuthorativeServer
{
get => Aa == 1;
set => Aa = Convert.ToByte(value);
}
public bool Truncated
{
get => Tc == 1;
set => Tc = Convert.ToByte(value);
}
public bool RecursionDesired
{
get => Rd == 1;
set => Rd = Convert.ToByte(value);
}
public bool RecursionAvailable
{
get => Ra == 1;
set => Ra = Convert.ToByte(value);
}
public DnsResponseCode ResponseCode
{
get => (DnsResponseCode)RCode;
set => RCode = (byte)value;
}
public int Size => SIZE;
// Query/Response Flag
private byte Qr
{
get => Flag0.GetBitValueAt(7);
set => Flag0 = Flag0.SetBitValueAt(7, 1, value);
}
// Operation Code
private byte Opcode
{
get => Flag0.GetBitValueAt(3, 4);
set => Flag0 = Flag0.SetBitValueAt(3, 4, value);
}
// Authorative Answer Flag
private byte Aa
{
get => Flag0.GetBitValueAt(2);
set => Flag0 = Flag0.SetBitValueAt(2, 1, value);
}
// Truncation Flag
private byte Tc
{
get => Flag0.GetBitValueAt(1);
set => Flag0 = Flag0.SetBitValueAt(1, 1, value);
}
// Recursion Desired
private byte Rd
{
get => Flag0.GetBitValueAt(0);
set => Flag0 = Flag0.SetBitValueAt(0, 1, value);
}
// Recursion Available
private byte Ra
{
get => Flag1.GetBitValueAt(7);
set => Flag1 = Flag1.SetBitValueAt(7, 1, value);
}
// Zero (Reserved)
private byte Z
{
get => Flag1.GetBitValueAt(4, 3);
set { }
}
// Response Code
private byte RCode
{
get => Flag1.GetBitValueAt(0, 4);
set => Flag1 = Flag1.SetBitValueAt(0, 4, value);
}
private byte Flag0
{
get => flag0;
set => flag0 = value;
}
private byte Flag1
{
get => flag1;
set => flag1 = value;
}
public static DnsHeader FromArray(byte[] header) =>
header.Length < SIZE
? throw new ArgumentException("Header length too small")
: header.ToStruct<DnsHeader>(0, SIZE);
public byte[] ToArray() => this.ToBytes();
public override string ToString()
=> Json.SerializeExcluding(this, true, nameof(Size));
}
public class DnsDomain : IComparable<DnsDomain>
{
private readonly string[] _labels;
public DnsDomain(string domain)
: this(domain.Split('.'))
{
}
public DnsDomain(string[] labels)
{
_labels = labels;
}
public int Size => _labels.Sum(l => l.Length) + _labels.Length + 1;
public static DnsDomain FromArray(byte[] message, int offset)
=> FromArray(message, offset, out offset);
public static DnsDomain FromArray(byte[] message, int offset, out int endOffset)
{
var labels = new List<byte[]>();
var endOffsetAssigned = false;
endOffset = 0;
byte lengthOrPointer;
while ((lengthOrPointer = message[offset++]) > 0)
{
// Two heighest bits are set (pointer)
if (lengthOrPointer.GetBitValueAt(6, 2) == 3)
{
if (!endOffsetAssigned)
{
endOffsetAssigned = true;
endOffset = offset + 1;
}
ushort pointer = lengthOrPointer.GetBitValueAt(0, 6);
offset = (pointer << 8) | message[offset];
continue;
}
if (lengthOrPointer.GetBitValueAt(6, 2) != 0)
{
throw new ArgumentException("Unexpected bit pattern in label length");
}
var length = lengthOrPointer;
var label = new byte[length];
Array.Copy(message, offset, label, 0, length);
labels.Add(label);
offset += length;
}
if (!endOffsetAssigned)
{
endOffset = offset;
}
return new DnsDomain(labels.Select(l => l.ToText(Encoding.ASCII)).ToArray());
}
public static DnsDomain PointerName(IPAddress ip)
=> new DnsDomain(FormatReverseIP(ip));
public byte[] ToArray()
{
var result = new byte[Size];
var offset = 0;
foreach (var l in _labels.Select(label => Encoding.ASCII.GetBytes(label)))
{
result[offset++] = (byte)l.Length;
l.CopyTo(result, offset);
offset += l.Length;
}
result[offset] = 0;
return result;
}
public override string ToString()
=> string.Join(".", _labels);
public int CompareTo(DnsDomain other)
=> string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);
public override bool Equals(object obj)
=> obj is DnsDomain domain && CompareTo(domain) == 0;
public override int GetHashCode() => ToString().GetHashCode();
private static string FormatReverseIP(IPAddress ip)
{
var address = ip.GetAddressBytes();
if (address.Length == 4)
{
return string.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa";
}
var nibbles = new byte[address.Length * 2];
for (int i = 0, j = 0; i < address.Length; i++, j = 2 * i)
{
var b = address[i];
nibbles[j] = b.GetBitValueAt(4, 4);
nibbles[j + 1] = b.GetBitValueAt(0, 4);
}
return string.Join(".", nibbles.Reverse().Select(b => b.ToString("x"))) + ".ip6.arpa";
}
}
public class DnsQuestion : IDnsMessageEntry
{
private readonly DnsRecordType _type;
private readonly DnsRecordClass _klass;
public static IList<DnsQuestion> GetAllFromArray(byte[] message, int offset, int questionCount) =>
GetAllFromArray(message, offset, questionCount, out offset);
public static IList<DnsQuestion> GetAllFromArray(
byte[] message,
int offset,
int questionCount,
out int endOffset)
{
IList<DnsQuestion> questions = new List<DnsQuestion>(questionCount);
for (var i = 0; i < questionCount; i++)
{
questions.Add(FromArray(message, offset, out offset));
}
endOffset = offset;
return questions;
}
public static DnsQuestion FromArray(byte[] message, int offset, out int endOffset)
{
var domain = DnsDomain.FromArray(message, offset, out offset);
var tail = message.ToStruct<Tail>(offset, Tail.SIZE);
endOffset = offset + Tail.SIZE;
return new DnsQuestion(domain, tail.Type, tail.Class);
}
public DnsQuestion(
DnsDomain domain,
DnsRecordType type = DnsRecordType.A,
DnsRecordClass klass = DnsRecordClass.IN)
{
Name = domain;
_type = type;
_klass = klass;
}
public DnsDomain Name { get; }
public DnsRecordType Type => _type;
public DnsRecordClass Class => _klass;
public int Size => Name.Size + Tail.SIZE;
public byte[] ToArray() =>
new MemoryStream(Size)
.Append(Name.ToArray())
.Append(new Tail { Type = Type, Class = Class }.ToBytes())
.ToArray();
public override string ToString()
=> Json.SerializeOnly(this, true, nameof(Name), nameof(Type), nameof(Class));
[StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct Tail
{
public const int SIZE = 4;
private ushort type;
private ushort klass;
public DnsRecordType Type
{
get => (DnsRecordType)type;
set => type = (ushort)value;
}
public DnsRecordClass Class
{
get => (DnsRecordClass)klass;
set => klass = (ushort)value;
}
}
}
}
}
+419
View File
@@ -0,0 +1,419 @@
namespace Swan.Net.Dns
{
using Formatters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
/// <summary>
/// DnsClient public methods.
/// </summary>
internal partial class DnsClient
{
public abstract class DnsResourceRecordBase : IDnsResourceRecord
{
private readonly IDnsResourceRecord _record;
protected DnsResourceRecordBase(IDnsResourceRecord record)
{
_record = record;
}
public DnsDomain Name => _record.Name;
public DnsRecordType Type => _record.Type;
public DnsRecordClass Class => _record.Class;
public TimeSpan TimeToLive => _record.TimeToLive;
public int DataLength => _record.DataLength;
public byte[] Data => _record.Data;
public int Size => _record.Size;
protected virtual string[] IncludedProperties
=> new[] {nameof(Name), nameof(Type), nameof(Class), nameof(TimeToLive), nameof(DataLength)};
public byte[] ToArray() => _record.ToArray();
public override string ToString()
=> Json.SerializeOnly(this, true, IncludedProperties);
}
public class DnsResourceRecord : IDnsResourceRecord
{
public DnsResourceRecord(
DnsDomain domain,
byte[] data,
DnsRecordType type,
DnsRecordClass klass = DnsRecordClass.IN,
TimeSpan ttl = default)
{
Name = domain;
Type = type;
Class = klass;
TimeToLive = ttl;
Data = data;
}
public DnsDomain Name { get; }
public DnsRecordType Type { get; }
public DnsRecordClass Class { get; }
public TimeSpan TimeToLive { get; }
public int DataLength => Data.Length;
public byte[] Data { get; }
public int Size => Name.Size + Tail.SIZE + Data.Length;
public static DnsResourceRecord FromArray(byte[] message, int offset, out int endOffset)
{
var domain = DnsDomain.FromArray(message, offset, out offset);
var tail = message.ToStruct<Tail>(offset, Tail.SIZE);
var data = new byte[tail.DataLength];
offset += Tail.SIZE;
Array.Copy(message, offset, data, 0, data.Length);
endOffset = offset + data.Length;
return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
}
public byte[] ToArray() =>
new MemoryStream(Size)
.Append(Name.ToArray())
.Append(new Tail()
{
Type = Type,
Class = Class,
TimeToLive = TimeToLive,
DataLength = Data.Length,
}.ToBytes())
.Append(Data)
.ToArray();
public override string ToString()
{
return Json.SerializeOnly(
this,
true,
nameof(Name),
nameof(Type),
nameof(Class),
nameof(TimeToLive),
nameof(DataLength));
}
[StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct Tail
{
public const int SIZE = 10;
private ushort type;
private ushort klass;
private uint ttl;
private ushort dataLength;
public DnsRecordType Type
{
get => (DnsRecordType) type;
set => type = (ushort) value;
}
public DnsRecordClass Class
{
get => (DnsRecordClass) klass;
set => klass = (ushort) value;
}
public TimeSpan TimeToLive
{
get => TimeSpan.FromSeconds(ttl);
set => ttl = (uint) value.TotalSeconds;
}
public int DataLength
{
get => dataLength;
set => dataLength = (ushort) value;
}
}
}
public class DnsPointerResourceRecord : DnsResourceRecordBase
{
public DnsPointerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
PointerDomainName = DnsDomain.FromArray(message, dataOffset);
}
public DnsDomain PointerDomainName { get; }
protected override string[] IncludedProperties
{
get
{
var temp = new List<string>(base.IncludedProperties) {nameof(PointerDomainName)};
return temp.ToArray();
}
}
}
public class DnsIPAddressResourceRecord : DnsResourceRecordBase
{
public DnsIPAddressResourceRecord(IDnsResourceRecord record)
: base(record)
{
IPAddress = new IPAddress(Data);
}
public IPAddress IPAddress { get; }
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(IPAddress)}.ToArray();
}
public class DnsNameServerResourceRecord : DnsResourceRecordBase
{
public DnsNameServerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
NSDomainName = DnsDomain.FromArray(message, dataOffset);
}
public DnsDomain NSDomainName { get; }
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(NSDomainName)}.ToArray();
}
public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase
{
public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);
}
public DnsDomain CanonicalDomainName { get; }
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(CanonicalDomainName)}.ToArray();
}
public class DnsMailExchangeResourceRecord : DnsResourceRecordBase
{
private const int PreferenceSize = 2;
public DnsMailExchangeResourceRecord(
IDnsResourceRecord record,
byte[] message,
int dataOffset)
: base(record)
{
var preference = new byte[PreferenceSize];
Array.Copy(message, dataOffset, preference, 0, preference.Length);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(preference);
}
dataOffset += PreferenceSize;
Preference = BitConverter.ToUInt16(preference, 0);
ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
}
public int Preference { get; }
public DnsDomain ExchangeDomainName { get; }
protected override string[] IncludedProperties => new List<string>(base.IncludedProperties)
{
nameof(Preference),
nameof(ExchangeDomainName),
}.ToArray();
}
public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase
{
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
var tail = message.ToStruct<Options>(dataOffset, Options.SIZE);
SerialNumber = tail.SerialNumber;
RefreshInterval = tail.RefreshInterval;
RetryInterval = tail.RetryInterval;
ExpireInterval = tail.ExpireInterval;
MinimumTimeToLive = tail.MinimumTimeToLive;
}
public DnsStartOfAuthorityResourceRecord(
DnsDomain domain,
DnsDomain master,
DnsDomain responsible,
long serial,
TimeSpan refresh,
TimeSpan retry,
TimeSpan expire,
TimeSpan minTtl,
TimeSpan ttl = default)
: base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl))
{
MasterDomainName = master;
ResponsibleDomainName = responsible;
SerialNumber = serial;
RefreshInterval = refresh;
RetryInterval = retry;
ExpireInterval = expire;
MinimumTimeToLive = minTtl;
}
public DnsDomain MasterDomainName { get; }
public DnsDomain ResponsibleDomainName { get; }
public long SerialNumber { get; }
public TimeSpan RefreshInterval { get; }
public TimeSpan RetryInterval { get; }
public TimeSpan ExpireInterval { get; }
public TimeSpan MinimumTimeToLive { get; }
protected override string[] IncludedProperties => new List<string>(base.IncludedProperties)
{
nameof(MasterDomainName),
nameof(ResponsibleDomainName),
nameof(SerialNumber),
}.ToArray();
private static IDnsResourceRecord Create(
DnsDomain domain,
DnsDomain master,
DnsDomain responsible,
long serial,
TimeSpan refresh,
TimeSpan retry,
TimeSpan expire,
TimeSpan minTtl,
TimeSpan ttl)
{
var data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
var tail = new Options
{
SerialNumber = serial,
RefreshInterval = refresh,
RetryInterval = retry,
ExpireInterval = expire,
MinimumTimeToLive = minTtl,
};
data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes());
return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl);
}
[StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct Options
{
public const int SIZE = 20;
private uint serialNumber;
private uint refreshInterval;
private uint retryInterval;
private uint expireInterval;
private uint ttl;
public long SerialNumber
{
get => serialNumber;
set => serialNumber = (uint) value;
}
public TimeSpan RefreshInterval
{
get => TimeSpan.FromSeconds(refreshInterval);
set => refreshInterval = (uint) value.TotalSeconds;
}
public TimeSpan RetryInterval
{
get => TimeSpan.FromSeconds(retryInterval);
set => retryInterval = (uint) value.TotalSeconds;
}
public TimeSpan ExpireInterval
{
get => TimeSpan.FromSeconds(expireInterval);
set => expireInterval = (uint) value.TotalSeconds;
}
public TimeSpan MinimumTimeToLive
{
get => TimeSpan.FromSeconds(ttl);
set => ttl = (uint) value.TotalSeconds;
}
}
}
private static class DnsResourceRecordFactory
{
public static IList<IDnsResourceRecord> GetAllFromArray(
byte[] message,
int offset,
int count,
out int endOffset)
{
var result = new List<IDnsResourceRecord>(count);
for (var i = 0; i < count; i++)
{
result.Add(GetFromArray(message, offset, out offset));
}
endOffset = offset;
return result;
}
private static IDnsResourceRecord GetFromArray(byte[] message, int offset, out int endOffset)
{
var record = DnsResourceRecord.FromArray(message, offset, out endOffset);
var dataOffset = endOffset - record.DataLength;
return record.Type switch
{
DnsRecordType.A => (IDnsResourceRecord) new DnsIPAddressResourceRecord(record),
DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record),
DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset),
DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset),
DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset),
DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset),
DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset),
_ => record
};
}
}
}
}
+215
View File
@@ -0,0 +1,215 @@
namespace Swan.Net.Dns
{
using Formatters;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
/// <summary>
/// DnsClient Response inner class.
/// </summary>
internal partial class DnsClient
{
public class DnsClientResponse : IDnsResponse
{
private readonly DnsResponse _response;
private readonly byte[] _message;
internal DnsClientResponse(DnsClientRequest request, DnsResponse response, byte[] message)
{
Request = request;
_message = message;
_response = response;
}
public DnsClientRequest Request { get; }
public int Id
{
get { return _response.Id; }
set { }
}
public IList<IDnsResourceRecord> AnswerRecords => _response.AnswerRecords;
public IList<IDnsResourceRecord> AuthorityRecords =>
new ReadOnlyCollection<IDnsResourceRecord>(_response.AuthorityRecords);
public IList<IDnsResourceRecord> AdditionalRecords =>
new ReadOnlyCollection<IDnsResourceRecord>(_response.AdditionalRecords);
public bool IsRecursionAvailable
{
get { return _response.IsRecursionAvailable; }
set { }
}
public bool IsAuthorativeServer
{
get { return _response.IsAuthorativeServer; }
set { }
}
public bool IsTruncated
{
get { return _response.IsTruncated; }
set { }
}
public DnsOperationCode OperationCode
{
get { return _response.OperationCode; }
set { }
}
public DnsResponseCode ResponseCode
{
get { return _response.ResponseCode; }
set { }
}
public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(_response.Questions);
public int Size => _message.Length;
public byte[] ToArray() => _message;
public override string ToString() => _response.ToString();
}
public class DnsResponse : IDnsResponse
{
private DnsHeader _header;
public DnsResponse(
DnsHeader header,
IList<DnsQuestion> questions,
IList<IDnsResourceRecord> answers,
IList<IDnsResourceRecord> authority,
IList<IDnsResourceRecord> additional)
{
_header = header;
Questions = questions;
AnswerRecords = answers;
AuthorityRecords = authority;
AdditionalRecords = additional;
}
public IList<DnsQuestion> Questions { get; }
public IList<IDnsResourceRecord> AnswerRecords { get; }
public IList<IDnsResourceRecord> AuthorityRecords { get; }
public IList<IDnsResourceRecord> AdditionalRecords { get; }
public int Id
{
get => _header.Id;
set => _header.Id = value;
}
public bool IsRecursionAvailable
{
get => _header.RecursionAvailable;
set => _header.RecursionAvailable = value;
}
public bool IsAuthorativeServer
{
get => _header.AuthorativeServer;
set => _header.AuthorativeServer = value;
}
public bool IsTruncated
{
get => _header.Truncated;
set => _header.Truncated = value;
}
public DnsOperationCode OperationCode
{
get => _header.OperationCode;
set => _header.OperationCode = value;
}
public DnsResponseCode ResponseCode
{
get => _header.ResponseCode;
set => _header.ResponseCode = value;
}
public int Size
=> _header.Size +
Questions.Sum(q => q.Size) +
AnswerRecords.Sum(a => a.Size) +
AuthorityRecords.Sum(a => a.Size) +
AdditionalRecords.Sum(a => a.Size);
public static DnsResponse FromArray(byte[] message)
{
var header = DnsHeader.FromArray(message);
var offset = header.Size;
if (!header.Response || header.QuestionCount == 0)
{
throw new ArgumentException("Invalid response message");
}
if (header.Truncated)
{
return new DnsResponse(header,
DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount),
new List<IDnsResourceRecord>(),
new List<IDnsResourceRecord>(),
new List<IDnsResourceRecord>());
}
return new DnsResponse(header,
DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset),
DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset),
DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset),
DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out offset));
}
public byte[] ToArray()
{
UpdateHeader();
var result = new MemoryStream(Size);
result
.Append(_header.ToArray())
.Append(Questions.Select(q => q.ToArray()))
.Append(AnswerRecords.Select(a => a.ToArray()))
.Append(AuthorityRecords.Select(a => a.ToArray()))
.Append(AdditionalRecords.Select(a => a.ToArray()));
return result.ToArray();
}
public override string ToString()
{
UpdateHeader();
return Json.SerializeOnly(
this,
true,
nameof(Questions),
nameof(AnswerRecords),
nameof(AuthorityRecords),
nameof(AdditionalRecords));
}
private void UpdateHeader()
{
_header.QuestionCount = Questions.Count;
_header.AnswerRecordCount = AnswerRecords.Count;
_header.AuthorityRecordCount = AuthorityRecords.Count;
_header.AdditionalRecordCount = AdditionalRecords.Count;
}
}
}
}
+79
View File
@@ -0,0 +1,79 @@
namespace Swan.Net.Dns
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
/// <summary>
/// DnsClient public methods.
/// </summary>
internal partial class DnsClient
{
private readonly IPEndPoint _dns;
private readonly IDnsRequestResolver _resolver;
public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null)
{
_dns = dns;
_resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
}
public DnsClient(IPAddress ip, int port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null)
: this(new IPEndPoint(ip, port), resolver)
{
}
public DnsClientRequest Create(IDnsRequest? request = null)
=> new DnsClientRequest(_dns, request, _resolver);
public async Task<IList<IPAddress>> Lookup(string domain, DnsRecordType type = DnsRecordType.A)
{
if (string.IsNullOrWhiteSpace(domain))
throw new ArgumentNullException(nameof(domain));
if (type != DnsRecordType.A && type != DnsRecordType.AAAA)
{
throw new ArgumentException("Invalid record type " + type);
}
var response = await Resolve(domain, type).ConfigureAwait(false);
var ips = response.AnswerRecords
.Where(r => r.Type == type)
.Cast<DnsIPAddressResourceRecord>()
.Select(r => r.IPAddress)
.ToList();
return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips;
}
public async Task<string> Reverse(IPAddress ip)
{
if (ip == null)
throw new ArgumentNullException(nameof(ip));
var response = await Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
var ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR);
return ptr == null
? throw new DnsQueryException(response, "No matching records")
: ((DnsPointerResourceRecord) ptr).PointerDomainName.ToString();
}
public Task<DnsClientResponse> Resolve(string domain, DnsRecordType type) =>
Resolve(new DnsDomain(domain), type);
public Task<DnsClientResponse> Resolve(DnsDomain domain, DnsRecordType type)
{
var request = Create();
var question = new DnsQuestion(domain, type);
request.Questions.Add(question);
request.OperationCode = DnsOperationCode.Query;
request.RecursionDesired = true;
return request.Resolve();
}
}
}
+37
View File
@@ -0,0 +1,37 @@
namespace Swan.Net.Dns
{
using System;
/// <summary>
/// An exception thrown when the DNS query fails.
/// </summary>
/// <seealso cref="Exception" />
[Serializable]
public class DnsQueryException : Exception
{
internal DnsQueryException(string message)
: base(message)
{
}
internal DnsQueryException(string message, Exception e)
: base(message, e)
{
}
internal DnsQueryException(DnsClient.IDnsResponse response)
: this(response, Format(response))
{
}
internal DnsQueryException(DnsClient.IDnsResponse response, string message)
: base(message)
{
Response = response;
}
internal DnsClient.IDnsResponse? Response { get; }
private static string Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
}
}
+123
View File
@@ -0,0 +1,123 @@
namespace Swan.Net.Dns
{
using System.Collections.Generic;
/// <summary>
/// Represents a response from a DNS server.
/// </summary>
public class DnsQueryResult
{
private readonly List<DnsRecord> _mAnswerRecords = new List<DnsRecord>();
private readonly List<DnsRecord> _mAdditionalRecords = new List<DnsRecord>();
private readonly List<DnsRecord> _mAuthorityRecords = new List<DnsRecord>();
/// <summary>
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
/// </summary>
/// <param name="response">The response.</param>
internal DnsQueryResult(DnsClient.IDnsResponse response)
: this()
{
Id = response.Id;
IsAuthoritativeServer = response.IsAuthorativeServer;
IsRecursionAvailable = response.IsRecursionAvailable;
IsTruncated = response.IsTruncated;
OperationCode = response.OperationCode;
ResponseCode = response.ResponseCode;
if (response.AnswerRecords != null)
{
foreach (var record in response.AnswerRecords)
AnswerRecords.Add(new DnsRecord(record));
}
if (response.AuthorityRecords != null)
{
foreach (var record in response.AuthorityRecords)
AuthorityRecords.Add(new DnsRecord(record));
}
if (response.AdditionalRecords != null)
{
foreach (var record in response.AdditionalRecords)
AdditionalRecords.Add(new DnsRecord(record));
}
}
private DnsQueryResult()
{
}
/// <summary>
/// Gets the identifier.
/// </summary>
/// <value>
/// The identifier.
/// </value>
public int Id { get; }
/// <summary>
/// Gets a value indicating whether this instance is authoritative server.
/// </summary>
/// <value>
/// <c>true</c> if this instance is authoritative server; otherwise, <c>false</c>.
/// </value>
public bool IsAuthoritativeServer { get; }
/// <summary>
/// Gets a value indicating whether this instance is truncated.
/// </summary>
/// <value>
/// <c>true</c> if this instance is truncated; otherwise, <c>false</c>.
/// </value>
public bool IsTruncated { get; }
/// <summary>
/// Gets a value indicating whether this instance is recursion available.
/// </summary>
/// <value>
/// <c>true</c> if this instance is recursion available; otherwise, <c>false</c>.
/// </value>
public bool IsRecursionAvailable { get; }
/// <summary>
/// Gets the operation code.
/// </summary>
/// <value>
/// The operation code.
/// </value>
public DnsOperationCode OperationCode { get; }
/// <summary>
/// Gets the response code.
/// </summary>
/// <value>
/// The response code.
/// </value>
public DnsResponseCode ResponseCode { get; }
/// <summary>
/// Gets the answer records.
/// </summary>
/// <value>
/// The answer records.
/// </value>
public IList<DnsRecord> AnswerRecords => _mAnswerRecords;
/// <summary>
/// Gets the additional records.
/// </summary>
/// <value>
/// The additional records.
/// </value>
public IList<DnsRecord> AdditionalRecords => _mAdditionalRecords;
/// <summary>
/// Gets the authority records.
/// </summary>
/// <value>
/// The authority records.
/// </value>
public IList<DnsRecord> AuthorityRecords => _mAuthorityRecords;
}
}
+208
View File
@@ -0,0 +1,208 @@
namespace Swan.Net.Dns
{
using System;
using System.Net;
using System.Text;
/// <summary>
/// Represents a DNS record entry.
/// </summary>
public class DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
/// </summary>
/// <param name="record">The record.</param>
internal DnsRecord(DnsClient.IDnsResourceRecord record)
: this()
{
Name = record.Name.ToString();
Type = record.Type;
Class = record.Class;
TimeToLive = record.TimeToLive;
Data = record.Data;
// PTR
PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
// A
IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
// NS
NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString();
// CNAME
CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString();
// MX
MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString();
MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference;
// SOA
SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString();
SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber;
SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval;
SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval;
SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive;
}
private DnsRecord()
{
// placeholder
}
/// <summary>
/// Gets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
public string Name { get; }
/// <summary>
/// Gets the type.
/// </summary>
/// <value>
/// The type.
/// </value>
public DnsRecordType Type { get; }
/// <summary>
/// Gets the class.
/// </summary>
/// <value>
/// The class.
/// </value>
public DnsRecordClass Class { get; }
/// <summary>
/// Gets the time to live.
/// </summary>
/// <value>
/// The time to live.
/// </value>
public TimeSpan TimeToLive { get; }
/// <summary>
/// Gets the raw data of the record.
/// </summary>
/// <value>
/// The data.
/// </value>
public byte[] Data { get; }
/// <summary>
/// Gets the data text bytes in ASCII encoding.
/// </summary>
/// <value>
/// The data text.
/// </value>
public string DataText => Data == null ? string.Empty : Encoding.ASCII.GetString(Data);
/// <summary>
/// Gets the name of the pointer domain.
/// </summary>
/// <value>
/// The name of the pointer domain.
/// </value>
public string PointerDomainName { get; }
/// <summary>
/// Gets the ip address.
/// </summary>
/// <value>
/// The ip address.
/// </value>
public IPAddress IPAddress { get; }
/// <summary>
/// Gets the name of the name server domain.
/// </summary>
/// <value>
/// The name of the name server domain.
/// </value>
public string NameServerDomainName { get; }
/// <summary>
/// Gets the name of the canonical domain.
/// </summary>
/// <value>
/// The name of the canonical domain.
/// </value>
public string CanonicalDomainName { get; }
/// <summary>
/// Gets the mail exchanger preference.
/// </summary>
/// <value>
/// The mail exchanger preference.
/// </value>
public int? MailExchangerPreference { get; }
/// <summary>
/// Gets the name of the mail exchanger domain.
/// </summary>
/// <value>
/// The name of the mail exchanger domain.
/// </value>
public string MailExchangerDomainName { get; }
/// <summary>
/// Gets the name of the soa master domain.
/// </summary>
/// <value>
/// The name of the soa master domain.
/// </value>
public string SoaMasterDomainName { get; }
/// <summary>
/// Gets the name of the soa responsible domain.
/// </summary>
/// <value>
/// The name of the soa responsible domain.
/// </value>
public string SoaResponsibleDomainName { get; }
/// <summary>
/// Gets the soa serial number.
/// </summary>
/// <value>
/// The soa serial number.
/// </value>
public long? SoaSerialNumber { get; }
/// <summary>
/// Gets the soa refresh interval.
/// </summary>
/// <value>
/// The soa refresh interval.
/// </value>
public TimeSpan? SoaRefreshInterval { get; }
/// <summary>
/// Gets the soa retry interval.
/// </summary>
/// <value>
/// The soa retry interval.
/// </value>
public TimeSpan? SoaRetryInterval { get; }
/// <summary>
/// Gets the soa expire interval.
/// </summary>
/// <value>
/// The soa expire interval.
/// </value>
public TimeSpan? SoaExpireInterval { get; }
/// <summary>
/// Gets the soa minimum time to live.
/// </summary>
/// <value>
/// The soa minimum time to live.
/// </value>
public TimeSpan? SoaMinimumTimeToLive { get; }
}
}
+172
View File
@@ -0,0 +1,172 @@
// ReSharper disable InconsistentNaming
namespace Swan.Net.Dns
{
/// <summary>
/// Enumerates the different DNS record types.
/// </summary>
public enum DnsRecordType
{
/// <summary>
/// A records
/// </summary>
A = 1,
/// <summary>
/// Nameserver records
/// </summary>
NS = 2,
/// <summary>
/// CNAME records
/// </summary>
CNAME = 5,
/// <summary>
/// SOA records
/// </summary>
SOA = 6,
/// <summary>
/// WKS records
/// </summary>
WKS = 11,
/// <summary>
/// PTR records
/// </summary>
PTR = 12,
/// <summary>
/// MX records
/// </summary>
MX = 15,
/// <summary>
/// TXT records
/// </summary>
TXT = 16,
/// <summary>
/// A records fot IPv6
/// </summary>
AAAA = 28,
/// <summary>
/// SRV records
/// </summary>
SRV = 33,
/// <summary>
/// ANY records
/// </summary>
ANY = 255,
}
/// <summary>
/// Enumerates the different DNS record classes.
/// </summary>
public enum DnsRecordClass
{
/// <summary>
/// IN records
/// </summary>
IN = 1,
/// <summary>
/// ANY records
/// </summary>
ANY = 255,
}
/// <summary>
/// Enumerates the different DNS operation codes.
/// </summary>
public enum DnsOperationCode
{
/// <summary>
/// Query operation
/// </summary>
Query = 0,
/// <summary>
/// IQuery operation
/// </summary>
IQuery,
/// <summary>
/// Status operation
/// </summary>
Status,
/// <summary>
/// Notify operation
/// </summary>
Notify = 4,
/// <summary>
/// Update operation
/// </summary>
Update,
}
/// <summary>
/// Enumerates the different DNS query response codes.
/// </summary>
public enum DnsResponseCode
{
/// <summary>
/// No error
/// </summary>
NoError = 0,
/// <summary>
/// No error
/// </summary>
FormatError,
/// <summary>
/// Format error
/// </summary>
ServerFailure,
/// <summary>
/// Server failure error
/// </summary>
NameError,
/// <summary>
/// Name error
/// </summary>
NotImplemented,
/// <summary>
/// Not implemented error
/// </summary>
Refused,
/// <summary>
/// Refused error
/// </summary>
YXDomain,
/// <summary>
/// YXRR error
/// </summary>
YXRRSet,
/// <summary>
/// NXRR Set error
/// </summary>
NXRRSet,
/// <summary>
/// Not authorized error
/// </summary>
NotAuth,
/// <summary>
/// Not zone error
/// </summary>
NotZone,
}
}