diff --git a/misc/Ae.Dns.Console/Program.cs b/misc/Ae.Dns.Console/Program.cs
index a76733f..17d2539 100644
--- a/misc/Ae.Dns.Console/Program.cs
+++ b/misc/Ae.Dns.Console/Program.cs
@@ -1,10 +1,10 @@
using Ae.Dns.Client;
using Ae.Dns.Client.Filters;
using Ae.Dns.Client.Lookup;
-using Ae.Dns.Client.Zone;
using Ae.Dns.Metrics.InfluxDb;
using Ae.Dns.Protocol;
using Ae.Dns.Protocol.Enums;
+using Ae.Dns.Protocol.Zone;
using Ae.Dns.Server;
using Ae.Dns.Server.Http;
using App.Metrics;
@@ -211,7 +211,11 @@ async Task ReportStats(CancellationToken token)
if (dnsConfiguration.UpdateZoneName != null && dnsConfiguration.UpdateZoneFile != null)
{
#pragma warning disable CS0618 // Type or member is obsolete
- updateClient = new DnsUpdateClient(new FileDnsZone(dnsConfiguration.UpdateZoneName, new FileInfo(dnsConfiguration.UpdateZoneFile)));
+ updateClient = new DnsUpdateClient(new DnsZone
+ {
+ Origin = dnsConfiguration.UpdateZoneName,
+ DefaultTtl = TimeSpan.FromHours(1)
+ });
#pragma warning restore CS0618 // Type or member is obsolete
}
diff --git a/src/Ae.Dns.Client/DnsUpdateClient.cs b/src/Ae.Dns.Client/DnsUpdateClient.cs
index a05ac6c..456e150 100644
--- a/src/Ae.Dns.Client/DnsUpdateClient.cs
+++ b/src/Ae.Dns.Client/DnsUpdateClient.cs
@@ -1,7 +1,7 @@
-using Ae.Dns.Client.Zone;
-using Ae.Dns.Protocol;
+using Ae.Dns.Protocol;
using Ae.Dns.Protocol.Enums;
using Ae.Dns.Protocol.Records;
+using Ae.Dns.Protocol.Zone;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -28,26 +28,36 @@ public DnsUpdateClient(IDnsZone dnsZone)
}
///
- public async Task Query(DnsMessage query, CancellationToken token = default)
+ public Task Query(DnsMessage query, CancellationToken token = default)
{
query.EnsureOperationCode(DnsOperationCode.UPDATE);
var hostnames = query.Nameservers.Select(x => x.Host).ToArray();
- void RemoveStaleRecords(ICollection records)
+ void ChangeRecords(ICollection records)
{
foreach (var recordToRemove in records.Where(x => hostnames.Contains(x.Host)).ToArray())
{
records.Remove(recordToRemove);
}
+
+ foreach (var nameserver in query.Nameservers)
+ {
+ records.Add(nameserver);
+ }
};
- if (query.Nameservers.Count > 0 && hostnames.Any(x => x.Last() != _dnsZone.Name) && await _dnsZone.ChangeRecords(RemoveStaleRecords, query.Nameservers, token))
+ if (query.Nameservers.Count > 0 && hostnames.Any(x => x.Last() != _dnsZone.Origin))
{
- return query.CreateAnswerMessage(DnsResponseCode.NoError, ToString());
+ lock (_dnsZone)
+ {
+ ChangeRecords(_dnsZone.Records);
+ }
+
+ return Task.FromResult(query.CreateAnswerMessage(DnsResponseCode.NoError, ToString()));
}
- return query.CreateAnswerMessage(DnsResponseCode.Refused, ToString());
+ return Task.FromResult(query.CreateAnswerMessage(DnsResponseCode.Refused, ToString()));
}
///
diff --git a/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs b/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs
index 37f6d26..fe29703 100644
--- a/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs
+++ b/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs
@@ -1,5 +1,5 @@
-using Ae.Dns.Client.Zone;
-using Ae.Dns.Protocol.Records;
+using Ae.Dns.Protocol.Records;
+using Ae.Dns.Protocol.Zone;
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/Ae.Dns.Client/Zone/FileDnsZone.cs b/src/Ae.Dns.Client/Zone/FileDnsZone.cs
deleted file mode 100644
index 377a9c8..0000000
--- a/src/Ae.Dns.Client/Zone/FileDnsZone.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using Ae.Dns.Protocol.Records;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Ae.Dns.Client.Zone
-{
- ///
- /// Represents a file-backed DNS zone.
- ///
- [Obsolete("Experimental: May change significantly in the future")]
- public sealed class FileDnsZone : IDnsZone
- {
- ///
- /// Construct a new zone, using the specified name (suffix) and file for persistence.
- ///
- ///
- ///
- public FileDnsZone(string name, FileInfo file)
- {
- _name = name;
- _file = file;
- _ = ReloadZone(CancellationToken.None);
- }
-
- private readonly SemaphoreSlim _zoneLock = new SemaphoreSlim(1, 1);
- private readonly IList _records = new List();
- private readonly string _name;
- private readonly FileInfo _file;
-
- ///
- public IEnumerable Records => _records;
-
- ///
- public string Name => _name;
-
- private async Task ReloadZone(CancellationToken token)
- {
- await _zoneLock.WaitAsync(token);
-
- try
- {
- DeserializeZone();
- }
- finally
- {
- _zoneLock.Release();
- }
- }
-
- ///
- public async Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default)
- {
- await _zoneLock.WaitAsync(token);
-
- try
- {
- changeDelegate(_records);
-
- foreach (var recordToAdd in recordsToAdd)
- {
- _records.Add(recordToAdd);
- }
-
- SerializeZone();
- }
- finally
- {
- _zoneLock.Release();
- }
-
- return true;
- }
-
- private void DeserializeZone()
- {
- using var file = _file.Open(FileMode.OpenOrCreate, FileAccess.Read);
- using var reader = new BinaryReader(file);
-
- _records.Clear();
-
- for (int i = 0; i < reader.ReadInt32(); i++)
- {
- var length = reader.ReadInt32();
- var buffer = reader.ReadBytes(length);
-
- var record = new DnsResourceRecord();
-
- int offset = 0;
- record.ReadBytes(buffer, ref offset);
-
- _records.Add(record);
- }
- }
-
- private void SerializeZone()
- {
- using var file = _file.Open(FileMode.OpenOrCreate, FileAccess.Write);
- using var writer = new BinaryWriter(file);
-
- writer.Write(_records.Count);
-
- foreach (var record in _records)
- {
- var buffer = new byte[4096];
-
- int offset = 0;
- record.WriteBytes(buffer, ref offset);
-
- writer.Write(offset);
- writer.Write(buffer, 0, offset);
- }
- }
- }
-}
diff --git a/src/Ae.Dns.Client/Zone/IDnsZone.cs b/src/Ae.Dns.Client/Zone/IDnsZone.cs
deleted file mode 100644
index f33219c..0000000
--- a/src/Ae.Dns.Client/Zone/IDnsZone.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Ae.Dns.Protocol.Records;
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Ae.Dns.Client.Zone
-{
- ///
- /// Provides methods to store data against a DNS zone.
- ///
- [Obsolete("Experimental: May change significantly in the future")]
- public interface IDnsZone
- {
- ///
- /// Add the specified enumerable of to this zone.
- ///
- ///
- ///
- ///
- Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default);
-
- ///
- /// Get all records in the zone.
- ///
- IEnumerable Records { get; }
-
- ///
- /// The name of the zone.
- ///
- string Name { get; }
- }
-}
diff --git a/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs b/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs
index e01cf65..9ccfbfd 100644
--- a/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs
@@ -1,4 +1,6 @@
-namespace Ae.Dns.Protocol.Records
+using Ae.Dns.Protocol.Zone;
+
+namespace Ae.Dns.Protocol.Records
{
///
/// Represents a DNS text resource containing a domain name.
@@ -15,5 +17,17 @@ public sealed class DnsDomainResource : DnsStringResource
///
public override string ToString() => Domain;
+
+ ///
+ public override string ToZone(IDnsZone zone)
+ {
+ return zone.ToFormattedHost(Entries);
+ }
+
+ ///
+ public override void FromZone(IDnsZone zone, string input)
+ {
+ Entries = zone.FromFormattedHost(input);
+ }
}
}
diff --git a/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs b/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs
index 06173e1..71a95ba 100644
--- a/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs
@@ -1,4 +1,5 @@
using Ae.Dns.Protocol.Enums;
+using Ae.Dns.Protocol.Zone;
using System;
using System.Collections.Generic;
using System.Net;
@@ -60,5 +61,18 @@ public void WriteBytes(Memory bytes, ref int offset)
address.CopyTo(bytes.Slice(offset).Span);
offset += address.Length;
}
+
+
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ return IPAddress.ToString();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ IPAddress = IPAddress.Parse(input);
+ }
}
}
diff --git a/src/Ae.Dns.Protocol/Records/DnsMxResource.cs b/src/Ae.Dns.Protocol/Records/DnsMxResource.cs
index 450bedb..fcd3e3a 100644
--- a/src/Ae.Dns.Protocol/Records/DnsMxResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsMxResource.cs
@@ -1,4 +1,5 @@
using Ae.Dns.Protocol.Enums;
+using Ae.Dns.Protocol.Zone;
using System;
namespace Ae.Dns.Protocol.Records
@@ -53,7 +54,21 @@ public override void ReadBytes(ReadOnlyMemory bytes, ref int offset, int l
}
///
- public override string ToString() => Exchange;
+ public override void FromZone(IDnsZone zone, string input)
+ {
+ var parts = input.Split(null);
+ Preference = ushort.Parse(parts[0]);
+ Entries = zone.FromFormattedHost(parts[1]);
+ }
+
+ ///
+ public override string ToZone(IDnsZone zone)
+ {
+ return $"{Preference} {zone.ToFormattedHost(Entries)}";
+ }
+
+ ///
+ public override string ToString() => $"{Preference} {Entries}";
///
public override void WriteBytes(Memory bytes, ref int offset)
diff --git a/src/Ae.Dns.Protocol/Records/DnsOptResource.cs b/src/Ae.Dns.Protocol/Records/DnsOptResource.cs
index c2e0686..9f3cd19 100644
--- a/src/Ae.Dns.Protocol/Records/DnsOptResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsOptResource.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
namespace Ae.Dns.Protocol.Records
{
///
@@ -46,6 +47,18 @@ public void WriteBytes(Memory bytes, ref int offset)
offset += Raw.Length;
}
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string ToString() => $"Raw: {Raw.Length} bytes";
}
diff --git a/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs b/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs
index bb821fa..5e795be 100644
--- a/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs
@@ -1,4 +1,5 @@
using Ae.Dns.Protocol.Enums;
+using Ae.Dns.Protocol.Zone;
using System;
using System.Linq;
@@ -7,7 +8,7 @@ namespace Ae.Dns.Protocol.Records
///
/// Represents metadata around a DNS resource record returned by a DNS server.
///
- public sealed class DnsResourceRecord : IEquatable, IDnsByteArrayReader, IDnsByteArrayWriter
+ public sealed class DnsResourceRecord : IEquatable, IDnsByteArrayReader, IDnsByteArrayWriter, IDnsZoneConverter
{
///
/// The type of DNS query.
@@ -118,5 +119,24 @@ public void WriteBytes(Memory bytes, ref int offset)
// Advance the offset with the size of the resource
offset += resourceSize;
}
+
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ return $"{zone.ToFormattedHost(Host)} {Class} {Type} {Resource.ToZone(zone)}";
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ var parts = input.Split(null, 4);
+
+ Host = zone.FromFormattedHost(parts[0]);
+ Class = (DnsQueryClass)Enum.Parse(typeof(DnsQueryClass), parts[1]);
+ Type = (DnsQueryType)Enum.Parse(typeof(DnsQueryType), parts[2]);
+ TimeToLive = (uint)zone.DefaultTtl.TotalSeconds;
+ Resource = CreateResourceRecord(Type);
+ Resource.FromZone(zone, parts[3]);
+ }
}
}
diff --git a/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs b/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs
index 11d9f2e..617585d 100644
--- a/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Net.Sockets;
using Ae.Dns.Protocol.Records.ServiceBinding;
+using Ae.Dns.Protocol.Zone;
namespace Ae.Dns.Protocol.Records
{
@@ -102,6 +103,18 @@ public void WriteBytes(Memory bytes, ref int offset)
}
}
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string ToString()
{
diff --git a/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs b/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs
index 056f784..df67a21 100644
--- a/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
using System.Linq;
namespace Ae.Dns.Protocol.Records
@@ -89,7 +90,28 @@ public void ReadBytes(ReadOnlyMemory bytes, ref int offset, int length)
}
///
- public override string ToString() => MName;
+ public string ToZone(IDnsZone zone)
+ {
+ return string.Join(" ", new string[] { MName + '.', RName + '.', $"({Serial} {(int)Refresh.TotalSeconds} {(int)Retry.TotalSeconds} {(int)Expire.TotalSeconds} {(int)Minimum.TotalSeconds})" });
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ var parts = input.Split(null, 3);
+ MName = parts[0].Trim('.');
+ RName = parts[1].Trim('.');
+
+ var parts1 = parts[2].Trim(new[] { '(', ')' }).Split(null);
+ Serial = uint.Parse(parts1[0]);
+ Refresh = TimeSpan.FromSeconds(int.Parse(parts1[1]));
+ Retry = TimeSpan.FromSeconds(int.Parse(parts1[2]));
+ Expire = TimeSpan.FromSeconds(int.Parse(parts1[3]));
+ Minimum = TimeSpan.FromSeconds(uint.Parse(parts1[4]));
+ }
+
+ ///
+ public override string ToString() => string.Join(" ", new string[] { MName, RName, $"({Serial} {(int)Refresh.TotalSeconds} {(int)Retry.TotalSeconds} {(int)Expire.TotalSeconds} {(int)Minimum.TotalSeconds})" });
///
public void WriteBytes(Memory bytes, ref int offset)
diff --git a/src/Ae.Dns.Protocol/Records/DnsStringResource.cs b/src/Ae.Dns.Protocol/Records/DnsStringResource.cs
index c76cd22..30f942d 100644
--- a/src/Ae.Dns.Protocol/Records/DnsStringResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsStringResource.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
using System.Linq;
namespace Ae.Dns.Protocol.Records
@@ -51,5 +52,11 @@ public virtual void WriteBytes(Memory bytes, ref int offset)
{
DnsByteExtensions.ToBytes(Entries.ToArray(), bytes, ref offset);
}
+
+ ///
+ public abstract string ToZone(IDnsZone zone);
+
+ ///
+ public abstract void FromZone(IDnsZone zone, string input);
}
}
diff --git a/src/Ae.Dns.Protocol/Records/DnsTextResource.cs b/src/Ae.Dns.Protocol/Records/DnsTextResource.cs
index 1200581..a3ede3e 100644
--- a/src/Ae.Dns.Protocol/Records/DnsTextResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsTextResource.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using Ae.Dns.Protocol.Zone;
namespace Ae.Dns.Protocol.Records
{
@@ -11,6 +11,18 @@ public sealed class DnsTextResource : DnsStringResource
protected override bool CanUseCompression => false;
///
- public override string ToString() => '"' + string.Join("\", \"", Entries.ToArray()) + '"';
+ public override string ToZone(IDnsZone zone)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ ///
+ public override void FromZone(IDnsZone zone, string input)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ ///
+ public override string ToString() => Entries;
}
}
diff --git a/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs b/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs
index cfc51c6..446694f 100644
--- a/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs
+++ b/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
using System.Linq;
namespace Ae.Dns.Protocol.Records
@@ -40,6 +41,18 @@ public void ReadBytes(ReadOnlyMemory bytes, ref int offset, int length)
Raw = DnsByteExtensions.ReadBytes(bytes, length, ref offset);
}
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string ToString() => $"Raw {Raw.Length} bytes";
diff --git a/src/Ae.Dns.Protocol/Records/IDnsResource.cs b/src/Ae.Dns.Protocol/Records/IDnsResource.cs
index baff1fb..d1e2a6e 100644
--- a/src/Ae.Dns.Protocol/Records/IDnsResource.cs
+++ b/src/Ae.Dns.Protocol/Records/IDnsResource.cs
@@ -5,7 +5,7 @@ namespace Ae.Dns.Protocol.Records
///
/// Represents a type of DNS resource.
///
- public interface IDnsResource : IDnsByteArrayWriter
+ public interface IDnsResource : IDnsByteArrayWriter, IDnsZoneConverter
{
///
/// Read from the specified byte array, starting at the specified offset.
diff --git a/src/Ae.Dns.Protocol/Records/IDnsZoneConverter.cs b/src/Ae.Dns.Protocol/Records/IDnsZoneConverter.cs
new file mode 100644
index 0000000..884dd65
--- /dev/null
+++ b/src/Ae.Dns.Protocol/Records/IDnsZoneConverter.cs
@@ -0,0 +1,22 @@
+using Ae.Dns.Protocol.Zone;
+
+namespace Ae.Dns.Protocol.Records
+{
+ ///
+ /// Provides methods to convert to and from the DNS zone file format.
+ ///
+ public interface IDnsZoneConverter
+ {
+ ///
+ /// Write to a zone file string.
+ ///
+ ///
+ public string ToZone(IDnsZone zone);
+ ///
+ /// Read from a zone file string.
+ ///
+ ///
+ ///
+ public void FromZone(IDnsZone zone, string input);
+ }
+}
diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs
index 8d15282..6ea65f0 100644
--- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs
+++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
@@ -74,6 +75,18 @@ public void WriteBytes(Memory bytes, ref int offset)
}
}
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string ToString() => string.Join(",", Entries.Select(x => x.ToString()));
}
diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs
index f46520c..6305740 100644
--- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs
+++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
using System.Collections.Generic;
using System.Linq;
@@ -49,6 +50,18 @@ public void WriteBytes(Memory bytes, ref int offset)
}
}
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string ToString() => string.Join(",", (IEnumerable)Entries);
}
diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs
index aee055f..61d6a09 100644
--- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs
+++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
namespace Ae.Dns.Protocol.Records.ServiceBinding
{
@@ -35,6 +36,18 @@ public bool Equals(SvcUShortParameter? other)
///
public void WriteBytes(Memory bytes, ref int offset) => DnsByteExtensions.ToBytes(Value, bytes, ref offset);
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string ToString() => Value.ToString();
}
diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs
index fb2eb59..929a11c 100644
--- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs
+++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs
@@ -1,4 +1,5 @@
-using System;
+using Ae.Dns.Protocol.Zone;
+using System;
using System.Text;
namespace Ae.Dns.Protocol.Records.ServiceBinding
@@ -45,6 +46,18 @@ public void WriteBytes(Memory bytes, ref int offset)
offset += stringBytes.Length;
}
+ ///
+ public string ToZone(IDnsZone zone)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public void FromZone(IDnsZone zone, string input)
+ {
+ throw new NotImplementedException();
+ }
+
///
public override string? ToString() => Value;
}
diff --git a/src/Ae.Dns.Protocol/Zone/DnsZone.cs b/src/Ae.Dns.Protocol/Zone/DnsZone.cs
new file mode 100644
index 0000000..5a4efb6
--- /dev/null
+++ b/src/Ae.Dns.Protocol/Zone/DnsZone.cs
@@ -0,0 +1,106 @@
+using Ae.Dns.Protocol.Enums;
+using Ae.Dns.Protocol.Records;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Ae.Dns.Protocol.Zone
+{
+ ///
+ /// Represents a file-backed DNS zone.
+ ///
+ [Obsolete("Experimental: May change significantly in the future")]
+ public sealed class DnsZone : IDnsZone
+ {
+ ///
+ public IList Records { get; set; } = new List();
+
+ ///
+ public DnsLabels Origin { get; set; }
+
+ ///
+ public TimeSpan DefaultTtl { get; set; }
+
+ ///
+ public void SerializeZone(StreamWriter writer)
+ {
+ writer.WriteLine($"$ORIGIN {Origin}.");
+ writer.WriteLine($"$TTL {(int)DefaultTtl.TotalSeconds}");
+
+ // Copy the records
+ var records = Records.ToList();
+
+ // Singular SOA must come first
+ var soa = records.Single(x => x.Type == DnsQueryType.SOA);
+
+ records.Remove(soa);
+ records.Insert(0, soa);
+
+ foreach (var record in records)
+ {
+ writer.WriteLine(record.ToZone(this));
+ }
+ }
+
+ ///
+ public void DeserializeZone(StreamReader reader)
+ {
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine()?.Trim() ?? string.Empty;
+
+ if (line.StartsWith("$ORIGIN"))
+ {
+ Origin = line.Substring("$ORIGIN".Length).Trim().Trim('.');
+ continue;
+ }
+
+ if (line.StartsWith("$TTL"))
+ {
+ DefaultTtl = TimeSpan.FromSeconds(int.Parse(line.Substring("$TTL".Length)));
+ continue;
+ }
+
+ var record = new DnsResourceRecord();
+ record.FromZone(this, line);
+ Records.Add(record);
+ }
+
+ }
+
+ ///
+ public string FromFormattedHost(string host)
+ {
+ if (host == "@")
+ {
+ return Origin;
+ }
+ else if (host.EndsWith("."))
+ {
+ return host.Substring(0, host.Length - 1);
+ }
+ else
+ {
+ return host + "." + Origin;
+ }
+ }
+
+ ///
+ public string ToFormattedHost(string host)
+ {
+ if (host == Origin)
+ {
+ return "@";
+ }
+ else if (host.EndsWith(Origin))
+ {
+ return host.Substring(0, host.Length - Origin.ToString().Length - 1);
+ }
+ else
+ {
+ return host + '.';
+ }
+ }
+ }
+}
diff --git a/src/Ae.Dns.Protocol/Zone/IDnsZone.cs b/src/Ae.Dns.Protocol/Zone/IDnsZone.cs
new file mode 100644
index 0000000..bddcd32
--- /dev/null
+++ b/src/Ae.Dns.Protocol/Zone/IDnsZone.cs
@@ -0,0 +1,54 @@
+using Ae.Dns.Protocol.Records;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Ae.Dns.Protocol.Zone
+{
+ ///
+ /// Provides methods to store data against a DNS zone.
+ ///
+ public interface IDnsZone
+ {
+ ///
+ /// Get all records in the zone.
+ ///
+ IList Records { get; set; }
+
+ ///
+ /// The name of the zone.
+ ///
+ DnsLabels Origin { get; set; }
+
+ ///
+ /// The default TTL of the zone.
+ ///
+ TimeSpan DefaultTtl { get; set; }
+
+ ///
+ /// Serialize the zone to a .
+ ///
+ ///
+ void SerializeZone(StreamWriter writer);
+
+ ///
+ /// Deserialize the zone from a .
+ ///
+ ///
+ void DeserializeZone(StreamReader reader);
+
+ ///
+ /// Format a host from the zone file format.
+ ///
+ ///
+ ///
+ string FromFormattedHost(string host);
+
+ ///
+ /// Format a host name for a zone file.
+ ///
+ ///
+ ///
+ string ToFormattedHost(string host);
+ }
+}
diff --git a/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs b/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs
index a2d5333..87c5e5d 100644
--- a/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs
+++ b/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs
@@ -1,28 +1,49 @@
-using Ae.Dns.Client.Lookup;
-using Ae.Dns.Client.Zone;
+#pragma warning disable CS0618 // Type or member is obsolete
+
+using Ae.Dns.Client.Lookup;
+using Ae.Dns.Protocol;
using Ae.Dns.Protocol.Enums;
using Ae.Dns.Protocol.Records;
+using Ae.Dns.Protocol.Zone;
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
-#pragma warning disable CS0618 // Type or member is obsolete
-
namespace Ae.Dns.Tests.Client.Lookup
{
public sealed class DnsZoneLookupTests
{
private sealed class DummyZoneNoRecords : IDnsZone
{
- public IEnumerable Records => Enumerable.Empty();
+ public IList Records { get; set; } = new List();
+ public DnsLabels Origin { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public TimeSpan DefaultTtl { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
- public string Name => throw new NotImplementedException();
+ public Task ChangeRecords(Action> changeDelegate, CancellationToken token = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DeserializeZone(StreamReader reader)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string FromFormattedHost(string host)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SerializeZone(StreamWriter writer)
+ {
+ throw new NotImplementedException();
+ }
- public Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default)
+ public string ToFormattedHost(string host)
{
throw new NotImplementedException();
}
@@ -41,7 +62,7 @@ public void TestLookupNoResults()
private sealed class DummyZoneWithRecords : IDnsZone
{
- public IEnumerable Records => new[]
+ public IList Records { get; set; } = new[]
{
new DnsResourceRecord { Host = "wibble", Class = DnsQueryClass.IN, Type = DnsQueryType.A, Resource = new DnsIpAddressResource { IPAddress = IPAddress.Loopback } },
new DnsResourceRecord { Host = "wibble", Class = DnsQueryClass.IN, Type = DnsQueryType.A, Resource = new DnsIpAddressResource { IPAddress = IPAddress.Broadcast } },
@@ -49,9 +70,30 @@ private sealed class DummyZoneWithRecords : IDnsZone
new DnsResourceRecord { Host = "wibble", Class = DnsQueryClass.IN, Type = DnsQueryType.TEXT, Resource = new DnsTextResource { Entries = "hello2" } },
};
- public string Name => throw new NotImplementedException();
+ public DnsLabels Origin { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public TimeSpan DefaultTtl { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public Task ChangeRecords(Action> changeDelegate, CancellationToken token = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DeserializeZone(StreamReader reader)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string FromFormattedHost(string host)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SerializeZone(StreamWriter writer)
+ {
+ throw new NotImplementedException();
+ }
- public Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default)
+ public string ToFormattedHost(string host)
{
throw new NotImplementedException();
}
diff --git a/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs b/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs
new file mode 100644
index 0000000..2a98aba
--- /dev/null
+++ b/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs
@@ -0,0 +1,184 @@
+using Ae.Dns.Protocol.Enums;
+using Ae.Dns.Protocol.Records;
+using Ae.Dns.Protocol.Zone;
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using Xunit;
+
+namespace Ae.Dns.Tests.Zone
+{
+ [Obsolete]
+ public sealed class DnsZoneTests
+ {
+ [Fact]
+ public void TestRoundTripZone()
+ {
+ var originalZone = new DnsZone();
+
+ originalZone.Origin = "example.com";
+ originalZone.DefaultTtl = TimeSpan.FromSeconds(3600);
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.SOA,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsSoaResource
+ {
+ MName = "ns.example.com",
+ RName = "username.example.com",
+ Serial = 2020091025,
+ Refresh = TimeSpan.FromSeconds(7200),
+ Retry = TimeSpan.FromSeconds(3600),
+ Expire = TimeSpan.FromSeconds(1209600),
+ Minimum = TimeSpan.FromSeconds(3600)
+ }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.NS,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsDomainResource { Entries = "ns" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.NS,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsDomainResource { Entries = "ns.somewhere.example" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.MX,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsMxResource { Preference = 10, Entries = "mail.example.com" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.MX,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsMxResource { Preference = 20, Entries = "mail2.example.com" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.MX,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsMxResource { Preference = 50, Entries = "mail3" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.A,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.1") }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.AAAA,
+ Host = "example.com",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("2001:db8:10::1") }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.A,
+ Host = "ns",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.2") }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.AAAA,
+ Host = "ns",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("2001:db8:10::2") }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.CNAME,
+ Host = "www",
+ TimeToLive = 3600,
+ Resource = new DnsDomainResource { Entries = "example.com" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.CNAME,
+ Host = "wwwtest",
+ TimeToLive = 3600,
+ Resource = new DnsDomainResource { Entries = "www" }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.A,
+ Host = "mail",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.3") }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.A,
+ Host = "mail2",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.4") }
+ });
+
+ originalZone.Records.Add(new DnsResourceRecord
+ {
+ Class = DnsQueryClass.IN,
+ Type = DnsQueryType.A,
+ Host = "mail3",
+ TimeToLive = 3600,
+ Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.5") }
+ });
+
+ var stream = new MemoryStream();
+ using (var sw = new StreamWriter(stream, Encoding.UTF8, 4096, true))
+ {
+ originalZone.SerializeZone(sw);
+ }
+
+ var clonedZone = new DnsZone();
+
+ stream.Position = 0;
+ using (var sr = new StreamReader(stream, Encoding.UTF8, true, 4096))
+ {
+ clonedZone.DeserializeZone(sr);
+ }
+
+ Assert.Equal(originalZone.Records, clonedZone.Records);
+ }
+ }
+}