mirror of
https://github.com/fergalmoran/DnsServer.git
synced 2025-12-22 09:29:50 +00:00
1592 lines
63 KiB
C#
1592 lines
63 KiB
C#
/*
|
|
Technitium DNS Server
|
|
Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
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 AdvancedBlocking
|
|
{
|
|
public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler
|
|
{
|
|
#region variables
|
|
|
|
IDnsServer _dnsServer;
|
|
string _localCacheFolder;
|
|
|
|
DnsSOARecordData _soaRecord;
|
|
DnsNSRecordData _nsRecord;
|
|
|
|
bool _enableBlocking;
|
|
int _blockListUrlUpdateIntervalHours;
|
|
|
|
IReadOnlyDictionary<NetworkAddress, string> _networkGroupMap;
|
|
IReadOnlyDictionary<string, Group> _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 UpdateAllListsAsync())
|
|
{
|
|
//block lists were updated
|
|
//save last updated on time
|
|
_blockListUrlLastUpdatedOn = DateTime.UtcNow;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.WriteLog(ex);
|
|
}
|
|
}
|
|
|
|
private void FindAndSetBlockListUrlLastUpdatedOn()
|
|
{
|
|
try
|
|
{
|
|
string[] files = Directory.GetFiles(_localCacheFolder);
|
|
DateTime latest = DateTime.MinValue;
|
|
|
|
foreach (string file in files)
|
|
{
|
|
DateTime lastModified = File.GetLastWriteTimeUtc(file);
|
|
|
|
if (lastModified > latest)
|
|
latest = lastModified;
|
|
}
|
|
|
|
_blockListUrlLastUpdatedOn = latest;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.WriteLog(ex);
|
|
}
|
|
}
|
|
|
|
private string GetListFilePath(Uri listUrl)
|
|
{
|
|
using (HashAlgorithm hash = SHA256.Create())
|
|
{
|
|
return Path.Combine(_localCacheFolder, Convert.ToHexString(hash.ComputeHash(Encoding.UTF8.GetBytes(listUrl.AbsoluteUri))).ToLower());
|
|
}
|
|
}
|
|
|
|
private async Task<bool> UpdateAllListsAsync()
|
|
{
|
|
List<Uri> downloadedAllowListUrls = new List<Uri>();
|
|
List<Uri> downloadedBlockListUrls = new List<Uri>();
|
|
List<Uri> downloadedRegexAllowListUrls = new List<Uri>();
|
|
List<Uri> downloadedRegexBlockListUrls = new List<Uri>();
|
|
List<Uri> downloadedAdblockListUrls = new List<Uri>();
|
|
bool notModified = false;
|
|
|
|
async Task DownloadListUrlAsync(Uri listUrl, bool isAllowList, bool isRegexList, bool isAdblockList)
|
|
{
|
|
string listFilePath = GetListFilePath(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 (isAdblockList)
|
|
{
|
|
lock (downloadedAdblockListUrls)
|
|
{
|
|
downloadedAdblockListUrls.Add(listUrl);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
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("Advanced Blocking app successfully downloaded " + (isAdblockList ? "adblock" : (isRegexList ? "regex " : "") + (isAllowList ? "allow" : "block")) + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri);
|
|
}
|
|
break;
|
|
|
|
case HttpStatusCode.NotModified:
|
|
{
|
|
notModified = true;
|
|
|
|
_dnsServer.WriteLog("Advanced Blocking app successfully checked for a new update of the " + (isAdblockList ? "adblock" : (isRegexList ? "regex " : "") + (isAllowList ? "allow" : "block")) + " list: " + listUrl.AbsoluteUri);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new HttpRequestException((int)httpResponse.StatusCode + " " + httpResponse.ReasonPhrase);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app failed to download " + (isAdblockList ? "adblock" : (isRegexList ? "regex " : "") + (isAllowList ? "allow" : "block")) + " list and will use previously downloaded file (if available): " + listUrl.AbsoluteUri + "\r\n" + ex.ToString());
|
|
}
|
|
}
|
|
|
|
List<Task> tasks = new List<Task>();
|
|
IReadOnlyList<Uri> uniqueAllowListUrls = GetUniqueAllowListUrls();
|
|
IReadOnlyList<Uri> uniqueBlockListUrls = GetUniqueBlockListUrls();
|
|
IReadOnlyList<Uri> uniqueRegexAllowListUrls = GetUniqueRegexAllowListUrls();
|
|
IReadOnlyList<Uri> uniqueRegexBlockListUrls = GetUniqueRegexBlockListUrls();
|
|
IReadOnlyList<Uri> uniqueAdblockListUrls = GetUniqueAdblockListUrls();
|
|
|
|
foreach (Uri allowListUrl in uniqueAllowListUrls)
|
|
tasks.Add(DownloadListUrlAsync(allowListUrl, true, false, false));
|
|
|
|
foreach (Uri blockListUrl in uniqueBlockListUrls)
|
|
tasks.Add(DownloadListUrlAsync(blockListUrl, false, false, false));
|
|
|
|
foreach (Uri regexAllowListUrl in uniqueRegexAllowListUrls)
|
|
tasks.Add(DownloadListUrlAsync(regexAllowListUrl, true, true, false));
|
|
|
|
foreach (Uri regexBlockListUrl in uniqueRegexBlockListUrls)
|
|
tasks.Add(DownloadListUrlAsync(regexBlockListUrl, false, true, false));
|
|
|
|
foreach (Uri adblockListUrl in uniqueAdblockListUrls)
|
|
tasks.Add(DownloadListUrlAsync(adblockListUrl, false, false, true));
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
bool downloaded = (downloadedAllowListUrls.Count > 0) || (downloadedBlockListUrls.Count > 0) || (downloadedRegexAllowListUrls.Count > 0) || (downloadedRegexBlockListUrls.Count > 0) || (downloadedAdblockListUrls.Count > 0);
|
|
if (downloaded)
|
|
LoadZones(downloadedAllowListUrls, downloadedBlockListUrls, downloadedRegexAllowListUrls, downloadedRegexBlockListUrls, downloadedAdblockListUrls);
|
|
|
|
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<string> ReadListFile(Uri listUrl, bool isAllowList)
|
|
{
|
|
Queue<string> domains = new Queue<string>();
|
|
|
|
try
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app is reading " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri);
|
|
|
|
using (FileStream fS = new FileStream(GetListFilePath(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("Advanced Blocking app read " + (isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domains) from: " + listUrl.AbsoluteUri);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app failed to read " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString());
|
|
}
|
|
|
|
return domains;
|
|
}
|
|
|
|
private Queue<string> ReadRegexListFile(Uri listUrl, bool isAllowList)
|
|
{
|
|
Queue<string> regices = new Queue<string>();
|
|
|
|
try
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app is reading regex " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri);
|
|
|
|
using (FileStream fS = new FileStream(GetListFilePath(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("Advanced Blocking app read regex " + (isAllowList ? "allow" : "block") + " list file (" + regices.Count + " regex patterns) from: " + listUrl.AbsoluteUri);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app failed to read regex " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString());
|
|
}
|
|
|
|
return regices;
|
|
}
|
|
|
|
private void ReadAdblockListFile(Uri listUrl, out Queue<string> allowedDomains, out Queue<string> blockedDomains)
|
|
{
|
|
allowedDomains = new Queue<string>();
|
|
blockedDomains = new Queue<string>();
|
|
|
|
try
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app is reading adblock list from: " + listUrl.AbsoluteUri);
|
|
|
|
using (FileStream fS = new FileStream(GetListFilePath(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
|
|
|
|
if (line.StartsWith("||"))
|
|
{
|
|
int i = line.IndexOf('^');
|
|
if (i > -1)
|
|
{
|
|
string domain = line.Substring(2, i - 2);
|
|
string options = line.Substring(i + 1);
|
|
|
|
if (((options.Length == 0) || (options.StartsWith("$") && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain))
|
|
blockedDomains.Enqueue(domain);
|
|
}
|
|
else
|
|
{
|
|
string domain = line.Substring(2);
|
|
|
|
if (DnsClient.IsDomainNameValid(domain))
|
|
blockedDomains.Enqueue(domain);
|
|
}
|
|
}
|
|
else if (line.StartsWith("@@||"))
|
|
{
|
|
int i = line.IndexOf('^');
|
|
if (i > -1)
|
|
{
|
|
string domain = line.Substring(4, i - 4);
|
|
string options = line.Substring(i + 1);
|
|
|
|
if (((options.Length == 0) || (options.StartsWith("$") && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain))
|
|
blockedDomains.Enqueue(domain);
|
|
}
|
|
else
|
|
{
|
|
string domain = line.Substring(4);
|
|
|
|
if (DnsClient.IsDomainNameValid(domain))
|
|
allowedDomains.Enqueue(domain);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_dnsServer.WriteLog("Advanced Blocking app read adblock list file (" + (allowedDomains.Count + blockedDomains.Count) + " domains) from: " + listUrl.AbsoluteUri);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dnsServer.WriteLog("Advanced Blocking app failed to read adblock list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString());
|
|
}
|
|
}
|
|
|
|
private IReadOnlyList<Uri> GetUniqueAllowListUrls()
|
|
{
|
|
List<Uri> allowListUrls = new List<Uri>();
|
|
|
|
foreach (KeyValuePair<string, Group> group in _groups)
|
|
{
|
|
foreach (Uri allowListUrl in group.Value.AllowListUrls)
|
|
{
|
|
if (!allowListUrls.Contains(allowListUrl))
|
|
allowListUrls.Add(allowListUrl);
|
|
}
|
|
}
|
|
|
|
return allowListUrls;
|
|
}
|
|
|
|
private IReadOnlyList<Uri> GetUniqueBlockListUrls()
|
|
{
|
|
List<Uri> blockListUrls = new List<Uri>();
|
|
|
|
foreach (KeyValuePair<string, Group> group in _groups)
|
|
{
|
|
foreach (Uri blockListUrl in group.Value.BlockListUrls)
|
|
{
|
|
if (!blockListUrls.Contains(blockListUrl))
|
|
blockListUrls.Add(blockListUrl);
|
|
}
|
|
}
|
|
|
|
return blockListUrls;
|
|
}
|
|
|
|
private IReadOnlyList<Uri> GetUniqueRegexAllowListUrls()
|
|
{
|
|
List<Uri> regexAllowListUrls = new List<Uri>();
|
|
|
|
foreach (KeyValuePair<string, Group> group in _groups)
|
|
{
|
|
foreach (Uri regexAllowListUrl in group.Value.RegexAllowListUrls)
|
|
{
|
|
if (!regexAllowListUrls.Contains(regexAllowListUrl))
|
|
regexAllowListUrls.Add(regexAllowListUrl);
|
|
}
|
|
}
|
|
|
|
return regexAllowListUrls;
|
|
}
|
|
|
|
private IReadOnlyList<Uri> GetUniqueRegexBlockListUrls()
|
|
{
|
|
List<Uri> regexBlockListUrls = new List<Uri>();
|
|
|
|
foreach (KeyValuePair<string, Group> group in _groups)
|
|
{
|
|
foreach (Uri regexBlockListUrl in group.Value.RegexBlockListUrls)
|
|
{
|
|
if (!regexBlockListUrls.Contains(regexBlockListUrl))
|
|
regexBlockListUrls.Add(regexBlockListUrl);
|
|
}
|
|
}
|
|
|
|
return regexBlockListUrls;
|
|
}
|
|
|
|
private IReadOnlyList<Uri> GetUniqueAdblockListUrls()
|
|
{
|
|
List<Uri> adblockListUrls = new List<Uri>();
|
|
|
|
foreach (KeyValuePair<string, Group> group in _groups)
|
|
{
|
|
foreach (Uri adblockListUrl in group.Value.AdblockListUrls)
|
|
{
|
|
if (!adblockListUrls.Contains(adblockListUrl))
|
|
adblockListUrls.Add(adblockListUrl);
|
|
}
|
|
}
|
|
|
|
return adblockListUrls;
|
|
}
|
|
|
|
private static IReadOnlyList<Uri> GetAllUniqueListUrls(IReadOnlyDictionary<Group, int> groups)
|
|
{
|
|
List<Uri> listUrls = new List<Uri>();
|
|
|
|
foreach (KeyValuePair<Group, int> group in groups)
|
|
{
|
|
foreach (Uri allowListUrl in group.Key.AllowListUrls)
|
|
{
|
|
if (!listUrls.Contains(allowListUrl))
|
|
listUrls.Add(allowListUrl);
|
|
}
|
|
|
|
foreach (Uri blockListUrl in group.Key.BlockListUrls)
|
|
{
|
|
if (!listUrls.Contains(blockListUrl))
|
|
listUrls.Add(blockListUrl);
|
|
}
|
|
|
|
foreach (Uri regexAllowListUrl in group.Key.RegexAllowListUrls)
|
|
{
|
|
if (!listUrls.Contains(regexAllowListUrl))
|
|
listUrls.Add(regexAllowListUrl);
|
|
}
|
|
|
|
foreach (Uri regexBlockListUrl in group.Key.RegexBlockListUrls)
|
|
{
|
|
if (!listUrls.Contains(regexBlockListUrl))
|
|
listUrls.Add(regexBlockListUrl);
|
|
}
|
|
|
|
foreach (Uri adblockListUrl in group.Key.AdblockListUrls)
|
|
{
|
|
if (!listUrls.Contains(adblockListUrl))
|
|
listUrls.Add(adblockListUrl);
|
|
}
|
|
}
|
|
|
|
return listUrls;
|
|
}
|
|
|
|
private void LoadZones(List<Uri> updatedAllowListUrls, List<Uri> updatedBlockListUrls, List<Uri> updatedRegexAllowListUrls, List<Uri> updatedRegexBlockListUrls, List<Uri> updatedAdblockListUrls)
|
|
{
|
|
Dictionary<Uri, Queue<string>> allowCache = new Dictionary<Uri, Queue<string>>();
|
|
Dictionary<Uri, Queue<string>> blockCache = new Dictionary<Uri, Queue<string>>();
|
|
|
|
foreach (KeyValuePair<string, Group> group in _groups)
|
|
{
|
|
bool loadAllowList = ListContainsAnyItem(group.Value.AllowListUrls, updatedAllowListUrls);
|
|
bool loadBlockList = ListContainsAnyItem(group.Value.BlockListUrls, updatedBlockListUrls);
|
|
bool loadRegexAllowList = ListContainsAnyItem(group.Value.RegexAllowListUrls, updatedRegexAllowListUrls);
|
|
bool loadRegexBlockList = ListContainsAnyItem(group.Value.RegexBlockListUrls, updatedRegexBlockListUrls);
|
|
bool loadAdblockList = ListContainsAnyItem(group.Value.AdblockListUrls, updatedAdblockListUrls);
|
|
|
|
LoadListZones(allowCache, blockCache, group.Value, loadAllowList, loadBlockList, loadRegexAllowList, loadRegexBlockList, loadAdblockList);
|
|
}
|
|
}
|
|
|
|
private void LoadListZones(Dictionary<Uri, Queue<string>> allowCache, Dictionary<Uri, Queue<string>> blockCache, Group group, bool loadAllowList, bool loadBlockList, bool loadRegexAllowList, bool loadRegexBlockList, bool loadAdblockList)
|
|
{
|
|
if (loadAdblockList)
|
|
{
|
|
loadAllowList = true;
|
|
loadBlockList = true;
|
|
}
|
|
|
|
Dictionary<Uri, Queue<string>> allAllowListQueues = new Dictionary<Uri, Queue<string>>();
|
|
Dictionary<Uri, Queue<string>> allBlockListQueues = new Dictionary<Uri, Queue<string>>();
|
|
Dictionary<Uri, Queue<string>> allRegexAllowListQueues = new Dictionary<Uri, Queue<string>>();
|
|
Dictionary<Uri, Queue<string>> allRegexBlockListQueues = new Dictionary<Uri, Queue<string>>();
|
|
|
|
if (loadAllowList)
|
|
{
|
|
//read all allow lists in a queue
|
|
foreach (Uri allowListUrl in group.AllowListUrls)
|
|
{
|
|
if (allAllowListQueues.ContainsKey(allowListUrl))
|
|
continue;
|
|
|
|
if (!allowCache.TryGetValue(allowListUrl, out Queue<string> allowListQueue))
|
|
{
|
|
allowListQueue = ReadListFile(allowListUrl, true);
|
|
allowCache.Add(allowListUrl, allowListQueue);
|
|
}
|
|
|
|
allAllowListQueues.Add(allowListUrl, allowListQueue);
|
|
}
|
|
}
|
|
|
|
if (loadBlockList)
|
|
{
|
|
//read all block lists in a queue
|
|
foreach (Uri blockListUrl in group.BlockListUrls)
|
|
{
|
|
if (allBlockListQueues.ContainsKey(blockListUrl))
|
|
continue;
|
|
|
|
if (!blockCache.TryGetValue(blockListUrl, out Queue<string> blockListQueue))
|
|
{
|
|
blockListQueue = ReadListFile(blockListUrl, false);
|
|
blockCache.Add(blockListUrl, blockListQueue);
|
|
}
|
|
|
|
allBlockListQueues.Add(blockListUrl, blockListQueue);
|
|
}
|
|
}
|
|
|
|
if (loadAdblockList)
|
|
{
|
|
//read all adblock lists in queue
|
|
foreach (Uri adblockListUrl in group.AdblockListUrls)
|
|
{
|
|
if (!allowCache.TryGetValue(adblockListUrl, out Queue<string> allowListQueue) & !blockCache.TryGetValue(adblockListUrl, out Queue<string> blockListQueue))
|
|
{
|
|
ReadAdblockListFile(adblockListUrl, out allowListQueue, out blockListQueue);
|
|
|
|
allowCache.Add(adblockListUrl, allowListQueue);
|
|
blockCache.Add(adblockListUrl, blockListQueue);
|
|
}
|
|
|
|
allAllowListQueues.Add(adblockListUrl, allowListQueue);
|
|
allBlockListQueues.Add(adblockListUrl, blockListQueue);
|
|
}
|
|
}
|
|
|
|
if (loadRegexAllowList)
|
|
{
|
|
//read all allow lists in a queue
|
|
foreach (Uri regexAllowListUrl in group.RegexAllowListUrls)
|
|
{
|
|
if (allRegexAllowListQueues.ContainsKey(regexAllowListUrl))
|
|
continue;
|
|
|
|
if (!allowCache.TryGetValue(regexAllowListUrl, out Queue<string> regexAllowListQueue))
|
|
{
|
|
regexAllowListQueue = ReadRegexListFile(regexAllowListUrl, true);
|
|
allowCache.Add(regexAllowListUrl, regexAllowListQueue);
|
|
}
|
|
|
|
allRegexAllowListQueues.Add(regexAllowListUrl, regexAllowListQueue);
|
|
}
|
|
}
|
|
|
|
if (loadRegexBlockList)
|
|
{
|
|
//read all regex block lists in a queue
|
|
foreach (Uri regexBlockListUrl in group.RegexBlockListUrls)
|
|
{
|
|
if (allRegexBlockListQueues.ContainsKey(regexBlockListUrl))
|
|
continue;
|
|
|
|
if (!blockCache.TryGetValue(regexBlockListUrl, out Queue<string> regexBlockListQueue))
|
|
{
|
|
regexBlockListQueue = ReadRegexListFile(regexBlockListUrl, false);
|
|
blockCache.Add(regexBlockListUrl, regexBlockListQueue);
|
|
}
|
|
|
|
allRegexBlockListQueues.Add(regexBlockListUrl, regexBlockListQueue);
|
|
}
|
|
}
|
|
|
|
//load block list zone
|
|
if (loadAllowList)
|
|
group.LoadAllowListZone(allAllowListQueues);
|
|
|
|
if (loadBlockList)
|
|
group.LoadBlockListZone(allBlockListQueues);
|
|
|
|
//load regex block list zone
|
|
if (loadRegexAllowList)
|
|
group.LoadRegexAllowListZone(allRegexAllowListQueues);
|
|
|
|
if (loadRegexBlockList)
|
|
group.LoadRegexBlockListZone(allRegexBlockListQueues);
|
|
|
|
_dnsServer.WriteLog("Advanced Blocking app loaded all zones successfully for group: " + group.Name);
|
|
}
|
|
|
|
private static bool ListsEquals<T>(IReadOnlyList<T> list1, IReadOnlyList<T> list2)
|
|
{
|
|
if (list1.Count != list2.Count)
|
|
return false;
|
|
|
|
foreach (T item in list1)
|
|
{
|
|
if (!list2.Contains(item))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool ListContainsAnyItem<T>(IReadOnlyList<T> list, IReadOnlyList<T> items)
|
|
{
|
|
foreach (T item in list)
|
|
{
|
|
if (items.Contains(item))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
|
|
_soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, "hostadmin." + _dnsServer.ServerDomain, 1, 14400, 3600, 604800, 60);
|
|
_nsRecord = new DnsNSRecordData(_dnsServer.ServerDomain);
|
|
|
|
dynamic jsonConfig = JsonConvert.DeserializeObject(config);
|
|
|
|
_enableBlocking = jsonConfig.enableBlocking.Value;
|
|
_blockListUrlUpdateIntervalHours = Convert.ToInt32(jsonConfig.blockListUrlUpdateIntervalHours.Value);
|
|
|
|
{
|
|
Dictionary<NetworkAddress, string> networkGroupMap = new Dictionary<NetworkAddress, string>();
|
|
|
|
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;
|
|
}
|
|
|
|
bool cachedListFileMissing = false;
|
|
|
|
{
|
|
const int LOAD_ALLOW_LIST_ZONE = 1;
|
|
const int LOAD_BLOCK_LIST_ZONE = 2;
|
|
const int LOAD_REGEX_ALLOW_LIST_ZONE = 4;
|
|
const int LOAD_REGEX_BLOCK_LIST_ZONE = 8;
|
|
const int LOAD_ADBLOCK_LIST_ZONE = 16;
|
|
|
|
Dictionary<Group, int> updatedGroups = new Dictionary<Group, int>();
|
|
Dictionary<string, Group> groups = new Dictionary<string, Group>();
|
|
|
|
foreach (dynamic jsonGroup in jsonConfig.groups)
|
|
{
|
|
Group group = new Group(this, jsonGroup);
|
|
|
|
if ((_groups is not null) && _groups.TryGetValue(group.Name, out Group existingGroup))
|
|
{
|
|
int loadFlags = 0;
|
|
|
|
if (!ListsEquals(group.AllowListUrls, existingGroup.AllowListUrls))
|
|
loadFlags |= LOAD_ALLOW_LIST_ZONE;
|
|
|
|
if (!ListsEquals(group.BlockListUrls, existingGroup.BlockListUrls))
|
|
loadFlags |= LOAD_BLOCK_LIST_ZONE;
|
|
|
|
if (!ListsEquals(group.RegexAllowListUrls, existingGroup.RegexAllowListUrls))
|
|
loadFlags |= LOAD_REGEX_ALLOW_LIST_ZONE;
|
|
|
|
if (!ListsEquals(group.RegexBlockListUrls, existingGroup.RegexBlockListUrls))
|
|
loadFlags |= LOAD_REGEX_BLOCK_LIST_ZONE;
|
|
|
|
if (!ListsEquals(group.AdblockListUrls, existingGroup.AdblockListUrls))
|
|
loadFlags |= LOAD_ADBLOCK_LIST_ZONE;
|
|
|
|
if (loadFlags > 0)
|
|
updatedGroups.Add(existingGroup, loadFlags);
|
|
|
|
existingGroup.EnableBlocking = group.EnableBlocking;
|
|
existingGroup.AllowTxtBlockingReport = group.AllowTxtBlockingReport;
|
|
existingGroup.BlockAsNxDomain = group.BlockAsNxDomain;
|
|
existingGroup.ARecords = group.ARecords;
|
|
existingGroup.AAAARecords = group.AAAARecords;
|
|
|
|
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;
|
|
|
|
existingGroup.AdblockListUrls = group.AdblockListUrls;
|
|
|
|
groups.TryAdd(existingGroup.Name, existingGroup);
|
|
}
|
|
else
|
|
{
|
|
updatedGroups.Add(group, LOAD_ALLOW_LIST_ZONE | LOAD_BLOCK_LIST_ZONE | LOAD_REGEX_ALLOW_LIST_ZONE | LOAD_REGEX_BLOCK_LIST_ZONE | LOAD_ADBLOCK_LIST_ZONE);
|
|
groups.TryAdd(group.Name, group);
|
|
}
|
|
}
|
|
|
|
_groups = groups;
|
|
|
|
if (updatedGroups.Count > 0)
|
|
{
|
|
foreach (Uri listUrl in GetAllUniqueListUrls(updatedGroups))
|
|
{
|
|
if (!File.Exists(GetListFilePath(listUrl)))
|
|
{
|
|
cachedListFileMissing = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!cachedListFileMissing)
|
|
{
|
|
Task.Run(delegate ()
|
|
{
|
|
Dictionary<Uri, Queue<string>> allowCache = new Dictionary<Uri, Queue<string>>();
|
|
Dictionary<Uri, Queue<string>> blockCache = new Dictionary<Uri, Queue<string>>();
|
|
|
|
foreach (KeyValuePair<Group, int> group in updatedGroups)
|
|
{
|
|
bool loadAllowList = (group.Value & LOAD_ALLOW_LIST_ZONE) > 0;
|
|
bool loadBlockList = (group.Value & LOAD_BLOCK_LIST_ZONE) > 0;
|
|
bool loadRegexAllowList = (group.Value & LOAD_REGEX_ALLOW_LIST_ZONE) > 0;
|
|
bool loadRegexBlockList = (group.Value & LOAD_REGEX_BLOCK_LIST_ZONE) > 0;
|
|
bool loadAdblockList = (group.Value & LOAD_ADBLOCK_LIST_ZONE) > 0;
|
|
|
|
LoadListZones(allowCache, blockCache, group.Key, loadAllowList, loadBlockList, loadRegexAllowList, loadRegexBlockList, loadAdblockList);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_blockListUrlUpdateTimer is null)
|
|
{
|
|
if (!cachedListFileMissing)
|
|
FindAndSetBlockListUrlLastUpdatedOn();
|
|
|
|
_blockListUrlUpdateTimer = new Timer(BlockListUrlUpdateTimerCallbackAsync, null, Timeout.Infinite, Timeout.Infinite);
|
|
_blockListUrlUpdateTimer.Change(BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL);
|
|
}
|
|
else
|
|
{
|
|
if (cachedListFileMissing)
|
|
{
|
|
//force update
|
|
_blockListUrlLastUpdatedOn = DateTime.MinValue;
|
|
_blockListUrlUpdateTimer.Change(BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL);
|
|
}
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)
|
|
{
|
|
if (!_enableBlocking)
|
|
return Task.FromResult<DnsDatagram>(null);
|
|
|
|
IPAddress remoteIP = remoteEP.Address;
|
|
string groupName = null;
|
|
|
|
foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap)
|
|
{
|
|
if (entry.Key.Contains(remoteIP))
|
|
{
|
|
groupName = entry.Value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableBlocking)
|
|
return Task.FromResult<DnsDatagram>(null);
|
|
|
|
DnsQuestionRecord question = request.Question[0];
|
|
|
|
IReadOnlyList<Uri> blockListUrls = group.IsZoneBlocked(question.Name, out string blockedDomain, out string blockedRegex);
|
|
if (blockListUrls is null)
|
|
return Task.FromResult<DnsDatagram>(null);
|
|
|
|
if (group.AllowTxtBlockingReport && (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 DnsTXTRecordData("source=advanced-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 DnsTXTRecordData("source=advanced-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 DnsTXTRecordData("source=advanced-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 DnsTXTRecordData("source=advanced-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<DnsResourceRecord> answer = null;
|
|
IReadOnlyList<DnsResourceRecord> authority = null;
|
|
|
|
if (group.BlockAsNxDomain)
|
|
{
|
|
rcode = DnsResponseCode.NxDomain;
|
|
|
|
if (blockedDomain is null)
|
|
blockedDomain = question.Name;
|
|
|
|
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<DnsResourceRecord> rrList = new List<DnsResourceRecord>(group.ARecords.Count);
|
|
|
|
foreach (DnsARecordData record in group.ARecords)
|
|
rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 60, record));
|
|
|
|
answer = rrList;
|
|
}
|
|
break;
|
|
|
|
case DnsResourceRecordType.AAAA:
|
|
{
|
|
List<DnsResourceRecord> rrList = new List<DnsResourceRecord>(group.AAAARecords.Count);
|
|
|
|
foreach (DnsAAAARecordData record in group.AAAARecords)
|
|
rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 60, record));
|
|
|
|
answer = rrList;
|
|
}
|
|
break;
|
|
|
|
case DnsResourceRecordType.NS:
|
|
if (blockedDomain is null)
|
|
blockedDomain = question.Name;
|
|
|
|
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;
|
|
|
|
case DnsResourceRecordType.SOA:
|
|
if (blockedDomain is null)
|
|
blockedDomain = question.Name;
|
|
|
|
answer = new DnsResourceRecord[] { new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) };
|
|
break;
|
|
|
|
default:
|
|
if (blockedDomain is null)
|
|
blockedDomain = question.Name;
|
|
|
|
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 App _app;
|
|
|
|
readonly string _name;
|
|
bool _enableBlocking;
|
|
bool _allowTxtBlockingReport;
|
|
bool _blockAsNxDomain;
|
|
|
|
IReadOnlyCollection<DnsARecordData> _aRecords;
|
|
IReadOnlyCollection<DnsAAAARecordData> _aaaaRecords;
|
|
|
|
IReadOnlyDictionary<string, object> _allowed;
|
|
IReadOnlyDictionary<string, object> _blocked;
|
|
IReadOnlyList<Uri> _allowListUrls;
|
|
IReadOnlyList<Uri> _blockListUrls;
|
|
|
|
IReadOnlyList<Regex> _allowedRegex;
|
|
IReadOnlyList<Regex> _blockedRegex;
|
|
IReadOnlyList<Uri> _regexAllowListUrls;
|
|
IReadOnlyList<Uri> _regexBlockListUrls;
|
|
|
|
IReadOnlyList<Uri> _adblockListUrls;
|
|
|
|
IReadOnlyDictionary<string, List<Uri>> _allowListZone = new Dictionary<string, List<Uri>>(0);
|
|
IReadOnlyDictionary<string, List<Uri>> _blockListZone = new Dictionary<string, List<Uri>>(0);
|
|
|
|
IReadOnlyList<RegexItem> _regexAllowListZone = Array.Empty<RegexItem>();
|
|
IReadOnlyList<RegexItem> _regexBlockListZone = Array.Empty<RegexItem>();
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
public Group(App app, dynamic jsonGroup)
|
|
{
|
|
_app = app;
|
|
|
|
_name = jsonGroup.name.Value;
|
|
_enableBlocking = jsonGroup.enableBlocking.Value;
|
|
_allowTxtBlockingReport = jsonGroup.allowTxtBlockingReport.Value;
|
|
_blockAsNxDomain = jsonGroup.blockAsNxDomain.Value;
|
|
|
|
{
|
|
List<DnsARecordData> aRecords = new List<DnsARecordData>();
|
|
List<DnsAAAARecordData> aaaaRecords = new List<DnsAAAARecordData>();
|
|
|
|
foreach (dynamic jsonBlockingAddress in jsonGroup.blockingAddresses)
|
|
{
|
|
string strAddress = jsonBlockingAddress.Value;
|
|
|
|
if (IPAddress.TryParse(strAddress, out IPAddress address))
|
|
{
|
|
switch (address.AddressFamily)
|
|
{
|
|
case AddressFamily.InterNetwork:
|
|
aRecords.Add(new DnsARecordData(address));
|
|
break;
|
|
|
|
case AddressFamily.InterNetworkV6:
|
|
aaaaRecords.Add(new DnsAAAARecordData(address));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_aRecords = aRecords;
|
|
_aaaaRecords = aaaaRecords;
|
|
}
|
|
|
|
_allowed = ReadJsonDomainArray(jsonGroup.allowed);
|
|
_blocked = ReadJsonDomainArray(jsonGroup.blocked);
|
|
_allowListUrls = ReadJsonUrlArray(jsonGroup.allowListUrls);
|
|
_blockListUrls = ReadJsonUrlArray(jsonGroup.blockListUrls);
|
|
|
|
_allowedRegex = ReadJsonRegexArray(jsonGroup.allowedRegex);
|
|
_blockedRegex = ReadJsonRegexArray(jsonGroup.blockedRegex);
|
|
_regexAllowListUrls = ReadJsonUrlArray(jsonGroup.regexAllowListUrls);
|
|
_regexBlockListUrls = ReadJsonUrlArray(jsonGroup.regexBlockListUrls);
|
|
|
|
_adblockListUrls = ReadJsonUrlArray(jsonGroup.adblockListUrls);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region private
|
|
|
|
private static IReadOnlyDictionary<string, object> ReadJsonDomainArray(dynamic jsonDomainArray)
|
|
{
|
|
Dictionary<string, object> domains = new Dictionary<string, object>(jsonDomainArray.Count);
|
|
|
|
foreach (dynamic jsonDomain in jsonDomainArray)
|
|
domains.TryAdd(jsonDomain.Value, null);
|
|
|
|
return domains;
|
|
}
|
|
|
|
private static IReadOnlyList<Regex> ReadJsonRegexArray(dynamic jsonRegexArray)
|
|
{
|
|
List<Regex> regices = new List<Regex>(jsonRegexArray.Count);
|
|
|
|
foreach (dynamic jsonRegex in jsonRegexArray)
|
|
{
|
|
string regexPattern = jsonRegex.Value;
|
|
|
|
regices.Add(new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled));
|
|
}
|
|
|
|
return regices;
|
|
}
|
|
|
|
private static IReadOnlyList<Uri> ReadJsonUrlArray(dynamic jsonUrlArray)
|
|
{
|
|
List<Uri> urls = new List<Uri>(jsonUrlArray.Count);
|
|
|
|
foreach (dynamic jsonUrl in jsonUrlArray)
|
|
{
|
|
string strUrl = jsonUrl.Value;
|
|
|
|
urls.Add(new Uri(strUrl));
|
|
}
|
|
|
|
return urls;
|
|
}
|
|
|
|
private static bool IsZoneFound<T>(IReadOnlyDictionary<string, T> domains, string domain, out string foundZone, out T foundValue) where T : class
|
|
{
|
|
do
|
|
{
|
|
if (domains.TryGetValue(domain, out T value))
|
|
{
|
|
foundZone = domain;
|
|
foundValue = value;
|
|
return true;
|
|
}
|
|
|
|
domain = GetParentZone(domain);
|
|
}
|
|
while (domain is not null);
|
|
|
|
foundZone = null;
|
|
foundValue = null;
|
|
return false;
|
|
}
|
|
|
|
private static bool IsMatchFound(IReadOnlyList<Regex> regices, string domain, out string matchingPattern)
|
|
{
|
|
foreach (Regex regex in regices)
|
|
{
|
|
if (regex.IsMatch(domain))
|
|
{
|
|
//found pattern
|
|
matchingPattern = regex.ToString();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
matchingPattern = null;
|
|
return false;
|
|
}
|
|
|
|
private static bool IsMatchFound(IReadOnlyList<RegexItem> regices, string domain, out string matchingPattern, out IReadOnlyList<Uri> blockListUrls)
|
|
{
|
|
foreach (RegexItem regex in regices)
|
|
{
|
|
if (regex.Regex.IsMatch(domain))
|
|
{
|
|
//found pattern
|
|
matchingPattern = regex.Regex.ToString();
|
|
blockListUrls = regex.BlockListUrls;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
matchingPattern = null;
|
|
blockListUrls = null;
|
|
return false;
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, List<Uri>> LoadListZone(IReadOnlyList<Uri> listUrls, Dictionary<Uri, Queue<string>> allListQueues)
|
|
{
|
|
//select lists
|
|
Dictionary<Uri, Queue<string>> listQueues = new Dictionary<Uri, Queue<string>>(listUrls.Count);
|
|
int totalDomains = 0;
|
|
|
|
foreach (Uri listUrl in listUrls)
|
|
{
|
|
if (allListQueues.TryGetValue(listUrl, out Queue<string> listQueue))
|
|
{
|
|
totalDomains += listQueue.Count;
|
|
listQueues.Add(listUrl, listQueue);
|
|
}
|
|
}
|
|
|
|
//load list zone
|
|
Dictionary<string, List<Uri>> listZone = new Dictionary<string, List<Uri>>(totalDomains);
|
|
|
|
foreach (KeyValuePair<Uri, Queue<string>> listQueue in listQueues)
|
|
{
|
|
Queue<string> queue = listQueue.Value;
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
string domain = queue.Dequeue();
|
|
|
|
if (!listZone.TryGetValue(domain, out List<Uri> sourceListUrls))
|
|
{
|
|
sourceListUrls = new List<Uri>(2);
|
|
listZone.Add(domain, sourceListUrls);
|
|
}
|
|
|
|
sourceListUrls.Add(listQueue.Key);
|
|
}
|
|
}
|
|
|
|
return listZone;
|
|
}
|
|
|
|
private IReadOnlyList<RegexItem> LoadRegexListZone(IReadOnlyList<Uri> regexListUrls, Dictionary<Uri, Queue<string>> allRegexListQueues)
|
|
{
|
|
//select regex lists
|
|
Dictionary<Uri, Queue<string>> regexListQueues = new Dictionary<Uri, Queue<string>>(regexListUrls.Count);
|
|
int totalRegexPatterns = 0;
|
|
|
|
foreach (Uri regexListUrl in regexListUrls)
|
|
{
|
|
if (allRegexListQueues.TryGetValue(regexListUrl, out Queue<string> regexListQueue))
|
|
{
|
|
totalRegexPatterns += regexListQueue.Count;
|
|
regexListQueues.Add(regexListUrl, regexListQueue);
|
|
}
|
|
}
|
|
|
|
//load regex list patterns from queue
|
|
Dictionary<string, List<Uri>> allRegexPatterns = new Dictionary<string, List<Uri>>(totalRegexPatterns);
|
|
|
|
foreach (KeyValuePair<Uri, Queue<string>> regexListQueue in regexListQueues)
|
|
{
|
|
Queue<string> queue = regexListQueue.Value;
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
string regex = queue.Dequeue();
|
|
|
|
if (!allRegexPatterns.TryGetValue(regex, out List<Uri> sourceListUrls))
|
|
{
|
|
sourceListUrls = new List<Uri>(2);
|
|
allRegexPatterns.Add(regex, sourceListUrls);
|
|
}
|
|
|
|
sourceListUrls.Add(regexListQueue.Key);
|
|
}
|
|
}
|
|
|
|
//load regex list zone
|
|
List<RegexItem> regexListZone = new List<RegexItem>(totalRegexPatterns);
|
|
|
|
foreach (KeyValuePair<string, List<Uri>> regexPattern in allRegexPatterns)
|
|
{
|
|
try
|
|
{
|
|
Regex regex = new Regex(regexPattern.Key, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
|
|
|
|
regexListZone.Add(new RegexItem(regex, regexPattern.Value));
|
|
}
|
|
catch (RegexParseException ex)
|
|
{
|
|
_app._dnsServer.WriteLog(ex);
|
|
}
|
|
}
|
|
|
|
return regexListZone;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public
|
|
|
|
public void LoadAllowListZone(Dictionary<Uri, Queue<string>> allAllowListQueues)
|
|
{
|
|
List<Uri> listUrls = new List<Uri>();
|
|
|
|
listUrls.AddRange(_allowListUrls);
|
|
listUrls.AddRange(_adblockListUrls);
|
|
|
|
_allowListZone = LoadListZone(listUrls, allAllowListQueues);
|
|
}
|
|
|
|
public void LoadBlockListZone(Dictionary<Uri, Queue<string>> allBlockListQueues)
|
|
{
|
|
List<Uri> listUrls = new List<Uri>();
|
|
|
|
listUrls.AddRange(_blockListUrls);
|
|
listUrls.AddRange(_adblockListUrls);
|
|
|
|
_blockListZone = LoadListZone(listUrls, allBlockListQueues);
|
|
}
|
|
|
|
public void LoadRegexAllowListZone(Dictionary<Uri, Queue<string>> allRegexAllowListQueues)
|
|
{
|
|
_regexAllowListZone = LoadRegexListZone(_regexAllowListUrls, allRegexAllowListQueues);
|
|
}
|
|
|
|
public void LoadRegexBlockListZone(Dictionary<Uri, Queue<string>> allRegexBlockListQueues)
|
|
{
|
|
_regexBlockListZone = LoadRegexListZone(_regexBlockListUrls, allRegexBlockListQueues);
|
|
}
|
|
|
|
public IReadOnlyList<Uri> IsZoneBlocked(string domain, out string blockedDomain, out string blockedRegex)
|
|
{
|
|
domain = domain.ToLower();
|
|
|
|
//allowed, allow list zone, allowedRegex, regex allow list zone
|
|
if (IsZoneFound(_allowed, domain, out _, out _) || IsZoneFound(_allowListZone, domain, out _, out _) || IsMatchFound(_allowedRegex, domain, out _) || IsMatchFound(_regexAllowListZone, domain, out _, out _))
|
|
{
|
|
//found zone allowed
|
|
blockedDomain = null;
|
|
blockedRegex = null;
|
|
return null;
|
|
}
|
|
|
|
//blocked
|
|
if (IsZoneFound(_blocked, domain, out string foundZone1, out _))
|
|
{
|
|
//found zone blocked
|
|
blockedDomain = foundZone1;
|
|
blockedRegex = null;
|
|
return Array.Empty<Uri>();
|
|
}
|
|
|
|
//block list zone
|
|
if (IsZoneFound(_blockListZone, domain, out string foundZone2, out List<Uri> blockListUrls1))
|
|
{
|
|
//found zone blocked
|
|
blockedDomain = foundZone2;
|
|
blockedRegex = null;
|
|
return blockListUrls1;
|
|
}
|
|
|
|
//blockedRegex
|
|
if (IsMatchFound(_blockedRegex, domain, out string blockedPattern1))
|
|
{
|
|
//found pattern blocked
|
|
blockedDomain = null;
|
|
blockedRegex = blockedPattern1;
|
|
return Array.Empty<Uri>();
|
|
}
|
|
|
|
//regex block list zone
|
|
if (IsMatchFound(_regexBlockListZone, domain, out string blockedPattern2, out IReadOnlyList<Uri> blockListUrls2))
|
|
{
|
|
//found pattern blocked
|
|
blockedDomain = null;
|
|
blockedRegex = blockedPattern2;
|
|
return blockListUrls2;
|
|
}
|
|
|
|
blockedDomain = null;
|
|
blockedRegex = null;
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region properties
|
|
|
|
public string Name
|
|
{ get { return _name; } }
|
|
|
|
public bool EnableBlocking
|
|
{
|
|
get { return _enableBlocking; }
|
|
set { _enableBlocking = value; }
|
|
}
|
|
|
|
public bool AllowTxtBlockingReport
|
|
{
|
|
get { return _allowTxtBlockingReport; }
|
|
set { _allowTxtBlockingReport = value; }
|
|
}
|
|
|
|
public bool BlockAsNxDomain
|
|
{
|
|
get { return _blockAsNxDomain; }
|
|
set { _blockAsNxDomain = value; }
|
|
}
|
|
|
|
public IReadOnlyCollection<DnsARecordData> ARecords
|
|
{
|
|
get { return _aRecords; }
|
|
set { _aRecords = value; }
|
|
}
|
|
|
|
public IReadOnlyCollection<DnsAAAARecordData> AAAARecords
|
|
{
|
|
get { return _aaaaRecords; }
|
|
set { _aaaaRecords = value; }
|
|
}
|
|
|
|
public IReadOnlyDictionary<string, object> Allowed
|
|
{
|
|
get { return _allowed; }
|
|
set { _allowed = value; }
|
|
}
|
|
|
|
public IReadOnlyDictionary<string, object> Blocked
|
|
{
|
|
get { return _blocked; }
|
|
set { _blocked = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Uri> AllowListUrls
|
|
{
|
|
get { return _allowListUrls; }
|
|
set { _allowListUrls = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Uri> BlockListUrls
|
|
{
|
|
get { return _blockListUrls; }
|
|
set { _blockListUrls = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Regex> AllowedRegex
|
|
{
|
|
get { return _allowedRegex; }
|
|
set { _allowedRegex = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Regex> BlockedRegex
|
|
{
|
|
get { return _blockedRegex; }
|
|
set { _blockedRegex = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Uri> RegexBlockListUrls
|
|
{
|
|
get { return _regexBlockListUrls; }
|
|
set { _regexBlockListUrls = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Uri> RegexAllowListUrls
|
|
{
|
|
get { return _regexAllowListUrls; }
|
|
set { _regexAllowListUrls = value; }
|
|
}
|
|
|
|
public IReadOnlyList<Uri> AdblockListUrls
|
|
{
|
|
get { return _adblockListUrls; }
|
|
set { _adblockListUrls = value; }
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
class RegexItem
|
|
{
|
|
#region variables
|
|
|
|
readonly Regex _regex;
|
|
readonly IReadOnlyList<Uri> _blockListUrls;
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
public RegexItem(Regex regex, IReadOnlyList<Uri> blockListUrls)
|
|
{
|
|
_regex = regex;
|
|
_blockListUrls = blockListUrls;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region properties
|
|
|
|
public Regex Regex
|
|
{ get { return _regex; } }
|
|
|
|
public IReadOnlyList<Uri> BlockListUrls
|
|
{ get { return _blockListUrls; } }
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
}
|