FailoverApp: app code with most features implemented.

This commit is contained in:
Shreyas Zare
2021-05-15 13:21:59 +05:30
parent fbe161a648
commit ee9e62a093
11 changed files with 2671 additions and 0 deletions

224
Apps/FailoverApp/Address.cs Normal file
View File

@@ -0,0 +1,224 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using DnsApplicationCommon;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using TechnitiumLibrary.IO;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace Failover
{
public class Address : IDnsApplicationRequestHandler
{
#region variables
HealthMonitoringService _healthMonitor;
#endregion
#region IDisposable
bool _disposed;
public void Dispose()
{
if (_disposed)
return;
if (_healthMonitor is not null)
_healthMonitor.Dispose();
_disposed = true;
}
#endregion
#region private
private void GetAnswers(dynamic jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List<DnsResourceRecord> answers)
{
if (jsonAddresses == null)
return;
switch (question.Type)
{
case DnsResourceRecordType.A:
foreach (dynamic jsonAddress in jsonAddresses)
{
IPAddress address = IPAddress.Parse(jsonAddress.Value);
if (address.AddressFamily == AddressFamily.InterNetwork)
{
HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, true);
if (status is null)
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecord(address)));
if (status.IsHealthy)
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecord(address)));
}
}
break;
case DnsResourceRecordType.AAAA:
foreach (dynamic jsonAddress in jsonAddresses)
{
IPAddress address = IPAddress.Parse(jsonAddress.Value);
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, true);
if (status is null)
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecord(address)));
if (status.IsHealthy)
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecord(address)));
}
}
break;
}
}
private void GetStatusAnswers(dynamic jsonAddresses, bool primary, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List<DnsResourceRecord> answers)
{
if (jsonAddresses == null)
return;
foreach (dynamic jsonAddress in jsonAddresses)
{
IPAddress address = IPAddress.Parse(jsonAddress.Value);
HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, false);
string text = "app=failover; addressType=" + (primary ? "primary" : "secondary") + "; address=" + address.ToString() + "; healthCheck=" + healthCheck;
if (status is null)
text += "; healthStatus=Unknown;";
else if (status.IsHealthy)
text += "; healthStatus=Healthy;";
else
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
}
}
#endregion
#region public
public Task InitializeAsync(IDnsServer dnsServer, string config)
{
if (_healthMonitor is null)
_healthMonitor = HealthMonitoringService.Create(dnsServer);
_healthMonitor.Initialize(JsonConvert.DeserializeObject(config));
return Task.CompletedTask;
}
public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, string zoneName, uint appRecordTtl, string appRecordData, bool isRecursionAllowed, IDnsServer dnsServer)
{
DnsQuestionRecord question = request.Question[0];
switch (question.Type)
{
case DnsResourceRecordType.A:
case DnsResourceRecordType.AAAA:
{
dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData);
string healthCheck = jsonAppRecordData.healthCheck?.Value;
List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
GetAnswers(jsonAppRecordData.primary, question, appRecordTtl, healthCheck, answers);
if (answers.Count == 0)
{
GetAnswers(jsonAppRecordData.secondary, question, appRecordTtl, healthCheck, answers);
if (answers.Count == 0)
return Task.FromResult<DnsDatagram>(null);
}
if (answers.Count > 1)
answers.Shuffle();
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
}
case DnsResourceRecordType.TXT:
{
dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData);
bool allowTxtStatus;
if (jsonAppRecordData.allowTxtStatus == null)
allowTxtStatus = false;
else
allowTxtStatus = jsonAppRecordData.allowTxtStatus.Value;
if (!allowTxtStatus)
return Task.FromResult<DnsDatagram>(null);
string healthCheck = jsonAppRecordData.healthCheck?.Value;
List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
GetStatusAnswers(jsonAppRecordData.primary, true, question, 30, healthCheck, answers);
GetStatusAnswers(jsonAppRecordData.secondary, false, question, 30, healthCheck, answers);
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
}
default:
return Task.FromResult<DnsDatagram>(null);
}
}
#endregion
#region properties
public string Description
{ get { return "Returns A or AAAA records from primary set of addresses with a continous health check as configured in the app config. When none of the primary addresses are healthy, the app returns healthy addresses from the secondary set of addresses.\n\nSet 'allowTxtStatus' to 'true' in your APP record data to allow checking health status by querying for TXT record."; } }
public string ApplicationRecordDataTemplate
{
get
{
return @"{
""primary"": [
""1.1.1.1"",
""::1""
],
""secondary"": [
""2.2.2.2"",
""::2""
],
""healthCheck"": ""tcp80"",
""allowTxtStatus"": false
}";
}
}
#endregion
}
}

View File

@@ -0,0 +1,121 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
namespace Failover
{
class AddressMonitoring : IDisposable
{
#region variables
readonly HealthMonitoringService _service;
readonly IPAddress _address;
readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>(1, 1);
#endregion
#region constructor
public AddressMonitoring(HealthMonitoringService service, IPAddress address, string healthCheck)
{
_service = service;
_address = address;
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, _address, existingHealthCheck));
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
healthMonitor.Value.Dispose();
_healthMonitors.Clear();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region public
public HealthCheckStatus QueryStatus(string healthCheck)
{
if (_healthMonitors.TryGetValue(healthCheck, out HealthMonitor monitor))
return monitor.HealthCheckStatus;
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, _address, existingHealthCheck));
return null;
}
public void RemoveHealthMonitor(string healthCheck)
{
if (_healthMonitors.TryRemove(healthCheck, out HealthMonitor removedMonitor))
removedMonitor.Dispose();
}
public bool IsExpired()
{
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
{
if (healthMonitor.Value.IsExpired())
{
if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))
removedMonitor.Dispose();
}
}
return _healthMonitors.IsEmpty;
}
#endregion
#region property
public IPAddress Address
{ get { return _address; } }
#endregion
}
}

203
Apps/FailoverApp/CNAME.cs Normal file
View File

