Давно читаю Хабр, и всегда хотелось что-нибудь да написать. И вот, тема появилась. Надо было на одном сервере запустить и 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" прямо из панели во вкладке Cloudflare SSL Certificate, но я честно говоря не понял в чём отличие от обычного, вроде живёт также 90 дней. Если домена нет, то панель сама запросит и выдаст короткоживущие сертификаты на 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.

Как +- будет выглядеть ваш inbound
Как +- будет выглядеть ваш inbound


ВАЖНО: не слушайте на 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 подключаться)
            #ВНИМАНИЕ! ОКАЗЫВАЕТСЯ В 3X-UI всё-таки можно нативно исправлять! 
            #Этот шаг больше не нужен!
            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. Вы можете так сделать, но спасибо @korn3r - он нашёл где можно сделать это прямо в панели. Ниже будет актуальная инструкция.

В настройках инбаунда ищите "External Proxy". Название обманчиво, но на самом деле это подмена security и host - то что нам и нужно!

Выставляете security TLS (обязательно), сайт на который у вас будет подключение (у меня - database.mydomain.site), и порт 443.

Внимание! Данная часть инструкции устарела! Выше актуальная версия!
Если прокся чисто для вас и меняться не планирует, просто ручками скопируйте URL подписки и замените все нужные ключи, и пропускайте шаг ниже:

  • Security - вместо none- TLS

  • ALPN - http/1.1,h2

  • host= (нужно добавить в таком виде, даже если он пустой, а он у нас пустой)

Для этого просто ставьте FastAPI, и меняете параметры в .env.example (переименуйте в .env). Учтите что он сделан за 5 минут, так что 100% будут баги. Pull requests are welcome, так сказать.

# Service bind port
SUB_PORT=Ваш порт к которому будет подключаться NGINX

# Upstream 3x-ui subscription source
BASE_SUB_PORT=Порт subscription end панели
BASE_SUB_URL=Base url для subscription сервиса
UPSTREAM_HOST=127.0.0.1 (или сайт если middleware крутится на другом сервере)

# Rewrites applied to every supported link
TARGET_HOST=Домен ИНБАУНДА!
TARGET_PORT=Порт домена инбаунда (443)
TARGET_LINK_NAME=Имя вашей подписки
# Также можно менять другие параметры типа SNI в XHTTP settings
# TARGET_SNI=server.example.com

# Upstream request timeout
UPSTREAM_TIMEOUT_SECONDS=10

Далее просто создаёте venv python -m venv venv, активируете (source venv/bin/activate), устанавливаете зависимости с помощью pip install -r requirements.txt, и запускаете с помощью python main.py. NGINX уже пробрасывает трафик куда надо. Так что можем пользоваться.

Плюсы моего метода

  1. Есть настоящий, легитимный сервис. Например, в этом туториале (отличный, кстати, я по нему изначально делал первый прокси) есть свой домен, но нет сервиса. Если главная страница домена выдаёт 404 — это уже подозрительно. А если главная страница database.домен или service.домен, или api.домен выдаёт 403 или 401, это уже более чем правдоподобно.

  2. Если вам нужно несколько inbound‑ов — можно либо наклепать субдоменов (не советую!), либо же сделать несколько secret paths, каждый для своего inbound.

  3. Расширение «в ширину» — можно сделать несколько айпишников на один и тот же домен или субдомен, и у вас выходит fault tolerant система:). А в случае чего можно просто сменить айпишник только субдомена, и клиенту даже не надо обновлять подписки, только A‑запись.

Минусы

  1. Самый главный — как и в том туториале, человеку хочется просто поставить и забыть, а не вот это всё. И я на это никак не могу ответить, потому что это так. Однако мой способ изначально для тех, кто хочет выстроить или у кого уже есть легитимный сервис.

  2. Нужен домен. Да, его просто купить, и даже дёшево, на том же namecheap.top стоит 3$ за первый год и 6$ за продление, но я не думаю что все хотят мучаться с покупкой криптовалюты.

Заключение

Я не очень уверен, насколько эта статья поможет другим, т.к. вряд ли у всех есть легитимный сервис, за которым можно «прикрыть» VPN‑сервис. Ну а про профессионализм написания я вообще молчу. Это мало того что первая статья на Хабре, это вообще одна из моих первых статей когда-либо написанных для широкой публики. Но зато это бесценный опыт и feedback от читателей, который мне как раз очень нужен. Спасибо, что дочитали до конца!

Туториал, которым я вдохновлялся

