mirror of
https://github.com/TechnitiumSoftware/DnsServer.git
synced 2026-10-12 02:09:51 +00:00
2245 lines
98 KiB
C#
2245 lines
98 KiB
C#
/*
|
|
Technitium DNS Server
|
|
Copyright (C) 2026 Shreyas Zare ([email protected])
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
using DnsServerCore.Auth;
|
|
using DnsServerCore.Dns;
|
|
using DnsServerCore.Dns.Dnssec;
|
|
using DnsServerCore.Dns.ResourceRecords;
|
|
using DnsServerCore.Dns.Zones;
|
|
using DnsServerCore.HttpApi;
|
|
using DnsServerCore.HttpApi.Models;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using TechnitiumLibrary;
|
|
using TechnitiumLibrary.IO;
|
|
using TechnitiumLibrary.Net;
|
|
using TechnitiumLibrary.Net.Dns;
|
|
using TechnitiumLibrary.Net.Dns.ResourceRecords;
|
|
|
|
namespace DnsServerCore.Cluster
|
|
{
|
|
sealed class ClusterManager : IDisposable
|
|
{
|
|
#region variables
|
|
|
|
const ushort HEARTBEAT_REFRESH_INTERVAL_SECONDS = 30;
|
|
const ushort HEARTBEAT_RETRY_INTERVAL_SECONDS = 10;
|
|
const ushort CONFIG_REFRESH_INTERVAL_SECONDS = 900;
|
|
const ushort CONFIG_RETRY_INTERVAL_SECONDS = 60;
|
|
|
|
readonly DnsWebService _dnsWebService;
|
|
|
|
string _clusterDomain;
|
|
ushort _heartbeatRefreshIntervalSeconds;
|
|
ushort _heartbeatRetryIntervalSeconds;
|
|
ushort _configRefreshIntervalSeconds;
|
|
ushort _configRetryIntervalSeconds;
|
|
DateTime _configLastSynced;
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> _clusterNodes;
|
|
|
|
readonly SemaphoreSlim _configRefreshLock = new SemaphoreSlim(1, 1);
|
|
readonly Timer _configRefreshTimer;
|
|
bool _configRefreshTimerTriggered;
|
|
IReadOnlyCollection<string> _configRefreshIncludeZones;
|
|
const int CONFIG_REFRESH_TIMER_INTERVAL = 5000;
|
|
|
|
readonly Timer _notifyAllSecondaryNodesTimer;
|
|
bool _notifyAllSecondaryNodesTimerTriggered;
|
|
const int NOTIFY_ALL_SECONDARY_NODES_TIMER_INTERVAL = 5000;
|
|
|
|
readonly Timer _clusterUpdateForSecondaryNodeChangesTimer;
|
|
bool _clusterUpdateForSecondaryNodeChangesTimerTriggered;
|
|
const int CLUSTER_UPDATE_FOR_SECONDARY_NODE_CHANGES_TIMER_INTERVAL = 5000;
|
|
|
|
readonly Lock _saveLock = new Lock();
|
|
bool _pendingSave;
|
|
readonly Timer _saveTimer;
|
|
const int SAVE_TIMER_INITIAL_INTERVAL = 5000;
|
|
|
|
volatile int _recordUpdateForMemberZonesId;
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
public ClusterManager(DnsWebService dnsWebService)
|
|
{
|
|
_dnsWebService = dnsWebService;
|
|
|
|
_configRefreshTimer = new Timer(ConfigRefreshTimerCallbackAsync);
|
|
_notifyAllSecondaryNodesTimer = new Timer(NotifyAllSecondaryNodesTimerCallbackAsync);
|
|
_clusterUpdateForSecondaryNodeChangesTimer = new Timer(ClusterUpdateForSecondaryNodeChangesTimerCallbackAsync);
|
|
|
|
_saveTimer = new Timer(delegate (object state)
|
|
{
|
|
lock (_saveLock)
|
|
{
|
|
if (_pendingSave)
|
|
{
|
|
try
|
|
{
|
|
SaveConfigFileInternal();
|
|
_pendingSave = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write(ex);
|
|
|
|
//set timer to retry again
|
|
_saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
LoadConfigFile();
|
|
|
|
if (ClusterInitialized)
|
|
{
|
|
//find and fix Cluster API token
|
|
bool foundClusterApiToken = false;
|
|
|
|
foreach (UserSession session in _dnsWebService.AuthManager.Sessions)
|
|
{
|
|
if (session.Type == UserSessionType.ClusterApiToken)
|
|
{
|
|
foundClusterApiToken = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundClusterApiToken)
|
|
{
|
|
Group administrators = _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS);
|
|
|
|
foreach (UserSession session in _dnsWebService.AuthManager.Sessions)
|
|
{
|
|
if ((session.Type == UserSessionType.ApiToken) && _clusterDomain.Equals(session.TokenName, StringComparison.OrdinalIgnoreCase) && session.User.IsMemberOfGroup(administrators))
|
|
{
|
|
session.UpgradeToClusterApiToken();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
|
|
bool _disposed;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_configRefreshTimer?.Dispose();
|
|
_notifyAllSecondaryNodesTimer?.Dispose();
|
|
_clusterUpdateForSecondaryNodeChangesTimer?.Dispose();
|
|
|
|
DisposeAllNodes();
|
|
|
|
lock (_saveLock)
|
|
{
|
|
_saveTimer?.Dispose();
|
|
|
|
if (_pendingSave)
|
|
{
|
|
try
|
|
{
|
|
SaveConfigFileInternal();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write(ex);
|
|
}
|
|
finally
|
|
{
|
|
_pendingSave = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
_configRefreshLock?.Dispose();
|
|
|
|
_disposed = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region config
|
|
|
|
private void LoadConfigFile()
|
|
{
|
|
string configFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.config");
|
|
|
|
try
|
|
{
|
|
DisposeAllNodes(); //dispose existing nodes, if any
|
|
|
|
using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read))
|
|
{
|
|
ReadConfigFrom(fS);
|
|
}
|
|
|
|
InitializeHeartbeatTimerFor(_clusterNodes);
|
|
UpdateConfigRefreshTimer();
|
|
|
|
_dnsWebService.LogManager.Write("DNS Server Cluster config file was loaded: " + configFile);
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
//do nothing
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write("DNS Server encountered an error while loading the Cluster config file: " + configFile, ex);
|
|
}
|
|
}
|
|
|
|
public void LoadConfig(Stream s)
|
|
{
|
|
lock (_saveLock)
|
|
{
|
|
DisposeAllNodes(); //dispose existing nodes, if any
|
|
|
|
ReadConfigFrom(s);
|
|
|
|
InitializeHeartbeatTimerFor(_clusterNodes);
|
|
UpdateConfigRefreshTimer();
|
|
|
|
//save config file
|
|
SaveConfigFileInternal();
|
|
|
|
if (_pendingSave)
|
|
{
|
|
_pendingSave = false;
|
|
_saveTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateConfigRefreshTimer(int refreshInterval = CONFIG_REFRESH_TIMER_INTERVAL)
|
|
{
|
|
//ensure that the new refresh interval is applied using lock
|
|
_configRefreshLock.Wait();
|
|
try
|
|
{
|
|
if (ClusterInitialized && (GetSelfNode().Type == ClusterNodeType.Secondary))
|
|
_configRefreshTimer.Change(refreshInterval, Timeout.Infinite); //start config refresh timer only for secondary nodes
|
|
else
|
|
_configRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
finally
|
|
{
|
|
_configRefreshLock.Release();
|
|
}
|
|
}
|
|
|
|
private void StopConfigRefreshTimer()
|
|
{
|
|
//ensure that the timer is stopped using lock
|
|
_configRefreshLock.Wait();
|
|
try
|
|
{
|
|
_configRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
finally
|
|
{
|
|
_configRefreshLock.Release();
|
|
}
|
|
}
|
|
|
|
private void SaveConfigFileInternal()
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new InvalidOperationException();
|
|
|
|
string tmpConfigFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.tmp");
|
|
string configFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.config");
|
|
|
|
using (FileStream fS = new FileStream(tmpConfigFile, FileMode.Create, FileAccess.Write))
|
|
{
|
|
WriteConfigTo(fS);
|
|
}
|
|
|
|
File.Move(tmpConfigFile, configFile, true);
|
|
|
|
_dnsWebService.LogManager.Write("DNS Server Cluster config file was saved: " + configFile);
|
|
}
|
|
|
|
public void SaveConfigFile()
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new InvalidOperationException();
|
|
|
|
lock (_saveLock)
|
|
{
|
|
if (_pendingSave)
|
|
return;
|
|
|
|
_pendingSave = true;
|
|
_saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
|
|
}
|
|
}
|
|
|
|
private void UnloadAndDeleteConfigFile()
|
|
{
|
|
StopConfigRefreshTimer();
|
|
|
|
DisposeAllNodes(); //dispose existing nodes, if any
|
|
|
|
lock (_saveLock)
|
|
{
|
|
//unload
|
|
_clusterDomain = null;
|
|
_configLastSynced = default;
|
|
_clusterNodes = null;
|
|
|
|
//delete config file
|
|
string configFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.config");
|
|
|
|
try
|
|
{
|
|
if (File.Exists(configFile))
|
|
{
|
|
File.Delete(configFile);
|
|
|
|
_dnsWebService.LogManager.Write("DNS Server Cluster config file was deleted: " + configFile);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write("DNS Server encountered an error while deleting the Cluster config file: " + configFile, ex);
|
|
}
|
|
|
|
if (_pendingSave)
|
|
{
|
|
_pendingSave = false;
|
|
_saveTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadConfigFrom(Stream s)
|
|
{
|
|
if (Encoding.ASCII.GetString(s.ReadExactly(2)) != "CL") //format
|
|
throw new InvalidDataException("DNS Server Cluster config file format is invalid.");
|
|
|
|
BinaryReader bR = new BinaryReader(s);
|
|
|
|
int version = bR.ReadByte();
|
|
switch (version)
|
|
{
|
|
case 1:
|
|
_clusterDomain = bR.ReadString();
|
|
_heartbeatRefreshIntervalSeconds = bR.ReadUInt16();
|
|
_heartbeatRetryIntervalSeconds = bR.ReadUInt16();
|
|
_configRefreshIntervalSeconds = bR.ReadUInt16();
|
|
_configRetryIntervalSeconds = bR.ReadUInt16();
|
|
_configLastSynced = s.ReadDateTime();
|
|
|
|
Dictionary<int, ClusterNode> clusterNodes = null;
|
|
int count = bR.ReadByte();
|
|
|
|
if (count > 0)
|
|
{
|
|
clusterNodes = new Dictionary<int, ClusterNode>(count);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
ClusterNode node = new ClusterNode(this, bR);
|
|
clusterNodes.TryAdd(node.Id, node);
|
|
}
|
|
}
|
|
|
|
_clusterNodes = clusterNodes;
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidDataException("DNS Server Cluster config version not supported.");
|
|
}
|
|
}
|
|
|
|
private void WriteConfigTo(Stream s)
|
|
{
|
|
BinaryWriter bW = new BinaryWriter(s);
|
|
|
|
bW.Write(Encoding.ASCII.GetBytes("CL")); //format
|
|
bW.Write((byte)1); //version
|
|
|
|
bW.Write(_clusterDomain);
|
|
bW.Write(_heartbeatRefreshIntervalSeconds);
|
|
bW.Write(_heartbeatRetryIntervalSeconds);
|
|
bW.Write(_configRefreshIntervalSeconds);
|
|
bW.Write(_configRetryIntervalSeconds);
|
|
s.WriteDateTime(_configLastSynced);
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
if (clusterNodes is null)
|
|
{
|
|
bW.Write((byte)0);
|
|
}
|
|
else
|
|
{
|
|
bW.Write(Convert.ToByte(clusterNodes.Count));
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> node in clusterNodes)
|
|
node.Value.WriteTo(bW);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region private
|
|
|
|
private void DisposeAllNodes()
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
if (clusterNodes is not null)
|
|
{
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
clusterNode.Value.Dispose();
|
|
}
|
|
}
|
|
|
|
private static void InitializeHeartbeatTimerFor(IReadOnlyDictionary<int, ClusterNode> clusterNodes)
|
|
{
|
|
//start heartbeat timers for all nodes except self node
|
|
foreach (KeyValuePair<int, ClusterNode> node in clusterNodes)
|
|
{
|
|
if (node.Value.State == ClusterNodeState.Self)
|
|
continue;
|
|
|
|
node.Value.InitializeHeartbeatTimer();
|
|
}
|
|
}
|
|
|
|
private void UpdateHeartbeatTimerForAllClusterNodes()
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
if (clusterNodes is not null)
|
|
{
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Value.State == ClusterNodeState.Self)
|
|
continue;
|
|
|
|
clusterNode.Value.UpdateHeartbeatTimer();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DeleteAllClusterConfig()
|
|
{
|
|
//delete cluster catalog zone
|
|
string clusterCatalogDomain = "cluster-catalog." + _clusterDomain;
|
|
|
|
AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);
|
|
if (clusterCatalogZoneInfo is not null)
|
|
{
|
|
if (_dnsWebService.DnsServer.AuthZoneManager.DeleteZone(clusterCatalogZoneInfo, true))
|
|
_dnsWebService.AuthManager.RemoveAllPermissions(PermissionSection.Zones, clusterCatalogDomain);
|
|
}
|
|
|
|
//remove TSIG key for cluster catalog zone
|
|
{
|
|
IReadOnlyDictionary<string, TsigKey> existingKeys = _dnsWebService.DnsServer.TsigKeys;
|
|
if (existingKeys is not null)
|
|
{
|
|
Dictionary<string, TsigKey> updatedKeys = new Dictionary<string, TsigKey>(existingKeys);
|
|
updatedKeys.Remove(clusterCatalogDomain);
|
|
|
|
_dnsWebService.DnsServer.TsigKeys = updatedKeys;
|
|
}
|
|
}
|
|
|
|
//delete cluster API token
|
|
{
|
|
foreach (UserSession session in _dnsWebService.AuthManager.Sessions)
|
|
{
|
|
if (session.Type == UserSessionType.ClusterApiToken)
|
|
{
|
|
_dnsWebService.AuthManager.DeleteSession(session.Token);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//finalize
|
|
if (_dnsWebService.DnsServer.ServerDomain.EndsWith("." + _clusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
_dnsWebService.DnsServer.ServerDomain = _dnsWebService.DnsServer.ServerDomain.Substring(0, _dnsWebService.DnsServer.ServerDomain.Length - (_clusterDomain.Length + 1));
|
|
|
|
//save all changes
|
|
_dnsWebService.DnsServer.SaveConfigFile();
|
|
_dnsWebService.AuthManager.SaveConfigFile();
|
|
|
|
UnloadAndDeleteConfigFile();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region primary node
|
|
|
|
public void InitializeCluster(string clusterDomain, IReadOnlyList<IPAddress> primaryNodeIpAddresses, UserSession session)
|
|
{
|
|
if (ClusterInitialized)
|
|
throw new DnsServerException("Failed to initialize Cluster: the Cluster is already initialized.");
|
|
|
|
if (!_dnsWebService.IsWebServiceTlsEnabled)
|
|
throw new InvalidOperationException();
|
|
|
|
if (session.User.Type != UserType.Local)
|
|
throw new DnsServerException("Failed to initialize Cluster: only a local administrator user can initialize cluster. Please login with a local administrator user account and try again.");
|
|
|
|
clusterDomain = clusterDomain.ToLowerInvariant();
|
|
|
|
//create self node
|
|
string serverDomain = _dnsWebService.DnsServer.ServerDomain;
|
|
if (!serverDomain.EndsWith("." + clusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
int x = serverDomain.IndexOf('.');
|
|
if (x < 0)
|
|
serverDomain = serverDomain + "." + clusterDomain;
|
|
else
|
|
serverDomain = string.Concat(serverDomain.AsSpan(0, x), ".", clusterDomain);
|
|
}
|
|
|
|
Uri primaryNodeUrl = new Uri($"https://{serverDomain}:{_dnsWebService.WebServiceTlsPort}/");
|
|
|
|
ClusterNode selfPrimaryNode = new ClusterNode(this, RandomNumberGenerator.GetInt32(int.MaxValue), primaryNodeUrl, primaryNodeIpAddresses, ClusterNodeType.Primary, ClusterNodeState.Self);
|
|
|
|
//create cluster primary zone
|
|
AuthZoneInfo clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterDomain);
|
|
if (clusterZoneInfo is null)
|
|
{
|
|
clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(clusterDomain);
|
|
if (clusterZoneInfo is null)
|
|
throw new DnsServerException($"Failed to initialize Cluster: failed to create the Cluster zone '{clusterDomain}'. Please try again.");
|
|
}
|
|
else if (clusterZoneInfo.Type != AuthZoneType.Primary)
|
|
{
|
|
throw new DnsServerException($"Failed to initialize Cluster: the zone '{clusterZoneInfo.Name}' already exists and is not a Primary zone. Please delete the existing zone or use a different Cluster domain name.");
|
|
}
|
|
|
|
//create cluster catalog zone
|
|
string clusterCatalogDomain = "cluster-catalog." + clusterDomain;
|
|
|
|
AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);
|
|
if (clusterCatalogZoneInfo is null)
|
|
{
|
|
clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateCatalogZone(clusterCatalogDomain);
|
|
if (clusterCatalogZoneInfo is null)
|
|
throw new DnsServerException($"Failed to initialize Cluster: failed to create the Cluster Catalog zone '{clusterCatalogDomain}'. Please try again.");
|
|
}
|
|
else if (clusterCatalogZoneInfo.Type != AuthZoneType.Catalog)
|
|
{
|
|
throw new DnsServerException($"Failed to initialize Cluster: the zone '{clusterCatalogZoneInfo.Name}' already exists and is not a Catalog zone. Please delete the existing zone or use a different Cluster domain name.");
|
|
}
|
|
|
|
//set cluster primary zone permissions
|
|
_dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View);
|
|
|
|
//set cluster catalog zone permissions
|
|
_dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View);
|
|
|
|
//ensure cluster zone is a member of cluster catalog zone
|
|
if (clusterZoneInfo.CatalogZoneName is null)
|
|
_dnsWebService.DnsServer.AuthZoneManager.AddCatalogMemberZone(clusterCatalogZoneInfo.Name, clusterZoneInfo);
|
|
else if (!clusterZoneInfo.CatalogZoneName.Equals(clusterCatalogZoneInfo.Name, StringComparison.OrdinalIgnoreCase))
|
|
_dnsWebService.DnsServer.AuthZoneManager.ChangeCatalogMemberZoneOwnership(clusterZoneInfo, clusterCatalogZoneInfo.Name);
|
|
|
|
//sign cluster zone
|
|
if (clusterZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)
|
|
{
|
|
DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.KeySigningKey);
|
|
DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.ZoneSigningKey);
|
|
zskPrivateKey.RolloverDays = 90;
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZone(clusterZoneInfo.Name, kskPrivateKey, zskPrivateKey, 3600, false);
|
|
}
|
|
|
|
//create TSIG key for cluster catalog zone if it does not exist
|
|
{
|
|
IReadOnlyDictionary<string, TsigKey> existingKeys = _dnsWebService.DnsServer.TsigKeys;
|
|
if (existingKeys is null)
|
|
{
|
|
Dictionary<string, TsigKey> updatedKeys = new Dictionary<string, TsigKey>();
|
|
updatedKeys[clusterCatalogDomain] = new TsigKey(clusterCatalogDomain, TsigAlgorithm.HMAC_SHA256);
|
|
|
|
_dnsWebService.DnsServer.TsigKeys = updatedKeys;
|
|
}
|
|
else if (!existingKeys.ContainsKey(clusterCatalogDomain))
|
|
{
|
|
Dictionary<string, TsigKey> updatedKeys = new Dictionary<string, TsigKey>(existingKeys);
|
|
updatedKeys[clusterCatalogDomain] = new TsigKey(clusterCatalogDomain, TsigAlgorithm.HMAC_SHA256);
|
|
|
|
_dnsWebService.DnsServer.TsigKeys = updatedKeys;
|
|
}
|
|
}
|
|
|
|
//create cluster API token if it does not exist
|
|
{
|
|
List<UserSession> userSessions = _dnsWebService.AuthManager.GetSessions(session.User);
|
|
bool apiTokenExists = false;
|
|
|
|
foreach (UserSession existingSession in userSessions)
|
|
{
|
|
if (existingSession.Type == UserSessionType.ClusterApiToken)
|
|
{
|
|
apiTokenExists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!apiTokenExists)
|
|
_dnsWebService.AuthManager.CreateSession(UserSessionType.ClusterApiToken, clusterZoneInfo.Name, session.User.Username, session.LastSeenRemoteAddress, session.LastSeenUserAgent);
|
|
}
|
|
|
|
//dispose existing nodes, if any
|
|
DisposeAllNodes();
|
|
|
|
//initialize cluster
|
|
_clusterNodes = new Dictionary<int, ClusterNode>(1)
|
|
{
|
|
[selfPrimaryNode.Id] = selfPrimaryNode
|
|
};
|
|
|
|
_clusterDomain = clusterZoneInfo.Name;
|
|
_heartbeatRefreshIntervalSeconds = HEARTBEAT_REFRESH_INTERVAL_SECONDS;
|
|
_heartbeatRetryIntervalSeconds = HEARTBEAT_RETRY_INTERVAL_SECONDS;
|
|
_configRefreshIntervalSeconds = CONFIG_REFRESH_INTERVAL_SECONDS;
|
|
_configRetryIntervalSeconds = CONFIG_RETRY_INTERVAL_SECONDS;
|
|
|
|
//update cluster primary zone and save zone file
|
|
FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values
|
|
RemoveAllClusterPrimaryZoneNSRecords(); //remove all existing NS records
|
|
AddClusterPrimaryZoneRecordsFor(selfPrimaryNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);
|
|
|
|
//update cluster catalog zone ACLs, TSIG key name and save zone file
|
|
UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo);
|
|
|
|
//finalize
|
|
_dnsWebService.DnsServer.ServerDomain = selfPrimaryNode.Name;
|
|
|
|
//save all changes
|
|
_dnsWebService.DnsServer.SaveConfigFile(true);
|
|
_dnsWebService.AuthManager.SaveConfigFile(true);
|
|
SaveConfigFile();
|
|
}
|
|
|
|
public void DeleteCluster(bool forceDelete)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to delete Cluster: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to delete Cluster: only a Primary node can delete the Cluster.");
|
|
|
|
if (!forceDelete && (_clusterNodes.Count > 1))
|
|
throw new DnsServerException("Failed to delete Cluster: please remove all Secondary nodes before deleting the Cluster.");
|
|
|
|
DeleteAllClusterConfig();
|
|
}
|
|
|
|
public ClusterNode JoinCluster(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to add Secondary node: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to add Secondary node: only a Primary node can add a Secondary node to the Cluster.");
|
|
|
|
string secondaryNodeDomain = secondaryNodeUrl.Host.ToLowerInvariant();
|
|
|
|
if (!secondaryNodeDomain.EndsWith("." + _clusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
throw new DnsServerException("Failed to add Secondary node: the Secondary node domain name must be a subdomain of the Cluster domain name.");
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;
|
|
|
|
//validate for duplicate names
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
if (existingClusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))
|
|
throw new DnsServerException("Failed to add Secondary node: A node with the same DNS Server Domain Name already exists in the Cluster. Please try again after changing the Secondary node's DNS Server Domain Name.");
|
|
}
|
|
|
|
//add secondary node to cluster nodes
|
|
ClusterNode secondaryNode = new ClusterNode(this, secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Unknown);
|
|
Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + 1);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
updatedClusterNodes[existingClusterNode.Value.Id] = existingClusterNode.Value;
|
|
|
|
if (!updatedClusterNodes.TryAdd(secondaryNode.Id, secondaryNode))
|
|
throw new DnsServerException("Failed to add Secondary node: node ID already exists in the Cluster. Please try again.");
|
|
|
|
if (updatedClusterNodes.Count > 255)
|
|
throw new DnsServerException("Failed to add Secondary node: a maximum of 255 nodes are supported by the Cluster.");
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes);
|
|
if (!ReferenceEquals(originalValue, existingClusterNodes))
|
|
throw new DnsServerException("Failed to add Secondary node: please try again.");
|
|
|
|
secondaryNode.InitializeHeartbeatTimer();
|
|
|
|
//update cluster zone and save zone file
|
|
FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values
|
|
AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate);
|
|
|
|
//update cluster catalog zone ACLs and save zone file
|
|
UpdateClusterCatalogZoneOptions();
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
//notify all secondary nodes
|
|
TriggerNotifyAllSecondaryNodes();
|
|
|
|
//trigger NS and SOA update for member zones
|
|
TriggerRecordUpdateForClusterCatalogMemberZones();
|
|
|
|
return secondaryNode;
|
|
}
|
|
|
|
public async Task<ClusterNode> AskSecondaryNodeToLeaveClusterAsync(int secondaryNodeId)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to ask Secondary node to leave: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to ask Secondary node to leave: only a Primary node can ask a Secondary node to leave the Cluster.");
|
|
|
|
//find existing secondary node
|
|
if (!_clusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode))
|
|
throw new DnsServerException("Failed to ask Secondary node to leave: the specified node does not exist in the Cluster.");
|
|
|
|
if (secondaryNode.Type == ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to ask Secondary node to leave: the specified node is the Cluster Primary node and cannot be removed.");
|
|
|
|
//ask secondary node to leave the cluster
|
|
await secondaryNode.AskSecondaryNodeToLeaveClusterAsync();
|
|
|
|
return secondaryNode;
|
|
}
|
|
|
|
public ClusterNode DeleteSecondaryNode(int secondaryNodeId)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to delete Secondary node: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to delete Secondary node: only a Primary node can delete a Secondary node from the Cluster.");
|
|
|
|
//find existing secondary node
|
|
IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;
|
|
|
|
if (!existingClusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode))
|
|
throw new DnsServerException("Failed to delete Secondary node: the specified node does not exist in the Cluster.");
|
|
|
|
if (secondaryNode.Type == ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to delete Secondary node: the specified node is the Cluster Primary node and cannot be deleted.");
|
|
|
|
//delete secondary node from cluster nodes
|
|
Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count - 1);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
if (existingClusterNode.Key == secondaryNodeId)
|
|
continue;
|
|
|
|
updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;
|
|
}
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes);
|
|
if (!ReferenceEquals(originalValue, existingClusterNodes))
|
|
throw new InvalidOperationException("Failed to delete Secondary node: please try again.");
|
|
|
|
secondaryNode.Dispose();
|
|
|
|
//update cluster zone and save zone file
|
|
RemoveClusterPrimaryZoneRecordsFor(secondaryNode);
|
|
|
|
//update cluster catalog zone ACLs and save zone file
|
|
UpdateClusterCatalogZoneOptions();
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
//notify all secondary nodes
|
|
TriggerNotifyAllSecondaryNodes();
|
|
|
|
//trigger NS and SOA update for member zones
|
|
TriggerRecordUpdateForClusterCatalogMemberZones();
|
|
|
|
return secondaryNode;
|
|
}
|
|
|
|
public ClusterNode UpdateSecondaryNode(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to update Secondary node: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to update Secondary node: only a Primary node can update a Secondary node's details in the Cluster.");
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
|
|
if (!clusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode))
|
|
throw new DnsServerException("Failed to update Secondary node: the specified node does not exist in the Cluster.");
|
|
|
|
if (secondaryNode.Type != ClusterNodeType.Secondary)
|
|
throw new DnsServerException("Failed to update Secondary node: the specified node to update must be a Secondary node.");
|
|
|
|
//validate for duplicate names
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Key == secondaryNodeId)
|
|
continue; //skip self
|
|
|
|
if (clusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))
|
|
throw new DnsServerException("Failed to update Secondary node: A node with the same DNS Server Domain Name already exists in the Cluster. Please try again after changing the Secondary node's DNS Server Domain Name.");
|
|
}
|
|
|
|
bool secondaryNodeDomainChanged = !secondaryNode.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase);
|
|
|
|
//find existing record TTL values
|
|
FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);
|
|
|
|
//update cluster zone to remove existing records for secondary node
|
|
RemoveClusterPrimaryZoneRecordsFor(secondaryNode);
|
|
|
|
//update secondary node URL and IP address
|
|
secondaryNode.UpdateNode(secondaryNodeUrl, secondaryNodeIpAddresses);
|
|
|
|
//update cluster zone to add updated records for secondary node and save zone file
|
|
AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate);
|
|
|
|
//update cluster catalog zone ACLs and save zone file
|
|
UpdateClusterCatalogZoneOptions();
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
//notify all secondary nodes
|
|
TriggerNotifyAllSecondaryNodes();
|
|
|
|
//trigger NS and SOA update for member zones only if secondary node domain name has changed
|
|
if (secondaryNodeDomainChanged)
|
|
TriggerRecordUpdateForClusterCatalogMemberZones();
|
|
|
|
return secondaryNode;
|
|
}
|
|
|
|
public Task TransferConfigAsync(Stream zipStream, DateTime ifModifiedSince, ICollection<string> includeZones)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to transfer configuration: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to transfer configuration: only the Primary node can transfer the configuration.");
|
|
|
|
return _dnsWebService.BackupConfigAsync(zipStream: zipStream,
|
|
authConfig: true,
|
|
clusterConfig: false,
|
|
webServiceSettings: false,
|
|
dnsSettings: true,
|
|
logSettings: false,
|
|
zones: true,
|
|
allowedZones: true,
|
|
blockedZones: true,
|
|
blockLists: true,
|
|
apps: true,
|
|
scopes: false,
|
|
stats: false,
|
|
logs: false,
|
|
isConfigTransfer: true,
|
|
ifModifiedSince: ifModifiedSince,
|
|
includeZones: includeZones);
|
|
}
|
|
|
|
public void UpdateClusterOptions(ushort heartbeatRefreshIntervalSeconds, ushort heartbeatRetryIntervalSeconds, ushort configRefreshIntervalSeconds, ushort configRetryIntervalSeconds)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to update Cluster options: the Cluster is not initialized.");
|
|
|
|
if (GetSelfNode().Type != ClusterNodeType.Primary)
|
|
throw new DnsServerException("Failed to update Cluster options: only the Primary node can update the Cluster options.");
|
|
|
|
if ((heartbeatRefreshIntervalSeconds < 10) || (heartbeatRefreshIntervalSeconds > 300))
|
|
throw new ArgumentOutOfRangeException(nameof(heartbeatRefreshIntervalSeconds));
|
|
|
|
if ((heartbeatRetryIntervalSeconds < 10) || (heartbeatRetryIntervalSeconds > 300))
|
|
throw new ArgumentOutOfRangeException(nameof(heartbeatRetryIntervalSeconds));
|
|
|
|
if ((configRefreshIntervalSeconds < 30) || (configRefreshIntervalSeconds > 3600))
|
|
throw new ArgumentOutOfRangeException(nameof(configRefreshIntervalSeconds));
|
|
|
|
if ((configRetryIntervalSeconds < 30) || (configRetryIntervalSeconds > 3600))
|
|
throw new ArgumentOutOfRangeException(nameof(configRetryIntervalSeconds));
|
|
|
|
if (configRefreshIntervalSeconds <= heartbeatRefreshIntervalSeconds)
|
|
throw new ArgumentException("Failed to update Cluster options: The config refresh interval must be greater than the heartbeat refresh interval.");
|
|
|
|
bool changed = false;
|
|
|
|
if (_heartbeatRefreshIntervalSeconds != heartbeatRefreshIntervalSeconds)
|
|
{
|
|
_heartbeatRefreshIntervalSeconds = heartbeatRefreshIntervalSeconds;
|
|
changed = true;
|
|
}
|
|
|
|
if (_heartbeatRetryIntervalSeconds != heartbeatRetryIntervalSeconds)
|
|
{
|
|
_heartbeatRetryIntervalSeconds = heartbeatRetryIntervalSeconds;
|
|
changed = true;
|
|
}
|
|
|
|
if (_configRefreshIntervalSeconds != configRefreshIntervalSeconds)
|
|
{
|
|
_configRefreshIntervalSeconds = configRefreshIntervalSeconds;
|
|
changed = true;
|
|
}
|
|
|
|
if (_configRetryIntervalSeconds != configRetryIntervalSeconds)
|
|
{
|
|
_configRetryIntervalSeconds = configRetryIntervalSeconds;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
//apply new interval to all cluster nodes immediately
|
|
UpdateHeartbeatTimerForAllClusterNodes();
|
|
|
|
//save changes
|
|
SaveConfigFile();
|
|
|
|
//trigger notify to all secondary nodes
|
|
TriggerNotifyAllSecondaryNodes();
|
|
}
|
|
}
|
|
|
|
private void FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl)
|
|
{
|
|
//try get existing NS record TTL
|
|
IReadOnlyList<DnsResourceRecord> existingNSRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS);
|
|
if (existingNSRecords.Count > 0)
|
|
{
|
|
DnsResourceRecord existingNSRecord = existingNSRecords[0];
|
|
|
|
nsTtl = existingNSRecord.TTL;
|
|
|
|
string nsDomain = (existingNSRecord.RDATA as DnsNSRecordData).NameServer;
|
|
|
|
if (nsDomain.EndsWith("." + _clusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
IReadOnlyList<DnsResourceRecord> existingRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, nsDomain, DnsResourceRecordType.A);
|
|
if (existingRecords.Count == 0)
|
|
existingRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, nsDomain, DnsResourceRecordType.AAAA);
|
|
|
|
if (existingRecords.Count > 0)
|
|
aTtl = existingRecords[0].TTL;
|
|
else
|
|
aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl;
|
|
}
|
|
else
|
|
{
|
|
aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
nsTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultNsRecordTtl;
|
|
aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl;
|
|
}
|
|
}
|
|
|
|
private void RemoveAllClusterPrimaryZoneNSRecords()
|
|
{
|
|
//remove all existing NS records
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS);
|
|
}
|
|
|
|
private void RemoveClusterPrimaryZoneRecordsFor(ClusterNode node)
|
|
{
|
|
//remove NS record
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS, new DnsNSRecordData(node.Name));
|
|
|
|
//remove A/AAAA records
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.A);
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.AAAA);
|
|
|
|
//remove CNAME records if manually added by user
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.CNAME);
|
|
|
|
//remove PTR record
|
|
foreach (IPAddress ipAddress in node.IPAddresses)
|
|
{
|
|
string ptrDomain = Zone.GetReverseZone(ipAddress, ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);
|
|
|
|
AuthZoneInfo reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain);
|
|
if (reverseZoneInfo is not null)
|
|
{
|
|
if ((reverseZoneInfo.Type == AuthZoneType.Primary) || (reverseZoneInfo.Type == AuthZoneType.Forwarder))
|
|
{
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(reverseZoneInfo.Name, ptrDomain, DnsResourceRecordType.PTR, new DnsPTRRecordData(node.Name));
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
//remove TLSA DANE-EE record
|
|
_dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, $"_{node.Url.Port}._tcp.{node.Name}", DnsResourceRecordType.TLSA);
|
|
|
|
//save zone file
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(_clusterDomain);
|
|
}
|
|
|
|
private void AddClusterPrimaryZoneRecordsFor(ClusterNode node, uint nsTtl, uint aTtl, X509Certificate2 certificate)
|
|
{
|
|
const string recordComments = "Cluster managed record. Do not update or delete.";
|
|
|
|
if (node.Type == ClusterNodeType.Primary)
|
|
{
|
|
//update SOA record
|
|
IReadOnlyList<DnsResourceRecord> existingSoaRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.SOA);
|
|
DnsResourceRecord existingSoaRecord = existingSoaRecords[0];
|
|
DnsSOARecordData existingSoa = existingSoaRecord.RDATA as DnsSOARecordData;
|
|
|
|
DnsSOARecordData newSoa = new DnsSOARecordData(node.Name, existingSoa.ResponsiblePerson, existingSoa.Serial, existingSoa.Refresh, existingSoa.Retry, existingSoa.Expire, existingSoa.Minimum);
|
|
DnsResourceRecord newSoaRecord = new DnsResourceRecord(_clusterDomain, DnsResourceRecordType.SOA, DnsClass.IN, existingSoaRecord.TTL, newSoa);
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecord(_clusterDomain, newSoaRecord);
|
|
}
|
|
|
|
//add NS record
|
|
DnsResourceRecord nsRecord = new DnsResourceRecord(_clusterDomain, DnsResourceRecordType.NS, DnsClass.IN, nsTtl, new DnsNSRecordData(node.Name));
|
|
|
|
GenericRecordInfo nsRecordInfo = nsRecord.GetAuthGenericRecordInfo();
|
|
nsRecordInfo.LastModified = DateTime.UtcNow;
|
|
nsRecordInfo.Comments = recordComments;
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.AddRecord(_clusterDomain, nsRecord);
|
|
|
|
//set A/AAAA record
|
|
List<DnsResourceRecord> ipv4AddressRecords = new List<DnsResourceRecord>(node.IPAddresses.Count);
|
|
List<DnsResourceRecord> ipv6AddressRecords = new List<DnsResourceRecord>(node.IPAddresses.Count);
|
|
|
|
foreach (IPAddress ipAddress in node.IPAddresses)
|
|
{
|
|
DnsResourceRecord record;
|
|
|
|
switch (ipAddress.AddressFamily)
|
|
{
|
|
case AddressFamily.InterNetwork:
|
|
record = new DnsResourceRecord(node.Name, DnsResourceRecordType.A, DnsClass.IN, aTtl, new DnsARecordData(ipAddress));
|
|
ipv4AddressRecords.Add(record);
|
|
break;
|
|
|
|
case AddressFamily.InterNetworkV6:
|
|
record = new DnsResourceRecord(node.Name, DnsResourceRecordType.AAAA, DnsClass.IN, aTtl, new DnsAAAARecordData(ipAddress));
|
|
ipv6AddressRecords.Add(record);
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();
|
|
recordInfo.LastModified = DateTime.UtcNow;
|
|
recordInfo.Comments = recordComments;
|
|
}
|
|
|
|
if (ipv4AddressRecords.Count > 0)
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecords(_clusterDomain, ipv4AddressRecords);
|
|
|
|
if (ipv6AddressRecords.Count > 0)
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecords(_clusterDomain, ipv6AddressRecords);
|
|
|
|
//set PTR record
|
|
foreach (IPAddress ipAddress in node.IPAddresses)
|
|
{
|
|
string ptrDomain = Zone.GetReverseZone(ipAddress, ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);
|
|
|
|
AuthZoneInfo reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain);
|
|
if (reverseZoneInfo is not null)
|
|
{
|
|
if ((reverseZoneInfo.Type == AuthZoneType.Primary) || (reverseZoneInfo.Type == AuthZoneType.Forwarder))
|
|
{
|
|
DnsResourceRecord ptrRecord = new DnsResourceRecord(ptrDomain, DnsResourceRecordType.PTR, DnsClass.IN, aTtl, new DnsPTRRecordData(node.Name));
|
|
|
|
GenericRecordInfo ptrRecordInfo = ptrRecord.GetAuthGenericRecordInfo();
|
|
ptrRecordInfo.LastModified = DateTime.UtcNow;
|
|
ptrRecordInfo.Comments = recordComments;
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecord(reverseZoneInfo.Name, ptrRecord);
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
//set TLSA DANE-EE record
|
|
DnsResourceRecord tlsaRecord = new DnsResourceRecord($"_{node.Url.Port}._tcp.{node.Name}", DnsResourceRecordType.TLSA, DnsClass.IN, aTtl, new DnsTLSARecordData(DnsTLSACertificateUsage.DANE_EE, DnsTLSASelector.SPKI, DnsTLSAMatchingType.SHA2_256, certificate));
|
|
|
|
GenericRecordInfo tlsaRecordInfo = tlsaRecord.GetAuthGenericRecordInfo();
|
|
tlsaRecordInfo.LastModified = DateTime.UtcNow;
|
|
tlsaRecordInfo.Comments = recordComments;
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecord(_clusterDomain, tlsaRecord);
|
|
|
|
//save zone file
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(_clusterDomain);
|
|
}
|
|
|
|
public void UpdateClusterRecordsFor(AuthZoneInfo zoneInfo)
|
|
{
|
|
if (zoneInfo.Type != AuthZoneType.Primary)
|
|
throw new InvalidOperationException();
|
|
|
|
//set NS records for cluster
|
|
IReadOnlyList<DnsResourceRecord> existingNSRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.NS);
|
|
uint ttl;
|
|
|
|
if (existingNSRecords.Count > 0)
|
|
ttl = existingNSRecords[0].TTL;
|
|
else
|
|
ttl = _dnsWebService.DnsServer.AuthZoneManager.DefaultNsRecordTtl;
|
|
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
DnsResourceRecord[] nsRecords = new DnsResourceRecord[clusterNodes.Count];
|
|
int i = 0;
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
nsRecords[i++] = new DnsResourceRecord(zoneInfo.Name, DnsResourceRecordType.NS, DnsClass.IN, ttl, new DnsNSRecordData(clusterNode.Value.Name));
|
|
|
|
//set NS record
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecords(zoneInfo.Name, nsRecords);
|
|
|
|
//ensure correct SOA primary name server
|
|
IReadOnlyList<DnsResourceRecord> existingSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);
|
|
if (existingSoaRecords.Count > 0)
|
|
{
|
|
DnsResourceRecord existingSoaRecord = existingSoaRecords[0];
|
|
DnsSOARecordData existingSoa = existingSoaRecord.RDATA as DnsSOARecordData;
|
|
|
|
//set SOA record
|
|
_dnsWebService.DnsServer.AuthZoneManager.SetRecords(zoneInfo.Name, [new DnsResourceRecord(zoneInfo.Name, DnsResourceRecordType.SOA, DnsClass.IN, existingSoaRecord.TTL, new DnsSOARecordData(_dnsWebService.DnsServer.ServerDomain, existingSoa.ResponsiblePerson, existingSoa.Serial, existingSoa.Refresh, existingSoa.Retry, existingSoa.Expire, existingSoa.Minimum))]);
|
|
}
|
|
}
|
|
|
|
private void TriggerRecordUpdateForClusterCatalogMemberZones()
|
|
{
|
|
int id = RandomNumberGenerator.GetInt32(int.MaxValue);
|
|
_recordUpdateForMemberZonesId = id;
|
|
|
|
ThreadPool.QueueUserWorkItem(delegate (object state)
|
|
{
|
|
try
|
|
{
|
|
//get cluster catalog zone info
|
|
AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo("cluster-catalog." + _clusterDomain);
|
|
if ((clusterCatalogZoneInfo is null) || (clusterCatalogZoneInfo.Type != AuthZoneType.Catalog))
|
|
throw new InvalidOperationException();
|
|
|
|
//get all member zone names for cluster catalog zone
|
|
IReadOnlyCollection<string> memberZoneNames = (clusterCatalogZoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames();
|
|
|
|
foreach (string memberZoneName in memberZoneNames)
|
|
{
|
|
if (_recordUpdateForMemberZonesId != id)
|
|
return; //stop current update since another update has been triggered
|
|
|
|
//get member zone info
|
|
AuthZoneInfo memberZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName);
|
|
if ((memberZoneInfo is null) || (memberZoneInfo.Type != AuthZoneType.Primary))
|
|
continue; //process is only for primary zones
|
|
|
|
//update NS and SOA records for the member zone
|
|
UpdateClusterRecordsFor(memberZoneInfo);
|
|
|
|
//save zone file
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(memberZoneName);
|
|
}
|
|
|
|
_dnsWebService.LogManager.Write("The Cluster Catalog member zones NS and SOA records were successfully updated to reflect the Cluster changes.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write(ex);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void UpdateClusterCatalogZoneOptions()
|
|
{
|
|
string clusterCatalogDomain = "cluster-catalog." + _clusterDomain;
|
|
|
|
AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);
|
|
if (clusterCatalogZoneInfo is null)
|
|
throw new InvalidOperationException();
|
|
|
|
UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo);
|
|
}
|
|
|
|
private void UpdateClusterCatalogZoneOptions(AuthZoneInfo clusterCatalogZoneInfo)
|
|
{
|
|
//set cluster catalog zone options for Zone Transfer ACLs and notify addresses
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
|
|
List<NetworkAccessControl> zoneTransferACL = new List<NetworkAccessControl>(clusterNodes.Count * 2);
|
|
List<IPAddress> notifyNameServers = new List<IPAddress>(clusterNodes.Count * 2);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Value.Type == ClusterNodeType.Primary)
|
|
continue;
|
|
|
|
foreach (IPAddress ipAddress in clusterNode.Value.IPAddresses)
|
|
zoneTransferACL.Add(new NetworkAccessControl(ipAddress, (byte)(ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128)));
|
|
|
|
notifyNameServers.AddRange(clusterNode.Value.IPAddresses);
|
|
}
|
|
|
|
clusterCatalogZoneInfo.ZoneTransferNetworkACL = zoneTransferACL;
|
|
clusterCatalogZoneInfo.NotifyNameServers = notifyNameServers;
|
|
|
|
clusterCatalogZoneInfo.ZoneTransfer = AuthZoneTransfer.UseSpecifiedNetworkACL;
|
|
clusterCatalogZoneInfo.Notify = AuthZoneNotify.SpecifiedNameServers;
|
|
|
|
//set cluster catalog zone options for zone transfer TSIG key names
|
|
IReadOnlySet<string> existingKeyNames = clusterCatalogZoneInfo.ZoneTransferTsigKeyNames;
|
|
if (existingKeyNames is null)
|
|
{
|
|
HashSet<string> updatedKeyNames = [clusterCatalogZoneInfo.Name];
|
|
|
|
clusterCatalogZoneInfo.ZoneTransferTsigKeyNames = updatedKeyNames;
|
|
}
|
|
else if (!existingKeyNames.Contains(clusterCatalogZoneInfo.Name))
|
|
{
|
|
HashSet<string> updatedKeyNames = [.. existingKeyNames, clusterCatalogZoneInfo.Name];
|
|
|
|
clusterCatalogZoneInfo.ZoneTransferTsigKeyNames = updatedKeyNames;
|
|
}
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(clusterCatalogZoneInfo.Name);
|
|
}
|
|
|
|
public void TriggerNotifyAllSecondaryNodesIfPrimarySelfNode()
|
|
{
|
|
if (GetSelfNode().Type == ClusterNodeType.Primary)
|
|
TriggerNotifyAllSecondaryNodes();
|
|
}
|
|
|
|
public void TriggerNotifyAllSecondaryNodes(int notifyInterval = NOTIFY_ALL_SECONDARY_NODES_TIMER_INTERVAL)
|
|
{
|
|
if (_notifyAllSecondaryNodesTimerTriggered)
|
|
return;
|
|
|
|
_notifyAllSecondaryNodesTimer.Change(notifyInterval, Timeout.Infinite);
|
|
_notifyAllSecondaryNodesTimerTriggered = true;
|
|
}
|
|
|
|
private async void NotifyAllSecondaryNodesTimerCallbackAsync(object state)
|
|
{
|
|
try
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
if ((clusterNodes is null) || (clusterNodes.Count < 1))
|
|
return; //cluster not initialized
|
|
|
|
ClusterNode primaryNode = null;
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Value.Type == ClusterNodeType.Primary)
|
|
{
|
|
primaryNode = clusterNode.Value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((primaryNode is null) || (primaryNode.State != ClusterNodeState.Self))
|
|
throw new InvalidOperationException();
|
|
|
|
List<Task> tasks = new List<Task>(clusterNodes.Count);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Value.Type == ClusterNodeType.Primary)
|
|
continue; //skip primary cluster node
|
|
|
|
tasks.Add(clusterNode.Value.NotifySecondaryNodeAsync(primaryNode));
|
|
}
|
|
|
|
await Task.WhenAll(tasks); //notify node call does error logging
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write(ex);
|
|
}
|
|
finally
|
|
{
|
|
_notifyAllSecondaryNodesTimerTriggered = false;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region secondary node
|
|
|
|
public async Task InitializeAndJoinClusterAsync(IReadOnlyList<IPAddress> secondaryNodeIpAddresses, Uri primaryNodeUrl, string primaryNodeUsername, string primaryNodePassword, string primaryNodeTotp = null, IReadOnlyList<IPAddress> primaryNodeIpAddresses = null, bool ignoreCertificateErrors = false, CancellationToken cancellationToken = default)
|
|
{
|
|
if (ClusterInitialized)
|
|
throw new DnsServerException("Failed to join Cluster: the Cluster is already initialized.");
|
|
|
|
if (!_dnsWebService.IsWebServiceTlsEnabled)
|
|
throw new InvalidOperationException();
|
|
|
|
if (IPAddress.TryParse(primaryNodeUrl.Host, out _))
|
|
throw new DnsServerException("Failed to join Cluster: the Primary Node URL must use the domain name of the Primary node and not its IP address.");
|
|
|
|
if (primaryNodeIpAddresses is null)
|
|
{
|
|
try
|
|
{
|
|
IReadOnlyList<IPAddress> ipAddresses = await DnsClient.ResolveIPAsync(_dnsWebService.DnsServer, primaryNodeUrl.Host, _dnsWebService.DnsServer.IPv6Mode, cancellationToken);
|
|
if (ipAddresses.Count < 1)
|
|
throw new DnsServerException($"Failed to join Cluster: the domain name '{primaryNodeUrl.Host}' does not have an A/AAAA record configured.");
|
|
|
|
primaryNodeIpAddresses = ipAddresses;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new DnsServerException($"Failed to join Cluster: the Primary node domain name '{primaryNodeUrl.Host}' could not be resolved to an IP address. Please specify the Primary node IP address manually.", ex);
|
|
}
|
|
}
|
|
|
|
//login to primary node API
|
|
using HttpApiClient primaryNodeApiClient = new HttpApiClient(primaryNodeUrl, _dnsWebService.DnsServer.Proxy, _dnsWebService.DnsServer.IPv6Mode, ignoreCertificateErrors, new InternalDnsClient(_dnsWebService.DnsServer, primaryNodeIpAddresses));
|
|
|
|
try
|
|
{
|
|
_ = await primaryNodeApiClient.LoginAsync(primaryNodeUsername, primaryNodePassword, primaryNodeTotp, false, cancellationToken);
|
|
}
|
|
catch (TwoFactorAuthRequiredHttpApiClientException ex)
|
|
{
|
|
throw new TwoFactorAuthRequiredWebServiceException("Failed to join Cluster: two-factor authentication is required by the Primary node user account.", ex);
|
|
}
|
|
|
|
try
|
|
{
|
|
//get cluster info
|
|
ClusterInfo primaryNodeClusterInfo = await primaryNodeApiClient.GetClusterStateAsync(cancellationToken: cancellationToken);
|
|
|
|
//do validations
|
|
if (!primaryNodeClusterInfo.ClusterInitialized)
|
|
throw new DnsServerException("Failed to join Cluster: the Primary node does not have a Cluster initialized.");
|
|
|
|
string clusterCatalogDomain = "cluster-catalog." + primaryNodeClusterInfo.ClusterDomain;
|
|
|
|
if (_dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain) is not null)
|
|
throw new DnsServerException($"Failed to join Cluster: the zone '{clusterCatalogDomain}' already exists. Please delete the '{clusterCatalogDomain}' zone and try again.");
|
|
|
|
if (_dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(primaryNodeClusterInfo.ClusterDomain) is not null)
|
|
throw new DnsServerException($"Failed to join Cluster: the zone '{primaryNodeClusterInfo.ClusterDomain}' already exists. Please delete the '{primaryNodeClusterInfo.ClusterDomain}' zone and try again.");
|
|
|
|
//create self node
|
|
string serverDomain = _dnsWebService.DnsServer.ServerDomain;
|
|
if (!serverDomain.EndsWith("." + primaryNodeClusterInfo.ClusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
int x = serverDomain.IndexOf('.');
|
|
if (x < 0)
|
|
serverDomain = serverDomain + "." + primaryNodeClusterInfo.ClusterDomain;
|
|
else
|
|
serverDomain = string.Concat(serverDomain.AsSpan(0, x), ".", primaryNodeClusterInfo.ClusterDomain);
|
|
}
|
|
|
|
Uri secondaryNodeUrl = new Uri($"https://{serverDomain}:{_dnsWebService.WebServiceTlsPort}/");
|
|
|
|
ClusterNode selfSecondaryNode = new ClusterNode(this, RandomNumberGenerator.GetInt32(int.MaxValue), secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Self);
|
|
|
|
//join cluster
|
|
primaryNodeClusterInfo = await primaryNodeApiClient.JoinClusterAsync(selfSecondaryNode.Id, secondaryNodeUrl, secondaryNodeIpAddresses, _dnsWebService.WebServiceTlsCertificate, cancellationToken);
|
|
|
|
//initialize cluster
|
|
Dictionary<int, ClusterNode> clusterNodes = new Dictionary<int, ClusterNode>(primaryNodeClusterInfo.ClusterNodes.Count + 1);
|
|
|
|
clusterNodes[selfSecondaryNode.Id] = selfSecondaryNode;
|
|
|
|
foreach (ClusterInfo.ClusterNodeInfo nodeInfo in primaryNodeClusterInfo.ClusterNodes)
|
|
{
|
|
if (nodeInfo.Id == selfSecondaryNode.Id)
|
|
continue; //skip self node
|
|
|
|
ClusterNode node = new ClusterNode(this, nodeInfo);
|
|
clusterNodes[node.Id] = node;
|
|
}
|
|
|
|
DisposeAllNodes(); //dispose existing nodes, if any
|
|
|
|
_clusterNodes = clusterNodes;
|
|
|
|
_clusterDomain = primaryNodeClusterInfo.ClusterDomain;
|
|
_heartbeatRefreshIntervalSeconds = primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds;
|
|
_heartbeatRetryIntervalSeconds = primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds;
|
|
_configRefreshIntervalSeconds = primaryNodeClusterInfo.ConfigRefreshIntervalSeconds;
|
|
_configRetryIntervalSeconds = primaryNodeClusterInfo.ConfigRetryIntervalSeconds;
|
|
|
|
try
|
|
{
|
|
//sync entire config from primary node first to get TSIG keys for secondary catalog zone transfer
|
|
_configLastSynced = DateTime.UnixEpoch; //reset last sync time to ensure full sync
|
|
await SyncConfigFromAsync(primaryNodeApiClient, cancellationToken: cancellationToken);
|
|
|
|
//create cluster secondary catalog zone
|
|
AuthZoneInfo clusterSecondaryCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateSecondaryCatalogZone(clusterCatalogDomain, primaryNodeIpAddresses.Convert(delegate (IPAddress ipAddress) { return new NameServerAddress(primaryNodeUrl.Host, ipAddress); }), DnsTransportProtocol.Tcp, clusterCatalogDomain);
|
|
if (clusterSecondaryCatalogZoneInfo is null)
|
|
throw new DnsServerException($"Failed to join Cluster: the zone '{clusterCatalogDomain}' already exists. Please delete the '{clusterCatalogDomain}' zone and try again.");
|
|
|
|
//set cluster secondary catalog zone permissions
|
|
_dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterSecondaryCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterSecondaryCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View);
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
await primaryNodeApiClient.DeleteSecondaryNodeAsync(selfSecondaryNode.Id, cancellationToken);
|
|
}
|
|
catch
|
|
{ }
|
|
|
|
try
|
|
{
|
|
DeleteAllClusterConfig();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write(ex);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
//initialize heartbeat timer for all nodes here since config sync and zone transfers needs to occur first for DANE validation to work
|
|
InitializeHeartbeatTimerFor(clusterNodes);
|
|
|
|
//start config refresh timer to refresh as per config refresh interval as config was just synced
|
|
UpdateConfigRefreshTimer(_configRefreshIntervalSeconds * 1000);
|
|
|
|
//finalize
|
|
_dnsWebService.DnsServer.ServerDomain = selfSecondaryNode.Name;
|
|
|
|
//save all changes
|
|
_dnsWebService.DnsServer.SaveConfigFile();
|
|
_dnsWebService.AuthManager.SaveConfigFile();
|
|
SaveConfigFile();
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
//logout from primary node
|
|
await primaryNodeApiClient.LogoutAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{ }
|
|
}
|
|
}
|
|
|
|
public async Task LeaveClusterAsync(bool forceLeave)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to leave Cluster: the Cluster is not initialized.");
|
|
|
|
ClusterNode primaryNode = GetPrimaryNode();
|
|
|
|
if (primaryNode.State == ClusterNodeState.Self)
|
|
throw new DnsServerException("Failed to leave Cluster: a Primary self node cannot leave the Cluster.");
|
|
|
|
ClusterNode secondaryNode = GetSelfNode();
|
|
|
|
if (secondaryNode.Type != ClusterNodeType.Secondary)
|
|
throw new DnsServerException("Failed to leave Cluster: only Secondary nodes can leave the Cluster.");
|
|
|
|
if (!forceLeave)
|
|
{
|
|
//delete self node from cluster on primary node
|
|
await primaryNode.DeleteSecondaryNodeAsync(secondaryNode);
|
|
}
|
|
|
|
//delete all cluster config
|
|
DeleteAllClusterConfig();
|
|
}
|
|
|
|
public async Task<ClusterNode> UpdatePrimaryNodeAsync(Uri primaryNodeUrl, IReadOnlyList<IPAddress> primaryNodeIpAddresses = null, int primaryNodeId = -1, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to update Primary node: the Cluster is not initialized.");
|
|
|
|
if (primaryNodeIpAddresses is null)
|
|
{
|
|
try
|
|
{
|
|
IReadOnlyList<IPAddress> ipAddresses = await DnsClient.ResolveIPAsync(_dnsWebService.DnsServer, primaryNodeUrl.Host, _dnsWebService.DnsServer.IPv6Mode, cancellationToken);
|
|
if (ipAddresses.Count < 1)
|
|
throw new DnsServerException($"The domain name '{primaryNodeUrl.Host}' does not have an A/AAAA record configured.");
|
|
|
|
primaryNodeIpAddresses = ipAddresses;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new DnsServerException($"Failed to update Primary node: the Primary node domain name '{primaryNodeUrl.Host}' could not be resolved to an IP address.", ex);
|
|
}
|
|
}
|
|
|
|
ClusterNode primaryNode;
|
|
|
|
if (primaryNodeId < 0)
|
|
primaryNode = GetPrimaryNode();
|
|
else if (!_clusterNodes.TryGetValue(primaryNodeId, out primaryNode))
|
|
throw new DnsServerException("Failed to update Primary node: the specified Primary node ID does not exists in the Cluster.");
|
|
|
|
if (primaryNode.State == ClusterNodeState.Self)
|
|
throw new DnsServerException("Failed to update Primary node: the specified node is the self node and cannot be updated this way.");
|
|
|
|
if (primaryNode.Type == ClusterNodeType.Secondary)
|
|
{
|
|
//secondary node was promoted to primary node
|
|
ClusterNode formerPrimaryNode = GetPrimaryNode();
|
|
|
|
//dispose former primary node immediately to stop heartbeat
|
|
formerPrimaryNode.Dispose();
|
|
|
|
//remove former primary node from cluster nodes
|
|
IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;
|
|
Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count - 1);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
if (existingClusterNode.Key == formerPrimaryNode.Id)
|
|
continue;
|
|
|
|
updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;
|
|
}
|
|
|
|
//update cluster nodes
|
|
_clusterNodes = updatedClusterNodes;
|
|
|
|
//promote secondary node to primary immediately
|
|
primaryNode.PromoteToPrimaryNode();
|
|
|
|
//ensure to save changes
|
|
SaveConfigFile();
|
|
}
|
|
|
|
//validate for duplicate names
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in _clusterNodes)
|
|
{
|
|
if (clusterNode.Key == primaryNode.Id)
|
|
continue; //skip self
|
|
|
|
if (clusterNode.Value.Name.Equals(primaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))
|
|
throw new DnsServerException("Failed to update Primary node: the Primary node's domain name already exists in the Cluster. Please try again after changing the Primary DNS Server's domain name.");
|
|
}
|
|
|
|
//get cluster secondary catalog zone
|
|
string clusterCatalogDomain = "cluster-catalog." + _clusterDomain;
|
|
|
|
AuthZoneInfo clusterSecondaryCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);
|
|
if (clusterSecondaryCatalogZoneInfo is null)
|
|
throw new DnsServerException($"Failed to update Primary node: the Cluster Secondary Catalog zone '{clusterCatalogDomain}' does not exists.");
|
|
|
|
//update primary node
|
|
primaryNode.UpdateNode(primaryNodeUrl, primaryNodeIpAddresses);
|
|
|
|
//update cluster catalog zone's primary name server
|
|
clusterSecondaryCatalogZoneInfo.PrimaryNameServerAddresses = primaryNodeIpAddresses.Convert(delegate (IPAddress ipAddress)
|
|
{
|
|
int port = 53;
|
|
|
|
if (clusterSecondaryCatalogZoneInfo.PrimaryNameServerAddresses is not null)
|
|
{
|
|
foreach (NameServerAddress primaryNameServerAddress in clusterSecondaryCatalogZoneInfo.PrimaryNameServerAddresses)
|
|
{
|
|
if ((primaryNameServerAddress.IPEndPoint is not null) && primaryNameServerAddress.IPEndPoint.Address.Equals(ipAddress))
|
|
{
|
|
port = primaryNameServerAddress.Port;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new NameServerAddress(primaryNodeUrl.Host, new IPEndPoint(ipAddress, port));
|
|
});
|
|
|
|
//save all changes
|
|
_dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(clusterSecondaryCatalogZoneInfo.Name);
|
|
SaveConfigFile();
|
|
|
|
//trigger config and zone refresh
|
|
TriggerRefreshForConfig(CONFIG_REFRESH_TIMER_INTERVAL);
|
|
|
|
return primaryNode;
|
|
}
|
|
|
|
public void TriggerRefreshForConfig(IReadOnlyCollection<string> configRefreshIncludeZones = null)
|
|
{
|
|
//do validation
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to refresh configuration: the Cluster is not initialized.");
|
|
|
|
ClusterNode primaryNode = GetPrimaryNode();
|
|
|
|
if (primaryNode.State == ClusterNodeState.Self)
|
|
throw new DnsServerException("Failed to refresh configuration: only Secondary nodes can sync configuration from Primary nodes.");
|
|
|
|
TriggerRefreshForConfig(CONFIG_REFRESH_TIMER_INTERVAL, configRefreshIncludeZones);
|
|
}
|
|
|
|
public void TriggerResyncForConfig()
|
|
{
|
|
//do validation
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to resync configuration: the Cluster is not initialized.");
|
|
|
|
ClusterNode primaryNode = GetPrimaryNode();
|
|
|
|
if (primaryNode.State == ClusterNodeState.Self)
|
|
throw new DnsServerException("Failed to resync configuration: only Secondary nodes can sync configuration from Primary nodes.");
|
|
|
|
_configLastSynced = DateTime.UnixEpoch; //to ensure complete config resync
|
|
|
|
//trigger immediate config refresh
|
|
TriggerRefreshForConfig(0);
|
|
}
|
|
|
|
private void TriggerRefreshForConfig(int refreshInterval, IReadOnlyCollection<string> configRefreshIncludeZones = null)
|
|
{
|
|
_configRefreshLock.Wait();
|
|
try
|
|
{
|
|
if (configRefreshIncludeZones is not null)
|
|
{
|
|
if (_configRefreshIncludeZones is null)
|
|
_configRefreshIncludeZones = configRefreshIncludeZones;
|
|
else
|
|
_configRefreshIncludeZones = [.. _configRefreshIncludeZones, .. configRefreshIncludeZones];
|
|
}
|
|
|
|
if (_configRefreshTimerTriggered)
|
|
return;
|
|
|
|
_configRefreshTimer.Change(refreshInterval, Timeout.Infinite);
|
|
_configRefreshTimerTriggered = true;
|
|
}
|
|
finally
|
|
{
|
|
_configRefreshLock.Release();
|
|
}
|
|
}
|
|
|
|
private async void ConfigRefreshTimerCallbackAsync(object state)
|
|
{
|
|
bool success = false;
|
|
|
|
await _configRefreshLock.WaitAsync();
|
|
try
|
|
{
|
|
ClusterNode primaryNode = GetPrimaryNode();
|
|
|
|
if (primaryNode.State == ClusterNodeState.Self)
|
|
throw new InvalidOperationException();
|
|
|
|
//update cluster options
|
|
UpdateClusterFromPrimaryNode(await primaryNode.GetClusterStateAsync());
|
|
|
|
//sync config from primary node
|
|
await primaryNode.SyncConfigAsync(_configRefreshIncludeZones);
|
|
|
|
success = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write("Failed to sync server configuration from the Primary node.", ex);
|
|
}
|
|
finally
|
|
{
|
|
if (success)
|
|
{
|
|
_configRefreshTimerTriggered = false;
|
|
_configRefreshIncludeZones = null;
|
|
}
|
|
|
|
try
|
|
{
|
|
_configRefreshTimer.Change(success ? _configRefreshIntervalSeconds * 1000 : _configRetryIntervalSeconds * 1000, Timeout.Infinite);
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{ }
|
|
|
|
try
|
|
{
|
|
_configRefreshLock.Release();
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{ }
|
|
}
|
|
}
|
|
|
|
public async Task SyncConfigFromAsync(HttpApiClient primaryNodeApiClient, IReadOnlyCollection<string> includeZones = null, CancellationToken cancellationToken = default)
|
|
{
|
|
string tmpFile = Path.GetTempFileName();
|
|
try
|
|
{
|
|
await using (FileStream configZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))
|
|
{
|
|
//get config from primary node
|
|
(Stream, DateTime) response = await primaryNodeApiClient.TransferConfigFromPrimaryNodeAsync(_configLastSynced, includeZones, cancellationToken);
|
|
|
|
await using (Stream stream = response.Item1)
|
|
{
|
|
//copy stream with idle timeout
|
|
await stream.CopyToAsync(configZipStream, TimeSpan.FromSeconds(60), cancellationToken);
|
|
}
|
|
|
|
//dynamically load config
|
|
configZipStream.Position = 0;
|
|
|
|
await _dnsWebService.RestoreConfigAsync(zipStream: configZipStream,
|
|
authConfig: true,
|
|
clusterConfig: false,
|
|
webServiceSettings: false,
|
|
dnsSettings: true,
|
|
logSettings: false,
|
|
zones: true,
|
|
allowedZones: true,
|
|
blockedZones: true,
|
|
blockLists: true,
|
|
apps: true,
|
|
scopes: false,
|
|
stats: false,
|
|
logs: false,
|
|
deleteExistingFiles: false,
|
|
isConfigTransfer: true);
|
|
|
|
_configLastSynced = response.Item2;
|
|
|
|
//save config
|
|
SaveConfigFile();
|
|
}
|
|
|
|
_dnsWebService.LogManager.Write("Server configuration was synced from the Primary node successfully.");
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
File.Delete(tmpFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TriggerClusterUpdateForSecondaryNodeChanges()
|
|
{
|
|
if (_clusterUpdateForSecondaryNodeChangesTimerTriggered)
|
|
return;
|
|
|
|
_clusterUpdateForSecondaryNodeChangesTimer.Change(CLUSTER_UPDATE_FOR_SECONDARY_NODE_CHANGES_TIMER_INTERVAL, Timeout.Infinite);
|
|
_clusterUpdateForSecondaryNodeChangesTimerTriggered = true;
|
|
}
|
|
|
|
private async void ClusterUpdateForSecondaryNodeChangesTimerCallbackAsync(object state)
|
|
{
|
|
try
|
|
{
|
|
ClusterNode primaryNode = GetPrimaryNode();
|
|
|
|
if (primaryNode.State == ClusterNodeState.Self)
|
|
throw new InvalidOperationException();
|
|
|
|
ClusterNode secondaryNode = GetSelfNode();
|
|
|
|
if (secondaryNode.Type != ClusterNodeType.Secondary)
|
|
throw new InvalidOperationException();
|
|
|
|
UpdateClusterFromPrimaryNode(await primaryNode.UpdateSecondaryNodeAsync(secondaryNode, _dnsWebService.WebServiceTlsCertificate));
|
|
|
|
_dnsWebService.LogManager.Write("DNS Server updated this Secondary node's details on the Primary node successfully.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsWebService.LogManager.Write("DNS Server failed to update this Secondary node's details on the Primary node.", ex);
|
|
}
|
|
finally
|
|
{
|
|
_clusterUpdateForSecondaryNodeChangesTimerTriggered = false;
|
|
}
|
|
}
|
|
|
|
public void UpdateClusterFromPrimaryNode(ClusterInfo primaryNodeClusterInfo)
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;
|
|
|
|
//validation
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
if (existingClusterNode.Value.Type == ClusterNodeType.Primary)
|
|
{
|
|
if (existingClusterNode.Value.State == ClusterNodeState.Self)
|
|
throw new InvalidOperationException(); //this is a self primary node itself
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
List<ClusterNode> clusterNodesToAdd = new List<ClusterNode>();
|
|
List<ClusterNode> clusterNodesToRemove = new List<ClusterNode>();
|
|
|
|
foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in primaryNodeClusterInfo.ClusterNodes)
|
|
{
|
|
if (existingClusterNodes.TryGetValue(clusterNodeInfo.Id, out ClusterNode existingClusterNode))
|
|
{
|
|
if (existingClusterNode.State == ClusterNodeState.Self)
|
|
continue; //skip self node
|
|
|
|
//update existing cluster node
|
|
existingClusterNode.UpdateNode(clusterNodeInfo);
|
|
}
|
|
else
|
|
{
|
|
//add new cluster node
|
|
clusterNodesToAdd.Add(new ClusterNode(this, clusterNodeInfo));
|
|
}
|
|
}
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
bool found = false;
|
|
|
|
foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in primaryNodeClusterInfo.ClusterNodes)
|
|
{
|
|
if (existingClusterNode.Key == clusterNodeInfo.Id)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
clusterNodesToRemove.Add(existingClusterNode.Value);
|
|
}
|
|
|
|
bool saveConfig = false;
|
|
|
|
if ((clusterNodesToAdd.Count > 0) || (clusterNodesToRemove.Count > 0))
|
|
{
|
|
Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + clusterNodesToAdd.Count - clusterNodesToRemove.Count);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
if (clusterNodesToRemove.Contains(existingClusterNode.Value))
|
|
continue; //skip removed node
|
|
|
|
updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;
|
|
}
|
|
|
|
foreach (ClusterNode clusterNode in clusterNodesToAdd)
|
|
updatedClusterNodes[clusterNode.Id] = clusterNode;
|
|
|
|
//verify if node is part of the cluster
|
|
{
|
|
bool foundSelfNode = false;
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in updatedClusterNodes)
|
|
{
|
|
if (clusterNode.Value.State == ClusterNodeState.Self)
|
|
{
|
|
foundSelfNode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundSelfNode)
|
|
{
|
|
//this node is not part of the cluster anymore
|
|
//delete all cluster config
|
|
DeleteAllClusterConfig();
|
|
|
|
_dnsWebService.LogManager.Write("Failed to sync Cluster config: this Secondary node is not part of the Cluster anymore.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
//dispose all removed nodes
|
|
foreach (ClusterNode removedNodes in clusterNodesToRemove)
|
|
removedNodes.Dispose();
|
|
|
|
_clusterNodes = updatedClusterNodes;
|
|
|
|
InitializeHeartbeatTimerFor(updatedClusterNodes);
|
|
saveConfig = true;
|
|
}
|
|
|
|
if (primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds != _heartbeatRefreshIntervalSeconds)
|
|
{
|
|
_heartbeatRefreshIntervalSeconds = primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds;
|
|
UpdateHeartbeatTimerForAllClusterNodes(); //apply new interval to all cluster nodes immediately
|
|
saveConfig = true;
|
|
}
|
|
|
|
if (primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds != _heartbeatRetryIntervalSeconds)
|
|
{
|
|
_heartbeatRetryIntervalSeconds = primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds;
|
|
saveConfig = true;
|
|
}
|
|
|
|
if (primaryNodeClusterInfo.ConfigRefreshIntervalSeconds != _configRefreshIntervalSeconds)
|
|
{
|
|
_configRefreshIntervalSeconds = primaryNodeClusterInfo.ConfigRefreshIntervalSeconds;
|
|
UpdateConfigRefreshTimer(_configRefreshIntervalSeconds * 1000); //apply new interval to config refresh timer immediately
|
|
saveConfig = true;
|
|
}
|
|
|
|
if (primaryNodeClusterInfo.ConfigRetryIntervalSeconds != _configRetryIntervalSeconds)
|
|
{
|
|
_configRetryIntervalSeconds = primaryNodeClusterInfo.ConfigRetryIntervalSeconds;
|
|
saveConfig = true;
|
|
}
|
|
|
|
//save changes
|
|
if (saveConfig)
|
|
SaveConfigFile();
|
|
}
|
|
|
|
public async Task PromoteToPrimaryNodeAsync(bool forceDeletePrimary)
|
|
{
|
|
if (!ClusterInitialized)
|
|
throw new DnsServerException("Failed to promote to Primary node: the Cluster is not initialized.");
|
|
|
|
//do validation
|
|
ClusterNode selfNewPrimaryNode = GetSelfNode();
|
|
if (selfNewPrimaryNode.Type != ClusterNodeType.Secondary)
|
|
throw new DnsServerException("Failed to promote to Primary node: only Secondary nodes can be promoted to Primary nodes.");
|
|
|
|
string clusterCatalogDomain = "cluster-catalog." + _clusterDomain;
|
|
|
|
AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);
|
|
if (clusterCatalogZoneInfo is null)
|
|
throw new DnsServerException("Failed to promote to Primary node: the Cluster Secondary Catalog zone does not exist.");
|
|
|
|
AuthZoneInfo clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(_clusterDomain);
|
|
if (clusterZoneInfo is null)
|
|
throw new DnsServerException("Failed to promote to Primary node: the Cluster Secondary zone does not exist.");
|
|
|
|
//stop cluster config refresh timer
|
|
StopConfigRefreshTimer();
|
|
|
|
//resync config and delete current primary node from the cluster immediately
|
|
ClusterNode existingPrimaryNode = GetPrimaryNode();
|
|
|
|
if (!forceDeletePrimary)
|
|
{
|
|
//resync complete config from current primary node to ensure all data is synced
|
|
_configLastSynced = DateTime.UnixEpoch; //to ensure complete config resync
|
|
await existingPrimaryNode.SyncConfigAsync();
|
|
|
|
//delete current cluster primary node
|
|
await existingPrimaryNode.DeleteClusterAsync(true);
|
|
}
|
|
|
|
//dispose primary node immediately to stop heartbeat
|
|
existingPrimaryNode.Dispose();
|
|
|
|
//remove primary node from cluster nodes
|
|
IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;
|
|
Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count - 1);
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
|
|
{
|
|
if (existingClusterNode.Key == existingPrimaryNode.Id)
|
|
continue;
|
|
|
|
updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;
|
|
}
|
|
|
|
//update cluster nodes
|
|
_clusterNodes = updatedClusterNodes;
|
|
|
|
//promote self node to primary immediately
|
|
selfNewPrimaryNode.PromoteToPrimaryNode();
|
|
|
|
//convert cluster secondary catalog zone to catalog zone along with all its member zones
|
|
if (clusterCatalogZoneInfo.Type == AuthZoneType.SecondaryCatalog)
|
|
clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.ConvertZoneTypeTo(clusterCatalogZoneInfo.Name, AuthZoneType.Catalog);
|
|
|
|
//get converted primary cluster zone info
|
|
clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(_clusterDomain);
|
|
if (clusterZoneInfo is null)
|
|
throw new DnsServerException("Failed to promote to Primary node: the Cluster Primary zone does not exist.");
|
|
|
|
//sign cluster zone in case when DNSSEC private keys were not available during ConvertZoneTypeTo() operation
|
|
if (clusterZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)
|
|
{
|
|
DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.KeySigningKey);
|
|
DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.ZoneSigningKey);
|
|
zskPrivateKey.RolloverDays = 90;
|
|
|
|
_dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZone(clusterZoneInfo.Name, kskPrivateKey, zskPrivateKey, 3600, false);
|
|
}
|
|
|
|
//find existing record TTL values
|
|
FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);
|
|
|
|
//remove old primary node records from cluster primary zone and save zone file
|
|
if (existingPrimaryNode is not null)
|
|
RemoveClusterPrimaryZoneRecordsFor(existingPrimaryNode);
|
|
|
|
//update cluster primary zone for new primary node
|
|
AddClusterPrimaryZoneRecordsFor(selfNewPrimaryNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);
|
|
|
|
//update cluster catalog zone ACLs, TSIG key name and save zone file
|
|
UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo);
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
//notify all secondary nodes as a primary node immediately
|
|
TriggerNotifyAllSecondaryNodes(0);
|
|
|
|
//trigger NS and SOA update for member zones
|
|
TriggerRecordUpdateForClusterCatalogMemberZones();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public
|
|
|
|
public ClusterNode GetPrimaryNode()
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
if (clusterNodes is null)
|
|
throw new InvalidOperationException();
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Value.Type == ClusterNodeType.Primary)
|
|
return clusterNode.Value;
|
|
}
|
|
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
public ClusterNode GetSelfNode()
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
if (clusterNodes is null)
|
|
throw new InvalidOperationException();
|
|
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)
|
|
{
|
|
if (clusterNode.Value.State == ClusterNodeState.Self)
|
|
return clusterNode.Value;
|
|
}
|
|
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
public bool TryGetClusterNode(string nodeName, out ClusterNode clusterNode)
|
|
{
|
|
foreach (KeyValuePair<int, ClusterNode> node in _clusterNodes)
|
|
{
|
|
if (node.Value.Name.Equals(nodeName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
clusterNode = node.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
clusterNode = null;
|
|
return false;
|
|
}
|
|
|
|
public bool IsClusterPrimaryZone(string zoneName)
|
|
{
|
|
return (zoneName is not null) && zoneName.Equals(_clusterDomain, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public bool IsClusterCatalogZone(string zoneName)
|
|
{
|
|
return (zoneName is not null) && zoneName.Equals("cluster-catalog." + _clusterDomain, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public ClusterNode UpdateSelfNodeIPAddresses(IReadOnlyList<IPAddress> ipAddresses)
|
|
{
|
|
ClusterNode selfNode = GetSelfNode();
|
|
|
|
switch (selfNode.Type)
|
|
{
|
|
case ClusterNodeType.Primary:
|
|
//find existing record TTL values
|
|
FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);
|
|
|
|
//update cluster zone to remove current self node records
|
|
RemoveClusterPrimaryZoneRecordsFor(selfNode);
|
|
|
|
//update self node
|
|
selfNode.UpdateSelfNodeIPAddresses(ipAddresses);
|
|
|
|
//update cluster zone to add updated self node records
|
|
AddClusterPrimaryZoneRecordsFor(selfNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);
|
|
|
|
//update cluster catalog zone ACLs and save zone file
|
|
UpdateClusterCatalogZoneOptions();
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
//notify all secondary nodes immediately
|
|
TriggerNotifyAllSecondaryNodes(0);
|
|
break;
|
|
|
|
case ClusterNodeType.Secondary:
|
|
//update self node
|
|
selfNode.UpdateSelfNodeIPAddresses(ipAddresses);
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
//trigger cluster node update on primary node
|
|
TriggerClusterUpdateForSecondaryNodeChanges();
|
|
break;
|
|
}
|
|
|
|
return selfNode;
|
|
}
|
|
|
|
public void UpdateSelfNodeUrlAndCertificate()
|
|
{
|
|
ClusterNode selfNode = GetSelfNode();
|
|
|
|
//validation
|
|
foreach (KeyValuePair<int, ClusterNode> clusterNode in _clusterNodes)
|
|
{
|
|
if (clusterNode.Key == selfNode.Id)
|
|
continue; //skip self
|
|
|
|
if (clusterNode.Value.Name.Equals(_dnsWebService.DnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase))
|
|
throw new DnsServerException("Failed to update self node URL: the node's domain name already exists in the Cluster. Please try again after changing the DNS Server's domain name.");
|
|
}
|
|
|
|
switch (selfNode.Type)
|
|
{
|
|
case ClusterNodeType.Primary:
|
|
//find existing record TTL values
|
|
FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);
|
|
|
|
//update cluster zone to remove current self node records
|
|
RemoveClusterPrimaryZoneRecordsFor(selfNode);
|
|
|
|
//update self node
|
|
selfNode.UpdateSelfNodeUrl();
|
|
|
|
//update cluster zone to add updated self node records
|
|
AddClusterPrimaryZoneRecordsFor(selfNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
_dnsWebService.LogManager.Write("Primary node '" + selfNode.ToString() + "' URL was updated successfully.");
|
|
|
|
//notify all secondary nodes
|
|
TriggerNotifyAllSecondaryNodes();
|
|
break;
|
|
|
|
case ClusterNodeType.Secondary:
|
|
//update self node
|
|
selfNode.UpdateSelfNodeUrl();
|
|
|
|
//save all changes
|
|
SaveConfigFile();
|
|
|
|
_dnsWebService.LogManager.Write("Secondary node '" + selfNode.ToString() + "' URL was updated successfully.");
|
|
|
|
//trigger cluster node update on primary node
|
|
TriggerClusterUpdateForSecondaryNodeChanges();
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region properties
|
|
|
|
public DnsWebService DnsWebService
|
|
{ get { return _dnsWebService; } }
|
|
|
|
public bool ClusterInitialized
|
|
{
|
|
get
|
|
{
|
|
IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;
|
|
|
|
return (clusterNodes is not null) && (clusterNodes.Count > 0);
|
|
}
|
|
}
|
|
|
|
public string ClusterDomain
|
|
{ get { return _clusterDomain; } }
|
|
|
|
public ushort HeartbeatRefreshIntervalSeconds
|
|
{ get { return _heartbeatRefreshIntervalSeconds; } }
|
|
|
|
public ushort HeartBeatRetryIntervalSeconds
|
|
{ get { return _heartbeatRetryIntervalSeconds; } }
|
|
|
|
public ushort ConfigRefreshIntervalSeconds
|
|
{ get { return _configRefreshIntervalSeconds; } }
|
|
|
|
public ushort ConfigRetryIntervalSeconds
|
|
{ get { return _configRetryIntervalSeconds; } }
|
|
|
|
public DateTime ConfigLastSynced
|
|
{ get { return _configLastSynced; } }
|
|
|
|
public IReadOnlyDictionary<int, ClusterNode> ClusterNodes
|
|
{ get { return _clusterNodes; } }
|
|
|
|
#endregion
|
|
}
|
|
}
|