@@ -0,0 +1,203 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using DnsApplicationCommon;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace Failover
{
public class CNAME : IDnsApplicationRequestHandler
{
#region variables
HealthMonitoringService _healthMonitor;
#endregion
#region IDisposable
bool _disposed;
public void Dispose()
{
if (_disposed)
return;
if (_healthMonitor is not null)
_healthMonitor.Dispose();
_disposed = true;
}
#endregion
#region private
private IReadOnlyList<DnsResourceRecord> GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck)
{
HealthCheckStatus status = _healthMonitor.QueryStatus(domain, question.Type, healthCheck, true);
if (status is null)
{
if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecord(domain)) }; //use ANAME
else
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 30, new DnsCNAMERecord(domain)) };
}
if (status.IsHealthy)
{
if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecord(domain)) }; //use ANAME
else
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecord(domain)) };
}
return null;
}
private void GetStatusAnswers(string domain, bool primary, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List<DnsResourceRecord> answers)
{
{
HealthCheckStatus status = _healthMonitor.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, false);
string text = "app=failover; cnameType=" + (primary ? "primary" : "secondary") + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck;
if (status is null)
text += "; healthStatus=Unknown;";
else if (status.IsHealthy)
text += "; healthStatus=Healthy;";
else
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
}
{
HealthCheckStatus status = _healthMonitor.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, false);
string text = "app=failover; cnameType=" + (primary ? "primary" : "secondary") + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck;
if (status is null)
text += "; healthStatus=Unknown;";
else if (status.IsHealthy)
text += "; healthStatus=Healthy;";
else
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
}
}
#endregion
#region public
public Task InitializeAsync(IDnsServer dnsServer, string config)
{
if (_healthMonitor is null)
_healthMonitor = HealthMonitoringService.Create(dnsServer);
//let Address class initialize config
return Task.CompletedTask;
}
public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, string zoneName, uint appRecordTtl, string appRecordData, bool isRecursionAllowed, IDnsServer dnsServer)
{
DnsQuestionRecord question = request.Question[0];
dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData);
string healthCheck = jsonAppRecordData.healthCheck?.Value;
IReadOnlyList<DnsResourceRecord> answers;
if (question.Type == DnsResourceRecordType.TXT)
{
bool allowTxtStatus;
if (jsonAppRecordData.allowTxtStatus == null)
allowTxtStatus = false;
else
allowTxtStatus = jsonAppRecordData.allowTxtStatus.Value;
if (!allowTxtStatus)
return Task.FromResult<DnsDatagram>(null);
List<DnsResourceRecord> txtAnswers = new List<DnsResourceRecord>();
GetStatusAnswers(jsonAppRecordData.primary.Value, true, question, 30, healthCheck, txtAnswers);
foreach (dynamic jsonDomain in jsonAppRecordData.secondary)
GetStatusAnswers(jsonDomain.Value, false, question, 30, healthCheck, txtAnswers);
answers = txtAnswers;
}
else
{
answers = GetAnswers(jsonAppRecordData.primary.Value, question, zoneName, appRecordTtl, healthCheck);
if (answers is null)
{
foreach (dynamic jsonDomain in jsonAppRecordData.secondary)
{
answers = GetAnswers(jsonDomain.Value, question, zoneName, appRecordTtl, healthCheck);
if (answers is not null)
break;
}
if (answers is null)
return Task.FromResult<DnsDatagram>(null);
}
}
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
}
#endregion
#region properties
public string Description
{ get { return "Returns CNAME record for primary domain name with a continous health check as configured in the app config. When the primary domain name is unhealthy, the app returns one of the secondary domain names in order of preference that is healthy. Note that the app will return ANAME record for an APP record at zone apex.\n\nSet 'allowTxtStatus' to 'true' in your APP record data to allow checking health status by querying for TXT record."; } }
public string ApplicationRecordDataTemplate
{
get
{
return @"{
""primary"": ""in.example.org"",
""secondary"": [
""sg.example.org"",
""eu.example.org""
],
""healthCheck"": ""tcp443"",
""allowTxtStatus"": false
}";
}
}
#endregion
}
}

View File

@@ -0,0 +1,126 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using TechnitiumLibrary.Net.Dns;
namespace Failover
{
class DomainMonitoring : IDisposable
{
#region variables
readonly HealthMonitoringService _service;
readonly string _domain;
readonly DnsResourceRecordType _type;
readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>(1, 1);
#endregion
#region constructor
public DomainMonitoring(HealthMonitoringService service, string domain, DnsResourceRecordType type, string healthCheck)
{
_service = service;
_domain = domain;
_type = type;
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, domain, type, existingHealthCheck));
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
healthMonitor.Value.Dispose();
_healthMonitors.Clear();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region public
public HealthCheckStatus QueryStatus(string healthCheck)
{
if (_healthMonitors.TryGetValue(healthCheck, out HealthMonitor monitor))
return monitor.HealthCheckStatus;
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, _domain, _type, existingHealthCheck));
return null;
}
public void RemoveHealthMonitor(string healthCheck)
{
if (_healthMonitors.TryRemove(healthCheck, out HealthMonitor removedMonitor))
removedMonitor.Dispose();
}
public bool IsExpired()
{
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
{
if (healthMonitor.Value.IsExpired())
{
if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))
removedMonitor.Dispose();
}
}
return _healthMonitors.IsEmpty;
}
#endregion
#region property
public string Domain
{ get { return _domain; } }
public DnsResourceRecordType Type
{ get { return _type; } }
#endregion
}
}

View File

