Skip to content

Instantly share code, notes, and snippets.

@ergolyam
Last active June 26, 2026 18:33
Show Gist options
  • Select an option

  • Save ergolyam/d4e7122e4f25b15e281a61ca95960ad9 to your computer and use it in GitHub Desktop.

Select an option

Save ergolyam/d4e7122e4f25b15e281a61ca95960ad9 to your computer and use it in GitHub Desktop.
Развёртывание VLESS (Reality) на VPS с маскировкой под свой сайт

Развёртывание VLESS (Reality) на VPS с маскировкой под свой сайт

Инструкция поднимает связку 3x-ui + Xray (VLESS/Reality) в podman-контейнерах, с Nginx как маскировкой под реальный TLS-сайт и с автоматическим обновлением сертификатов.

Панель 3x-ui остаётся доступной только через SSH-туннель (без публикации в интернет).

Note

Везде, где указано example.com, подставьте свой домен.

Note

VLESS/Reality является основной базой. После базовой настройки остальные протоколы и системные разделы можно подключать в нужной комбинации: например, VLESS/Reality + Hysteria2 + IPv6 или VLESS/Reality + XHTTP + Fail2Ban.

Содержание

Требования и проверки перед стартом

Необходимо

  • VPS с белым IPv4 и доступом по SSH (root или sudo).
  • Домен с A-записью на IP VPS.
  • Открытые порты: 80/tcp (для выпуска/обновления сертификата) и 443/tcp.
  • На VPS установлены:
    • podman версии >= 4.4
    • systemd версии >= 244

Проверка окружения

  1. Проверьте DNS: у домена A указывает на ваш IP.

    dig +short A example.com
  2. Убедитесь, что никто не слушает 80/443:

    ss -tulpn | grep -E ':(80|443)\b'
  3. Проверьте версии:

    podman --version
    systemctl --version

Подготовка окружения

1. Создайте volumes и каталоги

podman volume create nginx-certs
podman volume create 3xui-db
podman volume create 3xui-cert

mkdir -p /etc/nginx/conf.d
mkdir -p /etc/containers/systemd

Системные настройки

1. Включите BBR

echo -e "net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr" | tee /etc/sysctl.d/99-bbr.conf
sysctl --system

Настройка сертификатов

1. Выпустите TLS-сертификат

Сертификат сохраняется в volume nginx-certs.

  1. выбираем CA = Let's Encrypt

    podman run --rm -it -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \
      --set-default-ca --server letsencrypt
  2. HTTP-01: порт 80 должен быть свободен

    podman run --rm -it -p 80:80 -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \
      --issue --standalone -d example.com

Note

После успешного выпуска появятся файлы вида: /acme.sh/example.com_ecc/fullchain.cer и /acme.sh/example.com_ecc/example.com.key

2. Настройте автопродление сертификата

# /etc/containers/systemd/nginx-renew.container
[Container]
Image=docker.io/neilpang/acme.sh:latest
Volume=nginx-certs:/acme.sh
PublishPort=80:80
Exec=--renew-all --standalone

[Service]
Type=oneshot
ExecStartPost=/usr/bin/podman exec systemd-nginx nginx -s reload
# /etc/systemd/system/nginx-renew.timer
[Unit]
Description=Run nginx-renew.service

[Timer]
OnCalendar=Mon 03:00:00
RandomizedDelaySec=900
Persistent=true

[Install]
WantedBy=timers.target

Настройка сервисов

1. Создайте главный network

# /etc/containers/systemd/main.network
[Unit]
PartOf=main-pod.service

[Network]
Subnet=10.50.0.0/24
Gateway=10.50.0.1

2. Создайте главный pod

# /etc/containers/systemd/main.pod
[Unit]
After=network-online.target

[Pod]
Network=main.network
PublishPort=0.0.0.0:443:443/tcp
PublishPort=127.0.0.1:2053:2053

[Install]
WantedBy=default.target

3. Создайте сервис 3x-ui

# /etc/containers/systemd/3x-ui.container
[Unit]
PartOf=main-pod.service

[Container]
Environment=XRAY_VMESS_AEAD_FORCED=false
Environment=XUI_ENABLE_FAIL2BAN=true
Image=ghcr.io/mhsanaei/3x-ui:latest
AutoUpdate=registry
Pod=main.pod
Volume=3xui-db:/etc/x-ui
Volume=3xui-cert:/root/cert

[Service]
Restart=always

4. Создайте сервис Nginx

# /etc/containers/systemd/nginx.container
[Unit]
PartOf=main-pod.service

[Container]
Image=docker.io/nginx:stable-alpine-slim
AutoUpdate=registry
Pod=main.pod
Volume=/etc/nginx/conf.d:/etc/nginx/conf.d
Volume=nginx-certs:/etc/nginx/certs

