From db9f9d06bc79723889d18a748430e70dd38b6a54 Mon Sep 17 00:00:00 2001 From: Sazonov Andrey Date: Wed, 1 Apr 2026 17:38:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20=D1=80=D0=BE=D1=83=D1=82=D0=B8=D0=BD=D0=B3=20?= =?UTF-8?q?=D0=B2=20blackhole?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++ config.ini.example | 10 ++++ redirector.py | 119 ++++++++++++++++++++++----------------------- 3 files changed, 84 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 58e16f9..53f5c01 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ - резолва доменов (с разных DNS) - объединения IP / CIDR - генерации routing-скрипта для policy routing +- дополнительно можно использовать секцию `[Blackhole]` для блокировки нежелательных IP --- @@ -42,6 +43,21 @@ run = # Shell команда которая выполнитс ``` - Можно добавить несколько секций для нескольких конфигураций +#### Пример секции `[Blackhole]` +```ini +[Blackhole] +exclude_cloudflare = yes +filename = blackhole_list.txt +script = blackhole_ON.sh +rollback_script = blackhole_OFF.sh +threads = 50 +table = 1001 +priority = 10 +run = +``` +- Все домены и IP из файла `blackhole_list.txt` будут недоступны. + + --- ## Входной файл diff --git a/config.ini.example b/config.ini.example index 42be887..a07c91e 100644 --- a/config.ini.example +++ b/config.ini.example @@ -9,3 +9,13 @@ priority = gateway = interface = run = + +[Blackhole] +exclude_cloudflare = +filename = +script = +rollback_script = +threads = +table = +priority = +run = \ No newline at end of file diff --git a/redirector.py b/redirector.py index 0f38a77..541f106 100644 --- a/redirector.py +++ b/redirector.py @@ -56,8 +56,11 @@ def read_config(cfg_file, section=None): for sec in sections: cfg = config[sec] + is_blackhole = sec.lower() == "blackhole" + configs.append({ "name": sec, + "is_blackhole": is_blackhole, "threads": int(cfg.get("threads", "50")), "filename": os.path.join(BASE_DIR, cfg.get("filename", f"{sec}_forced_list.txt")), "script": os.path.join(BASE_DIR, cfg.get("script", f"{sec}_ON.sh")), @@ -66,7 +69,7 @@ def read_config(cfg_file, section=None): "gateway": cfg.get("gateway", ""), "interface": cfg.get("interface", ""), "table": cfg.get("table", "1010"), - "priority": cfg.get("priority", "101"), + "priority": cfg.get("priority", "10" if is_blackhole else "101"), "run": cfg.get("run", "") }) @@ -104,8 +107,7 @@ def load_forced_list(cfg): except Exception as e: logging.error(f"Ошибка чтения входящих данных: {e}") exit(1) - if len(domains) == 0 and len(ips) == 0: - logging.info(f"В файле {cfg['filename']} не найдено доменов и IP") + return domains, ips @@ -154,11 +156,8 @@ async def resolve_domain(domain, dns_name, server, semaphore): try: answer = await resolver.resolve(domain) return [r.address for r in answer] - except Exception as e: - logging.warning( - f"DNS ошибка [{dns_name} - {server} | {domain}]: {e}" - ) + logging.warning(f"DNS ошибка [{dns_name} - {server} | {domain}]: {e}") return [] @@ -173,7 +172,6 @@ def normalize_ip(ip): else: return f"{addr}/32" return ip - except ValueError: return ip @@ -192,6 +190,48 @@ async def get_cloudflare_ips(): return result +# ================= SCRIPT GENERATORS ================= +def generate_policy_script(cfg, ips): + with open(cfg["script"], "w") as f: + f.write( + f'#!/bin/bash\n\n' + f'if ! ip rule list | grep -q "lookup {cfg["table"]}"; then\n' + f' ip rule add table {cfg["table"]} priority {cfg["priority"]}\n' + f'fi\n\n' + f'ip route flush table {cfg["table"]}\n\n' + ) + + for ip in ips: + if cfg["gateway"]: + f.write(f'ip route replace {ip} via {cfg["gateway"]} table {cfg["table"]}\n') + else: + f.write(f'ip route replace {ip} dev {cfg["interface"]} table {cfg["table"]}\n') + + +def generate_blackhole_script(cfg, ips): + with open(cfg["script"], "w") as f: + f.write( + f'#!/bin/bash\n\n' + f'if ! ip rule list | grep -q "lookup {cfg["table"]}"; then\n' + f' ip rule add table {cfg["table"]} priority {cfg["priority"]}\n' + f'fi\n\n' + f'ip route flush table {cfg["table"]}\n\n' + ) + + for ip in ips: + f.write(f'ip route replace blackhole {ip} table {cfg["table"]}\n') + + +def generate_rollback(cfg): + with open(cfg["rollback_script"], "w") as f: + f.write( + f'#!/bin/bash\n\n' + f'ip route flush table {cfg["table"]}\n' + f'ip rule del table {cfg["table"]} 2>/dev/null\n' + ) + + +# ================= MAIN ================= async def main(): setup_logging() @@ -210,85 +250,42 @@ async def main(): domains, ready_ips = load_forced_list(cfg) resolvers = get_resolvers() - logging.info(f"Домены: {len(domains)} | IP/CIDR: {len(ready_ips)}") - tasks = [] - for dns_name, servers in resolvers: for server in servers: for domain in domains: - tasks.append( - resolve_domain(domain, dns_name, server, semaphore) - ) + tasks.append(resolve_domain(domain, dns_name, server, semaphore)) results = await asyncio.gather(*tasks) all_ips = set(ready_ips) - for res in results: all_ips.update(res) - 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} - logging.info(f"Удалено Cloudflare IP: {before - 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)}") + normalized_ips = sorted({normalize_ip(ip) for ip in all_ips}) - normalized_ips = {normalize_ip(ip) for ip in all_ips} - result_file = os.path.join(BASE_DIR, f"{cfg['name']}_result_ips.txt") - with open(result_file, "w") as f: - for ip in sorted(normalized_ips): - f.write(ip + "\n") + if cfg["is_blackhole"]: + generate_blackhole_script(cfg, normalized_ips) + else: + if not cfg["gateway"] and not cfg["interface"]: + logging.error("Не указан Gateway или Interface") + exit(1) + generate_policy_script(cfg, normalized_ips) - if not cfg["gateway"] and not cfg["interface"]: - logging.error("Не указан Gateway и Interface, выход.") - exit(1) - - with open(cfg["script"], "w") as f: - f.write( - f'#!/bin/bash\n\n' - f'if ! ip rule list | grep -q "lookup {cfg["table"]}"; then\n' - f' ip rule add table {cfg["table"]} priority {cfg["priority"]}\n' - f'fi\n\n' - f'ip route flush table {cfg["table"]}\n\n' - ) - - for ip in sorted(normalized_ips): - if "/" in ip: - if cfg["gateway"]: - f.write(f'ip route replace {ip} via {cfg["gateway"]} table {cfg["table"]}\n') - else: - f.write(f'ip route replace {ip} dev {cfg["interface"]} table {cfg["table"]}\n') - else: - if cfg["gateway"]: - f.write(f'ip route replace {ip}/32 via {cfg["gateway"]} table {cfg["table"]}\n') - else: - f.write(f'ip route replace {ip}/32 dev {cfg["interface"]} table {cfg["table"]}\n') + generate_rollback(cfg) os.chmod(cfg["script"], 0o755) - - with open(cfg["rollback_script"], "w") as f: - f.write( - f'#!/bin/bash\n\n' - f'ip route flush table {cfg["table"]}\n' - f'ip rule del table {cfg["table"]} 2>/dev/null\n' - ) - os.chmod(cfg["rollback_script"], 0o755) logging.info(f"ON: {cfg['script']}") logging.info(f"OFF: {cfg['rollback_script']}") if cfg["run"]: - logging.info("Запуск post-команды") os.system(cfg["run"])