Skip to content

Commit

Permalink
Adds logic to interpret the kind of zone update passed to the server.…
Browse files Browse the repository at this point in the history
… Adds logging.
  • Loading branch information
alanedwardes committed Feb 12, 2024
1 parent e532dd4 commit 43847d7
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 14 deletions.
2 changes: 1 addition & 1 deletion misc/Ae.Dns.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DnsZoneUpdateClient>(provider, dnsZone);

// Add the zone file as a source of automatic reverse lookups
staticLookupSources.Add(new DnsZoneLookupSource(dnsZone));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,24 +16,32 @@ namespace Ae.Dns.Client
/// Accepts messages with <see cref="DnsOperationCode.UPDATE"/>, and stores the record for use elsewhere.
/// </summary>
[Obsolete("Experimental: May change significantly in the future")]
public sealed class DnsUpdateClient : IDnsClient
public sealed class DnsZoneUpdateClient : IDnsClient
{
private readonly ILogger<DnsZoneUpdateClient> _logger;
private readonly IDnsZone _dnsZone;

/// <summary>
/// Create the new <see cref="DnsUpdateClient"/> using the specified <see cref="IDnsZone"/>.
/// Create the new <see cref="DnsZoneUpdateClient"/> using the specified <see cref="IDnsZone"/>.
/// </summary>
/// <param name="logger"></param>
/// <param name="dnsZone"></param>
public DnsUpdateClient(IDnsZone dnsZone)
public DnsZoneUpdateClient(ILogger<DnsZoneUpdateClient> logger, IDnsZone dnsZone)
{
_logger = logger;
_dnsZone = dnsZone;
}

/// <inheritdoc/>
public async Task<DnsMessage> 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<DnsIpAddressResource>().Select(x => x.IPAddress).ToArray();

Expand Down Expand Up @@ -69,6 +78,6 @@ public void Dispose()
}

/// <inheritdoc/>
public override string ToString() => $"{nameof(DnsUpdateClient)}({_dnsZone})";
public override string ToString() => $"{nameof(DnsZoneUpdateClient)}({_dnsZone})";
}
}
181 changes: 181 additions & 0 deletions src/Ae.Dns.Protocol/DnsMessageExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -130,5 +147,169 @@ private static IEnumerable<TSource[]> Chunk<TSource>(IEnumerable<TSource> source
Tags = { { "Resolver", resolver } }
}
};

/// <summary>
/// RFC 2136 2.4
/// </summary>
public enum ZoneUpdatePreRequisite
{
Unknown,
/// <summary>
/// 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)
/// </summary>
RRsetExistsValueIndependent,
/// <summary>
/// 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)
/// </summary>
RRsetExistsValueDependent,
/// <summary>
/// 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)
/// </summary>
RRsetDoesNotExist,
/// <summary>
/// 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)
/// </summary>
NameIsInUse,
/// <summary>
/// Name is not in use. No RR of any type is owned by a specified NAME. (RFC 2136 2.4.5)
/// </summary>
NameIsNotInUse
}

/// <summary>
/// Return the zone update type, if this is a <see cref="DnsOperationCode.UPDATE"/> operation.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
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;
}

/// <summary>
/// RFC 2136 2.5
/// </summary>
public enum ZoneUpdateType
{
Unknown,
/// <summary>
/// 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)
/// </summary>
AddToAnRRset,
/// <summary>
/// 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)
/// </summary>
DeleteAnRRset,
/// <summary>
/// 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)
/// </summary>
DeleteAllRRsetsFromAName,
/// <summary>
/// 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)
/// </summary>
DeleteAnRRFromAnRRset
}

/// <summary>
/// Get the zone update type specified by the message.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
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;
}
}
}
19 changes: 10 additions & 9 deletions tests/Ae.Dns.Tests/Client/DnsUpdateClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,11 +39,11 @@ public async Task TestWrongZone()
{
var zone = new TestDnsZone();

var updateClient = new DnsUpdateClient(zone);
var updateClient = new DnsZoneUpdateClient(NullLogger<DnsZoneUpdateClient>.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
Expand All @@ -64,11 +65,11 @@ public async Task TestWhitespaceHostname()
{
var zone = new TestDnsZone();

var updateClient = new DnsUpdateClient(zone);
var updateClient = new DnsZoneUpdateClient(NullLogger<DnsZoneUpdateClient>.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
Expand All @@ -90,11 +91,11 @@ public async Task TestAddRecords()
{
var zone = new TestDnsZone();

var updateClient = new DnsUpdateClient(zone);
var updateClient = new DnsZoneUpdateClient(NullLogger<DnsZoneUpdateClient>.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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/Ae.Dns.Tests/Protocol/DnsUpdateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,27 @@ public class DnsUpdateTests
public void ReadUpdate1()
{
var message = DnsByteExtensions.FromBytes<DnsMessage>(SampleDnsPackets.Update1);

Assert.Equal(DnsMessageExtensions.ZoneUpdatePreRequisite.NameIsNotInUse, message.GetZoneUpdatePreRequisite());
Assert.Equal(DnsMessageExtensions.ZoneUpdateType.AddToAnRRset, message.GetZoneUpdateType());
}

[Fact]
public void ReadUpdate2()
{
var message = DnsByteExtensions.FromBytes<DnsMessage>(SampleDnsPackets.Update2);

Assert.Equal(DnsMessageExtensions.ZoneUpdatePreRequisite.RRsetExistsValueDependent, message.GetZoneUpdatePreRequisite());
Assert.Equal(DnsMessageExtensions.ZoneUpdateType.DeleteAnRRFromAnRRset, message.GetZoneUpdateType());
}

[Fact]
public void ReadUpdate3()
{
var message = DnsByteExtensions.FromBytes<DnsMessage>(SampleDnsPackets.Update3);

Assert.Equal(DnsMessageExtensions.ZoneUpdatePreRequisite.NameIsNotInUse, message.GetZoneUpdatePreRequisite());
Assert.Equal(DnsMessageExtensions.ZoneUpdateType.AddToAnRRset, message.GetZoneUpdateType());
}
}
}

0 comments on commit 43847d7

Please sign in to comment.