From b3473ca897b9214edbece7484daa1c581b17569c Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sat, 6 Jun 2020 16:06:35 +0530 Subject: [PATCH] StubZone: implemented zone refresh from primary server. --- DnsServerCore/Dns/Zones/StubZone.cs | 306 ++++++++++++++++++++++++++-- 1 file changed, 292 insertions(+), 14 deletions(-) diff --git a/DnsServerCore/Dns/Zones/StubZone.cs b/DnsServerCore/Dns/Zones/StubZone.cs index d23ca23c..cdea4727 100644 --- a/DnsServerCore/Dns/Zones/StubZone.cs +++ b/DnsServerCore/Dns/Zones/StubZone.cs @@ -17,8 +17,11 @@ along with this program. If not, see . */ +using DnsServerCore.Dns.ResourceRecords; using System; using System.Collections.Generic; +using System.Net; +using System.Threading; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -26,40 +29,315 @@ namespace DnsServerCore.Dns.Zones { public sealed class StubZone : AuthZone { + #region variables + + readonly DnsServer _dnsServer; + + readonly Timer _refreshTimer; + const int REFRESH_TIMER_INITIAL_INTERVAL = 30000; + + const int REFRESH_TIMEOUT = 60000; + const int REFRESH_RETRIES = 5; + + #endregion + #region constructor - public StubZone(string name, DnsSOARecord soa) - : base(name, soa) - { } - - public StubZone(string name, bool disabled) - : base(name) + public StubZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) + : base(zoneInfo.Name) { - _disabled = disabled; + _dnsServer = dnsServer; + + _disabled = zoneInfo.Disabled; + + _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); + } + + public StubZone(DnsServer dnsServer, string name, DnsSOARecord soa) + : base(name, soa) + { + _dnsServer = dnsServer; + + _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); + } + + #endregion + + #region IDisposable + + bool _disposed; + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + if (_refreshTimer != null) + _refreshTimer.Dispose(); + } + + _disposed = true; + } + + #endregion + + #region private + + private void RefreshTimerCallback(object state) + { + if (_disabled) + return; + + DnsResourceRecord record = _entries[DnsResourceRecordType.SOA][0]; + DnsSOARecord soaRecord = record.RDATA as DnsSOARecord; + + try + { + string nsDomain = soaRecord.MasterNameServer; + List nameServers = new List(); + + IReadOnlyList glueRecords = record.GetGlueRecords(); + if (glueRecords.Count > 0) + { + foreach (DnsResourceRecord glueRecord in glueRecords) + { + switch (glueRecord.Type) + { + case DnsResourceRecordType.A: + nameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsARecord).Address)); + break; + + case DnsResourceRecordType.AAAA: + if (_dnsServer.PreferIPv6) + nameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsAAAARecord).Address)); + + break; + } + } + } + else + { + //resolve addresses + DnsDatagram response = _dnsServer.DirectQuery(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.A, DnsClass.IN)); + if (response != null) + { + IReadOnlyList addresses = DnsClient.ParseResponseA(response); + foreach (IPAddress address in addresses) + nameServers.Add(new NameServerAddress(nsDomain, address)); + } + + if (_dnsServer.PreferIPv6) + { + response = _dnsServer.DirectQuery(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.AAAA, DnsClass.IN)); + if (response != null) + { + IReadOnlyList addresses = DnsClient.ParseResponseAAAA(response); + foreach (IPAddress address in addresses) + nameServers.Add(new NameServerAddress(nsDomain, address)); + } + } + } + + //refresh zone + foreach (NameServerAddress nameServer in nameServers) + { + if (RefreshZone(nameServer)) + { + //zone refreshed; set timer for refresh + DnsSOARecord latestSoaRecord = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecord; + _refreshTimer.Change(latestSoaRecord.Refresh * 1000, Timeout.Infinite); + + _dnsServer.AuthZoneManager.SaveZoneFile(_name); + return; + } + } + + //no response from any of the name servers; set timer for retry + _refreshTimer.Change(soaRecord.Retry * 1000, Timeout.Infinite); + } + catch (Exception ex) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write(ex); + + //set timer for retry + _refreshTimer.Change(soaRecord.Retry * 1000, Timeout.Infinite); + } + } + + private bool RefreshZone(NameServerAddress nameServer) + { + try + { + DnsClient client = new DnsClient(nameServer); + + client.Timeout = REFRESH_TIMEOUT; + client.Retries = REFRESH_RETRIES; + + DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN) }); + DnsDatagram soaResponse = client.Resolve(soaRequest); + + if (soaResponse.RCODE != DnsResponseCode.NoError) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + _name + "' stub zone refresh from: " + nameServer.ToString()); + + return false; + } + + if (soaResponse.Answer.Count < 1) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server received an empty response for SOA query for '" + _name + "' stub zone refresh from: " + nameServer.ToString()); + + return false; + } + + DnsSOARecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecord; + DnsSOARecord receivedSoaRecord = soaResponse.Answer[0].RDATA as DnsSOARecord; + + //compare using sequence space arithmetic + if (!currentSoaRecord.IsZoneUpdateAvailable(receivedSoaRecord)) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server successfully refreshed '" + _name + "' stub zone from: " + nameServer.ToString()); + + return true; + } + + //update available; do zone sync + nameServer = new NameServerAddress(nameServer, DnsTransportProtocol.Tcp); + client = new DnsClient(nameServer); + client.Timeout = REFRESH_TIMEOUT; + client.Retries = REFRESH_RETRIES; + + DnsDatagram nsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.NS, DnsClass.IN) }); + DnsDatagram nsResponse = client.Resolve(nsRequest); + + if (nsResponse.RCODE != DnsResponseCode.NoError) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server received RCODE=" + nsResponse.RCODE.ToString() + " for '" + _name + "' stub zone refresh from: " + nameServer.ToString()); + + return false; + } + + if (nsResponse.Answer.Count < 1) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server received an empty response for NS query for '" + _name + "' stub zone from: " + nameServer.ToString()); + + return false; + } + + List allRecords = new List(); + + allRecords.AddRange(nsResponse.Answer); + allRecords.AddRange(soaResponse.Answer); //to sync latest SOA record + + _dnsServer.AuthZoneManager.SyncRecords(_name, allRecords, nsResponse.Additional, true); + + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server successfully refreshed '" + _name + "' stub zone from: " + nameServer.ToString()); + } + + return true; + } + catch (Exception ex) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + { + log.Write("DNS Server failed to refresh '" + _name + "' stub zone from: " + nameServer.ToString()); + log.Write(ex); + } + + return false; + } } #endregion #region public + public void RefreshZone() + { + if (_disabled) + return; + + _refreshTimer.Change(REFRESH_TIMER_INITIAL_INTERVAL, Timeout.Infinite); + } + public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { - throw new InvalidOperationException("Cannot set records for stub zone."); + switch (type) + { + case DnsResourceRecordType.CNAME: + throw new InvalidOperationException("Cannot set CNAME record to zone root."); + + case DnsResourceRecordType.NS: + throw new InvalidOperationException("Cannot set NS records at stub zone root."); + + case DnsResourceRecordType.SOA: + throw new InvalidOperationException("Cannot set SOA record in stub zone."); + + default: + base.SetRecords(type, records); + break; + } } public override void AddRecord(DnsResourceRecord record) { - throw new InvalidOperationException("Cannot add record for stub zone."); - } + switch (record.Type) + { + case DnsResourceRecordType.NS: + throw new InvalidOperationException("Cannot add NS record at stub zone root."); - public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) - { - throw new InvalidOperationException("Cannot delete record for stub zone."); + default: + base.AddRecord(record); + break; + } } public override bool DeleteRecords(DnsResourceRecordType type) { - throw new InvalidOperationException("Cannot delete records for stub zone."); + switch (type) + { + case DnsResourceRecordType.NS: + throw new InvalidOperationException("Cannot delete NS records in stub zone root."); + + case DnsResourceRecordType.SOA: + throw new InvalidOperationException("Cannot delete SOA record."); + + default: + return base.DeleteRecords(type); + } + } + + public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) + { + switch (type) + { + case DnsResourceRecordType.NS: + throw new InvalidOperationException("Cannot delete NS record in stub zone root."); + + case DnsResourceRecordType.SOA: + throw new InvalidOperationException("Cannot delete SOA record."); + + default: + return base.DeleteRecord(type, record); + } } #endregion