@@ -0,0 +1,366 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Mail;
namespace Failover
{
class EmailAlert : IDisposable
{
#region variables
readonly HealthMonitoringService _service;
string _name;
bool _enabled;
MailAddress[] _alertTo;
string _smtpServer;
int _smtpPort;
bool _startTls;
bool _smtpOverTls;
string _username;
string _password;
MailAddress _mailFrom;
readonly SmtpClientEx _smtpClient = new SmtpClientEx();
#endregion
#region constructor
public EmailAlert(HealthMonitoringService service, dynamic jsonEmailAlert)
{
_service = service;
Reload(jsonEmailAlert);
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_smtpClient is not null)
_smtpClient.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region private
private async Task SendMailAsync(MailMessage message)
{
try
{
await _smtpClient.SendMailAsync(message);
}
catch (Exception ex)
{
_service.DnsServer.WriteLog(ex);
}
}
#endregion
#region public
public void Reload(dynamic jsonEmailAlert)
{
if (jsonEmailAlert.name is null)
_name = "default";
else
_name = jsonEmailAlert.name.Value;
if (jsonEmailAlert.enabled is null)
_enabled = false;
else
_enabled = jsonEmailAlert.enabled.Value;
if (jsonEmailAlert.alertTo is null)
{
_alertTo = null;
}
else
{
_alertTo = new MailAddress[jsonEmailAlert.alertTo.Count];
for (int i = 0; i < _alertTo.Length; i++)
_alertTo[i] = new MailAddress(jsonEmailAlert.alertTo[i].Value);
}
if (jsonEmailAlert.smtpServer is null)
_smtpServer = null;
else
_smtpServer = jsonEmailAlert.smtpServer.Value;
if (jsonEmailAlert.smtpPort is null)
_smtpPort = 25;
else
_smtpPort = Convert.ToInt32(jsonEmailAlert.smtpPort.Value);
if (jsonEmailAlert.startTls is null)
_startTls = false;
else
_startTls = jsonEmailAlert.startTls.Value;
if (jsonEmailAlert.smtpOverTls is null)
_smtpOverTls = false;
else
_smtpOverTls = jsonEmailAlert.smtpOverTls.Value;
if (jsonEmailAlert.username is null)
_username = null;
else
_username = jsonEmailAlert.username.Value;
if (jsonEmailAlert.password is null)
_password = null;
else
_password = jsonEmailAlert.password.Value;
if (jsonEmailAlert.mailFrom is null)
{
_mailFrom = null;
}
else
{
if (jsonEmailAlert.mailFromName is null)
_mailFrom = new MailAddress(jsonEmailAlert.mailFrom.Value);
else
_mailFrom = new MailAddress(jsonEmailAlert.mailFrom.Value, jsonEmailAlert.mailFromName.Value, Encoding.UTF8);
}
//update smtp client settings
_smtpClient.Host = _smtpServer;
_smtpClient.Port = _smtpPort;
_smtpClient.EnableSsl = _startTls;
_smtpClient.EnableSslWrapper = _smtpOverTls;
if (string.IsNullOrEmpty(_username))
_smtpClient.Credentials = null;
else
_smtpClient.Credentials = new NetworkCredential(_username, _password);
_smtpClient.Proxy = _service.DnsServer.Proxy;
}
public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckStatus healthCheckStatus)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
if (healthCheckStatus.IsHealthy)
{
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Healthy";
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address was healthy.
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
Regards,
DNS Failover App
";
}
else
{
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Failed";
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address failed to respond.
The failure reason is: " + healthCheckStatus.FailureReason + @"
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
Regards,
DNS Failover App
";
}
return SendMailAsync(message);
}
public Task SendAlertAsync(IPAddress address, string healthCheck, Exception ex)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Error";
message.Body = @"Hi,
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"].
The error description is: " + ex.ToString() + @"
Alert time: " + DateTime.UtcNow.ToLongDateString() + @"
Regards,
DNS Failover App
";
return SendMailAsync(message);
}
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckStatus healthCheckStatus)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
if (healthCheckStatus.IsHealthy)
{
message.Subject = "[Alert] Domain [" + domain + "] Status Is Healthy";
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name was healthy.
DNS record type: " + type.ToString() + @"
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
Regards,
DNS Failover App
";
}
else
{
message.Subject = "[Alert] Domain [" + domain + "] Status Is Failed";
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name failed to respond.
The failure reason is: " + healthCheckStatus.FailureReason + @"
DNS record type: " + type.ToString() + @"
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
Regards,
DNS Failover App
";
}
return SendMailAsync(message);
}
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
message.Subject = "[Alert] Domain [" + domain + "] Status Is Error";
message.Body = @"Hi,
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"].
The error description is: " + ex.ToString() + @"
DNS record type: " + type.ToString() + @"
Alert time: " + DateTime.UtcNow.ToLongDateString() + @"
Regards,
DNS Failover App
";
return SendMailAsync(message);
}
#endregion
#region properties
public string Name
{ get { return _name; } }
public bool Enabled
{ get { return _enabled; } }
public IReadOnlyList<MailAddress> AlertTo
{ get { return _alertTo; } }
public string SmtpServer
{ get { return _smtpServer; } }
public int SmtpPort
{ get { return _smtpPort; } }
public bool StartTls
{ get { return _startTls; } }
public bool SmtpOverTls
{ get { return _smtpOverTls; } }
public string Username
{ get { return _username; } }
public string Password
{ get { return _password; } }
public MailAddress MailFrom
{ get { return _mailFrom; } }
#endregion
}
}

View File

