StubZone.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2023 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.Dns.ResourceRecords;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Threading;
  19. using System.Threading.Tasks;
  20. using TechnitiumLibrary;
  21. using TechnitiumLibrary.Net.Dns;
  22. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  23. namespace DnsServerCore.Dns.Zones
  24. {
  25. class StubZone : ApexZone
  26. {
  27. #region variables
  28. readonly DnsServer _dnsServer;
  29. readonly object _refreshTimerLock = new object();
  30. Timer _refreshTimer;
  31. bool _refreshTimerTriggered;
  32. const int REFRESH_TIMER_INTERVAL = 5000;
  33. const int REFRESH_TIMEOUT = 10000;
  34. const int REFRESH_RETRIES = 5;
  35. DateTime _expiry;
  36. bool _isExpired;
  37. bool _resync;
  38. #endregion
  39. #region constructor
  40. public StubZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)
  41. : base(zoneInfo)
  42. {
  43. _dnsServer = dnsServer;
  44. _expiry = zoneInfo.Expiry;
  45. _isExpired = DateTime.UtcNow > _expiry;
  46. _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
  47. }
  48. private StubZone(DnsServer dnsServer, string name)
  49. : base(name)
  50. {
  51. _dnsServer = dnsServer;
  52. _zoneTransfer = AuthZoneTransfer.Deny;
  53. _notify = AuthZoneNotify.None;
  54. _update = AuthZoneUpdate.Deny;
  55. }
  56. #endregion
  57. #region static
  58. public static async Task<StubZone> CreateAsync(DnsServer dnsServer, string name, string primaryNameServerAddresses = null)
  59. {
  60. StubZone stubZone = new StubZone(dnsServer, name);
  61. DnsQuestionRecord soaQuestion = new DnsQuestionRecord(name, DnsResourceRecordType.SOA, DnsClass.IN);
  62. DnsDatagram soaResponse;
  63. NameServerAddress[] primaryNameServers = null;
  64. if (string.IsNullOrEmpty(primaryNameServerAddresses))
  65. {
  66. soaResponse = await stubZone._dnsServer.DirectQueryAsync(soaQuestion);
  67. }
  68. else
  69. {
  70. primaryNameServers = primaryNameServerAddresses.Split(delegate (string address)
  71. {
  72. NameServerAddress nameServer = NameServerAddress.Parse(address);
  73. if (nameServer.Protocol != DnsTransportProtocol.Udp)
  74. nameServer = nameServer.ChangeProtocol(DnsTransportProtocol.Udp);
  75. return nameServer;
  76. }, ',');
  77. DnsClient dnsClient = new DnsClient(primaryNameServers);
  78. foreach (NameServerAddress nameServerAddress in dnsClient.Servers)
  79. {
  80. if (nameServerAddress.IsIPEndPointStale)
  81. await nameServerAddress.ResolveIPAddressAsync(stubZone._dnsServer, stubZone._dnsServer.PreferIPv6);
  82. }
  83. dnsClient.Proxy = stubZone._dnsServer.Proxy;
  84. dnsClient.PreferIPv6 = stubZone._dnsServer.PreferIPv6;
  85. DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { soaQuestion }, null, null, null, dnsServer.UdpPayloadSize);
  86. soaResponse = await dnsClient.ResolveAsync(soaRequest);
  87. }
  88. if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA))
  89. throw new DnsServerException("DNS Server failed to find SOA record for: " + name);
  90. DnsSOARecordData receivedSoa = soaResponse.Answer[0].RDATA as DnsSOARecordData;
  91. DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum);
  92. DnsResourceRecord[] soaRR = new DnsResourceRecord[] { new DnsResourceRecord(stubZone._name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Refresh, soa) };
  93. if (primaryNameServers is not null)
  94. soaRR[0].GetAuthRecordInfo().PrimaryNameServers = primaryNameServers;
  95. stubZone._entries[DnsResourceRecordType.SOA] = soaRR;
  96. stubZone._isExpired = true; //new stub zone is considered expired till it refreshes
  97. stubZone._refreshTimer = new Timer(stubZone.RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
  98. return stubZone;
  99. }
  100. #endregion
  101. #region IDisposable
  102. bool _disposed;
  103. protected override void Dispose(bool disposing)
  104. {
  105. try
  106. {
  107. if (_disposed)
  108. return;
  109. if (disposing)
  110. {
  111. lock (_refreshTimerLock)
  112. {
  113. if (_refreshTimer != null)
  114. {
  115. _refreshTimer.Dispose();
  116. _refreshTimer = null;
  117. }
  118. }
  119. }
  120. _disposed = true;
  121. }
  122. finally
  123. {
  124. base.Dispose(disposing);
  125. }
  126. }
  127. #endregion
  128. #region private
  129. private async void RefreshTimerCallback(object state)
  130. {
  131. try
  132. {
  133. if (_disabled && !_resync)
  134. return;
  135. _isExpired = DateTime.UtcNow > _expiry;
  136. //get primary name server addresses
  137. IReadOnlyList<NameServerAddress> primaryNameServers = await GetPrimaryNameServerAddressesAsync(_dnsServer);
  138. if (primaryNameServers.Count == 0)
  139. {
  140. LogManager log = _dnsServer.LogManager;
  141. if (log != null)
  142. log.Write("DNS Server could not find primary name server IP addresses for stub zone: " + (_name == "" ? "<root>" : _name));
  143. //set timer for retry
  144. DnsSOARecordData soa1 = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
  145. ResetRefreshTimer(soa1.Retry * 1000);
  146. _syncFailed = true;
  147. return;
  148. }
  149. //refresh zone
  150. if (await RefreshZoneAsync(primaryNameServers))
  151. {
  152. //zone refreshed; set timer for refresh
  153. DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
  154. ResetRefreshTimer(latestSoa.Refresh * 1000);
  155. _syncFailed = false;
  156. _expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire);
  157. _isExpired = false;
  158. _resync = false;
  159. _dnsServer.AuthZoneManager.SaveZoneFile(_name);
  160. return;
  161. }
  162. //no response from any of the name servers; set timer for retry
  163. DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
  164. ResetRefreshTimer(soa.Retry * 1000);
  165. _syncFailed = true;
  166. }
  167. catch (Exception ex)
  168. {
  169. LogManager log = _dnsServer.LogManager;
  170. if (log != null)
  171. log.Write(ex);
  172. //set timer for retry
  173. DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
  174. ResetRefreshTimer(soa.Retry * 1000);
  175. _syncFailed = true;
  176. }
  177. finally
  178. {
  179. _refreshTimerTriggered = false;
  180. }
  181. }
  182. private void ResetRefreshTimer(long dueTime)
  183. {
  184. lock (_refreshTimerLock)
  185. {
  186. if (_refreshTimer != null)
  187. _refreshTimer.Change(dueTime, Timeout.Infinite);
  188. }
  189. }
  190. private async Task<bool> RefreshZoneAsync(IReadOnlyList<NameServerAddress> nameServers)
  191. {
  192. try
  193. {
  194. {
  195. LogManager log = _dnsServer.LogManager;
  196. if (log != null)
  197. log.Write("DNS Server has started zone refresh for stub zone: " + (_name == "" ? "<root>" : _name));
  198. }
  199. DnsClient client = new DnsClient(nameServers);
  200. client.Proxy = _dnsServer.Proxy;
  201. client.PreferIPv6 = _dnsServer.PreferIPv6;
  202. client.Timeout = REFRESH_TIMEOUT;
  203. client.Retries = REFRESH_RETRIES;
  204. client.Concurrency = 1;
  205. DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN) }, null, null, null, _dnsServer.UdpPayloadSize);
  206. DnsDatagram soaResponse = await client.ResolveAsync(soaRequest);
  207. if (soaResponse.RCODE != DnsResponseCode.NoError)
  208. {
  209. LogManager log = _dnsServer.LogManager;
  210. if (log != null)
  211. log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "<root>" : _name) + "' stub zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
  212. return false;
  213. }
  214. if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase))
  215. {
  216. LogManager log = _dnsServer.LogManager;
  217. if (log != null)
  218. log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "<root>" : _name) + "' stub zone refresh from: " + soaResponse.Metadata.NameServer.ToString());
  219. return false;
  220. }
  221. DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];
  222. DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];
  223. DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;
  224. DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;
  225. //compare using sequence space arithmetic
  226. if (!_resync && !currentSoa.IsZoneUpdateAvailable(receivedSoa))
  227. {
  228. LogManager log = _dnsServer.LogManager;
  229. if (log != null)
  230. log.Write("DNS Server successfully checked for '" + (_name == "" ? "<root>" : _name) + "' stub zone update from: " + soaResponse.Metadata.NameServer.ToString());
  231. return true;
  232. }
  233. //update available; do zone sync with TCP transport
  234. List<NameServerAddress> tcpNameServers = new List<NameServerAddress>();
  235. foreach (NameServerAddress nameServer in nameServers)
  236. tcpNameServers.Add(nameServer.ChangeProtocol(DnsTransportProtocol.Tcp));
  237. client = new DnsClient(tcpNameServers);
  238. client.Proxy = _dnsServer.Proxy;
  239. client.PreferIPv6 = _dnsServer.PreferIPv6;
  240. client.Timeout = REFRESH_TIMEOUT;
  241. client.Retries = REFRESH_RETRIES;
  242. client.Concurrency = 1;
  243. DnsDatagram nsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.NS, DnsClass.IN) });
  244. DnsDatagram nsResponse = await client.ResolveAsync(nsRequest);
  245. if (nsResponse.RCODE != DnsResponseCode.NoError)
  246. {
  247. LogManager log = _dnsServer.LogManager;
  248. if (log != null)
  249. log.Write("DNS Server received RCODE=" + nsResponse.RCODE.ToString() + " for '" + (_name == "" ? "<root>" : _name) + "' stub zone refresh from: " + nsResponse.Metadata.NameServer.ToString());
  250. return false;
  251. }
  252. if (nsResponse.Answer.Count < 1)
  253. {
  254. LogManager log = _dnsServer.LogManager;
  255. if (log != null)
  256. log.Write("DNS Server received an empty response for NS query for '" + (_name == "" ? "<root>" : _name) + "' stub zone from: " + nsResponse.Metadata.NameServer.ToString());
  257. return false;
  258. }
  259. //prepare sync records
  260. List<DnsResourceRecord> nsRecords = new List<DnsResourceRecord>(nsResponse.Answer.Count);
  261. foreach (DnsResourceRecord record in nsResponse.Answer)
  262. {
  263. if ((record.Type == DnsResourceRecordType.NS) && record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase))
  264. {
  265. record.SyncGlueRecords(nsResponse.Additional);
  266. nsRecords.Add(record);
  267. }
  268. }
  269. receivedSoaRecord.CopyRecordInfoFrom(currentSoaRecord);
  270. //sync records
  271. _entries[DnsResourceRecordType.NS] = nsRecords;
  272. _entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { receivedSoaRecord };
  273. {
  274. LogManager log = _dnsServer.LogManager;
  275. if (log != null)
  276. log.Write("DNS Server successfully refreshed '" + (_name == "" ? "<root>" : _name) + "' stub zone from: " + nsResponse.Metadata.NameServer.ToString());
  277. }
  278. return true;
  279. }
  280. catch (Exception ex)
  281. {
  282. LogManager log = _dnsServer.LogManager;
  283. if (log != null)
  284. {
  285. string strNameServers = null;
  286. foreach (NameServerAddress nameServer in nameServers)
  287. {
  288. if (strNameServers == null)
  289. strNameServers = nameServer.ToString();
  290. else
  291. strNameServers += ", " + nameServer.ToString();
  292. }
  293. log.Write("DNS Server failed to refresh '" + (_name == "" ? "<root>" : _name) + "' stub zone from: " + strNameServers + "\r\n" + ex.ToString());
  294. }
  295. return false;
  296. }
  297. }
  298. #endregion
  299. #region public
  300. public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL)
  301. {
  302. if (_disabled)
  303. return;
  304. if (_refreshTimerTriggered)
  305. return;
  306. _refreshTimerTriggered = true;
  307. ResetRefreshTimer(refreshInterval);
  308. }
  309. public void TriggerResync()
  310. {
  311. if (_refreshTimerTriggered)
  312. return;
  313. _resync = true;
  314. _refreshTimerTriggered = true;
  315. ResetRefreshTimer(0);
  316. }
  317. public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)
  318. {
  319. switch (type)
  320. {
  321. case DnsResourceRecordType.SOA:
  322. if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))
  323. throw new InvalidOperationException("Invalid SOA record.");
  324. DnsResourceRecord existingSoaRecord = _entries[DnsResourceRecordType.SOA][0];
  325. DnsResourceRecord newSoaRecord = records[0];
  326. existingSoaRecord.CopyRecordInfoFrom(newSoaRecord);
  327. break;
  328. default:
  329. throw new InvalidOperationException("Cannot set records in stub zone.");
  330. }
  331. }
  332. public override void AddRecord(DnsResourceRecord record)
  333. {
  334. throw new InvalidOperationException("Cannot add record in stub zone.");
  335. }
  336. public override bool DeleteRecords(DnsResourceRecordType type)
  337. {
  338. throw new InvalidOperationException("Cannot delete record in stub zone.");
  339. }
  340. public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)
  341. {
  342. throw new InvalidOperationException("Cannot delete records in stub zone.");
  343. }
  344. public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)
  345. {
  346. throw new InvalidOperationException("Cannot update record in stub zone.");
  347. }
  348. public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)
  349. {
  350. return Array.Empty<DnsResourceRecord>(); //stub zone has no authority so cant return any records as query response to allow generating referral response
  351. }
  352. #endregion
  353. #region properties
  354. public DateTime Expiry
  355. { get { return _expiry; } }
  356. public bool IsExpired
  357. { get { return _isExpired; } }
  358. public override bool Disabled
  359. {
  360. get { return _disabled; }
  361. set
  362. {
  363. if (_disabled != value)
  364. {
  365. _disabled = value;
  366. if (_disabled)
  367. ResetRefreshTimer(Timeout.Infinite);
  368. else
  369. TriggerRefresh();
  370. }
  371. }
  372. }
  373. public override AuthZoneTransfer ZoneTransfer
  374. {
  375. get { return _zoneTransfer; }
  376. set { throw new InvalidOperationException(); }
  377. }
  378. public override AuthZoneNotify Notify
  379. {
  380. get { return _notify; }
  381. set { throw new InvalidOperationException(); }
  382. }
  383. public override AuthZoneUpdate Update
  384. {
  385. get { return _update; }
  386. set { throw new InvalidOperationException(); }
  387. }
  388. public override bool IsActive
  389. {
  390. get { return !_disabled && !_isExpired; }
  391. }
  392. #endregion
  393. }
  394. }