Инструкция поднимает связку 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.
- Требования и проверки перед стартом
- Подготовка окружения
- Системные настройки
- Настройка сертификатов
- Настройка сервисов
- Настройка Nginx
- Запуск и проверка
- Настройка 3x-ui
- Автообновление контейнеров
- Настройка протоколов
- Настройка клиентов
- IPv6
- Защита Nginx с помощью Fail2Ban
- VPS с белым IPv4 и доступом по SSH (root или sudo).
- Домен с A-записью на IP VPS.
- Открытые порты:
80/tcp(для выпуска/обновления сертификата) и443/tcp. - На VPS установлены:
podmanверсии>= 4.4systemdверсии>= 244
-
Проверьте DNS: у домена
Aуказывает на ваш IP.dig +short A example.com
-
Убедитесь, что никто не слушает
80/443:ss -tulpn | grep -E ':(80|443)\b'
-
Проверьте версии:
podman --version systemctl --version
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/systemdecho -e "net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr" | tee /etc/sysctl.d/99-bbr.conf
sysctl --systemСертификат сохраняется в volume nginx-certs.
-
выбираем CA = Let's Encrypt
podman run --rm -it -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \ --set-default-ca --server letsencrypt
-
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
# /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# /etc/containers/systemd/main.network
[Unit]
PartOf=main-pod.service
[Network]
Subnet=10.50.0.0/24
Gateway=10.50.0.1# /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# /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# /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# /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.
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'
systemctl enable --now nginx-renew.timer
systemctl list-timers | grep nginx-renew.timerПанель доступна только по SSH-туннелю.
ssh -L 2053:127.0.0.1:2053 root@example.com- Откройте в браузере:
http://127.0.0.1:2053- Логин:
admin - Пароль:
admin
- Логин:
- В панели:
Panel Settings -> Subscription- Reverse Proxy URI:
https://example.com/sub/ - Нажмите
Save, затемRestart Panel
- Reverse Proxy URI:
- В quadlet уже задано:
[Container] AutoUpdate=registry
Note
Это позволяет nginx и 3x-ui автоматически подтягивать новые образы и перезапускаться через podman-auto-update.
systemctl enable --now podman-auto-update.timersystemctl status podman-auto-update.timer --no-pager
systemctl list-timers | grep podman-auto-updateVLESS/Reality является основной базой. VLESS XHTTP и Hysteria2 можно добавить после базовой настройки, независимо друг от друга.
- В панели:
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
- Remark:
Этот вариант добавляет отдельный 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: кэширование нужно полностью отключить.
- В панели:
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
- Padding Placement:
- Uplink Data Placement:
header- Uplink Data Key:
X-Req
- Uplink Data Key:
- Security:
none - External Proxy: True
- Для CDN:
- Force TLS:
TLS - Dest:
cdn.example.com - Port:
443 - Remark:
CDN - Fingerprint:
firefox - Apln:
h2
- Force TLS:
- Без CDN:
- Force TLS:
TLS - Dest:
example.com - Port:
443 - Remark:
DIRECT - Fingerprint:
firefox - Apln:
h2
- Force TLS:
- Для CDN:
- Sniffing: True
- HTTP: Mark
- TLS: Mark
- Route only: True
- Нажмите
Create
- Remark:
- В конфигурацию 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; }
systemctl restart nginx.serviceЭтот вариант добавляет отдельный 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.
systemctl daemon-reload
systemctl restart main-pod.service- В панели:
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
- Public Key:
- Нажмите Get New ECH Cert и сгенерируйте ECH key и ECH config
- Masquerade: True
- Type:
proxy - URL:
http://127.0.0.1:8444
- Type:
- Создайте новый UDP Mask
- Type:
Salamander - Сгенерируйте пароль
- Type:
- QUIC Params: True
- Congestion:
Brutal - Brutal Up:
25000000 - Brutal Down:
25000000
- Congestion:
- Sniffing: True
- HTTP: Mark
- TLS: Mark
- Route only: True
- Нажмите
Create
- Remark:
Клиенты создаются отдельно в Clients. Поле Email используйте как имя пользователя: выбирайте понятное и осмысленное значение.
- В панели:
Clients- Нажмите
Add Client - Email: осмысленное имя пользователя
- Для VLESS/Reality обязательно:
- Flow:
xtls-rprx-vision
- Flow:
- Для VLESS/XHTTP и Hysteria2 остальные параметры можно оставить по умолчанию
- Обязательно присвойте клиенту нужные inbounds в
Attached inbounds - Нажмите
Create
- Нажмите
- В списке
Clients:- В списке клиентов у нужного клиента нажмите кнопку
More informationс иконкой перевёрнутого восклицательного знака - В окне будут обычные
vless://илиhysteria2://ссылки иSubscription info - Скопируйте
Subссылку изSubscription info - Эту ссылку нужно вставлять в клиентское приложение
- В списке клиентов у нужного клиента нажмите кнопку
Note
Обычные vless:// и hysteria2:// ссылки также доступны, но для подписки используйте Sub ссылку из Subscription info.
- Если создать пользователя с тем же
Subscription infoв других inbound, все ссылки будут попадать в одну подписку клиента. - Также можно копировать пользователей из одного inbound в другой:
Inbounds -> 3 точки у inbound -> Copy Clients from inbound- Выберите нужных клиентов галочками
- После копирования
Subscription infoсохранится на новом inbound - Новый inbound появится в той же подписке пользователя
- VPS с белым IPv6.
- Домен с AAAA-записью на IP VPS.
- Проверьте наличие IPv6 у вашего VPS:
curl -6 ifconfig.me
- Проверьте DNS: у домена
AAAAуказывает на ваш IP.dig +short AAAA example.com
1. Включите IPv6 в главном network
- Добавьте в секцию
[Network]следующие параметры:[Network] IPv6=yes Subnet=fd10:50::/64 Gateway=fd10:50::1
systemctl daemon-reload
systemctl restart main-pod.serviceNote
Данное действие требуется в случае, если 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 --system4. Опционально: Опубликуйте 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.
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; }
-
Создайте новый каталог для логов:
mkdir -p /etc/nginx/logs
-
Добавьте в секцию
[Container]Nginx сервиса:[Container] Volume=/etc/nginx/logs:/etc/nginx/logs
# /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; }
# /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;-
Создайте фильтр для 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 часов
- В панели:
Inbounds -> 3 точки у вашего vless reality inbound -> Edit- Xver:
1 - Нажмите
Update
- Xver:
systemctl restart nginx.service fail2ban.servicesystemctl status fail2ban.service --no-pagerfail2ban-client status nginx-flood