@@ -0,0 +1,466 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using TechnitiumLibrary.IO;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Proxy;
namespace Failover
{
enum HealthCheckType
{
Unknown = 0,
Ping = 1,
Tcp = 2,
Http = 3
}
class HealthCheck : IDisposable
{
#region variables
readonly HealthMonitoringService _service;
string _name;
HealthCheckType _type;
int _interval;
int _retries;
int _timeout;
int _port;
Uri _url;
EmailAlert _emailAlert;
WebHook _webHook;
SocketsHttpHandler _httpHandler;
HttpClient _httpClient;
#endregion
#region constructor
public HealthCheck(HealthMonitoringService service, dynamic jsonHealthCheck)
{
_service = service;
Reload(jsonHealthCheck);
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_httpClient != null)
{
_httpClient.Dispose();
_httpClient = null;
}
if (_httpHandler != null)
{
_httpHandler.Dispose();
_httpHandler = null;
}
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region private
private void ConditionalHttpReload()
{
if (_type == HealthCheckType.Http)
{
bool handlerChanged = false;
NetProxy proxy = _service.DnsServer.Proxy;
if (_httpHandler is null)
{
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
httpHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);
httpHandler.Proxy = proxy;
httpHandler.AllowAutoRedirect = true;
httpHandler.MaxAutomaticRedirections = 10;
_httpHandler = httpHandler;
handlerChanged = true;
}
else
{
if ((_httpHandler.ConnectTimeout.TotalMilliseconds != _timeout) || (_httpHandler.Proxy != proxy))
{
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
httpHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);
httpHandler.Proxy = proxy;
httpHandler.AllowAutoRedirect = true;
httpHandler.MaxAutomaticRedirections = 10;
SocketsHttpHandler oldHttpHandler = _httpHandler;
_httpHandler = httpHandler;
handlerChanged = true;
oldHttpHandler.Dispose();
}
}
if (_httpClient is null)
{
HttpClient httpClient = new HttpClient(_httpHandler);
httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);
_httpClient = httpClient;
}
else
{
if (handlerChanged || (_httpClient.Timeout.TotalMilliseconds != _timeout))
{
HttpClient httpClient = new HttpClient(_httpHandler);
httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);
HttpClient oldHttpClient = _httpClient;
_httpClient = httpClient;
oldHttpClient.Dispose();
}
}
}
else
{
if (_httpClient != null)
{
_httpClient.Dispose();
_httpClient = null;
}
if (_httpHandler != null)
{
_httpHandler.Dispose();
_httpHandler = null;
}
}
}
#endregion
#region public
public void Reload(dynamic jsonHealthCheck)
{
if (jsonHealthCheck.name is null)
_name = "default";
else
_name = jsonHealthCheck.name.Value;
if (jsonHealthCheck.type == null)
_type = HealthCheckType.Tcp;
else
_type = Enum.Parse<HealthCheckType>(jsonHealthCheck.type.Value, true);
if (jsonHealthCheck.interval is null)
_interval = 60000;
else
_interval = Convert.ToInt32(jsonHealthCheck.interval.Value) * 1000;
if (jsonHealthCheck.retries is null)
_retries = 3;
else
_retries = Convert.ToInt32(jsonHealthCheck.retries.Value);
if (jsonHealthCheck.timeout is null)
_timeout = 10000;
else
_timeout = Convert.ToInt32(jsonHealthCheck.timeout.Value) * 1000;
if (jsonHealthCheck.port is null)
_port = 80;
else
_port = Convert.ToInt32(jsonHealthCheck.port.Value);
if (jsonHealthCheck.url is null)
_url = null;
else
_url = new Uri(jsonHealthCheck.url.Value);
string emailAlertName;
if (jsonHealthCheck.emailAlert is null)
emailAlertName = null;
else
emailAlertName = jsonHealthCheck.emailAlert.Value;
if ((emailAlertName is not null) && _service.EmailAlerts.TryGetValue(emailAlertName, out EmailAlert emailAlert))
_emailAlert = emailAlert;
else
_emailAlert = null;
string webHookName;
if (jsonHealthCheck.webHook is null)
webHookName = null;
else
webHookName = jsonHealthCheck.webHook.Value;
if ((webHookName is not null) && _service.WebHooks.TryGetValue(webHookName, out WebHook webHook))
_webHook = webHook;
else
_webHook = null;
ConditionalHttpReload();
}
public async Task<HealthCheckStatus> IsHealthyAsync(string domain, DnsResourceRecordType type)
{
switch (type)
{
case DnsResourceRecordType.A:
{
DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
if ((response is null) || (response.Answer.Count == 0))
return HealthCheckStatus.FailedToResolve;
IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response);
if (addresses.Count > 0)
{
HealthCheckStatus lastStatus = null;
foreach (IPAddress address in addresses)
{
lastStatus = await IsHealthyAsync(address);
if (lastStatus.IsHealthy)
return lastStatus;
}
return lastStatus;
}
return HealthCheckStatus.FailedToResolve;
}
case DnsResourceRecordType.AAAA:
{
DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
if ((response is null) || (response.Answer.Count == 0))
return HealthCheckStatus.FailedToResolve;
IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response);
if (addresses.Count > 0)
{
HealthCheckStatus lastStatus = null;
foreach (IPAddress address in addresses)
{
lastStatus = await IsHealthyAsync(address);
if (lastStatus.IsHealthy)
return lastStatus;
}
return lastStatus;
}
return HealthCheckStatus.FailedToResolve;
}
default:
return HealthCheckStatus.NotSupported;
}
}
public async Task<HealthCheckStatus> IsHealthyAsync(IPAddress address)
{
switch (_type)
{
case HealthCheckType.Ping:
{
if (_service.DnsServer.Proxy != null)
throw new NotSupportedException("Health check type 'ping' is not supported over proxy.");
using (Ping ping = new Ping())
{
string lastReason;
int retry = 0;
do
{
PingReply reply = await ping.SendPingAsync(address, _timeout);
if (reply.Status == IPStatus.Success)
return HealthCheckStatus.Success;
lastReason = reply.Status.ToString();
}
while (++retry < _retries);
return new HealthCheckStatus(false, lastReason);
}
}
case HealthCheckType.Tcp:
{
Exception lastException = null;
string lastReason = null;
int retry = 0;
do
{
try
{
NetProxy proxy = _service.DnsServer.Proxy;
if (proxy is null)
{
using (Socket socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await socket.ConnectAsync(address, _port).WithTimeout(_timeout);
}
}
else
{
using (Socket socket = await proxy.ConnectAsync(new IPEndPoint(address, _port)).WithTimeout(_timeout))
{
//do nothing
}
}
return HealthCheckStatus.Success;
}
catch (TimeoutException)
{
lastReason = "Connection timed out.";
}
catch (SocketException ex)
{
lastReason = ex.Message;
}
catch (Exception ex)
{
lastException = ex;
}
}
while (++retry < _retries);
if (lastException is not null)
throw lastException;
return new HealthCheckStatus(false, lastReason);
}
case HealthCheckType.Http:
{
ConditionalHttpReload();
Exception lastException = null;
string lastReason = null;
int retry = 0;
do
{
try
{
IPEndPoint ep = new IPEndPoint(address, _url.Port);
Uri queryUri = new Uri(_url.Scheme + "://" + ep.ToString() + _url.PathAndQuery);
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, queryUri);
if (_url.IsDefaultPort)
httpRequest.Headers.Host = _url.Host;
else
httpRequest.Headers.Host = _url.Host + ":" + _url.Port;
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest);
if (httpResponse.IsSuccessStatusCode)
return HealthCheckStatus.Success;
lastReason = "Received HTTP status code: " + (int)httpResponse.StatusCode + " " + httpResponse.StatusCode.ToString();
break;
}
catch (TaskCanceledException)
{
lastReason = "Connection timed out.";
}
catch (Exception ex)
{
lastException = ex;
}
}
while (++retry < _retries);
if (lastException is not null)
throw lastException;
return new HealthCheckStatus(false, lastReason);
}
default:
throw new NotSupportedException();
}
}
#endregion
#region properties
public string Name
{ get { return _name; } }
public HealthCheckType Type
{ get { return _type; } }
public int Interval
{ get { return _interval; } }
public int Retries
{ get { return _retries; } }
public int Timeout
{ get { return _timeout; } }
public int Port
{ get { return _port; } }
public Uri Url
{ get { return _url; } }
public EmailAlert EmailAlert
{ get { return _emailAlert; } }
public WebHook WebHook
{ get { return _webHook; } }
#endregion
}
}

