Что входит в создание своего VPN-сервиса
Сначала карта компонентов — из неё вытекают все дальнейшие решения по деньгам и по устойчивости.
| Компонент | Что делает | Где живёт | Порты |
|---|---|---|---|
| Панель | пользователи, лимиты, сроки, API | отдельный VPS (на старте — на ноде) | 443 наружу, 3000 локально |
| Нода (Xray-core) | терминирует клиентский трафик | VPS в нужной стране | 443 / нестандартный TLS-порт |
| Выдача подписки | отдаёт актуальный список конфигов | рядом с панелью, свой домен | 443 |
| Биллинг | оплата, продление, напоминания | Telegram-бот + БД | — |
| Фронт (опционально) | «белый» вход перед нодой | CDN / реверс-прокси | 443 |
Два правила, которые дальше сэкономят вам месяцы.
Клиенту выдаётся ссылка-подписка, а не конфиг. Ноду можно переставить, IP сменить, добавить фронт — приложение подтянет новый список само. Если вы раздали статические vless://-строки, каждая блокировка превращается в ручную рассылку и волну тикетов.
Панель и нода — разные машины, как только клиентов больше сотни. Нода под блокировками расходник, её нормально пересоздавать раз в неделю. Панель с базой пользователей и платежей — актив, терять её вместе с нодой нельзя.
Протокол по умолчанию — VLESS поверх Xray-core. Reality хорош на «чистой» связности, XHTTP нужен, когда трафик идёт через HTTP-фронт (последний раздел). WireGuard/AmneziaWG держите как запасной сценарий для стран без активного DPI, а не как основу сервиса в РФ. Пошаговая установка панели, выбор VPS и настройка Reality разобраны у нас отдельными материалами — здесь только то, что влияет на выживаемость сервиса.
Панель и первая нода: что реально нужно сделать руками
Порядок: сервер → сетевой тюнинг → панель → нода → проверка подписки.
1. Подготовка VPS (Debian 12 / Ubuntu 22.04+):
apt update && apt -y upgrade
curl -fsSL https://get.docker.com | sh
ufw default deny incoming && ufw allow 22,80,443/tcp && ufw --force enable
Важная ловушка: ufw не закрывает порты, опубликованные Docker через -p — правила проброса обрабатываются раньше цепочек ufw. Порт панели или origin-порт ноды остаётся открыт всему интернету при «включённом файрволе». Закрывать надо в DOCKER-USER либо не публиковать порт наружу вовсе:
# либо публикуем только на loopback: ports: ["127.0.0.1:8080:8080"]
iptables -I DOCKER-USER -p tcp --dport 8080 -j DROP
iptables -I DOCKER-USER -p tcp --dport 8080 -s <IP_фронта> -j ACCEPT
Это не теория: на нашем проде, пока origin-порт был открыт всем, больше половины запросов приходило мимо фронта — напрямую на IP ноды.
2. Сетевой тюнинг ноды — без него на 200+ сессиях начинаются обрывы:
cat >/etc/sysctl.d/99-node.conf <<'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_fastopen = 3
fs.file-max = 1000000
EOF
sysctl --system
echo '* soft nofile 1000000' >> /etc/security/limits.conf
echo '* hard nofile 1000000' >> /etc/security/limits.conf
3. Панель. Remnawave ставится docker-compose'ом, Marzban и 3x-ui — своими инсталл-скриптами; схема одинаковая. Единственное, что нужно сделать иначе, чем в большинстве гайдов, — зафиксировать версию образа тегом:
# docker-compose.yml
image: remnawave/backend:2.7.4 # не latest
Мажорные обновления панелей меняют формат выдачи подписки; с latest контейнер обновится ночью сам, и утром вы будете разбирать, почему у половины базы «перестало работать». Админские пути (/api/…, дашборд) закрывайте reverse-proxy с TLS плюс IP-фильтр или basic-auth, на отдельном поддомене.
4. Нода. В мультинодовых панелях нода — агент, получающий конфиг Xray от панели по защищённому каналу: добавляете сервер в интерфейсе, копируете сертификат/ключ, поднимаете контейнер. Руками config.json править не нужно — любые ручные правки на ноде живут до первой синхронизации агента, всю кастомизацию делайте в панели.
5. Проверка. Тестовый пользователь, подписка открыта минимум в двух клиентах (Happ + v2rayNG/NekoBox) и проверена с реального мобильного интернета РФ. «Работает у меня с зарубежного хоста» не значит ничего.
Подписка: единственная точка контакта с клиентом
Страница выдачи определяет, сможете ли вы менять инфраструктуру, не трогая клиентов. Обязательный минимум на отдаче:
location /sub/ {
proxy_pass http://127.0.0.1:3010;
proxy_set_header Host $host;
proxy_hide_header ETag;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
}
Без no-store часть клиентов (по наблюдениям особенно Happ на iOS) держит старый список конфигов после переезда на новую ноду: на сервере всё живо, а в поддержку идут «не работает». Лечится либо заголовками, либо повторным добавлением подписки в приложении — второе делать руками на сотне клиентов вы не захотите.
Второй рычаг — заголовки профиля, которые понимают современные клиенты:
profile-title: base64:<название сервиса>
profile-update-interval: 6
subscription-userinfo: upload=0; download=53687091200; total=214748364800; expire=1790000000
profile-update-interval: 6 заставляет клиент перечитывать подписку каждые 6 часов — это ваш штатный канал доставки изменений, включая смену ноды. subscription-userinfo показывает остаток трафика прямо в приложении и снимает заметную долю обращений в поддержку.
Что заложить сразу:
- Отдельные домены под подписку, панель и сайт. Заблокируют один — переезжаете, не теряя остальное. Держите 2–3 запасных имени с уже выпущенными сертификатами.
- Длинный случайный токен в URL, без последовательных ID:
/sub/8f3c…. Иначе базу подписок перебирают за вечер. - Несколько конфигов в одной подписке — разные ноды и транспорты. Клиенты умеют перебирать; это бесплатный фейловер до того, как вы построите нормальный.
Оплата, продления и автоматизация
Ручное продление упирается в потолок на 100–150 клиентах: дальше вы просто не успеваете переписываться.
Каналы приёма (РФ, по практике операторов): Telegram Stars и крипта (USDT TRC-20) — быстрый старт без юрлица; ЮKassa/CloudPayments через ИП или самозанятость — выше конверсия, есть возвраты, но нужна легализация; зарубежные эквайринги — для нерезидентов. Комиссию 3.5–7 % закладывайте в юнит-экономику сразу.
Бот работает с панелью только через API — никаких правок в БД панели напрямую:
# продлить пользователя после успешного платежа
curl -X PATCH https://panel.example.com/api/users/$UUID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"expireAt":"2026-09-01T00:00:00.000Z","trafficLimitBytes":214748364800}'
Пути эндпоинтов различаются между панелями и версиями — сверяйтесь со Swagger своей панели (/docs или /api/docs), а не с чужими гайдами; это первое, что ломается после апдейта.
Три вещи, обязательные с первого дня:
- Идемпотентность платежей. Ключ — ID транзакции провайдера: сохраняйте в БД и проверяйте перед начислением. Иначе повторный вебхук выдаст два месяца за один платёж, а провайдеры ретраят вебхуки штатно.
- Напоминания. Крон за 3 дня, за 1 день и в день окончания. По наблюдениям это самый дешёвый способ снизить отток — дешевле скидок.
- Ежедневная сверка. Скрипт сравнивает «оплачено до» в вашей БД и
expireAtв панели, расхождения пишет в админский чат. Рассинхрон случается всегда; вопрос только в том, узнаете вы о нём от крона или от клиента.
Триал 3–7 дней поднимает конверсию, но провоцирует абьюз: привязывайте к Telegram-аккаунту, ограничивайте 5–10 ГБ и одной нодой.
Экономика: можно ли на этом зарабатывать
Вопрос «можно ли запустить свой VPN-сервис и зарабатывать» имеет численный ответ. Считаем на ARPU 200 ₽/мес и медиане потребления 60–120 ГБ на активного клиента (p90 уходит за 300 ГБ — это видео и торренты через туннель).
| Статья расходов, ₽/мес | 100 клиентов | 500 клиентов |
|---|---|---|
| Ноды (VPS 2 vCPU / 1 Гбит) | 800 (×1) | 2 400 (×3) |
| Сервер панели | на ноде, 0 | 700 |
| Домены и резервные имена | 150 | 300 |
| Эквайринг ~5 % | 1 000 | 5 000 |
| Итого | ~1 950 | ~8 400 |
| Выручка | 20 000 | 100 000 |
| Валовая маржа | ~90 % | ~92 % |
Цифры красивые ровно до момента, когда ноды начинают резать и трафик приходится заводить через «белый» фронт. Ориентир по рынку — порядка 2.5–2.8 ₽/ГБ плюс разовое/фиксированное подключение около 2 500 ₽ (у разных провайдеров условия отличаются, уточняйте по своему объёму). Пересчёт:
| Сценарий (100 клиентов, 80 ГБ каждый) | Затраты на фронт | Итоговая маржа |
|---|---|---|
| Белый фронт всей базе | 8 000 ГБ × 2.8 = 22 400 ₽ | отрицательная |
| Фронт 30 % базы | 2 400 ГБ × 2.8 = 6 720 ₽ | ~55 % |
| Фронт 30 % базы + лимит 50 ГБ в тарифе | 1 500 ГБ × 2.8 = 4 200 ₽ | ~68 % |
Почему гигабайт через фронт стоит дороже обычного CDN-трафика: XHTTP в режиме packet-up дробит аплинк на множество мелких запросов, а CDN тарифицирует ещё и их количество. На масштабе плата за запросы становится отдельной статьёй расхода, и считать её надо по своей выгрузке из биллинга, а не по прайсу.
Практические выводы:
- Лимит трафика в тарифе — не жадность, а необходимость. 50–100 ГБ в базовом и отдельный дорогой «безлимит». Верхний дециль потребления не должен есть маржу всей базы.
- Белый вход включается адресно, а не всем. Технически это просто отдельный конфиг в той же подписке.
- Ёмкость ноды. 2 vCPU / 2 ГБ по наблюдениям тянут 150–300 одновременных активных сессий; упирается обычно не CPU, а полоса и pps. На подписчиков это 300–500 человек при активности 20–40 %. Вторую ноду ставьте на 60 % расчётной полки, а не когда первая уже деградировала.
Блокировку нод по IP закладывайте в проект с первого дня
Из-за этого раздела сервисы закрываются на третий месяц. В обычном режиме DPI борется с протоколом — против этого работают Reality, маскировка, смена SNI. Но при белых списках режется сам факт соединения с неизвестным IP дата-центра: пакет до вашего VPS не доходит вообще, и маскировка транспорта не помогает. Выпадают обычно не отдельные адреса, а диапазоны /24 целиком, вместе с соседями по хостингу.
Ответа два, и они дополняют друг друга.
1. Быстрый переезд. 2–3 подготовленные ноды (образ, ключи, готовый inbound в панели) и скрипт, переключающий подписку на живой адрес. Метрика зрелости — время от «клиенты пишут» до «подписка обновлена»: минуты, а не часы.
2. «Белый» вход перед нодой. Клиент подключается не к IP вашего дата-центра, а к адресу инфраструктуры, входящей в белые списки; фронт терминирует TLS и проксирует соединение на скрытую ноду. Это ниша whitelist-CDN как сервиса (класса Clearway): фронт заводится один раз, ноды за ним меняются свободно. Честная оговорка: «белизна» подсети не вечна — каждую /24 нужно перепроверять регулярно, а origin обязан принимать только адреса фронта (см. DOCKER-USER выше), иначе смысла в схеме нет.
Транспорт для такой схемы — XHTTP, потому что через HTTP-фронт нужно пройти обычным веб-трафиком:
"streamSettings": {
"network": "xhttp",
"security": "tls",
"xhttpSettings": {
"host": "edge.example.ru",
"path": "/xh",
"mode": "packet-up"
}
}
Два факта, на которых спотыкаются почти все:
modeобязателен и должен бытьpacket-up. У Yandex CDN нет POST, поэтому аплинк XHTTP уходит методом GET, а GET допустим только в режимеpacket-up. Оставитеauto— часть клиентов подключится, часть нет, и вы неделю будете искать «плавающий баг».extraна ноде и в клиенте должны совпадать точь-в-точь. Расхождение в этом блоке и есть классическое «в одном приложении работает, в другом нет». Держитеextraминимальным: экзотические obfuscation-поля ломают совместимость между версиями Xray-core, а версии клиентов у ваших пользователей заведомо разные.
Правило простое: не «если заблокируют», а «когда». Инфраструктура проектируется под переезд сразу.
Что мерить в первый месяц и типовые грабли
Четыре цифры, без которых вы управляете вслепую:
| Метрика | Как снять | Ориентир |
|---|---|---|
| Отток (churn) | доля не продливших к концу месяца | > 20 % — проблема со связностью, не с ценой |
| ГБ на клиента, p50 / p90 | статистика панели | p90 > 300 ГБ — нужен лимит в тарифе |
| Доля клиентов на «белом» фронте | по конфигам в подписке | растёт — пересчитывайте тарифы |
| Время переезда ноды | от алерта до обновления подписки | цель — минуты |
Базовая диагностика на ноде:
ss -s # число соединений, не упёрлись ли в лимиты
docker stats --no-stream # CPU/RAM агента и Xray
vnstat -d # реальный трафик по дням
journalctl -u xray -f | grep -i "rejected\|failed"
Проверка, что подписка отдаётся и не кэшируется:
curl -sI -H 'User-Agent: Happ' https://sub.example.com/sub/<token> \
| grep -iE 'cache|profile|userinfo'
Типовые грабли на старте: панель и нода на одной машине, когда клиентов уже 300 (теряете базу вместе с сервером); latest-теги образов; один домен на всё; открытый наружу origin-порт при «включённом» ufw; отсутствие бэкапа — pg_dump базы панели по крону с выгрузкой дампа на отдельное хранилище; тестирование только с зарубежного хоста. Последнее — самая дорогая ошибка: всё, что касается блокировок, проверяется исключительно с реального мобильного интернета РФ.