BlockListZoneManager.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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 System;
  16. using System.Collections.Generic;
  17. using System.IO;
  18. using System.Net;
  19. using System.Net.Http;
  20. using System.Security.Cryptography;
  21. using System.Text;
  22. using System.Threading.Tasks;
  23. using TechnitiumLibrary.Net;
  24. using TechnitiumLibrary.Net.Dns;
  25. using TechnitiumLibrary.Net.Dns.EDnsOptions;
  26. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  27. using TechnitiumLibrary.Net.Http.Client;
  28. namespace DnsServerCore.Dns.ZoneManagers
  29. {
  30. public sealed class BlockListZoneManager
  31. {
  32. #region variables
  33. readonly static char[] _popWordSeperator = new char[] { ' ', '\t' };
  34. readonly DnsServer _dnsServer;
  35. readonly string _localCacheFolder;
  36. readonly List<Uri> _allowListUrls = new List<Uri>();
  37. readonly List<Uri> _blockListUrls = new List<Uri>();
  38. IReadOnlyDictionary<string, object> _allowListZone = new Dictionary<string, object>();
  39. IReadOnlyDictionary<string, List<Uri>> _blockListZone = new Dictionary<string, List<Uri>>();
  40. DnsSOARecordData _soaRecord;
  41. DnsNSRecordData _nsRecord;
  42. readonly IReadOnlyCollection<DnsARecordData> _aRecords = new DnsARecordData[] { new DnsARecordData(IPAddress.Any) };
  43. readonly IReadOnlyCollection<DnsAAAARecordData> _aaaaRecords = new DnsAAAARecordData[] { new DnsAAAARecordData(IPAddress.IPv6Any) };
  44. #endregion
  45. #region constructor
  46. public BlockListZoneManager(DnsServer dnsServer)
  47. {
  48. _dnsServer = dnsServer;
  49. _localCacheFolder = Path.Combine(_dnsServer.ConfigFolder, "blocklists");
  50. if (!Directory.Exists(_localCacheFolder))
  51. Directory.CreateDirectory(_localCacheFolder);
  52. UpdateServerDomain(_dnsServer.ServerDomain);
  53. }
  54. #endregion
  55. #region private
  56. private void UpdateServerDomain(string serverDomain)
  57. {
  58. _soaRecord = new DnsSOARecordData(serverDomain, "hostadmin@" + serverDomain, 1, 14400, 3600, 604800, 60);
  59. _nsRecord = new DnsNSRecordData(serverDomain);
  60. }
  61. private string GetBlockListFilePath(Uri blockListUrl)
  62. {
  63. using (HashAlgorithm hash = SHA256.Create())
  64. {
  65. return Path.Combine(_localCacheFolder, Convert.ToHexString(hash.ComputeHash(Encoding.UTF8.GetBytes(blockListUrl.AbsoluteUri))).ToLower());
  66. }
  67. }
  68. private static string PopWord(ref string line)
  69. {
  70. if (line.Length == 0)
  71. return line;
  72. line = line.TrimStart(_popWordSeperator);
  73. int i = line.IndexOfAny(_popWordSeperator);
  74. string word;
  75. if (i < 0)
  76. {
  77. word = line;
  78. line = "";
  79. }
  80. else
  81. {
  82. word = line.Substring(0, i);
  83. line = line.Substring(i + 1);
  84. }
  85. return word;
  86. }
  87. private Queue<string> ReadListFile(Uri listUrl, bool isAllowList, out Queue<string> exceptionDomains)
  88. {
  89. Queue<string> domains = new Queue<string>();
  90. exceptionDomains = new Queue<string>();
  91. try
  92. {
  93. _dnsServer.LogManager?.Write("DNS Server is reading " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri);
  94. using (FileStream fS = new FileStream(GetBlockListFilePath(listUrl), FileMode.Open, FileAccess.Read))
  95. {
  96. //parse hosts file and populate block zone
  97. StreamReader sR = new StreamReader(fS, true);
  98. char[] trimSeperator = new char[] { ' ', '\t', '*', '.' };
  99. string line;
  100. string firstWord;
  101. string secondWord;
  102. string hostname;
  103. string domain;
  104. string options;
  105. int i;
  106. while (true)
  107. {
  108. line = sR.ReadLine();
  109. if (line is null)
  110. break; //eof
  111. line = line.TrimStart(trimSeperator);
  112. if (line.Length == 0)
  113. continue; //skip empty line
  114. if (line.StartsWith('#') || line.StartsWith("!"))
  115. continue; //skip comment line
  116. if (line.StartsWith("||"))
  117. {
  118. //adblock format
  119. i = line.IndexOf('^');
  120. if (i > -1)
  121. {
  122. domain = line.Substring(2, i - 2);
  123. options = line.Substring(i + 1);
  124. if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain))
  125. domains.Enqueue(domain.ToLower());
  126. }
  127. else
  128. {
  129. domain = line.Substring(2);
  130. if (DnsClient.IsDomainNameValid(domain))
  131. domains.Enqueue(domain.ToLower());
  132. }
  133. }
  134. else if (line.StartsWith("@@||"))
  135. {
  136. //adblock format - exception syntax
  137. i = line.IndexOf('^');
  138. if (i > -1)
  139. {
  140. domain = line.Substring(4, i - 4);
  141. options = line.Substring(i + 1);
  142. if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain))
  143. exceptionDomains.Enqueue(domain.ToLower());
  144. }
  145. else
  146. {
  147. domain = line.Substring(4);
  148. if (DnsClient.IsDomainNameValid(domain))
  149. exceptionDomains.Enqueue(domain.ToLower());
  150. }
  151. }
  152. else
  153. {
  154. //hosts file format
  155. firstWord = PopWord(ref line);
  156. if (line.Length == 0)
  157. {
  158. hostname = firstWord;
  159. }
  160. else
  161. {
  162. secondWord = PopWord(ref line);
  163. if ((secondWord.Length == 0) || secondWord.StartsWith('#'))
  164. hostname = firstWord;
  165. else
  166. hostname = secondWord;
  167. }
  168. hostname = hostname.Trim('.').ToLower();
  169. switch (hostname)
  170. {
  171. case "":
  172. case "localhost":
  173. case "localhost.localdomain":
  174. case "local":
  175. case "broadcasthost":
  176. case "ip6-localhost":
  177. case "ip6-loopback":
  178. case "ip6-localnet":
  179. case "ip6-mcastprefix":
  180. case "ip6-allnodes":
  181. case "ip6-allrouters":
  182. case "ip6-allhosts":
  183. continue; //skip these hostnames
  184. }
  185. if (!DnsClient.IsDomainNameValid(hostname))
  186. continue;
  187. if (IPAddress.TryParse(hostname, out _))
  188. continue; //skip line when hostname is IP address
  189. domains.Enqueue(hostname);
  190. }
  191. }
  192. }
  193. _dnsServer.LogManager?.Write("DNS Server read " + (isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domains) from: " + listUrl.AbsoluteUri);
  194. }
  195. catch (Exception ex)
  196. {
  197. _dnsServer.LogManager?.Write("DNS Server failed to read " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString());
  198. }
  199. return domains;
  200. }
  201. private List<Uri> IsZoneBlocked(string domain, out string blockedDomain)
  202. {
  203. domain = domain.ToLower();
  204. do
  205. {
  206. if (_blockListZone.TryGetValue(domain, out List<Uri> blockLists))
  207. {
  208. //found zone blocked
  209. blockedDomain = domain;
  210. return blockLists;
  211. }
  212. domain = AuthZoneManager.GetParentZone(domain);
  213. }
  214. while (domain is not null);
  215. blockedDomain = null;
  216. return null;
  217. }
  218. private bool IsZoneAllowed(string domain)
  219. {
  220. domain = domain.ToLower();
  221. do
  222. {
  223. if (_allowListZone.TryGetValue(domain, out _))
  224. return true;
  225. domain = AuthZoneManager.GetParentZone(domain);
  226. }
  227. while (domain is not null);
  228. return false;
  229. }
  230. #endregion
  231. #region public
  232. public void LoadBlockLists()
  233. {
  234. Dictionary<Uri, Queue<string>> allowListQueues = new Dictionary<Uri, Queue<string>>(_allowListUrls.Count);
  235. Dictionary<Uri, Queue<string>> blockListQueues = new Dictionary<Uri, Queue<string>>(_blockListUrls.Count);
  236. int totalAllowedDomains = 0;
  237. int totalBlockedDomains = 0;
  238. //read all allow lists in a queue
  239. foreach (Uri allowListUrl in _allowListUrls)
  240. {
  241. if (!allowListQueues.ContainsKey(allowListUrl))
  242. {
  243. Queue<string> allowListQueue = ReadListFile(allowListUrl, true, out Queue<string> blockListQueue);
  244. totalAllowedDomains += allowListQueue.Count;
  245. allowListQueues.Add(allowListUrl, allowListQueue);
  246. totalBlockedDomains += blockListQueue.Count;
  247. blockListQueues.Add(allowListUrl, blockListQueue);
  248. }
  249. }
  250. //read all block lists in a queue
  251. foreach (Uri blockListUrl in _blockListUrls)
  252. {
  253. if (!blockListQueues.ContainsKey(blockListUrl))
  254. {
  255. Queue<string> blockListQueue = ReadListFile(blockListUrl, false, out Queue<string> allowListQueue);
  256. totalBlockedDomains += blockListQueue.Count;
  257. blockListQueues.Add(blockListUrl, blockListQueue);
  258. totalAllowedDomains += allowListQueue.Count;
  259. allowListQueues.Add(blockListUrl, allowListQueue);
  260. }
  261. }
  262. //load block list zone
  263. Dictionary<string, object> allowListZone = new Dictionary<string, object>(totalAllowedDomains);
  264. foreach (KeyValuePair<Uri, Queue<string>> allowListQueue in allowListQueues)
  265. {
  266. Queue<string> queue = allowListQueue.Value;
  267. while (queue.Count > 0)
  268. {
  269. string domain = queue.Dequeue();
  270. allowListZone.TryAdd(domain, null);
  271. }
  272. }
  273. Dictionary<string, List<Uri>> blockListZone = new Dictionary<string, List<Uri>>(totalBlockedDomains);
  274. foreach (KeyValuePair<Uri, Queue<string>> blockListQueue in blockListQueues)
  275. {
  276. Queue<string> queue = blockListQueue.Value;
  277. while (queue.Count > 0)
  278. {
  279. string domain = queue.Dequeue();
  280. if (!blockListZone.TryGetValue(domain, out List<Uri> blockLists))
  281. {
  282. blockLists = new List<Uri>(2);
  283. blockListZone.Add(domain, blockLists);
  284. }
  285. blockLists.Add(blockListQueue.Key);
  286. }
  287. }
  288. //set new allowed and blocked zones
  289. _allowListZone = allowListZone;
  290. _blockListZone = blockListZone;
  291. _dnsServer.LogManager?.Write("DNS Server block list zone was loaded successfully.");
  292. }
  293. public void Flush()
  294. {
  295. _allowListZone = new Dictionary<string, object>();
  296. _blockListZone = new Dictionary<string, List<Uri>>();
  297. }
  298. public async Task<bool> UpdateBlockListsAsync()
  299. {
  300. bool downloaded = false;
  301. bool notModified = false;
  302. async Task DownloadListUrlAsync(Uri listUrl, bool isAllowList)
  303. {
  304. string listFilePath = GetBlockListFilePath(listUrl);
  305. string listDownloadFilePath = listFilePath + ".downloading";
  306. try
  307. {
  308. if (File.Exists(listDownloadFilePath))
  309. File.Delete(listDownloadFilePath);
  310. SocketsHttpHandler handler = new SocketsHttpHandler();
  311. handler.Proxy = _dnsServer.Proxy;
  312. handler.UseProxy = _dnsServer.Proxy is not null;
  313. handler.AutomaticDecompression = DecompressionMethods.All;
  314. using (HttpClient http = new HttpClient(new HttpClientRetryHandler(handler)))
  315. {
  316. if (File.Exists(listFilePath))
  317. http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(listFilePath);
  318. HttpResponseMessage httpResponse = await http.GetAsync(listUrl);
  319. switch (httpResponse.StatusCode)
  320. {
  321. case HttpStatusCode.OK:
  322. {
  323. using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write))
  324. {
  325. using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync())
  326. {
  327. await httpStream.CopyToAsync(fS);
  328. }
  329. }
  330. if (File.Exists(listFilePath))
  331. File.Delete(listFilePath);
  332. File.Move(listDownloadFilePath, listFilePath);
  333. if (httpResponse.Content.Headers.LastModified != null)
  334. File.SetLastWriteTimeUtc(listFilePath, httpResponse.Content.Headers.LastModified.Value.UtcDateTime);
  335. downloaded = true;
  336. LogManager log = _dnsServer.LogManager;
  337. if (log != null)
  338. log.Write("DNS Server successfully downloaded " + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri);
  339. }
  340. break;
  341. case HttpStatusCode.NotModified:
  342. {
  343. notModified = true;
  344. LogManager log = _dnsServer.LogManager;
  345. if (log != null)
  346. log.Write("DNS Server successfully checked for a new update of the " + (isAllowList ? "allow" : "block") + " list: " + listUrl.AbsoluteUri);
  347. }
  348. break;
  349. default:
  350. throw new HttpRequestException((int)httpResponse.StatusCode + " " + httpResponse.ReasonPhrase);
  351. }
  352. }
  353. }
  354. catch (Exception ex)
  355. {
  356. LogManager log = _dnsServer.LogManager;
  357. if (log != null)
  358. log.Write("DNS Server failed to download " + (isAllowList ? "allow" : "block") + " list and will use previously downloaded file (if available): " + listUrl.AbsoluteUri + "\r\n" + ex.ToString());
  359. }
  360. }
  361. List<Task> tasks = new List<Task>();
  362. foreach (Uri allowListUrl in _allowListUrls)
  363. tasks.Add(DownloadListUrlAsync(allowListUrl, true));
  364. foreach (Uri blockListUrl in _blockListUrls)
  365. tasks.Add(DownloadListUrlAsync(blockListUrl, false));
  366. await Task.WhenAll(tasks);
  367. if (downloaded)
  368. {
  369. LoadBlockLists();
  370. //force GC collection to remove old zone data from memory quickly
  371. GC.Collect();
  372. }
  373. return downloaded || notModified;
  374. }
  375. public bool IsAllowed(DnsDatagram request)
  376. {
  377. if (_allowListZone.Count < 1)
  378. return false;
  379. return IsZoneAllowed(request.Question[0].Name);
  380. }
  381. public DnsDatagram Query(DnsDatagram request)
  382. {
  383. if (_blockListZone.Count < 1)
  384. return null;
  385. DnsQuestionRecord question = request.Question[0];
  386. List<Uri> blockLists = IsZoneBlocked(question.Name, out string blockedDomain);
  387. if (blockLists is null)
  388. return null; //zone not blocked
  389. //zone is blocked
  390. if (_dnsServer.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT))
  391. {
  392. //return meta data
  393. DnsResourceRecord[] answer = new DnsResourceRecord[blockLists.Count];
  394. for (int i = 0; i < answer.Length; i++)
  395. answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=block-list-zone; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain));
  396. return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer);
  397. }
  398. else
  399. {
  400. EDnsOption[] options = null;
  401. if (_dnsServer.AllowTxtBlockingReport && (request.EDNS is not null))
  402. {
  403. options = new EDnsOption[blockLists.Count];
  404. for (int i = 0; i < options.Length; i++)
  405. options[i] = new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=block-list-zone; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain));
  406. }
  407. IReadOnlyCollection<DnsARecordData> aRecords;
  408. IReadOnlyCollection<DnsAAAARecordData> aaaaRecords;
  409. switch (_dnsServer.BlockingType)
  410. {
  411. case DnsServerBlockingType.AnyAddress:
  412. aRecords = _aRecords;
  413. aaaaRecords = _aaaaRecords;
  414. break;
  415. case DnsServerBlockingType.CustomAddress:
  416. aRecords = _dnsServer.CustomBlockingARecords;
  417. aaaaRecords = _dnsServer.CustomBlockingAAAARecords;
  418. break;
  419. case DnsServerBlockingType.NxDomain:
  420. string parentDomain = AuthZoneManager.GetParentZone(blockedDomain);
  421. if (parentDomain is null)
  422. parentDomain = string.Empty;
  423. return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options);
  424. default:
  425. throw new InvalidOperationException();
  426. }
  427. IReadOnlyList<DnsResourceRecord> answer = null;
  428. IReadOnlyList<DnsResourceRecord> authority = null;
  429. switch (question.Type)
  430. {
  431. case DnsResourceRecordType.A:
  432. {
  433. List<DnsResourceRecord> rrList = new List<DnsResourceRecord>(aRecords.Count);
  434. foreach (DnsARecordData record in aRecords)
  435. rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 60, record));
  436. answer = rrList;
  437. }
  438. break;
  439. case DnsResourceRecordType.AAAA:
  440. {
  441. List<DnsResourceRecord> rrList = new List<DnsResourceRecord>(aaaaRecords.Count);
  442. foreach (DnsAAAARecordData record in aaaaRecords)
  443. rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 60, record));
  444. answer = rrList;
  445. }
  446. break;
  447. case DnsResourceRecordType.NS:
  448. if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase))
  449. answer = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, 60, _nsRecord) };
  450. else
  451. authority = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) };
  452. break;
  453. case DnsResourceRecordType.SOA:
  454. answer = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) };
  455. break;
  456. default:
  457. authority = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) };
  458. break;
  459. }
  460. return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options);
  461. }
  462. }
  463. #endregion
  464. #region properties
  465. public string ServerDomain
  466. {
  467. get { return _soaRecord.PrimaryNameServer; }
  468. set { UpdateServerDomain(value); }
  469. }
  470. public List<Uri> AllowListUrls
  471. { get { return _allowListUrls; } }
  472. public List<Uri> BlockListUrls
  473. { get { return _blockListUrls; } }
  474. public int TotalZonesAllowed
  475. { get { return _allowListZone.Count; } }
  476. public int TotalZonesBlocked
  477. { get { return _blockListZone.Count; } }
  478. #endregion
  479. }
  480. }