diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0653552..780aacc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Additional articles for the documentation.
- `DateTime` extensions for ISO 8601 formatting.
- DNS Analytics
+- Zone Transfers
## [v0.1.0], [zones/v0.1.0], [dns/v0.1.0] - 2025-08-05
diff --git a/src/Extensions/Cloudflare.Dns/DnsZoneTransfersExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsZoneTransfersExtensions.cs
new file mode 100644
index 0000000..c0416ff
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/DnsZoneTransfersExtensions.cs
@@ -0,0 +1,584 @@
+using System.Linq;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare.Dns.Internals;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Extensions for DNS Zone Transfers.
+ ///
+ public static class DnsZoneTransfersExtensions
+ {
+ #region ACLs
+
+ ///
+ /// Create ACL.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreateACL(this ICloudflareClient client, CreateACLRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+
+ if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetwork && request.IpRangeSubnet < 24)
+ throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /24 for IPv4.");
+
+ if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetworkV6 && request.IpRangeSubnet < 64)
+ throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /64 for IPv6.");
+
+ var req = new InternalDnsZoneTransferAclRequest
+ {
+ Name = request.Name,
+ IpRange = request.IpRange
+ };
+
+ return client.PostAsync($"/accounts/{request.AccountId}/secondary_dns/acls", req, null, cancellationToken);
+ }
+
+ ///
+ /// Delete ACL.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The access control list identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeleteACL(this ICloudflareClient client, string accountId, string aclId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ aclId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/accounts/{accountId}/secondary_dns/acls/{aclId}", null, cancellationToken);
+ }
+
+ ///
+ /// Get ACL.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The access control list identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> ACLDetails(this ICloudflareClient client, string accountId, string aclId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ aclId.ValidateCloudflareId();
+
+ return client.GetAsync($"/accounts/{accountId}/secondary_dns/acls/{aclId}", null, cancellationToken);
+ }
+
+ ///
+ /// List ACLs.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> ListACLs(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+
+ return client.GetAsync>($"/accounts/{accountId}/secondary_dns/acls", null, cancellationToken);
+ }
+
+ ///
+ /// Update ACL.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> UpdateACL(this ICloudflareClient client, UpdateDnsZoneTransferAclRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+ request.AclId.ValidateCloudflareId();
+
+ if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetwork && request.IpRangeSubnet < 24)
+ throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /24 for IPv4.");
+
+ if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetworkV6 && request.IpRangeSubnet < 64)
+ throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /64 for IPv6.");
+
+ var req = new InternalDnsZoneTransferAclRequest
+ {
+ Name = request.Name,
+ IpRange = request.IpRange
+ };
+
+ return client.PutAsync($"/accounts/{request.AccountId}/secondary_dns/acls/{request.AclId}", req, cancellationToken);
+ }
+
+ #endregion ACLs
+
+ #region Force AXFR
+
+ ///
+ /// Sends AXFR zone transfer request to primary nameserver(s).
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> ForceAXFR(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.PostAsync($"/zones/{zoneId}/secondary_dns/force_axfr", null, null, cancellationToken);
+ }
+
+ #endregion Force AXFR
+
+ #region Incoming
+
+ ///
+ /// Create secondary zone configuration for incoming zone transfers.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreateSecondaryZoneConfiguration(this ICloudflareClient client, SecondaryZoneConfigurationRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The zone name is required.");
+
+ if (request.AutoRefreshSeconds < 0)
+ throw new ArgumentOutOfRangeException(nameof(request.AutoRefreshSeconds), "Auto refresh seconds must be greater than or equal to 0.");
+
+ if (request.Peers.Count == 0)
+ throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required.");
+
+ foreach (string peer in request.Peers)
+ peer.ValidateCloudflareId();
+
+ var req = new InternalSecondaryZoneConfigurationRequest
+ {
+ Name = request.Name,
+ AutoRefreshSeconds = request.AutoRefreshSeconds,
+ Peers = request.Peers.ToList()
+ };
+
+ return client.PostAsync($"/zones/{request.ZoneId}/secondary_dns/incoming", req, null, cancellationToken);
+ }
+
+ ///
+ /// Delete secondary zone configuration for incoming zone transfers.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeleteSecondaryZoneConfiguration(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/zones/{zoneId}/secondary_dns/incoming", null, cancellationToken);
+ }
+
+ ///
+ /// Get secondary zone configuration for incoming zone transfers.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> SecondaryZoneConfigurationDetails(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.GetAsync($"/zones/{zoneId}/secondary_dns/incoming", null, cancellationToken);
+ }
+
+ ///
+ /// Update secondary zone configuration for incoming zone transfers.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> UpdateSecondaryZoneConfiguration(this ICloudflareClient client, SecondaryZoneConfigurationRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The zone name is required.");
+
+ if (request.AutoRefreshSeconds < 0)
+ throw new ArgumentOutOfRangeException(nameof(request.AutoRefreshSeconds), "Auto refresh seconds must be greater than or equal to 0.");
+
+ if (request.Peers.Count == 0)
+ throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required.");
+
+ foreach (string peer in request.Peers)
+ peer.ValidateCloudflareId();
+
+ var req = new InternalSecondaryZoneConfigurationRequest
+ {
+ Name = request.Name,
+ AutoRefreshSeconds = request.AutoRefreshSeconds,
+ Peers = request.Peers.ToList()
+ };
+
+ return client.PutAsync($"/zones/{request.ZoneId}/secondary_dns/incoming", req, cancellationToken);
+ }
+
+ #endregion Incoming
+
+ #region Outgoing
+
+ ///
+ /// Create primary zone configuration for outgoing zone transfers.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreatePrimaryZoneConfiguration(this ICloudflareClient client, PrimaryZoneConfigurationRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The zone name is required.");
+
+ if (request.Peers.Count == 0)
+ throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required.");
+
+ foreach (string peer in request.Peers)
+ peer.ValidateCloudflareId();
+
+ var req = new InternalPrimaryZoneConfigurationRequest
+ {
+ Name = request.Name,
+ Peers = request.Peers.ToList()
+ };
+
+ return client.PostAsync($"/zones/{request.ZoneId}/secondary_dns/outgoing", req, null, cancellationToken);
+ }
+
+ ///
+ /// Delete primary zone configuration for outgoing zone transfers.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeletePrimaryZoneConfiguration(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/zones/{zoneId}/secondary_dns/outgoing", null, cancellationToken);
+ }
+
+ ///
+ /// Disable outgoing zone transfers for primary zone and clears IXFR backlog of primary zone.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ /// Referring to the
+ /// documentation,
+ /// the text value should be Disabled.
+ ///
+ public static Task> DisableOutgoingZoneTransfers(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.PostAsync($"/zones/{zoneId}/secondary_dns/outgoing/disable", null, null, cancellationToken);
+ }
+
+ ///
+ /// Enable outgoing zone transfers for primary zone.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ /// Referring to the
+ /// documentation,
+ /// the text value should be Enabled.
+ ///
+ public static Task> EnableOutgoingZoneTransfers(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.PostAsync($"/zones/{zoneId}/secondary_dns/outgoing/enable", null, null, cancellationToken);
+ }
+
+ ///
+ /// Get primary zone configuration for outgoing zone transfers
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> PrimaryZoneConfigurationDetails(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.GetAsync($"/zones/{zoneId}/secondary_dns/outgoing", null, cancellationToken);
+ }
+
+ ///
+ /// Update primary zone configuration for outgoing zone transfers.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> UpdatePrimaryZoneConfiguration(this ICloudflareClient client, PrimaryZoneConfigurationRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The zone name is required.");
+
+ if (request.Peers.Count == 0)
+ throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required.");
+
+ foreach (string peer in request.Peers)
+ peer.ValidateCloudflareId();
+
+ var req = new InternalPrimaryZoneConfigurationRequest
+ {
+ Name = request.Name,
+ Peers = request.Peers.ToList()
+ };
+
+ return client.PutAsync($"/zones/{request.ZoneId}/secondary_dns/outgoing", req, cancellationToken);
+ }
+
+ ///
+ /// Notifies the secondary nameserver(s) and clears IXFR backlog of primary zone.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ /// Referring to the
+ /// documentation,
+ /// the text value should be OK.
+ ///
+ public static Task> ForceDNSNotify(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.PostAsync($"/zones/{zoneId}/secondary_dns/outgoing/force_notify", null, null, cancellationToken);
+ }
+
+ ///
+ /// Enable outgoing zone transfers for primary zone.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ /// Referring to the
+ /// documentation,
+ /// the text value should be Enabled or Disabled.
+ ///
+ public static Task> GetOutgoingZoneTransferStatus(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.GetAsync($"/zones/{zoneId}/secondary_dns/outgoing/status", null, cancellationToken);
+ }
+
+ #endregion Outgoing
+
+ #region Peers
+
+ ///
+ /// List Peers.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> ListPeers(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+
+ return client.GetAsync>($"/accounts/{accountId}/secondary_dns/peers", null, cancellationToken);
+ }
+
+ ///
+ /// Get Peer.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The peer identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> PeerDetails(this ICloudflareClient client, string accountId, string peerId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ peerId.ValidateCloudflareId();
+
+ return client.GetAsync($"/accounts/{accountId}/secondary_dns/peers/{peerId}", null, cancellationToken);
+ }
+
+ ///
+ /// Create Peer.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreatePeer(this ICloudflareClient client, CreatePeerRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The peer name is required.");
+
+ var req = new InternalCreatePeerRequest
+ {
+ Name = request.Name
+ };
+
+ return client.PostAsync($"/accounts/{request.AccountId}/secondary_dns/peers", req, null, cancellationToken);
+ }
+
+ ///
+ /// Modify Peer.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> UpdatePeer(this ICloudflareClient client, UpdatePeerRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+ request.PeerId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The peer name is required.");
+
+ if (request.Port < 0 || 65535 < request.Port)
+ throw new ArgumentOutOfRangeException(nameof(request.Port), "The port must be between 0 and 65535.");
+
+ var req = new InternalUpdatePeerRequest
+ {
+ Name = request.Name,
+ Ip = request.IpAddress,
+ IxfrEnable = request.IXFREnable,
+ Port = request.Port,
+ TSigId = request.TSIGId
+ };
+
+ return client.PutAsync($"/accounts/{request.AccountId}/secondary_dns/peers/{request.PeerId}", req, cancellationToken);
+ }
+
+ ///
+ /// Delete Peer.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The peer identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeletePeer(this ICloudflareClient client, string accountId, string peerId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ peerId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/accounts/{accountId}/secondary_dns/peers/{peerId}", null, cancellationToken);
+ }
+
+ #endregion Peers
+
+ #region TSIGs
+
+ ///
+ /// List TSIGs.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> ListTSIGs(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+
+ return client.GetAsync>($"/accounts/{accountId}/secondary_dns/tsigs", null, cancellationToken);
+ }
+
+ ///
+ /// Get TSIG.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The TSIG identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> TSIGDetails(this ICloudflareClient client, string accountId, string tsigId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ tsigId.ValidateCloudflareId();
+
+ return client.GetAsync($"/accounts/{accountId}/secondary_dns/tsigs/{tsigId}", null, cancellationToken);
+ }
+
+ ///
+ /// Create TSIG.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreateTSIG(this ICloudflareClient client, CreateTSIGRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The TSIG name is required.");
+
+ if (!Enum.IsDefined(typeof(TSigAlgorithm), request.Algorithm))
+ throw new ArgumentOutOfRangeException(nameof(request.Algorithm), "The TSIG algorithm is invalid.");
+
+ if (string.IsNullOrWhiteSpace(request.Secret))
+ throw new ArgumentNullException(nameof(request.Secret), "The TSIG secret is required.");
+
+ var req = new InternalTSIGRequest
+ {
+ Name = request.Name,
+ Algorithm = request.Algorithm,
+ Secret = request.Secret
+ };
+
+ return client.PostAsync($"/accounts/{request.AccountId}/secondary_dns/tsigs", req, null, cancellationToken);
+ }
+
+ ///
+ /// Modify TSIG.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> UpdateTSIG(this ICloudflareClient client, UpdateTSIGRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+ request.TSigId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(request.Name))
+ throw new ArgumentNullException(nameof(request.Name), "The TSIG name is required.");
+
+ if (!Enum.IsDefined(typeof(TSigAlgorithm), request.Algorithm))
+ throw new ArgumentOutOfRangeException(nameof(request.Algorithm), "The TSIG algorithm is invalid.");
+
+ if (string.IsNullOrWhiteSpace(request.Secret))
+ throw new ArgumentNullException(nameof(request.Secret), "The TSIG secret is required.");
+
+ var req = new InternalTSIGRequest
+ {
+ Name = request.Name,
+ Algorithm = request.Algorithm,
+ Secret = request.Secret
+ };
+
+ return client.PutAsync($"/accounts/{request.AccountId}/secondary_dns/tsigs/{request.TSigId}", req, cancellationToken);
+ }
+
+ ///
+ /// Delete TSIG.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The TSIG identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeleteTSIG(this ICloudflareClient client, string accountId, string tsigId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ tsigId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/accounts/{accountId}/secondary_dns/tsigs/{tsigId}", null, cancellationToken);
+ }
+
+ #endregion TSIGs
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Enums/TSigAlgorithm.cs b/src/Extensions/Cloudflare.Dns/Enums/TSigAlgorithm.cs
new file mode 100644
index 0000000..e112e44
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Enums/TSigAlgorithm.cs
@@ -0,0 +1,100 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Available TSIG algorithms as recommended by IANA.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum TSigAlgorithm
+ {
+ ///
+ /// HMAC SHA1.
+ ///
+ ///
+ /// Implementation: must
+ ///
+ /// Use: not recommended
+ ///
+ [EnumMember(Value = "hmac-sha1")]
+ HMAC_SHA1 = 1,
+
+ ///
+ /// HMAC SHA224.
+ ///
+ ///
+ /// Implementation: may
+ ///
+ /// Use: may
+ ///
+ [EnumMember(Value = "hmac-sha224")]
+ HMAC_SHA224 = 2,
+
+ ///
+ /// HMAC SHA256.
+ ///
+ ///
+ /// Implementation: must
+ ///
+ /// Use: recommended
+ ///
+ [EnumMember(Value = "hmac-sha256")]
+ HMAC_SHA256 = 3,
+
+ ///
+ /// HMAC SHA384.
+ ///
+ ///
+ /// Implementation: may
+ ///
+ /// Use: may
+ ///
+ [EnumMember(Value = "hmac-sha384")]
+ HMAC_SHA384 = 4,
+
+ ///
+ /// HMAC SHA512.
+ ///
+ ///
+ /// Implementation: may
+ ///
+ /// Use: may
+ ///
+ [EnumMember(Value = "hmac-sha512")]
+ HMAC_SHA512 = 5,
+
+ ///
+ /// HMAC SHA256 128.
+ ///
+ ///
+ /// Implementation: may
+ ///
+ /// Use: may
+ ///
+ [EnumMember(Value = "hmac-sha256-128")]
+ HMAC_SHA256_128 = 6,
+
+ ///
+ /// HMAC SHA384 192.
+ ///
+ ///
+ /// Implementation: may
+ ///
+ /// Use: may
+ ///
+ [EnumMember(Value = "hmac-sha384-192")]
+ HMAC_SHA384_192 = 7,
+
+ ///
+ /// HMAC SHA512 256.
+ ///
+ ///
+ /// Implementation: may
+ ///
+ /// Use: may
+ ///
+ [EnumMember(Value = "hmac-sha512-256")]
+ HMAC_SHA512_256 = 8,
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalCreatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalCreatePeerRequest.cs
new file mode 100644
index 0000000..fcd7abb
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalCreatePeerRequest.cs
@@ -0,0 +1,8 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalCreatePeerRequest
+ {
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalDnsZoneTransferAclRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsZoneTransferAclRequest.cs
new file mode 100644
index 0000000..2f93223
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsZoneTransferAclRequest.cs
@@ -0,0 +1,11 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ internal class InternalDnsZoneTransferAclRequest
+ {
+ [JsonProperty("ip_range")]
+ public string? IpRange { get; set; }
+
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalPrimaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalPrimaryZoneConfigurationRequest.cs
new file mode 100644
index 0000000..7e9fcf8
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalPrimaryZoneConfigurationRequest.cs
@@ -0,0 +1,11 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalPrimaryZoneConfigurationRequest
+ {
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ [JsonProperty("peers")]
+ public IReadOnlyCollection? Peers { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalSecondaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalSecondaryZoneConfigurationRequest.cs
new file mode 100644
index 0000000..cb4fb95
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalSecondaryZoneConfigurationRequest.cs
@@ -0,0 +1,14 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalSecondaryZoneConfigurationRequest
+ {
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ [JsonProperty("auto_refresh_seconds")]
+ public int? AutoRefreshSeconds { get; set; }
+
+ [JsonProperty("peers")]
+ public IReadOnlyCollection? Peers { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalTSIGRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalTSIGRequest.cs
new file mode 100644
index 0000000..534f437
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalTSIGRequest.cs
@@ -0,0 +1,14 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalTSIGRequest
+ {
+ [JsonProperty("algo")]
+ public TSigAlgorithm? Algorithm { get; set; }
+
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ [JsonProperty("secret")]
+ public string? Secret { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalUpdatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdatePeerRequest.cs
new file mode 100644
index 0000000..bf084c0
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdatePeerRequest.cs
@@ -0,0 +1,17 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalUpdatePeerRequest : InternalCreatePeerRequest
+ {
+ [JsonProperty("ip")]
+ public string? Ip { get; set; }
+
+ [JsonProperty("ixfr_enable")]
+ public bool? IxfrEnable { get; set; }
+
+ [JsonProperty("port")]
+ public int? Port { get; set; }
+
+ [JsonProperty("tsig_id")]
+ public string? TSigId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/ACL.cs b/src/Extensions/Cloudflare.Dns/Models/ACL.cs
new file mode 100644
index 0000000..57fbd33
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/ACL.cs
@@ -0,0 +1,31 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents an Access Control List (ACL) entry, which defines the allowed IP ranges for DNS zone transfers.
+ ///
+ public class ACL
+ {
+ ///
+ /// The unique identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// Allowed IPv4/IPv6 address range of primary or secondary nameservers.
+ /// This will be applied for the entire account.
+ ///
+ ///
+ /// The IP range is used to allow additional NOTIFY IPs for secondary zones and IPs Cloudflare allows AXFR/IXFR requests from for primary zones.
+ /// CIDRs are limited to a maximum of /24 for IPv4 and /64 for IPv6 respectively.
+ ///
+ [JsonProperty("ip_range")]
+ public string? IpRange { get; set; }
+
+ ///
+ /// The name of the ACL.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/IncomingZoneConfiguration.cs b/src/Extensions/Cloudflare.Dns/Models/IncomingZoneConfiguration.cs
new file mode 100644
index 0000000..94f8f5d
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/IncomingZoneConfiguration.cs
@@ -0,0 +1,57 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a response of a secondary zone configuration.
+ ///
+ public class IncomingZoneConfiguration
+ {
+ ///
+ /// The unique identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// How often should a secondary zone auto refresh regardless of DNS NOTIFY.
+ /// Not applicable for primary zones.
+ ///
+ [JsonProperty("auto_refresh_seconds")]
+ public int? AutoRefreshSeconds { get; set; }
+
+ ///
+ /// The time for a specific event.
+ ///
+ [JsonProperty("checked_time")]
+ public DateTime? CheckedTime { get; set; }
+
+ ///
+ /// The time for a specific event.
+ ///
+ [JsonProperty("created_time")]
+ public DateTime? CreatedTime { get; set; }
+
+ ///
+ /// The time for a specific event.
+ ///
+ [JsonProperty("modified_time")]
+ public DateTime? ModifiedTime { get; set; }
+
+ ///
+ /// The zone name.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// A list of peer tags.
+ ///
+ [JsonProperty("peers")]
+ public IReadOnlyCollection? Peers { get; set; }
+
+ ///
+ /// The serial number of the SOA for the given zone.
+ ///
+ [JsonProperty("soa_serial")]
+ public int? SoaSerial { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/OutgoingZoneConfiguration.cs b/src/Extensions/Cloudflare.Dns/Models/OutgoingZoneConfiguration.cs
new file mode 100644
index 0000000..b93ec5e
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/OutgoingZoneConfiguration.cs
@@ -0,0 +1,50 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a response of a secondary zone configuration.
+ ///
+ public class OutgoingZoneConfiguration
+ {
+ ///
+ /// The unique identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// The time for a specific event.
+ ///
+ [JsonProperty("checked_time")]
+ public DateTime? CheckedTime { get; set; }
+
+ ///
+ /// The time for a specific event.
+ ///
+ [JsonProperty("created_time")]
+ public DateTime? CreatedTime { get; set; }
+
+ ///
+ /// The time for a specific event.
+ ///
+ [JsonProperty("last_transferred_time")]
+ public DateTime? LastTransferredTime { get; set; }
+
+ ///
+ /// The zone name.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// A list of peer tags.
+ ///
+ [JsonProperty("peers")]
+ public IReadOnlyCollection? Peers { get; set; }
+
+ ///
+ /// The serial number of the SOA for the given zone.
+ ///
+ [JsonProperty("soa_serial")]
+ public int? SoaSerial { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Peer.cs b/src/Extensions/Cloudflare.Dns/Models/Peer.cs
new file mode 100644
index 0000000..a29b422
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Peer.cs
@@ -0,0 +1,61 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// A Cloudflare peer.
+ /// Source
+ ///
+ public class Peer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The unique identifier.
+ /// The name of the peer.
+ public Peer(string id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+
+ ///
+ /// The unique identifier.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// The name of the peer.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// IPv4/IPv6 address of primary or secondary nameserver, depending on what zone this peer is linked to.
+ ///
+ /// - For primary zones this IP defines the IP of the secondary nameserver Cloudflare will NOTIFY upon zone changes.
+ /// - For secondary zones this IP defines the IP of the primary nameserver Cloudflare will send AXFR/IXFR requests to.
+ ///
+ ///
+ [JsonProperty("ip")]
+ public string? IpAddress { get; set; }
+
+ ///
+ /// Enable IXFR transfer protocol, default is AXFR.
+ /// Only applicable to secondary zones.
+ ///
+ [JsonProperty("ixfr_enable")]
+ public bool? IXFREnabled { get; set; }
+
+ ///
+ /// DNS port of primary or secondary nameserver, depending on what zone this peer is linked to.
+ ///
+ [JsonProperty("port")]
+ public int? Port { get; set; }
+
+ ///
+ /// TSIG authentication will be used for zone transfer if configured.
+ ///
+ [JsonProperty("tsig_id")]
+ public string? TSIGId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/TSIG.cs b/src/Extensions/Cloudflare.Dns/Models/TSIG.cs
new file mode 100644
index 0000000..d68e2cd
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/TSIG.cs
@@ -0,0 +1,35 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a Transaction Signature (TSIG) used for securing DNS messages.
+ ///
+ public class TSIG
+ {
+ ///
+ /// The unique identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// TSIG algorithm.
+ ///
+ [JsonProperty("algo")]
+ public TSigAlgorithm? Algorithm { get; set; }
+
+ ///
+ /// TSIG key name.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// TSIG secret.
+ ///
+ [JsonProperty("secret")]
+ public string? Secret { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md
index 54ab320..8c2ce02 100644
--- a/src/Extensions/Cloudflare.Dns/README.md
+++ b/src/Extensions/Cloudflare.Dns/README.md
@@ -59,6 +59,59 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
- [Show DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/methods/get/)
+#### [Zone Transfers]
+
+##### [ACLs]
+
+- [List ACLs](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/list/)
+- [ACL Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/get/)
+- [Create ACL](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/create/)
+- [Update ACL](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/update/)
+- [Delete ACL](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/delete/)
+
+
+##### Force AXFR
+
+- [Force AXFR](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/force_axfr/methods/create/)
+
+
+##### [Incoming]
+
+- [Create Secondary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/create/)
+- [Delete Secondary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/delete/)
+- [Secondary Zone Configuration Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/get/)
+- [Update Secondary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/update/)
+
+
+##### [Outgoing]
+
+- [Create Primary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/create/)
+- [Delete Primary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/delete/)
+- [Disable Outgoing Zone Transfers](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/disable/)
+- [Enable Outgoing Zone Transfers](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/enable/)
+- [Primary Zone Configuration Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/get/)
+- [Update Primary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/update/)
+- [Force DNS Notify](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/force_notify/)
+- [Get Outgoing Zone Transfer Status](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/subresources/status/methods/get/)
+
+
+##### [Peers]
+
+- [List Peers](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/list/)
+- [Peer Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/get/)
+- [Create Peer](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/create/)
+- [Update Peer](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/update/)
+- [Delete Peer](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/delete/)
+
+
+##### [TSIGs] (Transaction SIGnatures)
+
+- [List TSIGs](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/list/)
+- [TSIG Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/get/)
+- [Create TSIG](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/create/)
+- [Update TSIG](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/update/)
+- [Delete TSIG](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/delete/)
+
---
@@ -77,3 +130,9 @@ Published under MIT License (see [choose a license])
[Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/
[Account]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/
[Zone]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/
+ [Zone Transfers]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/
+ [ACLs]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/
+ [Incoming]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/
+ [Outgoing]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/
+ [Peers]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/
+ [TSIGs]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/
diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreateACLRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreateACLRequest.cs
new file mode 100644
index 0000000..c49daca
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/CreateACLRequest.cs
@@ -0,0 +1,77 @@
+using System.Net;
+using System.Net.Sockets;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to create a DNS zone transfer access control list (ACL).
+ ///
+ public class CreateACLRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// Allowed IPv4/IPv6 address range of primary or secondary nameservers (CIDR).
+ /// The name of the ACL.
+ public CreateACLRequest(string accountId, string ipRange, string name)
+ {
+ IpRangeBaseAddress = IPAddress.None;
+ IpRangeSubnet = 0;
+
+ AccountId = accountId;
+ IpRange = ipRange;
+ Name = name;
+ }
+
+ ///
+ /// The account identifier.
+ ///
+ public string AccountId { get; set; }
+
+ ///
+ /// Allowed IPv4/IPv6 address range of primary or secondary nameservers.
+ /// This will be applied for the entire account.
+ ///
+ ///
+ /// The IP range is used to allow additional NOTIFY IPs for secondary zones and IPs Cloudflare allows AXFR/IXFR requests from for primary zones.
+ /// CIDRs are limited to a maximum of /24 for IPv4 and /64 for IPv6 respectively.
+ ///
+ public string IpRange
+ {
+ get => $"{IpRangeBaseAddress}/{IpRangeSubnet}";
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ throw new ArgumentNullException(nameof(value), $"{nameof(IpRange)} cannot be null or empty.");
+
+ string[] parts = value.Split('/');
+ if (parts.Length != 2)
+ throw new FormatException("Invalid IP range format.");
+
+ var prefix = IPAddress.Parse(parts[0]);
+
+ if (!int.TryParse(parts[1], out int prefixLength))
+ throw new FormatException("Invalid IP range subnet format.");
+
+ if (prefix.AddressFamily == AddressFamily.InterNetwork && (prefixLength < 0 || 32 < prefixLength))
+ throw new FormatException("Invalid subnet length for IPv4.");
+
+ if (prefix.AddressFamily == AddressFamily.InterNetworkV6 && (prefixLength < 0 || 128 < prefixLength))
+ throw new FormatException("Invalid subnet length for IPv6.");
+
+ IpRangeBaseAddress = prefix;
+ IpRangeSubnet = prefixLength;
+ }
+ }
+
+ ///
+ /// The name of the ACL.
+ ///
+ public string Name { get; set; }
+
+ internal IPAddress IpRangeBaseAddress { get; set; }
+
+ internal int IpRangeSubnet { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreatePeerRequest.cs
new file mode 100644
index 0000000..015a18d
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/CreatePeerRequest.cs
@@ -0,0 +1,29 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to create a new peer within a specific account.
+ ///
+ public class CreatePeerRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// The name of the peer.
+ public CreatePeerRequest(string accountId, string name)
+ {
+ AccountId = accountId;
+ Name = name;
+ }
+
+ ///
+ /// The account identifier.
+ ///
+ public string AccountId { get; set; }
+
+ ///
+ /// The name of the peer.
+ ///
+ public string Name { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreateTSIGRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreateTSIGRequest.cs
new file mode 100644
index 0000000..037273e
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/CreateTSIGRequest.cs
@@ -0,0 +1,43 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to create a TSIG (Transaction Signature) key for DNS operations.
+ ///
+ public class CreateTSIGRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// TSIG key name.
+ /// TSIG secret.
+ public CreateTSIGRequest(string accountId, string name, string secret)
+ {
+ Algorithm = TSigAlgorithm.HMAC_SHA256;
+
+ AccountId = accountId;
+ Name = name;
+ Secret = secret;
+ }
+
+ ///
+ /// The account identifier.
+ ///
+ public string AccountId { get; set; }
+
+ ///
+ /// TSIG algorithm.
+ ///
+ public TSigAlgorithm Algorithm { get; set; }
+
+ ///
+ /// TSIG key name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// TSIG secret.
+ ///
+ public string Secret { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/PrimaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/PrimaryZoneConfigurationRequest.cs
new file mode 100644
index 0000000..2d18046
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/PrimaryZoneConfigurationRequest.cs
@@ -0,0 +1,34 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to create a primary zone configuration.
+ ///
+ public class PrimaryZoneConfigurationRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ /// The zone name.
+ public PrimaryZoneConfigurationRequest(string zoneId, string name)
+ {
+ ZoneId = zoneId;
+ Name = name;
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// The zone name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// A list of peer tags.
+ ///
+ public IReadOnlyCollection Peers { get; set; } = [];
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/SecondaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/SecondaryZoneConfigurationRequest.cs
new file mode 100644
index 0000000..4090caa
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/SecondaryZoneConfigurationRequest.cs
@@ -0,0 +1,41 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to create a secondary zone configuration.
+ ///
+ public class SecondaryZoneConfigurationRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ /// The zone name.
+ public SecondaryZoneConfigurationRequest(string zoneId, string name)
+ {
+ ZoneId = zoneId;
+ Name = name;
+
+ Peers = [];
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// The Zone name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// How often should a secondary zone auto refresh regardless of DNS NOTIFY. Not applicable for primary zones.
+ ///
+ public int AutoRefreshSeconds { get; set; }
+
+ ///
+ /// A list of peer tags.
+ ///
+ public IReadOnlyCollection Peers { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneTransferAclRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneTransferAclRequest.cs
new file mode 100644
index 0000000..a9566b4
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneTransferAclRequest.cs
@@ -0,0 +1,26 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to update an existing DNS zone transfer access control list (ACL).
+ ///
+ public class UpdateDnsZoneTransferAclRequest : CreateACLRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// The access control list identifier.
+ /// Allowed IPv4/IPv6 address range of primary or secondary nameservers (CIDR).
+ /// The name of the ACL.
+ public UpdateDnsZoneTransferAclRequest(string accountId, string aclId, string ipRange, string name)
+ : base(accountId, ipRange, name)
+ {
+ AclId = aclId;
+ }
+
+ ///
+ /// The access control list identifier.
+ ///
+ public string AclId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdatePeerRequest.cs
new file mode 100644
index 0000000..2d2a9a4
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/UpdatePeerRequest.cs
@@ -0,0 +1,49 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to update an existing peer with new configuration details.
+ ///
+ public class UpdatePeerRequest : CreatePeerRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// The peer identifier.
+ /// The name of the peer
+ public UpdatePeerRequest(string accountId, string peerId, string name)
+ : base(accountId, name)
+ {
+ PeerId = peerId;
+ }
+
+ ///
+ /// The peer identifier.
+ ///
+ public string PeerId { get; set; }
+
+ ///
+ /// IPv4/IPv6 address of primary or secondary nameserver, depending on what zone this peer is linked to.
+ ///
+ /// - For primary zones this IP defines the IP of the secondary nameserver Cloudflare will NOTIFY upon zone changes.
+ /// - For secondary zones this IP defines the IP of the primary nameserver Cloudflare will send AXFR/IXFR requests to.
+ ///
+ ///
+ public string? IpAddress { get; set; }
+
+ ///
+ /// Enable IXFR transfer protocol, default is AXFR. Only applicable to secondary zones.
+ ///
+ public bool? IXFREnable { get; set; }
+
+ ///
+ /// DNS port of primary or secondary nameserver, depending on what zone this peer is linked to.
+ ///
+ public int? Port { get; set; }
+
+ ///
+ /// TSIG authentication will be used for zone transfer if configured.
+ ///
+ public string? TSIGId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateTSIGRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateTSIGRequest.cs
new file mode 100644
index 0000000..4026c7b
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateTSIGRequest.cs
@@ -0,0 +1,26 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to update an existing TSIG (Transaction Signature) key.
+ ///
+ public class UpdateTSIGRequest : CreateTSIGRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// The TSIG identifier.
+ /// TSIG key name.
+ /// TSIG secret.
+ public UpdateTSIGRequest(string accountId, string tsigId, string name, string secret)
+ : base(accountId, name, secret)
+ {
+ TSigId = tsigId;
+ }
+
+ ///
+ /// The TSIG identifier.
+ ///
+ public string TSigId { get; set; }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ACLDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ACLDetailsTest.cs
new file mode 100644
index 0000000..c0eada6
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ACLDetailsTest.cs
@@ -0,0 +1,75 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions.ACLs
+{
+ [TestClass]
+ public class ACLDetailsTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+ private const string AclId = "23ff594956f20c2a721606e94745a8aa";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [new ResponseInfo(1000, "Message 1")],
+ Errors = [new ResponseInfo(1000, "Error 1")],
+ Result = new ACL
+ {
+ Id = AclId,
+ IpRange = "192.0.2.0/24",
+ Name = "Test ACL"
+ }
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldGetAclDetails()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.ACLDetails(AccountId, AclId, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ string requestPath = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", requestPath);
+
+ _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, _, _) => _callbacks.Add(requestPath))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/CreateACLTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/CreateACLTest.cs
new file mode 100644
index 0000000..dc6a158
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/CreateACLTest.cs
@@ -0,0 +1,166 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions.ACLs
+{
+ [TestClass]
+ public class CreateACLTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, InternalDnsZoneTransferAclRequest Request)> _callbacks;
+ private CreateACLRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = new ACL
+ {
+ Id = "23ff594956f20c2a721606e94745a8aa",
+ IpRange = "192.0.2.53/28",
+ Name = "my-acl-1"
+ }
+ };
+
+ _request = new CreateACLRequest(
+ accountId: AccountId,
+ ipRange: "192.0.2.53/28",
+ name: "my-acl-1"
+ );
+ }
+
+ [TestMethod]
+ public async Task ShouldCreateAcl()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateACL(_request, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, request) = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls", requestPath);
+
+ Assert.IsNotNull(request);
+ Assert.AreEqual(_request.Name, request.Name);
+ Assert.AreEqual(_request.IpRange, request.IpRange);
+
+ Assert.AreEqual("192.0.2.53", _request.IpRangeBaseAddress.ToString());
+ Assert.AreEqual(28, _request.IpRangeSubnet);
+
+ _clientMock.Verify(m => m.PostAsync($"/accounts/{AccountId}/secondary_dns/acls", It.IsAny(), null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ [DataRow("127.0.0.1/20")]
+ [DataRow("fd00::/56")]
+ public async Task ShouldThrowArumentOutOfRangeExceptionForWrongSubnetDefinition(string ipRange)
+ {
+ // Arrange
+ _request.IpRange = ipRange;
+ var client = GetClient();
+
+ // Act & Assert
+ await Assert.ThrowsExactlyAsync(async () =>
+ {
+ await client.CreateACL(_request, TestContext.CancellationToken);
+ });
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow("\t")]
+ [DataRow(" ")]
+ public void ShouldThrowArgumentNullExceptionForIpRange(string ipRange)
+ {
+ // Arrange
+
+ // Act & Assert
+ Assert.ThrowsExactly(() =>
+ {
+ _ = new CreateACLRequest(AccountId, ipRange, "my-acl-1");
+ });
+ }
+
+ [TestMethod]
+ [DataRow("192.0.2.53")]
+ [DataRow("192.0.2.53/28/28")]
+ public void ShouldThrowFormatExceptionForInvalidFormat(string ipRange)
+ {
+ // Arrange
+
+ // Act & Assert
+ Assert.ThrowsExactly(() =>
+ {
+ _ = new CreateACLRequest(AccountId, ipRange, "my-acl-1");
+ });
+ }
+
+ [TestMethod]
+ public void ShouldThrowFormatExceptionForInvalidSubnetFormat()
+ {
+ // Arrange
+
+ // Act & Assert
+ Assert.ThrowsExactly(() =>
+ {
+ _ = new CreateACLRequest(AccountId, "192.0.2.53/a", "my-acl-1");
+ });
+ }
+
+ [TestMethod]
+ [DataRow("127.0.0.1/-1")]
+ [DataRow("127.0.0.1/33")]
+ [DataRow("fd00::/-1")]
+ [DataRow("fd00::/129")]
+ public void ShouldThrowFormatExceptionForInvalidSubnetLength(string ipRange)
+ {
+ // Arrange
+
+ // Act & Assert
+ Assert.ThrowsExactly(() =>
+ {
+ _ = new CreateACLRequest(AccountId, ipRange, "my-acl-1");
+ });
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, _, _) => _callbacks.Add((requestPath, request)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/DeleteACLTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/DeleteACLTest.cs
new file mode 100644
index 0000000..ef0ee46
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/DeleteACLTest.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions.ACLs
+{
+ [TestClass]
+ public class DeleteACLTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+ private const string AclId = "23ff594956f20c2a721606e94745a8aa";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = new List();
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [new ResponseInfo(1000, "Message 1")],
+ Errors = [new ResponseInfo(1000, "Error 1")],
+ Result = new Identifier
+ {
+ Id = AclId
+ }
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldDeleteAcl()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.DeleteACL(AccountId, AclId, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ string requestPath = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", requestPath);
+
+ _clientMock.Verify(m => m.DeleteAsync($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.DeleteAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, _, _) => _callbacks.Add(requestPath))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ListACLsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ListACLsTest.cs
new file mode 100644
index 0000000..f8d7988
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ListACLsTest.cs
@@ -0,0 +1,77 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions.ACLs
+{
+ [TestClass]
+ public class ListACLsTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+ private CloudflareResponse> _response;
+ private List _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = new List();
+
+ _response = new CloudflareResponse>
+ {
+ Success = true,
+ Messages = new List { new ResponseInfo(1000, "Message 1") },
+ Errors = new List { new ResponseInfo(1000, "Error 1") },
+ Result = new List
+ {
+ new ACL
+ {
+ Id = "23ff594956f20c2a721606e94745a8aa",
+ IpRange = "192.0.2.0/24",
+ Name = "Test ACL"
+ }
+ }
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldListAcls()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.ListACLs(AccountId, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ string requestPath = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls", requestPath);
+
+ _clientMock.Verify(m => m.GetAsync>($"/accounts/{AccountId}/secondary_dns/acls", null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync>(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, _, _) => _callbacks.Add(requestPath))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/UpdateACLTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/UpdateACLTest.cs
new file mode 100644
index 0000000..289a641
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/UpdateACLTest.cs
@@ -0,0 +1,110 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions.ACLs
+{
+ [TestClass]
+ public class UpdateACLTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+ private const string AclId = "23ff594956f20c2a721606e94745a8aa";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, InternalDnsZoneTransferAclRequest Request)> _callbacks;
+ private UpdateDnsZoneTransferAclRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = new ACL
+ {
+ Id = AclId,
+ IpRange = "192.0.2.53/28",
+ Name = "my-acl-1"
+ }
+ };
+
+ _request = new UpdateDnsZoneTransferAclRequest(
+ accountId: AccountId,
+ aclId: AclId,
+ ipRange: "192.0.2.53/28",
+ name: "my-acl-1"
+ );
+ }
+
+ [TestMethod]
+ public async Task ShouldUpdateAcl()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.UpdateACL(_request, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, request) = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", requestPath);
+
+ Assert.IsNotNull(request);
+ Assert.AreEqual(_request.Name, request.Name);
+ Assert.AreEqual(_request.IpRange, request.IpRange);
+
+ Assert.AreEqual("192.0.2.53", _request.IpRangeBaseAddress.ToString());
+ Assert.AreEqual(28, _request.IpRangeSubnet);
+
+ _clientMock.Verify(m => m.PutAsync($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", It.IsAny(), TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ [DataRow("127.0.0.1/20")]
+ [DataRow("fd00::/56")]
+ public async Task ShouldThrowArumentOutOfRangeExceptionForWrongSubnetDefinition(string ipRange)
+ {
+ // Arrange
+ _request.IpRange = ipRange;
+ var client = GetClient();
+
+ // Act & Assert
+ await Assert.ThrowsExactlyAsync(async () =>
+ {
+ await client.UpdateACL(_request, TestContext.CancellationToken);
+ });
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PutAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, _) => _callbacks.Add((requestPath, request)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ForceAXFRTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ForceAXFRTest.cs
new file mode 100644
index 0000000..1376f17
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ForceAXFRTest.cs
@@ -0,0 +1,75 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions
+{
+ [TestClass]
+ public class ForceAXFRTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, object Request)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = "OK"
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldForceAXFR()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.ForceAXFR(ZoneId, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, request) = _callbacks.First();
+ Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/force_axfr", requestPath);
+
+ Assert.IsNull(request);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/secondary_dns/force_axfr", null, null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny