Initial commit: Timeweb floating IP grabber

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
q 2026-05-31 19:45:26 +03:00
commit cfdfd27196
7 changed files with 275 additions and 0 deletions

9
.env.example Normal file
View file

@ -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=

16
.gitignore vendored Normal file
View file

@ -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/

60
README.md Normal file
View file

@ -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
```

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
requests[socks]
python-dotenv

165
tw.py Normal file
View file

@ -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"🚀 <b>tw.py запущен</b>\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"✅ <b>{label}</b>: получен IP <code>{result['ip']}</code>\n"
f"ID: <code>{result['id']}</code>"
)
else:
failed.append(label)
send_telegram(
f"❌ <b>{label}</b>: не удалось получить подходящий IP "
f"за {MAX_ATTEMPTS} попыток"
)
except Exception as e:
print(f" Критическая ошибка для {label}: {e}\n")
failed.append(label)
send_telegram(f"💥 <b>{label}</b>: критическая ошибка — {e}")
summary_lines = [f"📊 <b>Итог:</b> найдено {len(found)}, не найдено {len(failed)}"]
if found:
summary_lines.append("\n<b>Успешные:</b>")
for lbl, ip, fid in found:
summary_lines.append(f"{lbl}: <code>{ip}</code>")
if failed:
summary_lines.append("\n<b>Неудачные:</b>")
for lbl in failed:
summary_lines.append(f"{lbl}")
send_telegram("\n".join(summary_lines))

14
tw.service Normal file
View file

@ -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

9
tw.timer Normal file
View file

@ -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