From cb2b0ef72c2932c11a481e4470a0289d8f0aa574 Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sat, 18 Sep 2021 11:50:02 +0530 Subject: [PATCH] Refactored block list groups app to advance blocking app. Moved regex block list app code into this app. --- .../AdvanceBlockingApp.csproj} | 6 +- Apps/AdvanceBlockingApp/App.cs | 1466 +++++++++++++++++ Apps/AdvanceBlockingApp/dnsApp.config | 49 + Apps/BlockListGroupsApp/App.cs | 626 ------- Apps/BlockListGroupsApp/dnsApp.config | 24 - 5 files changed, 1518 insertions(+), 653 deletions(-) rename Apps/{BlockListGroupsApp/BlockListGroupsApp.csproj => AdvanceBlockingApp/AdvanceBlockingApp.csproj} (84%) create mode 100644 Apps/AdvanceBlockingApp/App.cs create mode 100644 Apps/AdvanceBlockingApp/dnsApp.config delete mode 100644 Apps/BlockListGroupsApp/App.cs delete mode 100644 Apps/BlockListGroupsApp/dnsApp.config diff --git a/Apps/BlockListGroupsApp/BlockListGroupsApp.csproj b/Apps/AdvanceBlockingApp/AdvanceBlockingApp.csproj similarity index 84% rename from Apps/BlockListGroupsApp/BlockListGroupsApp.csproj rename to Apps/AdvanceBlockingApp/AdvanceBlockingApp.csproj index 3d4afc9b..900afaa1 100644 --- a/Apps/BlockListGroupsApp/BlockListGroupsApp.csproj +++ b/Apps/AdvanceBlockingApp/AdvanceBlockingApp.csproj @@ -7,8 +7,8 @@ Technitium Technitium DNS Server Shreyas Zare - BlockListGroupsApp - BlockListGroups + AdvanceBlockingApp + AdvanceBlocking https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer false @@ -20,7 +20,7 @@ - + false diff --git a/Apps/AdvanceBlockingApp/App.cs b/Apps/AdvanceBlockingApp/App.cs new file mode 100644 index 00000000..8db1a371 --- /dev/null +++ b/Apps/AdvanceBlockingApp/App.cs @@ -0,0 +1,1466 @@ +/* +Technitium DNS Server +Copyright (C) 2021 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 . + +*/ + +using DnsServerCore.ApplicationCommon; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace AdvanceBlocking +{ + public sealed class App : IDnsAuthoritativeRequestHandler + { + #region variables + + IDnsServer _dnsServer; + string _localCacheFolder; + + bool _enableBlocking; + bool _blockAsNxDomain; + int _blockListUrlUpdateIntervalHours; + + IReadOnlyCollection _aRecords; + IReadOnlyCollection _aaaaRecords; + DnsSOARecord _soaRecord; + DnsNSRecord _nsRecord; + + IReadOnlyDictionary _networkGroupMap; + IReadOnlyDictionary _groups; + + Timer _blockListUrlUpdateTimer; + DateTime _blockListUrlLastUpdatedOn; + const int BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL = 5000; + const int BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL = 900000; + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_blockListUrlUpdateTimer is not null) + { + _blockListUrlUpdateTimer.Dispose(); + _blockListUrlUpdateTimer = null; + } + } + + #endregion + + #region private + + private async void BlockListUrlUpdateTimerCallbackAsync(object state) + { + try + { + if (DateTime.UtcNow > _blockListUrlLastUpdatedOn.AddHours(_blockListUrlUpdateIntervalHours)) + { + if (await UpdateBlockListsAsync()) + { + //block lists were updated + //save last updated on time + _blockListUrlLastUpdatedOn = DateTime.UtcNow; + } + } + } + catch (Exception ex) + { + _dnsServer.WriteLog(ex); + } + } + + private string GetBlockListFilePath(Uri blockListUrl) + { + using (HashAlgorithm hash = SHA256.Create()) + { + return Path.Combine(_localCacheFolder, BitConverter.ToString(hash.ComputeHash(Encoding.UTF8.GetBytes(blockListUrl.AbsoluteUri))).Replace("-", "").ToLower()); + } + } + + private async Task UpdateBlockListsAsync() + { + List downloadedAllowListUrls = new List(); + List downloadedBlockListUrls = new List(); + List downloadedRegexAllowListUrls = new List(); + List downloadedRegexBlockListUrls = new List(); + bool notModified = false; + + async Task DownloadListUrlAsync(Uri listUrl, bool isAllowList, bool isRegexList) + { + string listFilePath = GetBlockListFilePath(listUrl); + string listDownloadFilePath = listFilePath + ".downloading"; + + try + { + if (File.Exists(listDownloadFilePath)) + File.Delete(listDownloadFilePath); + + SocketsHttpHandler handler = new SocketsHttpHandler(); + handler.Proxy = _dnsServer.Proxy; + handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + + using (HttpClient http = new HttpClient(handler)) + { + if (File.Exists(listFilePath)) + http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(listFilePath); + + HttpResponseMessage httpResponse = await http.GetAsync(listUrl); + switch (httpResponse.StatusCode) + { + case HttpStatusCode.OK: + { + using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write)) + { + using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync()) + { + await httpStream.CopyToAsync(fS); + } + } + + if (File.Exists(listFilePath)) + File.Delete(listFilePath); + + File.Move(listDownloadFilePath, listFilePath); + + if (httpResponse.Content.Headers.LastModified != null) + File.SetLastWriteTimeUtc(listFilePath, httpResponse.Content.Headers.LastModified.Value.UtcDateTime); + + if (isAllowList) + { + if (isRegexList) + { + lock (downloadedRegexAllowListUrls) + { + downloadedRegexAllowListUrls.Add(listUrl); + } + } + else + { + lock (downloadedAllowListUrls) + { + downloadedAllowListUrls.Add(listUrl); + } + } + } + else + { + if (isRegexList) + { + lock (downloadedRegexBlockListUrls) + { + downloadedRegexBlockListUrls.Add(listUrl); + } + } + else + { + lock (downloadedBlockListUrls) + { + downloadedBlockListUrls.Add(listUrl); + } + } + } + + _dnsServer.WriteLog("Advance Blocking app successfully downloaded " + (isRegexList ? "regex " : "") + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri); + } + break; + + case HttpStatusCode.NotModified: + { + notModified = true; + + _dnsServer.WriteLog("Advance Blocking app successfully checked for a new update of the " + (isRegexList ? "regex " : "") + (isAllowList ? "allow" : "block") + " list: " + listUrl.AbsoluteUri); + } + break; + + default: + throw new HttpRequestException((int)httpResponse.StatusCode + " " + httpResponse.ReasonPhrase); + } + } + } + catch (Exception ex) + { + _dnsServer.WriteLog("Advance Blocking app failed to download " + (isRegexList ? "regex " : "") + (isAllowList ? "allow" : "block") + " list and will use previously downloaded file (if available): " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); + } + } + + List tasks = new List(); + IReadOnlyList uniqueAllowListUrls = GetUniqueAllowListUrls(); + IReadOnlyList uniqueBlockListUrls = GetUniqueBlockListUrls(); + IReadOnlyList uniqueRegexAllowListUrls = GetUniqueRegexAllowListUrls(); + IReadOnlyList uniqueRegexBlockListUrls = GetUniqueRegexBlockListUrls(); + + foreach (Uri allowListUrl in uniqueAllowListUrls) + tasks.Add(DownloadListUrlAsync(allowListUrl, true, false)); + + foreach (Uri blockListUrl in uniqueBlockListUrls) + tasks.Add(DownloadListUrlAsync(blockListUrl, false, false)); + + foreach (Uri regexAllowListUrl in uniqueRegexAllowListUrls) + tasks.Add(DownloadListUrlAsync(regexAllowListUrl, true, true)); + + foreach (Uri regexBlockListUrl in uniqueRegexBlockListUrls) + tasks.Add(DownloadListUrlAsync(regexBlockListUrl, false, true)); + + await Task.WhenAll(tasks); + + if ((downloadedAllowListUrls.Count > 0) || (downloadedBlockListUrls.Count > 0)) + LoadBlockListZones(downloadedAllowListUrls, downloadedBlockListUrls); + + if ((downloadedRegexAllowListUrls.Count > 0) || (downloadedRegexBlockListUrls.Count > 0)) + LoadRegexBlockListZones(downloadedRegexAllowListUrls, downloadedRegexBlockListUrls); + + return (downloadedAllowListUrls.Count > 0) || (downloadedBlockListUrls.Count > 0) || (downloadedRegexAllowListUrls.Count > 0) || (downloadedRegexBlockListUrls.Count > 0) || notModified; + } + + private static string PopWord(ref string line) + { + if (line.Length == 0) + return line; + + line = line.TrimStart(' ', '\t'); + + int i = line.IndexOfAny(new char[] { ' ', '\t' }); + string word; + + if (i < 0) + { + word = line; + line = ""; + } + else + { + word = line.Substring(0, i); + line = line.Substring(i + 1); + } + + return word; + } + + private Queue ReadListFile(Uri listUrl, bool isAllowList) + { + Queue domains = new Queue(); + + try + { + _dnsServer.WriteLog("Advance Blocking app is reading " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri); + + using (FileStream fS = new FileStream(GetBlockListFilePath(listUrl), FileMode.Open, FileAccess.Read)) + { + //parse hosts file and populate block zone + StreamReader sR = new StreamReader(fS, true); + string line; + string firstWord; + string secondWord; + string hostname; + + while (true) + { + line = sR.ReadLine(); + if (line == null) + break; //eof + + line = line.TrimStart(' ', '\t'); + + if (line.Length == 0) + continue; //skip empty line + + if (line.StartsWith("#")) + continue; //skip comment line + + firstWord = PopWord(ref line); + + if (line.Length == 0) + { + hostname = firstWord; + } + else + { + secondWord = PopWord(ref line); + + if (secondWord.Length == 0) + hostname = firstWord; + else + hostname = secondWord; + } + + hostname = hostname.Trim('.').ToLower(); + + switch (hostname) + { + case "": + case "localhost": + case "localhost.localdomain": + case "local": + case "broadcasthost": + case "ip6-localhost": + case "ip6-loopback": + case "ip6-localnet": + case "ip6-mcastprefix": + case "ip6-allnodes": + case "ip6-allrouters": + case "ip6-allhosts": + continue; //skip these hostnames + } + + if (!DnsClient.IsDomainNameValid(hostname)) + continue; + + if (IPAddress.TryParse(hostname, out _)) + continue; //skip line when hostname is IP address + + domains.Enqueue(hostname); + } + } + + _dnsServer.WriteLog("Advance Blocking app read " + (isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domains) from: " + listUrl.AbsoluteUri); + } + catch (Exception ex) + { + _dnsServer.WriteLog("Advance Blocking app failed to read " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); + } + + return domains; + } + + private Queue ReadRegexListFile(Uri listUrl, bool isAllowList) + { + Queue regices = new Queue(); + + try + { + _dnsServer.WriteLog("Advance Blocking app is reading regex " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri); + + using (FileStream fS = new FileStream(GetBlockListFilePath(listUrl), FileMode.Open, FileAccess.Read)) + { + //parse hosts file and populate block zone + StreamReader sR = new StreamReader(fS, true); + string line; + + while (true) + { + line = sR.ReadLine(); + if (line == null) + break; //eof + + line = line.TrimStart(' ', '\t'); + + if (line.Length == 0) + continue; //skip empty line + + if (line.StartsWith("#")) + continue; //skip comment line + + regices.Enqueue(line); + } + } + + _dnsServer.WriteLog("Advance Blocking app read regex " + (isAllowList ? "allow" : "block") + " list file (" + regices.Count + " regex patterns) from: " + listUrl.AbsoluteUri); + } + catch (Exception ex) + { + _dnsServer.WriteLog("Advance Blocking app failed to read regex " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); + } + + return regices; + } + + private IReadOnlyList GetUniqueAllowListUrls() + { + List allowListUrls = new List(); + + foreach (KeyValuePair group in _groups) + { + foreach (Uri allowListUrl in group.Value.AllowListUrls) + { + if (!allowListUrls.Contains(allowListUrl)) + allowListUrls.Add(allowListUrl); + } + } + + return allowListUrls; + } + + private IReadOnlyList GetUniqueBlockListUrls() + { + List blockListUrls = new List(); + + foreach (KeyValuePair group in _groups) + { + foreach (Uri blockListUrl in group.Value.BlockListUrls) + { + if (!blockListUrls.Contains(blockListUrl)) + blockListUrls.Add(blockListUrl); + } + } + + return blockListUrls; + } + + private IReadOnlyList GetUniqueRegexAllowListUrls() + { + List regexAllowListUrls = new List(); + + foreach (KeyValuePair group in _groups) + { + foreach (Uri regexAllowListUrl in group.Value.RegexAllowListUrls) + { + if (!regexAllowListUrls.Contains(regexAllowListUrl)) + regexAllowListUrls.Add(regexAllowListUrl); + } + } + + return regexAllowListUrls; + } + + private IReadOnlyList GetUniqueRegexBlockListUrls() + { + List regexBlockListUrls = new List(); + + foreach (KeyValuePair group in _groups) + { + foreach (Uri regexBlockListUrl in group.Value.RegexBlockListUrls) + { + if (!regexBlockListUrls.Contains(regexBlockListUrl)) + regexBlockListUrls.Add(regexBlockListUrl); + } + } + + return regexBlockListUrls; + } + + private static IReadOnlyList GetUniqueAllowListUrls(IReadOnlyList groups) + { + List allowListUrls = new List(); + + foreach (Group group in groups) + { + foreach (Uri blockListUrl in group.AllowListUrls) + { + if (!allowListUrls.Contains(blockListUrl)) + allowListUrls.Add(blockListUrl); + } + } + + return allowListUrls; + } + + private static IReadOnlyList GetUniqueBlockListUrls(IReadOnlyList groups) + { + List blockListUrls = new List(); + + foreach (Group group in groups) + { + foreach (Uri blockListUrl in group.BlockListUrls) + { + if (!blockListUrls.Contains(blockListUrl)) + blockListUrls.Add(blockListUrl); + } + } + + return blockListUrls; + } + + private static IReadOnlyList GetUniqueRegexAllowListUrls(IReadOnlyList groups) + { + List regexAllowListUrls = new List(); + + foreach (Group group in groups) + { + foreach (Uri regexAllowListUrl in group.RegexAllowListUrls) + { + if (!regexAllowListUrls.Contains(regexAllowListUrl)) + regexAllowListUrls.Add(regexAllowListUrl); + } + } + + return regexAllowListUrls; + } + + private static IReadOnlyList GetUniqueRegexBlockListUrls(IReadOnlyList groups) + { + List regexBlockListUrls = new List(); + + foreach (Group group in groups) + { + foreach (Uri regexBlockListUrl in group.RegexBlockListUrls) + { + if (!regexBlockListUrls.Contains(regexBlockListUrl)) + regexBlockListUrls.Add(regexBlockListUrl); + } + } + + return regexBlockListUrls; + } + + private IReadOnlyList GetUpdatedGroups(List updatedAllowListUrls, List updatedBlockListUrls) + { + List updatedGroups = new List(); + + foreach (KeyValuePair group in _groups) + { + bool found = false; + + foreach (Uri allowListUrl in group.Value.AllowListUrls) + { + if (updatedAllowListUrls.Contains(allowListUrl)) + { + updatedGroups.Add(group.Value); + found = true; + break; + } + } + + if (found) + continue; + + foreach (Uri blockListUrl in group.Value.BlockListUrls) + { + if (updatedBlockListUrls.Contains(blockListUrl)) + { + updatedGroups.Add(group.Value); + break; + } + } + } + + return updatedGroups; + } + + private IReadOnlyList GetRegexUpdatedGroups(List updatedRegexAllowListUrls, List updatedRegexBlockListUrls) + { + List updatedGroups = new List(); + + foreach (KeyValuePair group in _groups) + { + bool found = false; + + foreach (Uri regexAllowListUrl in group.Value.RegexAllowListUrls) + { + if (updatedRegexAllowListUrls.Contains(regexAllowListUrl)) + { + updatedGroups.Add(group.Value); + found = true; + break; + } + } + + if (found) + continue; + + foreach (Uri regexBlockListUrl in group.Value.RegexBlockListUrls) + { + if (updatedRegexBlockListUrls.Contains(regexBlockListUrl)) + { + updatedGroups.Add(group.Value); + break; + } + } + } + + return updatedGroups; + } + + private void LoadBlockListZones(List updatedAllowListUrls, List updatedBlockListUrls) + { + LoadBlockListZones(GetUpdatedGroups(updatedAllowListUrls, updatedBlockListUrls)); + } + + private void LoadRegexBlockListZones(List updatedRegexAllowListUrls, List updatedRegexBlockListUrls) + { + LoadRegexBlockListZones(GetRegexUpdatedGroups(updatedRegexAllowListUrls, updatedRegexBlockListUrls)); + } + + private void LoadBlockListZones(IReadOnlyList updatedGroups) + { + //read all allow lists in a queue + IReadOnlyList uniqueAllowListUrls = GetUniqueAllowListUrls(updatedGroups); + Dictionary> allAllowListQueues = new Dictionary>(uniqueAllowListUrls.Count); + + foreach (Uri allowListUrl in uniqueAllowListUrls) + { + if (!allAllowListQueues.ContainsKey(allowListUrl)) + { + Queue allowListQueue = ReadListFile(allowListUrl, true); + allAllowListQueues.Add(allowListUrl, allowListQueue); + } + } + + //read all block lists in a queue + IReadOnlyList uniqueBlockListUrls = GetUniqueBlockListUrls(updatedGroups); + Dictionary> allBlockListQueues = new Dictionary>(uniqueBlockListUrls.Count); + + foreach (Uri blockListUrl in uniqueBlockListUrls) + { + if (!allBlockListQueues.ContainsKey(blockListUrl)) + { + Queue blockListQueue = ReadListFile(blockListUrl, false); + allBlockListQueues.Add(blockListUrl, blockListQueue); + } + } + + //load block list zone per group + foreach (Group group in updatedGroups) + group.LoadBlockListZone(allAllowListQueues, allBlockListQueues); + + _dnsServer.WriteLog("Advance Blocking app loaded all block list zones successfully."); + + //force GC collection to remove old zone data from memory quickly + GC.Collect(); + } + + private void LoadRegexBlockListZones(IReadOnlyList updatedGroups) + { + //read all allow lists in a queue + IReadOnlyList uniqueRegexAllowListUrls = GetUniqueRegexAllowListUrls(updatedGroups); + Dictionary> allRegexAllowListQueues = new Dictionary>(uniqueRegexAllowListUrls.Count); + + foreach (Uri regexAllowListUrl in uniqueRegexAllowListUrls) + { + if (!allRegexAllowListQueues.ContainsKey(regexAllowListUrl)) + { + Queue regexAllowListQueue = ReadRegexListFile(regexAllowListUrl, true); + allRegexAllowListQueues.Add(regexAllowListUrl, regexAllowListQueue); + } + } + + //read all regex block lists in a queue + IReadOnlyList uniqueRegexBlockListUrls = GetUniqueRegexBlockListUrls(updatedGroups); + Dictionary> allRegexBlockListQueues = new Dictionary>(uniqueRegexBlockListUrls.Count); + + foreach (Uri regexBlockListUrl in uniqueRegexBlockListUrls) + { + if (!allRegexBlockListQueues.ContainsKey(regexBlockListUrl)) + { + Queue regexBlockListQueue = ReadRegexListFile(regexBlockListUrl, false); + allRegexBlockListQueues.Add(regexBlockListUrl, regexBlockListQueue); + } + } + + //load regex block list zone per group + foreach (Group group in updatedGroups) + group.LoadRegexBlockListZone(allRegexAllowListQueues, allRegexBlockListQueues); + + _dnsServer.WriteLog("Advance Blocking app loaded all regex block list zones successfully."); + + //force GC collection to remove old zone data from memory quickly + GC.Collect(); + } + + private static bool ListsEquals(IReadOnlyList list1, IReadOnlyList list2) + { + if (list1.Count != list2.Count) + return false; + + foreach (T item in list1) + { + if (!list2.Contains(item)) + return false; + } + + return true; + } + + private static string GetParentZone(string domain) + { + int i = domain.IndexOf('.'); + if (i > -1) + return domain.Substring(i + 1); + + //dont return root zone + return null; + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + _dnsServer = dnsServer; + _localCacheFolder = Path.Combine(_dnsServer.ApplicationFolder, "blocklists"); + + Directory.CreateDirectory(_localCacheFolder); + + dynamic jsonConfig = JsonConvert.DeserializeObject(config); + + _enableBlocking = jsonConfig.enableBlocking.Value; + _blockAsNxDomain = jsonConfig.blockAsNxDomain.Value; + _blockListUrlUpdateIntervalHours = Convert.ToInt32(jsonConfig.blockListUrlUpdateIntervalHours.Value); + + { + List aRecords = new List(); + List aaaaRecords = new List(); + + foreach (dynamic jsonBlockingAddress in jsonConfig.blockingAddresses) + { + string strAddress = jsonBlockingAddress.Value; + + if (IPAddress.TryParse(strAddress, out IPAddress address)) + { + switch (address.AddressFamily) + { + case AddressFamily.InterNetwork: + aRecords.Add(new DnsARecord(address)); + break; + + case AddressFamily.InterNetworkV6: + aaaaRecords.Add(new DnsAAAARecord(address)); + break; + } + } + } + + _aRecords = aRecords; + _aaaaRecords = aaaaRecords; + _soaRecord = new DnsSOARecord(dnsServer.ServerDomain, "hostadmin." + dnsServer.ServerDomain, 1, 14400, 3600, 604800, 60); + _nsRecord = new DnsNSRecord(dnsServer.ServerDomain); + } + + { + Dictionary networkGroupMap = new Dictionary(); + + foreach (dynamic jsonProperty in jsonConfig.networkGroupMap) + { + string network = jsonProperty.Name; + string group = jsonProperty.Value; + + if (NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) + networkGroupMap.Add(networkAddress, group); + } + + _networkGroupMap = networkGroupMap; + } + + { + List updatedGroups = new List(); + List updatedRegexGroups = new List(); + Dictionary groups = new Dictionary(); + + foreach (dynamic jsonGroup in jsonConfig.groups) + { + Group group = new Group(jsonGroup); + + if ((_groups is not null) && _groups.TryGetValue(group.Name, out Group existingGroup)) + { + if (!ListsEquals(group.AllowListUrls, existingGroup.AllowListUrls) || !ListsEquals(group.BlockListUrls, existingGroup.BlockListUrls)) + updatedGroups.Add(existingGroup); + + if (!ListsEquals(group.RegexAllowListUrls, existingGroup.RegexAllowListUrls) || !ListsEquals(group.RegexBlockListUrls, existingGroup.RegexBlockListUrls)) + updatedRegexGroups.Add(existingGroup); + + existingGroup.Enabled = group.Enabled; + + existingGroup.Allowed = group.Allowed; + existingGroup.Blocked = group.Blocked; + existingGroup.AllowListUrls = group.AllowListUrls; + existingGroup.BlockListUrls = group.BlockListUrls; + + existingGroup.AllowedRegex = group.AllowedRegex; + existingGroup.BlockedRegex = group.BlockedRegex; + existingGroup.RegexAllowListUrls = group.RegexAllowListUrls; + existingGroup.RegexBlockListUrls = group.RegexBlockListUrls; + + groups.TryAdd(existingGroup.Name, existingGroup); + } + else + { + updatedGroups.Add(group); + updatedRegexGroups.Add(group); + groups.TryAdd(group.Name, group); + } + } + + _groups = groups; + + if (updatedGroups.Count > 0) + { + Task.Run(delegate () + { + LoadBlockListZones(updatedGroups); + }); + } + + if (updatedRegexGroups.Count > 0) + { + Task.Run(delegate () + { + LoadRegexBlockListZones(updatedRegexGroups); + }); + } + } + + if (_blockListUrlUpdateTimer is null) + { + _blockListUrlUpdateTimer = new Timer(BlockListUrlUpdateTimerCallbackAsync, null, Timeout.Infinite, Timeout.Infinite); + _blockListUrlUpdateTimer.Change(BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL); + } + + return Task.CompletedTask; + } + + public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) + { + if (!_enableBlocking) + return Task.FromResult(null); + + IPAddress remoteIP = remoteEP.Address; + string groupName = null; + + foreach (KeyValuePair entry in _networkGroupMap) + { + if (entry.Key.Contains(remoteIP)) + { + groupName = entry.Value; + break; + } + } + + if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled) + return Task.FromResult(null); + + DnsQuestionRecord question = request.Question[0]; + + IReadOnlyList blockListUrls = group.IsZoneBlocked(question.Name, out string blockedDomain, out string blockedRegex); + if (blockListUrls is null) + return Task.FromResult(null); + + if (question.Type == DnsResourceRecordType.TXT) + { + //return meta data + DnsResourceRecord[] answer; + + if (blockedRegex is null) + { + if (blockListUrls.Count > 0) + { + answer = new DnsResourceRecord[blockListUrls.Count]; + + for (int i = 0; i < answer.Length; i++) + answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("source=advance-blocking-app; group=" + group.Name + "; blockListUrl=" + blockListUrls[i].AbsoluteUri + "; domain=" + blockedDomain)); + } + else + { + answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("source=advance-blocking-app; group=" + group.Name + "; domain=" + blockedDomain)) }; + } + } + else + { + if (blockListUrls.Count > 0) + { + answer = new DnsResourceRecord[blockListUrls.Count]; + + for (int i = 0; i < answer.Length; i++) + answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("source=advance-blocking-app; group=" + group.Name + "; regexBlockListUrl=" + blockListUrls[i].AbsoluteUri + "; regex=" + blockedRegex)); + } + else + { + answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("source=advance-blocking-app; group=" + group.Name + "; regex=" + blockedRegex)) }; + } + } + + return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }); + } + else + { + DnsResponseCode rcode; + IReadOnlyList answer = null; + IReadOnlyList authority = null; + + if (_blockAsNxDomain) + { + rcode = DnsResponseCode.NxDomain; + + string parentDomain = GetParentZone(blockedDomain); + if (parentDomain is null) + parentDomain = string.Empty; + + authority = new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; + } + else + { + rcode = DnsResponseCode.NoError; + + switch (question.Type) + { + case DnsResourceRecordType.A: + { + List rrList = new List(_aRecords.Count); + + foreach (DnsARecord record in _aRecords) + rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 60, record)); + + answer = rrList; + } + break; + + case DnsResourceRecordType.AAAA: + { + List rrList = new List(_aaaaRecords.Count); + + foreach (DnsAAAARecord record in _aaaaRecords) + rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 60, record)); + + answer = rrList; + } + break; + + case DnsResourceRecordType.NS: + if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase)) + answer = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, 60, _nsRecord) }; + else + authority = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; + + break; + + default: + authority = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; + break; + } + } + + return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, answer, authority) { Tag = DnsServerResponseType.Blocked }); + } + } + + #endregion + + #region properties + + public string Description + { get { return "Blocks domain names using block lists and regex block lists. Supports creating groups based on client's IP address or subnet to enforce different block lists and regex block lists for each group."; } } + + #endregion + + class Group + { + #region variables + + readonly string _name; + bool _enabled; + + IReadOnlyDictionary _allowed; + IReadOnlyDictionary _blocked; + IReadOnlyList _allowListUrls; + IReadOnlyList _blockListUrls; + + IReadOnlyList _allowedRegex; + IReadOnlyList _blockedRegex; + IReadOnlyList _regexAllowListUrls; + IReadOnlyList _regexBlockListUrls; + + IReadOnlyDictionary> _blockListZone = new Dictionary>(0); + + IReadOnlyList _regexAllowListZone = Array.Empty(); + IReadOnlyList _regexBlockListZone = Array.Empty(); + + #endregion + + #region constructor + + public Group(dynamic jsonGroup) + { + _name = jsonGroup.name.Value; + _enabled = jsonGroup.enabled.Value; + + { + Dictionary allowed = new Dictionary(1); + + foreach (dynamic jsonDomain in jsonGroup.allowed) + allowed.TryAdd(jsonDomain.Value, null); + + _allowed = allowed; + } + + { + Dictionary blocked = new Dictionary(1); + + foreach (dynamic jsonDomain in jsonGroup.blocked) + blocked.TryAdd(jsonDomain.Value, null); + + _blocked = blocked; + } + + { + List allowListUrls = new List(2); + + foreach (dynamic jsonUrl in jsonGroup.allowListUrls) + { + Uri url = new Uri(jsonUrl.Value); + + if (!allowListUrls.Contains(url)) + allowListUrls.Add(url); + } + + _allowListUrls = allowListUrls; + } + + { + List blockListUrls = new List(2); + + foreach (dynamic jsonUrl in jsonGroup.blockListUrls) + { + Uri url = new Uri(jsonUrl.Value); + + if (!blockListUrls.Contains(url)) + blockListUrls.Add(url); + } + + _blockListUrls = blockListUrls; + } + + { + List allowedRegex = new List(); + + foreach (dynamic jsonRegex in jsonGroup.allowedRegex) + { + string regexPattern = jsonRegex.Value; + + allowedRegex.Add(new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)); + } + + _allowedRegex = allowedRegex; + } + + { + List blockedRegex = new List(); + + foreach (dynamic jsonRegex in jsonGroup.blockedRegex) + { + string regexPattern = jsonRegex.Value; + + blockedRegex.Add(new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)); + } + + _blockedRegex = blockedRegex; + } + + { + List regexAllowListUrls = new List(); + + foreach (dynamic jsonUrl in jsonGroup.regexAllowListUrls) + { + string strUrl = jsonUrl.Value; + + regexAllowListUrls.Add(new Uri(strUrl)); + } + + _regexAllowListUrls = regexAllowListUrls; + } + + { + List regexBlockListUrls = new List(); + + foreach (dynamic jsonUrl in jsonGroup.regexBlockListUrls) + { + string strUrl = jsonUrl.Value; + + regexBlockListUrls.Add(new Uri(strUrl)); + } + + _regexBlockListUrls = regexBlockListUrls; + } + } + + #endregion + + #region private + + private static bool IsZoneAllowed(IReadOnlyDictionary allowedDomains, string domain) + { + do + { + if (allowedDomains.TryGetValue(domain, out _)) + return true; + + domain = GetParentZone(domain); + } + while (domain is not null); + + return false; + } + + #endregion + + #region public + + public void LoadBlockListZone(Dictionary> allAllowListQueues, Dictionary> allBlockListQueues) + { + //read all allowed domains in dictionary + Dictionary allowedDomains = new Dictionary(); + + foreach (Uri allowListUrl in _allowListUrls) + { + if (allAllowListQueues.TryGetValue(allowListUrl, out Queue queue)) + { + while (queue.Count > 0) + { + string domain = queue.Dequeue(); + + allowedDomains.TryAdd(domain, null); + } + } + } + + //select block lists + Dictionary> blockListQueues = new Dictionary>(_blockListUrls.Count); + int totalDomains = 0; + + foreach (Uri blockListUrl in _blockListUrls) + { + if (allBlockListQueues.TryGetValue(blockListUrl, out Queue blockListQueue)) + { + totalDomains += blockListQueue.Count; + blockListQueues.Add(blockListUrl, blockListQueue); + } + } + + //load block list zone + Dictionary> blockListZone = new Dictionary>(totalDomains); + + foreach (KeyValuePair> blockListQueue in blockListQueues) + { + Queue queue = blockListQueue.Value; + + while (queue.Count > 0) + { + string domain = queue.Dequeue(); + + if (IsZoneAllowed(allowedDomains, domain)) + continue; //domain is in allowed list so skip adding it to block list zone + + if (!blockListZone.TryGetValue(domain, out List blockListUrls)) + { + blockListUrls = new List(2); + blockListZone.Add(domain, blockListUrls); + } + + blockListUrls.Add(blockListQueue.Key); + } + } + + _blockListZone = blockListZone; + } + + public void LoadRegexBlockListZone(Dictionary> allRegexAllowListQueues, Dictionary> allRegexBlockListQueues) + { + { + //select regex allow lists + Dictionary> regexAllowListQueues = new Dictionary>(_regexAllowListUrls.Count); + int totalRegexPatterns = 0; + + foreach (Uri regexAllowListUrl in _regexAllowListUrls) + { + if (allRegexAllowListQueues.TryGetValue(regexAllowListUrl, out Queue regexAllowListQueue)) + { + totalRegexPatterns += regexAllowListQueue.Count; + regexAllowListQueues.Add(regexAllowListUrl, regexAllowListQueue); + } + } + + //load regex allow list patterns from queue + Dictionary allRegexPatterns = new Dictionary(totalRegexPatterns); + + foreach (KeyValuePair> regexAllowListQueue in regexAllowListQueues) + { + Queue queue = regexAllowListQueue.Value; + + while (queue.Count > 0) + { + string regex = queue.Dequeue(); + + if (!allRegexPatterns.TryGetValue(regex, out _)) + allRegexPatterns.Add(regex, null); + } + } + + //load regex allow list zone + List regexAllowListZone = new List(totalRegexPatterns); + + foreach (KeyValuePair regexPattern in allRegexPatterns) + { + Regex regex = new Regex(regexPattern.Key, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + + regexAllowListZone.Add(new RegexItem(regex, null)); + } + + _regexAllowListZone = regexAllowListZone; + } + + { + //select regex block lists + Dictionary> regexBlockListQueues = new Dictionary>(_regexBlockListUrls.Count); + int totalRegexPatterns = 0; + + foreach (Uri regexBlockListUrl in _regexBlockListUrls) + { + if (allRegexBlockListQueues.TryGetValue(regexBlockListUrl, out Queue regexBlockListQueue)) + { + totalRegexPatterns += regexBlockListQueue.Count; + regexBlockListQueues.Add(regexBlockListUrl, regexBlockListQueue); + } + } + + //load regex block list patterns from queue + Dictionary> allRegexPatterns = new Dictionary>(totalRegexPatterns); + + foreach (KeyValuePair> regexBlockListQueue in regexBlockListQueues) + { + Queue queue = regexBlockListQueue.Value; + + while (queue.Count > 0) + { + string regexPattern = queue.Dequeue(); + + if (!allRegexPatterns.TryGetValue(regexPattern, out List regexBlockLists)) + { + regexBlockLists = new List(2); + allRegexPatterns.Add(regexPattern, regexBlockLists); + } + + regexBlockLists.Add(regexBlockListQueue.Key); + } + } + + //load regex block list zone + List regexBlockListZone = new List(totalRegexPatterns); + + foreach (KeyValuePair> regexPattern in allRegexPatterns) + { + Regex regex = new Regex(regexPattern.Key, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + + regexBlockListZone.Add(new RegexItem(regex, regexPattern.Value)); + } + + _regexBlockListZone = regexBlockListZone; + } + } + + public IReadOnlyList IsZoneBlocked(string domain, out string blockedDomain, out string blockedRegex) + { + domain = domain.ToLower(); + + //allowed + string domain1 = domain; + do + { + if (_allowed.TryGetValue(domain1, out _)) + { + //found zone allowed + blockedDomain = null; + blockedRegex = null; + return null; + } + + domain1 = GetParentZone(domain1); + } + while (domain1 is not null); + + //allowedRegex + foreach (Regex regex in _allowedRegex) + { + if (regex.IsMatch(domain)) + { + //found pattern allowed + blockedDomain = null; + blockedRegex = null; + return null; + } + } + + //regex allow list zone + foreach (RegexItem regexItem in _regexAllowListZone) + { + if (regexItem.Regex.IsMatch(domain)) + { + //found pattern allowed + blockedDomain = null; + blockedRegex = null; + return null; + } + } + + //blocked + string domain2 = domain; + do + { + if (_blocked.TryGetValue(domain2, out _)) + { + //found zone blocked + blockedDomain = domain2; + blockedRegex = null; + return Array.Empty(); + } + + domain2 = GetParentZone(domain2); + } + while (domain2 is not null); + + //block list zone + string domain3 = domain; + do + { + if (_blockListZone.TryGetValue(domain3, out List blockListUrls)) + { + //found zone blocked + blockedDomain = domain3; + blockedRegex = null; + return blockListUrls; + } + + domain3 = GetParentZone(domain3); + } + while (domain3 is not null); + + //blockedRegex + foreach (Regex regex in _blockedRegex) + { + if (regex.IsMatch(domain)) + { + //found pattern blocked + blockedDomain = null; + blockedRegex = regex.ToString(); + return Array.Empty(); + } + } + + //regex block list zone + foreach (RegexItem regexItem in _regexBlockListZone) + { + if (regexItem.Regex.IsMatch(domain)) + { + //found pattern blocked + blockedDomain = null; + blockedRegex = regexItem.Regex.ToString(); + return regexItem.BlockListUrls; + } + } + + blockedDomain = null; + blockedRegex = null; + return null; + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + public bool Enabled + { + get { return _enabled; } + set { _enabled = value; } + } + + public IReadOnlyDictionary Allowed + { + get { return _allowed; } + set { _allowed = value; } + } + + public IReadOnlyDictionary Blocked + { + get { return _blocked; } + set { _blocked = value; } + } + + public IReadOnlyList AllowListUrls + { + get { return _allowListUrls; } + set { _allowListUrls = value; } + } + + public IReadOnlyList BlockListUrls + { + get { return _blockListUrls; } + set { _blockListUrls = value; } + } + + public IReadOnlyList AllowedRegex + { + get { return _allowedRegex; } + set { _allowedRegex = value; } + } + + public IReadOnlyList BlockedRegex + { + get { return _blockedRegex; } + set { _blockedRegex = value; } + } + + public IReadOnlyList RegexBlockListUrls + { + get { return _regexBlockListUrls; } + set { _regexBlockListUrls = value; } + } + + public IReadOnlyList RegexAllowListUrls + { + get { return _regexAllowListUrls; } + set { _regexAllowListUrls = value; } + } + + #endregion + } + + class RegexItem + { + #region variables + + readonly Regex _regex; + readonly IReadOnlyList _blockListUrls; + + #endregion + + #region constructor + + public RegexItem(Regex regex, IReadOnlyList blockListUrls) + { + _regex = regex; + _blockListUrls = blockListUrls; + } + + #endregion + + #region properties + + public Regex Regex + { get { return _regex; } } + + public IReadOnlyList BlockListUrls + { get { return _blockListUrls; } } + + #endregion + } + } +} diff --git a/Apps/AdvanceBlockingApp/dnsApp.config b/Apps/AdvanceBlockingApp/dnsApp.config new file mode 100644 index 00000000..e5682649 --- /dev/null +++ b/Apps/AdvanceBlockingApp/dnsApp.config @@ -0,0 +1,49 @@ +{ + "enableBlocking": true, + "blockAsNxDomain": false, + "blockListUrlUpdateIntervalHours": 24, + "blockingAddresses": [ + "0.0.0.0", + "::" + ], + "networkGroupMap": { + "192.168.1.20": "kids", + "192.168.1.0/24": "home" + }, + "groups": [ + { + "name": "home", + "enabled": true, + "allowed": [], + "blocked": [ + "example.com" + ], + "allowListUrls": [], + "blockListUrls": [ + "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" + ], + "allowedRegex": [], + "blockedRegex": [ + "^ads\\." + ], + "regexAllowListUrls": [], + "regexBlockListUrls": [ + "http://localhost:5380/RegexBlockList.txt" + ] + }, + { + "name": "kids", + "enabled": true, + "allowed": [], + "blocked": [], + "allowListUrls": [], + "blockListUrls": [ + "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts" + ], + "allowedRegex": [], + "blockedRegex": [], + "regexAllowListUrls": [], + "regexBlockListUrls": [] + } + ] +} \ No newline at end of file diff --git a/Apps/BlockListGroupsApp/App.cs b/Apps/BlockListGroupsApp/App.cs deleted file mode 100644 index de8fe980..00000000 --- a/Apps/BlockListGroupsApp/App.cs +++ /dev/null @@ -1,626 +0,0 @@ -/* -Technitium DNS Server -Copyright (C) 2021 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 . - -*/ - -using DnsApplicationCommon; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using TechnitiumLibrary.Net; -using TechnitiumLibrary.Net.Dns; -using TechnitiumLibrary.Net.Dns.ResourceRecords; - -namespace BlockListGroups -{ - public class App : IDnsAuthoritativeRequestHandler - { - #region variables - - IDnsServer _dnsServer; - string _localCacheFolder; - - bool _enableBlocking; - bool _blockAsNxDomain; - int _blockListUrlUpdateIntervalHours; - - IReadOnlyCollection _aRecords; - IReadOnlyCollection _aaaaRecords; - DnsSOARecord _soaRecord; - DnsNSRecord _nsRecord; - - IReadOnlyDictionary> _blockListUrlGroups; - IReadOnlyDictionary _networkGroupMap; - - IReadOnlyDictionary>> _blockListZones; - - Timer _blockListUrlUpdateTimer; - DateTime _blockListUrlLastUpdatedOn; - const int BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL = 5000; - const int BLOCK_LIST_UPDATE_TIMER_INTERVAL = 900000; - - #endregion - - #region IDisposable - - public void Dispose() - { - if (_blockListUrlUpdateTimer is not null) - { - _blockListUrlUpdateTimer.Dispose(); - _blockListUrlUpdateTimer = null; - } - } - - #endregion - - #region private - - private async void BlockListUrlUpdateTimerCallbackAsync(object state) - { - try - { - if (DateTime.UtcNow > _blockListUrlLastUpdatedOn.AddHours(_blockListUrlUpdateIntervalHours)) - { - if (await UpdateBlockListsAsync()) - { - //block lists were updated - //save last updated on time - _blockListUrlLastUpdatedOn = DateTime.UtcNow; - } - } - } - catch (Exception ex) - { - _dnsServer.WriteLog(ex); - } - } - - private string GetBlockListFilePath(Uri blockListUrl) - { - using (HashAlgorithm hash = SHA256.Create()) - { - return Path.Combine(_localCacheFolder, BitConverter.ToString(hash.ComputeHash(Encoding.UTF8.GetBytes(blockListUrl.AbsoluteUri))).Replace("-", "").ToLower()); - } - } - - private async Task UpdateBlockListsAsync() - { - bool downloaded = false; - bool notModified = false; - - async Task DownloadListUrlAsync(Uri listUrl, bool isAllowList) - { - string listFilePath = GetBlockListFilePath(listUrl); - string listDownloadFilePath = listFilePath + ".downloading"; - - try - { - if (File.Exists(listDownloadFilePath)) - File.Delete(listDownloadFilePath); - - SocketsHttpHandler handler = new SocketsHttpHandler(); - handler.Proxy = _dnsServer.Proxy; - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - - using (HttpClient http = new HttpClient(handler)) - { - if (File.Exists(listFilePath)) - http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(listFilePath); - - HttpResponseMessage httpResponse = await http.GetAsync(listUrl); - switch (httpResponse.StatusCode) - { - case HttpStatusCode.OK: - { - using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write)) - { - using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync()) - { - await httpStream.CopyToAsync(fS); - } - } - - if (File.Exists(listFilePath)) - File.Delete(listFilePath); - - File.Move(listDownloadFilePath, listFilePath); - - if (httpResponse.Content.Headers.LastModified != null) - File.SetLastWriteTimeUtc(listFilePath, httpResponse.Content.Headers.LastModified.Value.UtcDateTime); - - downloaded = true; - - _dnsServer.WriteLog("Block List Groups app successfully downloaded " + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri); - } - break; - - case HttpStatusCode.NotModified: - { - notModified = true; - - _dnsServer.WriteLog("Block List Groups app successfully checked for a new update of the " + (isAllowList ? "allow" : "block") + " list: " + listUrl.AbsoluteUri); - } - break; - - default: - throw new HttpRequestException((int)httpResponse.StatusCode + " " + httpResponse.ReasonPhrase); - } - } - } - catch (Exception ex) - { - _dnsServer.WriteLog("Block List Groups app failed to download " + (isAllowList ? "allow" : "block") + " list and will use previously downloaded file (if available): " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); - } - } - - List tasks = new List(); - IReadOnlyList uniqueBlockListUrls = GetUniqueBlockListUrls(); - - foreach (Uri blockListUrl in uniqueBlockListUrls) - tasks.Add(DownloadListUrlAsync(blockListUrl, false)); - - await Task.WhenAll(tasks); - - if (downloaded) - LoadBlockListUrls(); - - return downloaded || notModified; - } - - private static string PopWord(ref string line) - { - if (line.Length == 0) - return line; - - line = line.TrimStart(' ', '\t'); - - int i = line.IndexOfAny(new char[] { ' ', '\t' }); - string word; - - if (i < 0) - { - word = line; - line = ""; - } - else - { - word = line.Substring(0, i); - line = line.Substring(i + 1); - } - - return word; - } - - private Queue ReadListFile(Uri listUrl, bool isAllow) - { - Queue domains = new Queue(); - - try - { - _dnsServer.WriteLog("Block List Groups app is reading " + (isAllow ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri); - - using (FileStream fS = new FileStream(GetBlockListFilePath(listUrl), FileMode.Open, FileAccess.Read)) - { - //parse hosts file and populate block zone - StreamReader sR = new StreamReader(fS, true); - string line; - string firstWord; - string secondWord; - string hostname; - - while (true) - { - line = sR.ReadLine(); - if (line == null) - break; //eof - - line = line.TrimStart(' ', '\t'); - - if (line.Length == 0) - continue; //skip empty line - - if (line.StartsWith("#")) - continue; //skip comment line - - firstWord = PopWord(ref line); - - if (line.Length == 0) - { - hostname = firstWord; - } - else - { - secondWord = PopWord(ref line); - - if (secondWord.Length == 0) - hostname = firstWord; - else - hostname = secondWord; - } - - hostname = hostname.Trim('.').ToLower(); - - switch (hostname) - { - case "": - case "localhost": - case "localhost.localdomain": - case "local": - case "broadcasthost": - case "ip6-localhost": - case "ip6-loopback": - case "ip6-localnet": - case "ip6-mcastprefix": - case "ip6-allnodes": - case "ip6-allrouters": - case "ip6-allhosts": - continue; //skip these hostnames - } - - if (!DnsClient.IsDomainNameValid(hostname)) - continue; - - if (IPAddress.TryParse(hostname, out _)) - continue; //skip line when hostname is IP address - - domains.Enqueue(hostname); - } - } - - _dnsServer.WriteLog("Block List Groups app " + (isAllow ? "allow" : "block") + " list file was read (" + domains.Count + " domains) from: " + listUrl.AbsoluteUri); - } - catch (Exception ex) - { - _dnsServer.WriteLog("Block List Groups app failed to read " + (isAllow ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); - } - - return domains; - } - - private IReadOnlyList GetUniqueBlockListUrls() - { - List blockListUrls = new List(); - - foreach (KeyValuePair> blockListUrlGroup in _blockListUrlGroups) - { - foreach (Uri blockListUrl in blockListUrlGroup.Value) - { - if (!blockListUrls.Contains(blockListUrl)) - blockListUrls.Add(blockListUrl); - } - } - - return blockListUrls; - } - - private void LoadBlockListUrls() - { - IReadOnlyList uniqueBlockListUrls = GetUniqueBlockListUrls(); - - //read all block lists in a queue - Dictionary> uniqueBlockListQueues = new Dictionary>(uniqueBlockListUrls.Count); - - foreach (Uri blockListUrl in uniqueBlockListUrls) - { - if (!uniqueBlockListQueues.ContainsKey(blockListUrl)) - { - Queue blockListQueue = ReadListFile(blockListUrl, false); - uniqueBlockListQueues.Add(blockListUrl, blockListQueue); - } - } - - //load block list zone per group - Dictionary>> blockListZones = new Dictionary>>(); - - foreach (KeyValuePair> blockListUrlGroup in _blockListUrlGroups) - { - string group = blockListUrlGroup.Key; - IReadOnlyList blockListUrls = blockListUrlGroup.Value; - - //prepare group wise block list queue - Dictionary> blockListQueues = new Dictionary>(uniqueBlockListUrls.Count); - int totalDomains = 0; - - foreach (Uri blockListUrl in blockListUrls) - { - if (uniqueBlockListQueues.TryGetValue(blockListUrl, out Queue blockListQueue)) - { - totalDomains += blockListQueue.Count; - blockListQueues.Add(blockListUrl, blockListQueue); - } - } - - //load block list zone - Dictionary> blockListZone = new Dictionary>(totalDomains); - - foreach (KeyValuePair> blockListQueue in blockListQueues) - { - Queue queue = blockListQueue.Value; - - while (queue.Count > 0) - { - string domain = queue.Dequeue(); - - if (!blockListZone.TryGetValue(domain, out List blockLists)) - { - blockLists = new List(2); - blockListZone.Add(domain, blockLists); - } - - blockLists.Add(blockListQueue.Key); - } - } - - blockListZones.Add(group, blockListZone); - } - - //set new blocked zone - _blockListZones = blockListZones; - - _dnsServer.WriteLog("Block List Groups app loaded all block list zones successfully."); - - //force GC collection to remove old zone data from memory quickly - GC.Collect(); - } - - private static string GetParentZone(string domain) - { - int i = domain.IndexOf('.'); - if (i > -1) - return domain.Substring(i + 1); - - //dont return root zone - return null; - } - - private IReadOnlyList IsZoneBlocked(string group, string domain, out string blockedDomain) - { - if (!_blockListZones.TryGetValue(group, out IReadOnlyDictionary> blockListZone)) - { - blockedDomain = null; - return null; - } - - domain = domain.ToLower(); - - do - { - if (blockListZone.TryGetValue(domain, out List blockLists)) - { - //found zone blocked - blockedDomain = domain; - return blockLists; - } - - domain = GetParentZone(domain); - } - while (domain is not null); - - blockedDomain = null; - return null; - } - - #endregion - - #region public - - public Task InitializeAsync(IDnsServer dnsServer, string config) - { - _dnsServer = dnsServer; - _localCacheFolder = Path.Combine(_dnsServer.ApplicationFolder, "blocklists"); - - Directory.CreateDirectory(_localCacheFolder); - - dynamic jsonConfig = JsonConvert.DeserializeObject(config); - - _enableBlocking = jsonConfig.enableBlocking.Value; - _blockAsNxDomain = jsonConfig.blockAsNxDomain.Value; - _blockListUrlUpdateIntervalHours = Convert.ToInt32(jsonConfig.blockListUrlUpdateIntervalHours.Value); - - { - List aRecords = new List(); - List aaaaRecords = new List(); - - foreach (dynamic jsonBlockingAddress in jsonConfig.blockingAddresses) - { - string strAddress = jsonBlockingAddress.Value; - - if (IPAddress.TryParse(strAddress, out IPAddress address)) - { - switch (address.AddressFamily) - { - case AddressFamily.InterNetwork: - aRecords.Add(new DnsARecord(address)); - break; - - case AddressFamily.InterNetworkV6: - aaaaRecords.Add(new DnsAAAARecord(address)); - break; - } - } - } - - _aRecords = aRecords; - _aaaaRecords = aaaaRecords; - _soaRecord = new DnsSOARecord(dnsServer.ServerDomain, "hostadmin." + dnsServer.ServerDomain, 1, 14400, 3600, 604800, 60); - _nsRecord = new DnsNSRecord(dnsServer.ServerDomain); - } - - { - Dictionary> blockListUrlGroups = new Dictionary>(); - - foreach (dynamic jsonProperty in jsonConfig.blockListUrlGroups) - { - string group = jsonProperty.Name; - - List blockListUrls = new List(); - - foreach (dynamic jsonUrl in jsonProperty.Value) - blockListUrls.Add(new Uri(jsonUrl.Value)); - - blockListUrlGroups.Add(group, blockListUrls); - } - - _blockListUrlGroups = blockListUrlGroups; - } - - { - Dictionary networkGroupMap = new Dictionary(); - - foreach (dynamic jsonProperty in jsonConfig.networkGroupMap) - { - string network = jsonProperty.Name; - string group = jsonProperty.Value; - - if (NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) - networkGroupMap.Add(networkAddress, group); - } - - _networkGroupMap = networkGroupMap; - } - - if (_blockListUrlUpdateTimer is null) - { - _blockListUrlUpdateTimer = new Timer(BlockListUrlUpdateTimerCallbackAsync, null, Timeout.Infinite, Timeout.Infinite); - _blockListUrlUpdateTimer.Change(BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, BLOCK_LIST_UPDATE_TIMER_INTERVAL); - } - - Task.Run(delegate () - { - LoadBlockListUrls(); - }); - - return Task.CompletedTask; - } - - public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) - { - if (!_enableBlocking) - return Task.FromResult(null); - - IPAddress remoteIP = remoteEP.Address; - string group = null; - - foreach (KeyValuePair entry in _networkGroupMap) - { - if (entry.Key.Contains(remoteIP)) - { - group = entry.Value; - break; - } - } - - if (group is null) - return Task.FromResult(null); - - DnsQuestionRecord question = request.Question[0]; - - IReadOnlyList blockLists = IsZoneBlocked(group, question.Name, out string blockedDomain); - if (blockLists is null) - return Task.FromResult(null); - - if (question.Type == DnsResourceRecordType.TXT) - { - //return meta data - DnsResourceRecord[] answer = new DnsResourceRecord[blockLists.Count]; - - for (int i = 0; i < answer.Length; i++) - answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("blockList=block-list-groups-app; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain)); - - return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }); - } - else - { - DnsResponseCode rcode; - IReadOnlyList answer = null; - IReadOnlyList authority = null; - - if (_blockAsNxDomain) - { - rcode = DnsResponseCode.NxDomain; - - string parentDomain = GetParentZone(blockedDomain); - if (parentDomain is null) - parentDomain = string.Empty; - - authority = new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; - } - else - { - rcode = DnsResponseCode.NoError; - - switch (question.Type) - { - case DnsResourceRecordType.A: - { - List rrList = new List(_aRecords.Count); - - foreach (DnsARecord record in _aRecords) - rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 60, record)); - - answer = rrList; - } - break; - - case DnsResourceRecordType.AAAA: - { - List rrList = new List(_aaaaRecords.Count); - - foreach (DnsAAAARecord record in _aaaaRecords) - rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 60, record)); - - answer = rrList; - } - break; - - case DnsResourceRecordType.NS: - if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase)) - answer = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, 60, _nsRecord) }; - else - authority = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; - - break; - - default: - authority = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; - break; - } - } - - return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, answer, authority) { Tag = DnsServerResponseType.Blocked }); - } - } - - #endregion - - #region properties - - public string Description - { get { return "Blocks domain names using client's IP address or subnet specific block list URLs."; } } - - #endregion - } -} diff --git a/Apps/BlockListGroupsApp/dnsApp.config b/Apps/BlockListGroupsApp/dnsApp.config deleted file mode 100644 index fde9d323..00000000 --- a/Apps/BlockListGroupsApp/dnsApp.config +++ /dev/null @@ -1,24 +0,0 @@ -{ - "enableBlocking": true, - "blockAsNxDomain": false, - "blockListUrlUpdateIntervalHours": 24, - "blockingAddresses": [ - "0.0.0.0", - "::" - ], - "blockListUrlGroups": { - "default": [ - ], - "everyone": [ - "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" - ], - "kids": [ - "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts", - "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts" - ] - }, - "networkGroupMap": { - "192.168.1.20": "kids", - "192.168.1.0/24": "everyone" - } -} \ No newline at end of file