- Rust 82.8%
- Python 9.9%
- Nix 6.2%
- C 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| ci | ||
| crates | ||
| docs/superpowers | ||
| examples | ||
| modules/shluz | ||
| tests/perapp | ||
| .envrc | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| project.md | ||
| README.md | ||
| shell.nix | ||
shluz
shluz — демон маршрутизации трафика отдельных приложений через разные VPN-прокси на NixOS.
Он объединяет подписки, серверы, правила маршрутизации и приложения в слоты. Каждый слот получает свой endpoint, routing profile и способы доступа: автоматическую per-app маршрутизацию, SOCKS5 и/или HTTP proxy.
Warning
Проект находится в ранней стадии разработки (
0.1.x). Per-app режим изменяет cgroup v2, nftables и policy routing, поэтому демон запускается с расширенными Linux capabilities.
Возможности
- маршрутизация приложений по имени процесса через cgroup v2 + nftables + TPROXY;
- единая генерируемая конфигурация Xray для всех слотов;
- VLESS endpoints, включая TCP/TLS/Reality и XHTTP-варианты;
- HTTP-подписки и импорт
happ://; - routing profiles с правилами по domain, IP, GeoSite и GeoIP;
- отдельные SOCKS5/HTTP inbounds для слотов;
- CLI с JSON-выводом и интерактивный TUI;
- периодическое обновление подписок и проверка доступности серверов;
- атомарное сохранение состояния;
- JSON overlays для точечной настройки сгенерированной конфигурации Xray.
Как это работает
application process
│
▼
cgroup slot-<UUID>
│
▼
nftables + TPROXY ──► Xray inbound ──► routing profile ──► VPN endpoint
- В подписку импортируются VPN-серверы и правила маршрутизации.
- Слот связывает endpoint, routing profile и доступные inbounds.
- Приложение привязывается к слоту по имени процесса.
shluzdотслеживает новые процессы черезcn_proc, помещает совпавшие процессы в cgroup слота и перенаправляет их трафик в Xray.- CLI и TUI управляют состоянием демона через Unix socket
/run/shluz.sock.
Per-app слой можно отключить и использовать shluz только как менеджер SOCKS5/HTTP-прокси.
Скачать Linux x86_64 без Nix
Откройте публичные Releases shluz-bin
и выберите build-<полный source SHA> после успешного job binary-flake.
Скачайте оба release assets: shluz-linux-x86_64.tar.gz и SHA256SUMS.
Вход, PAT и netrc не нужны. Автоматические архивы исходников — не готовые бинарники.
Эти durable assets доступны для trusted main, v* и manual-main без срока
Actions retention. Исходные releases v* и временные Actions artifacts остаются
в исходном репозитории;
наличие Actions artifact до smoke само по себе не доказывает успех — проверяйте job.
$ sha256sum --check --strict SHA256SUMS
$ tar -xzf shluz-linux-x86_64.tar.gz
$ cd shluz-linux-x86_64
$ sha256sum --check --strict SHA256SUMS
$ cat BUILD.json
$ ./shluzd --help
$ ./shluz --help
$ ./shluz-tui --help
Архив содержит три статических ELF (shluzd, shluz, shluz-tui), этот README,
BUILD.json (полный commit самостоятельного репозитория, Rust/Cargo, target,
SHA256 Cargo.lock, время исходного commit) и контрольные суммы содержимого.
Target — x86_64-unknown-linux-musl; Nix и системная glibc для этих ELF не нужны.
SHA256 обнаруживает повреждение, не заменяет подпись/доверенный канал: сверяйте
commit с доверенным тегом. Релизы не подписываются. Используйте публичный HTTPS
с проверкой TLS; не запускайте скачанное через curl | sh.
Пакет не устанавливает сервисы, capabilities
или firewall rules и ничего не активирует автоматически.
Запуск и внешние зависимости
Xray Core устанавливается отдельно из доверенного источника; для per-app также
нужны ip (iproute2), nft (nftables), cgroup v2 и поддержка kernel TPROXY/cn_proc.
GeoIP/GeoSite assets при необходимости настраиваются отдельно для Xray. Бинарники
не включают Xray или его данные. Значения helper paths по умолчанию остаются NixOS
(/run/current-system/sw/bin/...): на другом Linux явно передавайте пути
--xray-binary, --ip-binary, --nft-binary, проверив их владельца и права записи.
CLI/TUI подключаются к уже запущенному доверенному демону:
$ ./shluz --socket /путь/к/shluz.sock status
$ ./shluz-tui --socket /путь/к/shluz.sock
Для самостоятельного ознакомления допустим только отдельный непривилегированный
proxy-only экземпляр с новым приватным каталогом состояния, без переноса
production registry. Пример ниже запускается вручную, без sudo, после отдельной
установки Xray и проверки helper paths (замените /usr/bin//usr/sbin на свои):
$ mkdir -m 700 "$HOME/shluz-demo"
$ ./shluzd --no-perapp --socket "$HOME/shluz-demo/shluz.sock" \
--config "$HOME/shluz-demo/config.json" --xray-config "$HOME/shluz-demo/xray.json" \
--xray-binary /usr/bin/xray --ip-binary /usr/sbin/ip --nft-binary /usr/sbin/nft
Не запускайте shluzd без --no-perapp ради проверки скачивания: per-app по умолчанию
включён и изменяет сетевые ресурсы. Привилегированный production-запуск требует
отдельной настройки и проверки; архив не даёт разрешения на rollout. Socket и
состояние доступны только доверенным локальным пользователям (см. trust boundary
ниже). Вне NixOS готовый systemd unit не устанавливается.
Что проверяет CI и как долго доступны файлы
Workflow запускается только на push main, push тегов v* и workflow_dispatch,
не на PR; ручной запуск выполняет jobs только на main. Docker jobs изолированы, без Docker socket, KVM и привилегированных mounts.
Hard gates: cargo test --locked --workspace,
cargo clippy --locked --workspace --all-targets (существующие warnings допустимы),
unit-тесты упаковки/publisher, release build, отсутствие ELF interpreter/NEEDED/RPATH
и /nix/store/, SHA256, запуск всех трёх --help из распакованного архива в новом
минимальном Debian+Node без Rust/Nix/Xray. Это не тест networking, daemon startup,
TUI interaction или egress. Отдельный VM gate с KVM по-прежнему обязателен:
nix build .#checks.x86_64-linux.perapp-integration; обычный CI его не выполняет.
cargo fmt --all -- --check — явно advisory, пока сохраняется унаследованный
workspace formatting debt: failing step имеет предупреждение и отдельный артефакт
formatting-report-…. Зелёный workflow не означает fmt-clean. CI не скрывает
новые отклонения: полный fmt diff доступен для review, но baseline новых нарушений
автоматически не отделяется от старого долга.
Artifacts (включая fmt report и кандидатов тегов) запрашивают retention 14 дней;
серверная политика/удаление запуска может сократить доступность. Скачайте нужные
файлы заранее. Release assets не имеют этого CI retention, но администратор может
удалить release/репозиторий. В исходном репозитории только явный push нового v*
после hard gates создаёт release: draft → два assets → publish. Ручной запуск на
теге release не публикует. Отдельный post-build+smoke job автоматически публикует
binary flake в публичный forgejo-admin/shluz-bin для push main/v* и manual-main
(источник должен принадлежать полной истории main).
Повторный запуск при существующем release, даже draft, завершается ошибкой, никогда
не удаляет/перезаписывает assets и не двигает тег. Частичный сбой оставляет draft для
отдельного ручного разбора; автоматического resume/cleanup нет. Для новой сборки
используйте новый тег. Это политика publisher, не серверная WORM-immutability:
Forgejo 16 API не запрещает администратору последующие изменения. SHA тега проверяется
до draft и перед publish, но API не имеет атомарного compare-and-publish: защита тегов
от параллельного перемещения/удаления должна быть настроена владельцем репозитория.
Для исходного v* publisher политика в этом исправлении не менялась: существующий
Git-тег + draft подвержены преждевременной публикации при синхронизации тегов
Forgejo 16. Live acceptance исходного v* release пока не подтверждён.
Исходный release использует краткоживущий автоматический repo-scoped workflow token;
checkout — persist-credentials: false. Только последний шаг отдельного
binary-flake job получает secret SHLUZ_BIN_TOKEN: PAT со scope write:repository,
ограниченный только forgejo-admin/shluz-bin через Forgejo 16
CreateAccessTokenOption.repositories. Он не передаётся build/smoke или third-party
Actions, не пишется в remote URL/git config; временный GIT_ASKPASS читает его из env
и удаляется. Binary API/Git и новые archive URLs закреплены за
https://fjo.osds.digital, независимо от старого internal FORGEJO_API_URL;
исходный release API также использует этот HTTPS origin. Проверка TLS включена,
redirects запрещены, токен не логируется и не отправляется на legacy HTTP origin.
Target должен быть PUBLIC, начальный bootstrap main — только README.md, Actions
выключены (нет рекурсивного CI). Repo/token/secret/защиту refs создаёт владелец,
не workflow; никакого admin PAT в CI.
Forgejo 16 выдаёт trusted jobs автоматический repository-write token; GitHub-поле
permissions не обеспечивает здесь гранулярного read-only/release-only scope и
намеренно не указано. Разделение jobs не понижает права автоматического build token.
Доверяйте авторам workflow/веток/тегов: изменение самого workflow может запросить
секрет. Настройки защиты тегов/доступа остаются обязанностью владельца.
Упаковка воспроизводима для одинаковых бинарников, README и metadata (порядок, uid/gid, mode, tar time и gzip header фиксированы); полная bit-for-bit воспроизводимость компиляции не заявляется: toolchain/actions/container digests закреплены, apt packages не snapshot-pinned. Для локальной проверки из корня standalone репозитория:
$ nix develop --command python3 -m unittest discover -s ci -p 'test_*.py' -v
$ python3 ci/package.py pack target/x86_64-unknown-linux-musl/release dist
$ python3 ci/package.py validate dist "$(git rev-parse HEAD)"
Последние команды требуют уже собранных musl ELF, Rust/Cargo и readelf; dist
должен отсутствовать до pack. Локальный Nix dynamic build не является downloadable
пакетом и намеренно отвергается валидатором. Workflow использует standalone-root
paths: каталог packages/shluz экспортируется в Forgejo через subtree split.
Source flake и binary flake
forgejo-admin/shluz — исходники, Rust build, devShell и VM checks.
forgejo-admin/shluz-bin — только проверенные prebuilt x86_64-linux и тот же
NixOS-модуль (заменён только package builder). Он экспортирует
packages.x86_64-linux.default/shluz, apps всех трёх бинарников и
nixosModules.default/shluz. Xray/ip/nft остаются зависимостями модуля.
В binary repo каждый source SHA получает immutable tag/release build-<полный-SHA>;
тег указывает на соответствующий binary-flake commit. Git хранит flake, точную копию
source flake.lock, модуль и metadata/release.json (source SHA/date/version,
archive URL/flat SHA256), не бинарные blobs. Archive/SHA256SUMS — durable release
assets. Транзакция: candidate commit в non-tag refs/shluz/staging/<полный-SHA> →
draft с будущим build-тегом и target_commitish=candidate → upload → проверка обоих
assets через прямые HTTPS /attachments/<проверенный-uuid> → publish создаёт Git-тег →
проверка точного tag commit, опубликованного release и анонимного download/hash
по обычному planned release URL → fetch нового тега → non-force fast-forward main.
До publish build Git-тега нет: иначе Forgejo 16 tag sync может снять draft сам.
Staging ref резервируется только create-only CAS (единственный узкий empty-expected
--force-with-lease); существующий staging не перезаписывается, refs сохраняются,
это не consumer branches/tags. Для main и build-тегов force/lease не используется.
До успешной анонимной проверки прежний main не меняется; неожиданно private repo,
пропавший/перемещённый тег, изменённый draft или плохой публичный архив блокируют продвижение.
Опубликованный идентичный build допускает проверенный no-op/recovery; orphan staging,
draft/чужой тег/иной hash требуют ручного разбора, без overwrite/delete/resume.
Stale/divergent source
не откатывает main, конфликт main оставляет опубликованный pin доступным по тегу.
Для binary flake замените URL в примере ниже на
git+https://fjo.osds.digital/forgejo-admin/shluz-bin.git?ref=main (или
?ref=build-<полный-SHA>). Git и release assets доступны анонимно: consumer PAT,
netrc и login не нужны. builtins.fetchurl проверяет фиксированный flat hash.
CI write secret остаётся приватным и repo-scoped, потребителям он не передаётся.
Подробнее — в ci/binary-flake/README.md.
Старые commits/tags/releases/manifests не переписываются и могут содержать legacy
HTTP URL. Для миграции обновите consumer lock до нового main или нового
build-<полный-SHA>, опубликованного по HTTPS. Старый pin сам не изменит archive URL.
Проверенный legacy checkpoint допускается только для локальной сверки истории;
новая сборка создаёт новый matching flake commit/build tag/release, затем продвигает
main без force. Исторические lock/module/templates сверяются с исходным старым commit.
Новая публикация не обновляет consumer input lock и не активирует хост:
требуются явные nix flake update shluz и nixos-rebuild switch. VM/dataplane gate
не заменяется бинарным --help smoke.
Установка на NixOS
Добавьте исходный репозиторий как flake input (binary URL описан выше):
{
inputs.shluz = {
url = "git+https://fjo.osds.digital/forgejo-admin/shluz.git";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, shluz, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
shluz.nixosModules.default
({ ... }: {
programs.shluz.enable = true;
# Разрешить пользователю подключаться к Unix socket демона.
users.users.my-user.extraGroups = [ "shluz" ];
})
];
};
};
}
HTTPS URL исходного репозитория не меняет его политику доступа; миграция publisher не меняет visibility/permissions исходников. Публичный binary input описан выше.
Примените конфигурацию и проверьте сервис:
$ sudo nixos-rebuild switch --flake .#my-host
$ systemctl status shluzd
$ shluz status
Опции NixOS-модуля
| Опция | По умолчанию | Назначение |
|---|---|---|
programs.shluz.enable |
false |
Включить пакет и shluzd.service |
programs.shluz.package |
пакет из репозитория | Используемая сборка shluz |
programs.shluz.socketPath |
/run/shluz.sock |
Unix socket CLI/TUI |
programs.shluz.userGroup |
shluz |
Группа с доступом к socket |
programs.shluz.xrayPackage |
pkgs.xray |
Пакет Xray Core |
programs.shluz.enablePerApp |
true |
cgroup/nftables per-app слой; добавляет CAP_DAC_OVERRIDE |
programs.shluz.cgroupRoot |
/sys/fs/cgroup/shluz.slice |
Отдельное дерево приложений, не внутри service |
programs.shluz.perAppTableBase |
10000 |
Начало диапазона IPv4 routing tables |
programs.shluz.perAppRulePriority |
5000 |
Приоритет IPv4 fwmark rules перед tailnet |
Для proxy-only режима:
programs.shluz = {
enable = true;
enablePerApp = false;
};
Использование
TUI
$ shluz-tui
Основные клавиши:
1,2,3— Apps, Subscriptions и Slots;Tab/Shift+Tab— переключение вкладок;- стрелки или
hjkl— навигация; Enter— открыть действие или редактор;q— выход.
Через TUI можно импортировать подписки, создавать слоты, выбирать endpoint и routing profile, привязывать запущенные приложения, настраивать proxy-порты и редактировать Xray overlays.
CLI
# Состояние движка
$ shluz status
# Просмотр сущностей
$ shluz list subscriptions
$ shluz list servers
$ shluz list slots
$ shluz list apps
$ shluz list routing-profiles
# Добавление и обновление подписки
$ shluz subscription add 'https://example.org/subscription' --name primary
$ shluz subscription refresh primary
# Проверка одного или всех серверов
$ shluz ping server-name
$ shluz ping
# Управление Xray
$ shluz engine-reload
$ shluz engine-restart
# Машиночитаемый вывод
$ shluz --json list slots
Путь к socket можно переопределить глобальным параметром:
$ shluz --socket /tmp/shluz.sock status
$ shluz-tui --socket /tmp/shluz.sock
Xray overrides
Слот может содержать JSON overlay, который накладывается на сгенерированную конфигурацию Xray. Обычные JSON-объекты рекурсивно объединяются, а специальные директивы управляют заменой значений и массивами:
{
"routing": {
"rules": {
"#add-before": [
{
"type": "field",
"domain": ["domain:internal.example"],
"outboundTag": "$proxy"
}
]
}
}
}
Поддерживаемые директивы:
{"#replace": value}— полностью заменить значение;{"#add-before": [...]}— добавить элементы в начало массива;{"#add-after": [...]}— добавить элементы в конец массива.
$proxy внутри overlay заменяется на outbound выбранного сервером слота. Полный пример для private DNS находится в examples/wpn-private-dns.override.json.
$ shluz slot override-set slot-name examples/wpn-private-dns.override.json
$ shluz slot override-clear slot-name
Некорректный overlay не заменяет рабочую конфигурацию: демон сохраняет предыдущий валидный runtime-вариант и показывает ошибку в TUI.
Промежуточный reliability checkpoint (шаги 1–4)
Эта версия не готова к live rollout. Стабильный RuntimePlan, durable registry
и единый engine/network coordinator, дерево процессов и durable origin/detach
реализованы вместе с ACK-проверкой watcher и polling/loss recovery. Отдельный
runtime-health RPC/CLI/TUI реализован в шаге6; полная E2E-матрица остаётся шагом7 плана.
Наличие configured binding или PID в cgroup не доказывает egress.
Ресурсы назначаются по SlotId/UUID, не по позиции слота. Удалённые allocations
остаются tombstones до следующего boot; максимум — 255 boot-local allocations,
включая отклонённые candidates. Standalone параметры: --cgroup-root,
--perapp-table-base, --perapp-rule-priority. Mark mask — 0xff00, остальные
биты сохраняются; tables по умолчанию от 10000, rule priority 5000.
Весь диапазон TPROXY-портов 30000–30254 включительно зарезервирован только
при включённом per-app: proxy-порт в этом диапазоне отклоняется без перенумерации.
Проверяется и финальный overlay: только точный planned TPROXY inbound может
занять такой порт, включая unused/retiring/tombstoned номера; string ranges тоже
проверяются, динамические/непроверяемые port values отклоняются в per-app mode.
Proxy-only не создаёт allocations, cgroups или TPROXY inbounds и не резервирует
этот диапазон. Пользовательский config.json не переписывается ради fallback.
При per-app startup используется только валидированный last-known-good state с соответствующим планом. Если его нет, invalid overlay оставляет runtime недоступным/pending — политика не удаляется для выдуманного direct fallback. Независимый proxy-only сохраняет прежний override-free startup fallback. Manual reload/restart, startup и mutation retries проходят через coordinator; app-only rebind не перезапускает Xray при неизменном engine config.
Это не атомарная транзакция: prepare не перемещает PID; candidate Xray и union old+candidate interception проверяются до durable commit. Retirement журналируется отдельно после commit. Precommit failure пытается вернуть старый engine/network; неудачный rollback оставляет unavailable и останавливает candidate Xray. Postcommit retirement failure — degraded новая generation с inventory для retry, не «успешный rollback». После readiness/commit выполняется observed membership diff; пока retiring cgroup содержит процессы, interception сохраняется для recovery/retry. Attach/detach errors также не являются rollback.
Registry и boot marker — private файлы рядом с config.json; registry содержит
LKG, который может включать приватную конфигурацию: не публикуйте его в диагностике.
Intent сохраняется до kernel write, ownership — после подтверждения. Idle same-boot
crash восстанавливается; interrupted write или ошибка persistence сохраняет evidence
и требует recovery, без угадывания/усыновления/удаления ресурсов по одному tuple.
Fresh slot mkdir, route add и nft create не заменяют появившийся чужой ресурс;
неоднозначный duplicate rule сохраняет журнал. Это не защита от произвольных
параллельных действий другого привилегированного администратора в owned namespace.
Не удаляйте registry/boot marker для «починки» и не очищайте table 200.
При recovery-required сохраните приватные файлы и read-only inventory для отдельного
согласованного recovery. Повреждённый registry в enabled mode блокирует startup;
ошибка runtime persistence оставляет диагностический IPC, но замораживает переходы.
Явный disable после crash читает только существующий registry и очищает однозначно owned same-boot interception с persisted namespace и текущими trusted ip/nft paths. Неоднозначный journal/foreign interception не выдаётся за disabled-ready. Старый boot не разрешает удалять совпавший чужой ip tuple; изолированное чужое правило без owned cleanup само по себе не блокирует обычный proxy-only startup.
Перехват ограничен IPv4 TCP/UDP; IPv6 не маркируется. Это не kill switch:
старые сокеты не переключаются при attach, watcher не гарантирует первый сокет,
connected/unconnected UDP parity ещё не доказана. Явные stop/disable — fail-open
после восстановления origins и удаления owned interception. Приложения остаются
вне service cgroup демона. Unbind/rebind/stop/disable используют один diff actual
membership: (boot_id, TGID, start_time), origin path + inode сохраняются до записи
cgroup.procs. Inherited descendants получают origin от проверенного родителя;
после reparenting сохраняется исходная binding identity. Ушедший во внешнюю cgroup
PID не перетягивается обратно; исчезнувший/recreated origin — recovery-required,
без fallback в root и без убийства процесса. После exit запись очищается при сверке.
Общий snapshot/resolver выбирает direct binding, затем ближайшего ancestor,
не пересекает UID/session boundary и исключает root/system/infrastructure процессы.
Namespace-проверка — host-compatible metadata, не доказательство тождества:
production caps не позволяют читать чужие namespace symlinks (ptrace gate).
CAP_SYS_PTRACE и privileged helper намеренно не добавлены. Собственный initial-host
baseline проверяется по initial PID/user inode и identity uid/gid maps; у кандидата
нужны согласованные single-level NSpid/NStgid, matching parsed uid_map и gid_map.
Читаемый inode mismatch всегда исключает процесс, EACCES допускает только этот
консервативный metadata fallback; missing/malformed данные исключаются с counters
и причиной в диагностике. Обычные isolated containers исключаются, но hostile
host-root/privileged identity-mapped/shared-PID namespaces не являются обещанной
границей защиты. Остаётся финальная numeric-TGID race: у cgroup.procs нет pidfd API.
Watcher подписывается на connector (1,1) до initial scan. Успех send не означает
подписку: требуется собственный PROC_EVENT_NONE ACK с случайным token ack+1
(kernel send_msg перезаписывает seq). Ошибки ACK/frames, truncation, ENOBUFS,
разрыв и переполнение очереди переводят transport в polling; retry — 250ms..30s.
События используют TGID, а не TID; event/periodic reconciliation используют один
resolver и только ready applied plan. Очередь 256 событий coalesces scans не чаще
250ms; periodic scan каждые 5s закрывает пропуски, в том числе без cn_proc.
Снимок ограничен 65536 TGID и проверяемым между процессами бюджетом 2s: частичный
снимок никогда не применяется. Это не deadline для блокирующего syscall или
journaled migration; на остановке уже начатые операции дренируются, не abort.
Transport health (starting/subscribed/polling/stopped), error/counters и pending
rescan теперь доступны в shluz status, shluz --json status и TUI.
subscribed означает только собственный ACK, не healthy dataplane или успешный attach.
Old-socket/first-socket ограничения сохраняются.
Unit использует KillMode=mixed, TimeoutStopSec=60s: TERM получает coordinator,
он прекращает приём работы, до 45s дренирует операции и очищает runtime; helpers
имеют 15s budget (включая stdin), затем final systemd KILL охватывает service cgroup.
Не используются KillMode=none/process. Доступ к socket — только trusted local users.
VM gate: nix build .#checks.x86_64-linux.perapp-integration, production unit,
реальный Xray, disposable VMs без host network/cgroup mutation. Step7 fixture
проверяет серверный source IP + уникальный payload + nft counters, а не только
принадлежность PID: два независимых Xray SOCKS endpoint процесса выходят с разных
адресов. Root/helper/grandchild сами создают TCP, connected UDP и sendto sockets;
1200-byte QUIC-like datagram проверяет UDP transport, не QUIC handshake.
Проверены existing-tree attach, late/nonleader exec, rebind A→B, unbind, удаление A,
stop/restart/crash, IPv6 direct, rollback и durable recovery. Existing TCP stream
после rebind остался на A, новый пошёл на B; TCP/UDP sockets, созданные до первого
attach, в fixture вышли direct. Это измерения конкретного kernel/Xray, не обещание
миграции старых сокетов, отсутствия first-exec race или непрерывности Xray streams.
Coexistence fixture — не actual urix: отдельный реальный TPROXY Xray с mark2, table200 и disjoint sibling sockets, плюс непустая table52. Оба порядка запуска сохранили чужие правила/routes/PID и egress. При одинаковых A/B endpoints/domain rule HTTP Host sniffing дал B в shluz и A в fixture с sniffing disabled. Actual urix и полноценные TLS/QUIC sniffing/handshake parity не прогонялись; это отдельные gates, не основание менять urix. IPv6 egress в fixture остаётся direct до/после rebind/stop; IPv6 firewall/sysctl не изменяются production-кодом.
Fault gates включают invalid overlay, validation/exec-spawn/immediate-exit failure, реальный rejected nft batch (предшествующий flush атомарно откатан), repeated batch, ip add error, registry rename EIO, corrupt/ambiguous/stale-boot registry и forced systemd final group-kill. Весь fault shim/wrappers живёт только в VM. One-shot errors могут автоматически восстановиться до следующего status: failed RPC и monotonic error counter остаются evidence, но старое degraded не должно сохраняться после успешной проверки. Проверка штатного stop подтверждает origin restoration и fail-open новых соединений; forced timeout сохраняет ambiguous journal и отказывает в guessed cleanup. Не удаляйте evidence вручную, не отключайте IPv6 ради видимости защиты. Полный rollout по-прежнему требует independent review и отдельного разрешения.
Per-app runtime health (шаг6)
GetPerAppStatus возвращает отдельный snapshot; PerAppStatusChanged содержит ту же
форму. Клиент сначала читает additive capability EngineStatus.perapp_status_supported:
старый daemon без этого поля — unknown/unsupported, не disabled/ready. Новый
event отправляется только subscribed connections, запросившим GetPerAppStatus;
старые RPC variants сохранены. Events обновляются после reconcile/manual actions и
каждые 2s; TUI дополнительно запрашивает snapshot и сбрасывает ready при ошибке/timeout.
Status-чтение сериализовано с runtime, не изменяет user config или membership.
Во время долгой операции возможен unknown по 3s timeout, не устаревший ready.
| State | Значение |
|---|---|
disabled |
Per-app выключен и требуемая явная cleanup завершена; не ошибка |
starting |
Первичное применение/disable ещё не закончено |
ready |
Есть активный committed plan, engine liveness/readiness проверены, нет известных ошибок membership/watcher и pending rescan |
degraded |
Есть durable generation, но candidate отклонён, runtime/retirement/membership недоступны либо watcher в polling/loss recovery |
failed |
Нет применённого поколения либо startup/disable recovery запрещает работу |
unknown |
Старый daemon, ошибка связи, timeout или недоступен snapshot |
applied_generation — последний durable commit, не desired generation и не
обещание работающего dataplane: при retirement/engine failure он остаётся видимым.
ready отражает последнее успешное применение, текущую engine-проверку и известные
ошибки, не независимый непрерывный аудит каждого nft rule/packet. Внешнее изменение
kernel resources требует reconcile; факт egress проверяется отдельно тестовым endpoint.
assignments — только известные journal (TGID, start_time, slot_id) с независимо
перечитанным membership (observed/not_present/unknown), origin-known/relinquished.
Это не весь /proc, не process command line и не origin path. Snapshot ограничен
4096 записями и проверяемым между чтениями бюджетом 200ms; assignments_complete=false
означает частичный список известных identities, не отсутствие остальных процессов.
Блокирующий syscall не имеет жёсткого 200ms deadline. observed_at датирует наблюдение.
Счётчики apply/scan errors, watcher losses/queue overflow и namespace exclusions
сохраняются в пределах процесса, не обнуляются при успешном retry.
Namespace exclusions суммирует наблюдения исключений в завершённых /proc snapshots
(повторно исключённый TGID учитывается при каждом scan), не число уникальных процессов;
текущие причины хранятся отдельно. Счётчик насыщается, а не переполняется.
JSON CLI additive: прежние top-level processes, last_reconcile, last_error
сохранены; добавлены perapp_status_supported и per_app. Скриптам следует игнорировать
неизвестные поля. Raw engine error в status заменён фиксированной категорией, поскольку
Xray stderr может содержать credentials/override; per-app errors тоже не содержат
VPN URI, command output или config. Категории показывают validation, cgroup/ip,
nft, engine readiness, membership/retirement, polling и recovery-required. Старые
config/override/log APIs остаются привилегированными и могут содержать секреты;
не публикуйте их вывод как безопасную диагностику.
TUI пишет configured → slot отдельно от observed journal members [actual slots].
Configured B при observed A после rejection — ожидаемое честное отображение, не успех
перепривязки. Pickers add/rebind предлагают только per-app слоты. Membership не
доказывает egress соединений; IPv6, старые сокеты и first-exec race не защищены.
При apply_failed исправьте desired override/resource conflict и повторите reload;
при membership_failed проверьте права/origin, не переносите PID молча в root.
recovery_required/corrupt registry требует сохранения private evidence и отдельного
разбора ownership: не удаляйте registry/lock/cgroups/rules для “починки”. Enabled
corrupt/config startup оставляет diagnostic IPC без engine/watcher/allocations/cleanup;
reload/restart отклоняются, config сохранён. Неизвестный applied generation остаётся null.
Live socket/instance lock не заменяется вторым daemon; foreign regular/symlink paths
не удаляются. Locks живут до выхода процесса, не unlink при работающем daemon.
Trust boundary: socket mode 0660, programs.shluz.userGroup — trusted local users,
не read-only мониторинг и не sandbox для недоверенных пользователей. Члены группы
могут менять routing/override и управлять root daemon; conditional CAP_DAC_OVERRIDE
усиливает последствия этого доверия. Не добавляйте посторонних пользователей в группу,
не открывайте remote API/socket proxy, не передавайте группе write-доступ к runtime
registry/lock directories. Эта серия не добавляет capabilities или удалённый API.
Сборка и разработка
Требования: Linux, Rust toolchain, OpenSSL и pkg-config. Для полного per-app режима также нужны cgroup v2, nftables, iproute2 и Xray Core.
$ nix develop
$ cargo build --workspace
$ cargo test --workspace
$ cargo fmt --all -- --check
$ cargo clippy --workspace --all-targets
Сборка пакета через flake:
$ nix build
$ ./result/bin/shluz --help
Структура workspace
| Crate | Назначение |
|---|---|
shluz-core |
Доменная модель: slots, apps, subscriptions и routing profiles |
shluz-protocol |
JSON-Lines RPC и клиент Unix socket |
shluz-engine |
Абстракция VPN-движка и общие runtime-типы |
shluz-xray-engine |
Генерация конфигурации и управление Xray |
shluz-json-overlay |
Детерминированные рекурсивные JSON overlays |
shluzd |
Демон, persistence, subscriptions и per-app routing |
shluz |
Командный клиент |
shluz-tui |
Интерактивный терминальный интерфейс |
Runtime-файлы
| Путь | Назначение |
|---|---|
/run/shluz.sock |
Unix socket демона |
/run/shluz/xray.json |
Сгенерированная конфигурация Xray |
/var/lib/shluz/config.json |
Пользовательское desired state |
/var/lib/shluz/runtime-registry.json |
Private allocations, applied LKG и ownership journal |
/var/lib/shluz/runtime-registry.boot / .lock |
Boot marker и single-writer lock |
/var/lib/shluz/config.instance-lock, /run/shluz.socket-lock |
Lifetime instance/socket exclusivity (не удалять вручную) |
/sys/fs/cgroup/shluz.slice/ |
cgroups слотов в per-app режиме |