[Service]
Restart=always

Настройка Nginx

1. Добавьте конфиг Nginx

# /etc/nginx/conf.d/example.com.conf
server {
    listen 127.0.0.1:8443 ssl;
    server_name example.com;

    http2 on;
    port_in_redirect off;

    ssl_certificate /etc/nginx/certs/example.com_ecc/fullchain.cer;
    ssl_certificate_key /etc/nginx/certs/example.com_ecc/example.com.key;

    add_header X-Robots-Tag "noindex, nofollow" always;

    location / {
        root /usr/share/nginx/html;
    }

    location ~ ^(/sub/|/json/) {
        proxy_pass http://127.0.0.1:2096;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Range $http_range;
        proxy_set_header If-Range $http_if_range;
        proxy_redirect off;
        proxy_intercept_errors on;
        error_page 400 404 =404 @nginx_404;
    }

    location @nginx_404 {
        return 404;
    }
}

Note

Nginx слушает 127.0.0.1:8443, использует ваш сертификат и проксирует подписки в 3x-ui.

Запуск и проверка

1. Запустите pod и контейнеры

systemctl daemon-reload
systemctl start main-pod.service
systemctl status 3x-ui.service --no-pager
systemctl status nginx.service --no-pager
  • Проверьте, что 443 слушается:
    ss -tulpn | grep ':443'

2. Включите таймер продления сертификатов

systemctl enable --now nginx-renew.timer
systemctl list-timers | grep nginx-renew.timer

Настройка 3x-ui

Панель доступна только по SSH-туннелю.

1. Откройте панель локально

ssh -L 2053:127.0.0.1:2053 root@example.com
  • Откройте в браузере: http://127.0.0.1:2053
    • Логин: admin
    • Пароль: admin

2. Включите подписки через Nginx

  • В панели: Panel Settings -> Subscription
    • Reverse Proxy URI: https://example.com/sub/
    • Нажмите Save, затем Restart Panel

Автообновление контейнеров

  • В quadlet уже задано:
    [Container]
    AutoUpdate=registry

Note

Это позволяет nginx и 3x-ui автоматически подтягивать новые образы и перезапускаться через podman-auto-update.

1. Включите таймер автообновления

systemctl enable --now podman-auto-update.timer

2. Проверьте расписание

systemctl status podman-auto-update.timer --no-pager
systemctl list-timers | grep podman-auto-update

Настройка протоколов

VLESS/Reality является основной базой. VLESS XHTTP и Hysteria2 можно добавить после базовой настройки, независимо друг от друга.

VLESS Reality

1. Создайте inbound для VLESS/Reality

  • В панели: Inbounds -> Add Inbound
    • Remark: vless-reality
    • Protocol: vless
    • Port: 443
    • Security: Reality
    • Target: 127.0.0.1:8443
    • SNI: example.com
    • Нажмите Get New Cert и сгенерируйте Public Key и Private Key
    • Sniffing: True
      • HTTP: Mark
      • TLS: Mark
      • Route only: True
    • Нажмите Create

VLESS XHTTP

Этот вариант добавляет отдельный inbound для подключения через XHTTP. Основной Reality inbound при этом не меняется.

XHTTP лучше всего использовать с CDN, но CDN не является обязательным. Без CDN можно использовать External Proxy DIRECT на example.com; если CDN не нужен, обычно проще и надёжнее оставить только VLESS/Reality.

Note

Везде, где указано cdn.example.com, подставьте домен, который проксируется через CDN. Домен example.com должен оставаться доменом вашего VPS.

Необходимо

  • Для варианта через CDN: источник CDN должен смотреть на HTTPS-домен VPS: example.com:443.
  • Для варианта через CDN: источник CDN должен быть HTTPS.
  • Для варианта через CDN: кэширование нужно полностью отключить.

1. Создайте inbound для VLESS/XHTTP

  • В панели: Inbounds -> Add Inbound
    • Remark: vless-xhttp
    • Protocol: vless
    • Port: 7443
    • Transmission: XHTTP
    • Path: /assets/api/v2
    • Mode: packet-up
    • Max Upload Size (Byte): 3000-4000
    • Padding Bytes: 16-64
    • Uplink HTTP Method: GET
    • Padding Obfs Mode: True
      • Padding Placement: header
      • Padding Method: tokenish
    • Uplink Data Placement: header
      • Uplink Data Key: X-Req
    • Security: none
    • External Proxy: True
      • Для CDN:
        • Force TLS: TLS
        • Dest: cdn.example.com
        • Port: 443
        • Remark: CDN
        • Fingerprint: firefox
        • Apln: h2
      • Без CDN:
        • Force TLS: TLS
        • Dest: example.com
        • Port: 443
        • Remark: DIRECT
        • Fingerprint: firefox
        • Apln: h2
    • Sniffing: True
      • HTTP: Mark
      • TLS: Mark
      • Route only: True
    • Нажмите Create

