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