example.com + 3x-ui + VLESS REALITY Self-SNI
README — example.com + api.example.com + 3x-ui + VLESS REALITY + XHTTP
Автор: @Yazykow
Этот документ описывает итоговую конфигурацию сервера после развёртывания сайта, 3x-ui и VLESS REALITY с Self-SNI.
Цели конфигурации:
- обычный HTTPS-сайт продолжает работать на стандартном TCP-порту
443; - тот же
443/tcpиспользуется VLESS REALITY; - обычный TLS-клиент или сканер получает настоящий сайт с настоящим TLS-сертификатом;
- внутренний nginx поддерживает HTTP/2;
- Xray передаёт nginx реальный IP клиента через PROXY protocol v1;
- nginx передаёт реальный IP дальше приложению;
- 3x-ui panel не доступна из внешней сети и открывается только через SSH port forwarding;
- Xray core хранится отдельно от файлов панели и его версия фиксируется независимо от обновления 3x-ui;
- сертификаты Let's Encrypt продолжают автоматически обновляться через HTTP-01 на порту
80; - отдельный subscription endpoint работает через
api.example.com; - отдельный VLESS XHTTP
packet-upработает на публичном8444/tcp, не затрагивая основной REALITY на443.
В документе намеренно отсутствуют реальные пароли, Web Base Path панели, UUID клиентов, REALITY private/public keys, Short ID, Subscription ID, XHTTP private path, реальные IP-адреса сервера и клиентские URL/URI.
1. Итоговая архитектура
INTERNET
|
| TCP/443
v
Xray / REALITY
VLESS inbound
xver = 1
/ \
/ \
REALITY client обычный TLS client
| |
| |
v |
VLESS tunnel |
|
PROXY protocol v1
|
v
127.0.0.1:8443
nginx
TLS 1.2/1.3
HTTP/2
|
|
X-Real-IP / XFF
|
v
127.0.0.1:18080
|
v
Docker / Django
|
v
PostgreSQL
HTTP обслуживается отдельно:
Internet :80
|
v
nginx
|
+--> /.well-known/acme-challenge/
| |
| +--> Let's Encrypt HTTP-01
|
+--> остальные запросы
|
+--> redirect на HTTPS
Панель 3x-ui:
Internet
|
X прямого доступа нет
|
3x-ui panel
локальный компьютер
|
| SSH port forwarding
v
сервер 127.0.0.1:25572
|
v
3x-ui
2. Основные порты
Ожидаемое состояние:
| Порт | Bind | Процесс | Назначение |
|---|---|---|---|
| SSH | public | sshd |
администрирование только по SSH key |
80/tcp |
public | nginx | redirect + Let's Encrypt HTTP-01 |
443/tcp |
public | Xray | основной VLESS REALITY RAW/TCP + Self-SNI |
8444/tcp |
public | Xray | отдельный VLESS XHTTP packet-up + REALITY |
8443/tcp |
127.0.0.1 |
nginx | внутренний HTTPS Self-SNI backend |
18080/tcp |
127.0.0.1 |
Docker | Django application |
25572/tcp |
127.0.0.1 |
x-ui | 3x-ui web panel |
2096/tcp |
127.0.0.1 |
x-ui | 3x-ui Subscription Server |
Проверка:
sudo ss -lntp | grep -E ':(80|443|8444|8443|18080|25572|2096)?'
Ожидается концептуально:
0.0.0.0:80 nginx
[::]:80 nginx
*:443 Xray
*:8444 Xray
127.0.0.1:8443 nginx
127.0.0.1:18080 docker-proxy
127.0.0.1:25572 x-ui
127.0.0.1:2096 x-ui
Публичными являются только SSH, 80, 443 и 8444.
8443, 18080, 25572, 2096 наружу не открываются.
3. Сайт
Проект развёрнут как Docker Compose stack:
/opt/stacks/yazykow/
Основные команды:
cd /opt/stacks/yazykow
docker compose ps
docker compose logs --tail=100
docker compose restart
Проверка приложения напрямую:
curl http://127.0.0.1:18080/healthz
Ожидается:
ok
3.1. Проверка Docker
cd /opt/stacks/yazykow
docker compose ps
Контейнеры должны быть в состоянии running / healthy.
Логи:
docker compose logs --tail=200
Логи конкретного сервиса:
docker compose logs --tail=200 web
docker compose logs --tail=200 db
docker compose logs --tail=200 nginx
4. Обновление сайта
Перед каждым обновлением рекомендуется сделать backup базы.
cd /opt/stacks/yazykow
mkdir -p backup
chmod 700 backup
docker compose exec -T db sh -c \
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
> "backup/postgres-$(date +%Y%m%d-%H%M%S).dump"
Проверить:
ls -lh backup/
После загрузки новой версии:
cd /opt/stacks/yazykow
docker compose config --quiet
docker compose build --pull
docker compose up -d
docker compose ps
docker compose logs --tail=100
После обновления:
curl http://127.0.0.1:18080/healthz
И внешний HTTPS health check:
curl -I https://example.com
Важно
Без необходимости нельзя использовать:
docker compose down -v
Флаг -v удаляет Docker volumes и может удалить PostgreSQL-данные.
5. Публичный nginx — только порт 80
Файл:
/etc/nginx/sites-available/example.com
Он не должен слушать публичный 443, поскольку 443 принадлежит Xray.
Итоговый конфиг:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
access_log /var/log/nginx/yazykow-access.log;
error_log /var/log/nginx/yazykow-error.log;
}
Проверить активные listen:
sudo nginx -T 2>/dev/null | grep -nE 'listen .*443'
Публичного:
listen 443 ssl;
в обычном site-конфиге быть не должно.
6. Внутренний nginx на 8443
Это ключевая часть Self-SNI.
Файл:
/etc/nginx/sites-available/yazykow-reality-backend
Symlink:
/etc/nginx/sites-enabled/yazykow-reality-backend
Финальный конфиг:
server {
listen 127.0.0.1:8443 ssl proxy_protocol;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Xray подключается к backend локально.
# При xver=1 он передаёт исходный IP клиента
# в PROXY protocol v1.
real_ip_header proxy_protocol;
set_real_ip_from 127.0.0.1;
client_max_body_size 16m;
location / {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# После real_ip_header $remote_addr содержит
# IP исходного клиента, а не 127.0.0.1.
proxy_set_header X-Real-IP $remote_addr;
# Не доверяем произвольному X-Forwarded-For,
# присланному самим клиентом.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
}
access_log /var/log/nginx/yazykow-reality-access.log;
error_log /var/log/nginx/yazykow-reality-error.log;
}
Применение:
sudo nginx -t
sudo systemctl reload nginx
7. Почему включён HTTP/2
Внутренний nginx — это TLS target REALITY.
Обычный TLS-клиент, который не является REALITY-клиентом, в итоге попадает именно сюда.
Поэтому target должен вести себя как обычный современный HTTPS-сайт:
TLS 1.2
TLS 1.3
ALPN h2
ALPN http/1.1
настоящий сертификат
настоящий сайт
HTTP/2 включается:
http2 on;
Проверить поддержку nginx:
sudo nginx -V 2>&1 | grep -oE 'http_v2_module|http_realip_module'
Желательно наличие:
http_v2_module
http_realip_module
8. PROXY protocol и реальный IP
При xver=0 Xray подключается к nginx с localhost, поэтому nginx видит:
127.0.0.1
При xver=1 Xray передаёт исходный IP и порт клиента через PROXY protocol v1.
Поэтому обе стороны должны быть настроены согласованно:
Xray:
Xver = 1
nginx:
listen 127.0.0.1:8443 ssl proxy_protocol;
real_ip_header proxy_protocol;
set_real_ip_from 127.0.0.1;
Связка:
клиент
|
v
Xray :443
|
| PROXY protocol v1
| source=<реальный IP клиента>
v
nginx :8443
|
| real_ip_header proxy_protocol
v
$remote_addr = реальный IP клиента
После этого nginx передаёт IP приложению:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
9. Важное правило xver / proxy_protocol
Эти две настройки нельзя менять независимо.
Рабочие комбинации:
Xver 0
+
nginx без proxy_protocol
или:
Xver 1
+
nginx с proxy_protocol
Нерабочая комбинация:
Xver 1
+
nginx без proxy_protocol
Также нерабочая:
Xver 0
+
nginx ожидает proxy_protocol
Если стороны не согласованы, обычный HTTPS fallback перестаёт нормально работать.
10. Проверка внутреннего backend после xver=1
После включения proxy_protocol обычный прямой:
curl https://127.0.0.1:8443
не является корректным тестом, потому что nginx ожидает PROXY header перед TLS handshake.
Использовать:
curl \
--haproxy-protocol \
--http2 \
--resolve example.com:8443:127.0.0.1 \
-I \
https://example.com:8443/
Ожидается:
HTTP/2 200
Health check:
curl \
--haproxy-protocol \
--http2 \
--resolve example.com:8443:127.0.0.1 \
https://example.com:8443/healthz
Ожидается:
ok
Если локальный curl собран без HTTP/2, убрать --http2 и проверить ALPN через внешний 443.
11. Проверка HTTP/2 через внешний 443
Проверка:
curl --http2 -I https://example.com
Ожидается:
HTTP/2 200
ALPN:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-alpn 'h2,http/1.1' \
</dev/null 2>/dev/null \
| grep -i 'ALPN protocol'
Ожидается:
ALPN protocol: h2
12. Проверка реального IP посетителя
Лог внутреннего nginx:
sudo tail -f /var/log/nginx/yazykow-reality-access.log
После запроса с другого устройства/сети первый IP в access log должен быть реальным IP посетителя.
Не должно быть:
127.0.0.1
Если там остаётся localhost, проверить:
REALITY Xver = 1
и:
listen 127.0.0.1:8443 ssl proxy_protocol;
real_ip_header proxy_protocol;
set_real_ip_from 127.0.0.1;
13. REALITY inbound
Публичный inbound:
Protocol: VLESS
Port: 443
Security: REALITY
Transport: RAW / TCP
Flow: xtls-rprx-vision
Target: 127.0.0.1:8443
Xver: 1
Server Names:
example.com
www.example.com
Клиентская сторона концептуально:
Address: домен сервера
Port: 443
Protocol: VLESS
Security: REALITY
Flow: xtls-rprx-vision
SNI: example.com
Fingerprint: chrome
Также требуются индивидуальные:
UUID
REALITY Public Key
Short ID
Они не включены в этот README.
Private Key REALITY также не включён.
14. Как работает Self-SNI
Обычный REALITY client:
client
|
| правильный UUID / public key / short id / SNI
v
Xray :443
|
v
VLESS tunnel
Обычный браузер/сканер:
browser
|
| TLS ClientHello
v
Xray :443
|
| не REALITY client
|
| PROXY protocol v1
v
127.0.0.1:8443
|
v
nginx TLS + HTTP/2
|
v
настоящий сайт
Таким образом внешний 443 выглядит как настоящий HTTPS endpoint сайта.
15. 3x-ui panel
Systemd service:
sudo systemctl status x-ui
CLI:
sudo x-ui
Настройки:
sudo x-ui settings
База:
/etc/x-ui/x-ui.db
Panel bind:
127.0.0.1:25572
Проверка:
sudo ss -lntp | grep ':25572'
Правильно:
127.0.0.1:25572
Неправильно:
0.0.0.0:25572
или:
*:25572
16. Доступ к панели только через SSH tunnel
Порт панели наружу не открывается.
На клиентском компьютере создаётся SSH forwarding:
ssh -N \
-L 25572:127.0.0.1:25572 \
dds@SERVER_IP
Если используется отдельный SSH key:
ssh \
-i ~/.ssh/dds_new_server \
-N \
-L 25572:127.0.0.1:25572 \
dds@SERVER_IP
После создания tunnel браузер подключается к локальному 127.0.0.1:25572 с приватным Web Base Path.
Сам Web Base Path в этот README не записывается.
17. Subscription Server 3x-ui
Subscription Server используется отдельно от web-панели.
Параметры концептуально:
Enable Subscription: ON
Listen IP: 127.0.0.1
Port: 2096
Domain: api.example.com
URI Path: /offices-shown-liver-chapter/
Certificate File: пусто
Private Key File: пусто
TLS на 2096 не нужен: HTTPS завершается на внутреннем nginx.
Проверка listener:
sudo ss -lntp | grep ':2096'
Ожидается:
127.0.0.1:2096
Прямой локальный тест backend:
curl -i \
-H 'Host: api.example.com' \
"http://127.0.0.1:2096/offices-shown-liver-chapter/<SUB_ID>"
<SUB_ID> в README не хранится.
Внешняя цепочка:
client
|
| HTTPS :443
v
api.example.com
|
v
Xray REALITY
|
| обычный TLS fallback
| PROXY protocol v1
v
nginx 127.0.0.1:8443
|
| /offices-shown-liver-chapter/
v
127.0.0.1:2096
|
v
3x-ui Subscription Server
17.1. nginx для api.example.com
Отдельный HTTP server block нужен для ACME/redirect:
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
access_log /var/log/nginx/api-yazykow-access.log;
error_log /var/log/nginx/api-yazykow-error.log;
}
Внутренний TLS server block на 8443:
server {
listen 127.0.0.1:8443 ssl proxy_protocol;
http2 on;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
real_ip_header proxy_protocol;
set_real_ip_from 127.0.0.1;
location ^~ /offices-shown-liver-chapter/ {
proxy_pass http://127.0.0.1:2096;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
}
location / {
return 404;
}
access_log /var/log/nginx/api-yazykow-reality-access.log;
error_log /var/log/nginx/api-yazykow-reality-error.log;
}
Проверка через внутренний nginx:
curl \
--haproxy-protocol \
--http2 \
--resolve api.example.com:8443:127.0.0.1 \
-i \
"https://api.example.com:8443/offices-shown-liver-chapter/<SUB_ID>"
Внешний тест:
curl -i \
"https://api.example.com/offices-shown-liver-chapter/<SUB_ID>"
17.2. Отдельный VLESS XHTTP + REALITY
Основной REALITY на 443 не изменяется.
Добавлен отдельный inbound:
Port: 8444
Protocol: VLESS
Transport: XHTTP
Mode: packet-up
Security: REALITY
Flow: пусто
Target: 127.0.0.1:8443
Xver: 1
Server Names:
example.com
www.example.com
Для этого inbound используются отдельные:
UUID
REALITY Private/Public Key
Short ID
XHTTP Path
Эти значения в README не записываются.
XHTTP transport:
Mode: packet-up
Path: /<RANDOM_XHTTP_PATH>
Host: example.com
XMUX: OFF
Flow: пусто
Продвинутые XHTTP/session параметры без необходимости не меняются.
Логическая схема:
Internet :8444
|
v
Xray
VLESS + XHTTP
mode=packet-up
REALITY
xver=1
/ \
/ \
client обычный TLS
| |
v |
VLESS tunnel |
v
PROXY protocol v1
|
v
127.0.0.1:8443
|
v
nginx
|
v
настоящий сайт
Поскольку внутренний nginx ожидает PROXY protocol, XHTTP inbound также использует:
Xver = 1
Проверка listener:
sudo ss -lntp | grep ':8444'
Проверка обоих Xray ports:
sudo ss -lntp | grep -E ':(443|8444)\b'
Ожидается концептуально:
*:443 Xray
*:8444 Xray
Fallback-тест XHTTP inbound:
curl \
--resolve example.com:8444:127.0.0.1 \
--http2 \
-I \
https://example.com:8444/
Это проверяет только REALITY fallback и Self-SNI.
Сам XHTTP transport проверяется реальным Xray-compatible клиентом.
Клиент концептуально:
Protocol: VLESS
Address: основной домен
Port: 8444
UUID: отдельный UUID
Flow: пусто
Transport: XHTTP
Mode: packet-up
Path: /<RANDOM_XHTTP_PATH>
Host: example.com
Security: REALITY
SNI: example.com
Fingerprint: chrome
Public Key: отдельный Public Key
Short ID: отдельный Short ID
18. Pinned Xray core
Установленная и зафиксированная версия:
Xray 26.7.28
Оригинальный binary:
/usr/local/x-ui/bin/xray-linux-amd64
Pinned-копия:
/opt/xray-pinned/
Проверка:
sudo /opt/xray-pinned/xray-linux-amd64 version
Ожидается версия:
Xray 26.7.28
19. XUI_BIN_FOLDER
3x-ui настроена использовать отдельный каталог Xray:
XUI_BIN_FOLDER=/opt/xray-pinned
Файл:
/etc/default/x-ui
Содержимое:
XUI_BIN_FOLDER=/opt/xray-pinned
Systemd unit содержит:
EnvironmentFile=-/etc/default/x-ui
Проверить файл:
sudo cat /etc/default/x-ui
Проверить environment реально запущенного процесса:
PID=$(systemctl show -p MainPID --value x-ui)
sudo cat /proc/$PID/environ \
| tr '\0' '\n' \
| grep '^XUI_BIN_FOLDER='
Ожидается:
XUI_BIN_FOLDER=/opt/xray-pinned
20. Важное ограничение pinned core
Pinned Xray защищает от неожиданной замены бинарника при обновлении панели.
Но 3x-ui всё ещё управляет процессом Xray.
То есть:
панель != Xray binary
но:
панель управляет Xray process/config
Некоторые операции панели могут перезапустить Xray.
Перед обновлением панели:
- сделать backup базы 3x-ui;
- сделать backup
/opt/xray-pinned; - зафиксировать текущую версию Xray;
- проверить требования новой версии 3x-ui;
- не использовать
Update Xray, если core обновлять не планируется; - после обновления проверить сайт и VLESS.
21. Backup 3x-ui
Каталог:
sudo mkdir -p /root/3x-ui-backups
sudo chmod 700 /root/3x-ui-backups
Установить sqlite3 при необходимости:
sudo apt install -y sqlite3
Backup SQLite базы:
sudo sqlite3 /etc/x-ui/x-ui.db \
".backup '/root/3x-ui-backups/x-ui-$(date +%Y%m%d-%H%M%S).db'"
Backup pinned Xray:
sudo tar \
-C /opt \
-czpf "/root/3x-ui-backups/xray-pinned-$(date +%Y%m%d-%H%M%S).tar.gz" \
xray-pinned
Проверка:
sudo ls -lh /root/3x-ui-backups/
22. Nginx backups
Перед существенными изменениями конфигурации создавались копии /etc/nginx.
Пример паттернов:
/root/nginx-before-reality-*
/root/nginx-before-443-xray-*
/root/nginx-before-xver1-h2-*
Проверить:
sudo ls -ld /root/nginx-before-*
Перед любым восстановлением сначала проверить содержимое нужного backup.
23. Let's Encrypt
Сертификат внутреннего nginx:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
Он используется на:
127.0.0.1:8443
Публичный 443 при этом слушает Xray.
Проверить сертификаты:
sudo certbot certificates
24. Certbot renewal через HTTP-01
Поскольку nginx больше не владеет публичным 443, renewal рекомендуется делать через HTTP-01/webroot на 80.
Webroot:
/var/www/letsencrypt
Концептуальная настройка:
sudo certbot reconfigure \
--cert-name example.com \
--authenticator webroot \
--webroot-path /var/www/letsencrypt
Обязательная проверка:
sudo certbot renew --dry-run
После успешного renewal nginx должен перечитать сертификат.
Deploy hook:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
Файл:
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Содержимое:
#!/bin/sh
systemctl reload nginx
Создать:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod 755 \
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
25. Firewall
Публичная поверхность:
SSH
80/tcp
443/tcp
8444/tcp
8444 нужен для отдельного XHTTP inbound:
sudo ufw allow 8444/tcp
Только localhost:
8443
18080
25572
2096
Проверка:
sudo ufw status numbered
sudo ss -lntup
Не добавлять public UFW rules для:
8443
18080
25572
2096
26. SSH security
Основной администратор:
dds
Административные действия:
sudo ...
Используется:
SSH key authentication
Отключено:
password authentication
root SSH login
keyboard-interactive authentication
Проверить:
sudo sshd -T | grep -Ei \
'pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|permitrootlogin'
Ожидается:
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
permitrootlogin no
27. Полный health check
Сайт
curl -I https://example.com
HTTP/2
curl --http2 -I https://example.com
ALPN
openssl s_client \
-connect example.com:443 \
-servername example.com \
-alpn 'h2,http/1.1' \
</dev/null 2>/dev/null \
| grep -i 'ALPN protocol'
Ожидается:
ALPN protocol: h2
Django
curl http://127.0.0.1:18080/healthz
Ожидается:
ok
Internal TLS + PROXY protocol + HTTP/2
curl \
--haproxy-protocol \
--http2 \
--resolve example.com:8443:127.0.0.1 \
https://example.com:8443/healthz
Ожидается:
ok
Nginx
sudo nginx -t
Порты
sudo ss -lntp | grep -E ':(80|443|8444|8443|18080|25572|2096)\b'
Subscription backend
sudo ss -lntp | grep ':2096'
Subscription через nginx
curl \
--haproxy-protocol \
--http2 \
--resolve api.example.com:8443:127.0.0.1 \
-I \
"https://api.example.com:8443/offices-shown-liver-chapter/<SUB_ID>"
XHTTP listener
sudo ss -lntp | grep ':8444'
XHTTP fallback
curl \
--resolve example.com:8444:127.0.0.1 \
--http2 \
-I \
https://example.com:8444/
X-ui
sudo systemctl status x-ui --no-pager
Xray
ps -ef | grep '[x]ray'
Pinned version
sudo /opt/xray-pinned/xray-linux-amd64 version
Docker
cd /opt/stacks/yazykow
docker compose ps
28. Проверка реального IP end-to-end
Открыть лог:
sudo tail -f /var/log/nginx/yazykow-reality-access.log
С другого устройства или другой сети сделать HTTPS-запрос к сайту.
В access log должен появиться внешний IP клиента.
Путь IP:
client public IP
|
v
Xray :443
|
| PROXY protocol v1
v
nginx :8443
|
| real_ip_header proxy_protocol
v
$remote_addr
|
+--> X-Real-IP
|
+--> X-Forwarded-For
|
v
Django
29. Диагностика: сайт не открывается
Проверить, кто занимает 443:
sudo ss -lntp | grep ':443'
Должен быть Xray.
Проверить nginx:
sudo nginx -t
sudo systemctl status nginx --no-pager
Проверить 8443:
sudo ss -lntp | grep ':8443'
Проверить internal backend с PROXY protocol:
curl \
--haproxy-protocol \
--resolve example.com:8443:127.0.0.1 \
-I \
https://example.com:8443/
Проверить Django:
curl http://127.0.0.1:18080/healthz
Проверить Xray/x-ui:
sudo systemctl status x-ui --no-pager
sudo journalctl -u x-ui -n 100 --no-pager
30. Диагностика: сайт сломался после смены Xver
Проверить соответствие:
REALITY:
Xver = 1
nginx:
listen 127.0.0.1:8443 ssl proxy_protocol;
real_ip_header proxy_protocol;
set_real_ip_from 127.0.0.1;
Если Xver и nginx proxy_protocol не согласованы, fallback работать не будет.
31. Диагностика: сайт работает, но IP всегда localhost
Проверить в 3x-ui:
Xver = 1
Проверить nginx:
real_ip_header proxy_protocol;
set_real_ip_from 127.0.0.1;
Проверить log:
sudo tail -f /var/log/nginx/yazykow-reality-access.log
32. Диагностика HTTP/2
Проверить nginx build:
sudo nginx -V 2>&1 | grep http_v2_module
Проверить конфиг:
http2 on;
Проверить внешний ALPN:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-alpn 'h2,http/1.1' \
</dev/null 2>/dev/null \
| grep -i ALPN
33. Rollback REALITY -> обычный nginx
При серьёзной проблеме можно временно вернуть nginx непосредственно на публичный 443.
Общий порядок:
- освободить
443от Xray; - восстановить nginx-конфиг с публичным TLS listener;
- проверить
nginx -t; - reload nginx;
- проверить сайт.
Проверить backups:
sudo ls -dt /root/nginx-before-* 2>/dev/null
После восстановления:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://example.com
Важно: restore выполнять только после проверки выбранной резервной копии.
34. Что нельзя делать вслепую
Не использовать без backup:
docker compose down -v
Не открывать 3x-ui panel наружу.
Не открывать наружу:
8443
18080
25572
Не удалять:
/etc/x-ui/x-ui.db
/opt/xray-pinned
/etc/letsencrypt
Docker volumes
PostgreSQL volumes
Не обновлять Xray core без backup.
Не менять только Xver, не меняя соответствующим образом nginx proxy_protocol.
Не менять только nginx proxy_protocol, оставляя несовместимый Xver.
Не запускать сторонние Self-SNI/fakesite install-скрипты поверх рабочей конфигурации.
35. Ключевые пути
# Site
/opt/stacks/yazykow/
# Public nginx HTTP
/etc/nginx/sites-available/example.com
/etc/nginx/sites-available/api.example.com
# REALITY target nginx
/etc/nginx/sites-available/yazykow-reality-backend
/etc/nginx/sites-enabled/yazykow-reality-backend
# Let's Encrypt
/etc/letsencrypt/live/example.com/
/etc/letsencrypt/live/api.example.com/
/var/www/letsencrypt/
# 3x-ui
/etc/x-ui/x-ui.db
/usr/local/x-ui/
/etc/default/x-ui
# Pinned Xray
/opt/xray-pinned/
# Backups
/root/3x-ui-backups/
/root/nginx-before-reality-*
/root/nginx-before-443-xray-*
/root/nginx-before-xver1-h2-*
36. Финальное состояние
Основной REALITY:
client
|
| TCP/443
v
Xray
VLESS RAW/TCP + REALITY
xver=1
|
+--> REALITY client -> VLESS tunnel
|
+--> обычный TLS
|
| PROXY protocol v1
v
nginx 127.0.0.1:8443
|
v
настоящий сайт
XHTTP:
client
|
| TCP/8444
v
Xray
VLESS + XHTTP
mode=packet-up
REALITY
xver=1
|
+--> XHTTP/REALITY client -> VLESS tunnel
|
+--> обычный TLS
|
| PROXY protocol v1
v
nginx 127.0.0.1:8443
|
v
настоящий сайт
Subscription:
api.example.com:443
|
v
Xray :443
|
| ordinary TLS fallback
| PROXY protocol v1
v
nginx 127.0.0.1:8443
|
| /offices-shown-liver-chapter/
v
127.0.0.1:2096
|
v
3x-ui Subscription Server
Panel:
SSH tunnel
|
v
127.0.0.1:25572
|
v
3x-ui
Pinned Xray:
/opt/xray-pinned/xray-linux-amd64
Xray 26.7.28
Итоговые свойства:
80 -> nginx public HTTP / ACME
443 -> Xray / VLESS RAW + REALITY
8444 -> Xray / VLESS XHTTP packet-up + REALITY
8443 -> nginx localhost only
18080 -> Django localhost only
25572 -> 3x-ui panel localhost only
2096 -> Subscription Server localhost only
REALITY 443 -> xver=1
REALITY 8444 -> xver=1
nginx 8443 -> proxy_protocol
nginx 8443 -> HTTP/2
nginx -> real client IP
api domain -> subscription path -> 2096
Xray core -> pinned отдельно от panel
Это текущая целевая конфигурация сервера.