2. Добавьте location в Nginx

  • В конфигурацию Nginx добавьте следующие location внутри server:
        location /assets/api/v2/ {
            proxy_pass http://127.0.0.1:7443;
            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 $scheme;
            add_header Cache-Control "no-store" always;
            add_header CDN-Cache-Control "no-store" always;
            add_header X-Accel-Buffering "no" always;
            proxy_request_buffering off;
            client_body_buffer_size 1m;
            client_max_body_size 0;
            proxy_buffering off;
            proxy_redirect off;
            proxy_read_timeout 5m;
            send_timeout 5m;
            gzip off;
            proxy_intercept_errors on;
            error_page 400 404 =404 @nginx_404;
        }

3. Перезапустите сервис

systemctl restart nginx.service

Hysteria2

Этот вариант добавляет отдельный inbound Hysteria2 на 443/udp. Основной Reality inbound при этом не меняется.

Note

Везде, где указано example.com, подставьте свой домен.

Warning

В Xray-core для Hysteria2 пока нет аналога Xver: 1, который передаёт IP клиента в Nginx через proxy_protocol. Поэтому настройки proxy_protocol из раздела Fail2Ban работают для Reality fallback, но не для Hysteria2 masquerade. Именно поэтому для Hysteria2 создаётся отдельный listen 127.0.0.1:8444 без proxy_protocol. В таком режиме Fail2Ban почти бесполезен против сканеров и DDoS по Hysteria2; создавайте Hysteria2 только если это вас не смущает.

Необходимо

  • Открытый порт 443/udp.
  • Доступ 3x-ui к TLS-сертификату из volume nginx-certs.

1. Опубликуйте UDP порт в главном pod

  • Добавьте в секцию [Pod] следующий параметр:
    [Pod]
    PublishPort=0.0.0.0:443:443/udp

2. Примонтируйте сертификаты в сервис 3x-ui

  • Добавьте в секцию [Container] следующий volume:
    [Container]
    Volume=nginx-certs:/certs

3. Добавьте порт маскировки в конфигурацию Nginx

  • В секцию server добавьте следующие параметры:
    server {
        listen 127.0.0.1:8444;
        http3 on;
        add_header Alt-Svc 'h3=":443"; ma=86400' always;
    }

Note

127.0.0.1:8444 используется как URL маскировки для Hysteria.

4. Перезапустите pod

systemctl daemon-reload
systemctl restart main-pod.service

5. Создайте inbound для Hysteria

  • В панели: Inbounds -> Add Inbound
    • Remark: hysteria
    • Protocol: hysteria
    • Port: 443
    • SNI: example.com
    • uTLS: None
    • ALPN: h3
    • Reject Unknown SNI: True
    • Digital Certificate: File Path
      • Public Key: /certs/example.com_ecc/fullchain.cer
      • Private Key: /certs/example.com_ecc/example.com.key
    • Нажмите Get New ECH Cert и сгенерируйте ECH key и ECH config
    • Masquerade: True
      • Type: proxy
      • URL: http://127.0.0.1:8444
    • Создайте новый UDP Mask
      • Type: Salamander
      • Сгенерируйте пароль
    • QUIC Params: True
      • Congestion: Brutal
      • Brutal Up: 25000000
      • Brutal Down: 25000000
    • Sniffing: True
      • HTTP: Mark
      • TLS: Mark
      • Route only: True
    • Нажмите Create

Настройка клиентов

Клиенты создаются отдельно в Clients. Поле Email используйте как имя пользователя: выбирайте понятное и осмысленное значение.

1. Создайте клиента

  • В панели: Clients
    • Нажмите Add Client
    • Email: осмысленное имя пользователя
    • Для VLESS/Reality обязательно:
      • Flow: xtls-rprx-vision
    • Для VLESS/XHTTP и Hysteria2 остальные параметры можно оставить по умолчанию
    • Обязательно присвойте клиенту нужные inbounds в Attached inbounds
    • Нажмите Create

2. Получите ссылку подписки

  • В списке Clients:
    • В списке клиентов у нужного клиента нажмите кнопку More information с иконкой перевёрнутого восклицательного знака
    • В окне будут обычные vless:// или hysteria2:// ссылки и Subscription info
    • Скопируйте Sub ссылку из Subscription info
    • Эту ссылку нужно вставлять в клиентское приложение

Note

Обычные vless:// и hysteria2:// ссылки также доступны, но для подписки используйте Sub ссылку из Subscription info.

