Address.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using DnsServerCore.ApplicationCommon;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Net;
  19. using System.Net.Sockets;
  20. using System.Text.Json;
  21. using System.Threading.Tasks;
  22. using TechnitiumLibrary;
  23. using TechnitiumLibrary.Net.Dns;
  24. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  25. namespace Failover
  26. {
  27. enum FailoverType
  28. {
  29. Unknown = 0,
  30. Primary = 1,
  31. Secondary = 2
  32. }
  33. public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler
  34. {
  35. #region variables
  36. HealthService _healthService;
  37. #endregion
  38. #region IDisposable
  39. bool _disposed;
  40. public void Dispose()
  41. {
  42. if (_disposed)
  43. return;
  44. if (_healthService is not null)
  45. _healthService.Dispose();
  46. _disposed = true;
  47. }
  48. #endregion
  49. #region private
  50. private void GetAnswers(JsonElement jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers)
  51. {
  52. switch (question.Type)
  53. {
  54. case DnsResourceRecordType.A:
  55. foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())
  56. {
  57. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  58. if (address.AddressFamily == AddressFamily.InterNetwork)
  59. {
  60. HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true);
  61. switch (response.Status)
  62. {
  63. case HealthStatus.Unknown:
  64. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 10, new DnsARecordData(address)));
  65. break;
  66. case HealthStatus.Healthy:
  67. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecordData(address)));
  68. break;
  69. }
  70. }
  71. }
  72. break;
  73. case DnsResourceRecordType.AAAA:
  74. foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())
  75. {
  76. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  77. if (address.AddressFamily == AddressFamily.InterNetworkV6)
  78. {
  79. HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true);
  80. switch (response.Status)
  81. {
  82. case HealthStatus.Unknown:
  83. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 10, new DnsAAAARecordData(address)));
  84. break;
  85. case HealthStatus.Healthy:
  86. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecordData(address)));
  87. break;
  88. }
  89. }
  90. }
  91. break;
  92. }
  93. }
  94. private void GetStatusAnswers(JsonElement jsonAddresses, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers)
  95. {
  96. foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())
  97. {
  98. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  99. HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false);
  100. string text = "app=failover; addressType=" + type.ToString() + "; address=" + address.ToString() + "; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";";
  101. if (response.Status == HealthStatus.Failed)
  102. text += " failureReason=" + response.FailureReason + ";";
  103. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text)));
  104. }
  105. }
  106. #endregion
  107. #region public
  108. public Task InitializeAsync(IDnsServer dnsServer, string config)
  109. {
  110. if (_healthService is null)
  111. _healthService = HealthService.Create(dnsServer);
  112. _healthService.Initialize(config);
  113. return Task.CompletedTask;
  114. }
  115. public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)
  116. {
  117. DnsQuestionRecord question = request.Question[0];
  118. if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))
  119. return Task.FromResult<DnsDatagram>(null);
  120. switch (question.Type)
  121. {
  122. case DnsResourceRecordType.A:
  123. case DnsResourceRecordType.AAAA:
  124. {
  125. using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);
  126. JsonElement jsonAppRecordData = jsonDocument.RootElement;
  127. string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null);
  128. Uri healthCheckUrl = null;
  129. if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null))
  130. {
  131. //read health check url only for http/https type checks and only when app config does not have an url configured
  132. if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null))
  133. {
  134. healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString());
  135. }
  136. else
  137. {
  138. if (hc.Type == HealthCheckType.Https)
  139. healthCheckUrl = new Uri("https://" + question.Name);
  140. else
  141. healthCheckUrl = new Uri("http://" + question.Name);
  142. }
  143. }
  144. List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
  145. if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary))
  146. GetAnswers(jsonPrimary, question, appRecordTtl, healthCheck, healthCheckUrl, answers);
  147. if (answers.Count == 0)
  148. {
  149. if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary))
  150. GetAnswers(jsonSecondary, question, appRecordTtl, healthCheck, healthCheckUrl, answers);
  151. if (answers.Count == 0)
  152. {
  153. if (jsonAppRecordData.TryGetProperty("serverDown", out JsonElement jsonServerDown))
  154. {
  155. if (question.Type == DnsResourceRecordType.A)
  156. {
  157. foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray())
  158. {
  159. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  160. if (address.AddressFamily == AddressFamily.InterNetwork)
  161. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecordData(address)));
  162. }
  163. }
  164. else
  165. {
  166. foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray())
  167. {
  168. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  169. if (address.AddressFamily == AddressFamily.InterNetworkV6)
  170. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecordData(address)));
  171. }
  172. }
  173. }
  174. if (answers.Count == 0)
  175. return Task.FromResult<DnsDatagram>(null);
  176. }
  177. }
  178. if (answers.Count > 1)
  179. answers.Shuffle();
  180. return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
  181. }
  182. case DnsResourceRecordType.TXT:
  183. {
  184. using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);
  185. JsonElement jsonAppRecordData = jsonDocument.RootElement;
  186. bool allowTxtStatus = jsonAppRecordData.GetPropertyValue("allowTxtStatus", false);
  187. if (!allowTxtStatus)
  188. return Task.FromResult<DnsDatagram>(null);
  189. string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null);
  190. Uri healthCheckUrl = null;
  191. if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null))
  192. {
  193. //read health check url only for http/https type checks and only when app config does not have an url configured
  194. if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null))
  195. {
  196. healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString());
  197. }
  198. else
  199. {
  200. if (hc.Type == HealthCheckType.Https)
  201. healthCheckUrl = new Uri("https://" + question.Name);
  202. else
  203. healthCheckUrl = new Uri("http://" + question.Name);
  204. }
  205. }
  206. List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
  207. if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary))
  208. GetStatusAnswers(jsonPrimary, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, answers);
  209. if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary))
  210. GetStatusAnswers(jsonSecondary, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, answers);
  211. return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
  212. }
  213. default:
  214. return Task.FromResult<DnsDatagram>(null);
  215. }
  216. }
  217. #endregion
  218. #region properties
  219. public string Description
  220. { 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 all 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\nIf an URL is provided for the health check in the app's config then it will override the 'healthCheckUrl' parameter. When an URL is not provided in 'healthCheckUrl' parameter for 'http' or 'https' type health check, the domain name of the APP record will be used to auto generate an URL.\n\nSet 'allowTxtStatus' parameter to 'true' in your APP record data to allow checking health status by querying for TXT record."; } }
  221. public string ApplicationRecordDataTemplate
  222. {
  223. get
  224. {
  225. return @"{
  226. ""primary"": [
  227. ""1.1.1.1"",
  228. ""::1""
  229. ],
  230. ""secondary"": [
  231. ""2.2.2.2"",
  232. ""::2""
  233. ],
  234. ""serverDown"": [
  235. ""3.3.3.3""
  236. ],
  237. ""healthCheck"": ""https"",
  238. ""healthCheckUrl"": ""https://www.example.com/"",
  239. ""allowTxtStatus"": false
  240. }";
  241. }
  242. }
  243. #endregion
  244. }
  245. }