/*
Technitium DNS Server
Copyright (C) 2023 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 DnsServerCore.Dns.ResourceRecords;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.Zones
{
class SecondaryZone : ApexZone
{
#region variables
readonly DnsServer _dnsServer;
readonly object _refreshTimerLock = new object();
Timer _refreshTimer;
bool _refreshTimerTriggered;
const int REFRESH_TIMER_INTERVAL = 5000;
const int REFRESH_SOA_TIMEOUT = 10000;
const int REFRESH_XFR_TIMEOUT = 120000;
const int REFRESH_RETRIES = 5;
const int REFRESH_TSIG_FUDGE = 300;
DateTime _expiry;
bool _isExpired;
bool _resync;
#endregion
#region constructor
public SecondaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)
: base(zoneInfo)
{
_dnsServer = dnsServer;
_expiry = zoneInfo.Expiry;
_isExpired = DateTime.UtcNow > _expiry;
_refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
InitNotify(_dnsServer);
}
private SecondaryZone(DnsServer dnsServer, string name)
: base(name)
{
_dnsServer = dnsServer;
_zoneTransfer = AuthZoneTransfer.Deny;
_notify = AuthZoneNotify.None;
_update = AuthZoneUpdate.Deny;
InitNotify(_dnsServer);
}
#endregion
#region static
public static async Task CreateAsync(DnsServer dnsServer, string name, string primaryNameServerAddresses = null, DnsTransportProtocol zoneTransferProtocol = DnsTransportProtocol.Tcp, string tsigKeyName = null)
{
switch (zoneTransferProtocol)
{
case DnsTransportProtocol.Tcp:
case DnsTransportProtocol.Tls:
case DnsTransportProtocol.Quic:
break;
default:
throw new NotSupportedException("Zone transfer protocol is not supported: XFR-over-" + zoneTransferProtocol.ToString().ToUpper());
}
SecondaryZone secondaryZone = new SecondaryZone(dnsServer, name);
DnsQuestionRecord soaQuestion = new DnsQuestionRecord(name, DnsResourceRecordType.SOA, DnsClass.IN);
DnsDatagram soaResponse;
NameServerAddress[] primaryNameServers = null;
if (string.IsNullOrEmpty(primaryNameServerAddresses))
{
soaResponse = await secondaryZone._dnsServer.DirectQueryAsync(soaQuestion);
}
else
{
primaryNameServers = primaryNameServerAddresses.Split(delegate (string address)
{
NameServerAddress nameServer = NameServerAddress.Parse(address);
if (nameServer.Protocol != zoneTransferProtocol)
nameServer = nameServer.ChangeProtocol(zoneTransferProtocol);
return nameServer;
}, ',');
DnsClient dnsClient = new DnsClient(primaryNameServers);
foreach (NameServerAddress nameServerAddress in dnsClient.Servers)
{
if (nameServerAddress.IsIPEndPointStale)
await nameServerAddress.ResolveIPAddressAsync(secondaryZone._dnsServer, secondaryZone._dnsServer.PreferIPv6);
}
dnsClient.Proxy = secondaryZone._dnsServer.Proxy;
dnsClient.PreferIPv6 = secondaryZone._dnsServer.PreferIPv6;
DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { soaQuestion }, null, null, null, dnsServer.UdpPayloadSize);
if (string.IsNullOrEmpty(tsigKeyName))
soaResponse = await dnsClient.ResolveAsync(soaRequest);
else if ((dnsServer.TsigKeys is not null) && dnsServer.TsigKeys.TryGetValue(tsigKeyName, out TsigKey key))
soaResponse = await dnsClient.ResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
else
throw new DnsServerException("No such TSIG key was found configured: " + tsigKeyName);
}
if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA))
throw new DnsServerException("DNS Server failed to find SOA record for: " + name);
DnsSOARecordData receivedSoa = soaResponse.Answer[0].RDATA as DnsSOARecordData;
DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum);
DnsResourceRecord[] soaRR = new DnsResourceRecord[] { new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Refresh, soa) };
AuthRecordInfo authRecordInfo = soaRR[0].GetAuthRecordInfo();
authRecordInfo.PrimaryNameServers = primaryNameServers;
authRecordInfo.ZoneTransferProtocol = zoneTransferProtocol;
authRecordInfo.TsigKeyName = tsigKeyName;
secondaryZone._entries[DnsResourceRecordType.SOA] = soaRR;
secondaryZone._isExpired = true; //new secondary zone is considered expired till it refreshes
secondaryZone._refreshTimer = new Timer(secondaryZone.RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
return secondaryZone;
}
#endregion
#region IDisposable
bool _disposed;
protected override void Dispose(bool disposing)
{
try
{
if (_disposed)
return;
if (disposing)
{
lock (_refreshTimerLock)
{
if (_refreshTimer != null)
{
_refreshTimer.Dispose();
_refreshTimer = null;
}
}
}
_disposed = true;
}
finally
{
base.Dispose(disposing);
}
}
#endregion
#region private
private async void RefreshTimerCallback(object state)
{
try
{
if (_disabled && !_resync)
return;
_isExpired = DateTime.UtcNow > _expiry;
//get primary name server addresses
IReadOnlyList primaryNameServers = await GetPrimaryNameServerAddressesAsync(_dnsServer);
DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];
DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;
if (primaryNameServers.Count == 0)
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server could not find primary name server IP addresses for secondary zone: " + (_name == "" ? "" : _name));
//set timer for retry
ResetRefreshTimer(currentSoa.Retry * 1000);
_syncFailed = true;
return;
}
AuthRecordInfo recordInfo = currentSoaRecord.GetAuthRecordInfo();
TsigKey key = null;
if (!string.IsNullOrEmpty(recordInfo.TsigKeyName) && ((_dnsServer.TsigKeys is null) || !_dnsServer.TsigKeys.TryGetValue(recordInfo.TsigKeyName, out key)))
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server does not have TSIG key '" + recordInfo.TsigKeyName + "' configured for refreshing secondary zone: " + (_name == "" ? "" : _name));
//set timer for retry
ResetRefreshTimer(currentSoa.Retry * 1000);
_syncFailed = true;
return;
}
//refresh zone
if (await RefreshZoneAsync(primaryNameServers, recordInfo.ZoneTransferProtocol, key))
{
//zone refreshed; set timer for refresh
DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
ResetRefreshTimer(latestSoa.Refresh * 1000);
_syncFailed = false;
_expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire);
_isExpired = false;
_resync = false;
_dnsServer.AuthZoneManager.SaveZoneFile(_name);
return;
}
//no response from any of the name servers; set timer for retry
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
ResetRefreshTimer(soa.Retry * 1000);
_syncFailed = true;
}
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write(ex);
//set timer for retry
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
ResetRefreshTimer(soa.Retry * 1000);
_syncFailed = true;
}
finally
{
_refreshTimerTriggered = false;
}
}
private void ResetRefreshTimer(long dueTime)
{
lock (_refreshTimerLock)
{
if (_refreshTimer != null)
_refreshTimer.Change(dueTime, Timeout.Infinite);
}
}
private async Task RefreshZoneAsync(IReadOnlyList primaryNameServers, DnsTransportProtocol zoneTransferProtocol, TsigKey key)
{
try
{
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server has started zone refresh for secondary zone: " + (_name == "" ? "" : _name));
}
DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];
DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;
if (!_resync)
{
//check for update; use UDP transport
List udpNameServers = new List(primaryNameServers.Count);
foreach (NameServerAddress primaryNameServer in primaryNameServers)
{
if (primaryNameServer.Protocol == DnsTransportProtocol.Udp)
udpNameServers.Add(primaryNameServer);
else
udpNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Udp));
}
DnsClient client = new DnsClient(udpNameServers);
client.Proxy = _dnsServer.Proxy;
client.PreferIPv6 = _dnsServer.PreferIPv6;
client.Timeout = REFRESH_SOA_TIMEOUT;
client.Retries = REFRESH_RETRIES;
client.Concurrency = 1;
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) }, null, null, null, _dnsServer.UdpPayloadSize);
DnsDatagram soaResponse;
if (key is null)
soaResponse = await client.ResolveAsync(soaRequest);
else
soaResponse = await client.ResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);
if (soaResponse.RCODE != DnsResponseCode.NoError)
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
return false;
}
if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase))
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
return false;
}
DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];
DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;
//compare using sequence space arithmetic
if (!currentSoa.IsZoneUpdateAvailable(receivedSoa))
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + soaResponse.Metadata.NameServer.ToString());
return true;
}
}
//update available; do zone transfer with TLS, QUIC, or TCP transport
List updatedNameServers = new List(primaryNameServers.Count);
switch (zoneTransferProtocol)
{
case DnsTransportProtocol.Tls:
case DnsTransportProtocol.Quic:
//change name server protocol to TLS/QUIC
foreach (NameServerAddress primaryNameServer in primaryNameServers)
{
if (primaryNameServer.Protocol == zoneTransferProtocol)
updatedNameServers.Add(primaryNameServer);
else
updatedNameServers.Add(primaryNameServer.ChangeProtocol(zoneTransferProtocol));
}
break;
default:
//change name server protocol to TCP
foreach (NameServerAddress primaryNameServer in primaryNameServers)
{
if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp)
updatedNameServers.Add(primaryNameServer);
else
updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp));
}
break;
}
DnsClient xfrClient = new DnsClient(updatedNameServers);
xfrClient.Proxy = _dnsServer.Proxy;
xfrClient.PreferIPv6 = _dnsServer.PreferIPv6;
xfrClient.Timeout = REFRESH_XFR_TIMEOUT;
xfrClient.Retries = REFRESH_RETRIES;
xfrClient.Concurrency = 1;
bool doIXFR = !_isExpired && !_resync;
while (true)
{
DnsQuestionRecord xfrQuestion;
IReadOnlyList xfrAuthority;
if (doIXFR)
{
xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.IXFR, DnsClass.IN);
xfrAuthority = new DnsResourceRecord[] { currentSoaRecord };
}
else
{
xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.AXFR, DnsClass.IN);
xfrAuthority = null;
}
DnsDatagram xfrRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { xfrQuestion }, null, xfrAuthority);
DnsDatagram xfrResponse;
if (key is null)
xfrResponse = await xfrClient.ResolveAsync(xfrRequest);
else
xfrResponse = await xfrClient.ResolveAsync(xfrRequest, key, REFRESH_TSIG_FUDGE);
if (doIXFR && (xfrResponse.RCODE == DnsResponseCode.NotImplemented))
{
doIXFR = false;
continue;
}
if (xfrResponse.RCODE != DnsResponseCode.NoError)
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
return false;
}
if (xfrResponse.Answer.Count < 1)
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server received an empty response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
return false;
}
if (!_name.Equals(xfrResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase) || (xfrResponse.Answer[0].Type != DnsResourceRecordType.SOA) || (xfrResponse.Answer[0].RDATA is not DnsSOARecordData xfrSoa))
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server received invalid response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
return false;
}
if (_resync || currentSoa.IsZoneUpdateAvailable(xfrSoa))
{
xfrResponse = xfrResponse.Join(); //join multi message response
if (doIXFR)
{
IReadOnlyList historyRecords = _dnsServer.AuthZoneManager.SyncIncrementalZoneTransferRecords(_name, xfrResponse.Answer);
if (historyRecords.Count > 0)
CommitZoneHistory(historyRecords);
else
ClearZoneHistory(); //AXFR response was received
}
else
{
_dnsServer.AuthZoneManager.SyncZoneTransferRecords(_name, xfrResponse.Answer);
ClearZoneHistory();
}
//trigger notify
TriggerNotify();
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString());
}
else
{
LogManager log = _dnsServer.LogManager;
if (log != null)
log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + xfrResponse.Metadata.NameServer.ToString());
}
return true;
}
}
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
if (log != null)
{
string strNameServers = null;
foreach (NameServerAddress nameServer in primaryNameServers)
{
if (strNameServers == null)
strNameServers = nameServer.ToString();
else
strNameServers += ", " + nameServer.ToString();
}
log.Write("DNS Server failed to refresh '" + (_name == "" ? "" : _name) + "' secondary zone from: " + strNameServers + "\r\n" + ex.ToString());
}
return false;
}
}
private void CommitZoneHistory(IReadOnlyList historyRecords)
{
lock (_zoneHistory)
{
historyRecords[0].GetAuthRecordInfo().DeletedOn = DateTime.UtcNow;
//write history
_zoneHistory.AddRange(historyRecords);
CleanupHistory(_zoneHistory);
}
}
private void ClearZoneHistory()
{
lock (_zoneHistory)
{
_zoneHistory.Clear();
}
}
#endregion
#region public
public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL)
{
if (_disabled)
return;
if (_refreshTimerTriggered)
return;
_refreshTimerTriggered = true;
ResetRefreshTimer(refreshInterval);
}
public void TriggerResync()
{
if (_refreshTimerTriggered)
return;
_resync = true;
_refreshTimerTriggered = true;
ResetRefreshTimer(0);
}
public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records)
{
switch (type)
{
case DnsResourceRecordType.SOA:
if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Invalid SOA record.");
DnsResourceRecord existingSoaRecord = _entries[DnsResourceRecordType.SOA][0];
DnsResourceRecord newSoaRecord = records[0];
existingSoaRecord.CopyRecordInfoFrom(newSoaRecord);
break;
default:
throw new InvalidOperationException("Cannot set records in secondary zone.");
}
}
public override void AddRecord(DnsResourceRecord record)
{
throw new InvalidOperationException("Cannot add record in secondary zone.");
}
public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)
{
throw new InvalidOperationException("Cannot delete record in secondary zone.");
}
public override bool DeleteRecords(DnsResourceRecordType type)
{
throw new InvalidOperationException("Cannot delete records in secondary zone.");
}
public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)
{
throw new InvalidOperationException("Cannot update record in secondary zone.");
}
#endregion
#region properties
public override AuthZoneUpdate Update
{
get { return _update; }
set { throw new InvalidOperationException(); }
}
public DateTime Expiry
{ get { return _expiry; } }
public bool IsExpired
{ get { return _isExpired; } }
public override bool Disabled
{
get { return _disabled; }
set
{
if (_disabled != value)
{
_disabled = value;
if (_disabled)
{
DisableNotifyTimer();
ResetRefreshTimer(Timeout.Infinite);
}
else
{
TriggerNotify();
TriggerRefresh();
}
}
}
}
public override bool IsActive
{
get { return !_disabled && !_isExpired; }
}
#endregion
}
}