/* Technitium DNS Server Copyright (C) 2020 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class PrimaryZone : AuthZone { #region variables readonly DnsServer _dnsServer; readonly bool _internal; readonly Timer _notifyTimer; const int NOTIFY_TIMER_INTERVAL = 10000; readonly List _notifyList = new List(); const int NOTIFY_TIMEOUT = 10000; const int NOTIFY_RETRIES = 5; #endregion #region constructor public PrimaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(zoneInfo.Name) { _dnsServer = dnsServer; _disabled = zoneInfo.Disabled; _notifyTimer = new Timer(NotifyTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } public PrimaryZone(DnsServer dnsServer, string name, string primaryNameServer, bool @internal) : base(name) { _dnsServer = dnsServer; _internal = @internal; DnsSOARecord soa = new DnsSOARecord(primaryNameServer, "hostadmin." + name, 1, 14400, 3600, 604800, 900); _entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Refresh, soa) }; _entries[DnsResourceRecordType.NS] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, soa.Refresh, new DnsNSRecord(soa.PrimaryNameServer)) }; _notifyTimer = new Timer(NotifyTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } internal PrimaryZone(DnsServer dnsServer, string name, DnsSOARecord soa, DnsNSRecord ns) : base(name) { _dnsServer = dnsServer; _internal = true; _entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Refresh, soa) }; _entries[DnsResourceRecordType.NS] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, soa.Refresh, ns) }; _notifyTimer = new Timer(NotifyTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } #endregion #region IDisposable bool _disposed; protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_notifyTimer != null) _notifyTimer.Dispose(); } _disposed = true; } #endregion #region private private async void NotifyTimerCallback(object state) { try { IReadOnlyList secondaryNameServers = await GetSecondaryNameServerAddressesAsync(_dnsServer); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) NotifyNameServer(secondaryNameServer); } catch (Exception ex) { LogManager log = _dnsServer.LogManager; if (log != null) log.Write(ex); } } private void NotifyNameServer(NameServerAddress nameServer) { //use notify list to prevent multiple threads from notifying the same name server lock (_notifyList) { if (_notifyList.Contains(nameServer)) return; //already notifying the name server in another thread _notifyList.Add(nameServer); } _ = NotifyNameServerAsync(nameServer); } private async Task NotifyNameServerAsync(NameServerAddress nameServer) { try { DnsClient client = new DnsClient(nameServer); client.Timeout = NOTIFY_TIMEOUT; client.Retries = NOTIFY_RETRIES; DnsDatagram notifyRequest = new DnsDatagram(0, false, DnsOpcode.Notify, true, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN) }, _entries[DnsResourceRecordType.SOA]); DnsDatagram response = await client.ResolveAsync(notifyRequest); switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NotImplemented: { //transaction complete LogManager log = _dnsServer.LogManager; if (log != null) log.Write("DNS Server successfully notified name server for '" + _name + "' zone changes: " + nameServer.ToString()); } break; default: { //transaction failed LogManager log = _dnsServer.LogManager; if (log != null) log.Write("DNS Server received RCODE=" + response.RCODE.ToString() + " from name server for '" + _name + "' zone notification: " + nameServer.ToString()); } break; } } catch (Exception ex) { LogManager log = _dnsServer.LogManager; if (log != null) { log.Write("DNS Server failed to notify name server for '" + _name + "' zone changes: " + nameServer.ToString()); log.Write(ex); } } finally { lock (_notifyList) { _notifyList.Remove(nameServer); } } } #endregion #region public public void IncrementSoaSerial() { DnsResourceRecord record = _entries[DnsResourceRecordType.SOA][0]; DnsSOARecord soa = record.RDATA as DnsSOARecord; uint serial = soa.Serial; if (serial < uint.MaxValue) serial++; else serial = 0; DnsResourceRecord newRecord = new DnsResourceRecord(record.Name, record.Type, record.Class, record.TtlValue, new DnsSOARecord(soa.PrimaryNameServer, soa.ResponsiblePerson, serial, soa.Refresh, soa.Retry, soa.Expire, soa.Minimum)) { Tag = record.Tag }; _entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { newRecord }; } public void NotifyNameServers() { _notifyTimer.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite); } public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { switch (type) { case DnsResourceRecordType.CNAME: throw new InvalidOperationException("Cannot set CNAME record to zone root."); case DnsResourceRecordType.SOA: if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Invalid SOA record."); //remove any resource record info records[0].Tag = null; break; } base.SetRecords(type, records); IncrementSoaSerial(); NotifyNameServers(); } public override void AddRecord(DnsResourceRecord record) { base.AddRecord(record); IncrementSoaSerial(); NotifyNameServers(); } public override bool DeleteRecords(DnsResourceRecordType type) { if (type == DnsResourceRecordType.SOA) throw new InvalidOperationException("Cannot delete SOA record."); if (base.DeleteRecords(type)) { IncrementSoaSerial(); NotifyNameServers(); return true; } return false; } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) { if (type == DnsResourceRecordType.SOA) throw new InvalidOperationException("Cannot delete SOA record."); if (base.DeleteRecord(type, record)) { IncrementSoaSerial(); NotifyNameServers(); return true; } return false; } #endregion #region properties public bool Internal { get { return _internal; } } public override bool Disabled { get { return _disabled; } set { if (_disabled != value) { _disabled = value; if (_disabled) _notifyTimer.Change(Timeout.Infinite, Timeout.Infinite); else NotifyNameServers(); } } } #endregion } }