/*
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
}
}