View File

@@ -0,0 +1,40 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using System;
namespace Failover
{
class HealthCheckStatus
{
public static readonly HealthCheckStatus Success = new HealthCheckStatus(true, null);
public static readonly HealthCheckStatus NotSupported = new HealthCheckStatus(false, "Not supported.");
public static readonly HealthCheckStatus FailedToResolve = new HealthCheckStatus(false, "Failed to resolve address.");
public readonly DateTime DateTime = DateTime.UtcNow;
public readonly bool IsHealthy;
public readonly string FailureReason;
public HealthCheckStatus(bool isHealthy, string failureReason)
{
IsHealthy = isHealthy;
FailureReason = failureReason;
}
}
}

View File

@@ -0,0 +1,256 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using DnsApplicationCommon;
using System;
using System.Net;
using System.Threading;
using TechnitiumLibrary.Net.Dns;
namespace Failover
{
class HealthMonitor : IDisposable
{
#region variables
readonly IDnsServer _dnsServer;
readonly IPAddress _address;
readonly string _domain;
readonly DnsResourceRecordType _type;
readonly HealthCheck _healthCheck;
readonly Timer _healthCheckTimer;
HealthCheckStatus _healthCheckStatus;
const int MONITOR_EXPIRY = 1 * 60 * 60 * 1000; //1 hour
DateTime _lastStatusCheckedOn;
#endregion
#region constructor
public HealthMonitor(IDnsServer dnsServer, IPAddress address, HealthCheck healthCheck)
{
_dnsServer = dnsServer;
_address = address;
_healthCheck = healthCheck;
_healthCheckTimer = new Timer(async delegate (object state)
{
try
{
if (_healthCheck is null)
{
_healthCheckStatus = null;
}
else
{
HealthCheckStatus healthCheckStatus = await _healthCheck.IsHealthyAsync(_address);
bool sendAlert = false;
if (_healthCheckStatus is null)
{
if (!healthCheckStatus.IsHealthy)
sendAlert = true;
}
else
{
if (_healthCheckStatus.IsHealthy != healthCheckStatus.IsHealthy)
sendAlert = true;
else if (_healthCheckStatus.FailureReason != healthCheckStatus.FailureReason)
sendAlert = true;
}
if (sendAlert)
{
EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null)
_ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, healthCheckStatus);
WebHook webHook = _healthCheck.WebHook;
if (webHook is not null)
_ = webHook.CallAsync(_address, _healthCheck.Name, healthCheckStatus);
}
_healthCheckStatus = healthCheckStatus;
}
}
catch (Exception ex)
{
_dnsServer.WriteLog(ex);
if (_healthCheckStatus is null)
{
EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null)
_ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, ex);
WebHook webHook = _healthCheck.WebHook;
if (webHook is not null)
_ = webHook.CallAsync(_address, _healthCheck.Name, ex);
_healthCheckStatus = new HealthCheckStatus(false, ex.ToString());
}
else
{
_healthCheckStatus = null;
}
}
finally
{
if (!_disposed && (_healthCheck is not null))
_healthCheckTimer.Change(_healthCheck.Interval, Timeout.Infinite);
}
}, null, Timeout.Infinite, Timeout.Infinite);
_healthCheckTimer.Change(0, Timeout.Infinite);
}
public HealthMonitor(IDnsServer dnsServer, string domain, DnsResourceRecordType type, HealthCheck healthCheck)
{
_dnsServer = dnsServer;
_domain = domain;
_type = type;
_healthCheck = healthCheck;
_healthCheckTimer = new Timer(async delegate (object state)
{
try
{
if (_healthCheck is null)
{
_healthCheckStatus = null;
}
else
{
HealthCheckStatus healthCheckStatus = await _healthCheck.IsHealthyAsync(_domain, _type);
bool sendAlert = false;
if (_healthCheckStatus is null)
{
if (!healthCheckStatus.IsHealthy)
sendAlert = true;
}
else
{
if (_healthCheckStatus.IsHealthy != healthCheckStatus.IsHealthy)
sendAlert = true;
else if (_healthCheckStatus.FailureReason != healthCheckStatus.FailureReason)
sendAlert = true;
}
if (sendAlert)
{
EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null)
_ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, healthCheckStatus);
WebHook webHook = _healthCheck.WebHook;
if (webHook is not null)
_ = webHook.CallAsync(_domain, _type, _healthCheck.Name, healthCheckStatus);
}
_healthCheckStatus = healthCheckStatus;
}
}
catch (Exception ex)
{
_dnsServer.WriteLog(ex);
if (_healthCheckStatus is null)
{
EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null)
_ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, ex);
WebHook webHook = _healthCheck.WebHook;
if (webHook is not null)
_ = webHook.CallAsync(_domain, _type, _healthCheck.Name, ex);
_healthCheckStatus = new HealthCheckStatus(false, ex.ToString());
}
else
{
_healthCheckStatus = null;
}
}
finally
{
if (!_disposed && (_healthCheck is not null))
_healthCheckTimer.Change(_healthCheck.Interval, Timeout.Infinite);
}
}, null, Timeout.Infinite, Timeout.Infinite);
_healthCheckTimer.Change(0, Timeout.Infinite);
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_healthCheckTimer is not null)
_healthCheckTimer.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region public
public bool IsExpired()
{
return DateTime.UtcNow > _lastStatusCheckedOn.AddMilliseconds(MONITOR_EXPIRY);
}
#endregion
#region properties
public HealthCheckStatus HealthCheckStatus
{
get
{
_lastStatusCheckedOn = DateTime.UtcNow;
return _healthCheckStatus;
}
}
#endregion
}
}

