Давно читаю Хабр, и всегда хотелось что-нибудь да написать. И вот, тема появилась. Надо было на одном сервере запустить и VPN-сервер (который уже работал), и легитимный сервис.
Сразу нужно сказать, что этот метод подойдёт в основном для тех, у кого уже есть свой реальный веб-сервис. Подойдёт и обычный написанный на коленке сайтик, и что-то покрупнее. Главное чтобы оно было действительно вашим.
Как мы уже все знаем, без VPN в современном Интернете не выжить. Да даже эту статью вы наверняка читаете со способом обхода. Но с каждым днём РКН выкашивает всё активнее и всё больше VPN-серверов. И самая главная проблема «раскрытия» серверов — простукивание по портам, а также слепки. Если вы маскируетесь, например, под apple.com, то вас рано или поздно «по ойпи вычеслят», особенно если вы не закрыли панель. А мы тут не под кого не маскируемся — сервис легитимный и реально ваш. Просто его часть немного «скрыта» от ненужных глаз :). Кто его знает, может в database.mydomain.site крутится база данных.
Нам понадобится NGINX, и панель типа 3x-ui, но это не обязательно (если вы мазохист то можно и голый XRAY), однако желательно, потому что панель тоже будет закрыта за щитом NGINX, и рисков с ней нет :).
Простая схема как это выглядит
Для начала, у NGINX будет stream-block с ssl preread. Тоесть перед тем как мы подключимся, мы смотрим: «так, а на какой домен стучатся?». Если стучатся на IP, просто режем TLS-рукопожатие, выдавая SSL_ERROR_UNRECOGNIZED_NAME_ALERT («у нас нет сертификата на такой домен, простите»). Если стучатся прямо на домен, пропускаем и разбираемся в HTTP-блоке NGINXа, либо же сразу прокидываем на наш inbound.
Подготовка
Для начала, нужно установить NGINX с модулем stream.
Создаём APT-репозиторий для NGINX и устанавливаем
Так как в этом туториале будут директивы, которые поддерживаются только с версии 1.25 (Например, listen 443 ssl; вместо listen 443; ssl on;), лучше добавить APT-репозиторий непосредственно от NGINX, вместо дефолтных (Сейчас вроде как «из коробки» идёт 1.18). Инструкция будет для Debian/Ubuntu, для RHEL/CentOS и др. читайте здесь: https://nginx.org/en/linux_packages.html.
Устанавливаем зависимости:
Debian:sudo apt install curl gnupg2 ca-certificates lsb-release debian-archive-keyring -y
Ubuntu:sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
Скачиваем GPG-ключ:curl https://nginx.org/keys/nginx\_signing.key | gpg --dearmor \ | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
Создаём APT-репозиторий со стабильными релизами NGINX:echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ https://nginx.org/packages/debian lsb_release -cs nginx" \ | sudo tee /etc/apt/sources.list.d/nginx.list
Ставим новый репозиторий выше системного:echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \ | sudo tee /etc/apt/preferences.d/99nginx
Устанавливаем:sudo apt update && sudo apt install nginx
Получаем SSL-сертификаты
Вам понадобится SSL-сертификат. Если у вас есть домен (субдомены тоже подойдут). Панель предложит создать вам сертификат на 90 дней, однако (как я покажу ниже) можно получить куда более долгоживущий от Cloudflare. Если домена нет, то панель сама запросит и выдаст короткоживущие сертификаты на IP-адрес, которые будут обновляться автоматически. Плюс долгоживущего сертификата в том, что acme.sh (утилита для автообновления) необходим открытый порт 80, и без него она не работает, а это вредит маскировке. Конечно, можно настроить то чтобы (локальный) ACME listener слушал на другом порту, но внешний всё равно будет кидать трафик на порт 80. Так что придётся «вайтлистить» путь /.well-known/acme-challenge/*, что станет «ахилесовой пятой» нашей маскировки.
Получаем сертификат от Cloudflare
Для начала рекомендую иметь домен, т.к. сертификаты долгоживущие, особенно если они от «взрослых» CA типа GlobalSign. Панель предложит сертификат на 90 дней от Let’s Encrypt. Такая же проблема с ACME.sh listener (и вайтлистом), но обновлять каждые 3 месяца куда проще, чем каждую неделю, так что всё не так плохо.
Устанавливаем 3x-ui и подключаем сертификаты
Выполняем скрипт для установки 3x-ui: bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
ОЧЕНЬ ВАЖНЫЙ СОВЕТ НА БУДУЩЕЕ: НЕ ВЫПОЛНЯЙТЕ НИКАКИЕ СКРИПТЫ ИЗ ИНТЕРНЕТА, ПЕРЕД ТЕМ КАК ВЫ ИХ ПРОЧИТАЕТЕ, ВСЁ НА ВАШ СТРАХ И РИСК. Скажете спасибо потом.
Нам предложат либо создать SSL-сертификаты с помощью Let’s Encrypt либо на домен (1), либо на IP (2). Если у вас просто есть уже имеющийся сертификат, выбирайте опцию 3 и указываете путь. Как видите, у меня это /root/cert/mydomain_*.pem.

Настройка обфускации
Итак, когда панель и NGINX установлены, можно приступать к настройке. Чтобы куда-то пробрасывать траффик, нам нужно создать это куда-то. Для начала заходим в панель по внешней ссылке, и настравиваем inbound на XHTTP.
ВАЖНО: не слушайте на 0.0.0.0 или оставляйте Listen IP пустым, необходимо чтобы он был у вас на localhost-е (127.0.0.1)! Без этого на ваш inbound можно будет стучаться из интернета.
ВАЖНО 2: НЕ СТАВЬТЕ РЕЖИМ auto! Так как мы будем использовать grpc_pass, работают только stream-one и stream-up! packet-up просто отваливается!
ВАЖНО 3: security НА СЕРВЕРЕ ставьте none, так как этим будет заниматься NGINX, а не XRAY, после чего идёт уже передача ГОЛОГО gRPC. У КЛИЕНТА должен как раз таки стоять security TLS.
Кстати, именно поэтому нам придётся создать «перехватчик» подписок, который будет давать security=tls клиенту.
После этого сохраняйте инбаунд, переходите в настройки панели (Panel Settings) > Certificates > УБИРАЕТЕ пути до сертификатов (отключает HTTPS)
(Это нужно для того чтобы не было tls-in-tls, что детектируется.)
Итак, настроили inbound. Теперь можно и nginx. Переходим в директорию /etc/nginx/. Нам нужен nginx.conf. (Тем, кто собирал из исходников — /usr/local/bin/nginx/conf/nginx.conf). Для начала разберём блок stream:
# Тут ничего не трогайтеuser nginx;worker_processes auto;error_log logs/error.log;error_log logs/error.log notice;error_log logs/error.log info;pid logs/nginx.pid;events { worker_connections 1024;}########################################################################stream { map $ssl_preread_server_name $backend { mydomain.site webhook_backend; # Ваш легитимный сервис service.mydomain.site panel_backend; # Ваша панель database.mydomain.site inbound_1; # Наш inbound default rejector; # Сюда попадает всё остальное } upstream webhook_backend { server 127.0.0.1:<ПОРТ_ВАШЕГО_СЕРВИСА>; #Порт НЕ вебсервера nginx, а сразу вашего сервиса. } upstream panel_backend { server 127.0.0.1:<ПОРТ1>; #ВНИМАНИЕ: здесь не порт вашей панели, а порт вебсервера nginx! (будет ниже) } upstream inbound_1 { server 127.0.0.1:<ПОРТ2>; #ВНИМАНИЕ: здесь не порт вашего инбаунда, а порт сервера nginx! (будет ниже) } upstream rejector { server 127.0.0.1:8011; #Порт вебсервера NGINX (будет ниже) } server { listen 443; ssl_preread on; proxy_pass $backend; }}
В блоке map мы просто присваиваем нужным доменам названия upstream-ов ($backend).
Каждый upstream просто «биндится» на определенный IP и порт.
В блоке server мы включаем ssl_preread — т.е. заранее смотрим, на какой домен/IP летит трафик, не расшифровывая его заранее. Далее просто делаем proxy_pass на нужный upstream в зависимости от этого
mydomain.site — прокидываем на легитимный сервис без посредников.
service.mydomain.site — прокидываем на веб-сервер nginx-а, который крутится на localhost (посредник; будет ниже), а потом прокдывает уже на панель. Также можно прикрутить сюда «перехватчик» подписок, который будет переписывать IP с 127.0.0.1 на database.mydomain.site, порт с нашего на 443, а также security будет делать TLS.
database.mydomain.site — прокидываем на веб-сервер nginx-а, который крутится на localhost (посредник; будет ниже), а потом прокидывает трафик на XHTTP-inbound с помощью grpc_pass.
default — всё остальное (в том числе и IP) — просто идёт на сервер, в котором будет terminate SSL-рукопожатия.
Все эти серверы будут в блоке http.
http { include mime.types; default_type application/octet-stream; #Логирование - не трогайте (и у вас может быть не так как у меня) log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log logs/access.log main; ####################################################################### #Просто присваиваем переменной connection_upgrade либо HTTP upgrade, #либо close connection map $http_upgrade $connection_upgrade { default upgrade; "" close; } sendfile on; keepalive_timeout 65; server_tokens off; #Не нужно говорить какая у нас версия NGINX и подобное #Переносим все подключения на HTTPS server { listen 80; server_name service.mydomain.site www.service.mydomain.site; # Redirect all traffic to HTTPS return 301 https://$host$request_uri; } server { listen <ПОРТ1> ssl; server_name service.mydomain.site; set_real_ip_from 127.0.0.1; ssl_certificate /root/cert/mydomain.site/fullchain.pem; ssl_certificate_key /root/cert/mydomain.site/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location ~ ^/Ваша секретная ссылка на панель(.*)$ { #HTTP, не HTTPS, избегаем tls-in-tls proxy_pass http://127.0.0.1:<ПОРТ НЕПОСРЕДСТВЕННО ВАШЕЙ ПАНЕЛИ>; proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_ssl_verify off; } #По желанию - сервис-перехватчик подписок который даёт уже правильные данные #(т.к. 3x-ui выдаст что у нас security=none, и вообще надо на 127.0.0.1 подключаться) location ~ ^/Ваша секретная ссылка на сервис-исправлятель подписок/(.*)$ { proxy_pass http://127.0.0.1:<ПОРТ ПЕРЕХВАТЧИКА>; proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } #Притворяемся частью нашего легитимного сервиса, и выдаём Unauthorized, мол чего вы лезете во внутрянку? location = / { return 401; } location / { return 404; } } server { listen <ПОРТ2> ssl http2; server_name database.mydomain.site; ssl_certificate /root/cert/mydomain.site/fullchain.pem; ssl_certificate_key /root/cert/mydomain.site/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; client_header_timeout 5m; keepalive_timeout 5m; grpc_read_timeout 315; grpc_send_timeout 5m; location /ваша секретная ссылка { client_max_body_size 0; client_body_timeout 5m; grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; grpc_pass grpc://127.0.0.1:36490; #Внимание, нам не нужен gRPCs (хотя он и поддерживается), так как TLS уже обработал NGINX. } location / { return 401; #Посылаем любопытных } } server { listen 8011 ssl; ssl_reject_handshake on; #Обрываем соединение ssl_protocols TLSv1.2 TLSv1.3; }}
У некоторых наверняка возник вопрос: «так, а зачем возится с блоком http? Нельзя ли просто проксировать сразу на inbound?» Можно. НО: у вас по дефолту идёт ответ 404, да и нельзя кастомизировать страницу 404, а если у вас большой легитимный сервис, это может стать реальной проблемой. А ещё работает только режим stream-one, не packet-up или stream-up (можете меня поправить в комментах если у вас работает, потому что у меня не работало).
И, наверняка, вы думаете: «а откуда у вас взялся grpc_pass??? У нас же XHTTP?». А это интересный трюк, который включает мультиплексирование (и, до версии 1.29.4, когда ещё не было поддержки h2 в upstream, включало h2), который сам не знаю как, но работает. С grpc_pass этим работает и stream-one, и stream-up (самый быстрый). Packet-up можно включить, но придётся сделать отдельную директиву для packet-up (т.к. нужен proxy_pass вместо grpc_pass), использовать proxy_http_version 2, а также у вас будет http2 без мультиплексирования (или вообще без http2, если у вас 1.29.2 и ниже). На момент написания статьи актуальна версия 1.29, если появится полномасштабная поддержка http2 + multiplex для streams — напишите, мне правда интересно. Как я понимаю, чтобы получить мультиплексирование — нужно переписывать заново пол обработчика событий NGINX, поэтому этого так скоро не будет. Можете также меня поправить, если я не прав.
Исправляем security=none у клиентов
Итак, NGINX и 3x-ui настроены, можно тестировать! Но если вы просто скопируете inbound из панели — у вас ничего не получится. Это потому что (как я писал выше) вы выставили listen ip на 127.0.0.1, а также поставили security=none и всё сделали правильно. Но клиенту нужен ваш домен, а security=tls, так как NGINX всё-таки обрывает TLS, а на свой localhost стучаться смысла мало :). Поэтому необходимо поставить себе middleware, который будет «исправлять» подписки. Я навайбкодил такой клиент: https://github.com/JM001113/VPN_sub_middleware.
Если прокся чисто для вас и меняться не планирует, просто ручками скопируйте URL подписки и замените все нужные ключи, и пропускайте шаг ниже:
-
Security — вместо
none—TLS -
ALPN —
http/1.1,h2 -
host= (нужно добавить в таком виде, даже если он пустой, а он у нас пустой)
Для этого просто ставьте FastAPI, и меняете параметры в .env.example (переименуйте в .env). Учтите что он сделан за 5 минут, так что 100% будут баги. Pull requests are welcome, так сказать.
# Service bind portSUB_PORT=Ваш порт к которому будет подключаться NGINX# Upstream 3x-ui subscription sourceBASE_SUB_PORT=Порт subscription end панелиBASE_SUB_URL=Base url для subscription сервисаUPSTREAM_HOST=127.0.0.1 (или сайт если middleware крутится на другом сервере)# Rewrites applied to every supported linkTARGET_HOST=Домен ИНБАУНДА!TARGET_PORT=Порт домена инбаунда (443)TARGET_LINK_NAME=Имя вашей подписки# Также можно менять другие параметры типа SNI в XHTTP settings# TARGET_SNI=server.example.com# Upstream request timeoutUPSTREAM_TIMEOUT_SECONDS=10
Далее просто создаёте venv python -m venv venv, активируете (source venv/bin/activate), устанавливаете зависимости с помощью pip install -r requirements.txt, и запускаете с помощью python main.py. NGINX уже пробрасывает трафик куда надо. Так что можем пользоваться.
Плюсы моего метода
-
Есть настоящий, легитимный сервис. Например, в этом туториале (отличный, кстати, я по нему изначально делал первый прокси) есть свой домен, но нет сервиса. Если главная страница домена выдаёт 404 — это уже подозрительно. А если главная страница database.домен или service.домен, или api.домен выдаёт 403 или 401, это уже более чем правдоподобно.
-
Если вам нужно несколько inbound‑ов — можно либо наклепать субдоменов (не советую!), либо же сделать несколько secret paths, каждый для своего inbound.
-
Расширение «в ширину» — можно сделать несколько айпишников на один и тот же домен или субдомен, и у вас выходит fault tolerant система:). А в случае чего можно просто сменить айпишник только субдомена, и клиенту даже не надо обновлять подписки, только A‑запись.
Минусы
-
Самый главный — как и в том туториале, человеку хочется просто поставить и забыть, а не вот это всё. И я на это никак не могу ответить, потому что это так. Однако мой способ изначально для тех, кто хочет выстроить или у кого уже есть легитимный сервис.
-
Нужен домен. Да, его просто купить, и даже дёшево, на том же namecheap.top стоит 3$ за первый год и 6$ за продление, но я не думаю что все хотят мучаться с покупкой криптовалюты.
Заключение
Я не очень уверен, насколько эта статья поможет другим, т.к. вряд ли у всех есть легитимный сервис, за которым можно «прикрыть» VPN‑сервис. Ну а про профессионализм написания я вообще молчу. Это мало того что первая статья на Хабре, это вообще одна из моих первых статей когда-либо написанных для широкой публики. Но зато это бесценный опыт и feedback от читателей, который мне как раз очень нужен. Спасибо, что дочитали до конца!
ссылка на оригинал статьи https://habr.com/ru/articles/1029782/