Initial commit: Timeweb floating IP grabber
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
cfdfd27196
7 changed files with 275 additions and 0 deletions
9
.env.example
Normal file
9
.env.example
Normal 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
16
.gitignore
vendored
Normal 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
60
README.md
Normal 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
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
requests[socks]
|
||||
python-dotenv
|
||||
165
tw.py
Normal file
165
tw.py
Normal 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
14
tw.service
Normal 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
9
tw.timer
Normal 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
|
||||
Loading…
Reference in a new issue