View File

@@ -0,0 +1,428 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using DnsApplicationCommon;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using TechnitiumLibrary.Net.Dns;
namespace Failover
{
class HealthMonitoringService : IDisposable
{
#region variables
static HealthMonitoringService _healthMonitoringService;
readonly IDnsServer _dnsServer;
readonly ConcurrentDictionary<string, HealthCheck> _healthChecks = new ConcurrentDictionary<string, HealthCheck>(1, 5);
readonly ConcurrentDictionary<string, EmailAlert> _emailAlerts = new ConcurrentDictionary<string, EmailAlert>(1, 2);
readonly ConcurrentDictionary<string, WebHook> _webHooks = new ConcurrentDictionary<string, WebHook>(1, 2);
readonly ConcurrentDictionary<IPAddress, AddressMonitoring> _addressMonitoring = new ConcurrentDictionary<IPAddress, AddressMonitoring>();
readonly ConcurrentDictionary<string, DomainMonitoring> _domainMonitoringA = new ConcurrentDictionary<string, DomainMonitoring>();
readonly ConcurrentDictionary<string, DomainMonitoring> _domainMonitoringAAAA = new ConcurrentDictionary<string, DomainMonitoring>();
readonly Timer _maintenanceTimer;
const int MAINTENANCE_TIMER_INTERVAL = 15 * 60 * 1000; //15 mins
#endregion
#region constructor
private HealthMonitoringService(IDnsServer dnsServer)
{
_dnsServer = dnsServer;
_maintenanceTimer = new Timer(delegate (object state)
{
try
{
foreach (KeyValuePair<IPAddress, AddressMonitoring> monitoring in _addressMonitoring)
{
if (monitoring.Value.IsExpired())
{
if (_addressMonitoring.TryRemove(monitoring.Key, out AddressMonitoring removedMonitoring))
removedMonitoring.Dispose();
}
}
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringA)
{
if (monitoring.Value.IsExpired())
{
if (_domainMonitoringA.TryRemove(monitoring.Key, out DomainMonitoring removedMonitoring))
removedMonitoring.Dispose();
}
}
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringAAAA)
{
if (monitoring.Value.IsExpired())
{
if (_domainMonitoringAAAA.TryRemove(monitoring.Key, out DomainMonitoring removedMonitoring))
removedMonitoring.Dispose();
}
}
}
catch (Exception ex)
{
_dnsServer.WriteLog(ex);
}
finally
{
if (!_disposed)
_maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);
}
}, null, Timeout.Infinite, Timeout.Infinite);
_maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
foreach (KeyValuePair<string, HealthCheck> healthCheck in _healthChecks)
healthCheck.Value.Dispose();
_healthChecks.Clear();
foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)
emailAlert.Value.Dispose();
_emailAlerts.Clear();
foreach (KeyValuePair<string, WebHook> webHook in _webHooks)
webHook.Value.Dispose();
_webHooks.Clear();
foreach (KeyValuePair<IPAddress, AddressMonitoring> monitoring in _addressMonitoring)
monitoring.Value.Dispose();
_addressMonitoring.Clear();
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringA)
monitoring.Value.Dispose();
_domainMonitoringA.Clear();
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringAAAA)
monitoring.Value.Dispose();
_domainMonitoringAAAA.Clear();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region static
public static HealthMonitoringService Create(IDnsServer dnsServer)
{
if (_healthMonitoringService is null)
_healthMonitoringService = new HealthMonitoringService(dnsServer);
return _healthMonitoringService;
}
public void Initialize(dynamic jsonConfig)
{
//email alerts
{
//add or update email alerts
foreach (dynamic jsonEmailAlert in jsonConfig.emailAlerts)
{
string name;
if (jsonEmailAlert.name is null)
name = "default";
else
name = jsonEmailAlert.name.Value;
if (_emailAlerts.TryGetValue(name, out EmailAlert existingEmailAlert))
{
//update
existingEmailAlert.Reload(jsonEmailAlert);
}
else
{
//add
EmailAlert emailAlert = new EmailAlert(this, jsonEmailAlert);
_emailAlerts.TryAdd(emailAlert.Name, emailAlert);
}
}
//remove email alerts that dont exists in config
foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)
{
bool emailAlertExists = false;
foreach (dynamic jsonEmailAlert in jsonConfig.emailAlerts)
{
string name;
if (jsonEmailAlert.name is null)
name = "default";
else
name = jsonEmailAlert.name.Value;
if (name == emailAlert.Key)
{
emailAlertExists = true;
break;
}
}
if (!emailAlertExists)
{
if (_emailAlerts.TryRemove(emailAlert.Key, out EmailAlert removedEmailAlert))
removedEmailAlert.Dispose();
}
}
}
//web hooks
{
//add or update email alerts
foreach (dynamic jsonWebHook in jsonConfig.webHooks)
{
string name;
if (jsonWebHook.name is null)
name = "default";
else
name = jsonWebHook.name.Value;
if (_webHooks.TryGetValue(name, out WebHook existingWebHook))
{
//update
existingWebHook.Reload(jsonWebHook);
}
else
{
//add
WebHook webHook = new WebHook(this, jsonWebHook);
_webHooks.TryAdd(webHook.Name, webHook);
}
}
//remove email alerts that dont exists in config
foreach (KeyValuePair<string, WebHook> webHook in _webHooks)
{
bool webHookExists = false;
foreach (dynamic jsonWebHook in jsonConfig.webHooks)
{
string name;
if (jsonWebHook.name is null)
name = "default";
else
name = jsonWebHook.name.Value;
if (name == webHook.Key)
{
webHookExists = true;
break;
}
}
if (!webHookExists)
{
if (_webHooks.TryRemove(webHook.Key, out WebHook removedWebHook))
removedWebHook.Dispose();
}
}
}
//health checks
{
//add or update health checks
foreach (dynamic jsonHealthCheck in jsonConfig.healthChecks)
{
string name;
if (jsonHealthCheck.name is null)
name = "default";
else
name = jsonHealthCheck.name.Value;
if (_healthChecks.TryGetValue(name, out HealthCheck existingHealthCheck))
{
//update
existingHealthCheck.Reload(jsonHealthCheck);
}
else
{
//add
HealthCheck healthCheck = new HealthCheck(this, jsonHealthCheck);
_healthChecks.TryAdd(healthCheck.Name, healthCheck);
}
}
//remove health checks that dont exists in config
foreach (KeyValuePair<string, HealthCheck> healthCheck in _healthChecks)
{
bool healthCheckExists = false;
foreach (dynamic jsonHealthCheck in jsonConfig.healthChecks)
{
string name;
if (jsonHealthCheck.name is null)
name = "default";
else
name = jsonHealthCheck.name.Value;
if (name == healthCheck.Key)
{
healthCheckExists = true;
break;
}
}
if (!healthCheckExists)
{
if (_healthChecks.TryRemove(healthCheck.Key, out HealthCheck removedHealthCheck))
{
//remove health monitors using this health check
foreach (KeyValuePair<IPAddress, AddressMonitoring> monitoring in _addressMonitoring)
monitoring.Value.RemoveHealthMonitor(healthCheck.Key);
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringA)
monitoring.Value.RemoveHealthMonitor(healthCheck.Key);
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringAAAA)
monitoring.Value.RemoveHealthMonitor(healthCheck.Key);
removedHealthCheck.Dispose();
}
}
}
}
}
#endregion
#region public
public HealthCheckStatus QueryStatus(IPAddress address, string healthCheck, bool tryAdd)
{
if (_addressMonitoring.TryGetValue(address, out AddressMonitoring monitoring))
{
return monitoring.QueryStatus(healthCheck);
}
else if (tryAdd)
{
monitoring = new AddressMonitoring(this, address, healthCheck);
if (!_addressMonitoring.TryAdd(address, monitoring))
monitoring.Dispose(); //failed to add first
}
return null;
}
public HealthCheckStatus QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, bool tryAdd)
{
domain = domain.ToLower();
switch (type)
{
case DnsResourceRecordType.A:
{
if (_domainMonitoringA.TryGetValue(domain, out DomainMonitoring monitoring))
{
return monitoring.QueryStatus(healthCheck);
}
else if (tryAdd)
{
monitoring = new DomainMonitoring(this, domain, type, healthCheck);
if (!_domainMonitoringA.TryAdd(domain, monitoring))
monitoring.Dispose(); //failed to add first
}
}
break;
case DnsResourceRecordType.AAAA:
{
if (_domainMonitoringAAAA.TryGetValue(domain, out DomainMonitoring monitoring))
{
return monitoring.QueryStatus(healthCheck);
}
else if (tryAdd)
{
monitoring = new DomainMonitoring(this, domain, type, healthCheck);
if (!_domainMonitoringAAAA.TryAdd(domain, monitoring))
monitoring.Dispose(); //failed to add first
}
}
break;
}
return null;
}
#endregion
#region properties
internal IReadOnlyDictionary<string, HealthCheck> HealthChecks
{ get { return _healthChecks; } }
internal IReadOnlyDictionary<string, EmailAlert> EmailAlerts
{ get { return _emailAlerts; } }
internal IReadOnlyDictionary<string, WebHook> WebHooks
{ get { return _webHooks; } }
internal IDnsServer DnsServer
{ get { return _dnsServer; } }
#endregion
}
}

