From 2e1b3e01da19c56cc50342b6d8693f2ead80c126 Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sat, 7 Aug 2021 16:42:38 +0530 Subject: [PATCH] FailoverApp: updated code to support `healthCheckUrl` and `serverDown` features. --- Apps/FailoverApp/Address.cs | 64 ++++++++++++++++++++++++++----------- Apps/FailoverApp/CNAME.cs | 51 ++++++++++++++++++----------- 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/Apps/FailoverApp/Address.cs b/Apps/FailoverApp/Address.cs index 85bb046b..c798ae7e 100644 --- a/Apps/FailoverApp/Address.cs +++ b/Apps/FailoverApp/Address.cs @@ -19,6 +19,7 @@ along with this program. If not, see . using DnsApplicationCommon; using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; @@ -29,11 +30,19 @@ using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Failover { + enum FailoverType + { + Unknown = 0, + Primary = 1, + Secondary = 2, + ServerDown = 3 + } + public class Address : IDnsApplicationRequestHandler { #region variables - HealthMonitoringService _healthMonitor; + HealthMonitoringService _healthService; #endregion @@ -46,8 +55,8 @@ namespace Failover if (_disposed) return; - if (_healthMonitor is not null) - _healthMonitor.Dispose(); + if (_healthService is not null) + _healthService.Dispose(); _disposed = true; } @@ -56,7 +65,7 @@ namespace Failover #region private - private void GetAnswers(dynamic jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List answers) + private void GetAnswers(dynamic jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { if (jsonAddresses == null) return; @@ -70,7 +79,7 @@ namespace Failover if (address.AddressFamily == AddressFamily.InterNetwork) { - HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, true); + HealthCheckStatus status = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true); if (status is null) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecord(address))); else if (status.IsHealthy) @@ -86,7 +95,7 @@ namespace Failover if (address.AddressFamily == AddressFamily.InterNetworkV6) { - HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, true); + HealthCheckStatus status = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true); if (status is null) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecord(address))); else if (status.IsHealthy) @@ -97,7 +106,7 @@ namespace Failover } } - private void GetStatusAnswers(dynamic jsonAddresses, bool primary, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List answers) + private void GetStatusAnswers(dynamic jsonAddresses, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { if (jsonAddresses == null) return; @@ -105,9 +114,9 @@ namespace Failover foreach (dynamic jsonAddress in jsonAddresses) { IPAddress address = IPAddress.Parse(jsonAddress.Value); - HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, false); + HealthCheckStatus status = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false); - string text = "app=failover; addressType=" + (primary ? "primary" : "secondary") + "; address=" + address.ToString() + "; healthCheck=" + healthCheck; + string text = "app=failover; addressType=" + type.ToString() + "; address=" + address.ToString() + "; healthCheck=" + healthCheck; if (status is null) text += "; healthStatus=Unknown;"; @@ -126,10 +135,10 @@ namespace Failover public Task InitializeAsync(IDnsServer dnsServer, string config) { - if (_healthMonitor is null) - _healthMonitor = HealthMonitoringService.Create(dnsServer); + if (_healthService is null) + _healthService = HealthMonitoringService.Create(dnsServer); - _healthMonitor.Initialize(JsonConvert.DeserializeObject(config)); + _healthService.Initialize(JsonConvert.DeserializeObject(config)); return Task.CompletedTask; } @@ -145,15 +154,23 @@ namespace Failover dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); string healthCheck = jsonAppRecordData.healthCheck?.Value; + Uri healthCheckUrl = null; + + if (jsonAppRecordData.healthCheckUrl != null) + healthCheckUrl = new Uri(jsonAppRecordData.healthCheckUrl.Value); List answers = new List(); - GetAnswers(jsonAppRecordData.primary, question, appRecordTtl, healthCheck, answers); + GetAnswers(jsonAppRecordData.primary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); if (answers.Count == 0) { - GetAnswers(jsonAppRecordData.secondary, question, appRecordTtl, healthCheck, answers); + GetAnswers(jsonAppRecordData.secondary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); if (answers.Count == 0) - return Task.FromResult(null); + { + GetAnswers(jsonAppRecordData.serverDown, question, appRecordTtl, healthCheck, healthCheckUrl, answers); + if (answers.Count == 0) + return Task.FromResult(null); + } } if (answers.Count > 1) @@ -177,11 +194,16 @@ namespace Failover return Task.FromResult(null); string healthCheck = jsonAppRecordData.healthCheck?.Value; + Uri healthCheckUrl = null; + + if (jsonAppRecordData.healthCheckUrl != null) + healthCheckUrl = new Uri(jsonAppRecordData.healthCheckUrl.Value); List answers = new List(); - GetStatusAnswers(jsonAppRecordData.primary, true, question, 30, healthCheck, answers); - GetStatusAnswers(jsonAppRecordData.secondary, false, question, 30, healthCheck, answers); + GetStatusAnswers(jsonAppRecordData.primary, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, answers); + GetStatusAnswers(jsonAppRecordData.secondary, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, answers); + GetStatusAnswers(jsonAppRecordData.serverDown, FailoverType.ServerDown, question, 30, healthCheck, healthCheckUrl, answers); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); } @@ -196,7 +218,7 @@ namespace Failover #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."; } } + { 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. When none of the primary and secondary addresses are healthy, the app returns healthy addresses from the server down set of addresses. The server down feature is expected to be used for showing a service status page and not to serve the actual content.\n\nSet 'allowTxtStatus' to 'true' in your APP record data to allow checking health status by querying for TXT record."; } } public string ApplicationRecordDataTemplate { @@ -211,7 +233,11 @@ namespace Failover ""2.2.2.2"", ""::2"" ], - ""healthCheck"": ""tcp80"", + ""serverDown"": [ + ""3.3.3.3"" + ], + ""healthCheck"": ""http"", + ""healthCheckUrl"": ""https://www.example.com"", ""allowTxtStatus"": false }"; } diff --git a/Apps/FailoverApp/CNAME.cs b/Apps/FailoverApp/CNAME.cs index 95f30233..89007c0b 100644 --- a/Apps/FailoverApp/CNAME.cs +++ b/Apps/FailoverApp/CNAME.cs @@ -32,7 +32,7 @@ namespace Failover { #region variables - HealthMonitoringService _healthMonitor; + HealthMonitoringService _healthService; #endregion @@ -45,8 +45,8 @@ namespace Failover if (_disposed) return; - if (_healthMonitor is not null) - _healthMonitor.Dispose(); + if (_healthService is not null) + _healthService.Dispose(); _disposed = true; } @@ -55,9 +55,9 @@ namespace Failover #region private - private IReadOnlyList GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck) + private IReadOnlyList GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck, Uri healthCheckUrl) { - HealthCheckStatus status = _healthMonitor.QueryStatus(domain, question.Type, healthCheck, true); + HealthCheckStatus status = _healthService.QueryStatus(domain, question.Type, healthCheck, healthCheckUrl, true); if (status is null) { if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex @@ -76,12 +76,12 @@ namespace Failover return null; } - private void GetStatusAnswers(string domain, bool primary, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List answers) + private void GetStatusAnswers(string domain, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { { - HealthCheckStatus status = _healthMonitor.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, false); + HealthCheckStatus status = _healthService.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, healthCheckUrl, false); - string text = "app=failover; cnameType=" + (primary ? "primary" : "secondary") + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck; + string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck; if (status is null) text += "; healthStatus=Unknown;"; @@ -94,9 +94,9 @@ namespace Failover } { - HealthCheckStatus status = _healthMonitor.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, false); + HealthCheckStatus status = _healthService.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, healthCheckUrl, false); - string text = "app=failover; cnameType=" + (primary ? "primary" : "secondary") + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck; + string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck; if (status is null) text += "; healthStatus=Unknown;"; @@ -115,8 +115,8 @@ namespace Failover public Task InitializeAsync(IDnsServer dnsServer, string config) { - if (_healthMonitor is null) - _healthMonitor = HealthMonitoringService.Create(dnsServer); + if (_healthService is null) + _healthService = HealthMonitoringService.Create(dnsServer); //let Address class initialize config @@ -130,6 +130,10 @@ namespace Failover dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); string healthCheck = jsonAppRecordData.healthCheck?.Value; + Uri healthCheckUrl = null; + + if (jsonAppRecordData.healthCheckUrl != null) + healthCheckUrl = new Uri(jsonAppRecordData.healthCheckUrl.Value); IReadOnlyList answers; @@ -147,27 +151,36 @@ namespace Failover List txtAnswers = new List(); - GetStatusAnswers(jsonAppRecordData.primary.Value, true, question, 30, healthCheck, txtAnswers); + GetStatusAnswers(jsonAppRecordData.primary.Value, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, txtAnswers); foreach (dynamic jsonDomain in jsonAppRecordData.secondary) - GetStatusAnswers(jsonDomain.Value, false, question, 30, healthCheck, txtAnswers); + GetStatusAnswers(jsonDomain.Value, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, txtAnswers); + + GetStatusAnswers(jsonAppRecordData.serverDown.Value, FailoverType.ServerDown, question, 30, healthCheck, healthCheckUrl, txtAnswers); answers = txtAnswers; } else { - answers = GetAnswers(jsonAppRecordData.primary.Value, question, zoneName, appRecordTtl, healthCheck); + answers = GetAnswers(jsonAppRecordData.primary.Value, question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); if (answers is null) { foreach (dynamic jsonDomain in jsonAppRecordData.secondary) { - answers = GetAnswers(jsonDomain.Value, question, zoneName, appRecordTtl, healthCheck); + answers = GetAnswers(jsonDomain.Value, question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); if (answers is not null) break; } if (answers is null) - return Task.FromResult(null); + { + if (jsonAppRecordData.serverDown == null) + return Task.FromResult(null); + + answers = GetAnswers(jsonAppRecordData.serverDown.Value, question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); + if (answers is null) + return Task.FromResult(null); + } } } @@ -179,7 +192,7 @@ namespace Failover #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."; } } + { 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 the given order of preference that is healthy. When none of the primary and secondary domain names are healthy, the app returns the server down domain name. The server down feature is expected to be used for showing a service status page and not to serve the actual content. 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 { @@ -191,7 +204,9 @@ namespace Failover ""sg.example.org"", ""eu.example.org"" ], + ""serverDown"": ""status.example.org"", ""healthCheck"": ""tcp443"", + ""healthCheckUrl"": null, ""allowTxtStatus"": false }"; }