diff --git a/misc/Ae.Dns.Console/Program.cs b/misc/Ae.Dns.Console/Program.cs index dd49bde..f63446a 100644 --- a/misc/Ae.Dns.Console/Program.cs +++ b/misc/Ae.Dns.Console/Program.cs @@ -219,7 +219,7 @@ async Task ReportStats(CancellationToken token) // Replace the clients with clients for the zone queryClient = new DnsZoneClient(queryClient, dnsZone); - updateClient = new DnsUpdateClient(dnsZone); + updateClient = ActivatorUtilities.CreateInstance(provider, dnsZone); // Add the zone file as a source of automatic reverse lookups staticLookupSources.Add(new DnsZoneLookupSource(dnsZone)); diff --git a/src/Ae.Dns.Client/DnsUpdateClient.cs b/src/Ae.Dns.Client/DnsZoneUpdateClient.cs similarity index 73% rename from src/Ae.Dns.Client/DnsUpdateClient.cs rename to src/Ae.Dns.Client/DnsZoneUpdateClient.cs index ebef6cc..e063042 100644 --- a/src/Ae.Dns.Client/DnsUpdateClient.cs +++ b/src/Ae.Dns.Client/DnsZoneUpdateClient.cs @@ -2,6 +2,7 @@ using Ae.Dns.Protocol.Enums; using Ae.Dns.Protocol.Records; using Ae.Dns.Protocol.Zone; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -15,16 +16,19 @@ namespace Ae.Dns.Client /// Accepts messages with , and stores the record for use elsewhere. /// [Obsolete("Experimental: May change significantly in the future")] - public sealed class DnsUpdateClient : IDnsClient + public sealed class DnsZoneUpdateClient : IDnsClient { + private readonly ILogger _logger; private readonly IDnsZone _dnsZone; /// - /// Create the new using the specified . + /// Create the new using the specified . /// + /// /// - public DnsUpdateClient(IDnsZone dnsZone) + public DnsZoneUpdateClient(ILogger logger, IDnsZone dnsZone) { + _logger = logger; _dnsZone = dnsZone; } @@ -32,7 +36,12 @@ public DnsUpdateClient(IDnsZone dnsZone) public async Task Query(DnsMessage query, CancellationToken token = default) { query.EnsureOperationCode(DnsOperationCode.UPDATE); + query.EnsureQueryType(DnsQueryType.SOA); + query.EnsureHost(_dnsZone.Origin); + _logger.LogInformation("Recieved update query with pre-reqs {PreReqs} and update type {UpdateType}", query.GetZoneUpdatePreRequisite(), query.GetZoneUpdateType()); + + // TODO: this logic is bad var hostnames = query.Nameservers.Select(x => x.Host.ToString()).ToArray(); var addresses = query.Nameservers.Select(x => x.Resource).OfType().Select(x => x.IPAddress).ToArray(); @@ -69,6 +78,6 @@ public void Dispose() } /// - public override string ToString() => $"{nameof(DnsUpdateClient)}({_dnsZone})"; + public override string ToString() => $"{nameof(DnsZoneUpdateClient)}({_dnsZone})"; } } diff --git a/src/Ae.Dns.Protocol/DnsMessageExtensions.cs b/src/Ae.Dns.Protocol/DnsMessageExtensions.cs index a4f47aa..4463263 100644 --- a/src/Ae.Dns.Protocol/DnsMessageExtensions.cs +++ b/src/Ae.Dns.Protocol/DnsMessageExtensions.cs @@ -1,6 +1,7 @@ using Ae.Dns.Protocol.Enums; using Ae.Dns.Protocol.Records; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; @@ -52,6 +53,22 @@ public static void EnsureOperationCode(this DnsMessage message, DnsOperationCode } } + public static void EnsureQueryType(this DnsMessage message, DnsQueryType expected) + { + if (message.Header.QueryType != expected) + { + throw new Exception($"The query type {message.Header.QueryType} was not expected (needed {expected})"); + } + } + + public static void EnsureHost(this DnsMessage message, DnsLabels expected) + { + if (message.Header.Host != expected) + { + throw new Exception($"The host {message.Header.Host} was not expected (needed {expected})"); + } + } + public static void EnsureSuccessResponseCode(this DnsMessage message) { if (message.Header.ResponseCode == DnsResponseCode.ServFail || @@ -130,5 +147,169 @@ private static IEnumerable Chunk(IEnumerable source Tags = { { "Resolver", resolver } } } }; + + /// + /// RFC 2136 2.4 + /// + public enum ZoneUpdatePreRequisite + { + Unknown, + /// + /// At least one RR with a specified NAME and TYPE (in the zone and class specified in the Zone Section) must exist. (RFC 2136 2.4.1) + /// + RRsetExistsValueIndependent, + /// + /// A set of RRs with a specified NAME and TYPE exists and has the same members with the same RDATAs as the RRset specified here in this section. (RFC 2136 2.4.2) + /// + RRsetExistsValueDependent, + /// + /// No RRs with a specified NAME and TYPE (in the zone and class denoted by the Zone Section) can exist. (RFC 2136 2.4.3) + /// + RRsetDoesNotExist, + /// + /// Name is in use. At least one RR with a specified NAME (in the zone and class specified by the Zone Section) must exist. (RFC 2136 2.4.4) + /// + NameIsInUse, + /// + /// Name is not in use. No RR of any type is owned by a specified NAME. (RFC 2136 2.4.5) + /// + NameIsNotInUse + } + + /// + /// Return the zone update type, if this is a operation. + /// + /// + /// + public static ZoneUpdatePreRequisite GetZoneUpdatePreRequisite(this DnsMessage message) + { + var preRequisites = message.Answers; + if (preRequisites.Count == 0 || preRequisites.Any(x => x.TimeToLive != 0)) + { + // At least one pre-req is expected, and TTL always must be zero + return ZoneUpdatePreRequisite.Unknown; + } + + if (preRequisites.Count == 1) + { + var preRequisite = preRequisites.Single(); + + // For this prerequisite, a requestor adds to the section a single RR + // whose NAME and TYPE are equal to that of the zone RRset whose + // existence is required.RDLENGTH is zero and RDATA is therefore + // empty. CLASS must be specified as ANY to differentiate this + // condition from that of an actual RR whose RDLENGTH is naturally zero + // (0)(e.g., NULL).TTL is specified as zero (0). + if (preRequisite.Class == DnsQueryClass.QCLASS_ANY && preRequisite.Type != DnsQueryType.ANY) + { + return ZoneUpdatePreRequisite.RRsetExistsValueIndependent; + } + + // For this prerequisite, a requestor adds to the section a single RR + // whose NAME and TYPE are equal to that of the RRset whose nonexistence + // is required.The RDLENGTH of this record is zero(0), and RDATA + // field is therefore empty. CLASS must be specified as NONE in order + // to distinguish this condition from a valid RR whose RDLENGTH is + // naturally zero (0)(for example, the NULL RR).TTL must be specified + // as zero (0). + if (preRequisite.Class == DnsQueryClass.QCLASS_NONE && preRequisite.Type != DnsQueryType.ANY) + { + return ZoneUpdatePreRequisite.RRsetDoesNotExist; + } + + // For this prerequisite, a requestor adds to the section a single RR + // whose NAME is equal to that of the name whose ownership of an RR is + // required.RDLENGTH is zero and RDATA is therefore empty. CLASS must + // be specified as ANY to differentiate this condition from that of an + // actual RR whose RDLENGTH is naturally zero (0)(e.g., NULL).TYPE + // must be specified as ANY to differentiate this case from that of an + // RRset existence test.TTL is specified as zero (0). + if (preRequisite.Class == DnsQueryClass.QCLASS_ANY && preRequisite.Type == DnsQueryType.ANY) + { + return ZoneUpdatePreRequisite.NameIsInUse; + } + + // For this prerequisite, a requestor adds to the section a single RR + // whose NAME is equal to that of the name whose nonownership of any RRs + // is required.RDLENGTH is zero and RDATA is therefore empty. CLASS + // must be specified as NONE.TYPE must be specified as ANY.TTL must + // be specified as zero (0). + if (preRequisite.Class == DnsQueryClass.QCLASS_NONE && preRequisite.Type == DnsQueryType.ANY) + { + return ZoneUpdatePreRequisite.NameIsNotInUse; + } + } + + // For this prerequisite, a requestor adds to the section an entire + // RRset whose preexistence is required.NAME and TYPE are that of the + // RRset being denoted. CLASS is that of the zone. TTL must be + // specified as zero (0) and is ignored when comparing RRsets for + // identity. + return ZoneUpdatePreRequisite.RRsetExistsValueDependent; + } + + /// + /// RFC 2136 2.5 + /// + public enum ZoneUpdateType + { + Unknown, + /// + /// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH and RDATA are those being added, and CLASS is the same as the zone class. Any duplicate RRs will be silently ignored by the primary master. (RFC 2136 2.5.1) + /// + AddToAnRRset, + /// + /// One RR is added to the Update Section whose NAME and TYPE are those of the RRset to be deleted. TTL must be specified as zero (0) and is otherwise not used by the primary master.CLASS must be specified as + /// ANY.RDLENGTH must be zero(0) and RDATA must therefore be empty. If no such RRset exists, then this Update RR will be silently ignored by the primary master. (RFC 2136 2.5.2) + /// + DeleteAnRRset, + /// + /// One RR is added to the Update Section whose NAME is that of the name to be cleansed of RRsets. TYPE must be specified as ANY. TTL must be specified as zero (0) and is otherwise not used by the primary + /// master.CLASS must be specified as ANY.RDLENGTH must be zero(0) and RDATA must therefore be empty.If no such RRsets exist, then this Update RR will be silently ignored by the primary master. (RFC 2136 2.5.3) + /// + DeleteAllRRsetsFromAName, + /// + /// RRs to be deleted are added to the Update Section. The NAME, TYPE, RDLENGTH and RDATA must match the RR being deleted. TTL must be specified as zero (0) and will otherwise be ignored by the primary + /// master.CLASS must be specified as NONE to distinguish this from an RR addition. If no such RRs exist, then this Update RR will be silently ignored by the primary master. (RFC 2136 2.5.4) + /// + DeleteAnRRFromAnRRset + } + + /// + /// Get the zone update type specified by the message. + /// + /// + /// + public static ZoneUpdateType GetZoneUpdateType(this DnsMessage message) + { + var updates = message.Nameservers; + + if (updates.Count == 0) + { + return ZoneUpdateType.Unknown; + } + + if (updates.Count == 1) + { + var update = updates.Single(); + + if (update.Type != DnsQueryType.ANY && update.Class == DnsQueryClass.QCLASS_ANY) + { + return ZoneUpdateType.DeleteAnRRset; + } + + if (update.Type == DnsQueryType.ANY && update.Class == DnsQueryClass.QCLASS_ANY) + { + return ZoneUpdateType.DeleteAllRRsetsFromAName; + } + } + + if (updates.All(x => x.Class == DnsQueryClass.QCLASS_NONE)) + { + return ZoneUpdateType.DeleteAnRRFromAnRRset; + } + + return ZoneUpdateType.AddToAnRRset; + } } } diff --git a/tests/Ae.Dns.Tests/Client/DnsUpdateClientTests.cs b/tests/Ae.Dns.Tests/Client/DnsUpdateClientTests.cs index 7b3a715..397017c 100644 --- a/tests/Ae.Dns.Tests/Client/DnsUpdateClientTests.cs +++ b/tests/Ae.Dns.Tests/Client/DnsUpdateClientTests.cs @@ -3,6 +3,7 @@ using Ae.Dns.Protocol.Enums; using Ae.Dns.Protocol.Records; using Ae.Dns.Protocol.Zone; +using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; using System.Net; @@ -38,11 +39,11 @@ public async Task TestWrongZone() { var zone = new TestDnsZone(); - var updateClient = new DnsUpdateClient(zone); + var updateClient = new DnsZoneUpdateClient(NullLogger.Instance, zone); var result = await updateClient.Query(new DnsMessage { - Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE }, + Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE, QueryType = DnsQueryType.SOA, Host = "example.com" }, Nameservers = new[] { new DnsResourceRecord @@ -64,11 +65,11 @@ public async Task TestWhitespaceHostname() { var zone = new TestDnsZone(); - var updateClient = new DnsUpdateClient(zone); + var updateClient = new DnsZoneUpdateClient(NullLogger.Instance, zone); var result = await updateClient.Query(new DnsMessage { - Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE }, + Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE, QueryType = DnsQueryType.SOA, Host = "example.com" }, Nameservers = new[] { new DnsResourceRecord @@ -90,11 +91,11 @@ public async Task TestAddRecords() { var zone = new TestDnsZone(); - var updateClient = new DnsUpdateClient(zone); + var updateClient = new DnsZoneUpdateClient(NullLogger.Instance, zone); var result1 = await updateClient.Query(new DnsMessage { - Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE }, + Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE, QueryType = DnsQueryType.SOA, Host = "example.com" }, Nameservers = new[] { new DnsResourceRecord @@ -110,7 +111,7 @@ public async Task TestAddRecords() var result2 = await updateClient.Query(new DnsMessage { - Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE }, + Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE, QueryType = DnsQueryType.SOA, Host = "example.com" }, Nameservers = new[] { new DnsResourceRecord @@ -126,7 +127,7 @@ public async Task TestAddRecords() var result3 = await updateClient.Query(new DnsMessage { - Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE }, + Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE, QueryType = DnsQueryType.SOA, Host = "example.com" }, Nameservers = new[] { new DnsResourceRecord @@ -142,7 +143,7 @@ public async Task TestAddRecords() var result4 = await updateClient.Query(new DnsMessage { - Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE }, + Header = new DnsHeader { OperationCode = DnsOperationCode.UPDATE, QueryType = DnsQueryType.SOA, Host = "example.com" }, Nameservers = new[] { new DnsResourceRecord diff --git a/tests/Ae.Dns.Tests/Protocol/DnsUpdateTests.cs b/tests/Ae.Dns.Tests/Protocol/DnsUpdateTests.cs index 1d0a4b6..e9a2053 100644 --- a/tests/Ae.Dns.Tests/Protocol/DnsUpdateTests.cs +++ b/tests/Ae.Dns.Tests/Protocol/DnsUpdateTests.cs @@ -9,18 +9,27 @@ public class DnsUpdateTests public void ReadUpdate1() { var message = DnsByteExtensions.FromBytes(SampleDnsPackets.Update1); + + Assert.Equal(DnsMessageExtensions.ZoneUpdatePreRequisite.NameIsNotInUse, message.GetZoneUpdatePreRequisite()); + Assert.Equal(DnsMessageExtensions.ZoneUpdateType.AddToAnRRset, message.GetZoneUpdateType()); } [Fact] public void ReadUpdate2() { var message = DnsByteExtensions.FromBytes(SampleDnsPackets.Update2); + + Assert.Equal(DnsMessageExtensions.ZoneUpdatePreRequisite.RRsetExistsValueDependent, message.GetZoneUpdatePreRequisite()); + Assert.Equal(DnsMessageExtensions.ZoneUpdateType.DeleteAnRRFromAnRRset, message.GetZoneUpdateType()); } [Fact] public void ReadUpdate3() { var message = DnsByteExtensions.FromBytes(SampleDnsPackets.Update3); + + Assert.Equal(DnsMessageExtensions.ZoneUpdatePreRequisite.NameIsNotInUse, message.GetZoneUpdatePreRequisite()); + Assert.Equal(DnsMessageExtensions.ZoneUpdateType.AddToAnRRset, message.GetZoneUpdateType()); } } }