From c38e6552fea926c8aacb6ebb75a050cec5fd1bfe Mon Sep 17 00:00:00 2001 From: Sazonov Andrey Date: Fri, 20 Mar 2026 16:44:33 +0300 Subject: [PATCH] fixik --- redirector.py | 154 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 110 insertions(+), 44 deletions(-) diff --git a/redirector.py b/redirector.py index 7836718..0127558 100644 --- a/redirector.py +++ b/redirector.py @@ -5,6 +5,8 @@ import asyncio import configparser import ipaddress import os +import logging +from logging.handlers import RotatingFileHandler from asyncio import Semaphore import dns.asyncresolver @@ -13,11 +15,41 @@ import httpx BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -def read_config(cfg_file): +# ================= LOGGING ================= +def setup_logging(): + log_dir = os.path.join(BASE_DIR, "logs") + os.makedirs(log_dir, exist_ok=True) + + log_file = os.path.join(log_dir, "redirector.log") + + handler = RotatingFileHandler( + log_file, + maxBytes=2 * 1024 * 1024, # 2MB + backupCount=5 + ) + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[handler, logging.StreamHandler()] + ) + + +# ================= CONFIG ================= +def read_config(cfg_file, section=None): config = configparser.ConfigParser() config.read(cfg_file) - cfg = config['Redirector'] + if section: + section = section.capitalize() + + if section and section in config: + cfg = config[section] + logging.info(f"Используется конфиг: {section}") + else: + default_section = config.sections()[0] + cfg = config[default_section] + logging.info(f"Используется дефолтный конфиг: {default_section}") return { "threads": int(cfg.get("threads", "50")), @@ -35,28 +67,32 @@ def load_forced_list(cfg): domains = [] ips = set() - with open(cfg["filename"], "r", encoding="utf-8") as f: - for line in f: - line = line.strip() + try: + with open(cfg["filename"], "r", encoding="utf-8") as f: + for line in f: + line = line.strip() - if not line or line.startswith("#"): - continue + if not line or line.startswith("#"): + continue - try: - ip = ipaddress.ip_address(line) - ips.add(str(ip)) - continue - except ValueError: - pass + try: + ip = ipaddress.ip_address(line) + ips.add(str(ip)) + continue + except ValueError: + pass - try: - net = ipaddress.ip_network(line, strict=False) - ips.add(str(net)) - continue - except ValueError: - pass + try: + net = ipaddress.ip_network(line, strict=False) + ips.add(str(net)) + continue + except ValueError: + pass - domains.append(line) + domains.append(line) + + except Exception as e: + logging.error(f"Ошибка чтения входящих данных: {e}") return domains, ips @@ -72,12 +108,38 @@ def get_resolvers(): ] +def is_public_ip(ip_str): + try: + if "/" in ip_str: + net = ipaddress.ip_network(ip_str, strict=False) + return not ( + net.is_private + or net.is_loopback + or net.is_link_local + or net.is_multicast + or net.is_reserved + ) + else: + ip = ipaddress.ip_address(ip_str) + return not ( + ip.is_private + or ip.is_loopback + or ip.is_link_local + or ip.is_multicast + or ip.is_reserved + or ip.is_unspecified + ) + except ValueError: + return False + + async def resolve_domain(domain, resolver, semaphore): async with semaphore: try: answer = await resolver.resolve(domain) return [r.address for r in answer] - except Exception: + except Exception as e: + logging.warning(f"DNS ошибка {domain}: {e}") return [] @@ -88,28 +150,29 @@ async def get_cloudflare_ips(): result = set() for line in r.text.splitlines(): - line = line.strip() - if "/" in line: - net = ipaddress.ip_network(line) - for ip in net: - result.add(str(ip)) + net = ipaddress.ip_network(line.strip()) + for ip in net: + result.add(str(ip)) return result async def main(): + setup_logging() + parser = argparse.ArgumentParser() parser.add_argument("-c", "--config", default=os.path.join(BASE_DIR, "config.ini")) + parser.add_argument("--env", help="Config section: test/prod") args = parser.parse_args() - cfg = read_config(args.config) + cfg = read_config(args.config, args.env) semaphore = Semaphore(cfg["threads"]) domains, ready_ips = load_forced_list(cfg) resolvers = get_resolvers() - print(f"Домены: {len(domains)} | IP/CIDR: {len(ready_ips)}") + logging.info(f"Домены: {len(domains)} | IP/CIDR: {len(ready_ips)}") tasks = [] @@ -125,28 +188,33 @@ async def main(): all_ips = set(ready_ips) for res in results: - for ip in res: - all_ips.add(ip) + all_ips.update(res) - print(f"Всего IP до фильтра: {len(all_ips)}") + logging.info(f"Всего IP до фильтра: {len(all_ips)}") if cfg["exclude_cloudflare"]: cf_ips = await get_cloudflare_ips() before = len(all_ips) all_ips = {ip for ip in all_ips if ip not in cf_ips} - print(f"Удалено Cloudflare IP: {before - len(all_ips)}") + logging.info(f"Удалено Cloudflare IP: {before - len(all_ips)}") - print(f"Финальных IP: {len(all_ips)}") + before_private = len(all_ips) + all_ips = {ip for ip in all_ips if is_public_ip(ip)} + logging.info(f"Удалено приватных IP: {before_private - len(all_ips)}") + logging.info(f"Финальных IP: {len(all_ips)}") - with open(cfg["script"], "w", encoding="utf-8") as f: + result_file = os.path.join(BASE_DIR, "result_ips.txt") + with open(result_file, "w") as f: + for ip in sorted(all_ips): + f.write(ip + "\n") + + with open(cfg["script"], "w") as f: f.write( f'#!/bin/bash\n\n' - f'# Добавляем правило если нет\n' f'if ! ip rule list | grep -q "lookup {cfg["table"]}"; then\n' f' ip rule add table {cfg["table"]} priority 101\n' f'fi\n\n' - f'# Чистим таблицу\n' f'ip route flush table {cfg["table"]}\n\n' ) @@ -158,24 +226,22 @@ async def main(): os.chmod(cfg["script"], 0o755) - with open(cfg["rollback_script"], "w", encoding="utf-8") as f: + with open(cfg["rollback_script"], "w") as f: f.write( f'#!/bin/bash\n\n' - f'# Чистим таблицу\n' - f'ip route flush table {cfg["table"]}\n\n' - f'# Удаляем правило если есть\n' + f'ip route flush table {cfg["table"]}\n' f'ip rule del table {cfg["table"]} priority 101 2>/dev/null\n' ) os.chmod(cfg["rollback_script"], 0o755) - print(f"ON script: {cfg['script']}") - print(f"OFF script: {cfg['rollback_script']}") + logging.info(f"ON: {cfg['script']}") + logging.info(f"OFF: {cfg['rollback_script']}") if cfg["run"]: - print("Запуск скрипта...") + logging.info("Запуск post-команды") os.system(cfg["run"]) if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file