Neukonstruktion

This commit is contained in:
Fabian Stamm 2016-11-26 12:11:27 +01:00
parent 1af64ace95
commit aab06c8ea8
106 changed files with 4576 additions and 704 deletions

View File

@ -1,9 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.25914.0
VisualStudioVersion = 15.0.25920.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMTPServer", "SMTPServer\SMTPServer.csproj", "{ABB6A3E6-38B6-4D02-AC9C-91FA69CF03BE}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailServer", "MailServer\MailServer.csproj", "{ABB6A3E6-38B6-4D02-AC9C-91FA69CF03BE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D43013F8-933F-4ADC-8943-08E91662A070}"
EndProject

View File

@ -0,0 +1,199 @@
////using System;
////using System.Collections.Generic;
////namespace DnsClient
////{
//// /*
//// * Reference RFC6895#section-2.3
//// *
//// RCODE Name Description Reference
//// Decimal
//// Hexadecimal
//// 0 NoError No Error [RFC1035]
//// 1 FormErr Format Error [RFC1035]
//// 2 ServFail Server Failure [RFC1035]
//// 3 NXDomain Non-Existent Domain [RFC1035]
//// 4 NotImp Not Implemented [RFC1035]
//// 5 Refused Query Refused [RFC1035]
//// 6 YXDomain Name Exists when it should not [RFC2136]
//// 7 YXRRSet RR Set Exists when it should not [RFC2136]
//// 8 NXRRSet RR Set that should exist does not [RFC2136]
//// 9 NotAuth Server Not Authoritative for zone [RFC2136]
//// 9 NotAuth Not Authorized [RFC2845]
//// 10 NotZone Name not contained in zone [RFC2136]
//// 11 - 15
//// 0xB - 0xF Unassigned
//// 16 BADVERS Bad OPT Version [RFC6891]
//// 16 BADSIG TSIG Signature Failure [RFC2845]
//// 17 BADKEY Key not recognized [RFC2845]
//// 18 BADTIME Signature out of time window [RFC2845]
//// 19 BADMODE Bad TKEY Mode [RFC2930]
//// 20 BADNAME Duplicate key name [RFC2930]
//// 21 BADALG Algorithm not supported [RFC2930]
//// 22 BADTRUNC Bad Truncation [RFC4635]
//// 23 - 3,840
//// 0x0017 - 0x0F00 Unassigned
//// 3,841 - 4,095
//// 0x0F01 - 0x0FFF Reserved for Private Use
//// 4,096 - 65,534
//// 0x1000 - 0xFFFE Unassigned
//// 65,535
//// 0xFFFF Reserved; can only be allocated by Standards
//// Action.
//// */
//// public enum DnsErrorCode : ushort
//// {
//// NoError = 0,
//// FormErr = 1,
//// ServFail = 2,
//// NXDomain = 3,
//// NotImp = 4,
//// Refused = 5,
//// YXDomain = 6,
//// YXRRSet = 7,
//// NXRRSet = 8,
//// NotAuth = 9,
//// NotZone = 10,
//// BADVERS = 16, // or BADSIG
//// BADKEY = 17,
//// BADTIME = 18,
//// BADMODE = 19,
//// BADNAME = 20,
//// BADALG = 21,
//// BADTRUNC = 22,
//// BADCOOKIE = 23,
//// Unassigned = 666
//// }
//// public static class DnsErrorCodeText
//// {
//// public const string BADALG = "Algorithm not supported";
//// public const string BADCOOKIE = "Bad/missing Server Cookie";
//// public const string BADKEY = "Key not recognized";
//// public const string BADMODE = "Bad TKEY Mode";
//// public const string BADNAME = "Duplicate key name";
//// public const string BADSIG = "TSIG Signature Failure";
//// public const string BADTIME = "Signature out of time window";
//// public const string BADTRUNC = "Bad Truncation";
//// public const string BADVERS = "Bad OPT Version";
//// public const string FormErr = "Format Error";
//// public const string NoError = "No Error";
//// public const string NotAuth = "Server Not Authoritative for zone or Not Authorized";
//// public const string NotImp = "Not Implemented";
//// public const string NotZone = "Name not contained in zone";
//// public const string NXDomain = "Non-Existent Domain";
//// public const string NXRRSet = "RR Set that should exist does not";
//// public const string Refused = "Query Refused";
//// public const string ServFail = "Server Failure";
//// public const string Unassigned = "Unknown Error";
//// public const string YXDomain = "Name Exists when it should not";
//// public const string YXRRSet = "RR Set Exists when it should not";
//// private static readonly Dictionary<DnsErrorCode, string> errors = new Dictionary<DnsErrorCode, string>()
//// {
//// { DnsErrorCode.NoError, DnsErrorCodeText.NoError },
//// { DnsErrorCode.FormErr, DnsErrorCodeText.FormErr },
//// { DnsErrorCode.ServFail, DnsErrorCodeText.ServFail },
//// { DnsErrorCode.NXDomain, DnsErrorCodeText.NXDomain },
//// { DnsErrorCode.NotImp, DnsErrorCodeText.NotImp },
//// { DnsErrorCode.Refused, DnsErrorCodeText.Refused },
//// { DnsErrorCode.YXDomain, DnsErrorCodeText.YXDomain },
//// { DnsErrorCode.YXRRSet, DnsErrorCodeText.YXRRSet },
//// { DnsErrorCode.NXRRSet, DnsErrorCodeText.NXRRSet },
//// { DnsErrorCode.NotAuth, DnsErrorCodeText.NotAuth },
//// { DnsErrorCode.NotZone, DnsErrorCodeText.NotZone },
//// { DnsErrorCode.BADVERS, DnsErrorCodeText.BADVERS },
//// { DnsErrorCode.BADKEY, DnsErrorCodeText.BADKEY },
//// { DnsErrorCode.BADTIME, DnsErrorCodeText.BADTIME },
//// { DnsErrorCode.BADMODE, DnsErrorCodeText.BADMODE },
//// { DnsErrorCode.BADNAME, DnsErrorCodeText.BADNAME },
//// { DnsErrorCode.BADALG, DnsErrorCodeText.BADALG },
//// { DnsErrorCode.BADTRUNC, DnsErrorCodeText.BADTRUNC },
//// { DnsErrorCode.BADCOOKIE, DnsErrorCodeText.BADCOOKIE },
//// };
//// public static string GetErrorText(DnsErrorCode code)
//// {
//// if (!errors.ContainsKey(code))
//// {
//// return Unassigned;
//// }
//// return errors[code];
//// }
//// }
//// public class DnsErrorException : Exception
//// {
//// public DnsErrorCode Code { get; }
//// public string DnsError { get; }
//// /// <summary>
//// /// Creates an instance of <see cref="DnsErrorException"/> with <see cref="DnsErrorCode.Unassigned"/>.
//// /// </summary>
//// public DnsErrorException() : base(DnsErrorCodeText.Unassigned)
//// {
//// Code = DnsErrorCode.Unassigned;
//// DnsError = DnsErrorCodeText.GetErrorText(Code);
//// }
//// /// <summary>
//// /// Creates an instance of <see cref="DnsErrorException"/> with <see cref="DnsErrorCode.Unassigned"/>
//// /// and a custom message.
//// /// </summary>
//// public DnsErrorException(string message) : base(message)
//// {
//// Code = DnsErrorCode.Unassigned;
//// DnsError = DnsErrorCodeText.GetErrorText(Code);
//// }
//// /// <summary>
//// /// Creates an instance of <see cref="DnsErrorException"/> with
//// /// the standard error text for this <paramref name="code"/>.
//// /// </summary>
//// public DnsErrorException(DnsErrorCode code) : base(DnsErrorCodeText.GetErrorText(code))
//// {
//// Code = code;
//// DnsError = DnsErrorCodeText.GetErrorText(Code);
//// }
//// /// <summary>
//// /// Creates an instance of <see cref="DnsErrorException"/> with <see cref="DnsErrorCode.Unassigned"/>
//// /// and a custom message.
//// /// </summary>
//// public DnsErrorException(string message, Exception innerException) : base(message, innerException)
//// {
//// Code = DnsErrorCode.Unassigned;
//// DnsError = DnsErrorCodeText.GetErrorText(Code);
//// }
//// /// <summary>
//// /// Creates an instance of <see cref="DnsErrorException"/> with a custom message
//// /// and the given <paramref name="code"/>.
//// /// </summary>
//// public DnsErrorException(DnsErrorCode code, string message) : base(message)
//// {
//// Code = code;
//// DnsError = DnsErrorCodeText.GetErrorText(Code);
//// }
//// /// <summary>
//// /// Creates an instance of <see cref="DnsErrorException"/> with a custom message
//// /// and the given <paramref name="code"/>.
//// /// </summary>
//// public DnsErrorException(DnsErrorCode code, string message, Exception innerException) : base(message, innerException)
//// {
//// Code = code;
//// DnsError = DnsErrorCodeText.GetErrorText(Code);
//// }
//// }
////}

View File

@ -0,0 +1,47 @@
using System;
namespace DnsClient
{
/* Reference: https://tools.ietf.org/html/rfc6895#section-2
* Response header fields
*
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
==> |QR| OpCode |AA|TC|RD|RA| Z|AD|CD| RCODE | <==
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT/ZOCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT/PRCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT/UPCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* */
/// <summary>
/// The flags of the header's second 16bit value
/// </summary>
public enum DnsHeaderFlag : ushort
{
IsCheckingDisabled = 0x0010,
IsAuthenticData = 0x0020,
FutureUse = 0x0040, // Z bit seems to be ignored now, see https://tools.ietf.org/html/rfc6895#section-2.1
RecursionAvailable = 0x0080,
RecursionDesired = 0x0100,
ResultTruncated = 0x0200,
HasAuthorityAnswer = 0x0400,
HasQuery = 0x8000,
}
public static class DnsHeader
{
public static readonly ushort OPCODE_MASK = 0x7800;
public static readonly ushort OPCODE_SHIFT = 11;
public static readonly ushort RCODE_MASK = 0x000F;
}
}

View File