3. Объедините несколько inbound в одну подписку

  • Если создать пользователя с тем же Subscription info в других inbound, все ссылки будут попадать в одну подписку клиента.
  • Также можно копировать пользователей из одного inbound в другой:
    • Inbounds -> 3 точки у inbound -> Copy Clients from inbound
    • Выберите нужных клиентов галочками
    • После копирования Subscription info сохранится на новом inbound
    • Новый inbound появится в той же подписке пользователя

IPv6

Необходимо

  • VPS с белым IPv6.
  • Домен с AAAA-записью на IP VPS.

Проверка окружения

  1. Проверьте наличие IPv6 у вашего VPS:
    curl -6 ifconfig.me
  2. Проверьте DNS: у домена AAAA указывает на ваш IP.
    dig +short AAAA example.com

1. Включите IPv6 в главном network

  • Добавьте в секцию [Network] следующие параметры:
    [Network]
    IPv6=yes
    Subnet=fd10:50::/64
    Gateway=fd10:50::1

2. Перезапустите pod

systemctl daemon-reload
systemctl restart main-pod.service

3. Опционально: Включите forwarding и accept_ra

Note

Данное действие требуется в случае, если IPv6-адрес назначается через RA.

IFACE=$(ip -o -6 route show default | awk '{print $5; exit}')
[ -z "${IFACE}" ] && exit 0
echo -e "net.ipv6.conf.all.forwarding=1\nnet.ipv6.conf.${IFACE}.accept_ra=2" | tee /etc/sysctl.d/99-ipv6.conf
sysctl --system

4. Опционально: Опубликуйте IPv6 порт в главном pod

Warning

Рекомендуется выполнять этот шаг только в том случае, если порт 443 недоступен по IPv6.

curl -6 -I https://example.com
  • Добавьте в секцию [Pod] следующий параметр:
    [Pod]
    PublishPort=[::]:443:443

Warning

На некоторых версиях Podman/netavark явная публикация IPv6-порта ([::]:443:443) может приводить к дублированию nft/DNAT-правил при restart. Если это воспроизводится, временный обходной путь - оставить только IPv4 publish. При этом IPv6 может продолжать работать, но это зависит от версии и используемого network backend.

Защита Nginx с помощью Fail2Ban

1. Включите proxy protocol в конфигурации Nginx

  • Добавьте proxy_protocol на уровне server в секцию listen:

    server {
        listen 127.0.0.1:8443 ssl proxy_protocol;
    }
  • Также в секцию server добавьте следующие параметры:

    server {
        set_real_ip_from 127.0.0.1;
        real_ip_header proxy_protocol;
    }

2. Подготовьте среду для логов Nginx

  • Создайте новый каталог для логов:

    mkdir -p /etc/nginx/logs
  • Добавьте в секцию [Container] Nginx сервиса:

    [Container]
    Volume=/etc/nginx/logs:/etc/nginx/logs

3. Создайте конфигурацию для limit requests

# /etc/nginx/conf.d/00-limit_req.conf
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req_status 429;
  • Добавьте limit_req на уровне server в конфигурацию Nginx:
    server {
        limit_req zone=one burst=20 nodelay;
    }

4. Создайте конфигурацию для логов

# /etc/nginx/conf.d/01-logs.conf
log_format  main_log  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" limit=$limit_req_status '
                      '"$http_user_agent" "$http_x_forwarded_for"';

access_log  /etc/nginx/logs/access.log  main_log;

5. Создайте конфигурации для Fail2Ban

  • Создайте фильтр для Fail2Ban:

    # /etc/fail2ban/filter.d/nginx-flood.conf
    [Definition]
    failregex = ^<HOST>\s.*\slimit=REJECTED\b
  • Создайте jail конфигурацию:

    # /etc/fail2ban/jail.local
    [DEFAULT]
    ignoreip = 127.0.0.1/8 ::1 10.50.0.0/24 fd10:50::/64
    
    [nginx-flood]
    enabled = true
    filter = nginx-flood
    logpath = /etc/nginx/logs/access.log
    findtime = 10
    maxretry = 10
    bantime = 5h
    action = nftables[chain_hook=forward, chain=f2b-forward, port="https"]

Note

10 запросов 429 за 10 секунд - бан на 5 часов

6. Настройте 3x-ui для proxy protocol

  • В панели: Inbounds -> 3 точки у вашего vless reality inbound -> Edit
    • Xver: 1
    • Нажмите Update

7. Перезапустите сервисы

systemctl restart nginx.service fail2ban.service

8. Проверьте работоспособность Fail2Ban

systemctl status fail2ban.service --no-pager
fail2ban-client status nginx-flood
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment