From 9a4f0bced8699add4fe392d8e1c30db64aff5338 Mon Sep 17 00:00:00 2001 From: ValdikSS Date: Sun, 13 Sep 2020 22:11:20 +0300 Subject: [PATCH] Implement NXDOMAIN resolving and filtering --- README.md | 9 +-- config/config.sh | 3 + parse.sh | 8 +++ scripts/resolve-dns-nxdomain.py | 124 ++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100755 scripts/resolve-dns-nxdomain.py diff --git a/README.md b/README.md index c8a015e..e1e901e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Russian PAC file generator, light version Данный набор скриптов создаёт файл [автоконфигурации прокси](https://en.wikipedia.org/wiki/Proxy_auto-config) со списком сайтов, заблокированных на территории Российской Федерации Роскомнадзором и другими государственными органами, который можно использовать в браузерах, для автоматического проксирования заблокированных ресурсов. -Помимо основного назнчения скрипта (генерации PAC-файла), он также умеет создавать: +Помимо основного назначения скрипта (генерации PAC-файла), он также умеет создавать: * Файл клиентской конфигурации (client-config, CCD) с заблокированными диапазонами IP-адресов для OpenVPN; * Файл с заблокированными доменными зонами для Squid; @@ -19,15 +19,16 @@ Russian PAC file generator, light version * GNU AWK (gawk) * sipcalc * idn -* Python 3.4+ +* Python 3.6+ +* dnspython 2.0.0+ ### Конфигурационные файлы * **{in,ex}clude-{hosts,ips}-dist** — конфигурация дистрибутива, предназначена для изменения автором репозитория; * **{in,ex}clude-{hosts,ips}-custom** — пользовательская конфигурация, предназначена для изменения конечным пользователем скрипта; * **exclude-regexp-dist.awk** — файл с различным заблокированным «мусором», раздувающим PAC-файл: зеркалами сайтов, неработающими сайтами, и т.д. -* **config.sh** — файл с адресами прокси. +* **config.sh** — файл с адресами прокси и прочей конфигурацией. ### Установка и запуск -Склонируйте git-репозиторий, отредактируйте **doall.sh** и **process.sh** под собственные нужды, запустите **doall.sh**. +Склонируйте git-репозиторий, отредактируйте **config/config.sh**, **doall.sh** и **process.sh** под собственные нужды, запустите **doall.sh**. diff --git a/config/config.sh b/config/config.sh index 98e9346..09427b1 100755 --- a/config/config.sh +++ b/config/config.sh @@ -8,3 +8,6 @@ PACPROXYHOST='proxy-nossl.antizapret.prostovpn.org:29976' PACFILE="result/proxy-host-ssl.pac" PACFILE_NOSSL="result/proxy-host-nossl.pac" + +# Perform DNS resolving to detect and filter non-existent domains +RESOLVE_NXDOMAIN="yes" diff --git a/parse.sh b/parse.sh index daf6713..7eda4ee 100755 --- a/parse.sh +++ b/parse.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +source config/config.sh + HERE="$(dirname "$(readlink -f "${0}")")" cd "$HERE" @@ -19,6 +21,12 @@ sort -u temp/include-hosts.txt result/hostlist_original.txt > temp/hostlist_orig awk -f scripts/getzones.awk temp/hostlist_original_with_include.txt | grep -v -F -x -f temp/exclude-hosts.txt | sort -u > result/hostlist_zones.txt +if [[ "$RESOLVE_NXDOMAIN" == "yes" ]]; +then + scripts/resolve-dns-nxdomain.py result/hostlist_zones.txt >> temp/exclude-hosts.txt + awk -f scripts/getzones.awk temp/hostlist_original_with_include.txt | grep -v -F -x -f temp/exclude-hosts.txt | sort -u > result/hostlist_zones.txt +fi + # Generate a list of IP addresses awk -F';' '$1 ~ /\// {print $1}' temp/list.csv | grep -P '([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}' -o | sort -Vu > result/iplist_special_range.txt diff --git a/scripts/resolve-dns-nxdomain.py b/scripts/resolve-dns-nxdomain.py new file mode 100755 index 0000000..3adf1bd --- /dev/null +++ b/scripts/resolve-dns-nxdomain.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +import sys +import os +import asyncio +import dns.resolver +import dns.asyncresolver +import dns.exception +import dns._asyncio_backend + +# DNS timeout (in seconds) for the initial DNS resolving pass +INITIAL_PASS_TIMEOUT = 3 +# Number of concurrent resolving 'threads' for initial pass +INITIAL_PASS_CONCURRENCY = 100 + +# DNS timeout (in seconds) for the final (second) DNS resolving pass +FINAL_PASS_TIMEOUT = 10 +# Number of concurrent resolving 'threads' for final pass +FINAL_PASS_CONCURRENCY = 35 + + +class AZResolver(dns.asyncresolver.Resolver): + def __init__(self, *args, **kwargs): + self.limitConcurrency(25) # default limit + super().__init__(*args, **kwargs) + + def limitConcurrency(self, count): + self.limitingsemaphore = asyncio.Semaphore(count) + + async def nxresolve(self, domain): + async with self.limitingsemaphore: + try: + #print(domain, file=sys.stderr) + await self.resolve(domain) + + except (dns.exception.Timeout, dns.resolver.NXDOMAIN, + dns.resolver.YXDOMAIN, dns.resolver.NoAnswer, + dns.resolver.NoNameservers): + return domain + +async def runTasksWithProgress(tasks): + progress = 0 + old_progress = 0 + ret = [] + + for task in asyncio.as_completed(tasks): + ret.append(await task) + progress = int(len(ret) / len(tasks) * 100) + if old_progress < progress: + print("{}%...".format(progress), end='\r', file=sys.stderr, flush=True) + old_progress = progress + print(file=sys.stderr) + return ret + +async def main(): + if len(sys.argv) != 2: + print("Incorrect arguments!") + sys.exit(1) + + r = AZResolver() + r.limitConcurrency(INITIAL_PASS_CONCURRENCY) + r.timeout = INITIAL_PASS_TIMEOUT + r.lifetime = INITIAL_PASS_TIMEOUT + + # Load domain file list and schedule resolving + tasks = [] + try: + with open(sys.argv[1], 'rb') as domainlist: + for domain in domainlist: + tasks.append(asyncio.ensure_future(r.nxresolve(domain.decode().strip()))) + except OSError as e: + print("Can't open file", sys.argv[1], e, file=sys.stderr) + sys.exit(2) + + print("Loaded list of {} elements, resolving NXDOMAINS".format(len(tasks)), file=sys.stderr) + #sys.exit(0) + + try: + # Resolve domains, first try + nxresolved_first = await runTasksWithProgress(tasks) + nxresolved_first = list(filter(None, nxresolved_first)) + + print("Got {} broken domains, trying to resolve them again " + "to make sure".format(len(nxresolved_first)), file=sys.stderr) + + # Second try + tasks = [] + r.limitConcurrency(FINAL_PASS_CONCURRENCY) + r.timeout = FINAL_PASS_TIMEOUT + r.lifetime = FINAL_PASS_TIMEOUT + + for domain in nxresolved_first: + tasks.append(asyncio.ensure_future(r.nxresolve(domain))) + nxresolved_second = await runTasksWithProgress(tasks) + nxresolved_second = list(filter(None, nxresolved_second)) + + print("Finally, got {} broken domains".format(len(nxresolved_second)), file=sys.stderr) + for domain in nxresolved_second: + print(domain) + + except (SystemExit, KeyboardInterrupt): + for task in tasks: + task.cancel() + + +if __name__ == '__main__': + if dns.__version__ == '2.0.0': + # Monkey-patch dnspython 2.0.0 bug #572 + # https://github.com/rthalley/dnspython/issues/572 + class monkeypatched_DatagramProtocol(dns._asyncio_backend._DatagramProtocol): + def error_received(self, exc): # pragma: no cover + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_exception(exc) + + def connection_lost(self, exc): + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_exception(exc) + + dns._asyncio_backend._DatagramProtocol = monkeypatched_DatagramProtocol + + try: + asyncio.run(main()) + except (SystemExit, KeyboardInterrupt): + sys.exit(3)