From 477390cc1eac8ebba60a67604ed404a4b773972b Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sat, 11 Sep 2021 16:34:13 +0530 Subject: [PATCH] RegexBlockListApp: added new app --- Apps/RegexBlockListApp/App.cs | 536 ++++++++++++++++++ .../RegexBlockListApp.csproj | 41 ++ Apps/RegexBlockListApp/dnsApp.config | 17 + 3 files changed, 594 insertions(+) create mode 100644 Apps/RegexBlockListApp/App.cs create mode 100644 Apps/RegexBlockListApp/RegexBlockListApp.csproj create mode 100644 Apps/RegexBlockListApp/dnsApp.config diff --git a/Apps/RegexBlockListApp/App.cs b/Apps/RegexBlockListApp/App.cs new file mode 100644 index 00000000..00760634 --- /dev/null +++ b/Apps/RegexBlockListApp/App.cs @@ -0,0 +1,536 @@ +/* +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.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace RegexBlockList +{ + public class App : IDnsAuthoritativeRequestHandler + { + #region variables + + IDnsServer _dnsServer; + string _localCacheFolder; + + bool _enableBlocking; + bool _blockAsNxDomain; + int _blockListUrlUpdateIntervalHours; + + IReadOnlyCollection _aRecords; + IReadOnlyCollection _aaaaRecords; + DnsSOARecord _soaRecord; + DnsNSRecord _nsRecord; + + IReadOnlyList _regexAllowListPatterns; + IReadOnlyList _regexBlockListPatterns; + IReadOnlyList _regexBlockListUrlPatterns = Array.Empty(); + + IReadOnlyList _regexBlockListUrls; + + 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("Regex Block List app successfully downloaded " + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri); + } + break; + + case HttpStatusCode.NotModified: + { + notModified = true; + + _dnsServer.WriteLog("Regex Block List 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("Regex Block List 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(); + + foreach (Uri blockListUrl in _regexBlockListUrls) + tasks.Add(DownloadListUrlAsync(blockListUrl, false)); + + await Task.WhenAll(tasks); + + if (downloaded) + LoadBlockListUrls(); + + return downloaded || notModified; + } + + private Queue ReadListFile(Uri listUrl, bool isAllow) + { + Queue regexPatterns = new Queue(); + + try + { + _dnsServer.WriteLog("Regex Block List 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; + + 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 + + regexPatterns.Enqueue(line); + } + } + + _dnsServer.WriteLog("Regex Block List app " + (isAllow ? "allow" : "block") + " list file was read (" + regexPatterns.Count + " regex patterns) from: " + listUrl.AbsoluteUri); + } + catch (Exception ex) + { + _dnsServer.WriteLog("Regex Block List app failed to read " + (isAllow ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); + } + + return regexPatterns; + } + + private void LoadBlockListUrls() + { + //read all block lists in a queue + Dictionary> blockListQueues = new Dictionary>(_regexBlockListUrls.Count); + int totalPatterns = 0; + + foreach (Uri blockListUrl in _regexBlockListUrls) + { + if (!blockListQueues.ContainsKey(blockListUrl)) + { + Queue regexPatterns = ReadListFile(blockListUrl, false); + totalPatterns += regexPatterns.Count; + blockListQueues.Add(blockListUrl, regexPatterns); + } + } + + //load block list patterns from queue + Dictionary> blockListPatterns = new Dictionary>(totalPatterns); + + foreach (KeyValuePair> blockListQueue in blockListQueues) + { + Queue queue = blockListQueue.Value; + + while (queue.Count > 0) + { + string regexPattern = queue.Dequeue(); + + if (!blockListPatterns.TryGetValue(regexPattern, out List blockLists)) + { + blockLists = new List(2); + blockListPatterns.Add(regexPattern, blockLists); + } + + blockLists.Add(blockListQueue.Key); + } + } + + //load block list patterns into regex list + List regexBlockListUrlPatterns = new List(); + + foreach (KeyValuePair> item in blockListPatterns) + { + Regex regex = new Regex(item.Key, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + + regexBlockListUrlPatterns.Add(new RegexItem(regex, item.Value)); + } + + _regexBlockListUrlPatterns = regexBlockListUrlPatterns; + + _dnsServer.WriteLog("Regex Block List app block list URL regex patterns were loaded successfully."); + } + + 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); + } + + { + List regexAllowListPatterns = new List(); + + foreach (dynamic jsonRegex in jsonConfig.regexAllowList) + { + string regexPattern = jsonRegex.Value; + + regexAllowListPatterns.Add(new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)); + } + + _regexAllowListPatterns = regexAllowListPatterns; + } + + { + List regexBlockListPatterns = new List(); + + foreach (dynamic jsonRegex in jsonConfig.regexBlockList) + { + string regexPattern = jsonRegex.Value; + + regexBlockListPatterns.Add(new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)); + } + + _regexBlockListPatterns = regexBlockListPatterns; + } + + { + List regexBlockListUrls = new List(); + + foreach (dynamic jsonUrl in jsonConfig.regexBlockListUrls) + { + string strUrl = jsonUrl.Value; + + regexBlockListUrls.Add(new Uri(strUrl)); + } + + _regexBlockListUrls = regexBlockListUrls; + } + + 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); + } + + LoadBlockListUrls(); + + return Task.CompletedTask; + } + + public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) + { + if (!_enableBlocking) + return Task.FromResult(null); + + DnsQuestionRecord question = request.Question[0]; + string domain = question.Name; + + foreach (Regex regex in _regexAllowListPatterns) + { + if (regex.IsMatch(domain)) + return Task.FromResult(null); + } + + bool isBlocked = false; + Regex matchedRegex = null; + RegexItem matchedRegexItem = null; + + foreach (Regex regex in _regexBlockListPatterns) + { + if (regex.IsMatch(domain)) + { + isBlocked = true; + matchedRegex = regex; + break; + } + } + + if (!isBlocked) + { + foreach (RegexItem regexItem in _regexBlockListUrlPatterns) + { + if (regexItem.Regex.IsMatch(domain)) + { + isBlocked = true; + matchedRegexItem = regexItem; + break; + } + } + } + + if (!isBlocked) + return Task.FromResult(null); + + if (question.Type == DnsResourceRecordType.TXT) + { + //return meta data + DnsResourceRecord[] answer; + + if (matchedRegexItem is null) + { + answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("blockList=regex-block-list-app; pattern=" + matchedRegex.ToString())) }; + } + else + { + answer = new DnsResourceRecord[matchedRegexItem.BlockListUrls.Count]; + + for (int i = 0; i < answer.Length; i++) + answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecord("blockList=regex-block-list-app; regexBlockListUrl=" + matchedRegexItem.BlockListUrls[i].AbsoluteUri + "; pattern=" + matchedRegexItem.Regex.ToString())); + } + + 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(question.Name); + 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: + answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.NS, question.Class, 60, _nsRecord) }; + break; + + default: + authority = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, 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 that match the regex defined in the config and from regex based block list URLs."; } } + + #endregion + + class RegexItem + { + public readonly Regex Regex; + public readonly IReadOnlyList BlockListUrls; + + public RegexItem(Regex regex, IReadOnlyList blockListUrls) + { + Regex = regex; + BlockListUrls = blockListUrls; + } + } + } +} diff --git a/Apps/RegexBlockListApp/RegexBlockListApp.csproj b/Apps/RegexBlockListApp/RegexBlockListApp.csproj new file mode 100644 index 00000000..f26721ce --- /dev/null +++ b/Apps/RegexBlockListApp/RegexBlockListApp.csproj @@ -0,0 +1,41 @@ + + + + net5.0 + false + 1.0 + Technitium + Technitium DNS Server + Shreyas Zare + RegexBlockListApp + RegexBlockList + https://technitium.com/dns/ + https://github.com/TechnitiumSoftware/DnsServer + false + Library + + + + + + + + + false + + + + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll + false + + + + + + PreserveNewest + + + + diff --git a/Apps/RegexBlockListApp/dnsApp.config b/Apps/RegexBlockListApp/dnsApp.config new file mode 100644 index 00000000..b98d40fe --- /dev/null +++ b/Apps/RegexBlockListApp/dnsApp.config @@ -0,0 +1,17 @@ +{ + "enableBlocking": true, + "blockAsNxDomain": false, + "blockListUrlUpdateIntervalHours": 24, + "blockingAddresses": [ + "0.0.0.0", + "::" + ], + "regexAllowList": [ + ], + "regexBlockList": [ + "^ads\\." + ], + "regexBlockListUrls": [ + "http://localhost:5380/RegexBlockList.txt" + ] +} \ No newline at end of file