Skip to content

xray: роль VLESS+Reality с режимом реле, хук против ключ-строк - #112

Merged
gistrec merged 2 commits into
mainfrom
xray-role
Oct 3, 2026
Merged

gistrec merged 2 commits into
mainfrom
xray-role

Conversation

@gistrec

@gistrec gistrec commented Oct 3, 2026

Copy link
Copy Markdown
Member

Личный VPN на своём парке вместо внешнего сервиса.

Роль

Inbound VLESS+Reality и отдельный режим реле (dokodemo-door), переливающий TCP на дальний хост по мешу: клиент стучится на ближний хост, рукопожатие Reality терминируется на дальнем, поэтому реле не нужны ни ключи, ни список клиентов — в клиентском профиле меняется только адрес сервера.

Что роль делает сама:

  • пинит версию и sha256 архива — иначе пересборка хоста поставила бы другую сборку;
  • собирает конфиг словарём и сериализует целиком (ручная JSON-разметка ломается на первой запятой при пустом списке), а потом проверяет его xray run -test до рестарта: иначе xray стартует, молча проигнорировав опечатку в имени поля Reality, и диагноз сводится к «не подключается»;
  • сверяет порт каждого inbound'а с firewall хоста и падает, если порт закрыт — на default-drop хосте закрытый inbound снаружи неотличим от блокировки у провайдера;
  • требует, чтобы serverNames и shortIds были списками: скаляр Jinja перебрала бы посимвольно, JSON остался бы валидным, и возразил бы только xray — рукопожатием;
  • закрывает клиенту петлю, меш и приватные сети (за ними MySQL, ClickHouse и wg0) и исходящий 25 — первый порт, из-за которого приходит abuse от хостера;
  • держит access_log выключенным: он пишет каждое соединение с адресом назначения независимо от loglevel, то есть в journald оседала бы история всего, что открывали клиенты.

Роль подключена в site.yml после firewall и под when: xray_managed | default(false) — обычный деплой её не трогает.

Значений в git нет: uuid клиентов и ключи Reality живут в гитигнорных host_vars и в vault, в all.vault.yml.example — только имена переменных.

Хук против ключ-строк

gitleaks ловит секреты по форме токенов известных сервисов, а uuid клиента и base64-ключ кривой на них не похожи — хотя uuid и есть пароль к inbound, а приватный x25519 открывает его целиком. Новый хук no-key-material держит эту границу.

Короткие hex-строки (shortId и подобное) сознательно не ловятся: от короткого git-хеша их не отличить, а ложные срабатывания на каждом коммите стоят дороже — без uuid и ключа такая строка сама по себе ничего не открывает. Разрешить конкретную строку — комментарий allow-secret в ней же.

Разбор добавленных строк индекса вынесен из check-staged-ips.py в общий staged_diff.py — без изменения поведения. 100.64.0.0/10 внесён в allowed-public-ips.txt: он стоит в xray_blocked_cidrs, а ipaddress.is_private приватным его не считает.

Проверка

Прогнано локально, без деплоя:

  • ansible-lint roles/xray — 0 failures, 0 warnings, профиль production. Полный прогон по репозиторию находок в отслеживаемых файлах не даёт (всё, что он сообщает, — в гитигнорных host_vars / inventory / apps.yml, которых в CI нет).
  • config.json.j2 отрендерен в двух конфигурациях и распарсен как JSON: Reality-вариант даёт serverNames списком, а не посимвольно; relay-вариант — правило relay→direct первым (иначе 10/8 ушла бы в blackhole) и без sniffing, который подменил бы адрес назначения на имя из SNI и сломал реле.
  • Новый хук на синтетических строках: ловит uuid и x25519-ключ в обоих base64 (43 символа urlsafe, 44 со знаком =), уважает allow-secret, пропускает короткий hex и git-хеши. На реальном индексе оба хука зелёные.

xray_archive_sha256 помечен # gitleaks:allow: gitleaks принимает сумму публичного релизного архива за jfrog-токен. Маркер на строке, а не fingerprint в .gitleaksignore, — CI сканирует всю историю с fetch-depth: 0, и запись пришлось бы заводить дважды и переписывать после каждого rebase.

Роль ни на одном хосте не прогонялась.

Inbound VLESS+Reality и отдельный режим реле (dokodemo-door), переливающий
TCP на дальний хост по мешу: клиент стучится на ближний хост, рукопожатие
Reality терминируется на дальнем. Версия и sha256 архива пинятся, конфиг
собирается словарём и проверяется `xray run -test` до рестарта, порт inbound'а
сверяется с firewall хоста — на default-drop закрытый порт снаружи
неотличим от блокировки у провайдера.

Значений в git нет: uuid клиентов и ключи Reality живут в гитигнорных
host_vars и в vault, в примере — только имена переменных.

Чтобы так и осталось, добавлен pre-commit хук no-key-material: gitleaks ловит
секреты по форме токенов известных сервисов, а uuid клиента и base64-ключ
кривой на них не похожи, хотя uuid и есть пароль к inbound. Разбор добавленных
строк индекса вынесен из check-staged-ips в общий staged_diff. 100.64.0.0/10
внесён в allowed-public-ips: он стоит в xray_blocked_cidrs, а
ipaddress.is_private приватным его не считает.
Многострочные обоснования в defaults, tasks и шаблоне конфига дублировали
README роли — в defaults остался указатель на него, в коде по одной строке
там, где значение иначе выглядит произвольным или где отсутствие чего-то
(sniffing на реле, порядок правил) легко «починить» обратно.

В README роли переехало то, чего там не было: параметры реле, access_log,
reality_enabled и причина UseIPv4 — иначе эти обоснования пропали бы вместе
с комментариями.

Корневой README актуализирован: роли xray не было ни в дереве, ни в таблице
ролей, а в разделе про безопасность — нового хука и общего staged_diff.
@gistrec
gistrec merged commit 70580da into main Oct 3, 2026
3 checks passed
@gistrec
gistrec deleted the xray-role branch October 3, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant