mirror of
https://github.com/oqyude/nixos.git
synced 2026-10-07 20:47:14 +03:00
317 lines
21 KiB
Markdown
317 lines
21 KiB
Markdown
# 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/<service>` пуст, и сервис **молча** стартует на чистой
|
||
базе. Пользователь увидит «потерялись данные».
|
||
**Решение (рекомендую):** добавить в `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` |
|