Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DNS/DNS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
</PropertyGroup>

<ItemGroup>
<Folder Include="Properties\" />
<None Include="..\README.md" Pack="true" PackagePath="\"/>
<None Include="..\LICENSE.md" Pack="true" PackagePath="\"/>
</ItemGroup>
Expand Down
16 changes: 10 additions & 6 deletions DNS/Protocol/Domain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ public class Domain : IComparable<Domain> {
private const byte ASCII_LOWERCASE_LAST = 122;
private const byte ASCII_UPPERCASE_MASK = 223;

private static readonly char[] LabelSeparator = new[] { '.' };

private byte[][] labels;

public static readonly Domain Empty = FromArray(new byte[] { 0 }, 0);

public static Domain FromString(string domain) {
return new Domain(domain);
}
Expand Down Expand Up @@ -98,7 +102,7 @@ private static bool IsASCIIAlphabet(byte b) {
}

private static int CompareTo(byte a, byte b) {
if(IsASCIIAlphabet(a) && IsASCIIAlphabet(b)) {
if (IsASCIIAlphabet(a) && IsASCIIAlphabet(b)) {
a &= ASCII_UPPERCASE_MASK;
b &= ASCII_UPPERCASE_MASK;
}
Expand All @@ -111,7 +115,7 @@ private static int CompareTo(byte[] a, byte[] b) {

for (int i = 0; i < length; i++) {
int v = CompareTo(a[i], b[i]);
if(v != 0) return v;
if (v != 0) return v;
}

return a.Length - b.Length;
Expand All @@ -125,9 +129,9 @@ public Domain(string[] labels, Encoding encoding) {
this.labels = labels.Select(label => encoding.GetBytes(label)).ToArray();
}

public Domain(string domain) : this(domain.Split('.')) {}
public Domain(string domain) : this(domain.Split(LabelSeparator, StringSplitOptions.RemoveEmptyEntries)) { }

public Domain(string[] labels) : this(labels, Encoding.ASCII) {}
public Domain(string[] labels) : this(labels, Encoding.ASCII) { }

public int Size {
get { return labels.Sum(l => l.Length) + labels.Length + 1; }
Expand All @@ -138,7 +142,7 @@ public byte[] ToArray() {
int offset = 0;

foreach (byte[] label in labels) {
result[offset++] = (byte) label.Length;
result[offset++] = (byte)label.Length;
label.CopyTo(result, offset);
offset += label.Length;
}
Expand All @@ -160,7 +164,7 @@ public int CompareTo(Domain other) {

for (int i = 0; i < length; i++) {
int v = CompareTo(this.labels[i], other.labels[i]);
if(v != 0) return v;
if (v != 0) return v;
}

return this.labels.Length - other.labels.Length;
Expand Down
114 changes: 114 additions & 0 deletions DNS/Protocol/ResourceRecords/OptResourceRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.IO;
using System.Net;
using System.Collections.ObjectModel;

namespace DNS.Protocol.ResourceRecords {
public class OptResourceRecord : BaseResourceRecord {
public ushort UdpPayloadSize => (ushort)Class;

public byte ExtendedResponseCode => (byte)((uint)TimeToLive.TotalSeconds >> 24);

public byte Version => (byte)((uint)TimeToLive.TotalSeconds >> 16);

public ushort Z => (ushort)((uint)TimeToLive.TotalSeconds & 0xffff);

public IReadOnlyList<Option> Options { get; }

private static IResourceRecord Create(ushort udpPayloadSize, params Option[] options) {
var data = new byte[(Option.CodeSize + Option.LengthSize) * options.Length + options.Sum(o => o.Length)];
var offset = 0;

// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
//
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// 0: | OPTION-CODE |
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// 2: | OPTION-LENGTH |
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// 4: | |
// / OPTION-DATA /
// / /
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

foreach (var option in options) {
data[offset++] = (byte)(option.Code >> 8);
data[offset++] = (byte)option.Code;

data[offset++] = (byte)(option.Length >> 8);
data[offset++] = (byte)option.Length;

option.Data.CopyTo(data, offset);
offset += option.Data.Count;
}

return new ResourceRecord(Domain.Empty, data, RecordType.OPT, (RecordClass)udpPayloadSize);
}

public OptResourceRecord(IResourceRecord record) : base(record) {
Options = GetOptions();
}

public OptResourceRecord(ushort udpPayloadSize, params Option[] options) : base(Create(udpPayloadSize, options)) {
Options = options;
}

private IReadOnlyList<Option> GetOptions() {
const int OptionHeaderSize = 4;

if (DataLength == 0)
return Array.Empty<Option>();
if (DataLength < OptionHeaderSize)
throw new InvalidDataException($"The {nameof(Data)} length is less than {OptionHeaderSize}, it is not a valid opt resource record option");

var options = new List<Option>();
var offset = 0;

while (offset < DataLength) {
var optionCode = (ushort)IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(Data, offset));
offset += sizeof(ushort);

var optionLength = (ushort)IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(Data, offset));
offset += sizeof(ushort);

var data = new ArraySegment<byte>(Data, offset, optionLength);
options.Add(new Option(optionCode, data));
offset += optionLength;

var remainBytes = DataLength - offset;
if (remainBytes > 0 && remainBytes < OptionHeaderSize)
throw new InvalidDataException($"The remaining bytes of {nameof(Data)} at offset {offset} less than {OptionHeaderSize}, which is not enough to parse as a valid option");
}

return options;
}
}

public class Option {
internal const int CodeSize = 2;
internal const int LengthSize = 2;

public ushort Code { get; }

public ushort Length => (ushort)Data.Count;

public ReadOnlyCollection<byte> Data { get; }

public Option(ushort code, IEnumerable<byte> data) {
Code = code;
Data = Array.AsReadOnly(data.ToArray());

if (Data.Count > ushort.MaxValue)
throw new InvalidOperationException($"Option data length MUST be <= {ushort.MaxValue}.");
}

internal Option(ushort code, ArraySegment<byte> data) {
Code = code;
Data = new ReadOnlyCollection<byte>(data);
}
}
}
2 changes: 2 additions & 0 deletions DNS/Protocol/ResourceRecords/ResourceRecordFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public static IResourceRecord FromArray(byte[] message, int offset, out int endO
return new TextResourceRecord(record);
case RecordType.SRV:
return new ServiceResourceRecord(record, message, dataOffset);
case RecordType.OPT:
return new OptResourceRecord(record);
default:
return record;
}
Expand Down
Binary file added Tests/Fixtures/ResourceRecord/edns-option-padding
Binary file not shown.
5 changes: 5 additions & 0 deletions Tests/Protocol/ParseDomainTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public void EmptyDomain() {
Assert.Equal("", domain.ToString());
Assert.Equal(1, domain.Size);
Assert.Equal(1, endOffset);

domain = Domain.Empty;
Assert.Equal("", domain.ToString());
Assert.Equal(1, domain.Size);
Assert.Single(domain.ToArray());
}

[Fact]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Protocol/ResourceRecords/ParseResourceRecordTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Xunit;
using DNS.Protocol;
using DNS.Protocol.ResourceRecords;
using System.Linq;

namespace DNS.Tests.Protocol.ResourceRecords {

Expand Down Expand Up @@ -178,5 +179,27 @@ public void SrvResourceRecordWithEmptyDomain() {
Assert.Equal(8080, srv.Port);
Assert.Equal("example.com", srv.Target.ToString());
}

[Fact]
public void OptResourceRecordWithOptionPadding() {
byte[] content = Helper.ReadFixture("ResourceRecord", "edns-option-padding");

ResourceRecord record = ResourceRecord.FromArray(content, 0);

Assert.Equal("", record.Name.ToString());
Assert.Equal(RecordType.OPT, record.Type);
Assert.Equal(31, record.Size);

OptResourceRecord opt = new OptResourceRecord(record);

Assert.Equal(512, opt.UdpPayloadSize);
Assert.Equal(0, opt.ExtendedResponseCode);
Assert.Equal(0, opt.Version);
Assert.Equal(0, opt.Z);
Assert.Equal(1, opt.Options.Count);
Assert.Equal(12, opt.Options[0].Code);
Assert.Equal(16, opt.Options[0].Length);
Assert.True(opt.Options[0].Data.All(x => x == 0));
}
}
}
9 changes: 9 additions & 0 deletions Tests/Protocol/ResourceRecords/SerializeResourceRecordTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,14 @@ public void SrvResourceRecordWithEmptyDomain() {

Assert.Equal(content, srv.ToArray());
}

[Fact]
public void OptResourceRecordWithOptionPadding() {
byte[] content = Helper.ReadFixture("ResourceRecord", "edns-option-padding");

OptResourceRecord opt = new OptResourceRecord(512, new Option(12, new byte[16]));

Assert.Equal(content, opt.ToArray());
}
}
}