@ -0,0 +1,101 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using DnsClient.Protocol;
namespace DnsClient
{
public abstract class DnsMessageHandler : IDisposable
{
private bool _disposedValue = false;
public abstract Task<DnsResponseMessage> QueryAsync(IPEndPoint server, DnsRequestMessage request, CancellationToken cancellationToken);
public abstract bool IsTransientException<T>(T exception) where T : Exception;
public virtual byte[] GetRequestData(DnsRequestMessage request)
{
var question = request.Question;
var questionData = question.QueryName.AsBytes();
// 4 more bytes for the type and class
var writer = new DnsDatagramWriter(DnsRequestHeader.HeaderLength + questionData.Length + 4);
writer.SetInt16Network((short)request.Header.Id);
writer.SetUInt16Network(request.Header.RawFlags);
writer.SetInt16Network((short)request.Header.QuestionCount);
// jump to end of header, we didn't write all fields
writer.Index = DnsRequestHeader.HeaderLength;
writer.SetBytes(questionData, questionData.Length);
writer.SetUInt16Network((ushort)question.QuestionType);
writer.SetUInt16Network((ushort)question.QuestionClass);
return writer.Data;
}
public virtual DnsResponseMessage GetResponseMessage(byte[] responseData)
{
var reader = new DnsDatagramReader(responseData);
var factory = new DnsRecordFactory(reader);
var id = reader.ReadUInt16Reverse();
var flags = reader.ReadUInt16Reverse();
var questionCount = reader.ReadUInt16Reverse();
var answerCount = reader.ReadUInt16Reverse();
var nameServerCount = reader.ReadUInt16Reverse();
var additionalCount = reader.ReadUInt16Reverse();
var header = new DnsResponseHeader(id, flags, questionCount, answerCount, additionalCount, nameServerCount);
var response = new DnsResponseMessage(header);
for (int questionIndex = 0; questionIndex < questionCount; questionIndex++)
{
var question = new DnsQuestion(reader.ReadName(), (QueryType)reader.ReadUInt16Reverse(), (QueryClass)reader.ReadUInt16Reverse());
response.AddQuestion(question);
}
for (int answerIndex = 0; answerIndex < answerCount; answerIndex++)
{
var info = factory.ReadRecordInfo();
var record = factory.GetRecord(info);
response.AddAnswer(record);
}
for (int serverIndex = 0; serverIndex < nameServerCount; serverIndex++)
{
var info = factory.ReadRecordInfo();
var record = factory.GetRecord(info);
response.AddAuthority(record);
}
for (int additionalIndex = 0; additionalIndex < additionalCount; additionalIndex++)
{
var info = factory.ReadRecordInfo();
var record = factory.GetRecord(info);
response.AddAdditional(record);
}
return response;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
}
}

View File

@ -0,0 +1,350 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DnsClient
{
public class DnsName : IComparable
{
private const byte ReferenceByte = 0xc0;
private List<string> _labels = new List<string>();
private short _octets = 1;
public bool HasRootLabel => (_labels.Count > 0 && Get(0).Equals(""));
public bool IsEmpty => Size == 0;
public bool IsHostName => !_labels.Any(p => !IsHostNameLabel(p));
public int Octets => _octets;
public int Size => _labels.Where(p => p != "").Count();
/// <summary>
/// Creates an empty <see cref="DnsName"/> instance.
/// </summary>
public DnsName()
{
Add(0, "");
}
/// <summary>
/// Initializes a new instance of <see cref="DnsName"/> by parsing the <paramref name="name"/>.
/// </summary>
/// <param name="name">The input name.</param>
public DnsName(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (name.Length > 0)
{
Parse(name);
}
if (!HasRootLabel) Add(0, "");
}
public static DnsName FromBytes(byte[] data, ref int offset)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
if (offset > data.Length - 1)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
var result = new DnsName();
// read the length byte for the label, then get the content from offset+1 to length
// proceed till we reach zero length byte.
byte length;
while ((length = data[offset++]) != 0)
{
// respect the reference bit and lookup the name at the given position
// the reader will advance only for the 2 bytes read.
if ((length & ReferenceByte) != 0)
{
var subset = (length & 0x3f) << 8 | data[offset++];
var subName = FromBytes(data, ref subset);
result.Concat(subName);
return result;
}
if (offset + length > data.Length - 1)
{
throw new ArgumentOutOfRangeException(
nameof(data),
$"Found invalid label position {offset - 1} or length {length} in the source data.");
}
var label = Encoding.ASCII.GetString(data, offset, length);
result.Add(1, label);
offset += length;
}
return result;
}
public void Add(int pos, string label)
{
if (label == null)
{
throw new ArgumentNullException(nameof(label));
}
if (pos < 0 || pos > _labels.Count)
{
throw new ArgumentOutOfRangeException(nameof(pos));
}
// Check for empty labels: may have only one, and only at end.
int len = label.Length;
if ((pos > 0 && len == 0) ||
(pos == 0 && HasRootLabel))
{
throw new InvalidOperationException("Empty label must be the last label in a domain name");
}
// Total length must not be larger than 255 characters (including the ending zero).
if (len > 0)
{
if (_octets + len + 1 >= 256)
{
throw new InvalidOperationException("Name too long");
}
_octets += (short)(len + 1);
}
int i = _labels.Count - pos;
VerifyLabel(label);
_labels.Insert(i, label);
}
public byte[] AsBytes()
{
var bytes = new byte[_octets];
var offset = 0;
for (int i = _labels.Count - 1; i >= 0; i--)
{
var label = Get(i);
// should never cause issues as each label's length is limited to 64 chars.
var len = checked((byte)label.Length);
// set the label length byte
bytes[offset++] = len;
// set the label's content
var labelBytes = Encoding.ASCII.GetBytes(label);
Array.ConstrainedCopy(labelBytes, 0, bytes, offset, len);
offset += len;
}
return bytes;
}
public int CompareTo(object obj)
{
if (obj == null)
{
return 1;
}
return ToString().CompareTo(obj.ToString());
}
public void Concat(DnsName other)
{
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
foreach (var label in other._labels.Where(p => !string.IsNullOrWhiteSpace(p)))
{
Add(1, label);
}
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var otherName = obj as DnsName;
if (otherName == null)
{
return false;
}
return CompareTo(otherName) == 0;
}
public string Get(int pos)
{
if (pos < 0 || pos > _labels.Count)
{
throw new ArgumentOutOfRangeException(nameof(pos));
}
return _labels[_labels.Count - pos - 1];
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
public override string ToString()
{
var buf = new StringBuilder();
foreach (var label in _labels)
{
if (buf.Length > 0 || label.Length == 0)
{
buf.Append('.');
}
Escaped(buf, label);
}
var name = buf.ToString();
return name;
}
private static bool IsHostNameChar(char c)
{
return (c == '-' ||
c >= 'a' && c <= 'z' ||
c >= 'A' && c <= 'Z' ||
c >= '0' && c <= '9');
}
private static bool IsHostNameLabel(string label)
{
for (int i = 0; i < label.Length; i++)
{
char c = label.ElementAt(i);
if (!IsHostNameChar(c))
{
return false;
}
}
return !(label.StartsWith("-") || label.EndsWith("-"));
}
private static void VerifyLabel(string label)
{
// http://www.freesoft.org/CIE/RFC/1035/9.htm
// dns name limits are 63octets per label
// (63 letters).(63 letters).(63 letters).(62 letters)
if (label.Length > 63)
{
throw new InvalidOperationException("Label exceeds 63 octets: " + label);
}
// Check for two-byte characters.
for (int i = 0; i < label.Length; i++)
{
char c = label.ElementAt(i);
if ((c & 0xFF00) != 0)
{
throw new InvalidOperationException("Label has two-byte char: " + label);
}
}
}
private void Escaped(StringBuilder buf, string label)
{
for (int i = 0; i < label.Length; i++)
{
char c = label.ElementAt(i);
if (c == '.' || c == '\\')
{
buf.Append('\\');
}
buf.Append(c);
}
}
private char GetEscaped(string domainName, int pos)
{
try
{
// assert (name.charAt(pos) == '\\');
char c1 = domainName.ElementAt(++pos);
if (IsDigit(c1))
{
// sequence is `\DDD'
char c2 = domainName.ElementAt(++pos);
char c3 = domainName.ElementAt(++pos);
if (IsDigit(c2) && IsDigit(c3))
{
return (char)((c1 - '0') * 100 + (c2 - '0') * 10 + (c3 - '0'));
}
else
{
throw new ArgumentException("Invalid escape sequence.", nameof(domainName));
}
}
else
{
return c1;
}
}
catch (ArgumentOutOfRangeException)
{
throw new ArgumentException("Invalid escape sequence.", nameof(domainName));
}
}
private bool IsDigit(char c)
{
return (c >= '0' && c <= '9');
}
private void Parse(string domainName)
{
var label = new StringBuilder();
for (int index = 0; index < domainName.Length; index++)
{
var c = domainName[index];
if (c == '\\')
{
c = GetEscaped(domainName, index++);
if (IsDigit(domainName[index]))
{
index += 2;
}
label.Append(c);
}
else if (c != '.')
{
label.Append(c);
}
else
{
Add(0, label.ToString());
label.Clear();
}
}
if (label.Length > 0)
{
Add(0, label.ToString());
}
}
}
}

View File

@ -0,0 +1,66 @@
using System;
namespace DnsClient
{
/*
*
* Reference: [RFC6895][RFC1035]
0 Query [RFC1035]
1 IQuery (Inverse Query, OBSOLETE) [RFC3425]
2 Status [RFC1035]
3 Unassigned
4 Notify [RFC1996]
5 Update [RFC2136]
6-15 Unassigned
* */
/// <summary>
/// RFCs 1035, 1996, 2136, 3425.
/// Specifies kind of query in this message.
/// This value is set by the originator of a query and copied into the response.
/// </summary>
public enum DnsOpCode : short
{
/// <summary>
/// RFC 1035.
/// A standard query.
/// </summary>
Query,
/// <summary>
/// RFC 3425.
/// An inverse query.
/// </summary>
[Obsolete]
IQuery,
/// <summary>
/// RFC 1035.
/// A server status request.
/// </summary>
Status,
Unassinged3,
/// <summary>
/// RFC 1996.
/// </summary>
Notify,
/// <summary>
/// RFC 2136.
/// </summary>
Update,
Unassinged6,
Unassinged7,
Unassinged8,
Unassinged9,
Unassinged10,
Unassinged11,
Unassinged12,
Unassinged13,
Unassinged14,
Unassinged15,
}
}

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DnsClient.Protocol;
using DnsClient.Protocol.Record;
namespace DnsClient
{
/// <summary>
/// Immutable version of the <see cref="DnsResponseMessage"/>.
/// </summary>
public class DnsQueryResponse
{
private int? _hashCode;
/// <summary>
/// Gets a list of additional records.
/// </summary>
public IReadOnlyCollection<DnsResourceRecord> Additionals { get; }
/// <summary>
/// Gets a list of all answers, addtional and authority records.
/// </summary>
public IReadOnlyCollection<DnsResourceRecord> AllRecords
{
get
{
return Answers.Concat(Additionals).Concat(Authorities).ToArray();
}
}
/// <summary>
/// Gets a list of answer records.
/// </summary>
public IReadOnlyCollection<DnsResourceRecord> Answers { get; }
/// <summary>
/// Gets a list of authority records.
/// </summary>
public IReadOnlyCollection<DnsResourceRecord> Authorities { get; }
/// <summary>
/// Returns a string value representing the error response code in case an error occured, otherwise empty.
/// </summary>
public string ErrorMessage => HasError ? DnsResponseCodeText.GetErrorText(Header.ResponseCode) : string.Empty;
/// <summary>
/// A flag indicating if the header contains a response codde other than <see cref="DnsResponseCode.NoError"/>.
/// </summary>
public bool HasError => Header?.ResponseCode != DnsResponseCode.NoError;
/// <summary>
/// Gets the header of the response.
/// </summary>
public DnsResponseHeader Header { get; }
/// <summary>
/// Gets the list of questions.
/// </summary>
public IReadOnlyCollection<DnsQuestion> Questions { get; }
/// <summary>
/// Creates a new instace of <see cref="DnsQueryResponse"/>.
/// </summary>
/// <see cref="DnsResponseMessage"/>
public DnsQueryResponse(
DnsResponseHeader header,
IReadOnlyCollection<DnsQuestion> questions,
IReadOnlyCollection<DnsResourceRecord> answers,
IReadOnlyCollection<DnsResourceRecord> additionals,
IReadOnlyCollection<DnsResourceRecord> authorities)
{
if (header == null) throw new ArgumentNullException(nameof(header));
if (questions == null) throw new ArgumentNullException(nameof(questions));
if (answers == null) throw new ArgumentNullException(nameof(answers));
if (additionals == null) throw new ArgumentNullException(nameof(additionals));
if (authorities == null) throw new ArgumentNullException(nameof(authorities));
Header = header;
Questions = questions;
Answers = answers;
Additionals = additionals;
Authorities = authorities;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var response = obj as DnsQueryResponse;
if (response == null)
{
return false;
}
return
Header.ToString().Equals(response.Header.ToString())
&& string.Join("", Questions).Equals(string.Join("", response.Questions))
&& string.Join("", AllRecords).Equals(string.Join("", response.AllRecords));
}
/// <inheritdoc />
public override int GetHashCode()
{
if (!_hashCode.HasValue)
{
_hashCode = (Header.ToString() + string.Join("", Questions) + string.Join("", AllRecords)).GetHashCode();
}
return _hashCode.Value;
}
}
}

View File

@ -0,0 +1,40 @@
using System;
namespace DnsClient
{
public class DnsQuestion
{
public DnsName QueryName { get; }
public QueryClass QuestionClass { get; }
public QueryType QuestionType { get; }
public DnsQuestion(string queryName, QueryType questionType, QueryClass questionClass)
: this(new DnsName(queryName), questionType, questionClass)
{
}
public DnsQuestion(DnsName queryName, QueryType questionType, QueryClass questionClass)
{
if (queryName == null)
{
throw new ArgumentNullException(nameof(queryName));
}
QueryName = queryName;
QuestionType = questionType;
QuestionClass = questionClass;
}
public override string ToString()
{
return ToString(0);
}
public string ToString(int offset = -32)
{
return string.Format("{0,"+offset+"} \t{1} \t{2}", QueryName, QuestionClass, QuestionType);
}
}
}

View File

@ -0,0 +1,75 @@
namespace DnsClient
{
public class DnsRequestHeader
{
public const int HeaderLength = 12;
private ushort _flags = 0;
public ushort RawFlags => _flags;
public DnsHeaderFlag HeaderFlags
{
get
{
return (DnsHeaderFlag)_flags;
}
set
{
_flags &= (ushort)~(DnsHeaderFlag.IsCheckingDisabled);
_flags &= (ushort)~(DnsHeaderFlag.IsAuthenticData);
_flags &= (ushort)~(DnsHeaderFlag.FutureUse);
_flags &= (ushort)~(DnsHeaderFlag.HasQuery);
_flags &= (ushort)~(DnsHeaderFlag.HasAuthorityAnswer);
_flags &= (ushort)~(DnsHeaderFlag.ResultTruncated);
_flags &= (ushort)~(DnsHeaderFlag.RecursionDesired);
_flags &= (ushort)~(DnsHeaderFlag.RecursionAvailable);
_flags |= (ushort)value;
}
}
public int Id { get; set; }
public DnsOpCode OpCode
{
get
{
return (DnsOpCode)((DnsHeader.OPCODE_MASK & _flags) >> DnsHeader.OPCODE_SHIFT);
}
set
{
_flags &= (ushort)~(DnsHeader.OPCODE_MASK);
_flags |= (ushort)(((ushort)value << DnsHeader.OPCODE_SHIFT) & DnsHeader.OPCODE_MASK);
}
}
public int QuestionCount { get; set; }
public bool UseRecursion
{
get { return (HeaderFlags | DnsHeaderFlag.RecursionDesired) != 0; }
set
{
HeaderFlags |= DnsHeaderFlag.RecursionDesired;
}
}
public DnsRequestHeader(int id, int questionCount, DnsOpCode queryKind)
: this(id, questionCount, true, queryKind)
{
}
public DnsRequestHeader(int id, int questionCount, bool useRecursion, DnsOpCode queryKind)
{
Id = id;
QuestionCount = questionCount;
OpCode = queryKind;
UseRecursion = useRecursion;
}
public override string ToString()
{
return $"{Id} - Qs: {QuestionCount} Recursion: {UseRecursion} OpCode: {OpCode}";
}
}
}

View File

@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
namespace DnsClient
{
/*
* Reference RFC6895#section-2.3
*/
// <summary>
/// RFCs 1035, 2136, 2671, 2845, 2930, 4635.
/// </summary>
public enum DnsResponseCode : ushort
{
/// <summary>
/// RFC 1035.
/// No error condition
/// </summary>
NoError = 0,
/// <summary>
/// RFC 1035.
/// Format error. The name server was unable to interpret the query.
/// </summary>
FormatError = 1,
/// <summary>
/// RFC 1035.
/// Server failure. The name server was unable to process this query due to a problem with the name server.
/// </summary>
ServerFailure = 2,
/// <summary>
/// RFC 1035.
/// Name Error. Meaningful only for responses from an authoritative name server,
/// this code signifies that the domain name referenced in the query does not exist.
/// </summary>
NotExistentDomain = 3,
/// <summary>
/// RFC 1035.
/// Not Implemented. The name server does not support the requested kind of query.
/// </summary>
NotImplemented = 4,
/// <summary>
/// RFC 1035.
/// Refused. The name server refuses to perform the specified operation for policy reasons.
/// For example, a name server may not wish to provide the information to the particular requester,
/// or a name server may not wish to perform a particular operation (e.g., zone transfer) for particular data.
/// </summary>
Refused = 5,
/// <summary>
/// RFC 2136.
/// Name Exists when it should not.
/// </summary>
ExistingDomain = 6,
/// <summary>
/// RFC 2136.
/// Resource record set exists when it should not.
/// </summary>
ExistingResourceRecordSet = 7,
/// <summary>
/// RFC 2136.
/// Resource record set that should exist but does not.
/// </summary>
MissingResourceRecordSet = 8,
/// <summary>
/// RFC 2136 / RFC2845
/// Server Not Authoritative for zone / Not Authorized.
/// </summary>
NotAuthorized = 9,
/// <summary>
/// RFC 2136.
/// Name not contained in zone.
/// </summary>
NotZone = 10,
/// <summary>
/// RFCs 2671 / 2845.
/// Bad OPT Version or TSIG Signature Failure.
/// </summary>
BadVersionOrBadSignature = 16,
/// <summary>
/// RFC 2845.
/// Key not recognized.
/// </summary>
BadKey = 17,
/// <summary>
/// RFC 2845.
/// Signature out of time window.
/// </summary>
BadTime = 18,
/// <summary>
/// RFC 2930.
/// Bad TKEY Mode.
/// </summary>
BadMode = 19,
/// <summary>
/// RFC 2930.
/// Duplicate key name.
/// </summary>
BadName = 20,
/// <summary>
/// RFC 2930.
/// Algorithm not supported.
/// </summary>
BadAlgorithm = 21,
/// <summary>
/// RFC 4635.
/// BADTRUNC - Bad Truncation.
/// </summary>
BadTruncation = 22,
/// <summary>
/// RFC 7873
/// Bad/missing Server Cookie
/// </summary>
BadCookie = 23,
/// <summary>
/// Unknown error.
/// </summary>
Unassigned = 666
}
public static class DnsResponseCodeText
{
internal const string BADALG = "Algorithm not supported";
internal const string BADCOOKIE = "Bad/missing Server Cookie";
internal const string BADKEY = "Key not recognized";
internal const string BADMODE = "Bad TKEY Mode";
internal const string BADNAME = "Duplicate key name";
internal const string BADSIG = "TSIG Signature Failure";
internal const string BADTIME = "Signature out of time window";
internal const string BADTRUNC = "Bad Truncation";
internal const string BADVERS = "Bad OPT Version";
internal const string FormErr = "Format Error";
internal const string NoError = "No Error";
internal const string NotAuth = "Server Not Authoritative for zone or Not Authorized";
internal const string NotImp = "Not Implemented";
internal const string NotZone = "Name not contained in zone";
internal const string NXDomain = "Non-Existent Domain";
internal const string NXRRSet = "RR Set that should exist does not";
internal const string Refused = "Query Refused";
internal const string ServFail = "Server Failure";
internal const string Unassigned = "Unknown Error";
internal const string YXDomain = "Name Exists when it should not";
internal const string YXRRSet = "RR Set Exists when it should not";
private static readonly Dictionary<DnsResponseCode, string> errors = new Dictionary<DnsResponseCode, string>()
{
{ DnsResponseCode.NoError, DnsResponseCodeText.NoError },
{ DnsResponseCode.FormatError, DnsResponseCodeText.FormErr },
{ DnsResponseCode.ServerFailure, DnsResponseCodeText.ServFail },
{ DnsResponseCode.NotExistentDomain, DnsResponseCodeText.NXDomain },
{ DnsResponseCode.NotImplemented, DnsResponseCodeText.NotImp },
{ DnsResponseCode.Refused, DnsResponseCodeText.Refused },
{ DnsResponseCode.ExistingDomain, DnsResponseCodeText.YXDomain },
{ DnsResponseCode.ExistingResourceRecordSet, DnsResponseCodeText.YXRRSet },
{ DnsResponseCode.MissingResourceRecordSet, DnsResponseCodeText.NXRRSet },
{ DnsResponseCode.NotAuthorized, DnsResponseCodeText.NotAuth },
{ DnsResponseCode.NotZone, DnsResponseCodeText.NotZone },
{ DnsResponseCode.BadVersionOrBadSignature, DnsResponseCodeText.BADVERS },
{ DnsResponseCode.BadKey, DnsResponseCodeText.BADKEY },
{ DnsResponseCode.BadTime, DnsResponseCodeText.BADTIME },
{ DnsResponseCode.BadMode, DnsResponseCodeText.BADMODE },
{ DnsResponseCode.BadName, DnsResponseCodeText.BADNAME },
{ DnsResponseCode.BadAlgorithm, DnsResponseCodeText.BADALG },
{ DnsResponseCode.BadTruncation, DnsResponseCodeText.BADTRUNC },
{ DnsResponseCode.BadCookie, DnsResponseCodeText.BADCOOKIE },
};
public static string GetErrorText(DnsResponseCode code)
{
if (!errors.ContainsKey(code))
{
return Unassigned;
}
return errors[code];
}
}
public class DnsResponseException : Exception
{
public DnsResponseCode Code { get; }
public string DnsError { get; }
/// <summary>
/// Creates an instance of <see cref="DnsResponseException"/> with <see cref="DnsResponseCode.Unassigned"/>.
/// </summary>
public DnsResponseException() : base(DnsResponseCodeText.Unassigned)
{
Code = DnsResponseCode.Unassigned;
DnsError = DnsResponseCodeText.GetErrorText(Code);
}
/// <summary>
/// Creates an instance of <see cref="DnsResponseException"/> with <see cref="DnsResponseCode.Unassigned"/>
/// and a custom message.
/// </summary>
public DnsResponseException(string message) : base(message)
{
Code = DnsResponseCode.Unassigned;
DnsError = DnsResponseCodeText.GetErrorText(Code);
}
/// <summary>
/// Creates an instance of <see cref="DnsResponseException"/> with
/// the standard error text for this <paramref name="code"/>.
/// </summary>
public DnsResponseException(DnsResponseCode code) : base(DnsResponseCodeText.GetErrorText(code))
{
Code = code;
DnsError = DnsResponseCodeText.GetErrorText(Code);
}
/// <summary>
/// Creates an instance of <see cref="DnsResponseException"/> with <see cref="DnsResponseCode.Unassigned"/>
/// and a custom message.
/// </summary>
public DnsResponseException(string message, Exception innerException) : base(message, innerException)
{
Code = DnsResponseCode.Unassigned;
DnsError = DnsResponseCodeText.GetErrorText(Code);
}
/// <summary>
/// Creates an instance of <see cref="DnsResponseException"/> with a custom message
/// and the given <paramref name="code"/>.
/// </summary>
public DnsResponseException(DnsResponseCode code, string message) : base(message)
{
Code = code;
DnsError = DnsResponseCodeText.GetErrorText(Code);
}
/// <summary>
/// Creates an instance of <see cref="DnsResponseException"/> with a custom message
/// and the given <paramref name="code"/>.
/// </summary>
public DnsResponseException(DnsResponseCode code, string message, Exception innerException) : base(message, innerException)
{
Code = code;
DnsError = DnsResponseCodeText.GetErrorText(Code);
}
}
}

View File

@ -0,0 +1,57 @@
namespace DnsClient
{
public class DnsResponseHeader
{
private readonly ushort _flags = 0;
public int AdditionalCount { get; }
public int AnswerCount { get; }
public bool FutureUse => HasFlag(DnsHeaderFlag.FutureUse);
public bool HasAuthorityAnswer => HasFlag(DnsHeaderFlag.HasAuthorityAnswer);
public DnsHeaderFlag HeaderFlags => (DnsHeaderFlag)_flags;
public int Id { get; }
public bool IsAuthenticData => HasFlag(DnsHeaderFlag.IsAuthenticData);
public bool IsCheckingDisabled => HasFlag(DnsHeaderFlag.IsCheckingDisabled);
public bool HasQuery => HasFlag(DnsHeaderFlag.HasQuery);
public int NameServerCount { get; }
public DnsOpCode OPCode => (DnsOpCode)((DnsHeader.OPCODE_MASK & _flags) >> DnsHeader.OPCODE_SHIFT);
public int QuestionCount { get; }
public bool RecursionAvailable => HasFlag(DnsHeaderFlag.RecursionAvailable);
public DnsResponseCode ResponseCode => (DnsResponseCode)(_flags & DnsHeader.RCODE_MASK);
////ResponseCode {set
////{
//// _flags &= (ushort)~(DnsHeader.RCODE_MASK);
//// _flags |= (ushort)((ushort)value & DnsHeader.RCODE_MASK);
////}}
public bool ResultTruncated => HasFlag(DnsHeaderFlag.ResultTruncated);
public bool RecursionDesired => HasFlag(DnsHeaderFlag.RecursionDesired);
public DnsResponseHeader(int id, ushort flags, int questionCount, int answerCount, int additionalCount, int serverCount)
{
Id = id;
_flags = flags;
QuestionCount = questionCount;
AnswerCount = answerCount;
AdditionalCount = additionalCount;
NameServerCount = serverCount;
}
private bool HasFlag(DnsHeaderFlag flag) => (HeaderFlags & flag) != 0;
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using DnsClient.Protocol;
namespace DnsClient
{
public class DnsUdpMessageHandler : DnsMessageHandler, IDisposable
{
private readonly UdpClient _client = new UdpClient();
private bool _disposedValue = false;
public override bool IsTransientException<T>(T exception)
{
Debug.WriteLine("Check transient {0}.", exception);
if (exception is SocketException) return true;
return false;
}
public override async Task<DnsResponseMessage> QueryAsync(
IPEndPoint server,
DnsRequestMessage request,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
//using (var udpClient = new UdpClient())
//{
var data = GetRequestData(request);
await _client.SendAsync(data, data.Length, server);
var result = await _client.ReceiveAsync();
var response = GetResponseMessage(result.Buffer);
if (request.Header.Id != response.Header.Id)
{
throw new DnsResponseException("Header id missmatch.");
}
return response;
//}
}
protected override void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_client.Dispose();
}
_disposedValue = true;
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DnsClient.Protocol;
namespace DnsClient
{
public class LookupClient : IDisposable
{
private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan s_infiniteTimeout = System.Threading.Timeout.InfiniteTimeSpan;
private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue);
private static ushort _uniqueId = 0;
private readonly ResponseCache _cache = new ResponseCache(true);
private readonly object _endpointLock = new object();
private readonly DnsMessageHandler _messageHandler;
private Queue<EndPointInfo> _endpoints;
private TimeSpan _timeout = s_defaultTimeout;
private bool _disposedValue = false;
/// <summary>
/// Gets the list of configured name servers.
/// </summary>
public IReadOnlyCollection<IPEndPoint> NameServers { get; }
/// <summary>
/// Gets or set a flag indicating if recursion should be enabled for DNS queries.
/// </summary>
public bool Recursion { get; set; } = true;
/// <summary>
/// Gets or sets number of tries to connect to one name server before trying the next one or throwing an exception.
/// </summary>
public int Retries { get; set; } = 5;
/// <summary>
/// Gets or sets a flag indicating if the <see cref="LookupClient"/> should throw an <see cref="DnsResponseException"/>
/// if the returned result contains an error flag other than <see cref="DnsResponseCode.NoError"/>.
/// (The default behavior is <c>False</c>).
/// </summary>
public bool ThrowDnsErrors { get; set; } = false;
/// <summary>
/// Gets or sets timeout in milliseconds.
/// Timeout must be greater than zero and less than <see cref="int.MaxValue"/>.
/// </summary>
public TimeSpan Timeout
{
get { return _timeout; }
set
{
if ((value <= TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_timeout = value;
}
}
/// <summary>
/// Gets or sets a flag indicating if the <see cref="LookupClient"/> should use caching or not.
/// The TTL of cached results is defined by each resource record individually.
/// </summary>
public bool UseCache
{
get
{
return _cache.Enabled;
}
set
{
_cache.Enabled = value;
}
}
/// <summary>
/// Gets or sets a <see cref="TimeSpan"/> which can override the TTL of a resource record in case the
/// TTL of the record is lower than this minimum value.
/// This is useful in cases where the server retruns a zero TTL and the record should be cached for a
/// very short duration anyways.
///
/// This setting gets igonred in case <see cref="UseCache"/> is set to <c>False</c>.
/// </summary>
public TimeSpan? MimimumCacheTimeout
{
get
{
return _cache.MinimumTimout;
}
set
{
_cache.MinimumTimout = value;
}
}
public LookupClient()
: this(NameServer.ResolveNameServers().ToArray())
{
}
public LookupClient(params IPEndPoint[] nameServers)
: this(new DnsUdpMessageHandler(), nameServers)
{
}
public LookupClient(params IPAddress[] nameServers)
: this(
new DnsUdpMessageHandler(),
nameServers.Select(p => new IPEndPoint(p, NameServer.DefaultPort)).ToArray())
{
}
public LookupClient(DnsMessageHandler messageHandler, ICollection<IPEndPoint> nameServers)
{
if (messageHandler == null)
{
throw new ArgumentNullException(nameof(messageHandler));
}
if (nameServers == null || nameServers.Count == 0)
{
throw new ArgumentException("At least one name server must be configured.", nameof(nameServers));
}
NameServers = nameServers.ToArray();
_endpoints = new Queue<EndPointInfo>();
foreach (var server in NameServers)
{
_endpoints.Enqueue(new EndPointInfo(server));
}
_messageHandler = messageHandler;
}
/// <summary>
/// Translates the IPV4 or IPV6 address into an arpa address.
/// </summary>
/// <param name="ip">IP address to get the arpa address form</param>
/// <returns>The mirrored IPV4 or IPV6 arpa address</returns>
public static string GetArpaName(IPAddress ip)
{
var bytes = ip.GetAddressBytes();
Array.Reverse(bytes);
// check IP6
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
// reveresed bytes need to be split into 4 bit parts and separated by '.'
var newBytes = bytes
.SelectMany(b => new[] { (b >> 0) & 0xf, (b >> 4) & 0xf })
.Aggregate(new StringBuilder(), (s, b) => s.Append(b.ToString("x")).Append(".")) + "ip6.arpa.";
return newBytes;
}
else if (ip.AddressFamily == AddressFamily.InterNetwork)
{
// else IP4
return string.Join(".", bytes) + ".in-addr.arpa.";
}
throw new InvalidOperationException("Not a valid IP4 or IP6 address.");
}
public Task<DnsQueryResponse> QueryAsync(string query, QueryType queryType)
=> QueryAsync(query, queryType, CancellationToken.None);
public Task<DnsQueryResponse> QueryAsync(string query, QueryType queryType, CancellationToken cancellationToken)
=> QueryAsync(query, queryType, QueryClass.IN, cancellationToken);
public Task<DnsQueryResponse> QueryAsync(string query, QueryType queryType, QueryClass queryClass)
=> QueryAsync(query, queryType, queryClass, CancellationToken.None);
public Task<DnsQueryResponse> QueryAsync(string query, QueryType queryType, QueryClass queryClass, CancellationToken cancellationToken)
=> QueryAsync(new DnsQuestion(query, queryType, queryClass), cancellationToken);
////public Task<DnsQueryResponse> QueryAsync(params DnsQuestion[] questions)
//// => QueryAsync(CancellationToken.None, questions);
private async Task<DnsQueryResponse> QueryAsync(DnsQuestion question, CancellationToken cancellationToken)
{
if (question == null)
{
throw new ArgumentNullException(nameof(question));
}
var head = new DnsRequestHeader(GetNextUniqueId(), 1, Recursion, DnsOpCode.Query);
var request = new DnsRequestMessage(head, question);
var cacheKey = ResponseCache.GetCacheKey(question);
var result = await _cache.GetOrAdd(cacheKey, async () => await ResolveQueryAsync(request, cancellationToken));
return result;
}
public Task<DnsQueryResponse> QueryReverseAsync(IPAddress ipAddress)
=> QueryReverseAsync(ipAddress, CancellationToken.None);
public Task<DnsQueryResponse> QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken)
{
if (ipAddress == null)
{
throw new ArgumentNullException(nameof(ipAddress));
}
var arpa = GetArpaName(ipAddress);
return QueryAsync(arpa, QueryType.PTR, QueryClass.IN, cancellationToken);
}
private static ushort GetNextUniqueId()
{
if (_uniqueId == ushort.MaxValue || _uniqueId == 0)
{
_uniqueId = (ushort)(new Random()).Next(ushort.MaxValue / 2);
}
return _uniqueId++;
}
// TODO: TCP fallback on truncates
// TODO: most popular DNS servers do not support mulitple queries in one packet, therefore, split it into multiple requests?
//private async Task<DnsQueryResponse> QueryAsync(DnsRequestMessage request, CancellationToken cancellationToken)
//{
//}
private async Task<DnsQueryResponse> ResolveQueryAsync(DnsRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
for (int index = 0; index < NameServers.Count; index++)
{
EndPointInfo serverInfo = null;
lock (_endpointLock)
{
while (_endpoints.Count > 0 && serverInfo == null)
{
serverInfo = _endpoints.Dequeue();
if (serverInfo.IsDisabled)
{
serverInfo = null;
}
else
{
// put it back and then use it..
_endpoints.Enqueue(serverInfo);
}
}
if (serverInfo == null)
{
// let's be optimistic and eable them again, maybe they wher offline one for a while
_endpoints.ToList().ForEach(p => p.IsDisabled = false);
continue;
}
}
var tries = 0;
do
{
tries++;
try
{
DnsResponseMessage response;
var resultTask = _messageHandler.QueryAsync(serverInfo.Endpoint, request, cancellationToken);
if (Timeout != s_infiniteTimeout)
{
response = await resultTask.TimeoutAfter(Timeout);
}
response = await resultTask;
var result = response.AsReadonly;
if (ThrowDnsErrors && result.Header.ResponseCode != DnsResponseCode.NoError)
{
throw new DnsResponseException(result.Header.ResponseCode);
}
return result;
}
catch (DnsResponseException)
{
// occurs only if the option to throw dns exceptions is enabled on the lookup client. (see above).
// lets not mess with the stack
throw;
}
catch (TimeoutException)
{
// do nothing... transient if timeoutAfter timed out
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressFamilyNotSupported)
{
// this socket error might indicate the server endpoint is actually bad and should be ignored in future queries.
serverInfo.IsDisabled = true;
Debug.WriteLine($"Disabling name server {serverInfo.Endpoint}.");
break;
}
catch (Exception ex) when (_messageHandler.IsTransientException(ex))
{
}
catch (Exception ex)
{
var agg = ex as AggregateException;
if (agg != null)
{
agg.Handle(e =>
{
if (e is TimeoutException) return true;
if (_messageHandler.IsTransientException(e)) return true;
return false;
});
throw new DnsResponseException("Unhandled exception", agg.InnerException);
}
throw new DnsResponseException("Unhandled exception", ex);
}
finally
{
// do cleanup stuff or logging?
}
} while (tries <= Retries && !cancellationToken.IsCancellationRequested);
}
throw new DnsResponseException($"No connection could be established to any of the following name servers: {string.Join(", ", NameServers)}.");
}
private class EndPointInfo
{
public IPEndPoint Endpoint { get; }
public bool IsDisabled { get; set; }
public EndPointInfo(IPEndPoint endpoint)
{
Endpoint = endpoint;
IsDisabled = false;
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_messageHandler.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
namespace DnsClient
{
public class NameServer
{
/// <summary>
/// The default DNS server port.
/// </summary>
public const int DefaultPort = 53;
/// <summary>
/// Gets a list of name servers by iterating over the available network interfaces.
/// </summary>
/// <returns>The list of name servers.</returns>
public static ICollection<IPEndPoint> ResolveNameServers()
{
var result = new HashSet<IPEndPoint>();
var adapters = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface networkInterface in
adapters
.Where(p => p.OperationalStatus == OperationalStatus.Up
&& p.NetworkInterfaceType != NetworkInterfaceType.Loopback))
{
foreach (IPAddress dnsAddress in networkInterface
.GetIPProperties()
.DnsAddresses
.Where(i =>
i.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork
|| i.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6))
{
result.Add(new IPEndPoint(dnsAddress, DefaultPort));
}
}
return result.ToArray();
}
}
}

View File

@ -0,0 +1,135 @@
using System;
using System.Net;
using System.Text;
namespace DnsClient.Protocol
{
public class DnsDatagramReader
{
private readonly byte[] _data;
private int _index;
public int Index
{
get
{
return _index;
}
set
{
if (value < 0 || value > _data.Length)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_index = value;
}
}
public DnsDatagramReader(byte[] data, int startIndex = 0)
{
_data = data;
Index = startIndex;
}
/*
https://tools.ietf.org/html/rfc1035#section-3.3:
<character-string> is a single
length octet followed by that number of characters. <character-string>
is treated as binary information, and can be up to 256 characters in
length (including the length octet).
* */
/// <summary>
/// Reads the single length octet and the following characters as ASCII text.
/// </summary>
/// <returns></returns>
public string ReadString()
{
var length = ReadByte();
var result = Encoding.ASCII.GetString(_data, _index, length);
_index += length;
return result;
}
public byte ReadByte()
{
if (_index >= _data.Length)
{
throw new IndexOutOfRangeException("Cannot read byte.");
}
else
{
return _data[_index++];
}
}
public byte[] ReadBytes(int length)
{
if (_data.Length < _index + length)
{
throw new IndexOutOfRangeException($"Cannot read that many bytes: '{length}'.");
}
var result = new byte[length];
Array.Copy(_data, _index, result, 0, length);
_index += length;
return result;
}
/// <summary>
/// Reads an IP address from the next 4 bytes.
/// </summary>
/// <returns>The <see cref="IPAddress"/>.</returns>
/// <exception cref="IndexOutOfRangeException">If there are no 4 bytes to read.</exception>
public IPAddress ReadIPAddress()
{
if (_data.Length < _index + 4)
{
throw new IndexOutOfRangeException("IPAddress expected exactly 4 bytes.");
}
return new IPAddress(ReadBytes(4));
}
public IPAddress ReadIPv6Address()
{
var address = new IPAddress(ReadBytes(8 * 2));
return address;
}
public DnsName ReadName()
{
return DnsName.FromBytes(_data, ref _index);
}
public ushort ReadUInt16()
{
if (_data.Length < Index + 2)
{
throw new IndexOutOfRangeException("Cannot read more data.");
}
var result = BitConverter.ToUInt16(_data, _index);
_index += 2;
return result;
}
public ushort ReadUInt16Reverse()
{
if (_data.Length < Index + 2)
{
throw new IndexOutOfRangeException("Cannot read more data.");
}
byte a = _data[_index++], b = _data[_index++];
return (ushort)(a << 8 | b);
}
public uint ReadUInt32Reverse()
{
return (uint)(ReadUInt16Reverse() << 16 | ReadUInt16Reverse());
}
}
}

View File

@ -0,0 +1,113 @@
using System;
using System.Net;
namespace DnsClient.Protocol
{
public class DnsDatagramWriter
{
private byte[] _buffer;
public byte[] Data => _buffer;
public int Index { get; set; }
public DnsDatagramWriter(int length)
{
_buffer = new byte[length];
}
private DnsDatagramWriter(byte[] data, int newLength)
{
if (data.Length > newLength)
{
throw new ArgumentOutOfRangeException(nameof(newLength));
}
_buffer = new byte[newLength];
Array.Copy(data, _buffer, data.Length);
}
/// <summary>
/// Creates a new writer instance with a new length.
/// </summary>
/// <param name="byLength">The amount of bytes the current buffer should be extended by.</param>
/// <returns>A new writer.</returns>
public void Extend(int byLength)
{
if (byLength <= 0)
{
throw new ArgumentOutOfRangeException(nameof(byLength));
}
var fullLength = byLength + _buffer.Length;
var newBuffer = new byte[fullLength];
Array.Copy(_buffer, 0, newBuffer, 0, _buffer.Length);
//return new DnsDatagramWriter(newBuffer, fullLength) { Offset = Offset };
_buffer = newBuffer;
}
public void SetBytes(byte[] data, int length) => SetBytes(data, 0, Index, length);
public void SetInt(int value) => SetInt(value, Index);
public void SetInt16(short value) => SetInt16(value, Index);
public void SetInt16Network(short value) => SetInt16Network(value, Index);
public void SetUInt32Network(uint value)
{
var bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)value));
SetBytes(bytes, Index, bytes.Length);
}
public void SetIntNetwork(int value) => SetIntNetwork(value, Index);
public void SetUInt16(ushort value) => SetInt16((short)value, Index);
public void SetUInt16Network(ushort value) => SetInt16Network((short)value, Index);
private void SetBytes(byte[] data, int destOffset, int length)
{
SetBytes(data, 0, destOffset, length);
}
private void SetBytes(byte[] data, int dataOffset, int destOffset, int length)
{
if (length + dataOffset > data.Length || length + dataOffset > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (destOffset + dataOffset + length > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(destOffset));
}
Array.ConstrainedCopy(data, dataOffset, _buffer, destOffset, length);
Index = destOffset + length;
}
private void SetInt(int value, int offset)
{
var bytes = BitConverter.GetBytes(value);
SetBytes(bytes, offset, bytes.Length);
}
private void SetInt16(short value, int offset)
{
var bytes = BitConverter.GetBytes(value);
SetBytes(bytes, offset, bytes.Length);
}
private void SetInt16Network(short value, int offset)
{
var bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(value));
SetBytes(bytes, offset, bytes.Length);
}
private void SetIntNetwork(int value, int offset)
{
var bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(value));
SetBytes(bytes, offset, bytes.Length);
}
}
}

View File

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using DnsClient.Protocol.Record;
namespace DnsClient.Protocol
{
public class DnsRecordFactory
{
public static IDictionary<ResourceRecordType, Func<DnsDatagramReader, ResourceRecordInfo, DnsResourceRecord>> s_recordFactory =
new Dictionary<ResourceRecordType, Func<DnsDatagramReader, ResourceRecordInfo, DnsResourceRecord>>();
private readonly DnsDatagramReader _reader;
public DnsRecordFactory(DnsDatagramReader reader)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}
_reader = reader;
}
/*
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* */
public ResourceRecordInfo ReadRecordInfo()
{
return new ResourceRecordInfo(
_reader.ReadName().ToString(), // name
(ResourceRecordType)_reader.ReadUInt16Reverse(),// type
(QueryClass)_reader.ReadUInt16Reverse(), // class
_reader.ReadUInt32Reverse(), // ttl - 32bit!!
_reader.ReadUInt16Reverse()); // RDLength
//reader.ReadBytes(reader.ReadUInt16Reverse())); // rdata
}
public DnsResourceRecord GetRecord(ResourceRecordInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
var oldIndex = _reader.Index;
DnsResourceRecord result;
if (s_recordFactory.ContainsKey(info.RecordType))
{
result = s_recordFactory[info.RecordType](_reader, info);
}
else
{
switch (info.RecordType)
{
case ResourceRecordType.A:
result = ResolveARecord(info);
break;
case ResourceRecordType.NS:
result = ResolveNsRecord(info);
break;
case ResourceRecordType.SOA:
result = ResolveSoaRecord(info);
break;
case ResourceRecordType.PTR:
result = ResolvePtrRecord(info);
break;
case ResourceRecordType.MX:
result = ResolveMXRecord(info);
break;
case ResourceRecordType.TXT:
result = ResolveTXTRecord(info);
break;
case ResourceRecordType.AAAA:
result = ResolveAAAARecord(info);
break;
case ResourceRecordType.SRV:
result = ResolveSrvRecord(info);
break;
default:
// update reader index because we don't read full data for the empty record
_reader.Index += info.RawDataLength;
result = new EmptyRecord(info);
break;
}
}
// sanity check
if (_reader.Index != oldIndex + info.RawDataLength)
{
throw new InvalidOperationException("Record reader index out of sync.");
}
return result;
}
private PtrRecord ResolvePtrRecord(ResourceRecordInfo info)
{
return new PtrRecord(info, _reader.ReadName().ToString());
}
private AAAARecord ResolveAAAARecord(ResourceRecordInfo info)
{
var address = _reader.ReadIPv6Address();
return new AAAARecord(info, address);
}
// default resolver implementation for an A Record
private ARecord ResolveARecord(ResourceRecordInfo info)
{
if (info.RawDataLength != 4)
{
throw new IndexOutOfRangeException($"Reading wrong length for an IP address. Expected 4 found {info.RawDataLength}.");
}
return new ARecord(info, _reader.ReadIPAddress());
}
private MxRecord ResolveMXRecord(ResourceRecordInfo info)
{
var preference = _reader.ReadUInt16Reverse();
var domain = _reader.ReadName();
return new MxRecord(info, preference, domain.ToString());
}
private NsRecord ResolveNsRecord(ResourceRecordInfo info)
{
var name = _reader.ReadName();
return new NsRecord(info, name.ToString());
}
private SoaRecord ResolveSoaRecord(ResourceRecordInfo info)
{
var mName = _reader.ReadName();
var rName = _reader.ReadName();
var serial = _reader.ReadUInt32Reverse();
var refresh = _reader.ReadUInt32Reverse();
var retry = _reader.ReadUInt32Reverse();
var expire = _reader.ReadUInt32Reverse();
var minimum = _reader.ReadUInt32Reverse();
return new SoaRecord(info, mName.ToString(), rName.ToString(), serial, refresh, retry, expire, minimum);
}
private SrvRecord ResolveSrvRecord(ResourceRecordInfo info)
{
var priority = _reader.ReadUInt16Reverse();
var weight = _reader.ReadUInt16Reverse();
var port = _reader.ReadUInt16Reverse();
var target = _reader.ReadName();
return new SrvRecord(info, priority, weight, port, target.ToString());
}
private TxtRecord ResolveTXTRecord(ResourceRecordInfo info)
{
int pos = _reader.Index;
var values = new List<string>();
while ((_reader.Index - pos) < info.RawDataLength)
{
values.Add(_reader.ReadString());
}
return new TxtRecord(info, values.ToArray());
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Linq;
namespace DnsClient.Protocol
{
/// <summary>
/// Represents a simple request message which can be send through <see cref="DnsMessageHandler"/>.
/// </summary>
public class DnsRequestMessage
{
public DnsRequestHeader Header { get; }
public DnsQuestion Question { get; }
public DnsRequestMessage(DnsRequestHeader header, DnsQuestion question)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
if (question == null )
{
throw new ArgumentNullException(nameof(question));
}
if (header.QuestionCount != 1)
{
throw new InvalidOperationException("Header question count and number of questions do not match.");
}
Header = header;
Question = question;
}
}
}

View File

@ -0,0 +1,84 @@
using System;
namespace DnsClient.Protocol
{
public abstract class DnsResourceRecord : ResourceRecordInfo
{
public DnsResourceRecord(ResourceRecordInfo info)
: base(info.QueryName, info.RecordType, info.RecordClass, info.TimeToLive, info.RawDataLength)
{
}
/// <inheritdoc />
public override string ToString()
{
return ToString(0);
}
/// <summary>
/// Same as <c>ToString</c> but offsets the <see cref="ResourceRecordInfo.QueryName"/>
/// by <paramref name="offset"/>.
/// Set the offset to -32 for example to make it print nicely in consols.
/// </summary>
/// <param name="offset">The offset.</param>
/// <returns>A string representing this instance.</returns>
public virtual string ToString(int offset = 0)
{
return string.Format("{0," + offset + "}{1} \t{2} \t{3} \t{4}",
QueryName,
TimeToLive,
RecordClass,
RecordType,
RecordToString());
}
/// <summary>
/// Returns the actual record's value only and not the full object representation.
/// <see cref="ToString(int)"/> uses this to compose the full string value of this instance.
/// </summary>
/// <returns>A string representing this record.</returns>
public abstract string RecordToString();
}
public class ResourceRecordInfo
{
/// <summary>
/// The query name.
/// </summary>
public string QueryName { get; }
/// <summary>
/// Specifies type of resource record.
/// </summary>
public ResourceRecordType RecordType { get; }
/// <summary>
/// Specifies type class of resource record, mostly IN but can be CS, CH or HS .
/// </summary>
public QueryClass RecordClass { get; }
/// <summary>
/// The TTL value for the record set by the server.
/// </summary>
public uint TimeToLive { get; }
/// <summary>
/// Gets the number of bytes for this resource record stored in RDATA
/// </summary>
public int RawDataLength { get; }
public ResourceRecordInfo(string queryName, ResourceRecordType recordType, QueryClass recordClass, uint ttl, int length)
{
if (queryName == null)
{
throw new ArgumentNullException(nameof(queryName));
}
QueryName = queryName;
RecordType = recordType;
RecordClass = recordClass;
TimeToLive = ttl;
RawDataLength = length;
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DnsClient.Protocol
{
/// <summary>
/// A simple response message which gets returned by the <see cref="LookupClient"/>.
/// </summary>
public class DnsResponseMessage
{
private readonly IList<DnsResourceRecord> _additionals = new List<DnsResourceRecord>();
private readonly IList<DnsResourceRecord> _answers = new List<DnsResourceRecord>();
private readonly IList<DnsResourceRecord> _authorities = new List<DnsResourceRecord>();
private readonly DnsResponseHeader _header;
private readonly IList<DnsQuestion> _questions = new List<DnsQuestion>();
/// <summary>
/// Gets the readonly representation of this message which can be returned.
/// </summary>
public DnsQueryResponse AsReadonly
=> new DnsQueryResponse(_header, _questions.ToArray(), _answers.ToArray(), _additionals.ToArray(), _authorities.ToArray());
public DnsResponseHeader Header => _header;
public DnsResponseMessage(DnsResponseHeader header)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
_header = header;
}
public void AddAdditional(DnsResourceRecord record)
{
if (record == null)
{
throw new ArgumentNullException(nameof(record));
}
_additionals.Add(record);
}
public void AddAnswer(DnsResourceRecord record)
{
if (record == null)
{
throw new ArgumentNullException(nameof(record));
}
_answers.Add(record);
}
public void AddAuthority(DnsResourceRecord record)
{
if (record == null)
{
throw new ArgumentNullException(nameof(record));
}
_authorities.Add(record);
}
public void AddQuestion(DnsQuestion question)
{
if (question == null)
{
throw new ArgumentNullException(nameof(question));
}
_questions.Add(question);
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Net;
namespace DnsClient.Protocol.Record
{
/*
3.4.1. A RDATA format
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ADDRESS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
where:
ADDRESS A 32 bit Internet address.
Hosts that have multiple Internet addresses will have multiple A
records.
*
*/
/// <summary>
/// A DNS resource record represending an IP address.
/// Hosts that have multiple Internet addresses will have multiple A records.
/// </summary>
public class ARecord : DnsResourceRecord
{
public IPAddress Address { get; }
public ARecord(ResourceRecordInfo info, IPAddress address) : base(info)
{
if (address == null)
{
throw new ArgumentNullException(nameof(address));
}
Address = address;
}
public override string RecordToString()
{
return Address.ToString();
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Net;
namespace DnsClient.Protocol.Record
{
/* https://tools.ietf.org/html/rfc3596#section-2.2
2.2 AAAA data format
A 128 bit IPv6 address is encoded in the data portion of an AAAA
resource record in network byte order (high-order byte first).
*/
/// <summary>
/// A 128 bit IPv6 address is encoded in the data portion of an AAAA
/// resource record in network byte order(high-order byte first).
/// </summary>
public class AAAARecord : DnsResourceRecord
{
public IPAddress Address { get; }
public AAAARecord(ResourceRecordInfo info, IPAddress address) : base(info)
{
if (address == null)
{
throw new ArgumentNullException(nameof(address));
}
Address = address;
}
public override string RecordToString()
{
return Address.ToString();
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DnsClient.Protocol.Record
{
/* RFC 6844 (https://tools.ietf.org/html/rfc6844#section-5.1)
A CAA RR contains a single property entry consisting of a tag-value
pair. Each tag represents a property of the CAA record. The value
of a CAA property is that specified in the corresponding value field.
A domain name MAY have multiple CAA RRs associated with it and a
given property MAY be specified more than once.
The CAA data field contains one property entry. A property entry
consists of the following data fields:
+0-1-2-3-4-5-6-7-|0-1-2-3-4-5-6-7-|
| Flags | Tag Length = n |
+----------------+----------------+...+---------------+
| Tag char 0 | Tag char 1 |...| Tag char n-1 |
+----------------+----------------+...+---------------+
+----------------+----------------+.....+----------------+
| Value byte 0 | Value byte 1 |.....| Value byte m-1 |
+----------------+----------------+.....+----------------+
Where n is the length specified in the Tag length field and m is the
remaining octets in the Value field (m = d - n - 2) where d is the
length of the RDATA section.
The data fields are defined as follows:
Flags: One octet containing the following fields:
Bit 0, Issuer Critical Flag: If the value is set to '1', the
critical flag is asserted and the property MUST be understood
if the CAA record is to be correctly processed by a certificate
issuer.
A Certification Authority MUST NOT issue certificates for any
Domain that contains a CAA critical property for an unknown or
unsupported property tag that for which the issuer critical
flag is set.
Note that according to the conventions set out in [RFC1035], bit 0
is the Most Significant Bit and bit 7 is the Least Significant
Bit. Thus, the Flags value 1 means that bit 7 is set while a value
of 128 means that bit 0 is set according to this convention.
All other bit positions are reserved for future use.
To ensure compatibility with future extensions to CAA, DNS records
compliant with this version of the CAA specification MUST clear
(set to "0") all reserved flags bits. Applications that interpret
CAA records MUST ignore the value of all reserved flag bits.
Tag Length: A single octet containing an unsigned integer specifying
the tag length in octets. The tag length MUST be at least 1 and
SHOULD be no more than 15.
Tag: The property identifier, a sequence of US-ASCII characters.
Tag values MAY contain US-ASCII characters 'a' through 'z', 'A'
through 'Z', and the numbers 0 through 9. Tag values SHOULD NOT
contain any other characters. Matching of tag values is case
insensitive.
Tag values submitted for registration by IANA MUST NOT contain any
characters other than the (lowercase) US-ASCII characters 'a'
through 'z' and the numbers 0 through 9.
Value: A sequence of octets representing the property value.
Property values are encoded as binary values and MAY employ sub-
formats.
The length of the value field is specified implicitly as the
remaining length of the enclosing Resource Record data field.
* */
/// <summary>
/// Record type 257
/// The Certification Authority Authorization (CAA) DNS Resource Record
/// allows a DNS domain name holder to specify one or more Certification
/// Authorities(CAs) authorized to issue certificates for that domain.
/// CAA Resource Records allow a public Certification Authority to
/// implement additional controls to reduce the risk of unintended
/// certificate mis-issue.This document defines the syntax of the CAA
/// record and rules for processing CAA records by certificate issuers.
/// </summary>
public class CaaRecord
{
}
}

View File

@ -0,0 +1,14 @@
namespace DnsClient.Protocol.Record
{
public class EmptyRecord : DnsResourceRecord
{
public EmptyRecord(ResourceRecordInfo info) : base(info)
{
}
public override string RecordToString()
{
return string.Empty;
}
}
}

View File

@ -0,0 +1,65 @@
using System;
namespace DnsClient.Protocol.Record
{
/*
3.3.9. MX RDATA format
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| PREFERENCE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/ EXCHANGE /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
where:
PREFERENCE A 16 bit integer which specifies the preference given to
this RR among others at the same owner. Lower values
are preferred.
EXCHANGE A <domain-name> which specifies a host willing to act as
a mail exchange for the owner name.
MX records cause type A additional section processing for the host
specified by EXCHANGE. The use of MX RRs is explained in detail in
[RFC-974].
*/
/// <summary>
/// MX records cause type A additional section processing for the host
/// specified by EXCHANGE.The use of MX RRs is explained in detail in
/// [RFC-974].
/// </summary>
public class MxRecord : DnsResourceRecord
{
/// <summary>
/// Gets a 16 bit integer which specifies the preference given to
/// this RR among others at the same owner.
/// Lower values are preferred.
/// </summary>
public ushort Preference { get; }
/// <summary>
/// A <domain-name> which specifies a host willing to act as a mail exchange.
/// </summary>
public string Exchange { get; }
public MxRecord(ResourceRecordInfo info, ushort preference, string domainName)
: base(info)
{
if (string.IsNullOrWhiteSpace(domainName))
{
throw new ArgumentNullException(nameof(domainName));
}
Preference = preference;
Exchange = domainName;
}
public override string RecordToString()
{
return string.Format("{0} {1}", Preference, Exchange);
}
}
}

View File

@ -0,0 +1,51 @@
using System;
namespace DnsClient.Protocol.Record
{
/*
https://tools.ietf.org/html/rfc1035#section-3.3.11:
NS RDATA format
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/ NSDNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
where:
NSDNAME A <domain-name> which specifies a host which should be
authoritative for the specified class and domain.
NS records cause both the usual additional section processing to locate
a type A record, and, when used in a referral, a special search of the
zone in which they reside for glue information.
The NS RR states that the named host should be expected to have a zone
starting at owner name of the specified class. Note that the class may
not indicate the protocol family which should be used to communicate
with the host, although it is typically a strong hint. For example,
hosts which are name servers for either Internet (IN) or Hesiod (HS)
class information are normally queried using IN class protocols.
*/
public class NsRecord : DnsResourceRecord
{
public string NSDName { get; }
internal NsRecord(ResourceRecordInfo info, string name)
: base(info)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
NSDName = name;
}
public override string RecordToString()
{
return NSDName;
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Linq;
namespace DnsClient.Protocol.Record
{
/* RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.3.12)
3.3.12. PTR RDATA format
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/ PTRDNAME /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
where:
PTRDNAME A <domain-name> which points to some location in the
domain name space.
PTR records cause no additional section processing. These RRs are used
in special domains to point to some other location in the domain space.
These records are simple data, and don't imply any special processing
similar to that performed by CNAME, which identifies aliases. See the
description of the IN-ADDR.ARPA domain for an example.
*/
public class PtrRecord : DnsResourceRecord
{
public string PtrDomainName { get; }
internal PtrRecord(ResourceRecordInfo info, string ptrDName)
: base(info)
{
if (string.IsNullOrWhiteSpace(ptrDName))
{
throw new ArgumentNullException(nameof(ptrDName));
}
PtrDomainName = ptrDName;
}
public override string RecordToString()
{
return PtrDomainName;
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DnsClient.Protocol;
using DnsClient.Protocol.Record;
namespace DnsClient
{
public static class RecordCollectionExtension
{
public static IEnumerable<AAAARecord> AaaaRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<AAAARecord>();
}
public static IEnumerable<ARecord> ARecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<ARecord>();
}
public static IEnumerable<CaaRecord> CaaRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<CaaRecord>();
}
public static IEnumerable<NsRecord> NsRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<NsRecord>();
}
public static IEnumerable<DnsResourceRecord> OfRecordType(this IEnumerable<DnsResourceRecord> records, ResourceRecordType type)
{
return records.Where(p => p.RecordType == type);
}
public static IEnumerable<PtrRecord> PtrRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<PtrRecord>();
}
public static IEnumerable<SoaRecord> SoaRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<SoaRecord>();
}
public static IEnumerable<SrvRecord> SrvRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<SrvRecord>();
}
public static IEnumerable<TxtRecord> TxtRecords(this IEnumerable<DnsResourceRecord> records)
{
return records.OfType<TxtRecord>();
}
}
}

View File

@ -0,0 +1,111 @@
namespace DnsClient.Protocol.Record
{
/*
3.3.13. SOA RDATA format
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/ MNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/ RNAME /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| SERIAL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| REFRESH |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RETRY |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| EXPIRE |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| MINIMUM |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
where:
MNAME The <domain-name> of the name server that was the
original or primary source of data for this zone.
RNAME A <domain-name> which specifies the mailbox of the
person responsible for this zone.
SERIAL The unsigned 32 bit version number of the original copy
of the zone. Zone transfers preserve this value. This
value wraps and should be compared using sequence space
arithmetic.
REFRESH A 32 bit time interval before the zone should be
refreshed.
RETRY A 32 bit time interval that should elapse before a
failed refresh should be retried.
EXPIRE A 32 bit time value that specifies the upper limit on
the time interval that can elapse before the zone is no
longer authoritative.
MINIMUM The unsigned 32 bit minimum TTL field that should be
exported with any RR from this zone.
SOA records cause no additional section processing.
All times are in units of seconds.
Most of these fields are pertinent only for name server maintenance
operations. However, MINIMUM is used in all query operations that
retrieve RRs from a zone. Whenever a RR is sent in a response to a
query, the TTL field is set to the maximum of the TTL field from the RR
and the MINIMUM field in the appropriate SOA. Thus MINIMUM is a lower
bound on the TTL field for all RRs in a zone. Note that this use of
MINIMUM should occur when the RRs are copied into the response and not
when the zone is loaded from a master file or via a zone transfer. The
reason for this provison is to allow future dynamic update facilities to
change the SOA RR with known semantics.
*/
public class SoaRecord : DnsResourceRecord
{
public uint Expire { get; }
public uint Minimum { get; }
public string MName { get; }
public uint Refresh { get; }
public uint Retry { get; }
public string RName { get; }
public uint Serial { get; }
public SoaRecord(ResourceRecordInfo info, string mName, string rName, uint serial, uint refresh, uint retry, uint expire, uint minimum)
: base(info)
{
MName = mName;
RName = rName;
Serial = serial;
Refresh = refresh;
Retry = retry;
Expire = expire;
Minimum = minimum;
}
public override string RecordToString()
{
return string.Format(
"{0} {1} {2} {3} {4} {5} {6}",
MName,
RName,
Serial,
Refresh,
Retry,
Expire,
Minimum);
}
}
}

View File

@ -0,0 +1,147 @@
namespace DnsClient.Protocol.Record
{
/* RFC 2782 (https://tools.ietf.org/html/rfc2782)
The format of the SRV RR
Here is the format of the SRV RR, whose DNS type code is 33:
_Service._Proto.Name TTL Class SRV Priority Weight Port Target
(There is an example near the end of this document.)
Service
The symbolic name of the desired service, as defined in Assigned
Numbers [STD 2] or locally. An underscore (_) is prepended to
the service identifier to avoid collisions with DNS labels that
occur in nature.
Some widely used services, notably POP, don't have a single
universal name. If Assigned Numbers names the service
indicated, that name is the only name which is legal for SRV
lookups. The Service is case insensitive.
Proto
The symbolic name of the desired protocol, with an underscore
(_) prepended to prevent collisions with DNS labels that occur
in nature. _TCP and _UDP are at present the most useful values
for this field, though any name defined by Assigned Numbers or
locally may be used (as for Service). The Proto is case
insensitive.
Name
The domain this RR refers to. The SRV RR is unique in that the
name one searches for is not this name; the example near the end
shows this clearly.
TTL
Standard DNS meaning [RFC 1035].
Class
Standard DNS meaning [RFC 1035]. SRV records occur in the IN
Class.
Priority
The priority of this target host. A client MUST attempt to
contact the target host with the lowest-numbered priority it can
reach; target hosts with the same priority SHOULD be tried in an
order defined by the weight field. The range is 0-65535. This
is a 16 bit unsigned integer in network byte order.
Weight
A server selection mechanism. The weight field specifies a
relative weight for entries with the same priority. Larger
weights SHOULD be given a proportionately higher probability of
being selected. The range of this number is 0-65535. This is a
16 bit unsigned integer in network byte order. Domain
administrators SHOULD use Weight 0 when there isn't any server
selection to do, to make the RR easier to read for humans (less
noisy). In the presence of records containing weights greater
than 0, records with weight 0 should have a very small chance of
being selected.
In the absence of a protocol whose specification calls for the
use of other weighting information, a client arranges the SRV
RRs of the same Priority in the order in which target hosts,
specified by the SRV RRs, will be contacted. The following
algorithm SHOULD be used to order the SRV RRs of the same
priority:
To select a target to be contacted next, arrange all SRV RRs
(that have not been ordered yet) in any order, except that all
those with weight 0 are placed at the beginning of the list.
Compute the sum of the weights of those RRs, and with each RR
associate the running sum in the selected order. Then choose a
uniform random number between 0 and the sum computed
(inclusive), and select the RR whose running sum value is the
first in the selected order which is greater than or equal to
the random number selected. The target host specified in the
selected SRV RR is the next one to be contacted by the client.
Remove this SRV RR from the set of the unordered SRV RRs and
apply the described algorithm to the unordered SRV RRs to select
the next target host. Continue the ordering process until there
are no unordered SRV RRs. This process is repeated for each
Priority.
Port
The port on this target host of this service. The range is 0-
65535. This is a 16 bit unsigned integer in network byte order.
This is often as specified in Assigned Numbers but need not be.
Target
The domain name of the target host. There MUST be one or more
address records for this name, the name MUST NOT be an alias (in
the sense of RFC 1034 or RFC 2181). Implementors are urged, but
not required, to return the address record(s) in the Additional
Data section. Unless and until permitted by future standards
action, name compression is not to be used for this field.
A Target of "." means that the service is decidedly not
available at this domain.
*/
/// <summary>
/// The SRV RR allows administrators to use several servers for a single
/// domain, to move services from host to host with little fuss, and to
/// designate some hosts as primary servers for a service and others as
/// backups.
///
/// Clients ask for a specific service/protocol for a specific domain
/// (the word domain is used here in the strict RFC 1034 sense), and get
/// back the names of any available servers.
///
/// Note that where this document refers to "address records", it means A
/// RR's, AAAA RR's, or their most modern equivalent.
/// </summary>
public class SrvRecord : DnsResourceRecord
{
public ushort Port { get; }
public ushort Priority { get; }
public string Target { get; }
public ushort Weight { get; }
public SrvRecord(ResourceRecordInfo info, ushort priority, ushort weigth, ushort port, string target)
: base(info)
{
Priority = priority;
Weight = weigth;
Port = port;
Target = target;
}
public override string RecordToString()
{
return string.Format(
"{0} {1} {2} {3}",
Priority,
Weight,
Port,
Target);
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
namespace DnsClient.Protocol.Record
{
/*
* RFC 1464 https://tools.ietf.org/html/rfc1464
https://tools.ietf.org/html/rfc1035#section-3.3:
<character-string> is a single
length octet followed by that number of characters. <character-string>
is treated as binary information, and can be up to 256 characters in
length (including the length octet).
https://tools.ietf.org/html/rfc1035#section-3.3.14:
TXT RDATA format
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/ TXT-DATA /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
where:
TXT-DATA One or more <character-string>s.
TXT RRs are used to hold descriptive text. The semantics of the text
depends on the domain where it is found.
*/
/// <summary>
/// TXT RRs are used to hold descriptive text. The semantics of the text
/// depends on the domain where it is found.
/// </summary>
public class TxtRecord : DnsResourceRecord
{
/// <summary>
/// The list of TXT values of this TXT RR.
/// </summary>
public IReadOnlyCollection<string> Text { get; }
public TxtRecord(ResourceRecordInfo info, string[] values)
: base(info)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
Text = values;
}
public override string RecordToString()
{
return string.Join(" ", Text).Trim();
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Linq;
namespace DnsClient
{
/* RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.2.4)
* 3.2.4. CLASS values
*
CLASS fields appear in resource records. The following CLASS mnemonics
and values are defined:
IN 1 the Internet
CS 2 the CSNET class (Obsolete - used only for examples in
some obsolete RFCs)
CH 3 the CHAOS class
HS 4 Hesiod [Dyer 87]
*/
/// <summary>
/// CLASS fields appear in resource records.
/// </summary>
public enum QueryClass : short
{
/// <summary>
/// The Internet.
/// </summary>
IN = 1,
/// <summary>
/// The CSNET class (Obsolete - used only for examples in some obsolete RFCs).
/// </summary>
CS = 2,
/// <summary>
/// The CHAOS class.
/// </summary>
CH = 3,
/// <summary>
/// Hesiod [Dyer 87].
/// </summary>
HS = 4
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Linq;
namespace DnsClient
{
/*
* RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.2.3)
* */
/// <summary>
/// QTYPE fields appear in the question part of a query. QTYPES are a superset of TYPEs, hence all TYPEs are valid QTYPEs.
/// </summary>
public enum QueryType : short
{
/// <summary>
/// A host address [RFC1035].
/// </summary>
A = 1,
/// <summary>
/// An authoritative name server [RFC1035].
/// </summary>
NS = 2,
/// <summary>
/// A mail destination (OBSOLETE - use MX) [RFC1035].
/// </summary>
MD = 3,
/// <summary>
/// A mail forwarder (OBSOLETE - use MX) [RFC1035].
/// </summary>
MF = 4,
/// <summary>
/// The canonical name for an alias [RFC1035].
/// </summary>
CNAME = 5,
/// <summary>
/// Marks the start of a zone of authority [RFC1035].
/// </summary>
SOA = 6,
/// <summary>
/// A mailbox domain name (EXPERIMENTAL) [RFC1035]. TODO:impl
/// </summary>
MB = 7,
/// <summary>
/// A mail group member (EXPERIMENTAL) [RFC1035]. TODO:impl
/// </summary>
MG = 8,
/// <summary>
/// A mail rename domain name (EXPERIMENTAL) [RFC1035]. TODO:impl
/// </summary>
MR = 9,
/// <summary>
/// A null RR (EXPERIMENTAL) [RFC1035]. TODO:impl
/// </summary>
NULL = 10,
/// <summary>
/// A well known service description [RFC1035] TODO:impl
/// </summary>
WKS = 11,
/// <summary>
/// A domain name pointer [RFC1035]
/// </summary>
PTR = 12,
/// <summary>
/// Host information [RFC1035] TODO:impl
/// </summary>
HINFO = 13,
/// <summary>
/// Mailbox or mail list information [RFC1035] TODO:impl
/// </summary>
MINFO = 14,
/// <summary>
/// Mail exchange [RFC1035]
/// </summary>
MX = 15,
/// <summary>
/// Text strings [RFC1035]
/// </summary>
TXT = 16,
/// <summary>
/// A IPV6 host address, [RFC3596]
/// </summary>
AAAA = 28,
/// <summary>
/// Location of services [RFC2782]
/// </summary>
SRV = 33,
/// <summary>
/// RRSIG rfc3755. TODO:impl
/// </summary>
RRSIG = 46,
/// <summary>
/// Generic any query *.
/// </summary>
ANY = 255,
CAA = 257,
}
}

View File

@ -0,0 +1,114 @@
using System;
using System.Linq;
namespace DnsClient
{
/*
* RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.2.2)
* */
/// <summary>
/// TYPE fields are used in resource records. Note that these types are a subset of QTYPEs.
/// </summary>
public enum ResourceRecordType : short
{
/// <summary>
/// A host address [RFC1035].
/// </summary>
A = 1,
/// <summary>
/// An authoritative name server [RFC1035].
/// </summary>
NS = 2,
/// <summary>
/// A mail destination (OBSOLETE - use MX) [RFC1035].
/// </summary>
MD = 3,
/// <summary>
/// A mail forwarder (OBSOLETE - use MX) [RFC1035].
/// </summary>
MF = 4,
/// <summary>
/// The canonical name for an alias [RFC1035].
/// </summary>
CNAME = 5,
/// <summary>
/// Marks the start of a zone of authority [RFC1035].
/// </summary>
SOA = 6,
/// <summary>
/// A mailbox domain name (EXPERIMENTAL) [RFC1035].
/// </summary>
MB = 7,
/// <summary>
/// A mail group member (EXPERIMENTAL) [RFC1035].
/// </summary>
MG = 8,
/// <summary>
/// A mail rename domain name (EXPERIMENTAL) [RFC1035].
/// </summary>
MR = 9,
/// <summary>
/// A null RR (EXPERIMENTAL) [RFC1035].
/// </summary>
NULL = 10,
/// <summary>
/// A well known service description [RFC1035]
/// </summary>
WKS = 11,
/// <summary>
/// A domain name pointer [RFC1035]
/// </summary>
PTR = 12,
/// <summary>
/// Host information [RFC1035]
/// </summary>
HINFO = 13,
/// <summary>
/// Mailbox or mail list information [RFC1035]
/// </summary>
MINFO = 14,
/// <summary>
/// Mail exchange [RFC1035]
/// </summary>
MX = 15,
/// <summary>
/// Text strings [RFC1035]
/// </summary>
TXT = 16,
/// <summary>
/// A IPV6 host address, [RFC3596]
/// </summary>
AAAA = 28,
/// <summary>
/// Location of services [RFC2782]
/// </summary>
SRV = 33,
/// <summary>
/// RRSIG rfc3755. TODO:impl
/// </summary>
RRSIG = 46,
/// <summary>
/// TODO:impl
/// </summary>
CAA = 257,
}
}

View File

@ -0,0 +1,154 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace DnsClient
{
internal class ResponseCache
{
private static readonly long CleanupInterval = (long)TimeSpan.FromMinutes(10).TotalMilliseconds;
private ConcurrentDictionary<string, ResponseEntry> _cache = new ConcurrentDictionary<string, ResponseEntry>();
private object _cleanupLock = new object();
private bool _cleanupRunning = false;
private int _lastCleanup = 0;
public int Count => _cache.Count;
public bool Enabled { get; set; }
public TimeSpan? MinimumTimout { get; set; }
public ResponseCache(bool enabled = true, TimeSpan? minimumTimout = null)
{
Enabled = enabled;
MinimumTimout = minimumTimout;
}
public static string GetCacheKey(DnsQuestion question)
{
if (question == null)
{
throw new ArgumentNullException(nameof(question));
}
return question.QueryName + ":" + question.QuestionClass + ":" + question.QuestionType;
}
public async Task<DnsQueryResponse> GetOrAdd(string key, Func<Task<DnsQueryResponse>> create)
{
if (create == null)
{
throw new ArgumentNullException(nameof(create));
}
if (!Enabled)
{
return await create();
}
ResponseEntry entry;
if (_cache.TryGetValue(key, out entry))
{
if (entry.IsExpired)
{
_cache.TryRemove(key, out entry);
}
else
{
return entry.Response;
}
}
var record = await create();
// only cache in case the result is valid and does need caching
if (record != null)
{
var newEntry = CreatedEntry(record);
// respecting minimum expiration value which gets evaluated in CreateEntry
if (newEntry.TTL > 0)
{
_cache.TryAdd(key, newEntry);
StartCleanup();
}
}
return record;
}
private static void DoCleanup(ResponseCache cache)
{
cache._cleanupRunning = true;
foreach (var entry in cache._cache)
{
if (entry.Value.IsExpired)
{
ResponseEntry o;
cache._cache.TryRemove(entry.Key, out o);
}
}
cache._cleanupRunning = false;
}
private ResponseEntry CreatedEntry(DnsQueryResponse response)
{
var entry = new ResponseEntry(response);
// minimum timeout
if (MinimumTimout.HasValue && entry.TTL < MinimumTimout.Value.TotalMilliseconds)
{
entry.TTL = (long)MinimumTimout.Value.TotalMilliseconds;
}
return entry;
}
private void StartCleanup()
{
var currentTicks = Environment.TickCount;
if (!_cleanupRunning && _lastCleanup + CleanupInterval < currentTicks)
{
lock (_cleanupLock)
{
if (!_cleanupRunning && _lastCleanup + CleanupInterval < currentTicks)
{
_lastCleanup = currentTicks;
Task.Factory.StartNew(
state => DoCleanup((ResponseCache)state),
this,
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
}
}
}
}
private class ResponseEntry
{
public bool IsExpired => Environment.TickCount > TicksCreated + TTL;
public DnsQueryResponse Response { get; set; }
public int TicksCreated { get; }
public long TTL { get; set; }
public ResponseEntry(DnsQueryResponse response)
{
var minTtl = response.AllRecords.Min(p => p?.TimeToLive);
Response = response;
TTL = response.HasError || !minTtl.HasValue ? 0 : (int)minTtl.Value * 1000; // ttl is in second, we calculate in millis
TicksCreated = Environment.TickCount;
}
}
}
}

View File

@ -0,0 +1,35 @@
namespace System.Threading.Tasks
{
internal static class TaskExtensions
{
public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
var cts = new CancellationTokenSource();
if (task == await Task.WhenAny(task, Task.Delay((int)timeout.TotalMilliseconds, cts.Token)))
{
cts.Cancel();
await task;
}
else
{
throw new TimeoutException();
}
}
public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
var cts = new CancellationTokenSource();
if (task == await Task.WhenAny(task, Task.Delay((int)timeout.TotalMilliseconds, cts.Token)))
{
cts.Cancel();
return await task;
}
else
{
throw new TimeoutException();
}
}
}
}

View File

@ -0,0 +1,52 @@
{
"version": "1.0.0-*",
"authors": [ "MichaCo" ],
"packOptions": {
"licenseUrl": "https://github.com/MichaCo/DnsClient.NET/blob/master/LICENSE",
"projectUrl": "https://github.com/MichaCo/DnsClient.NET",
"repository": {
"type": "git",
"url": "https://github.com/MichaCo/DnsClient.NET"
},
"summary": "A simple DNS client written in C#.",
"tags": [ "DNS", "Name Server", "CSharp", ".NET", ".NET Core" ]
},
"buildOptions": {
"allowUnsafe": false,
"languageVersion": "csharp6",
"warningsAsErrors": false,
"xmlDoc": true,
"keyFile": "../../Tools/key.snk"
},
"copyright": "Copyright (c) 2016 MichaConrad",
"configurations": {
"Debug": {
"buildOptions": {
"define": [ "DEBUG" ]
}
},
"Release": {
"buildOptions": {
"define": [ "RELEASE" ],
"optimize": true
}
}
},
"dependencies": {
},
"frameworks": {
"netstandard1.3": {
"dependencies": {
"NETStandard.Library": "1.6.0",
"System.Net.NameResolution": "4.0.0",
"System.Net.NetworkInformation": "4.3.0",
"System.Runtime": "4.3.0"
},
"buildOptions": { "define": [ "XPLAT" ] }
},
"net45": {
"frameworkAssemblies": {
}
}
}
}

View File

@ -38,12 +38,12 @@ using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.Security.Permissions;
using System.Text;
using DnDns.Enums;
using DnDns.Records;
using DnDns.Security;
using System.Threading.Tasks;
namespace DnDns.Query
{
@ -54,8 +54,6 @@ namespace DnDns.Query
{
private static Random r = new Random();
private DnsPermission _dnsPermissions;
private int _bytesSent = 0;
private int _socketTimeout = 5000;
@ -80,8 +78,6 @@ namespace DnDns.Query
#region Constructors
public DnsQueryRequest()
{
_dnsPermissions = new DnsPermission(PermissionState.Unrestricted);
// Construct the class with some defaults
_transactionId = (ushort) r.Next();
_flags = 0;
@ -151,16 +147,16 @@ namespace DnDns.Query
/// <param name="queryClass"></param>
/// <param name="protocol"></param>
/// <returns></returns>
public DnsQueryResponse Resolve(string host, NsType queryType, NsClass queryClass, ProtocolType protocol)
public async Task<DnsQueryResponse> ResolveAsync(string host, NsType queryType, NsClass queryClass, ProtocolType protocol)
{
return Resolve(host, queryType, queryClass, protocol, null);
return await ResolveAsync(host, queryType, queryClass, protocol, null);
}
public DnsQueryResponse Resolve(string host, NsType queryType, NsClass queryClass, ProtocolType protocol, TsigMessageSecurityProvider provider)
public async Task<DnsQueryResponse> ResolveAsync(string host, NsType queryType, NsClass queryClass, ProtocolType protocol, TsigMessageSecurityProvider provider)
{
string dnsServer = string.Empty;
// Test for Unix/Linux OS
dnsServer = "8.8.8.8";
/*// Test for Unix/Linux OS
if (Tools.IsPlatformLinuxUnix())
{
dnsServer = Tools.DiscoverUnixDnsServerAddress();
@ -176,9 +172,8 @@ namespace DnDns.Query
if (String.IsNullOrEmpty(dnsServer))
throw new Exception("Couldn't detect local DNS Server.");
return Resolve(dnsServer, host, queryType, queryClass, protocol, provider);
*/
return await ResolveAsync(dnsServer, host, queryType, queryClass, protocol, provider);
}
/// <summary>
@ -194,15 +189,12 @@ namespace DnDns.Query
/// <PermissionSet>
/// <IPermission class="System.Net.DnsPermission, System, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
/// </PermissionSet>
public DnsQueryResponse Resolve(string dnsServer, string host, NsType queryType, NsClass queryClass, ProtocolType protocol, IMessageSecurityProvider messageSecurityProvider)
public async Task<DnsQueryResponse> ResolveAsync(string dnsServer, string host, NsType queryType, NsClass queryClass, ProtocolType protocol, IMessageSecurityProvider messageSecurityProvider)
{
// Do stack walk and Demand all callers have DnsPermission.
_dnsPermissions.Demand();
byte[] bDnsQuery = this.BuildDnsRequest(host, queryType, queryClass, protocol, messageSecurityProvider);
// Connect to DNS server and get the record for the current server.
IPHostEntry ipe = System.Net.Dns.GetHostEntry(dnsServer);
IPHostEntry ipe = await Dns.GetHostEntryAsync(dnsServer);
IPAddress ipa = ipe.AddressList[0];
IPEndPoint ipep = new IPEndPoint(ipa, (int)UdpServices.Domain);
@ -226,7 +218,11 @@ namespace DnDns.Query
}
}
Trace.Assert(recvBytes != null, "Failed to retrieve data from the remote DNS server.");
//Console.Assert(recvBytes != null, "Failed to retrieve data from the remote DNS server.");
if(recvBytes == null)
{
Logging.AddLogMessage(Logging.LoggingType.ERROR, "Failed to retrieve data from the remote DNS server.");
}
DnsQueryResponse dnsQR = new DnsQueryResponse();
@ -238,59 +234,71 @@ namespace DnDns.Query
private byte[] ResolveUdp(byte[] bDnsQuery, IPEndPoint ipep)
{
// UDP messages, data size = 512 octets or less
UdpClient udpClient = new UdpClient();
//UdpClient udpClient = new UdpClient();
Socket udp = new Socket(SocketType.Dgram, ProtocolType.Udp);
byte[] recvBytes = null;
try
{
udpClient.Client.ReceiveTimeout = _socketTimeout;
udpClient.Connect(ipep);
udpClient.Send(bDnsQuery, bDnsQuery.Length);
recvBytes = udpClient.Receive(ref ipep);
udp.ReceiveTimeout = _socketTimeout;
//udpClient.Client.ReceiveTimeout = _socketTimeout;
//udpClient.ConnectAsync(ipep);
udp.Connect(ipep);
//udpClient.Send(bDnsQuery, bDnsQuery.Length);
udp.Send(bDnsQuery);
//recvBytes = udpClient.Receive(ref ipep);
recvBytes = new byte[udp.ReceiveBufferSize];
udp.Receive(recvBytes);
}
finally
{
udpClient.Close();
//udpClient.Close();
}
return recvBytes;
}
private static byte[] ResolveTcp(byte[] bDnsQuery, IPEndPoint ipep)
{
TcpClient tcpClient = new TcpClient();
Socket tcp = new Socket(SocketType.Stream, ProtocolType.Tcp);
//TcpClient tcpClient = new TcpClient();
byte[] recvBytes = null;
try
{
tcpClient.Connect(ipep);
NetworkStream netStream = tcpClient.GetStream();
BinaryReader netReader = new System.IO.BinaryReader(netStream);
netStream.Write(bDnsQuery, 0, bDnsQuery.Length);
tcp.Connect(ipep);
//tcpClient.ConnectAsync(ipep);
//NetworkStream ns = tcp.Receive
//NetworkStream netStream = tcpClient.GetStream();
//BinaryReader netReader = new System.IO.BinaryReader(netStream);
//netStream.Write(bDnsQuery, 0, bDnsQuery.Length);
tcp.Send(bDnsQuery);
// wait until data is avail
while (!netStream.DataAvailable) ;
//while (!netStream.DataAvailable) ;
while (tcp.Available < 1) ;
if (tcpClient.Connected && netStream.DataAvailable)
//if (tcpClient.Connected && netStream.DataAvailable)
if (tcp.Connected && tcp.Available > 1)
{
// Read first two bytes to find out the length of the response
byte[] bLen = new byte[2];
// NOTE: The order of the next two lines matter. Do not reorder
// Array indexes are also intentionally reversed
bLen[1] = (byte)netStream.ReadByte();
bLen[0] = (byte)netStream.ReadByte();
tcp.Receive(bLen, bLen.Length, SocketFlags.None);
//bLen[1] = (byte)netStream.ReadByte();
//bLen[0] = (byte)netStream.ReadByte();
UInt16 length = BitConverter.ToUInt16(bLen, 0);
recvBytes = new byte[length];
netStream.Read(recvBytes, 0, length);
tcp.Receive(recvBytes, length, SocketFlags.None);
//netStream.Read(recvBytes, 0, length);
}
}
finally
{
tcpClient.Close();
} catch (Exception e) {
Logging.AddException(e);
}
return recvBytes;
}
@ -369,7 +377,7 @@ namespace DnDns.Query
memoryStream.Write(data, 0, data.Length);
}
Trace.WriteLine(String.Format("The message bytes: {0}", DnsHelpers.DumpArrayToString(memoryStream.ToArray())));
Logging.AddLogMessage(Logging.LoggingType.INFO, String.Format("The message bytes: {0}", DnsHelpers.DumpArrayToString(memoryStream.ToArray())));
return memoryStream.ToArray();
}

View File

@ -335,4 +335,12 @@ namespace DnDns.Records
#endregion
}
internal class Trace
{
public static void WriteLine(string message)
{
Logging.AddLogMessage(Logging.LoggingType.INFO, message);
}
}
}

View File

@ -39,6 +39,7 @@ using System.Net.NetworkInformation;
using System.Net.Sockets;
using DnDns.Enums;
using System.Runtime.InteropServices;
namespace DnDns
{
@ -66,20 +67,11 @@ namespace DnDns
/// Tests to see if this is running on a Linux or Unix Platform
/// </summary>
/// <returns>true if unix or linux is detected</returns>
//public static bool IsPlatformLinuxUnix()
//{
// int p = (int)Environment.OSVersion.Platform;
// Test for Unix / Linux OS
// if (p == 4 || p == 128)
// {
// return true;
// }
// else
// {
// return false;
// }
//}
public static bool IsPlatformLinuxUnix()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false;
return true;
}
/// <summary>
/// Look up the port name for any given port number.
@ -149,36 +141,36 @@ namespace DnDns
}
return string.Empty;
}
//public static IPAddressCollection DiscoverDnsServerAddresses()
//{
// NetworkInterface[] arrNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
public static IPAddressCollection DiscoverDnsServerAddresses()
{
NetworkInterface[] arrNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
// foreach (NetworkInterface objNetworkInterface in arrNetworkInterfaces)
// {
// if (
// (objNetworkInterface.OperationalStatus == OperationalStatus.Up) &&
// (objNetworkInterface.Speed > 0) &&
// (objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback) &&
// (objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Tunnel)
// )
// {
// IPAddressCollection candidate = objNetworkInterface.GetIPProperties().DnsAddresses;
// bool found = false;
// foreach (IPAddress addr in candidate)
// {
// // Make this collection contains IPv4 addresses only
// if (addr.AddressFamily == AddressFamily.InterNetwork)
// found = true;
// }
foreach (NetworkInterface objNetworkInterface in arrNetworkInterfaces)
{
if (
(objNetworkInterface.OperationalStatus == OperationalStatus.Up) &&
(objNetworkInterface.Speed > 0) &&
(objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback) &&
(objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Tunnel)
)
{
IPAddressCollection candidate = objNetworkInterface.GetIPProperties().DnsAddresses;
bool found = false;
foreach (IPAddress addr in candidate)
{
// Make this collection contains IPv4 addresses only
if (addr.AddressFamily == AddressFamily.InterNetwork)
found = true;
}
// if (found)
// return candidate;
// }
// }
if (found)
return candidate;
}
}
return null;
}
// return null;
//}
//public static uint ByteToUInt(byte[] theByte)
//{

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SMTPServer
{
public class MysqlDB : DbContext
{
const string Hostname = "192.168.178.148";
const string DatabaseName = "vmail";
const string Uid = "root";
const string Pwd = "fabian.11";
public DbSet<Domains> Domains { get; set; }
public DbSet<Accounts> Accounts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseMySql(@"Server=" + Hostname + ";database=" + DatabaseName + ";uid=" + Uid + ";pwd=" + Pwd + ";");
}
public class Domains
{
public int Id { get; set; }
public string Domain { get; set; }
public bool IsMail { get; set; }
}
public class Accounts
{
public int Id { get; set; }
public string Name { get; set; }
public int Domain { get; set; }
public string Password { get; set; }
public bool Active { get; set; }
}
}

View File

@ -0,0 +1,71 @@
using DnsClient;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SMTPServer
{
public class GetDNSEntries
{
public static async System.Threading.Tasks.Task<string[]> GetMXRecordAsync(string domain) => await GetMXRecordAsync(domain, 0);
public static async System.Threading.Tasks.Task<string[]> GetMXRecordAsync(string domain, int nr)
{
var lookup = new LookupClient();
var result = await lookup.QueryAsync(domain, QueryType.MX);
var records = result.AllRecords.ToArray();
//ToDo priority sorting
var rec = new string[records.Length];
for(int i = 0; i < records.Length; i++)
{
var str = records[i].RecordToString();
var s = str.Split(' ');
if (s.Length == 1)
{
rec[i] = s[0];
} else if (s.Length == 2)
{
rec[i] = s[1];
} else
{
rec[i] = null;
}
}
return rec;
/*
foreach (var r in result.AllRecords)
{
Console.WriteLine(r.RecordToString());
}*/
}
public static async System.Threading.Tasks.Task<string[]> GetIPAddressFromDnsArrayAsync(string[] dnsnames)
{
var ips = new string[dnsnames.Length];
for(int i = 0; i < dnsnames.Length; i++)
{
ips[i] = await GetIpFromDnsNameAsync(dnsnames[i]);
}
return ips;
}
public static async System.Threading.Tasks.Task<string> GetIpFromDnsNameAsync(string dnsname)
{
var lookup = new LookupClient();
var result = await lookup.QueryAsync(dnsname, QueryType.A);
var records = result.AllRecords.ToArray();
if (records.Length < 1) return null;
return records[0].RecordToString();
}
}
}

View File

@ -0,0 +1,20 @@

// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._Count")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._Mail")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._QueueEntered")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._DestinationIps")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._DestinationDnsNames")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._From")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._To")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._Subject")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._Others")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._Date")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._MessageId")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.Mail._MIME_Version")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "<Ausstehend>", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._DestinationIndex")]

View File

@ -1,7 +1,7 @@
using System;
namespace Logging
{
//namespace Logging
//{
public class Logging
{
private static LoggingType[] _ActiveLoggingTypes { get; set; }
@ -16,15 +16,18 @@ namespace Logging
ALL
}
private static void AddLogMessage(LoggingType messagetype, string message)
public static void AddLogMessage(LoggingType messagetype, string message)
{
if(CheckLoggingType(messagetype)) PrintToConsole(messagetype, message);
}
public static void AddException(Exception e)
{
AddLogMessage(LoggingType.ERROR, e.Message);
}
private static bool CheckLoggingType(LoggingType lt){
if (_ActiveLoggingTypes.Length < 1) {
if (_ActiveLoggingTypes == null ||_ActiveLoggingTypes.Length < 1) {
_ActiveLoggingTypes = new LoggingType[1];
_ActiveLoggingTypes[0] = LoggingType.ALL;
}
@ -47,4 +50,4 @@ namespace Logging
Console.WriteLine("[" + lt.ToString() + "]" + message);
}
}
}
//}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SMTPServer
{
public class MTACommandsDict : Dictionary<MTACommands, String>
{
public MTACommandsDict()
{
Add(MTACommands.HELO, "HELO");
Add(MTACommands.DATA, "DATA");
Add(MTACommands.EXPN, "EXPN");
Add(MTACommands.HELP, "HELP");
Add(MTACommands.MAIL_FROM, "MAIL FROM");
Add(MTACommands.NOOP, "NOOP");
Add(MTACommands.QUIT, "QUIT");
Add(MTACommands.RCPT_TO, "RCPT TO");
Add(MTACommands.RSET, "RSET");
Add(MTACommands.SAML_FROM, "SAML FROM");
Add(MTACommands.SEND_FROM, "SEND FROM");
Add(MTACommands.SOML_FROM, "SOML FROM");
Add(MTACommands.TURN, "TURN");
Add(MTACommands.VERB, "VERB");
Add(MTACommands.VRFY, "VRFY");
Add(MTACommands.ATRN, "ATRN");
Add(MTACommands.AUTH, "AUTH");
Add(MTACommands.BDAT, "BDAT");
Add(MTACommands.EHLO, "EHLO");
Add(MTACommands.ETRN, "ETRN");
Add(MTACommands.RCPT, "RCPT");
Add(MTACommands.SAML, "SAML");
Add(MTACommands.SEND, "SEND");
Add(MTACommands.SOML, "SOML");
Add(MTACommands.STARTTL, "STARTTLs");
Add(MTACommands.AUTH_LOGIN, "AUTH LOGIN");
}
}
public enum MTACommands
{
HELO,
MAIL_FROM,
RCPT_TO,
DATA,
RSET,
QUIT,
HELP,
VRFY,
EXPN,
VERB,
NOOP,
TURN,
SEND_FROM,
SOML_FROM,
SAML_FROM,
ATRN,
AUTH,
BDAT,
EHLO,
ETRN,
RCPT,
SAML,
SEND,
SOML,
STARTTL,
AUTH_LOGIN
}
public enum ResponseCodes
{
C211 = 211,
C214 = 214,
C220 = 220,
C221 = 221,
C250 = 250,
C251 = 251,
C252 = 252,
C253 = 253,
C334 = 334,
C354 = 354,
C355 = 355,
C421 = 421,
C432 = 432,
C450 = 450,
C451 = 451,
C452 = 452,
C453 = 453,
C454 = 454,
C458 = 458,
C459 = 459,
C500 = 500,
C501 = 501,
C502 = 502,
C503 = 503,
C504 = 504,
C521 = 521,
C530 = 530,
C534 = 534,
C538 = 538,
C550 = 550,
C551 = 551,
C552 = 552,
C553 = 553,
C554 = 554
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace SMTPServer
{
class MTASession
{
private static MTACommandsDict MTACommandsDict = new MTACommandsDict();
private ConnectedEventArgs ConnectedEventArgs { get; set; }
private bool Initialized { get; set; }
private string SenderHostname { get; set; }
private bool AuthActive { get; set; }
private string Username { get; set; }
private string Password { get; set; }
private bool Authenticated { get; set; }
private bool DATA { get; set; }
private string Mail_From { get; set; }
public MTASession(ConnectedEventArgs e)
{
ConnectedEventArgs = e;
Thread th = new Thread(new ThreadStart(StartCommunication));
th.Start();
}
private void StartCommunication()
{
ConnectedEventArgs.SendResponse(ResponseCodes.C220, MailTransferAgent.Hostname);
ConnectedEventArgs.NewLine += ConnectedEventArgs_NewLine;
}
private void ConnectedEventArgs_NewLine(string line)
{
if (line.StartsWith(MTACommandsDict[MTACommands.RSET]))
{
// Schauen was alles resettet wird
}
if (!Initialized)
{
if (line.StartsWith(MTACommandsDict[MTACommands.HELO]))
{
ConnectedEventArgs.SendResponse(ResponseCodes.C250, MailTransferAgent.Hostname);
//} else if (line.StartsWith(MTACommandsDict[MTACommands.EHLO])) {
// ConnectedEventArgs.SendResponse(ResponseCodes.C250, MailTransferAgent.Hostname);
}
else
{
ConnectedEventArgs.SendResponse(ResponseCodes.C503, MailTransferAgent.Hostname);
return;
}
Initialized = true;
}
if (line.StartsWith(MTACommandsDict[MTACommands.AUTH_LOGIN]))
{
AuthActive = true;
ConnectedEventArgs.SendResponse(ResponseCodes.C334);
}
else if (line.StartsWith(MTACommandsDict[MTACommands.MAIL_FROM]))
{
var args = line.Split(':');
if (args.Length < 2)
{
ConnectedEventArgs.SendResponse(ResponseCodes.C501);
return;
}
var mailFrom = args[1];
mailFrom.Replace('<', ' ');
}
else if (line.StartsWith(MTACommandsDict[MTACommands.QUIT]))
{
//throw new NotImplementedException();
} else
{
ConnectedEventArgs.SendResponse(ResponseCodes.C500);
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SMTPServer
{
public class Mail
{
public string To { get; private set; }
public string From { get; private set; }
public string Subject { get; private set; }
public string MessageId { get; private set; }
public string Date { get; private set; }
public string MIME_Version { get; private set; }
public string Others { get; private set; }
public Mail(string mail)
{
}
public string GetSenderDomain()
{
return From.Split('@')[1];
}
}
}

View File

@ -0,0 +1,87 @@
using DnsClient;
using SMTPServer.Exceptions;
using System;
using System.Collections.Generic;
namespace SMTPServer
{
public class MailQueue
{
private static List<QueueMail> Mails = null;
public static async void AddToMesssageQueueAsync(Mail mail)
{
var qmail = new QueueMail(mail);
qmail.DestinationDnsNames = await GetDNSEntries.GetMXRecordAsync(mail.GetSenderDomain());
qmail.DestinationIps = await GetDNSEntries.GetIPAddressFromDnsArrayAsync(qmail.DestinationDnsNames);
lock (Mails)
{
if (Mails == null)
{
Mails = new List<QueueMail>();
}
Mails.Add(qmail);
}
}
public static QueueMail GetNextMail()
{
lock (Mails)
{
if(Mails.Count < 1)
{
throw new NoMailsInQueueException();
}
var m = Mails[0];
Mails.Remove(m);
m.Count++;
Mails.Add(m);
return m;
}
}
public static void RemoveMailFromQueue(QueueMail mail)
{
lock (Mails)
{
Mails.Remove(mail);
}
}
public class QueueMail
{
public DateTime QueueEntered { get; private set; }
public int Count { get; set; }
public Mail Mail { get; private set; }
public string[] DestinationIps { get; set; }
public string[] DestinationDnsNames { get; set; }
public int DestinationIndex { get; set; }
public QueueMail (Mail mail)
{
Mail = mail;
QueueEntered = DateTime.Now;
Count = 0;
}
public byte[] GetIpBytes() => GetIpBytes(DestinationIndex);
public byte[] GetIpBytes(int index)
{
var parts = DestinationIps[index].Split('.');
var bytes = new byte[parts.Length];
for(int i = 0; i < parts.Length; i++)
{
bytes[i] = byte.Parse(parts[i]);
}
return bytes;
}
}
}
}

View File

@ -1,7 +1,8 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup Label="Configuration">
<NoWarn>1701;1702;1705;1006</NoWarn>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
@ -12,6 +13,25 @@
<EmbeddedResource Include="**\*.resx" />
</ItemGroup>
<ItemGroup>
<Compile Remove="DNSClient\**" />
<Compile Remove="DNS\**" />
<EmbeddedResource Remove="DNSClient\**" />
<EmbeddedResource Remove="DNS\**" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<PackageReference Include="DnsClient">
<Version>1.0.0-beta-1011</Version>
</PackageReference>
<PackageReference Include="Microsoft.DotNet.Cli.Utils">
<Version>1.0.0-preview2-1-003177</Version>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore">
<Version>5.0.2</Version>
</PackageReference>
@ -31,6 +51,12 @@
<PackageReference Include="Microsoft.NETCore.Targets">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql">
<Version>1.1.0-rtm-10012</Version>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Design">
<Version>1.1.0-rtm-10012</Version>
</PackageReference>
<PackageReference Include="System.IO.FileSystem">
<Version>4.0.1</Version>
</PackageReference>
@ -38,5 +64,6 @@
<Version>4.3.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using SMTPServer.Exceptions;
using System.Net;
namespace SMTPServer
{
class MailTransferAgent
{
public const string Hostname = "mail.fstamm.net";
private static MTACommandsDict MTACommandsDict = new MTACommandsDict();
private static Thread Thread;
private static List<MTASession> Sessions { get; set; }
public static void StartMailTransferAgent()
{
Thread = new Thread(new ThreadStart(MTA));
Thread.Start();
var portListener = new PortListener(25);
portListener.OnConnected += PortListener_OnConnected;
}
private static void PortListener_OnConnected(object source, ConnectedEventArgs e)
{
var session = new MTASession(e);
lock (Sessions)
{
Sessions.Add(session);
}
//throw new NotImplementedException();
}
public static void MTA()
{
while (true)
{
MailQueue.QueueMail mail = null;
try
{
MailQueue.GetNextMail();
}
catch (NoMailsInQueueException)
{
Thread.Sleep(100);
continue;
}
var charset = Encoding.UTF8;
var client = new StartTcpConnection(25, new IPAddress(mail.GetIpBytes()), charset);
if (!client.Connected) ; //ToDo Errorfall
}
}
public static string WaitForResponseCode(ResponseCodes code)
{
return null;
}
}
}

View File

@ -0,0 +1,125 @@
using SMTPServer.Exceptions;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace SMTPServer
{
public delegate void ConnectedEventHandler(object source, ConnectedEventArgs e);
public class ConnectedEventArgs : EventArgs
{
public delegate void ReceivedLineEventHandler(string line);
public event ReceivedLineEventHandler NewLine;
private Encoding Encoding { get; set; }
private string Others { get; set; }
public TcpClient Client { get; private set; }
private NetworkStream Stream { get; set; }
public ConnectedEventArgs(TcpClient client, Encoding encoding)
{
Client = client;
Encoding = encoding;
Stream = client.GetStream();
}
public void SendResponse(ResponseCodes responseCode) => SendResponse(responseCode, "");
public void SendResponse (ResponseCodes responseCode, string args)
{
var text = ((int)responseCode).ToString();
text += " " + args;
var bytes = Encoding.UTF8.GetBytes(text);
Stream.Write(bytes, 0 , bytes.Length);
}
private void ReadClientInput()
{
while (true)
{
if (Stream.DataAvailable)
{
byte[] buffer = new byte[Client.ReceiveBufferSize];
Stream.Read(buffer, 0, buffer.Length);
var str = Encoding.GetString(buffer);
lock (Others)
{
Others += str;
}
CheckLines();
} else
{
Thread.Sleep(10);
}
}
}
private void CheckLines()
{
lock (Others)
{
var line = "";
foreach (char c in Others)
{
if (c.Equals('\n'))
{
Console.WriteLine(line);
NewLine(line);
line = "";
}
else line += c;
}
Others = line;
}
}
}
public class PortListener
{
static List<int> _Ports = null;
TcpListener _Listener;
public event ConnectedEventHandler OnConnected;
public PortListener(int port)
{
if(_Ports == null)
{
_Ports = new List<int>();
}
foreach(var i in _Ports)
{
if (i == port)
{
throw new PortUsedException();
}
}
_Ports.Add(port);
StartListening(new TcpListener(IPAddress.Any, port));
}
private void StartListening(TcpListener listener)
{
listener.Start();
_Listener = listener;
Thread thread = new Thread(new ThreadStart(ListenerAsync));
thread.Start();
}
private async void ListenerAsync()
{
var client = await _Listener.AcceptTcpClientAsync();
Console.WriteLine("Connection");
OnConnected(this, new ConnectedEventArgs(client, Encoding.UTF8));
}
}
}

View File

@ -0,0 +1,119 @@
using DnsClient;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace SMTPServer
{
class Program
{
static void Main(string[] args)
{
/*using (var context = new MysqlDB())
{
context.Database.EnsureCreated();
var domain = new Domains()
{
Domain = "HI",
IsMail = true
};
context.Add(domain);
context.SaveChanges();
var res = context.Domains.Find();
}*/
//Console.WriteLine("####################");
/*var request = new DnsQueryRequest();
var response = request.ResolveAsync("fabianstamm.de", DnDns.Enums.NsType.MX, DnDns.Enums.NsClass.ANY, System.Net.Sockets.ProtocolType.Tcp);
OutputResults(response);*/
//var t = new Thread(new ThreadStart(Test));
//t.Start();
//TestAsync();
var listener = new PortListener(5122);
listener.OnConnected += Listener_OnConnected;
while (true)
{
Thread.Sleep(500);
}
//var listener = new PortListener(5122);
//listener.OnConnected += Listener_OnConnected;
}
private static async void TestAsync()
{
var lookup = new LookupClient();
var result = await lookup.QueryAsync("fabianstamm.de.", QueryType.MX);
foreach (var r in result.AllRecords)
{
Console.WriteLine(r.RecordToString());
}
}
private static void Listener_OnConnected(object source, ConnectedEventArgs e)
{
var session = new MTASession(e);
//throw new NotImplementedException();
}
/*
private static async void OutputResults(Task<DnDns.Query.DnsQueryResponse> res)
{
OutputResults(await res);
}
private static void OutputResults(DnDns.Query.DnsQueryResponse response)
{
Console.WriteLine("Bytes received: " + response.BytesReceived);
Console.WriteLine("Name: " + response.Name);
Console.WriteLine("OpCode: " + response.NsClass);
Console.WriteLine("NsFlags: " + response.NsFlags);
Console.WriteLine("NsType: " + response.NsType);
Console.WriteLine("RCode: " + response.RCode);
Console.WriteLine("OpCode: " + response.OpCode);
// Enumerate the Answer Records
Console.WriteLine("Answers:");
foreach (IDnsRecord record in response.Answers)
{
Console.WriteLine(record.Answer);
Console.WriteLine(" |--- RDATA Field Length: " + record.DnsHeader.DataLength);
Console.WriteLine(" |--- Name: " + record.DnsHeader.Name);
Console.WriteLine(" |--- NS Class: " + record.DnsHeader.NsClass);
Console.WriteLine(" |--- NS Type: " + record.DnsHeader.NsType);
Console.WriteLine(" |--- TTL: " + record.DnsHeader.TimeToLive);
Console.WriteLine();
}
foreach (IDnsRecord record in response.AuthoritiveNameServers)
{
Console.WriteLine(record.Answer);
Console.WriteLine(" |--- RDATA Field Length: " + record.DnsHeader.DataLength);
Console.WriteLine(" |--- Name: " + record.DnsHeader.Name);
Console.WriteLine(" |--- NS Class: " + record.DnsHeader.NsClass);
Console.WriteLine(" |--- NS Type: " + record.DnsHeader.NsType);
Console.WriteLine(" |--- TTL: " + record.DnsHeader.TimeToLive);
Console.WriteLine();
}
foreach (IDnsRecord record in response.AdditionalRRecords)
{
Console.WriteLine(record.Answer);
Console.WriteLine(" |--- RDATA Field Length: " + record.DnsHeader.DataLength);
Console.WriteLine(" |--- Name: " + record.DnsHeader.Name);
Console.WriteLine(" |--- NS Class: " + record.DnsHeader.NsClass);
Console.WriteLine(" |--- NS Type: " + record.DnsHeader.NsType);
Console.WriteLine(" |--- TTL: " + record.DnsHeader.TimeToLive);
Console.WriteLine();
}
}*/
}
}

View File

@ -0,0 +1,7 @@
{
"profiles": {
"SMTPServer": {
"commandName": "Project"
}
}
}

View File

@ -9,9 +9,15 @@ namespace SMTPServer
{
class StartTcpConnection : Socket
{
private List<string> _Lines = new List<string>();
private string _Others { get; set; }
private Encoding _Encoding { get; set; }
private List<string> Lines = new List<string>();
private int LinesAvailable { get
{
return Lines.Count;
}
}
private string Others { get; set; }
private Encoding Encoding { get; set; }
public StartTcpConnection(int port, IPAddress destination, Encoding encoding) : base (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
{
@ -23,13 +29,13 @@ namespace SMTPServer
{
while (true)
{
byte[] buffer = new byte[this.ReceiveBufferSize];
byte[] buffer = new byte[ReceiveBufferSize];
Receive(buffer);
var str = _Encoding.GetString(buffer);
lock (_Others)
var str = Encoding.GetString(buffer);
lock (Others)
{
_Others += str;
Others += str;
}
CheckLines();
@ -38,30 +44,30 @@ namespace SMTPServer
private void CheckLines()
{
lock (_Others)
lock (Others)
{
var line = "";
foreach (char c in _Others)
foreach (char c in Others)
{
if (c.Equals('\n'))
{
lock (_Lines)
lock (Lines)
{
_Lines.Add(line);
Lines.Add(line);
line = "";
}
}
else line += c;
}
_Others = line;
Others = line;
}
}
public string GetLine()
{
lock (_Lines)
lock (Lines)
{
return _Lines[0];
return Lines[0];
}
}
}

View File

@ -1,9 +0,0 @@
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}

View File

@ -1,25 +0,0 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.0.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,24 +0,0 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup Label="Configuration">
<NoWarn>1701;1702;1705;1006</NoWarn>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.0.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,115 +0,0 @@
using System;
using System.Text;
namespace DNSResolver
{
internal static class DnsHelpers
{
private const long Epoch = 621355968000000000;
internal static byte[] CanonicaliseDnsName(string name, bool lowerCase)
{
if (!name.EndsWith("."))
{
name += ".";
}
if (name == ".")
{
return new byte[1];
}
StringBuilder sb = new StringBuilder();
sb.Append('\0');
for (int i = 0, j = 0; i < name.Length; i++, j++)
{
if (lowerCase)
{
sb.Append(char.ToLower(name[i]));
}
else
{
sb.Append(name[i]);
}
if (name[i] == '.')
{
sb[i - j] = (char)(j & 0xff);
j = -1;
}
}
sb[sb.Length - 1] = '\0';
return Encoding.ASCII.GetBytes(sb.ToString());
}
internal static String DumpArrayToString(byte[] bytes)
{
StringBuilder builder = new StringBuilder();
builder.Append("[");
foreach (byte b in bytes)
{
builder.Append(" ");
builder.Append((sbyte)b);
builder.Append(" ");
}
builder.Append("]");
return builder.ToString();
}
/// <summary>
/// Converts a instance of a <see cref="DateTime"/> class to a 48 bit format time since epoch.
/// Epoch is defined as 1-Jan-70 UTC.
/// </summary>
/// <param name="dateTimeToConvert">The <see cref="DateTime"/> instance to convert to DNS format.</param>
/// <param name="timeHigh">The upper 16 bits of time.</param>
/// <param name="timeLow">The lower 32 bits of the time object.</param>
internal static void ConvertToDnsTime(DateTime dateTimeToConvert, out int timeHigh, out long timeLow)
{
long secondsFromEpoch = (dateTimeToConvert.ToUniversalTime().Ticks - Epoch) / 10000000;
timeHigh = (int)(secondsFromEpoch >> 32);
timeLow = (secondsFromEpoch & 0xFFFFFFFFL);
Logging.AddLogMessage(LoggingType.TRACE, "test");
Trace.WriteLine(String.Format("Date: {0}", dateTimeToConvert));
Trace.WriteLine(String.Format("secondsFromEpoch: {0}", secondsFromEpoch));
Trace.WriteLine(String.Format("timeHigh: {0}", timeHigh));
Trace.WriteLine(String.Format("timeLow: {0}", timeLow));
}
/// <summary>
/// Convert from DNS 48 but time format to a <see cref="DateTime"/> instance.
/// </summary>
/// <param name="timeHigh">The upper 16 bits of time.</param>
/// <param name="timeLow">The lower 32 bits of the time object.</param>
/// <returns>The converted date time</returns>
internal static DateTime ConvertFromDnsTime(long timeLow, long timeHigh)
{
long time = (timeHigh << 32) + timeLow;
time = time * 10000000;
time += Epoch;
return new DateTime(time);
}
/// <summary>
/// Convert from DNS 48 but time format to a <see cref="DateTime"/> instance.
/// </summary>
/// <param name="dnsTime">The upper 48 bits of time.</param>
/// <returns>The converted date time</returns>
internal static DateTime ConvertFromDnsTime(long dnsTime)
{
dnsTime = dnsTime * 10000000;
dnsTime += Epoch;
return new DateTime(dnsTime);
}
}
}

View File

@ -1,23 +0,0 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup Label="Configuration">
<NoWarn>1701;1702;1705;1006</NoWarn>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard1.4</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NETStandard.Library">
<Version>1.6</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,50 +0,0 @@
using System;
namespace Logging
{
public class Logging
{
private static LoggingType[] _ActiveLoggingTypes { get; set; }
public enum LoggingType
{
TRACE,
ERROR,
DEBUG,
WARNING,
INFO,
ALL
}
private static void AddLogMessage(LoggingType messagetype, string message)
{
if(CheckLoggingType(messagetype)) PrintToConsole(messagetype, message);
}
private static bool CheckLoggingType(LoggingType lt){
if (_ActiveLoggingTypes.Length < 1) {
_ActiveLoggingTypes = new LoggingType[1];
_ActiveLoggingTypes[0] = LoggingType.ALL;
}
bool found = false;
foreach(LoggingType l in _ActiveLoggingTypes)
{
if (l == lt || l == LoggingType.ALL)
{
found = true;
break;
}
}
return found;
}
private static void PrintToConsole(LoggingType lt, string message)
{
Console.WriteLine("[" + lt.ToString() + "]" + message);
}
}
}

View File

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SMTPServer
{
public class MTACommandsDict : Dictionary<MTACommands, String>
{
public MTACommandsDict()
{
Add(MTACommands.HELO, "HELO");
Add(MTACommands.DATA, "DATA");
Add(MTACommands.EXPN, "EXPN");
Add(MTACommands.HELP, "HELP");
Add(MTACommands.MAIL_FROM, "MAIL FROM");
Add(MTACommands.NOOP, "NOOP");
Add(MTACommands.QUIT, "QUIT");
Add(MTACommands.RCPT_TO, "RCPT TO");
Add(MTACommands.RSET, "RSET");
Add(MTACommands.SAML_FROM, "SAML FROM");
Add(MTACommands.SEND_FROM, "SEND FROM");
Add(MTACommands.SOML_FROM, "SOML FROM");
Add(MTACommands.TURN, "TURN");
Add(MTACommands.VERB, "VERB");
Add(MTACommands.VRFY, "VRFY");
}
}
public enum MTACommands
{
HELO,
MAIL_FROM,
RCPT_TO,
DATA,
RSET,
QUIT,
HELP,
VRFY,
EXPN,
VERB,
NOOP,
TURN,
SEND_FROM,
SOML_FROM,
SAML_FROM
}
}

Some files were not shown because too many files have changed in this diff Show More