377
Apps/FailoverApp/WebHook.cs Normal file
View File

@@ -0,0 +1,377 @@
/*
Technitium DNS Server
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Proxy;
namespace Failover
{
class WebHook : IDisposable
{
#region variables
readonly HealthMonitoringService _service;
string _name;
bool _enabled;
Uri[] _urls;
SocketsHttpHandler _httpHandler;
HttpClient _httpClient;
#endregion
#region constructor
public WebHook(HealthMonitoringService service, dynamic jsonWebHook)
{
_service = service;
Reload(jsonWebHook);
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_httpClient != null)
{
_httpClient.Dispose();
_httpClient = null;
}
if (_httpHandler != null)
{
_httpHandler.Dispose();
_httpHandler = null;
}
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region private
private void ConditionalHttpReload()
{
bool handlerChanged = false;
NetProxy proxy = _service.DnsServer.Proxy;
if (_httpHandler is null)
{
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
httpHandler.Proxy = proxy;
httpHandler.AllowAutoRedirect = true;
httpHandler.MaxAutomaticRedirections = 10;
_httpHandler = httpHandler;
handlerChanged = true;
}
else
{
if (_httpHandler.Proxy != proxy)
{
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
httpHandler.Proxy = proxy;
httpHandler.AllowAutoRedirect = true;
httpHandler.MaxAutomaticRedirections = 10;
SocketsHttpHandler oldHttpHandler = _httpHandler;
_httpHandler = httpHandler;
handlerChanged = true;
oldHttpHandler.Dispose();
}
}
if (_httpClient is null)
{
HttpClient httpClient = new HttpClient(_httpHandler);
_httpClient = httpClient;
}
else
{
if (handlerChanged)
{
HttpClient httpClient = new HttpClient(_httpHandler);
HttpClient oldHttpClient = _httpClient;
_httpClient = httpClient;
oldHttpClient.Dispose();
}
}
}
private async Task CallAsync(HttpContent content)
{
ConditionalHttpReload();
async Task CallWebHook(Uri url)
{
try
{
HttpResponseMessage response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
_service.DnsServer.WriteLog(ex);
}
}
List<Task> tasks = new List<Task>();
foreach (Uri url in _urls)
tasks.Add(CallWebHook(url));
await Task.WhenAll(tasks);
}
#endregion
#region public
public void Reload(dynamic jsonWebHook)
{
if (jsonWebHook.name is null)
_name = "default";
else
_name = jsonWebHook.name.Value;
if (jsonWebHook.enabled is null)
_enabled = false;
else
_enabled = jsonWebHook.enabled.Value;
if (jsonWebHook.urls is null)
{
_urls = null;
}
else
{
_urls = new Uri[jsonWebHook.urls.Count];
for (int i = 0; i < _urls.Length; i++)
_urls[i] = new Uri(jsonWebHook.urls[i].Value);
}
ConditionalHttpReload();
}
public Task CallAsync(IPAddress address, string healthCheck, HealthCheckStatus healthCheckStatus)
{
if (!_enabled)
return Task.CompletedTask;
HttpContent content;
{
using (MemoryStream mS = new MemoryStream())
{
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("address");
jsonWriter.WriteValue(address.ToString());
jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy");
jsonWriter.WriteValue(healthCheckStatus.IsHealthy);
jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(healthCheckStatus.FailureReason);
jsonWriter.WritePropertyName("dateTime");
jsonWriter.WriteValue(healthCheckStatus.DateTime);
jsonWriter.WriteEndObject();
jsonWriter.Flush();
content = new ByteArrayContent(mS.ToArray());
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
return CallAsync(content);
}
public Task CallAsync(IPAddress address, string healthCheck, Exception ex)
{
if (!_enabled)
return Task.CompletedTask;
HttpContent content;
{
using (MemoryStream mS = new MemoryStream())
{
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("address");
jsonWriter.WriteValue(address.ToString());
jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy");
jsonWriter.WriteValue(false);
jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(ex.ToString());
jsonWriter.WritePropertyName("dateTime");
jsonWriter.WriteValue(DateTime.UtcNow);
jsonWriter.WriteEndObject();
jsonWriter.Flush();
content = new ByteArrayContent(mS.ToArray());
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
return CallAsync(content);
}
public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckStatus healthCheckStatus)
{
if (!_enabled)
return Task.CompletedTask;
HttpContent content;
{
using (MemoryStream mS = new MemoryStream())
{
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("domain");
jsonWriter.WriteValue(domain);
jsonWriter.WritePropertyName("recordType");
jsonWriter.WriteValue(type.ToString());
jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy");
jsonWriter.WriteValue(healthCheckStatus.IsHealthy);
jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(healthCheckStatus.FailureReason);
jsonWriter.WritePropertyName("dateTime");
jsonWriter.WriteValue(healthCheckStatus.DateTime);
jsonWriter.WriteEndObject();
jsonWriter.Flush();
content = new ByteArrayContent(mS.ToArray());
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
return CallAsync(content);
}
public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)
{
if (!_enabled)
return Task.CompletedTask;
HttpContent content;
{
using (MemoryStream mS = new MemoryStream())
{
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("domain");
jsonWriter.WriteValue(domain);
jsonWriter.WritePropertyName("recordType");
jsonWriter.WriteValue(type.ToString());
jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy");
jsonWriter.WriteValue(false);
jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(ex.ToString());
jsonWriter.WritePropertyName("dateTime");
jsonWriter.WriteValue(DateTime.UtcNow);
jsonWriter.WriteEndObject();
jsonWriter.Flush();
content = new ByteArrayContent(mS.ToArray());
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
return CallAsync(content);
}
#endregion
#region properties
public string Name
{ get { return _name; } }
public bool Enabled
{ get { return _enabled; } }
public IReadOnlyList<Uri> Urls
{ get { return _urls; } }
#endregion
}
}

View File

@@ -0,0 +1,64 @@
{
"healthChecks": [
{
"name": "ping",
"type": "ping",
"interval": 60,
"retries": 3,
"timeout": 10,
"emailAlert": "default"
},
{
"name": "tcp80",
"type": "tcp",
"interval": 60,
"retries": 3,
"timeout": 10,
"port": 80
},
{
"name": "tcp443",
"type": "tcp",
"interval": 60,
"retries": 3,
"timeout": 10,
"port": 443
},
{
"name": "www.example.com",
"type": "http",
"interval": 60,
"retries": 3,
"timeout": 10,
"url": "https://www.example.com",
"emailAlert": "default",
"webHook": "default"
}
],
"emailAlerts": [
{
"name": "default",
"enabled": false,
"alertTo": [
"admin@example.com"
],
"smtpServer": "smtp.example.com",
"smtpPort": 465,
"startTls": false,
"smtpOverTls": true,
"username": "alerts@example.com",
"password": "password",
"mailFrom": "alerts@example.com",
"mailFromName": "DNS Server Alert"
}
],
"webHooks": [
{
"name": "default",
"enabled": false,
"urls": [
"https://webhooks.example.com/default"
]
}
]
}