Address.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 MaxMind.GeoIP2.Model;
  17. using MaxMind.GeoIP2.Responses;
  18. using System;
  19. using System.Collections.Generic;
  20. using System.Net;
  21. using System.Net.Sockets;
  22. using System.Text.Json;
  23. using System.Threading.Tasks;
  24. using TechnitiumLibrary;
  25. using TechnitiumLibrary.Net.Dns;
  26. using TechnitiumLibrary.Net.Dns.EDnsOptions;
  27. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  28. namespace GeoDistance
  29. {
  30. public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler
  31. {
  32. #region variables
  33. IDnsServer _dnsServer;
  34. MaxMind _maxMind;
  35. #endregion
  36. #region IDisposable
  37. bool _disposed;
  38. private void Dispose(bool disposing)
  39. {
  40. if (_disposed)
  41. return;
  42. if (disposing)
  43. {
  44. if (_maxMind is not null)
  45. _maxMind.Dispose();
  46. }
  47. _disposed = true;
  48. }
  49. public void Dispose()
  50. {
  51. Dispose(true);
  52. }
  53. #endregion
  54. #region private
  55. private static double GetDistance(double lat1, double long1, double lat2, double long2)
  56. {
  57. double d1 = lat1 * (Math.PI / 180.0);
  58. double num1 = long1 * (Math.PI / 180.0);
  59. double d2 = lat2 * (Math.PI / 180.0);
  60. double num2 = long2 * (Math.PI / 180.0) - num1;
  61. double d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0);
  62. return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
  63. }
  64. #endregion
  65. #region public
  66. public Task InitializeAsync(IDnsServer dnsServer, string config)
  67. {
  68. _dnsServer = dnsServer;
  69. _maxMind = MaxMind.Create(dnsServer);
  70. return Task.CompletedTask;
  71. }
  72. public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)
  73. {
  74. DnsQuestionRecord question = request.Question[0];
  75. if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))
  76. return Task.FromResult<DnsDatagram>(null);
  77. switch (question.Type)
  78. {
  79. case DnsResourceRecordType.A:
  80. case DnsResourceRecordType.AAAA:
  81. Location location = null;
  82. byte scopePrefixLength = 0;
  83. EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();
  84. if (requestECS is not null)
  85. {
  86. if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))
  87. scopePrefixLength = (byte)csIsp.Network.PrefixLength;
  88. else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))
  89. scopePrefixLength = (byte)csAsn.Network.PrefixLength;
  90. else
  91. scopePrefixLength = requestECS.SourcePrefixLength;
  92. if (_maxMind.CityReader.TryCity(requestECS.Address, out CityResponse csResponse) && csResponse.Location.HasCoordinates)
  93. location = csResponse.Location;
  94. }
  95. if ((location is null) && _maxMind.CityReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates)
  96. location = response.Location;
  97. using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))
  98. {
  99. JsonElement jsonAppRecordData = jsonDocument.RootElement;
  100. JsonElement jsonClosestServer = default;
  101. if (location is null)
  102. {
  103. if (jsonAppRecordData.GetArrayLength() > 0)
  104. jsonClosestServer = jsonAppRecordData[0];
  105. }
  106. else
  107. {
  108. double lastDistance = double.MaxValue;
  109. foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray())
  110. {
  111. double lat = Convert.ToDouble(jsonServer.GetProperty("lat").GetString());
  112. double @long = Convert.ToDouble(jsonServer.GetProperty("long").GetString());
  113. double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value);
  114. if (distance < lastDistance)
  115. {
  116. lastDistance = distance;
  117. jsonClosestServer = jsonServer;
  118. }
  119. }
  120. }
  121. if (jsonClosestServer.ValueKind == JsonValueKind.Undefined)
  122. return Task.FromResult<DnsDatagram>(null);
  123. List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
  124. switch (question.Type)
  125. {
  126. case DnsResourceRecordType.A:
  127. foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty("addresses").EnumerateArray())
  128. {
  129. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  130. if (address.AddressFamily == AddressFamily.InterNetwork)
  131. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)));
  132. }
  133. break;
  134. case DnsResourceRecordType.AAAA:
  135. foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty("addresses").EnumerateArray())
  136. {
  137. IPAddress address = IPAddress.Parse(jsonAddress.GetString());
  138. if (address.AddressFamily == AddressFamily.InterNetworkV6)
  139. answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)));
  140. }
  141. break;
  142. }
  143. if (answers.Count == 0)
  144. return Task.FromResult<DnsDatagram>(null);
  145. if (answers.Count > 1)
  146. answers.Shuffle();
  147. EDnsOption[] options = null;
  148. if (requestECS is not null)
  149. options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);
  150. return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options));
  151. }
  152. default:
  153. return Task.FromResult<DnsDatagram>(null);
  154. }
  155. }
  156. #endregion
  157. #region properties
  158. public string Description
  159. { get { return "Returns A or AAAA records of the server located geographically closest to the client using MaxMind GeoIP2 City database. Use the geographic coordinates in decimal degrees (DD) form for the city the server is located in."; } }
  160. public string ApplicationRecordDataTemplate
  161. {
  162. get
  163. {
  164. return @"[
  165. {
  166. ""name"": ""server1-mumbai"",
  167. ""lat"": ""19.07283"",
  168. ""long"": ""72.88261"",
  169. ""addresses"": [
  170. ""1.1.1.1""
  171. ]
  172. },
  173. {
  174. ""name"": ""server2-london"",
  175. ""lat"": ""51.50853"",
  176. ""long"": ""-0.12574"",
  177. ""addresses"": [
  178. ""2.2.2.2""
  179. ]
  180. }
  181. ]";
  182. }
  183. }
  184. #endregion
  185. }
  186. }