diff --git a/README.md b/README.md index 75f6129..b887da2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@
Что нового (нажать, чтобы открыть) +- Реворк работы с DNS серверами. Прогрессбар. Разделение файла на части для некоторых форматов. Обновлена утилита Сonvert. - Keenetic BAT формат сохранения. Небольшие изменения в интерфейсе. Некоторые доработки/улучшения. - Доабвлены некоторые [оналйн кинотеатры](https://github.com/Ground-Zerro/DomainMapper/blob/main/platforms/dns-onlinetheater.txt). Запрос @Andrey_schumacher - Добавлены списки от [ITDog](https://t.me/itdoginfo/36). @@ -66,6 +67,7 @@ - Агрегация маршрутов в /16 (255.255.0.0) и /24 (255.255.255.0) подсети. Комбинированный режим /24 + /32. - Фильтрация IP-адресов Cloudflare (опционально). - Множество форматов сохранения результата. +- Разделение больших файлов на части для некоторых форматов. **Ключевые особенности** @@ -173,6 +175,6 @@ curl -L -s "https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/refs/hea # ☕ Поддержка -Если проект оказался Вам полезен — можно поддержать автора: +Если проект оказался Вам полезен — можно поблагодарить автора: - [Поддержать на Boosty](https://boosty.to/ground_zerro) diff --git a/Windows/Win.bat b/Windows/Win.bat index 2e1cd1e..5343b5d 100644 --- a/Windows/Win.bat +++ b/Windows/Win.bat @@ -1,96 +1,112 @@ -@echo off -setlocal enabledelayedexpansion -chcp 65001 > NUL - -REM Проверка Python 3 -:CheckPython -python --version 2>NUL | findstr /I "Python 3" >NUL -if ERRORLEVEL 1 ( - echo Python 3 не установлен. - choice /C YN /M "Установить?" - if ERRORLEVEL 2 ( - echo Без Python 3 ничего не получится... - pause - exit /b 1 - ) else ( - call :InstallPython - ) -) else ( - echo Python 3 установлен. -) -goto :CheckModules - -REM Инсталляция Python 3 -:InstallPython -echo Загрузка дистрибутива... -powershell -Command "if ($PSVersionTable.PSVersion.Major -ge 3) {Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.12.5/python-3.12.5-amd64.exe' -OutFile 'python_installer.exe'} else {Start-BitsTransfer -Source 'https://www.python.org/ftp/python/3.12.5/python-3.12.5-amd64.exe' -Destination 'python_installer.exe'}" - -REM Проверяем успешность загрузки -if not exist "python_installer.exe" ( - echo Ошибка загрузки установщика Python 3. - pause - exit /b 1 -) - -REM Установка Python 3 -echo Установка... -echo PS - не забудьте ее разрешить в соседнем окне -python_installer.exe /quiet InstallAllUsers=1 PrependPath=1 -del /q /f python_installer.exe - -REM Оповещение о перезапуске -echo. -echo Установка завершена, но требуется обновить окружение. -echo - закройте это окно и запустите скрипт снова. -pause -exit /b 0 - -REM Проверка и установка необходимых модулей Python -:CheckModules -set "modules=requests dnspython ipaddress configparser httpx colorama" -echo. -echo Проверка необходимых библиотек... - -for %%m in (%modules%) do ( - pip show %%m >NUL 2>&1 - if ERRORLEVEL 1 ( - echo Установка библиотеки %%m... - pip install %%m - if ERRORLEVEL 1 ( - echo Не удалось установить библиотеку %%m. Проверьте pip. - exit /b 1 - ) - ) -) - -goto :DownloadMain - -REM Загрузка и запуск main.py -:DownloadMain -echo Загрузка Domain Mapper... -powershell -Command "if ($PSVersionTable.PSVersion.Major -ge 3) {Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/main/main.py' -OutFile 'main.py'} else {Start-BitsTransfer -Source 'https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/main/main.py' -Destination 'main.py'}" - -if not exist "main.py" ( - echo Ошибка загрузки Domain Mapper. - pause - exit /b 1 -) - -cls -REM Запуск main.py -echo Запускаем... -python main.py -if ERRORLEVEL 1 ( - echo Ошибка выполнения main.py. - pause - del /q /f main.py - exit /b 1 -) - -move /y domain-ip-resolve.txt %UserProfile%\Desktop\domain-ip-resolve.txt -echo Программа завершена. -del /q /f main.py -endlocal -echo файл скопирован в %UserProfile%\Desktop\domain-ip-resolve.txt -pause -exit /b 0 +@echo off +setlocal enabledelayedexpansion +chcp 65001 > NUL + +REM Проверка Python 3 +:CheckPython +python --version 2>NUL | findstr /I "Python 3" >NUL +if ERRORLEVEL 1 ( + echo Python 3 не установлен. + choice /C YN /M "Установить?" + if ERRORLEVEL 2 ( + echo Без Python 3 ничего не получится... + pause + exit /b 1 + ) else ( + call :InstallPython + ) +) else ( + echo Python 3 установлен. +) +goto :CheckModules + +REM Инсталляция Python 3 +:InstallPython +echo Загрузка дистрибутива... +powershell -Command "if ($PSVersionTable.PSVersion.Major -ge 3) {Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.12.5/python-3.12.5-amd64.exe' -OutFile 'python_installer.exe'} else {Start-BitsTransfer -Source 'https://www.python.org/ftp/python/3.12.5/python-3.12.5-amd64.exe' -Destination 'python_installer.exe'}" + +REM Проверяем успешность загрузки +if not exist "python_installer.exe" ( + echo Ошибка загрузки установщика Python 3. + pause + exit /b 1 +) + +REM Установка Python 3 +echo Установка... +echo PS - не забудьте ее разрешить в соседнем окне +python_installer.exe /quiet InstallAllUsers=1 PrependPath=1 +del /q /f python_installer.exe + +REM Оповещение о перезапуске +echo. +echo Установка завершена, но требуется обновить окружение. +echo - закройте это окно и запустите скрипт снова. +pause +exit /b 0 + +REM Проверка и установка необходимых модулей Python +:CheckModules +set "modules=dnspython httpx colorama tqdm" +echo. +echo Проверка необходимых библиотек... + +for %%m in (%modules%) do ( + pip show %%m >NUL 2>&1 + if ERRORLEVEL 1 ( + echo Установка библиотеки %%m... + pip install %%m + if ERRORLEVEL 1 ( + echo Не удалось установить библиотеку %%m. Проверьте pip. + exit /b 1 + ) + ) +) + +goto :DownloadMain + +REM Загрузка и запуск main.py +:DownloadMain +echo Загрузка Domain Mapper... +powershell -Command "if ($PSVersionTable.PSVersion.Major -ge 3) {Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/main/main.py' -OutFile 'main.py'} else {Start-BitsTransfer -Source 'https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/main/main.py' -Destination 'main.py'}" + +if not exist "main.py" ( + echo Ошибка загрузки Domain Mapper. + pause + exit /b 1 +) + +cls +REM Запуск main.py +echo Запускаем... +python main.py +if ERRORLEVEL 1 ( + echo Ошибка выполнения main.py. + pause + del /q /f main.py + exit /b 1 +) + +echo Копирование файлов на рабочий стол... + +if exist domain-ip-resolve.txt ( + move /y domain-ip-resolve.txt %UserProfile%\Desktop\domain-ip-resolve.txt + echo Файл скопирован в %UserProfile%\Desktop\domain-ip-resolve.txt +) else ( + echo Поиск разделенных файлов... + set "found=0" + for %%f in (domain-ip-resolve_p*.txt) do ( + move /y "%%f" "%UserProfile%\Desktop\%%f" + echo Файл %%f скопирован на рабочий стол + set "found=1" + ) + if "!found!"=="0" ( + echo Не найдено файлов для копирования. + ) +) + +echo Программа завершена. +del /q /f main.py +endlocal +pause +exit /b 0 diff --git a/config.ini b/config.ini index e2fbace..cdb616f 100644 --- a/config.ini +++ b/config.ini @@ -10,7 +10,7 @@ service = dnsserver = # Исключить Cloudflare IP (yes/no) -cloudflare = +cloudflare = # Агрегация подсетей (16, 24, mix, no) subnet = @@ -18,8 +18,9 @@ subnet = # Имя выходного файла filename = domain-ip-resolve.txt -# Количество потоков (по умолчанию 20) -threads = +# Лимит запросов к каждому DNS серверу (запросов в секунду, по умолчанию 50) +# Контролирует максимальное количество DNS запросов к одному серверу в секунду +rate_limit = 50 # Формат результата (ip, unix, win, mikrotik, ovpn, wireguard, cidr, keenetic bat и т.д.) filetype = diff --git a/domain-ip-resolve.txt b/domain-ip-resolve.txt new file mode 100644 index 0000000..8f911ad --- /dev/null +++ b/domain-ip-resolve.txt @@ -0,0 +1,325 @@ +103.136.41.171 +104.192.140.24 +104.192.140.25 +104.192.140.26 +104.244.43.131 +104.247.81.99 +107.22.247.231 +108.181.60.131 +116.202.120.165 +116.202.120.166 +12.5.163.52 +128.150.221.244 +13.107.21.200 +13.107.213.46 +13.107.246.46 +13.107.253.51 +13.216.68.41 +13.227.180.4 +13.248.188.196 +13.249.91.105 +13.249.91.27 +13.249.91.55 +13.249.91.77 +13.33.252.118 +13.33.252.123 +13.33.252.125 +13.33.252.128 +13.33.252.129 +13.33.252.35 +13.33.252.55 +13.33.252.61 +13.33.252.62 +13.33.252.7 +13.33.252.87 +13.33.252.90 +13.35.93.102 +13.35.93.103 +13.35.93.12 +13.35.93.125 +13.83.13.216 +13.91.95.74 +138.68.89.1 +140.82.112.21 +141.95.92.64 +142.229.226.127 +142.229.246.30 +142.250.196.104 +142.250.196.110 +142.250.207.46 +143.166.136.12 +143.166.30.172 +145.239.35.18 +146.75.112.157 +146.75.112.159 +150.171.22.11 +150.171.27.11 +150.171.28.11 +151.101.0.81 +151.101.1.194 +151.101.1.224 +151.101.128.81 +151.101.129.194 +151.101.129.224 +151.101.131.42 +151.101.192.81 +151.101.193.194 +151.101.193.224 +151.101.195.1 +151.101.195.42 +151.101.3.1 +151.101.3.42 +151.101.64.81 +151.101.65.194 +151.101.65.224 +151.101.67.42 +157.240.31.35 +16.15.178.207 +16.15.181.233 +160.79.104.10 +161.35.220.135 +162.55.241.153 +165.22.91.219 +172.105.192.79 +172.105.69.103 +172.233.219.123 +172.233.219.49 +172.233.219.78 +172.237.146.25 +172.237.146.38 +172.237.146.8 +178.248.237.68 +179.43.150.83 +179.43.151.32 +179.43.166.40 +18.154.132.120 +18.154.132.30 +18.154.132.61 +18.154.132.68 +18.154.132.83 +18.154.132.86 +18.154.132.87 +18.154.132.9 +18.154.144.104 +18.154.144.18 +18.154.144.70 +18.154.144.88 +18.158.198.204 +18.164.124.48 +18.164.124.69 +18.164.124.80 +18.164.124.92 +18.173.219.124 +18.173.219.5 +18.173.219.85 +18.173.219.98 +18.192.161.188 +18.192.252.77 +18.192.94.166 +18.193.221.227 +18.197.250.227 +18.197.5.201 +18.207.85.246 +18.238.80.11 +18.238.80.111 +18.238.80.17 +18.238.80.20 +18.238.80.43 +18.238.80.53 +18.238.80.73 +18.238.80.84 +18.64.122.111 +18.64.122.116 +18.64.122.55 +18.64.122.75 +185.110.92.40 +185.110.92.41 +185.155.96.196 +185.70.42.25 +185.70.42.42 +185.81.128.108 +190.115.31.47 +192.0.66.233 +192.104.24.52 +192.124.249.107 +194.55.26.46 +194.55.30.46 +195.12.177.96 +195.123.208.131 +195.16.73.95 +195.245.213.251 +195.245.213.252 +198.148.79.54 +198.202.211.1 +199.83.44.71 +20.27.177.116 +204.79.197.200 +204.8.99.144 +204.8.99.146 +212.53.146.29 +212.58.244.129 +212.58.244.210 +212.58.249.206 +212.58.249.207 +213.166.70.101 +213.183.31.3 +217.140.110.36 +23.192.45.250 +23.192.45.251 +23.192.46.10 +23.192.46.16 +23.192.46.17 +23.192.46.18 +23.192.46.19 +23.192.46.8 +23.192.46.9 +23.192.47.66 +23.192.47.88 +23.208.232.54 +23.219.68.213 +23.35.101.86 +23.36.17.19 +23.36.17.218 +23.39.216.112 +23.53.3.132 +23.53.3.137 +23.53.3.138 +3.120.236.241 +3.122.12.167 +3.163.158.102 +3.163.158.51 +3.163.158.77 +3.163.158.83 +3.163.165.106 +3.163.165.108 +3.163.165.117 +3.163.165.21 +3.163.165.58 +3.163.165.65 +3.163.165.67 +3.163.165.77 +3.165.160.119 +3.165.160.125 +3.165.160.82 +3.165.160.93 +3.168.122.102 +3.168.122.30 +3.168.122.70 +3.168.122.96 +3.168.2.124 +3.168.2.34 +3.168.2.45 +3.168.2.73 +3.168.73.124 +3.168.73.14 +3.168.73.40 +3.168.73.5 +3.219.7.240 +3.70.4.80 +3.72.128.20 +3.93.103.92 +3.93.83.52 +31.13.82.174 +31.13.82.36 +31.13.82.52 +34.120.127.130 +34.172.121.65 +34.193.227.236 +34.210.63.15 +34.217.249.79 +34.240.120.87 +34.78.67.165 +34.8.0.82 +34.96.84.62 +35.156.83.59 +35.157.126.60 +35.157.160.164 +35.167.41.134 +35.186.224.24 +35.212.37.82 +37.1.201.40 +37.220.83.128 +44.227.138.182 +44.234.232.238 +44.237.234.25 +44.241.61.155 +44.242.60.85 +45.137.66.127 +5.61.53.100 +5.9.141.28 +50.112.202.115 +51.15.27.55 +52.13.171.212 +52.13.57.203 +52.175.140.176 +52.209.241.99 +52.216.60.56 +52.217.132.208 +52.217.174.112 +52.217.223.32 +52.217.234.232 +52.26.244.222 +52.29.75.203 +52.32.28.26 +52.33.95.61 +52.49.216.222 +52.50.101.102 +52.57.208.252 +52.72.30.131 +52.84.20.107 +52.84.20.112 +52.84.20.55 +52.84.20.75 +52.88.5.90 +54.144.73.197 +54.163.108.212 +54.167.157.63 +54.167.246.209 +54.221.253.164 +54.231.200.248 +54.235.8.174 +54.247.109.89 +54.68.22.26 +54.86.126.30 +63.34.30.85 +64.227.45.125 +66.254.114.41 +67.22.51.32 +67.22.51.33 +67.22.51.34 +67.22.51.35 +67.22.51.36 +67.22.51.37 +67.22.51.38 +67.22.51.39 +69.55.53.168 +69.55.53.169 +69.55.53.170 +69.55.53.171 +69.55.53.172 +72.163.4.185 +75.2.124.44 +76.223.63.197 +77.37.83.161 +77.86.162.1 +77.86.162.2 +78.46.102.85 +82.221.104.145 +84.15.66.97 +84.246.85.45 +87.245.208.97 +91.108.98.62 +91.200.40.44 +91.216.218.44 +94.130.182.82 +95.216.145.1 +99.80.51.179 +99.83.136.94 +99.84.234.112 +99.84.234.128 +99.84.234.31 +99.84.234.42 +99.84.234.5 +99.84.234.61 +99.84.234.89 +99.84.234.96 diff --git a/main.py b/main.py index 2561ff9..7ec3e01 100644 --- a/main.py +++ b/main.py @@ -3,16 +3,129 @@ import asyncio import configparser import ipaddress import os -from asyncio import Semaphore -from collections import defaultdict +import time +from collections import defaultdict, deque from typing import Dict, List, Set, Tuple, Optional import dns.asyncresolver import httpx from colorama import Fore, Style, init +from tqdm import tqdm init(autoreset=True) +class ProgressTracker: + def __init__(self, total: int, stats: Dict, unique_ips_set: Set[str], + num_dns_servers: int = 1, rate_limit: int = 10, domains_count: int = 0): + self.total = total + self.stats = stats + self.unique_ips = unique_ips_set + self.pbar = None + self.lock = asyncio.Lock() + self.num_dns_servers = num_dns_servers + self.rate_limit = rate_limit + self.domains_count = domains_count + self.effective_rate = num_dns_servers * rate_limit + self.start_time = time.time() + + def start(self): + self.pbar = tqdm( + total=self.total, + bar_format='[{bar:40}] {percentage:3.1f}% | Прошло: {elapsed} | Осталось (примерно): {desc}', + unit=' запр', + ncols=120, + leave=True, + mininterval=0, + desc='расчет...' + ) + + async def update_progress(self): + if self.pbar: + async with self.lock: + processed = self.stats.get('total_domains_processed', 0) + remaining_time = self.calculate_remaining_time() + + self.pbar.n = processed + self.pbar.set_description_str(remaining_time) + self.pbar.refresh() + + def format_time(self, seconds: float) -> str: + if seconds < 0: + seconds = 0 + mins = int(seconds // 60) + secs = int(seconds % 60) + return f"{mins:02d}:{secs:02d}" + + def calculate_remaining_time(self) -> str: + processed = self.stats.get('total_domains_processed', 0) + remaining = self.total - processed + + if self.effective_rate > 0: + time_remaining = remaining / self.effective_rate + return self.format_time(time_remaining) + return "00:00" + + def close(self): + if self.pbar: + self.pbar.n = self.total + self.pbar.refresh() + self.pbar.close() + + elapsed = time.time() - self.stats['start_time'] + total = self.stats['total_domains'] + processed = self.stats['total_domains_processed'] + errors = self.stats['domain_errors'] + + error_pct = (errors / total * 100) if total > 0 else 0 + total_ips_found = len(self.unique_ips) + self.stats['null_ips_count'] + self.stats.get('cloudflare_ips_count', 0) + null_pct = (self.stats['null_ips_count'] / total_ips_found * 100) if total_ips_found > 0 else 0 + cf_pct = (self.stats.get('cloudflare_ips_count', 0) / total_ips_found * 100) if total_ips_found > 0 else 0 + + print(f"\n{yellow('Проверка завершена.')}") + print(f"{Style.BRIGHT}Всего обработано DNS имен:{Style.RESET_ALL} {processed} из {total}") + print(f"{Style.BRIGHT}Разрешено уникальных IP-адресов:{Style.RESET_ALL} {len(self.unique_ips)}") + print(f"{Style.BRIGHT}Ошибок разрешения доменов:{Style.RESET_ALL} {errors} ({error_pct:.1f}%)") + + if self.stats['null_ips_count'] > 0: + print(f"{Style.BRIGHT}Исключено IP-адресов 'заглушек':{Style.RESET_ALL} {self.stats['null_ips_count']} ({null_pct:.1f}%)") + + if self.stats.get('cloudflare_ips_count', 0) > 0: + print(f"{Style.BRIGHT}Исключено IP-адресов Cloudflare:{Style.RESET_ALL} {self.stats['cloudflare_ips_count']} ({cf_pct:.1f}%)") + +class PeriodicProgressUpdater: + def __init__(self, progress_tracker: ProgressTracker, stats: Dict): + self.progress_tracker = progress_tracker + self.stats = stats + self.is_running = False + self.task = None + + async def start(self): + if not self.is_running: + self.is_running = True + self.task = asyncio.create_task(self._periodic_update()) + + async def stop(self): + if self.is_running: + self.is_running = False + if self.task: + self.task.cancel() + try: + await self.task + except asyncio.CancelledError: + pass + + async def _periodic_update(self): + await asyncio.sleep(2) + while self.is_running: + try: + await self.progress_tracker.update_progress() + await asyncio.sleep(2) + except asyncio.CancelledError: + break + except Exception as e: + print(f"Error in periodic progress update: {e}") + await asyncio.sleep(2) + def yellow(text): return f"{Fore.YELLOW}{text}{Style.RESET_ALL}" @@ -60,7 +173,7 @@ def read_config(cfg_file): config = config['DomainMapper'] service = config.get('service') or '' - request_limit = int(config.get('threads') or 15) + rate_limit = int(config.get('rate_limit') or 50) filename = config.get('filename') or 'domain-ip-resolve.txt' cloudflare = config.get('cloudflare') or '' filetype = config.get('filetype') or '' @@ -79,7 +192,7 @@ def read_config(cfg_file): print(f"{yellow(f'Загружена конфигурация из {cfg_file}:')}") print(f"{Style.BRIGHT}Сервисы для проверки:{Style.RESET_ALL} {service if service else 'спросить у пользователя'}") print(f"{Style.BRIGHT}Использовать DNS сервер:{Style.RESET_ALL} {dns_server_indices if dns_server_indices else 'спросить у пользователя'}") - print(f"{Style.BRIGHT}Количество одновременных запросов к одному DNS серверу:{Style.RESET_ALL} {request_limit}") + print(f"{Style.BRIGHT}Лимит запросов к каждому DNS серверу (запросов/сек):{Style.RESET_ALL} {rate_limit}") print(f"{Style.BRIGHT}Фильтрация IP-адресов Cloudflare:{Style.RESET_ALL} {'включена' if cloudflare in ['y', 'yes'] else 'выключена' if cloudflare in ['n', 'no'] else 'спросить у пользователя'}") print(f"{Style.BRIGHT}Агрегация IP-адресов:{Style.RESET_ALL} {'mix режим /24 (255.255.255.0) + /32 (255.255.255.255)' if subnet == 'mix' else 'до /16 подсети (255.255.0.0)' if subnet == '16' else 'до /24 подсети (255.255.255.0)' if subnet == '24' else 'выключена' if subnet in ['n', 'no'] else 'спросить у пользователя'}") print(f"{Style.BRIGHT}Формат сохранения:{Style.RESET_ALL} {'только IP' if filetype == 'ip' else 'Linux route' if filetype == 'unix' else 'CIDR-нотация' if filetype == 'cidr' else 'Windows route' if filetype == 'win' else 'Mikrotik CLI' if filetype == 'mikrotik' else 'open vpn' if filetype == 'ovpn' else 'Keenetic CLI' if filetype == 'keenetic' else 'Wireguard' if filetype == 'wireguard' else 'спросить у пользователя'}") @@ -96,11 +209,11 @@ def read_config(cfg_file): print(f"{Style.BRIGHT}Локальный список платформ:{Style.RESET_ALL} {'включен' if str(localplatform).strip().lower() in ('yes', 'y') else 'выключен'}") print(f"{Style.BRIGHT}Локальный список DNS серверов:{Style.RESET_ALL} {'включен' if str(localdns).strip().lower() in ('yes', 'y') else 'выключен'}") - return service, request_limit, filename, cloudflare, filetype, gateway, run_command, dns_server_indices, mk_list_name, subnet, ken_gateway, localplatform, localdns, mk_comment + return service, rate_limit, filename, cloudflare, filetype, gateway, run_command, dns_server_indices, mk_list_name, subnet, ken_gateway, localplatform, localdns, mk_comment except Exception as e: print(f"{yellow(f'Ошибка загрузки {cfg_file}:')} {e}\n{Style.BRIGHT}Используются настройки 'по умолчанию'.{Style.RESET_ALL}") - return '', 20, 'domain-ip-resolve.txt', '', '', '', '', [], '', '', '', '', '', 'off' + return '', 50, 'domain-ip-resolve.txt', '', '', '', '', [], '', '', '', '', '', 'off' def gateway_input(gateway): if not gateway: @@ -116,11 +229,107 @@ def ken_gateway_input(ken_gateway): else: return ken_gateway -def get_semaphore(request_limit): - return defaultdict(lambda: Semaphore(request_limit)) +class DNSServerWorker: + def __init__(self, name: str, nameservers: List[str], rate_limit: int = 10, stats_lock=None): + self.name = name + self.nameservers = nameservers + self.rate_limit = rate_limit + self.queue = asyncio.Queue() + self.request_times = deque() + self.results = [] + self.stats = { + 'processed': 0, + 'errors': 0, + 'success': 0 + } + self.stats_lock = stats_lock or asyncio.Lock() + self.rate_limit_lock = asyncio.Lock() -def init_semaphores(request_limit): - return get_semaphore(request_limit) + async def add_domain(self, domain: str): + await self.queue.put(domain) + + async def _enforce_rate_limit(self): + async with self.rate_limit_lock: + now = time.monotonic() + + while self.request_times and now - self.request_times[0] >= 1.0: + self.request_times.popleft() + + if len(self.request_times) >= self.rate_limit: + sleep_time = 1.0 - (now - self.request_times[0]) + if sleep_time > 0: + await asyncio.sleep(sleep_time) + now = time.monotonic() + while self.request_times and now - self.request_times[0] >= 1.0: + self.request_times.popleft() + + self.request_times.append(now) + + async def process_queue(self, global_stats: Dict[str, int]): + resolver = dns.asyncresolver.Resolver() + resolver.nameservers = self.nameservers + resolver.timeout = 10.0 + resolver.lifetime = 15.0 + + domains = [] + while not self.queue.empty(): + domain = await self.queue.get() + domains.append(domain) + + async def process_single_domain(domain): + await self._enforce_rate_limit() + + try: + response = await resolver.resolve(domain) + ips = [ip.address for ip in response] + + async with self.stats_lock: + global_stats['total_domains_processed'] += 1 + self.stats['processed'] += 1 + self.stats['success'] += 1 + + return ips + except dns.resolver.NoNameservers: + async with self.stats_lock: + global_stats['total_domains_processed'] += 1 + global_stats['domain_errors'] += 1 + self.stats['processed'] += 1 + self.stats['errors'] += 1 + return [] + except dns.resolver.Timeout: + async with self.stats_lock: + global_stats['total_domains_processed'] += 1 + global_stats['domain_errors'] += 1 + self.stats['processed'] += 1 + self.stats['errors'] += 1 + return [] + except dns.resolver.NXDOMAIN: + async with self.stats_lock: + global_stats['total_domains_processed'] += 1 + global_stats['domain_errors'] += 1 + self.stats['processed'] += 1 + self.stats['errors'] += 1 + return [] + except dns.resolver.NoAnswer: + async with self.stats_lock: + global_stats['total_domains_processed'] += 1 + global_stats['domain_errors'] += 1 + self.stats['processed'] += 1 + self.stats['errors'] += 1 + return [] + except Exception: + async with self.stats_lock: + global_stats['total_domains_processed'] += 1 + global_stats['domain_errors'] += 1 + self.stats['processed'] += 1 + self.stats['errors'] += 1 + return [] + + results = await asyncio.gather(*[process_single_domain(domain) for domain in domains], return_exceptions=True) + + for result in results: + if isinstance(result, list): + self.results.extend(result) async def load_urls(url: str) -> Dict[str, str]: try: @@ -224,73 +433,49 @@ async def load_dns_names(url_or_file: str) -> List[str]: print(f"Ошибка при чтении файла {url_or_file}: {e}") return [] -async def resolve_domain_batch(domains: List[str], resolver: dns.asyncresolver.Resolver, - semaphore: Semaphore, dns_server_name: str, - stats: Dict[str, int], cloudflare_ips: Set[str], - include_cloudflare: bool) -> List[str]: - async with semaphore: - resolved_ips = [] - for domain in domains: - try: - stats['total_domains_processed'] += 1 - response = await resolver.resolve(domain) - ips = [ip.address for ip in response] - - for ip_address in ips: - if ip_address in ('127.0.0.1', '0.0.0.0') or ip_address in resolver.nameservers: - stats['null_ips_count'] += 1 - elif include_cloudflare and ip_address in cloudflare_ips: - stats['cloudflare_ips_count'] += 1 - else: - resolved_ips.append(ip_address) - print(f"{Fore.BLUE}{domain} IP-адрес: {ip_address} - {dns_server_name}{Style.RESET_ALL}") - - except Exception: - stats['domain_errors'] += 1 - - return resolved_ips - -async def resolve_dns_optimized(service: str, dns_names: List[str], - dns_servers: List[Tuple[str, List[str]]], - cloudflare_ips: Set[str], unique_ips_all_services: Set[str], - semaphore_dict: Dict, stats: Dict[str, int], - include_cloudflare: bool, batch_size: int = 50) -> str: +async def resolve_dns_with_workers(service: str, dns_names: List[str], + dns_servers: List[Tuple[str, List[str]]], + cloudflare_ips: Set[str], unique_ips_all_services: Set[str], + stats: Dict[str, int], include_cloudflare: bool, + rate_limit: int, stats_lock: asyncio.Lock = None) -> str: try: - print(f"{Fore.YELLOW}Загрузка DNS имен платформы {service}...{Style.RESET_ALL}") - - domain_batches = [dns_names[i:i + batch_size] for i in range(0, len(dns_names), batch_size)] - - tasks = [] - - for batch in domain_batches: - for server_name, servers in dns_servers: - resolver = dns.asyncresolver.Resolver() - resolver.nameservers = servers - - tasks.append(resolve_domain_batch( - batch, resolver, semaphore_dict[server_name], - server_name, stats, cloudflare_ips, include_cloudflare - )) - - max_concurrent_tasks = min(len(tasks), 100) - - results = [] - for i in range(0, len(tasks), max_concurrent_tasks): - batch_tasks = tasks[i:i + max_concurrent_tasks] - batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True) - - for result in batch_results: - if not isinstance(result, Exception): - results.extend(result) - + if stats_lock is None: + stats_lock = asyncio.Lock() + + workers = [] + for server_name, servers in dns_servers: + worker = DNSServerWorker(server_name, servers, rate_limit, stats_lock) + workers.append(worker) + + for domain in dns_names: + for worker in workers: + await worker.add_domain(domain) + + tasks = [worker.process_queue(stats) for worker in workers] + + await asyncio.gather(*tasks) + + all_nameservers = set() + for _, servers in dns_servers: + all_nameservers.update(servers) + unique_ips_current_service = set() - for ip_address in results: - if ip_address not in unique_ips_all_services: - unique_ips_current_service.add(ip_address) - unique_ips_all_services.add(ip_address) - + for worker in workers: + for ip_address in worker.results: + if ip_address in ('127.0.0.1', '0.0.0.0') or ip_address in all_nameservers: + stats['null_ips_count'] += 1 + continue + + if include_cloudflare and ip_address in cloudflare_ips: + stats['cloudflare_ips_count'] += 1 + continue + + if ip_address not in unique_ips_all_services: + unique_ips_current_service.add(ip_address) + unique_ips_all_services.add(ip_address) + return '\n'.join(sorted(unique_ips_current_service)) + '\n' if unique_ips_current_service else '' - + except Exception as e: print(f"Не удалось сопоставить IP адреса {service} его доменным именам: {e}") return "" @@ -465,6 +650,46 @@ def group_ips_in_subnets_optimized(filename: str, subnet: str): except Exception as e: print(f"Ошибка при обработке файла: {e}") +def split_file_by_lines(filename: str, max_lines: int = 999): + try: + with open(filename, 'r', encoding='utf-8') as file: + lines = file.readlines() + + total_lines = len(lines) + + if total_lines <= max_lines: + return False + + base_name = filename.rsplit('.', 1)[0] if '.' in filename else filename + extension = '.' + filename.rsplit('.', 1)[1] if '.' in filename else '.txt' + + num_parts = (total_lines + max_lines - 1) // max_lines + + print(f"\n{Style.BRIGHT}Результаты сохранены в файлы:{Style.RESET_ALL}") + for part in range(num_parts): + start_index = part * max_lines + end_index = min((part + 1) * max_lines, total_lines) + + part_filename = f"{base_name}_p{part + 1}{extension}" + + with open(part_filename, 'w', encoding='utf-8') as file: + file.writelines(lines[start_index:end_index]) + + print(f"{Style.BRIGHT}{part_filename} ({end_index - start_index} строк){Style.RESET_ALL}") + + print(f"{Style.BRIGHT}Разделение завершено. Создано {num_parts} частей{Style.RESET_ALL}") + + try: + os.remove(filename) + except Exception as e: + print(f"{red('Не удалось удалить исходный файл:')} {e}") + + return True + + except Exception as e: + print(f"{red('Ошибка при разделении файла:')} {e}") + return False + def process_file_format(filename, filetype, gateway, selected_service, mk_list_name, mk_comment, subnet, ken_gateway): def read_file(filename): try: @@ -554,6 +779,11 @@ def process_file_format(filename, filetype, gateway, selected_service, mk_list_n if filetype.lower() in formatters: write_file(filename, ips, formatters[filetype.lower()]) + if filetype.lower() == 'keenetic bat': + return split_file_by_lines(filename, max_lines=999) + + return False + async def main(): parser = argparse.ArgumentParser(description="DNS resolver script with custom config file.") parser.add_argument( @@ -566,7 +796,7 @@ async def main(): try: config_file = args.config - (service, request_limit, filename, cloudflare, filetype, gateway, run_command, + (service, rate_limit, filename, cloudflare, filetype, gateway, run_command, dns_server_indices, mk_list_name, subnet, ken_gateway, localplatform, localdns, mk_comment) = read_config(config_file) @@ -596,37 +826,70 @@ async def main(): cloudflare_ips = set() unique_ips_all_services = set() - semaphore = init_semaphores(request_limit) - + stats = { 'null_ips_count': 0, 'cloudflare_ips_count': 0, 'total_domains_processed': 0, 'domain_errors': 0 } - + + total_domains = 0 + for service_name in selected_services: + if service_name == 'Custom DNS list': + total_domains += len(local_dns_names) + else: + url_or_file = urls[service_name] + print(f"{Style.BRIGHT}Загрузка DNS имен платформы{Style.RESET_ALL} {service_name}...") + dns_names = await load_dns_names(url_or_file) + total_domains += len(dns_names) + + domains_count = total_domains + total_domains *= len(selected_dns_servers) + stats['total_domains'] = total_domains + stats['start_time'] = time.time() + + print(f"{Style.BRIGHT}Загружено {domains_count} DNS имен.{Style.RESET_ALL}\n{yellow('Резолвинг...')}") + + progress_tracker = ProgressTracker( + total=total_domains, + stats=stats, + unique_ips_set=unique_ips_all_services, + num_dns_servers=len(selected_dns_servers), + rate_limit=rate_limit, + domains_count=domains_count + ) + progress_tracker.start() + + stats_lock = asyncio.Lock() + + periodic_updater = PeriodicProgressUpdater(progress_tracker, stats) + await periodic_updater.start() + tasks = [] for service_name in selected_services: if service_name == 'Custom DNS list': - tasks.append(resolve_dns_optimized( - service_name, local_dns_names, selected_dns_servers, - cloudflare_ips, unique_ips_all_services, semaphore, - stats, include_cloudflare + tasks.append(resolve_dns_with_workers( + service_name, local_dns_names, selected_dns_servers, + cloudflare_ips, unique_ips_all_services, + stats, include_cloudflare, rate_limit, + stats_lock )) else: url_or_file = urls[service_name] dns_names = await load_dns_names(url_or_file) if dns_names: - tasks.append(resolve_dns_optimized( - service_name, dns_names, selected_dns_servers, - cloudflare_ips, unique_ips_all_services, semaphore, - stats, include_cloudflare + tasks.append(resolve_dns_with_workers( + service_name, dns_names, selected_dns_servers, + cloudflare_ips, unique_ips_all_services, + stats, include_cloudflare, rate_limit, + stats_lock )) if tasks: results = await asyncio.gather(*tasks, return_exceptions=True) - + with open(filename, 'w', encoding='utf-8') as file: for result in results: if isinstance(result, str) and result.strip(): @@ -635,32 +898,31 @@ async def main(): with open(filename, 'w', encoding='utf-8') as file: pass - print(f"\n{yellow('Проверка завершена.')}") - print(f"{Style.BRIGHT}Всего обработано DNS имен:{Style.RESET_ALL} {stats['total_domains_processed']}") - print(f"{Style.BRIGHT}Разрешено IP-адресов из DNS имен:{Style.RESET_ALL} {len(unique_ips_all_services)}") - print(f"{Style.BRIGHT}Ошибок разрешения доменов:{Style.RESET_ALL} {stats['domain_errors']}") - if stats['null_ips_count'] > 0: - print(f"{Style.BRIGHT}Исключено IP-адресов 'заглушек':{Style.RESET_ALL} {stats['null_ips_count']}") - if include_cloudflare: - print(f"{Style.BRIGHT}Исключено IP-адресов Cloudflare:{Style.RESET_ALL} {stats['cloudflare_ips_count']}") - print(f"{Style.BRIGHT}Использовались DNS серверы:{Style.RESET_ALL} " + ', '.join( - [f'{pair[0]} ({", ".join(pair[1])})' for pair in selected_dns_servers])) + await periodic_updater.stop() + progress_tracker.close() + print(f"{Style.BRIGHT}Использовались DNS серверы:{Style.RESET_ALL} " + ', '.join( + [pair[0] for pair in selected_dns_servers])) + + print(f"\n{yellow('Обработка результатов...')}") subnet = subnet_input(subnet) if subnet != '32': group_ips_in_subnets_optimized(filename, subnet) - process_file_format(filename, filetype, gateway, selected_services, mk_list_name, mk_comment, subnet, ken_gateway) + file_was_split = process_file_format(filename, filetype, gateway, selected_services, mk_list_name, mk_comment, subnet, ken_gateway) if run_command: print("\nВыполнение команды после завершения скрипта...") os.system(run_command) else: - print(f"\n{Style.BRIGHT}Результаты сохранены в файл:{Style.RESET_ALL}", filename) + if not file_was_split: + print(f"\n{Style.BRIGHT}Результаты сохранены в файл:{Style.RESET_ALL}", filename) if os.name == 'nt': input(f"Нажмите {green('Enter')} для выхода...") + print(f"\n{Style.BRIGHT}Если есть желание, можно угостить автора чашечкой какао:{Style.RESET_ALL} {green('https://boosty.to/ground_zerro')}") + except KeyboardInterrupt: print(f"\n{red('Программа прервана пользователем')}") except Exception as e: @@ -674,4 +936,4 @@ if __name__ == "__main__": except KeyboardInterrupt: print(f"\n{red('Программа прервана пользователем')}") except Exception as e: - print(f"\n{red('Критическая ошибка:')} {e}") + print(f"\n{red('Критическая ошибка:')} {e}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b5617eb..fd1f2a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -configparser~=7.0.1 -ipaddress~=1.0.23 -dnspython~=2.6.1 -httpx~=0.27.0 -colorama~=0.4.6 -requests~=2.31.0 -beautifulsoup4~=4.12.3 \ No newline at end of file +dnspython>=2.6.1 +httpx>=0.27.0 +colorama>=0.4.6 +tqdm>=4.66.0 + +requests>=2.31.0 +beautifulsoup4>=4.12.3 diff --git a/utilities/README.md b/utilities/README.md index cfa6a1c..4774ee6 100644 --- a/utilities/README.md +++ b/utilities/README.md @@ -71,17 +71,25 @@ ### Функции -- Загрузка списка IP-адресов из файла. -- Агрегация IP-адресов в подсети с масками `/16`, `/24`, или объединение нескольких подсетей. -- Исключение IP-адресов Cloudflare из итогового списка (при необходимости). +- Извлечение IP-адресов из файла (файл может содержать любой текст - IP автоматически извлекаются). +- Исключение IP-адресов Cloudflare из итогового списка (опционально). +- Агрегация IP-адресов в подсети: + - `/16` (255.255.0.0) + - `/24` (255.255.255.0) + - Mix режим (`/24` + `/32`) - Поддержка различных форматов маршрутизации: - - Windows (`route add`) - - Unix (`ip route`) - - Keenetic (`ip route` с интерфейсом) - - Mikrotik (`/ip firewall`) + - Только IP-адреса + - Windows route (`route add`) + - Linux route (`ip route`) + - Keenetic BAT (`route add` для bat-файлов) + - Keenetic CLI (`ip route` с интерфейсом) + - Mikrotik firewall (`/ip/firewall/address-list`) - WireGuard - - OpenVPN - - CIDR (с указанием маски) + - OpenVPN (`push "route"`) + - CIDR нотация +- Автоматическое разделение больших файлов на части (для Keenetic BAT формата, max 999 строк). +- Удаление исходного файла после разделения на части. +- Проверка наличия входного файла с выводом инструкций при его отсутствии. ### Использование @@ -91,7 +99,14 @@ pip install -r requirements.txt ``` -2. Поместите файл c IP-адресами `ip.txt` в корневую директорию проекта. Файл может содержать любой текст и IP-адреса в любом виде - лишнее будет убрано автоматически. +2. Создайте файл `ip.txt` в директории со скриптом и добавьте в него IP-адреса (по одному на строку) или любой текст содержащий IP-адреса. + + Пример содержимого `ip.txt`: + ``` + 192.168.1.1 + 10.0.0.1 + Какой-то текст с IP: 172.16.0.1 + ``` 3. Запустите скрипт: @@ -99,7 +114,18 @@ python convert.py ``` -4. Следуйте подсказкам на экране. +4. Следуйте интерактивным подсказкам на экране: + - Выберите, нужно ли исключить IP-адреса Cloudflare (1 - да, Enter - нет) + - Выберите агрегацию подсетей (1 - /16, 2 - /24, 3 - mix, Enter - без агрегации) + - Выберите формат сохранения (1-8 или Enter для простого списка IP) + - При необходимости укажите шлюз/интерфейс/имя списка + +5. Результат будет сохранен в файл `ip.txt` (или в несколько файлов, если был выбран формат с автоматическим разделением). + +### Примечания + +- Если файл `ip.txt` не найден, скрипт выведет подробную инструкцию по его созданию и корректно завершится. +- Для формата Keenetic BAT файл автоматически разделяется на части по 999 строк, исходный файл удаляется. ## split diff --git a/utilities/convert.py b/utilities/convert.py index 557bd90..0e009ad 100644 --- a/utilities/convert.py +++ b/utilities/convert.py @@ -1,11 +1,12 @@ import asyncio import ipaddress +import os import re +from collections import defaultdict import httpx from colorama import Fore, Style, init -# Цвета init(autoreset=True) @@ -28,12 +29,9 @@ def red(text): def magneta(text): return f"{Fore.MAGENTA}{text}{Style.RESET_ALL}" - def blue(text): return f"{Fore.BLUE}{text}{Style.RESET_ALL}" - -# IP шлюза для win и unix def gateway_input(gateway): if not gateway: input_gateway = input(f"Укажите {green('IP шлюза')} или {green('имя интерфейса')}: ") @@ -41,8 +39,6 @@ def gateway_input(gateway): else: return gateway - -# IP шлюза и имя интерфейса для keenetic def ken_gateway_input(ken_gateway): if not ken_gateway: input_ken_gateway = input( @@ -51,8 +47,6 @@ def ken_gateway_input(ken_gateway): else: return ken_gateway - -# Загрузка IP-адресов cloudflare async def get_cloudflare_ips(): try: async with httpx.AsyncClient() as client: @@ -74,17 +68,22 @@ async def get_cloudflare_ips(): print("Ошибка при получении IP адресов Cloudflare:", e) return set() - -# Промт cloudflare фильтр def check_include_cloudflare(cloudflare): if cloudflare in ['yes', 'y', 'no', 'n']: return cloudflare in ['yes', 'y'] - return input(f"\n{yellow('Исключить IP адреса Cloudflare из итогового списка?')}" - f"\n{green('yes')} - исключить" - f"\n{green('Enter')} - оставить: ").strip().lower() in ['yes', 'y'] + user_input = input( + f"\n{yellow('Исключить IP адреса Cloudflare из итогового списка?')}" + f"\n1. исключить" + f"\n{green('Enter')} - оставить" + f"\nВаш выбор: " + ).strip() + + if user_input == '1': + return True + else: + return False -# комментарий для microtik firewall def mk_list_name_input(mk_list_name): if not mk_list_name: input_mk_list_name = input(f"Введите {green('LIST_NAME')} для Mikrotik firewall: ") @@ -92,80 +91,117 @@ def mk_list_name_input(mk_list_name): else: return mk_list_name - -# Уплотняем имена сервисов def comment(selected_service): return ",".join(["".join(word.title() for word in s.split()) for s in selected_service]) - -# Промт на объединение IP в подсети def subnet_input(subnet): if not subnet: - subnet = input( - f"\n{yellow('Объединить IP-адреса в подсети?')} " - f"\n{green('16')} - сократить до /16 (255.255.0.0)" - f"\n{green('24')} - сократить до /24 (255.255.255.0)" - f"\n{green('mix')} - сократить до /24 (255.255.255.0) и /32 (255.255.255.255)" - f"\n{green('Enter')} - пропустить: " - ).strip().lower() + choice = input( + f"\n{yellow('Объединить IP-адреса в подсети?')}" + f"\n1. сократить до {green('/16')} (255.255.0.0)" + f"\n2. сократить до {green('/24')} (255.255.255.0)" + f"\n3. сократить до {green('/24')} + {green('/32')} (255.255.255.0 и 255.255.255.255)" + f"\n{green('Enter')} - пропустить" + f"\nВаш выбор: " + ).strip() + + if choice == '1': + subnet = '16' + elif choice == '2': + subnet = '24' + elif choice == '3': + subnet = 'mix' + else: + subnet = '32' return subnet if subnet in {'16', '24', 'mix'} else '32' - -# Агрегация маршрутов -def group_ips_in_subnets(filename, subnet): +def group_ips_in_subnets_optimized(filename: str, subnet: str): try: with open(filename, 'r', encoding='utf-8') as file: - ips = {line.strip() for line in file if line.strip()} # Собираем уникальные IP адреса + ips = {line.strip() for line in file if line.strip()} subnets = set() - def process_ips(subnet): + if subnet == "16": for ip in ips: try: - if subnet == "16": - # Преобразуем в /16 (два последних октета заменяются на 0.0) - network = ipaddress.IPv4Network(f"{ip}/16", strict=False) - subnets.add(f"{network.network_address}") - elif subnet == "24": - # Преобразуем в /24 (последний октет заменяется на 0) - network = ipaddress.IPv4Network(f"{ip}/24", strict=False) - subnets.add(f"{network.network_address}") - except ValueError as e: - print(f"Ошибка в IP адресе: {ip} - {e}") + network = ipaddress.IPv4Network(f"{ip}/16", strict=False) + subnets.add(str(network.network_address)) + except ValueError: + continue + print(f"{Style.BRIGHT}IP-адреса агрегированы до /16 подсети{Style.RESET_ALL}") - if subnet in ["24", "16"]: - process_ips(subnet) - print(f"{Style.BRIGHT}IP-адреса агрегированы до /{subnet} подсети{Style.RESET_ALL}") + elif subnet == "24": + for ip in ips: + try: + network = ipaddress.IPv4Network(f"{ip}/24", strict=False) + subnets.add(str(network.network_address)) + except ValueError: + continue + print(f"{Style.BRIGHT}IP-адреса агрегированы до /24 подсети{Style.RESET_ALL}") elif subnet == "mix": - octet_groups = {} + octet_groups = defaultdict(list) for ip in ips: - key = '.'.join(ip.split('.')[:3]) # Группировка по первым трем октетам - if key not in octet_groups: - octet_groups[key] = [] + key = '.'.join(ip.split('.')[:3]) octet_groups[key].append(ip) - # IP-адреса с совпадающими первыми тремя октетами - network_24 = {key + '.0' for key, group in octet_groups.items() if - len(group) > 1} # Базовый IP для /24 подсетей - # Удаляем IP с совпадающими первыми тремя октетами из множества - ips -= {ip for group in octet_groups.values() if len(group) > 1 for ip in group} - # Оставляем только IP без указания маски для /24 и одиночных IP - subnets.update(ips) # IP без маски для одиночных IP - subnets.update(network_24) # Базовые IP для /24 подсетей + for key, group in octet_groups.items(): + if len(group) > 1: + subnets.add(key + '.0') + else: + subnets.update(group) + print(f"{Style.BRIGHT}IP-адреса агрегированы до масок /24 и /32{Style.RESET_ALL}") with open(filename, 'w', encoding='utf-8') as file: - for subnet in sorted(subnets): - file.write(subnet + '\n') + for subnet_ip in sorted(subnets, key=lambda x: ipaddress.IPv4Address(x.split('/')[0])): + file.write(subnet_ip + '\n') except Exception as e: print(f"Ошибка при обработке файла: {e}") +def split_file_by_lines(filename: str, max_lines: int = 999): + try: + with open(filename, 'r', encoding='utf-8') as file: + lines = file.readlines() -# Выбор формата сохранения результатов -def process_file_format(filename, filetype, gateway, selected_service, mk_list_name, subnet, ken_gateway): + total_lines = len(lines) + + if total_lines <= max_lines: + return False + + base_name = filename.rsplit('.', 1)[0] if '.' in filename else filename + extension = '.' + filename.rsplit('.', 1)[1] if '.' in filename else '.txt' + + num_parts = (total_lines + max_lines - 1) // max_lines + + print(f"\n{Style.BRIGHT}Результаты сохранены в файлы:{Style.RESET_ALL}") + for part in range(num_parts): + start_index = part * max_lines + end_index = min((part + 1) * max_lines, total_lines) + + part_filename = f"{base_name}_p{part + 1}{extension}" + + with open(part_filename, 'w', encoding='utf-8') as file: + file.writelines(lines[start_index:end_index]) + + print(f"{Style.BRIGHT}{part_filename} ({end_index - start_index} строк){Style.RESET_ALL}") + + print(f"{Style.BRIGHT}Разделение завершено. Создано {num_parts} частей{Style.RESET_ALL}") + + try: + os.remove(filename) + except Exception as e: + print(f"{red('Не удалось удалить исходный файл:')} {e}") + + return True + + except Exception as e: + print(f"{red('Ошибка при разделении файла:')} {e}") + return False +def process_file_format(filename, filetype, gateway, selected_service, mk_list_name, mk_comment, subnet, ken_gateway): def read_file(filename): try: with open(filename, 'r', encoding='utf-8') as file: @@ -182,73 +218,83 @@ def process_file_format(filename, filetype, gateway, selected_service, mk_list_n else: file.write('\n'.join(formatted_ips)) - # Определение маски подсети net_mask = subnet if subnet == "mix" else "255.255.0.0" if subnet == "16" else "255.255.255.0" if subnet == "24" else "255.255.255.255" if not filetype: - filetype = input(f""" + user_input = input(f""" {yellow('В каком формате сохранить файл?')} -{green('win')} - route add {cyan('IP')} mask {net_mask} {cyan('GATEWAY')} -{green('unix')} - ip route {cyan('IP')}/{subnet} {cyan('GATEWAY')} -{green('keenetic')} - ip route {cyan('IP')}/{subnet} {cyan('GATEWAY GATEWAY_NAME')} auto !{comment(selected_service)} -{green('cidr')} - {cyan('IP')}/{subnet} -{green('mikrotik')} - /ip/firewall/address-list add list={cyan("LIST_NAME")} comment="{comment(selected_service)}" address={cyan("IP")}/{subnet} -{green('ovpn')} - push "route {cyan('IP')} {net_mask}" -{green('wireguard')} - {cyan('IP')}/{subnet}, {cyan('IP')}/{subnet}, и т.д... +1. {green('win')} - route add {cyan('IP')} mask {net_mask} {cyan('GATEWAY')} +2. {green('unix')} - ip route {cyan('IP')}/{subnet} {cyan('GATEWAY')} +3. {green('keenetic bat')} - route add {cyan('IP')} mask {net_mask} 0.0.0.0 +4. {green('keenetic cli')} - ip route {cyan('IP')}/{subnet} {cyan('GATEWAY GATEWAY_NAME')} auto !{comment(selected_service)} +5. {green('cidr')} - {cyan('IP')}/{subnet} +6. {green('mikrotik')} - /ip/firewall/address-list add list={cyan("LIST_NAME")}{f' comment="{comment(selected_service)}"' if mk_comment != "off" else ""} address={cyan("IP")}/{subnet} +7. {green('ovpn')} - push "route {cyan('IP')} {net_mask}" +8. {green('wireguard')} - {cyan('IP')}/{subnet}, {cyan('IP')}/{subnet}, и т.д... {green('Enter')} - {cyan('IP')} -Ваш выбор: """) +Ваш выбор: """).strip() + + mapping = { + '1': 'win', + '2': 'unix', + '3': 'keenetic bat', + '4': 'keenetic cli', + '5': 'cidr', + '6': 'mikrotik', + '7': 'ovpn', + '8': 'wireguard' + } + filetype = mapping.get(user_input, '') ips = read_file(filename) if not ips: return - # Дополнительные запросы в зависимости от формата файла - if filetype in ['win', 'unix']: # Запрашиваем IP шлюза для win и unix + if filetype in ['win', 'unix']: gateway = gateway_input(gateway) - elif filetype == 'keenetic': # Запрашиваем IP шлюза и имя интерфейса для keenetic + elif filetype == 'keenetic cli': ken_gateway = ken_gateway_input(ken_gateway) - elif filetype == 'mikrotik': # Запрашиваем ввод комментария для microtik firewall + elif filetype == 'mikrotik': mk_list_name = mk_list_name_input(mk_list_name) - # обычный формат formatters = { 'win': lambda ip: f"route add {ip} mask {net_mask} {gateway}", 'unix': lambda ip: f"ip route {ip}/{subnet} {gateway}", - 'keenetic': lambda ip: f"ip route {ip}/{subnet} {ken_gateway} auto !{comment(selected_service)}", + 'keenetic bat': lambda ip: f"route add {ip} mask {net_mask} 0.0.0.0", + 'keenetic cli': lambda ip: f"ip route {ip}/{subnet} {ken_gateway} auto !{comment(selected_service)}", 'cidr': lambda ip: f"{ip}/{subnet}", 'ovpn': lambda ip: f'push "route {ip} {net_mask}"', - 'mikrotik': lambda - ip: f'/ip/firewall/address-list add list={mk_list_name} comment="{comment(selected_service)}" address={ip}/{subnet}', + 'mikrotik': lambda ip: f'/ip/firewall/address-list add list={mk_list_name}' + (f' comment="{comment(selected_service)}"' if mk_comment != "off" else "") + f' address={ip}/{subnet}', 'wireguard': lambda ip: f"{ip}/{subnet}" } - # mix формат if subnet == "mix": - if filetype.lower() == 'win': # Обработка для win - mix_formatter = lambda ip: f"{ip.strip()} mask 255.255.255.0" if ip.endswith( - '.0') else f"{ip.strip()} mask 255.255.255.255" - elif filetype.lower() == 'ovpn': # Обработка для ovpn - mix_formatter = lambda ip: f"{ip.strip()} 255.255.255.0" if ip.endswith( - '.0') else f"{ip.strip()} 255.255.255.255" - else: # Обработка для остальных форматов + if filetype in ['win', 'keenetic bat']: + mix_formatter = lambda ip: f"{ip.strip()} mask 255.255.255.0" if ip.endswith('.0') else f"{ip.strip()} mask 255.255.255.255" + elif filetype.lower() == 'ovpn': + mix_formatter = lambda ip: f"{ip.strip()} 255.255.255.0" if ip.endswith('.0') else f"{ip.strip()} 255.255.255.255" + else: mix_formatter = lambda ip: f"{ip.strip()}/24" if ip.endswith('.0') else f"{ip.strip()}/32" formatters.update({ 'win': lambda ip: f"route add {mix_formatter(ip)} {gateway}", 'unix': lambda ip: f"ip route {mix_formatter(ip)} {gateway}", - 'keenetic': lambda ip: f"ip route {mix_formatter(ip)} {ken_gateway} auto !{comment(selected_service)}", + 'keenetic bat': lambda ip: f"route add {mix_formatter(ip)} 0.0.0.0", + 'keenetic cli': lambda ip: f"ip route {mix_formatter(ip)} {ken_gateway} auto !{comment(selected_service)}", 'cidr': lambda ip: f"{mix_formatter(ip)}", 'ovpn': lambda ip: f'push "route {mix_formatter(ip)}"', - 'mikrotik': lambda ip: f'/ip/firewall/address-list add list={mk_list_name} comment="{comment(selected_service)}" address={mix_formatter(ip)}', + 'mikrotik': lambda ip: f'/ip/firewall/address-list add list={mk_list_name}' + (f' comment="{comment(selected_service)}"' if mk_comment != "off" else "") + f' address={mix_formatter(ip)}', 'wireguard': lambda ip: f"{mix_formatter(ip)}" }) - # Запись в файл if filetype.lower() in formatters: write_file(filename, ips, formatters[filetype.lower()]) + if filetype.lower() == 'keenetic bat': + return split_file_by_lines(filename, max_lines=999) + + return False -# Стартуем async def main(): filename = "ip.txt" cloudflare = None @@ -257,42 +303,45 @@ async def main(): gateway = None selected_services = ["Service"] mk_list_name = None + mk_comment = 'off' ken_gateway = None + if not os.path.exists(filename): + print(f"\n{red(f'Ошибка: файл {filename} не найден!')}") + print(f"{yellow('Инструкция:')}") + print(f"1. Создайте файл {green(filename)} в текущей директории") + print(f"2. Добавьте в него IP-адреса (по одному на строку) или текст содержащий IP-адреса") + print(f"3. Запустите скрипт снова") + return + ip_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b') - # Открываем файл и читаем строки with open(filename, 'r') as file: - # Создаем множество для хранения уникальных IP-адресов ips = set() - - # Проходим по каждой строке файла for line in file: - # Ищем все IP-адреса в строке found_ips = ip_pattern.findall(line) - # Добавляем найденные IP-адреса в множество ips.update(found_ips) - # Фильтр Cloudflare include_cloudflare = check_include_cloudflare(cloudflare) - if include_cloudflare: # Загрузка IP-адресов Cloudflare + if include_cloudflare: cloudflare_ips = await get_cloudflare_ips() else: cloudflare_ips = set() - # Удаляем IP-адреса Cloudflare ips -= cloudflare_ips with open(filename, 'w', encoding='utf-8') as file: for ip in sorted(ips): file.write(ip + '\n') - # Группировка IP-адресов в подсети subnet = subnet_input(subnet) - if subnet != '32': # Если не '32', вызываем функцию для агрегации - group_ips_in_subnets(filename, subnet) + if subnet != '32': + group_ips_in_subnets_optimized(filename, subnet) - process_file_format(filename, filetype, gateway, selected_services, mk_list_name, subnet, ken_gateway) + file_was_split = process_file_format(filename, filetype, gateway, selected_services, mk_list_name, mk_comment, subnet, ken_gateway) + + if not file_was_split: + print(f"\n{Style.BRIGHT}Результаты сохранены в файл:{Style.RESET_ALL} {filename}") if __name__ == "__main__": diff --git a/web/README.md b/web/README.md deleted file mode 100644 index 44c254f..0000000 --- a/web/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Попытка перенести DomainMapper на WEB платформу для размещения желающими на собсвтенном хостинге. - -Не уверен, что закончу начатое. - -Предложения в виде **pull requests** приветствуются. - - -``` -bash <(curl -s https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/refs/heads/main/web/web_install.sh) - -``` diff --git a/web/app.py b/web/app.py deleted file mode 100644 index 7e4a83c..0000000 --- a/web/app.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import subprocess -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -import uvicorn - -# Определение модели для данных запроса -class RunScriptRequest(BaseModel): - config: str - userId: str - -# Инициализация FastAPI приложения -app = FastAPI() - -@app.post("/run") -async def run_script(request: RunScriptRequest): - config_content = request.config - user_id = request.userId - - # Создание имени файла конфигурации - config_filename = f"config-id_{user_id}.ini" - try: - # Запись конфигурации в файл - with open(config_filename, 'w') as f: - f.write(config_content) - - # Выполнение команды через subprocess - result = subprocess.run( - ['python3', 'main.py', '-c', config_filename], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - - # Возвращение результатов выполнения скрипта - return {"stdout": result.stdout, "stderr": result.stderr} - - except Exception as e: - raise HTTPException(status_code=500, detail=f"Ошибка: {str(e)}") - -# Запуск приложения (для использования с Uvicorn) -if __name__ == "__main__": - # Запуск FastAPI с использованием Uvicorn - uvicorn.run(app, host="0.0.0.0", port=5000) diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 3da7a8e..0000000 --- a/web/index.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - DNS Resolver Settings - - - -

Настройки

-
-
-

- -
-

- -
- Исключить Cloudflare

- -
- До /16 (255.255.0.0)
- До /24 (255.255.255.0)
- Микс /24 и /32
- Не агрегировать

- -
- IP (только IP адреса)
- CIDR (%IP%/32)
- Windows Route (route add %IP% mask 255.255.255.255 %gateway%)
- Unix Route (ip route %IP%/32 %gateway%)
- Wireguard/AmneziaWG (%IP%/32, %IP%/32, и т.д...)
- Open VPN (push "route %IP% 255.255.255.255")
- Keenetic CLI (ip route %IP%/32 %gateway% auto !%commentary%)
- Mikrotik firewall (/ip/firewall/address-list add list=%commentary% address=%IP%/32)

- - - - - - -
- -
- - diff --git a/web/static/для статики JS b/web/static/для статики JS deleted file mode 100644 index e69de29..0000000 diff --git a/web/templates/для шаблонов b/web/templates/для шаблонов deleted file mode 100644 index e69de29..0000000 diff --git a/web/web_install.sh b/web/web_install.sh deleted file mode 100644 index f7e797f..0000000 --- a/web/web_install.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash - -set -e # Завершение скрипта при ошибке -set -u # Завершение при использовании необъявленных переменных - -# Переменные -USERNAME="test123" -APP_DIR="/home/$USERNAME/dns_resolver_app" -SERVICE_FILE="/etc/systemd/system/dns_resolver.service" -NGINX_CONF="/etc/nginx/sites-available/dns_resolver" -EMAIL_ADR="email@example.com" -DOMAIN_NAME="your-domain.com" - -# Проверка существования пользователя -if ! id "$USERNAME" &>/dev/null; then - echo "Пользователь $USERNAME не существует." - read -p "Хотите создать пользователя? (y/n): " CREATE_USER - if [[ "$CREATE_USER" =~ ^[Yy]$ ]]; then - sudo useradd -m -s /bin/bash "$USERNAME" - echo "Пользователь $USERNAME успешно создан." - else - echo "Скрипт завершён, так как пользователь не существует." - exit 1 - fi -fi - -# Убедиться, что пользователь $USERNAME и www-data имеют общую группу -sudo usermod -aG www-data "$USERNAME" - -# Обновление системы и установка зависимостей -echo "Обновляем систему и устанавливаем зависимости..." -sudo apt update && sudo apt upgrade -y -sudo apt install python3 python3-pip python3-venv gunicorn nginx certbot python3-certbot-nginx -y - -# Создание директории приложения -if [[ ! -d "$APP_DIR" ]]; then - echo "Создаем директорию приложения..." - sudo mkdir -p "$APP_DIR" - sudo chown -R "$USERNAME:www-data" "$APP_DIR" - sudo chmod -R 750 "$APP_DIR" -else - echo "Директория приложения уже существует. Пропускаем." -fi - -# Создание виртуального окружения от имени www-data -if [[ ! -d "$APP_DIR/venv" ]]; then - echo "Создаем виртуальное окружение..." - sudo -u www-data python3 -m venv "$APP_DIR/venv" - sudo chown -R "$USERNAME:www-data" "$APP_DIR/venv" - sudo chmod -R 750 "$APP_DIR/venv" -else - echo "Виртуальное окружение уже существует. Пропускаем." -fi - -# Загрузка файла requirements.txt -REQUIREMENTS_URL="https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/refs/heads/main/requirements.txt" -if curl --head --fail "$REQUIREMENTS_URL" &>/dev/null; then - curl -o "$APP_DIR/requirements.txt" "$REQUIREMENTS_URL" - echo "Файл requirements.txt успешно загружен." -else - echo "Ошибка: Файл requirements.txt недоступен." - exit 1 -fi - -# Установка зависимостей Python от имени www-data -echo "Устанавливаем зависимости Python..." -sudo -u www-data bash -c "source $APP_DIR/venv/bin/activate && pip install -r $APP_DIR/requirements.txt fastapi uvicorn pydantic gunicorn" - -# Загрузка файлов приложения -FILES=("index.html" "app.py" "main.py") -for FILE in "${FILES[@]}"; do - URL="https://raw.githubusercontent.com/Ground-Zerro/DomainMapper/refs/heads/main/web/$FILE" - if curl --head --fail "$URL" &>/dev/null; then - curl -o "$APP_DIR/$FILE" "$URL" - echo "Файл $FILE успешно загружен." - sudo chown "$USERNAME:www-data" "$APP_DIR/$FILE" - sudo chmod 640 "$APP_DIR/$FILE" - else - echo "Ошибка: Файл $FILE недоступен." - fi -done - -# Проверка прав доступа -sudo chown -R "$USERNAME:www-data" "$APP_DIR" -sudo chmod -R 750 "$APP_DIR" - -# Создание системного сервиса -echo "Создаем системный сервис..." -sudo bash -c "cat < $SERVICE_FILE -[Unit] -Description=DNS Resolver Web App -After=network.target - -[Service] -User=www-data -Group=www-data -WorkingDirectory=$APP_DIR -ExecStart=$APP_DIR/venv/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:5000 app:app - -[Install] -WantedBy=multi-user.target -EOF" - -sudo systemctl daemon-reload -sudo systemctl enable --now dns_resolver - -# Настройка Nginx -if [[ ! -f "$NGINX_CONF" ]]; then - echo "Настраиваем Nginx..." - sudo bash -c "cat < $NGINX_CONF -server { - listen 80; - server_name $DOMAIN_NAME; - - root $APP_DIR; - index index.html; - - location / { - try_files \$uri /index.html; - } - - location /run { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host \$host; - proxy_set_header X-Real-IP \$remote_addr; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto \$scheme; - } - - error_page 404 /index.html; -} -EOF" - - sudo ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/ - sudo nginx -t && sudo systemctl restart nginx -else - echo "Конфигурация Nginx уже существует. Пропускаем." -fi - -# Настройка HTTPS -echo "Настраиваем HTTPS..." -sudo certbot --nginx -n --agree-tos --email "$EMAIL_ADR" -d "$DOMAIN_NAME" - -echo "Скрипт выполнен успешно. Приложение доступно по адресу https://$DOMAIN_NAME"