Тестов в стеке нет. Диагностика — это чтение живого состояния системы:
логи, ss, nft list, docker exec ... version. Почти каждая ошибка
ниже выглядела не тем, чем была — экономит время правило менять по
одному и смотреть на факт, а не на конфиг.
sudo ./scripts/check-stack.sh # все слои разом, read-only
sudo ./scripts/check-reality-target.sh 127.0.0.1:10444 # годится ли таргет
sudo ./scripts/diagnose-reality.sh 200 plug2.example.com # разбор лога nginxПример вывода check-stack.sh на здоровой системе:
── Контейнеры
✓ 3x-ui — running
✓ telemt — running
✓ mtproto_gate — running
✓ stub-site1 — running
✓ stub-site2 — running
── JA4T-сплит (nftables)
✓ таблица inet mtpr_ja4t_split присутствует
✓ правило редиректа на месте
✓ лимит для iOS выключен — режим по умолчанию
── Сертификаты
✓ example-rsa — RSA, сертификатов в цепочке: 3, осталось 89 дн.
✓ example.com — ECDSA, сертификатов в цепочке: 4, осталось 87 дн.
✓ deploy-хук перезагрузки nginx на месте
── Xray
✓ версия 26.6.1 — совпадает с ожидаемой
Строка «лимит для iOS выключен — режим по умолчанию» — норма, не поломка: лимит для iOS отключён намеренно (см. 04-services.md).
Главный диагностический инструмент: по ответу nginx видно, на каком слое сломалось, даже без логов Xray.
bytes_sent |
session_time |
Диагноз |
|---|---|---|
| ≈ 3–4 КБ | ≈ 0.1 с | Reality-fallback: клиент не аутентифицирован, получил TLS заглушки и оборвался |
| 0 | ~0.01 с | Таргет недоступен либо Xray упал раньше ответа |
| > 10 КБ | секунды | Рабочий туннель |
sni="-", нули |
~0.01 с | Сканирование без SNI, обрыв по default "" — норма |
Точный размер fallback свой у каждой заглушки; diagnose-reality.sh
определяет его сам по самому частому значению.
docker exec 3x-ui grep -viE "dokodemo|detour \[api\]" /var/log/x-ui/error.log | tail -30| Сообщение | Слой | Куда смотреть |
|---|---|---|
transport/internet/tcp: REALITY: ... authentication failed or validation criteria not met |
TLS/Reality | ключи, shortId, SNI, версия Xray, ML-DSA65 |
transport/internet/tcp: REALITY: ... handshake did not complete successfully |
TLS/Reality | таргет недоступен или не подходит |
proxy/vless/inbound: ML-KEM-768 handshake failed |
VLESS | Reality прошёл, клиент не умеет VLESS Encryption |
proxy/vless/inbound: invalid request user id |
VLESS | Reality прошёл, неверный UUID |
Самое ценное различение здесь: первые два — проблема до туннеля, вторые два — туннель установился, дело в VLESS.
flowchart TD
S["VPN не работает"] --> L{"Что в<br/>stream_access.log?"}
L -->|"записей нет"| N1["Не доходит до nginx:<br/>DNS, UFW, JA4T"]
L -->|"bytes_sent = 0"| N2["Таргет недоступен<br/>или Xray упал"]
L -->|"bytes_sent ≈ 3-4 КБ<br/>session ≈ 0.1 с"| N3["Reality не аутентифицировал<br/>клиента → fallback"]
L -->|"трафик идёт"| N4["Транспорт жив,<br/>ищите выше: DNS, роутинг"]
N3 --> E{"Что в логе Xray?"}
E -->|"transport/internet/tcp:<br/>REALITY:"| R1["Слой TLS:<br/>ключи · shortId · SNI ·<br/>версия Xray · mldsa65"]
E -->|"proxy/vless/inbound:<br/>ML-KEM-768"| R2["Reality прошёл.<br/>Слой VLESS Encryption:<br/>клиент не поддерживает"]
style N3 fill:#2d1418,stroke:#f85149,color:#c9d1d9
style R1 fill:#0d2136,stroke:#2f81f7,color:#c9d1d9
style R2 fill:#1a1030,stroke:#a371f7,color:#c9d1d9
От дешёвого к дорогому.
- Ссылка перевыпущена? Удалить профиль целиком и добавить заново.
- Ключи парные?
docker exec 3x-ui /app/bin/xray-linux-amd64 x25519 -i "<privateKey>"—Password (PublicKey)=pbkв ссылке. - Версия Xray?
docker exec 3x-ui /app/bin/xray-linux-amd64 version— 26.7.x ломает Reality (грабля №1). - Таргет жив и подходит?
./scripts/check-reality-target.sh 127.0.0.1:10444 - Время синхронно?
date; docker exec 3x-ui date— сравнивать в UTC. - Контейнер ходит наружу?
docker exec 3x-ui /app/bin/xray-linux-amd64 tls ping www.google.com:443
| # | Симптом | Причина | Лечение |
|---|---|---|---|
| 1 | Shadowrocket/mihomo не пускает, Happ работает | Регрессия Xray 26.7.11 (#6477) | Пин 3x-ui:v3.3.1 (Xray 26.6.1) |
| 2 | Версия в compose верная, а работает другая | «Обновить ядро» в панели пишет в writable-слой, не видно в теге образа | ... version + --force-recreate |
| 3 | С mldsa65Seed Reality не работает |
Нужен объёмный сертификат таргета — разбор ниже | RSA-линия для таргета либо без ML-DSA65 |
| 4 | Убрали seed на сервере — не помогло | На клиенте остался mldsa65Verify |
Чистить обе стороны сразу |
| 5 | Через 60–90 дней VPN тихо умирает | Нет deploy-хука, nginx держит старый сертификат в памяти | Хук — 03-certificates.md |
| 6 | certbot renew --dry-run падает |
Две линии на одни домены пишут одну TXT-запись, 10 сек мало | dns_cloudflare_propagation_seconds = 60 |
| 7 | Connection refused при живом контейнере |
Bridge-сеть прячет инбаунды; mask_host=127.0.0.1 указал бы в никуда |
network_mode: host обязателен |
| 8 | Заглушка открывается, маскировка нет | mask_port указывал на HTTP-бэкенд вместо TLS-фронта |
Указать порт TLS-терминации |
| 9 | Ссылки telemt выглядят верно и не работают | public_host разошёлся с tls_domain |
Свести оба значения |
| 10 | nftables-правила не применяются | reject недопустим в цепочке nat |
reject — в отдельной filter/input |
| 11 | После proxy_protocol on падает один бэкенд |
Настройка на весь блок, не per-backend — 04-services.md | Все апстримы принимают PROXY-преамбулу |
| 12 | www.microsoft.com непригоден как таргет |
Сертификат 8273 байта против лимита 8192 (#6356) — разбор ниже | Для теста брать заведомо рабочий домен |
| 13 | Скрипт говорит «правила нет», хотя оно есть | pipefail + grep -q = SIGPIPE, конвейер считается упавшим |
Сохранять вывод в переменную |
Разбор граблей №3 и №12. С mldsa65Seed в поддельный сертификат Reality
встраивается пост-квантовая подпись ~3.3 КБ — чтобы она не выглядела
аномалией, TLS-запись таргета должна быть достаточно крупной. Порог не про
алгоритм, а про объём. Измерения на Xray 26.6.1, единственная переменная —
таргет или сертификат.
Внешние таргеты: nvidia.com, apple.com, bing.com, samsung.com —
✅. cloudflare.com, vk.com, microsoft.com — ❌.
Своя заглушка, разные сертификаты (таргет один — 127.0.0.1:10444):
| Сертификат | Длина цепочки | ML-DSA65 |
|---|---|---|
| ECDSA Let's Encrypt | 3390 байт | ❌ |
| RSA самоподписанный | 2569 байт | ❌ |
| RSA самоподписанный | 4196 байт | ✅ |
| RSA Let's Encrypt 4096 | 4308 байт | ✅ (5/5) |
Дело не в алгоритме, а в объёме: короткий RSA (2569) падает так же, как
ECDSA. Мерить надо подключением — vk.com (2759 байт) не работает,
nvidia.com работает при похожей длине. tls ping показывает не ту
метрику: длину цепочки сертификатов, а не размер всей TLS-записи (плюс
OCSP-степлинг, SCT, расширения) — величины близкие, но не равные, а на
границе это решает.
Отдельно — microsoft.com: запись сертификата 8273 байта против
захардкоженного лимита 8192 в парсере Reality
(XTLS/Xray-core#6356).
Непригоден как таргет вообще, даже без ML-DSA65 — важно, раз он часто
встречается в примерах конфигов.
Проверка конкретного таргета: ./scripts/check-reality-target.sh <host:port>.
sudo ./scripts/check-stack.sh # раз в неделю
sudo certbot renew --dry-run # раз в месяц
sudo ./scripts/diagnose-reality.sh 500 # при жалобах
docker exec 3x-ui /app/bin/xray-linux-amd64 version # раз в месяцПоследнее — на случай, если кто-то нажал «обновить ядро» в панели (грабля
№2); check-stack.sh это тоже ловит, но привычка проверять напрямую не
повредит.