# TODO: правки и инварианты Источник: `docs/arch/invariants.md`. Ответы владельца от 2026-10-05 учтены. Подтверждённые факты зафиксированы в `AGENTS.md` (корень) и `docs/arch/map.md`; этот файл — только **незакрытые правки и неотвеченные вопросы**. Порядок: A → B → C → D, потом E (документация для агента). Обозначения: `[ ]` не начато, `[x]` сделано, `[!]` блокирует остальное. --- ## Подтверждено (зафиксировано в `AGENTS.md` / `map.md`) Эти инварианты уже учтены в ядре и карте — при правке кода опираться на зафиксированные формулировки. - **6.1** Явная финальная политика nftables на VDS → `todo A3` ещё открыто, но сам «надо запилить» закреплён. - **6.3** Firewall на sapphira выключен намеренно (граница — роутер, 5 портов: 22, 80, 443, 8443, 22000) → формулировка в `AGENTS.md §3`, `todo D1`. - **6.5** `100.64.0.0` = Tailscale-адрес sapphira (назначен вручную) → `AGENTS.md §5`, `map.md §Сеть и firewall`. - **7.1 / 7.2** 3x-ui заморожен: панель на latest, ядро Xray на 26.7.x, миграция 26.9 провалена → `AGENTS.md §4`, `todo C1–C5`. --- ## Ловушки для агента: выглядит сломанным, но это намеренно Прежде чем чинить — проверить этот список. Здесь лежат решения, которые иначе «поправляются» обратно и ломают рабочую систему. | Где | Что выглядит ошибкой | На самом деле | |---|---|---| | `configurations/server.nix:130` | `networking.firewall.enable = false` на сервере с 20 сервисами на `0.0.0.0` | Намеренно: фильтр на роутере, он пробрасывает 5 портов (см. D1) | | `configurations/mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 против 26.05 у остальных | Каждый хост зафиксирован на своей версии; не «подровнять» | | `modules/users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` с пометкой TODO | Осознанный костыль под старый uid 1000 = `yuyus`; удалять только после миграции ФС | | `modules/containers/3x-ui.nix:54` | `image = …:latest` | Панель намеренно на последней версии; **ядро** Xray — на 26.7.x, миграция на 26.9 провалена | | `modules/containers/3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS при откате nginx-stream | Следствие отката `c8d4a12`; смысл утрачен, но опция объявлена — см. C5 | | `modules/server/default.nix:33-47` | 15 закомментированных модулей с живым кодом | Отключены осознанно; см. E3 | | `modules/server/{mealie,memos,n8n,netdata,nfs,open-webui,rsync,step-ca,transmission,trilium,zerotier}.nix` | Агент насчитает лишние порты и каталоги | Модули вне `imports` = мёртвый код | | `home/modules/opencode.nix:339` | `systemd.user.services.opencode-web.Service` вместо привычного `serviceConfig` | `serviceConfig` рендерится в секцию `[serviceConfig]`, которую systemd **молча игнорирует** (`c73a698`) | | `configurations/vds.nix:73-91` | nftables без финального правила | Известный пробел,см. A3 — **не** «случайно потерялось» | | `100.64.0.0` в `nginx.nix`, `nextcloud.nix`, `vds/*` | Первый адрес CGNAT `/10`, похож на сетевой | Tailscale-адрес sapphira, назначен вручную | | `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. B1 | --- ## A. Блокеры: сломано или не защищено ### [ ] A1. `mobile.nix` импортирует несуществующий файл **Где:** `configurations/mobile.nix:12` ```nix xlib = import ../lib/xlib.nix { lib = inputs.nixpkgs.lib; }; ``` Файла `lib/xlib.nix` нет — есть каталог `lib/xlib/` с `default.nix`. **Правка** (как в `configurations/default.nix:5`): ```nix xlib = import ../lib/xlib { inherit lib; }; ``` **Следствие:** до правки `nixOnDroidConfigurations.epral` и `.default` не вычисляются. Устройство `epral` мертво. **Проверка:** ``` nix eval --raw .#nixOnDroidConfigurations.epral.config.environment.etcBackupExtension # ожидается .bak ``` ### [ ] A2. Убедиться, что `nix flake check` вообще запускается **Где:** нет CI; `checks` в `deploy/default.nix:27-29` покрывают только deploy. **Сначала проверить**, ловит ли текущий `nix flake check` поломку из A1: ``` nix flake check ``` Ожидание, которое надо подтвердить: он **уже падает** на `epral`, то есть проверка существует, но её не запускали. Если падает — A1 и был бы замечен. **Проверка после A1:** та же команда должна стать зелёной. **Затем** (E2) — превратить в привычку: прогонять перед каждым коммитом. ### [ ] A3. Явная финальная политика nftables на VDS **Где:** `configurations/vds.nix:73-91` **Сначала диагностика на otreca** (без неё править опасно — можно отрезать SSH): ``` nft list ruleset systemctl status nftables firewall-nftables ``` Нужно понять, кто реально владеет набором правил: `nftables.enable = true` с собственным ruleset **и** `networking.firewall.*` включены одновременно (инвариант 6.2). Затем — править **один** механизм, не оба. **Что должно получиться** (политика — на выбор владельца, два варианта): ``` # Вариант «белый список» (предпочтительно): chain input { type filter hook input priority 0; policy drop; iif lo accept ct state established,related accept iif "tailscale0" accept tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets accept tcp dport { 22 } ct state new accept # только если 22 нужен на ens3 } # Вариант «мягкий» (минимум изменений, фиксирует текущее поведение): chain input { type filter hook input priority 0; iif lo accept ct state established,related accept tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets accept tcp dport { 80, 443 } ct state new drop # финал accept — но ТОЛЬКО как явно помеченное «разрешено всё остальное»: iif "ens3" accept comment "PROVISIONAL: explicit allow-all, см. A3" } ``` **Инвариант к записи:** последнее правило самописной цепочки всегда явное. **Проверка:** `nft list chain inet filter input` + `ssh` с внешнего адреса. --- ## B. Защита данных ### [ ] B1. Guard на несмонтированный носитель `/mnt/services` **Где:** `lib/xlib/helpers.nix` (`mkServiceStorage`), потребители — `modules/server/{postgresql,n8n,samba,homebox,minecraft}.nix` + `modules/containers/3x-ui.nix` **Проблема (подтверждена владельцем как не продуманная):** `mkServiceStorage` даёт `bind,x-systemd.automount,nofail`. Если диск `External` (`xlib.dirs.server-home`, ext4 по UUID, `configurations/server.nix:55-58`) не смонтирован, то `/mnt/services` — обычный каталог, `/var/lib/` пуст, и сервис **молча** стартует на чистой базе. Пользователь увидит «потерялись данные». **Решение (рекомендую):** добавить в `xlib/helpers.nix` ```nix mkStorageGuard = { dir }: { # сервис не стартует, пока /mnt/services не смонтирован: # Requires+After на mnt-services.mount, который упадёт, если нет источника requiresMountsFor = [ dir ]; }; ``` и в каждом потребителе: ```nix systemd.services.postgresql = xlib.helpers.mkStorageGuard { dir = xlib.dirs.services-mnt-folder; }; ``` **Важно — не проверять `ConditionPathIsMountPoint=/mnt/services`:** bind-mount внутри одной ФС не меняет `st_dev`, условие вернёт false даже при корректном монтировании. Надёжны `requiresMountsFor` или `ConditionPathIsMountPoint` на `xlib.dirs.server-home` (там `st_dev` действительно другой). **Плюс операционная строка в `AGENTS.md`:** перед рестартом этих сервисов — `findmnt /mnt/services`. **Проверка (имитация отказа):** ``` systemctl stop postgresql sudo umount /mnt/services # или остановить automount systemctl start postgresql # ожидается FAIL, а не пустая база ``` ### [ ] B2. Зафиксировать, что бэкапов в конфигурации нет **Где:** `modules/server/postgresql.nix:23` (`postgresqlBackup.enable` закомментирован), бэкап-сервиса в репозитории нет вообще; БД 3x-ui — sqlite на том же диске. **Задача — не код, а запись:** в `AGENTS.md` и `invariants.md` явно сказать, что бэкапы ведутся вне Nix. Иначе агент считает конфиг самодостаточным. **Ждёт ответа:** где бэкапы и как их проверять (инвариант 5.6). --- ## C. 3x-ui: заморозить рабочее состояние ### [ ] C1. Вернуть расследование, потерянное при откате **Где:** 200 строк удалены коммитом `22a19be`. **Восстановить и дополнить выводом:** ``` git show 9974784:modules/containers/3x-ui-migration-notes.md > docs/arch/notes/3x-ui-xray-26.9.md ``` Дописать в конец: вердикт — миграция ядра 26.7 → 26.9 **провалена**, откат на рабочее состояние (панель последняя, ядро 26.7.x), обходные скрипты отключены осознанно; причина отказа — обязательный постквантовый обмен X25519MLKEM768, ломающий старых клиентов. **Инвариант:** откат кода не удаляет расследование; заметка живёт в `docs/arch/notes/`, а не рядом с откатываемым файлом. ### [ ] C2. Зафиксировать фактические версии панели и ядра **Где:** `modules/containers/3x-ui.nix:54` **Сначала узнать, что реально работает** (на sapphira и на otreca): ``` podman images --format '{{.Repository}}:{{.Tag}} {{.Id}} {{.Created}}' | grep 3x-ui podman inspect ghcr.io/mhsanaei/3x-ui --format '{{index .RepoDigests 0}}' podman exec 3xui_app /app/bin/xray-linux-amd64 version ``` **Потом** заменить `:latest` на найденный тег (или digest) в коде. **Инвариант:** образы контейнеров запинены; `latest` запрещён — обновление образа это правка в коде, а не `podman pull` на хосте. **Почему срочно:** `podman.autoPrune.flags = ["--all"]` (`3x-ui.nix:45-47`) + `:latest` = рабочее состояние может смениться без единого коммита. ### [ ] C3. Убрать сервис автообновления 3x-ui **Где:** `modules/containers/3x-ui.nix:80-90` (`podman-update-3xui_app` с `podman pull … :latest`) и закомментированный таймер (строка 97-103). **Предложение:** удалить сервис целиком, оставив комментарий-предупреждение. Обновление панели через `pull` — ровно тот путь, которым в 2026-10-04 декларация разошлась с рантаймом; автоматизировать его нельзя. **Инвариант:** ни один контейнер в этом репозитории не обновляется сам. ### [ ] C4. Записать в AGENTS.md, что ядро Xray — состояние панели, а не Nix Версия ядра выбирается в UI панели и лежит в её sqlite-БД, то есть **вне** Nix. Репозиторий не может её гарантировать. **Операционное правило:** перед деплоем/рестартом 3x-ui проверять версию ядра в панели; обновление ядра = отдельная задача с записью в `docs/arch/notes/`, а не молчаливый `podman pull`. ### [ ] C5. Решить судьбу `reality443Forwarding` **Где:** `modules/vds/default.nix:19` (`= true`), `modules/options.nix:66-75`, `modules/containers/3x-ui.nix:33-35`. Состояние после отката `c8d4a12`: опция включена, поэтому на otreca пробрасывается `127.0.0.1:15380:443`, тогда как единственный Reality-инбаунд контейнера слушает 8443, а публичный 8443 проброшен напрямую (`0.0.0.0:8443`). Потребителя потока (nginx-stream) откат убрал. **Варианты:** (а) оставить как есть и описать в инвариантах; (б) погасить опцию в `vds/default.nix` и убрать её из `options.nix`; (в) довести до рабочего состояния. **Ждёт решения** — связано с 6.9. --- ## D. Сетевая граница: записать то, чего нет в репозитории ### [ ] D1. Пробросы роутера — главный недостающий инвариант Ответ владельца: на сервер пробрасываются **443, 80, 22000 (syncthing), 8443 (xray), 22 (ssh)**. Это **настоящая граница доверия**, и она живёт в конфиге роутера, то есть вне репозитория. **Записать в двух местах:** `docs/arch/invariants.md` (слой 6) и `AGENTS.md`. Формулировка инварианта: > Экспозиция наружу определяется пробросами на роутере, не `openFirewall`. > На `sapphira` `networking.firewall.enable = false` намеренно. > Список пробросов: 22, 80, 443, 8443 (3x-ui/Xray REALITY), 22000 (syncthing). > Новый сервис не становится доступен из интернета, пока не добавлен проброс. > `networking.firewall.*` на `sapphira` не имеет эффекта. ### [ ] D2. Зафиксировать `100.64.0.0` как Tailscale-адрес sapphira Моё прежнее замечание («сеть вместо адреса») было неверным — адрес назначен вручную. Записать как факт + список из 4 мест, которые придётся править при смене: `modules/server/nginx.nix`, `modules/server/nextcloud.nix`, `modules/vds/systemd.nix`, `modules/vds/nginx.nix`. **Опционально (отложено):** вынести `192.168.1.20` в `xlib.dirs` — сейчас зашит в ~30 местах в 6 файлах. Не срочно, это рефакторинг. ### [ ] D3. Убрать мёртвое правило firewall **Где:** `modules/server/nginx.nix:225-228` — `allowedTCPPorts = [80 443]` не действует при `firewall.enable = false` (`server.nix:130`). Удалить или пометить комментарием «депенит от D1». --- ## E. Документация для агента (после прохода по invariants.md) ### [ ] E1. Написать `AGENTS.md` в корне Собирается из подтверждённых инвариантов. Структура: карта хостов → что где лежит → инварианты (нарушишь = сломает) → ловушки из таблицы выше → команды проверки. Ожидаемый бюджет — до 150 строк. ### [ ] E2. Выбрать проверки, которые заменят половину инвариантов Кандидаты из инварианта 11.2: 1. ни одного `:latest` в образах (grep по `image =`); 2. `nix flake check` зелёный — уже ловит A1; 3. домены в `coredns.nix` ↔ vhost'ы в `nginx.nix` совпадают в обе стороны; 4. для каждого потребителя `mkServiceStorage` каталог существует на `External`; 5. последнее правило самописной nftables-цепочки явное; 6. `listen.addr` — адрес интерфейса, а не сеть; 7. все файлы в `secrets/` матчат `path_regex` из `.sops.yaml`. **Ждёт ответа:** какие из них делать, какие — избыточны. ### [ ] E3. Судьба 15 закомментированных модулей `modules/server/default.nix:33-47` — `remnawave, coturn, mealie, memos, minecraft, n8n, netdata, nfs, open-webui, rsync, step-ca, stirling-pdf, transmission, trilium, zerotier`. Удалить или оставить как референс? Они мешают агенту насчитывать порты и каталоги, которых нет. --- ## F. Ждут ответа (блокируют E1) Индексы в `docs/arch/invariants.md`: | № | Вопрос, который блокирует запись инварианта | |---|---| | 2.2 | `vetymae` / `lamet` / `therima` / `soptur` — это те же машины или хосты вне репозитория? | | 2.5 | `stateVersion` дрейфует 24.05 / 24.11 / 25.05 / 26.05 — намеренно? | | 2.6 | Есть ли escape hatch для per-host отличий в `xlib`, или «у всех хостов одно» — закон? | | 3.2 | `any.nix` (minimal) действительно нуждается в home-manager + sops + disko? | | 4.1 | Как root получает доступ по SSH — `authorizedKeys` для root в коде нет | | 4.2 | Как разрешается цикл «ключ `/etc/ssh/id_ed25519` лежит внутри секрета, а нужен для расшифровки» | | 4.3 | Что лежит в `secrets/`, все ли файлы покрыты `path_regex` | | 4.4 | Как подключается вторая машина / второй человек при одном age-ключе | | 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя» | | 5.1 | `/mnt/services` в режиме 0777 — осознанно? | | 5.3 | NFS выключен, Samba работает — миграция? | | 5.4 | NTFS-том `lamet-drive` с `mask = "0000"` — что на нём лежит | | 5.5 | `therima` / `vetymae` / `soptur` — несуществующие остатки или сетевые шары | | 5.6 | Где бэкапы БД и 3x-ui (→ B2) | | 6.6 | `192.168.1.20` зашит в 30 мест — считаем константой? | | 6.7 | DNS дублирует инвентарь сервисов — как проверяем рассинхрон | | 6.8 | Публичные IP и SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна? | | 6.9 | Какой путь REALITY считается правильным (→ C5) | | 7.4 | Почему не публиковать весь диапазон 14380-15379 | | 8.2 | `lamet.opencodes` → `:6061` — это miniflux; ошибка или так задумано | | 8.4 | `onlyoffice` — работает после трёх регрессов? | | 8.5 | Что слушает `:3002` (`/whiteboard` в nextcloud) | | 9.2 | Кто создаёт `~/Music` и `~/Storage` при `createDirectories = false` | | 10.1 | Почему `deploy-rs` не деплоит `atoridu`, `wsl`, `epral` |