Комментарии (38)


  1. korn3r
    29.04.2026 17:09

    можно еще proxy_protocol on; в стриме и соответственно proxy_protocol в листенерах.

    тогда бэкенды будут видеть айпи клиента.

    неужели 3x-ui не умеет переназначать адреса подключения для клиентов? другие панели вроде бы умеют из коробки (в настройках самой панели).


    1. JustMe_001 Автор
      29.04.2026 17:09

      А дело в том что мы используем не proxy_pass, а grpc_pass дабы получить мультиплекс, т.к. NGINX, как я писал, в него толком не умеет (вообще как я понял у них даже философия такая - одно подключение = один апдейт). Колхоз? Да. Работает? Тоже да.

      И, собственно, прокси протокола не будет (насколько я уверен, может я не прав и так работает).

      Да и плюс, как я уже написал:

      У некоторых наверняка возник вопрос: "так, а зачем возится с блоком http? Нельзя ли просто проксировать сразу на inbound?" Можно. НО: у вас по дефолту идёт ответ 404, да и нельзя кастомизировать страницу 404, а если у вас большой легитимный сервис, это может стать реальной проблемой. А ещё работает только режим stream-one, не packet-up или stream-up.


      1. JustMe_001 Автор
        29.04.2026 17:09

        Изменено: чтобы работал прокси-протокол нужно соединение TCP (Raw), а у меня XHTTP.

        TCP, как видно Proxy Protocol принимается
        TCP, как видно Proxy Protocol принимается
        XHTTP, как видите нигде нет Proxy protocol.
        XHTTP, как видите нигде нет Proxy protocol.

        И да, Host у меня стоит т.к. я его специально добавляю NGINX-ом

        UPD: извиняюсь, забыл про sockopt-ы.


        1. ki11j0y
          29.04.2026 17:09

          1. Sockopt там можно включить proxy protocol.

          2. Без proxy protocol можно обойтись установив forwardfor заголовки

          3. В haproxy, XHTTP работает в AUTO есть поддержка всего.

          В haproxy так же я реализовал и reality с xtls и steel oneself, нужно два домена. Два фронтеда, в tcp соответственно ssl passthrough, и http там, xhttp и fallback, может быть doh ещё.


          1. korn3r
            29.04.2026 17:09

            я вам (наверно всем) кратно поясню что такое Proxy_protocol в tcp. вдруг кто не знает. совсем простыми словами, прошу не докапываться тех кто знает:

            на уровне сети у вас может быть просто tcp и tcp в режиме прокси. они не совместимы и нужны когда в стыке соединения надо вкрячить маршрутизацию. например логическую, как прокси пасс и grpc пасс или балансинг соединений на уровне tcp.

            оно в идеале ВКЛ на пересыл (не на прослушивание!) на первом интерфейсе где вы начали что-то ковырять, и ВЫКЛ на последнем (где оно пошло дальше).

            чтоб везде в логах был адрес клиента не 127.0.0.1, надо вот включить. типовая цепочка включения в серверах начинается с proxy_protocol on; в стриме (не в листенере) или если это иксрей, то с xver 1 где-то в аутбаунде, и заканчивается в листенере чего-то у вас. либо это локальный нгинкс, который слушает и принимает его (proxy_protocol в listen у локального нгинкса или в листенере иксрей).

            если торренты блочите, например, то оно в целом по ip блочит в основном (клиентов).


            1. korn3r
              29.04.2026 17:09

              я еще и не туда отправил. прошу прощения @ki11j0y


            1. korn3r
              29.04.2026 17:09

              и grpc_pass уже вроде прям хедерами начинает слать. но если в каком-то приложении надо получать клиентский айпи, то хедеры все равно надо будет слать и до хедеров должен быть еще и прокси пасс, чтобы у приложений во всех на свете логах был не 127.0.0.1, если у вас это приложение (чьи логи) само видит клиентов не на внешнем порту 443. если делаете tcp стримы, то клиент tcp стрима это 127.0.0.1 (или адрес перенаправляющего сервера в сети)


        1. korn3r
          29.04.2026 17:09

          так grpc_pass бэкенд делает. там хедеров достаточно. чтобы работало Proxy_protocol должен быть включен в стриме (не в листенере, а для пересыла) и в листенере бэкенда с grpc_pass (точнее всех бэкендов)


        1. korn3r
          29.04.2026 17:09

          ну и я больше про реврайт хоста в подписке писал. это панели обычно умеют из коробки. хост подписки если не переназначается в панели, может браться из соответствия в хостах например. переназначение внешнего адреса таким приложениям нужно априори и если его там нет, это косяк панели (мне лень ставить эту панель, у меня других хватает). базовый минимум - соответствие внешнего айпи днс имени сервера.

          и даже если не умеет, можно в нгинксе хедер править просто в http {}

                  map $upstream_http_profile_web_page_url $new_header {
                          "" "";
                          "~^http://localhost/(.*)$" "https://DOMAIN1.COM/$1";
                          "~^http://DOMAIN1.COM/(.*)$" "https://DOMAIN1.COM/$1";
                  }
          

          и в локейшен подписки

                          proxy_hide_header profile-web-page-url;
                          add_header profile-web-page-url $new_header;

          ну и что у вас там пишет вписать в переназначение.

          это если панель в подпискее херню выдает.

          а security=none во всех панелях исправляется обычно в свойствах инбаунда в панели (клиентского). там обычно помимо листенера есть еще и инбаунд. и должно быть в листенере security none, а в инбаунде security tls (отдельная настройка того что получают клиенты).

          это панель довольно старая (в смысле давно есть), и я почему-то уверен что там где-то есть настройки инбаунда, потому как сервису даже если он чисто на один сервер, инбаунд клиентам переназначать надо уметь в настройках.


          1. JustMe_001 Автор
            29.04.2026 17:09

            Насчёт rewrite host-а - да, так можно сделать. Я просто поленился.

            а security=none во всех панелях исправляется обычно в свойствах инбаунда в панели (клиентского). там обычно помимо листенера есть еще и инбаунд. и должно быть в листенере security none, а в инбаунде security tls (отдельная настройка того что получают клиенты).

            Если найдёте - огромное спасибо вам. Потому что я не нашёл.


            1. korn3r
              29.04.2026 17:09

              вообще и я не нашел. попробовал поставить и не нашел. очень странно, видимо не очень дружественно настроенная к стоять за нгинксом иксрее панель. настройки адреса подписки в настройки-> подписка есть, и даже /path можно задать длинный для подписки, а вот переназначения адреса инбаунда нет вроде. т.е. он принципиально только то на чем буквально слушает выдает клиентам.

              я бы наверно разработчиков тикетом замучил и они бы меня забанили, если бы я 3x-ui использовал.

              это получается там из коробки нельзя кучу инбаундов на одном 443 иметь? стиранно. у меня все равно ощущение что я не нашел, хотя там и настроек то не так уж и много разных.


              1. JustMe_001 Автор
                29.04.2026 17:09

                Неа. Подскажите тогда панель получше, может быть на неё перестроюсь, если будет меньше гемороя.


                1. korn3r
                  29.04.2026 17:09

                  нашел - в инбаунде галочка external proxy и там можно вписать и адрес подключения и включить для него тлс.

                  в Настройки -> Подписка можно прописать url подписки правильный


                  1. JustMe_001 Автор
                    29.04.2026 17:09

                    Спасибо <3.

                    А панель получше подскажите в любом случае.


            1. korn3r
              29.04.2026 17:09

              можно все равно в нгинксе попробовать что-то вроде этого для пути подписки добавить (возможно нужны кавычки). и вместо sub_filter_types *; application/json text/json и другие нужные, но это надо знать/смотреть какие там.

                      location /sub/ {
                          sub_filter_types *;
                          sub_filter 127.0.0.1:1000 ip_фактического_входа:443;
                          sub_filter security=none security=tls;
                          sub_filter_once off;
                      }


  1. korn3r
    29.04.2026 17:09

    если кто-то вдруг решит включать прокси протокол, то:

    на листен самого стрима НЕ надо. должен быть

    stream {
            server {
                    listen 443; 
                    ssl_preread on;
                    proxy_pass $backend;
                    proxy_protocol on;
            }
    }

    и бэкенды

        server {
                listen <ПОРТ1> ssl proxy_protocol;
        }

    так в логе бэкенда будет айпи клиента и иксрей тоже видит айпи клиентов, но ему надо в инбаунд для этого “acceptProxyProtocol”: true в “streamSettings” -> "sockopt"

            {
                "tag": "mylistener",
                "listen": "127.0.0.1:порт",
                "protocol": "vless"
                "settings": {},
                "streamSettings": {
                    "sockopt": {
                        "acceptProxyProtocol": true
                    }
            }

    обратите внимание что т.к. включается прокси протокол на уровне листенера/инбаунда, всё что вы настраиваете в апстримах, получит айпи клиента. и соответственно у апстримов в листенерах должен быть прокси протокол включен (у всех).

    ну и для любителей пооптимизировать листенер можно и в unix socket запихнуть, так быстрее, но это вы пишете в файл со всеми вытекающими минусам для контейнеров.

    волюм для рамдиска под сокет в docker-compose

    volumes:
      xray-sock:
        driver: local
        name: xray-sock
        driver_opts:
          type: tmpfs
          device: tmpfs
          o: size=4k,nosuid,nodev

    и в волюм обоим контейнерам нгинкс и иксрей. добавить, а не заменить весь volumes

            volumes:
                - xray-sock:/opt/run/xray/:rw

    минусы:

    если не на рамдиске, в случае краша сервера (типа выключения по питанию физического хоста, а не вм), иксрей может не стартануть листенер (как и нгинкс). у меня были тикеты и туда и туда даже.

    плюсы:

    нет кучи слушающих портов на вм.

    минует сетевой стек

    ps:

    извините что влез.


    1. korn3r
      29.04.2026 17:09

      забыл еще.

      чтобы нгинкс проставлял айпи, надо в блок сервер (который принимает proxy_protocol, т.е. бэкенд) вписать еще откуда брать айпи.

      server {
              set_real_ip_from 127.0.0.1;
              real_ip_header proxy_protocol;
              real_ip_recursive on;
      }

      в случае с юникс сокет листенерами вместо "127.0.0.1" должно быть "unix:"


  1. SantaClaus16
    29.04.2026 17:09

    А чем плохо выставить фронтом xray, а уже после него fallback nginx?


    1. JustMe_001 Автор
      29.04.2026 17:09

      Fallback только TCP (RAW) подключение, а так можно и gRPC, и XHTTP.


      1. crazyballs
        29.04.2026 17:09

        Xray видит path, alpn. Можно обойтись без nginx на самом деле


        1. korn3r
          29.04.2026 17:09

          чтобы видеть path, ему нужно делать ssl терминацию, если ssl терминацию делает го приложение, а не нгинкс, это несколько заметно при анализе.


          1. ki11j0y
            29.04.2026 17:09

            Кстати, не несколько, а ого-го как, что это не стандартный openssl. По этому принципу банят(не у нас пока что) openconnect там gnutls


            1. korn3r
              29.04.2026 17:09

              да, спасибо это было скорее литературное преуменьшение :)

              нгинкс иксрею архитектурно полезен как сервер стрима (балансить до инбаунда) или сразу делать ssl-терминацию (на нгинксе до иксрей).

              xhttp+reality сейчас самый модный как раз потому что там нет ссл терминации на вообще у впна внешней (в смысле впну как надо при помощи живого веб-сервера делает все равно веб-сервер, но с изменениями и сам), при этом ссл терминацию делает нгинкс (или таргет, если это под внешний сайт). xhttp+tls в целом то же самое, но тут есть ссл терминация и у впна со стороны нгинкса, которой нельзя манипулировать. С ней можно что-то делать без того чтобы го приложение делало ссл терминацию у чего-либо (нгинкс же донор для ссл-терминации как раз).

              это я второй раз за день справкой по настройке панелей решил поделиться с человеком, который судя по всему и сам умеет. еще раз извините, @ki11j0y

              ладно, интернета пока у меня временно не планируется ближайшее время, так что надеюсь отстану я со своими комментариями от всех.


              1. JustMe_001 Автор
                29.04.2026 17:09

                Напишите мне, поделитесь мудростью xD. Потому что я хоть и не такой зелёный каким был до того как пришлось с панелями мучаться - всё ещё довольно зеленоват). Так что я буду совсем не против дельных советов.


        1. korn3r
          29.04.2026 17:09

          извините, я наверно не допер что вы тут реалити и имели в виду. помимо ссл терминации на го есть еще реалити. если сэфлстил (под свой же нгинкс), то должно быть не видно ничего подозрительного по идее (я сам трафик не анализировал, но вроде работает)


          1. JustMe_001 Автор
            29.04.2026 17:09

            Self-steal вроде как тоже заметен, если у вас собственный DNS. Тоесть если вы стучитесь сами на себя "а кто такой __?", и сами к себе обращаетесь - это подозрительно. Не проверял сам, но говорили что у них отлетало.


            1. korn3r
              29.04.2026 17:09

              nano /etc/hosts поможет от этого. и в целом некоторые панели (не из поста - 3xui не ставил) нормально домен начинают отдавать когда там прописано соответствие домена, так что иметь домен в хостах тоже это норм практика.

              это в принципе.

              а в случае с Reality и сэлфстилом у вас дест прописывается как айпи (127.0.0.1) или юникс сокет. если вы туда вписали домен, то по домену (и ресолвинг соответственно) идет потому что вы туда фигню вписали.

              это не https подключение, это реалити и домен там нужен только в servernames


  1. bat_n1
    29.04.2026 17:09

    По тексту: Панель предложит сертификат на 90 дней от Let's Encrypt.

    Это и есть ответ на то, как получить сертификат от Cloudflare?


    1. JustMe_001 Автор
      29.04.2026 17:09

      Короче, это я жёстко тупанул. Дело в том что я перепутал сертификаты от Cloudflare и Cloudflare Origin Certificates. Первое это полноценный SSL за 10$, второе - только валидно при оранжевом облаке. В итоге вырезал тот фрагмент потому что... через оранжевое облако не проксируем. Спасибо что нашли недочёт, вырежу упоминания о фрагменте.