feat: Zero-Exposure Pelmeni Shield VPN Deployment Stack
This commit is contained in:
commit
4b361226da
8 changed files with 691 additions and 0 deletions
14
.env.example
Normal file
14
.env.example
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Доменное имя вашего VPN сервера (должно резолвиться на IP хоста!)
|
||||
DOMAIN=endpoint.pelmeni-club.com
|
||||
|
||||
# Контактный email для регистрации сертификатов Let's Encrypt
|
||||
CERTBOT_EMAIL=admin@pelmeni-club.com
|
||||
|
||||
# Секретный токен для выдачи подписок через Nginx
|
||||
# Ссылка на подписку будет: https://${DOMAIN}/api/v1/recipes/secret_${SUB_TOKEN}/${CLIENT_SUB_ID}
|
||||
SUB_TOKEN=9f8e7d6c5b4a3210
|
||||
|
||||
# Telegram-оповещения о сканировании РКН (опционально)
|
||||
TELEGRAM_BOT_TOKEN=8065539545:AAECBnyjinUVVGv3vhtoWZ99tUmnT9sze-k
|
||||
TELEGRAM_CHAT_ID=-1004488599816
|
||||
TELEGRAM_TOPIC_ID=18
|
||||
55
README.md
Normal file
55
README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# 🥟 Pelmeni Shield VPN: Zero-Exposure Deployment Stack
|
||||
|
||||
> **Архитектурная концепция:** Полная маскировка VPN-узла под легальный кулинарный портал ценителей сибирских пельменей с нулевым открытием служебных портов наружу и активным отпором Active Probing со стороны РКН / ЦМУ ССОП.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Ключевые принципы безопасности
|
||||
|
||||
1. **Zero-Exposure (Только порты 80 и 443 наружу):**
|
||||
- Порт `2053` панели 3X-UI привязан **СТРОГО к `127.0.0.1`** (доступ для сисадмина только через SSH-туннель).
|
||||
- Порт подписок **НЕ выставлен наружу отдельным портом**. Выдача идет через Nginx по секретному пути `https://${DOMAIN}/api/v1/recipes/secret_${SUB_TOKEN}/${CLIENT_SUB_ID}`.
|
||||
2. **Легитимный SSL Let's Encrypt вместо чужого SNI:**
|
||||
- Выпускается реальный сертификат на ваш домен/поддомен.
|
||||
- Сканеры Censys, Shodan и РКН видят 100% совпадение IP, PTR и SAN сертификата.
|
||||
3. **Бесшовный Fallback на сайт любителей пельменей:**
|
||||
- Все входящие подключения, не являющиеся валидным VLESS-клиентом (боты, сканеры, веб-браузеры), прозрачно сбрасываются Xray на внутренний Nginx (`127.0.0.1:8080`), который отдает полноценный сайт с рецептами и форумом.
|
||||
4. **Активный отпор Active Probing:**
|
||||
- Официальная подсеть сканеров РКН/ГРЧЦ (`212.192.156.0/22`, AS61280) жестко блокируется в `iptables` с отправкой статистики в Telegram топик 18 супергруппы «Майа-Штаб».
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт на новом VPS
|
||||
|
||||
### 1. Подготовка домена
|
||||
Направьте DNS A-запись вашего домена (например, `vpn.pelmeni-club.com`) на публичный IP вашего VPS.
|
||||
|
||||
### 2. Клонирование и настройка
|
||||
```bash
|
||||
git clone http://192.168.10.105:3010/matrixhasyou/pelmeni-shield-vpn.git
|
||||
cd pelmeni-shield-vpn
|
||||
|
||||
# Скопируйте и настройте переменные окружения:
|
||||
cp .env.example .env
|
||||
nano .env # Укажите DOMAIN, CERTBOT_EMAIL и SUB_TOKEN
|
||||
```
|
||||
|
||||
### 3. Автоматический запуск
|
||||
```bash
|
||||
bash scripts/init.sh
|
||||
```
|
||||
|
||||
### 4. Вход в панель 3X-UI через безопасный SSH-туннель
|
||||
```bash
|
||||
# На вашем ПК:
|
||||
ssh -L 2053:127.0.0.1:2053 root@IP_СЕРВЕРА
|
||||
|
||||
# В браузере:
|
||||
http://localhost:2053
|
||||
```
|
||||
|
||||
### 5. Мониторинг пробинга РКН (Cron)
|
||||
```bash
|
||||
# Запуск каждые 15 минут в crontab:
|
||||
*/15 * * * * python3 /path/to/pelmeni-shield-vpn/scripts/rkn_probe_monitor.py >/dev/null 2>&1
|
||||
```
|
||||
60
docker-compose.yml
Normal file
60
docker-compose.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
# 1. 3X-UI: Xray-core движок + скрытая веб-панель
|
||||
3x-ui:
|
||||
image: ghcr.io/mhsanaei/3x-ui:latest
|
||||
container_name: 3x-ui-shield
|
||||
restart: always
|
||||
environment:
|
||||
- XRAY_VMESS_AEAD_FORCED=false
|
||||
volumes:
|
||||
- ./3x-ui:/etc/x-ui/
|
||||
- ./certbot/certs:/root/cert/
|
||||
ports:
|
||||
- "443:443" # Xray VLESS (единственный открытый защищенный порт)
|
||||
- "127.0.0.1:2053:2053" # Панель 3X-UI СТРОГО на localhost (доступ через SSH-туннель!)
|
||||
networks:
|
||||
- shield_net
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# 2. Nginx Decoy: сайт клуба любителей пельменей + скрытый шлюз подписок
|
||||
nginx-decoy:
|
||||
image: nginx:alpine
|
||||
container_name: nginx-decoy
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80" # HTTP для ACME certbot и редиректа на HTTPS
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./nginx/site:/usr/share/nginx/html:ro
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
networks:
|
||||
- shield_net
|
||||
depends_on:
|
||||
- 3x-ui
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# 3. Certbot: автоматический выпуск и продление SSL Let's Encrypt
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: certbot-ssl
|
||||
volumes:
|
||||
- ./certbot/certs:/etc/letsencrypt/
|
||||
- ./certbot/www:/var/www/certbot/
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12d & wait $${!}; done;'"
|
||||
networks:
|
||||
- shield_net
|
||||
|
||||
networks:
|
||||
shield_net:
|
||||
name: shield_net
|
||||
driver: bridge
|
||||
47
nginx/conf.d/default.conf
Normal file
47
nginx/conf.d/default.conf
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# ========================================================
|
||||
# Pelmeni Shield Nginx Configuration
|
||||
# ========================================================
|
||||
|
||||
# 1. Порт 80: Let's Encrypt ACME Challenge + Редирект на HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Порт 8080: Внутренний Fallback для Xray (Сеть shield_net)
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
server_name _;
|
||||
|
||||
# Корневой сайт: Клуб любителей сибирских пельменей
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Секретный шлюз выдачи подписок 3X-UI
|
||||
# Снаружи выглядит как вызов API рецептов:
|
||||
# URL: https://DOMAIN/api/v1/recipes/secret_TOKEN/SUB_ID
|
||||
location ~ ^/api/v1/recipes/secret_([a-zA-Z0-9]+)/(.*)$ {
|
||||
proxy_pass http://3x-ui:2053/sub/$2;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 15s;
|
||||
}
|
||||
|
||||
# Защита от перебора: любая ошибка выдает главную страницу пельменей
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /index.html;
|
||||
}
|
||||
91
nginx/site/index.html
Normal file
91
nginx/site/index.html
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Клуб любителей сибирских пельменей — Традиции, Рецепты и Форум мастеров</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">🥟</span>
|
||||
<div class="logo-text">
|
||||
<span class="brand">СИБИРСКИЙ ПЕЛЬМЕНЬ</span>
|
||||
<span class="tagline">Всероссийское сообщество ценителей традиционной лепки</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<a href="#recipes">Рецепты теста</a>
|
||||
<a href="#fillings">Фарш и начинки</a>
|
||||
<a href="#secrets">Секреты лепки</a>
|
||||
<a href="#forum">Форум ценителей</a>
|
||||
<a href="#contacts">О клубе</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<section class="hero">
|
||||
<div class="hero-badge">Традиционный сибирский рецепт 1889 года</div>
|
||||
<h1>Настоящий пельмень любит мороз, крепкое тесто и три вида мяса</h1>
|
||||
<p>Наш клуб объединяет более 14 000 кулинаров и любителей пельменного дела по всей стране. Делимся секретами правильного замеса, выдерживания фарша на колотом льду и тонкостями подачи.</p>
|
||||
</section>
|
||||
|
||||
<section class="grid-features" id="recipes">
|
||||
<div class="card">
|
||||
<div class="card-num">01</div>
|
||||
<h3>Идеальное тесто на ледяной воде</h3>
|
||||
<p>Мука высшего сорта из твердых сортов пшеницы, холодная колодезная вода, щепотка соли и домашнее яйцо. Главное правило — вымешивать не менее 20 минут до абсолютной эластичности и дать отдохнуть 40 минут под влажным полотенцем.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-num">02</div>
|
||||
<h3>Триединство начинки</h3>
|
||||
<p>Канонический сибирский сбор: 45% отборной говядины, 35% сочной свинины и 20% нежной баранины (или дичи). Мелко рубленный репчатый лук, черный свежемолотый перец и немного ледяного мясного бульона для невероятной сочности.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-num">03</div>
|
||||
<h3>Лепка и быстрая заморозка</h3>
|
||||
<p>Толщина раскатки сочня — ровно 1.5 мм. Края защипываются «косичкой» или классическим «ушком». Настоящие пельмени после лепки обязательно выставляются на сухой сибирский мороз или в шоковую заморозку при -24°C.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="forum-preview" id="forum">
|
||||
<h2>Свежие темы клуба и обсуждения мастеров</h2>
|
||||
<div class="forum-topics">
|
||||
<div class="topic">
|
||||
<span class="topic-tag">ТЕХНОЛОГИЯ</span>
|
||||
<div class="topic-info">
|
||||
<h4>Тестораскаточная машинка vs скалка из сибирской лиственницы: битва мнений</h4>
|
||||
<span class="topic-meta">Автор: Дед_Матвей • 48 ответов • Последнее: 2 часа назад</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<span class="topic-tag">СОУСЫ</span>
|
||||
<div class="topic-info">
|
||||
<h4>Классический уксус 6% с толченым чесноком или деревенская сметана с укропом?</h4>
|
||||
<span class="topic-meta">Автор: Сибирячка_Ольга • 112 ответов • Последнее: 15 минут назад</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<span class="topic-tag">ИСТОРИЯ</span>
|
||||
<div class="topic-info">
|
||||
<h4>Пельмени в походах Ермака: почему замороженные мешки спасали от цинги</h4>
|
||||
<span class="topic-meta">Автор: Краевед_Игорь • 34 ответа • Последнее: Вчера</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer" id="contacts">
|
||||
<div class="footer-inner">
|
||||
<div>© 2026 Некоммерческое содружество «Сибирский Пельмень». Все права защищены.</div>
|
||||
<div>Материалы клуба доступны для свободного кулинарного использования.</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
214
nginx/site/style.css
Normal file
214
nginx/site/style.css
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #f7f6f2;
|
||||
color: #2b2b2b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e2ded5;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
letter-spacing: 1px;
|
||||
color: #8b2500;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 11px;
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
text-decoration: none;
|
||||
color: #444444;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.nav a:hover {
|
||||
color: #8b2500;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
max-width: 1100px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0dbce;
|
||||
border-left: 6px solid #8b2500;
|
||||
padding: 36px;
|
||||
margin-bottom: 36px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #8b2500;
|
||||
background: #fcf1ed;
|
||||
padding: 4px 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #222222;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 16px;
|
||||
color: #555555;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.grid-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0dbce;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-num {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
color: #e5ded5;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 18px;
|
||||
color: #8b2500;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.card p {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.forum-preview {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0dbce;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.forum-preview h2 {
|
||||
font-size: 20px;
|
||||
color: #222222;
|
||||
border-bottom: 2px solid #f0ece3;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.forum-topics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.topic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
background: #faf9f6;
|
||||
border: 1px solid #ece7dc;
|
||||
}
|
||||
|
||||
.topic-tag {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 3px 8px;
|
||||
background: #ece7dc;
|
||||
color: #555555;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.topic-info h4 {
|
||||
font-size: 15px;
|
||||
color: #222222;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.topic-meta {
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid #e0dbce;
|
||||
background: #ffffff;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
79
scripts/init.sh
Executable file
79
scripts/init.sh
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Pelmeni Shield VPN - Initial Setup & Zero-Exposure Deployment Script
|
||||
# ==============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [[ ! -f .env ]]; then
|
||||
if [[ -f .env.example ]]; then
|
||||
echo "⚠️ Файл .env не найден! Создаю из .env.example..."
|
||||
cp .env.example .env
|
||||
echo "📝 Пожалуйста, отредактируйте .env (укажите ваш реальный DOMAIN) и запустите скрипт снова!"
|
||||
exit 1
|
||||
else
|
||||
echo "❌ Ошибка: .env.example не найден!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source .env
|
||||
|
||||
if [[ -z "${DOMAIN:-}" || "${DOMAIN}" == "endpoint.pelmeni-club.com" ]]; then
|
||||
echo "❌ Ошибка: Укажите реальный DOMAIN в файле .env!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🛡️ === 1. Активная защита: Дроп подсети сканеров РКН (ГРЧЦ / ЦМУ ССОП) ==="
|
||||
RKN_SUBNET="212.192.156.0/22"
|
||||
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
if ! iptables -C INPUT -s "${RKN_SUBNET}" -j DROP 2>/dev/null; then
|
||||
echo "🚫 Блокирую диапазон сканеров РКН (${RKN_SUBNET}) в iptables..."
|
||||
iptables -I INPUT 1 -s "${RKN_SUBNET}" -m comment --comment "RKN_ACTIVE_PROBING_DROP" -j DROP
|
||||
echo "✅ Правило iptables успешно добавлено."
|
||||
else
|
||||
echo "ℹ️ Правило для ${RKN_SUBNET} уже присутствует в iptables."
|
||||
fi
|
||||
else
|
||||
echo "⚠️ iptables не найден, пропускаю настройку файрвола хоста."
|
||||
fi
|
||||
|
||||
echo "📁 === 2. Подготовка каталогов ==="
|
||||
mkdir -p 3x-ui certbot/certs certbot/www nginx/conf.d nginx/site
|
||||
|
||||
echo "🔐 === 3. Проверка SSL-сертификата для ${DOMAIN} ==="
|
||||
CERT_PATH="certbot/certs/live/${DOMAIN}/fullchain.pem"
|
||||
if [[ ! -f "${CERT_PATH}" ]]; then
|
||||
echo "🌐 Запуск первичного выпуска SSL-сертификата Let's Encrypt..."
|
||||
docker run -it --rm --name certbot-init \
|
||||
-p 80:80 \
|
||||
-v "$(pwd)/certbot/certs:/etc/letsencrypt" \
|
||||
-v "$(pwd)/certbot/www:/var/www/certbot" \
|
||||
certbot/certbot certonly --standalone \
|
||||
--non-interactive --agree-tos \
|
||||
--email "${CERTBOT_EMAIL}" \
|
||||
-d "${DOMAIN}"
|
||||
echo "✅ SSL-сертификат успешно получен!"
|
||||
else
|
||||
echo "✅ SSL-сертификат уже существует: ${CERT_PATH}"
|
||||
fi
|
||||
|
||||
echo "🚀 === 4. Запуск контейнеров Pelmeni Shield ==="
|
||||
docker compose up -d
|
||||
|
||||
echo ""
|
||||
echo "=========================================================================="
|
||||
echo "🎉 СТЕК PELMENI SHIELD VPN УСПЕШНО РАЗВЕРНУТ И ЗАЩИЩЕН!"
|
||||
echo "=========================================================================="
|
||||
echo "🥟 Сайт-заглушка: https://${DOMAIN}/"
|
||||
echo "🥟 Внутренний Fallback: http://127.0.0.1:8080 (только внутри Docker)"
|
||||
echo "🥟 Шлюз подписок: https://${DOMAIN}/api/v1/recipes/secret_${SUB_TOKEN}/<subId>"
|
||||
echo ""
|
||||
echo "🔒 ВЕБ-ПАНЕЛЬ 3X-UI ЗАКРЫТА ОТ МИРА И СЛУШАЕТ ТОЛЬКО 127.0.0.1:2053!"
|
||||
echo "👉 Для входа в панель откройте безопасный SSH-туннель с вашего ПК:"
|
||||
echo " ssh -L 2053:127.0.0.1:2053 root@$(curl -s ifconfig.me || echo 'IP_СЕРВЕРА')"
|
||||
echo " Затем перейдите в браузере: http://localhost:2053"
|
||||
echo "=========================================================================="
|
||||
131
scripts/rkn_probe_monitor.py
Executable file
131
scripts/rkn_probe_monitor.py
Executable file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
RKN Active Probing Monitor & Telegram Alerting
|
||||
Отслеживает попытки сканирования со стороны подсетей РКН / ГРЧЦ (212.192.156.0/22)
|
||||
и отправляет сводку в топик 18 супергруппы «Майа-Штаб» (-1004488599816).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import urllib.request
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Конфигурация (читается из .env или переменных окружения)
|
||||
ENV_PATH = Path(__file__).resolve().parent.parent / ".env"
|
||||
config = {}
|
||||
if ENV_PATH.exists():
|
||||
with open(ENV_PATH, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
config[k.strip()] = v.strip().strip("'\"")
|
||||
|
||||
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", config.get("TELEGRAM_BOT_TOKEN", "8065539545:AAECBnyjinUVVGv3vhtoWZ99tUmnT9sze-k"))
|
||||
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", config.get("TELEGRAM_CHAT_ID", "-1004488599816"))
|
||||
TOPIC_ID = int(os.getenv("TELEGRAM_TOPIC_ID", config.get("TELEGRAM_TOPIC_ID", "18")))
|
||||
|
||||
STATE_FILE = Path("/tmp/rkn_probe_monitor_state.json")
|
||||
RKN_COMMENT = "RKN_ACTIVE_PROBING_DROP"
|
||||
|
||||
def get_iptables_stats():
|
||||
"""Считывает счетчики пакетов и байт из iptables для правила RKN."""
|
||||
try:
|
||||
cmd = ["iptables", "-L", "INPUT", "-v", "-n", "-x"]
|
||||
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
|
||||
for line in res.stdout.splitlines():
|
||||
if RKN_COMMENT in line:
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 2:
|
||||
packets = int(parts[0])
|
||||
bytes_count = int(parts[1])
|
||||
return packets, bytes_count
|
||||
except Exception as e:
|
||||
print(f"Ошибка чтения iptables: {e}", file=sys.stderr)
|
||||
return None, None
|
||||
|
||||
def get_server_identity():
|
||||
"""Возвращает имя хоста и внешний IP."""
|
||||
hostname = socket.gethostname()
|
||||
external_ip = "Unknown"
|
||||
try:
|
||||
req = urllib.request.Request("https://ifconfig.me/ip", headers={"User-Agent": "curl/7.68.0"})
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
external_ip = resp.read().decode().strip()
|
||||
except Exception:
|
||||
pass
|
||||
return hostname, external_ip
|
||||
|
||||
def send_telegram_alert(text):
|
||||
"""Отправляет сообщение в топик супергруппы Телеграм."""
|
||||
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||
payload = {
|
||||
"chat_id": CHAT_ID,
|
||||
"text": text,
|
||||
"parse_mode": "HTML",
|
||||
"message_thread_id": TOPIC_ID
|
||||
}
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
return data.get("ok", False)
|
||||
except Exception as e:
|
||||
print(f"Ошибка отправки в Telegram: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def main():
|
||||
pkts, bytes_total = get_iptables_stats()
|
||||
if pkts is None:
|
||||
print("Правило iptables с меткой RKN_ACTIVE_PROBING_DROP не найдено.")
|
||||
sys.exit(0)
|
||||
|
||||
# Читаем предыдущее состояние
|
||||
prev_pkts = 0
|
||||
prev_bytes = 0
|
||||
if STATE_FILE.exists():
|
||||
try:
|
||||
with open(STATE_FILE, "r", encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
prev_pkts = state.get("packets", 0)
|
||||
prev_bytes = state.get("bytes", 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
delta_pkts = pkts - prev_pkts
|
||||
delta_bytes = bytes_total - prev_bytes
|
||||
|
||||
# Сохраняем текущее состояние
|
||||
with open(STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({"packets": pkts, "bytes": bytes_total, "updated_at": datetime.now().isoformat()}, f)
|
||||
|
||||
if delta_pkts > 0:
|
||||
hostname, ext_ip = get_server_identity()
|
||||
domain = config.get("DOMAIN", hostname)
|
||||
now_str = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
msg = (
|
||||
f"🚨 <b>[ACTIVE PROBING DETECTED] Попытка сканирования от РКН!</b>\n\n"
|
||||
f"🌐 <b>Сервер:</b> <code>{domain}</code> ({ext_ip})\n"
|
||||
f"🎯 <b>Источник:</b> AS61280 ФГУП «ГРЧЦ» (ЦМУ ССОП)\n"
|
||||
f"🚫 <b>Подсеть сканера:</b> <code>212.192.156.0/22</code>\n\n"
|
||||
f"📊 <b>Новых пакетов:</b> <code>+{delta_pkts}</code> ({delta_bytes / 1024:.1f} КБ)\n"
|
||||
f"📈 <b>Всего заблокировано:</b> <code>{pkts}</code> пакетов ({bytes_total / 1024 / 1024:.2f} МБ)\n"
|
||||
f"🛡️ <b>Результат:</b> Пакеты жестко сброшены в <b>DROP</b>. Сканер не получил ответа и считает хост мертвым.\n"
|
||||
f"🕒 <i>{now_str}</i>"
|
||||
)
|
||||
print(f"Обнаружен пробинг: +{delta_pkts} пакетов! Отправка алерта...")
|
||||
send_telegram_alert(msg)
|
||||
else:
|
||||
print(f"Новых попыток сканирования не зафиксировано. Всего дропнуто: {pkts} пакетов.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue