123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- /*
- Technitium DNS Server
- Copyright (C) 2024 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.Text.Json;
- using System.Threading;
- using System.Threading.Tasks;
- using TechnitiumLibrary;
- using TechnitiumLibrary.Net;
- using TechnitiumLibrary.Net.Dns;
- using TechnitiumLibrary.Net.Dns.ResourceRecords;
- using TechnitiumLibrary.Net.Http.Client;
- using TechnitiumLibrary.Net.Proxy;
- namespace Failover
- {
- enum HealthCheckType
- {
- Unknown = 0,
- Ping = 1,
- Tcp = 2,
- Http = 3,
- Https = 4
- }
- class HealthCheck : IDisposable
- {
- #region variables
- const string HTTP_HEALTH_CHECK_USER_AGENT = "DNS Failover App (Technitium DNS Server)";
- readonly HealthService _service;
- readonly string _name;
- HealthCheckType _type;
- int _interval;
- int _retries;
- int _timeout;
- int _port;
- Uri _url;
- EmailAlert _emailAlert;
- WebHook _webHook;
- SocketsHttpHandler _httpHandler;
- HttpClientNetworkHandler _httpCustomResolverHandler;
- HttpClient _httpClient;
- #endregion
- #region constructor
- public HealthCheck(HealthService service, JsonElement jsonHealthCheck)
- {
- _service = service;
- _name = jsonHealthCheck.GetPropertyValue("name", "default");
- 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()
- {
- switch (_type)
- {
- case HealthCheckType.Http:
- case HealthCheckType.Https:
- bool handlerChanged = false;
- NetProxy proxy = _service.DnsServer.Proxy;
- if (_httpHandler is null)
- {
- SocketsHttpHandler httpHandler = new SocketsHttpHandler();
- httpHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);
- httpHandler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(Math.Max(10000, _timeout));
- httpHandler.Proxy = proxy;
- httpHandler.UseProxy = proxy is not null;
- httpHandler.AllowAutoRedirect = false;
- _httpHandler = httpHandler;
- handlerChanged = true;
- }
- else
- {
- if ((_httpHandler.ConnectTimeout.TotalMilliseconds != _timeout) || (_httpHandler.Proxy != proxy))
- {
- SocketsHttpHandler httpHandler = new SocketsHttpHandler();
- httpHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);
- httpHandler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(Math.Max(10000, _timeout));
- httpHandler.Proxy = proxy;
- httpHandler.UseProxy = proxy is not null;
- httpHandler.AllowAutoRedirect = false;
- SocketsHttpHandler oldHttpHandler = _httpHandler;
- _httpHandler = httpHandler;
- handlerChanged = true;
- oldHttpHandler.Dispose();
- }
- }
- if ((_httpCustomResolverHandler is null) || handlerChanged)
- _httpCustomResolverHandler = new HttpClientNetworkHandler(_httpHandler, _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default, _service.DnsServer);
- if (_httpClient is null)
- {
- HttpClient httpClient = new HttpClient(_httpCustomResolverHandler);
- httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);
- httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(HTTP_HEALTH_CHECK_USER_AGENT);
- httpClient.DefaultRequestHeaders.ConnectionClose = true;
- _httpClient = httpClient;
- }
- else
- {
- if (handlerChanged || (_httpClient.Timeout.TotalMilliseconds != _timeout))
- {
- HttpClient httpClient = new HttpClient(_httpCustomResolverHandler);
- httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);
- httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(HTTP_HEALTH_CHECK_USER_AGENT);
- httpClient.DefaultRequestHeaders.ConnectionClose = true;
- HttpClient oldHttpClient = _httpClient;
- _httpClient = httpClient;
- oldHttpClient.Dispose();
- }
- }
- break;
- default:
- if (_httpClient != null)
- {
- _httpClient.Dispose();
- _httpClient = null;
- }
- if (_httpHandler != null)
- {
- _httpHandler.Dispose();
- _httpHandler = null;
- }
- break;
- }
- }
- #endregion
- #region public
- public void Reload(JsonElement jsonHealthCheck)
- {
- _type = Enum.Parse<HealthCheckType>(jsonHealthCheck.GetPropertyValue("type", "Tcp"), true);
- _interval = jsonHealthCheck.GetPropertyValue("interval", 60) * 1000;
- _retries = jsonHealthCheck.GetPropertyValue("retries", 3);
- _timeout = jsonHealthCheck.GetPropertyValue("timeout", 10) * 1000;
- _port = jsonHealthCheck.GetPropertyValue("port", 80);
- if (jsonHealthCheck.TryGetProperty("url", out JsonElement jsonUrl) && (jsonUrl.ValueKind != JsonValueKind.Null))
- _url = new Uri(jsonUrl.GetString());
- else
- _url = null;
- if (jsonHealthCheck.TryGetProperty("emailAlert", out JsonElement jsonEmailAlert) && _service.EmailAlerts.TryGetValue(jsonEmailAlert.GetString(), out EmailAlert emailAlert))
- _emailAlert = emailAlert;
- else
- _emailAlert = null;
- if (jsonHealthCheck.TryGetProperty("webHook", out JsonElement jsonWebHook) && _service.WebHooks.TryGetValue(jsonWebHook.GetString(), out WebHook webHook))
- _webHook = webHook;
- else
- _webHook = null;
- ConditionalHttpReload();
- }
- public async Task<HealthCheckResponse> IsHealthyAsync(string domain, DnsResourceRecordType type, Uri healthCheckUrl)
- {
- 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 new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
- IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response);
- if (addresses.Count > 0)
- {
- HealthCheckResponse lastResponse = null;
- foreach (IPAddress address in addresses)
- {
- lastResponse = await IsHealthyAsync(address, healthCheckUrl);
- if (lastResponse.Status == HealthStatus.Healthy)
- return lastResponse;
- }
- return lastResponse;
- }
- return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
- }
- case DnsResourceRecordType.AAAA:
- {
- DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
- if ((response is null) || (response.Answer.Count == 0))
- return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
- IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response);
- if (addresses.Count > 0)
- {
- HealthCheckResponse lastResponse = null;
- foreach (IPAddress address in addresses)
- {
- lastResponse = await IsHealthyAsync(address, healthCheckUrl);
- if (lastResponse.Status == HealthStatus.Healthy)
- return lastResponse;
- }
- return lastResponse;
- }
- return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
- }
- default:
- return new HealthCheckResponse(HealthStatus.Failed, "Not supported.");
- }
- }
- public async Task<HealthCheckResponse> IsHealthyAsync(IPAddress address, Uri healthCheckUrl)
- {
- foreach (KeyValuePair<NetworkAddress, bool> network in _service.UnderMaintenance)
- {
- if (network.Key.Contains(address))
- {
- if (network.Value)
- return new HealthCheckResponse(HealthStatus.Maintenance);
- break;
- }
- }
- 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 new HealthCheckResponse(HealthStatus.Healthy);
- lastReason = reply.Status.ToString();
- }
- while (++retry < _retries);
- return new HealthCheckResponse(HealthStatus.Failed, lastReason);
- }
- }
- case HealthCheckType.Tcp:
- {
- Exception lastException;
- 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 TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
- {
- return socket.ConnectAsync(address, _port, cancellationToken1).AsTask();
- }, _timeout);
- }
- }
- else
- {
- using (Socket socket = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)
- {
- return proxy.ConnectAsync(new IPEndPoint(address, _port), cancellationToken1);
- }, _timeout))
- {
- //do nothing
- }
- }
- return new HealthCheckResponse(HealthStatus.Healthy);
- }
- catch (TimeoutException ex)
- {
- lastReason = "Connection timed out.";
- lastException = ex;
- }
- catch (SocketException ex)
- {
- lastReason = ex.Message;
- lastException = ex;
- }
- catch (Exception ex)
- {
- lastException = ex;
- }
- }
- while (++retry < _retries);
- return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException);
- }
- case HealthCheckType.Http:
- case HealthCheckType.Https:
- {
- ConditionalHttpReload();
- Exception lastException;
- string lastReason = null;
- int retry = 0;
- do
- {
- try
- {
- Uri url;
- if (_url is null)
- url = healthCheckUrl;
- else
- url = _url;
- if (url is null)
- return new HealthCheckResponse(HealthStatus.Failed, "Missing health check URL in APP record as well as in app config.");
- if (_type == HealthCheckType.Http)
- {
- if (url.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
- url = new Uri("http://" + url.Host + (url.IsDefaultPort ? "" : ":" + url.Port) + url.PathAndQuery);
- }
- else
- {
- if (url.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
- url = new Uri("https://" + url.Host + (url.IsDefaultPort ? "" : ":" + url.Port) + url.PathAndQuery);
- }
- 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 new HealthCheckResponse(HealthStatus.Healthy);
- return new HealthCheckResponse(HealthStatus.Failed, "Received HTTP status code: " + (int)httpResponse.StatusCode + " " + httpResponse.StatusCode.ToString() + "; URL: " + url.AbsoluteUri);
- }
- catch (TaskCanceledException ex)
- {
- lastReason = "Connection timed out.";
- lastException = ex;
- }
- catch (HttpRequestException ex)
- {
- lastReason = ex.Message;
- lastException = ex;
- }
- catch (Exception ex)
- {
- lastException = ex;
- }
- }
- while (++retry < _retries);
- return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException);
- }
- 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
- }
- }
|