From cfdfd27196fd9f11f050809d780e7d055f4b5aa6 Mon Sep 17 00:00:00 2001 From: q Date: Sun, 31 May 2026 19:45:26 +0300 Subject: [PATCH] Initial commit: Timeweb floating IP grabber Co-Authored-By: Claude Opus 4.8 --- .env.example | 9 +++ .gitignore | 16 +++++ README.md | 60 +++++++++++++++++ requirements.txt | 2 + tw.py | 165 +++++++++++++++++++++++++++++++++++++++++++++++ tw.service | 14 ++++ tw.timer | 9 +++ 7 files changed, 275 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 tw.py create mode 100644 tw.service create mode 100644 tw.timer diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fc10692 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +AVAILABILITY_ZONE=spb-3 +IS_DDOS_GUARD=false +MAX_ATTEMPTS=10 +ACCOUNTS_FILE=accounts.txt +PROXIES_FILE=proxies.txt + +# Telegram: получить токен у @BotFather, chat_id — у @userinfobot +TG_BOT_TOKEN= +TG_CHAT_ID= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4cc09a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Secrets — do NOT commit +.env +accounts.txt +proxies.txt + +# Python +venv/ +.venv/ +__pycache__/ +*.py[cod] +*.egg-info/ + +# Editors / tooling +.claude/ +.idea/ +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..900bb67 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# timeweb-loodka + +Скрипт для получения «floating IP» в облаке Timeweb из нужных подсетей. +Для каждого аккаунта по кругу создаёт floating IP, проверяет попадание в заданный +диапазон подсетей и удаляет неподходящие. О результатах уведомляет в Telegram. + +## Установка + +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Настройка + +1. Скопируйте пример конфигурации и заполните значения: + + ```bash + cp .env.example .env + ``` + + | Переменная | Описание | + | ------------------- | ----------------------------------------------------- | + | `AVAILABILITY_ZONE` | Зона доступности (например `spb-3`) | + | `IS_DDOS_GUARD` | Включить DDoS-Guard (`true`/`false`) | + | `MAX_ATTEMPTS` | Сколько раз пытаться получить подходящий IP | + | `ACCOUNTS_FILE` | Файл с API-ключами аккаунтов (по одному на строку) | + | `PROXIES_FILE` | Файл с прокси (по одному на строку, опционально) | + | `TG_BOT_TOKEN` | Токен Telegram-бота (у `@BotFather`) | + | `TG_CHAT_ID` | Chat ID для уведомлений (у `@userinfobot`) | + +2. Создайте `accounts.txt` — по одному API-ключу Timeweb на строку. + +3. (Опционально) создайте `proxies.txt` — по одному прокси на строку, например: + + ``` + socks5://user:pass@host:port + ``` + + Прокси назначаются аккаунтам по кругу. Если файл отсутствует — запросы идут напрямую. + +> Файлы `.env`, `accounts.txt` и `proxies.txt` содержат секреты и игнорируются git. + +## Запуск + +```bash +python tw.py +``` + +## Запуск по расписанию (systemd) + +В комплекте есть `tw.service` и `tw.timer` (ежедневный запуск в 08:00). +Отредактируйте пути/пользователя под своё окружение, затем: + +```bash +sudo cp tw.service tw.timer /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now tw.timer +``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ae7c3e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests[socks] +python-dotenv diff --git a/tw.py b/tw.py new file mode 100644 index 0000000..e5fd115 --- /dev/null +++ b/tw.py @@ -0,0 +1,165 @@ +import ipaddress +import requests +from dotenv import load_dotenv +import os +import sys +import io + +if isinstance(sys.stdout, io.TextIOWrapper): + sys.stdout.reconfigure(encoding="utf-8") + + +load_dotenv() + +# pip install python-dotenv requests +# Прокси назначаются аккаунтам по кругу — если прокси меньше чем аккаунтов, они переиспользуются. +# Если файл proxies.txt отсутствует, все запросы идут без прокси. Каждый аккаунт использует отдельную requests.Session, что изолирует соединения. + +BASE_URL = "https://api.timeweb.cloud/api/v1" + +AVAILABILITY_ZONE = os.getenv("AVAILABILITY_ZONE", "spb-3") +IS_DDOS_GUARD = os.getenv("IS_DDOS_GUARD", "false").lower() == "true" +MAX_ATTEMPTS = int(os.getenv("MAX_ATTEMPTS", "10")) +ACCOUNTS_FILE = os.getenv("ACCOUNTS_FILE", "accounts.txt") +PROXIES_FILE = os.getenv("PROXIES_FILE", "proxies.txt") + +TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "") +TG_CHAT_ID = os.getenv("TG_CHAT_ID", "") + +DESIRED_SUBNETS = [ + "81.200.148.0/24", + "81.200.149.0/24", + "81.200.150.0/24", + "81.200.151.0/24", + "94.228.117.0/24", + "185.200.242.0/24", +] + + +def send_telegram(text: str): + if not TG_BOT_TOKEN or not TG_CHAT_ID: + return + try: + requests.post( + f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage", + json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "HTML"}, + timeout=10, + ) + except Exception as e: + print(f" [TG] Не удалось отправить сообщение: {e}") + + +def load_lines(filepath: str) -> list[str]: + with open(filepath, "r", encoding="utf-8") as f: + return [line.strip() for line in f if line.strip()] + + +def ip_in_desired_subnets(ip: str) -> bool: + addr = ipaddress.ip_address(ip) + return any(addr in ipaddress.ip_network(subnet) for subnet in DESIRED_SUBNETS) + + +def make_session(proxy: str | None) -> requests.Session: + session = requests.Session() + if proxy: + session.proxies = {"http": proxy, "https": proxy} + return session + + +def create_floating_ip(session: requests.Session, headers: dict) -> dict: + payload = { + "availability_zone": AVAILABILITY_ZONE, + "is_ddos_guard": IS_DDOS_GUARD, + } + response = session.post(f"{BASE_URL}/floating-ips", headers=headers, json=payload) + if not response.ok: + print(f" Ошибка {response.status_code}: {response.text}") + response.raise_for_status() + return response.json()["ip"] + + +def delete_floating_ip(session: requests.Session, headers: dict, floating_ip_id: str): + response = session.delete( + f"{BASE_URL}/floating-ips/{floating_ip_id}", headers=headers + ) + if not response.ok: + print( + f" Не удалось удалить IP {floating_ip_id}: {response.status_code} {response.text}" + ) + + +def get_ip_from_desired_range( + session: requests.Session, headers: dict, account_label: str +) -> dict | None: + for attempt in range(1, MAX_ATTEMPTS + 1): + ip_data = create_floating_ip(session, headers) + ip_addr = ip_data["ip"] + print(f" [{account_label}] Попытка {attempt}: получен IP {ip_addr}") + + if ip_in_desired_subnets(ip_addr): + print(f" [{account_label}] ✅ IP {ip_addr} входит в нужный диапазон!") + return ip_data + else: + print(f" [{account_label}] ❌ IP {ip_addr} не подходит, удаляем...") + delete_floating_ip(session, headers, ip_data["id"]) + + print(f" [{account_label}] Не удалось получить IP за {MAX_ATTEMPTS} попыток.") + return None + + +if __name__ == "__main__": + tokens = load_lines(ACCOUNTS_FILE) + proxies = load_lines(PROXIES_FILE) if os.path.exists(PROXIES_FILE) else [] + + print(f"Загружено аккаунтов: {len(tokens)}, прокси: {len(proxies)}\n") + send_telegram( + f"🚀 tw.py запущен\n" + f"Аккаунтов: {len(tokens)}, прокси: {len(proxies)}\n" + f"Зона: {AVAILABILITY_ZONE}, DDoS-Guard: {IS_DDOS_GUARD}, попыток: {MAX_ATTEMPTS}" + ) + + found = [] + failed = [] + + for i, token in enumerate(tokens): + proxy = proxies[i % len(proxies)] if proxies else None + label = f"аккаунт {i + 1}" + + print(f"▶ Обработка {label} | прокси: {proxy or 'нет'}") + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + session = make_session(proxy) + + try: + result = get_ip_from_desired_range(session, headers, label) + if result: + print(f" Итоговый IP: {result['ip']}, ID: {result['id']}\n") + found.append((label, result["ip"], result["id"])) + send_telegram( + f"✅ {label}: получен IP {result['ip']}\n" + f"ID: {result['id']}" + ) + else: + failed.append(label) + send_telegram( + f"❌ {label}: не удалось получить подходящий IP " + f"за {MAX_ATTEMPTS} попыток" + ) + except Exception as e: + print(f" Критическая ошибка для {label}: {e}\n") + failed.append(label) + send_telegram(f"💥 {label}: критическая ошибка — {e}") + + summary_lines = [f"📊 Итог: найдено {len(found)}, не найдено {len(failed)}"] + if found: + summary_lines.append("\nУспешные:") + for lbl, ip, fid in found: + summary_lines.append(f" • {lbl}: {ip}") + if failed: + summary_lines.append("\nНеудачные:") + for lbl in failed: + summary_lines.append(f" • {lbl}") + send_telegram("\n".join(summary_lines)) diff --git a/tw.service b/tw.service new file mode 100644 index 0000000..574d986 --- /dev/null +++ b/tw.service @@ -0,0 +1,14 @@ +[Unit] +Description=Timeweb floating IP grabber +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +User=q +Group=q +WorkingDirectory=/opt/tw +ExecStart=/opt/tw/venv/bin/python /opt/tw/tw.py +EnvironmentFile=/opt/tw/.env +StandardOutput=journal +StandardError=journal diff --git a/tw.timer b/tw.timer new file mode 100644 index 0000000..d966e2a --- /dev/null +++ b/tw.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Run tw.py every day at 08:00 + +[Timer] +OnCalendar=*-*-* 08:00:00 +Persistent=true + +[Install] +WantedBy=timers.target