feat(tui): add safe client and backup workflows
This commit is contained in:
@@ -1,118 +1,225 @@
|
||||
# wireguard-scripts
|
||||
|
||||
## M1 runtime foundation
|
||||
Безопасный копируемый Bash-инструмент для управления одним WireGuard-интерфейсом
|
||||
на Linux. Скрипт не устанавливается в `PATH`, не запускает установщики и не
|
||||
изменяет систему вне явно настроенных путей. Операции изменения выполняются под
|
||||
`flock`, с резервной копией, атомарной заменой, проверкой runtime и откатом.
|
||||
|
||||
The current entry point provides dependency-free `help` and `version` commands. The
|
||||
planned operational commands are recognized but remain unavailable until later
|
||||
milestones.
|
||||
## Поддерживаемая среда и ограничения
|
||||
|
||||
Runtime locations default to the project directory and can be redirected for an
|
||||
isolated run with absolute paths using these environment variables:
|
||||
Требуются Linux, Bash 4.3+, `wg`, `wg-quick`, `ip`, `flock`, стандартные
|
||||
coreutils и util-linux. Минимальная заявленная архитектура — x86_64. Другие
|
||||
архитектуры не считаются протестированными; `gum` и `qrencode` должны
|
||||
соответствовать архитектуре самостоятельно установленного оператором бинарника.
|
||||
|
||||
Обязательные capabilities: `bash`, `wg`, `wg-quick`, `ip`, `flock`, `awk`,
|
||||
`grep`, `sed`, `sort`, `stat`, `mktemp`, `sha256sum`, `cp`, `mv`, `chmod` и
|
||||
прочие обычные coreutils, используемые системой. Необязательные: `gum` (TUI),
|
||||
`qrencode` (QR), `systemctl`/`journalctl` (systemd), `ss` (UDP), `nft` и
|
||||
`iptables` (наблюдение firewall). Отсутствие optional capability даёт
|
||||
`SKIP`/fallback, а не скрытый вызов host-команды.
|
||||
|
||||
Проект можно копировать как каталог:
|
||||
|
||||
```text
|
||||
wireguard-scripts/
|
||||
├── wg-manager.sh
|
||||
├── wg-manager.conf.example
|
||||
├── add-user.sh
|
||||
├── bin/gum # optional, если оператор его положил сюда
|
||||
├── tests/
|
||||
│ ├── fixtures/ # placeholder-only fixtures
|
||||
│ └── mocks/bin/ # deny-by-default test commands
|
||||
├── backups/ logs/ state/ # runtime paths, создаются при настройке
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Настройка
|
||||
|
||||
Скопируйте `wg-manager.conf.example` в `wg-manager.conf`, замените только
|
||||
placeholder-значения и задайте владельца root и режим `0600`. Все пути должны
|
||||
быть абсолютными, без symlink-компонентов, а каталоги client/metadata/backup/
|
||||
archive/log — `0700`; конфигурации, metadata, key и log — `0600`.
|
||||
|
||||
| Ключ | Назначение и ограничения |
|
||||
|---|---|
|
||||
| `WG_INTERFACE` | имя интерфейса, например `wg0` |
|
||||
| `WG_CONF` | абсолютный серверный конфиг |
|
||||
| `CLIENT_DIR` | каталог клиентских `.conf/.key/.pub/.psk` |
|
||||
| `SERVER_ENDPOINT` | проверяемое имя/IP сервера без порта |
|
||||
| `SERVER_PORT` | UDP-порт 1..65535 |
|
||||
| `SERVER_VPN_IP` | адрес сервера IPv4 |
|
||||
| `CLIENT_DNS` | непустой comma-separated список IPv4/IPv6 |
|
||||
| `VPN_SUBNET_PREFIX` | IPv4 prefix из трёх октетов, например `192.0.2` |
|
||||
| `START_HOST` | начало пула host-частей |
|
||||
| `END_HOST` | конец пула host-частей |
|
||||
| `USE_IPV6` | `0` или `1` |
|
||||
| `IPV6_PREFIX` | prefix, оканчивающийся `::`, если IPv6 включён |
|
||||
| `DEFAULT_CLIENT_MODE` | `full-tunnel` или `split-tunnel` |
|
||||
| `RECENT_HANDSHAKE_SECONDS` | неотрицательное окно active-state |
|
||||
| `BACKUP_RETENTION` | bounded число automatic backups |
|
||||
|
||||
Окружение может изолированно переопределить расположение:
|
||||
`WG_MANAGER_CONFIG`, `WG_MANAGER_BACKUP_DIR`, `WG_MANAGER_LOG_DIR`,
|
||||
`WG_MANAGER_METADATA_DIR`, `WG_MANAGER_ARCHIVE_DIR`, and `WG_MANAGER_LOCK_FILE`.
|
||||
`WG_MANAGER_METADATA_DIR`, `WG_MANAGER_ARCHIVE_DIR`, `WG_MANAGER_LOCK_FILE`.
|
||||
`WG_MANAGER_GUM` имеет приоритет над локальным `bin/gum`, затем `PATH`.
|
||||
`WG_MANAGER_NOW` — только документированный deterministic clock override; перед
|
||||
боевым запуском эти переменные следует убрать. Test-only hooks принимаются лишь
|
||||
в confined `WG_MANAGER_TEST_MODE` под `TEST_ROOT` и не являются production API.
|
||||
|
||||
`WG_MANAGER_GUM` selects an executable `gum` explicitly; otherwise the bundled
|
||||
`bin/gum` and then `PATH` are checked in that order. Configuration is parsed as
|
||||
restricted `KEY=VALUE` data and is never executed as shell code. Blank lines and
|
||||
full-line `#` comments are ignored; spaces around the key, `=`, and value are
|
||||
trimmed. Values are either one unquoted token or a double-quoted value without
|
||||
escapes, and only documented keys are accepted.
|
||||
## Команды и коды
|
||||
|
||||
For deterministic read-only tests, `WG_MANAGER_NOW` may override the current
|
||||
epoch when set to decimal seconds; invalid values are rejected.
|
||||
| Команда | Поведение, stdout и подтверждение |
|
||||
|---|---|
|
||||
| `help`, `version` | безопасная справка/версия, без mutation |
|
||||
| `status` | disk/runtime summary, без secrets |
|
||||
| `list` | tab-separated peer rows с `state`/`origin`, без secrets |
|
||||
| `add NAME [--mode MODE] [--split-networks CIDRS] [--endpoint HOST]` | явный add под lock |
|
||||
| `show NAME` | identity-safe details и safe path, если он доступен |
|
||||
| `config NAME` | печатает только валидированный путь |
|
||||
| `config NAME --show` | единственная форма полного client config |
|
||||
| `qr NAME` | QR через stdin; без `qrencode` печатает путь и возвращает `69` |
|
||||
| `remove NAME [--yes]` | managed peer, archive вместо purge; noninteractive требует `--yes` |
|
||||
| `diagnose [--quiet]` | read-only checks; quiet даёт `id=STATUS` |
|
||||
| `backup` | ручная backup текущего server config под lock |
|
||||
| `backups` | только валидные backups current interface, newest-first |
|
||||
| `restore BACKUP [--yes]` | CLI: без `--yes` нужен обычный интерактивный `y/N`; без terminal команда отклоняется. В TUI требуется ввод полного backup ID; сначала создаётся current-state backup |
|
||||
| `log [LIMIT]` | validated audit records newest-first, `LIMIT` 1..1000 |
|
||||
| `tui` или запуск без аргументов | gum TUI, требует terminal stdin и stdout |
|
||||
|
||||
M2 adds read-only `status` and `list`. They parse the configured WireGuard file
|
||||
and, when available, `wg show <interface> dump`; a stopped or unavailable
|
||||
runtime is reported as unavailable and disk-only peers remain visible. `list`
|
||||
uses stable tab-separated output with English machine fields `state` and
|
||||
`origin` (`managed`, `legacy`, or `unmanaged`) and never prints private or
|
||||
preshared keys.
|
||||
Коды: `0` — успех или безопасная отмена; `64` — usage/flags/arity; `65` —
|
||||
validation или unsafe identity/path; `69` — отсутствующая optional capability;
|
||||
`70` — operation/backend failure; `71` — critical rollback/recovery failure;
|
||||
`77` — недостаточные права. Ошибка audit после mutation сообщается как partial
|
||||
operation и не маскирует исходный status. `FAIL` в diagnose даёт `70`,
|
||||
`WARN`/`SKIP` сами по себе не дают failure.
|
||||
|
||||
The explicit peer comment marker is `# wg-manager-client: NAME`; it is only a
|
||||
display candidate. A peer is considered managed only when its exact public key
|
||||
and configured addresses match a valid metadata record with `MANAGED=1`.
|
||||
Примеры используют только документационные адреса и имена:
|
||||
|
||||
## M3 transaction foundation
|
||||
```bash
|
||||
./wg-manager.sh status
|
||||
./wg-manager.sh list
|
||||
./wg-manager.sh add alice --mode full-tunnel
|
||||
./wg-manager.sh add bob --mode split-tunnel --split-networks '192.0.2.0/24,2001:db8:40::/48' --endpoint vpn.example.invalid
|
||||
./wg-manager.sh show alice
|
||||
./wg-manager.sh config alice
|
||||
./wg-manager.sh config alice --show
|
||||
./wg-manager.sh qr alice
|
||||
./wg-manager.sh remove alice --yes
|
||||
./wg-manager.sh diagnose --quiet
|
||||
./wg-manager.sh log 25
|
||||
./wg-manager.sh backup
|
||||
./wg-manager.sh backups
|
||||
./wg-manager.sh restore 20231114T221500Z-wg0-001 --yes
|
||||
./wg-manager.sh tui
|
||||
./add-user.sh alice vpn.example.invalid
|
||||
```
|
||||
|
||||
The internal `run_transaction OPERATION TARGET BUILDER [METADATA_FILE...]`
|
||||
primitive acquires `WG_MANAGER_LOCK_FILE`, then
|
||||
reloads and validates the live configuration before calling its builder. It
|
||||
creates a restrictive, timestamped backup, validates a same-filesystem staged
|
||||
configuration with `wg-quick strip`, replaces the live file atomically, applies
|
||||
it with `wg syncconf`, and verifies the active peer set. Apply or verification
|
||||
failures restore both disk and runtime; a failed restoration is reported as a
|
||||
critical rollback error. No M3 mutation command is exposed yet.
|
||||
`add-user.sh` — source-safe compatibility wrapper: он только передаёт
|
||||
`add NAME [--endpoint HOST]`, не исполняет аргументы и не содержит второй
|
||||
реализации backend.
|
||||
|
||||
M3 tests use only isolated overrides and deny-by-default command mocks; they do
|
||||
not inspect or modify host WireGuard state.
|
||||
## Транзакции, backups, archive и audit
|
||||
|
||||
## M4 client creation
|
||||
Каждая mutation повторно читает ресурсы после lock, валидирует staged config,
|
||||
создаёт backup, делает atomic replace, выполняет `wg syncconf`, затем сравнивает
|
||||
runtime dump. При ошибке восстанавливаются disk и runtime; если восстановление
|
||||
не доказано, возвращается `71` и сообщение `КРИТИЧНО`. Add публикует client
|
||||
artifacts только после успешной server transaction. Remove архивирует связанные
|
||||
файлы в новый restrictive каталог и не удаляет архивы автоматически. Restore
|
||||
перед применением создаёт backup текущего состояния, не восстанавливает client
|
||||
metadata/key files и принимает только validated backup своего интерфейса.
|
||||
|
||||
`add NAME` creates a client transactionally. The optional forms are
|
||||
`--mode full-tunnel|split-tunnel`, optional `--split-networks CIDRS` for
|
||||
split-tunnel, and `--endpoint HOST`; unknown or extra
|
||||
arguments are rejected. Names are restricted to ASCII letters, digits, `.`,
|
||||
`_`, and `-` (without a leading `-`). Root is required for production use;
|
||||
tests enable only an isolated `WG_MANAGER_TEST_MODE` under `TEST_ROOT`.
|
||||
Audit line содержит только UTC timestamp, actor UID, safe target, outcome и
|
||||
короткую error category. PrivateKey, PresharedKey, полный config, команды и
|
||||
окружение туда не попадают. `log` не source-ит записи и пропускает
|
||||
malformed/hostile lines. Handshake — текущее значение dump, не история.
|
||||
`firewall=OK` означает только видимое ожидаемое NAT-свидетельство, не полную
|
||||
проверку политики firewall.
|
||||
|
||||
Private/public/PSK files, client configuration, and metadata use restrictive
|
||||
permissions and are removed if the server transaction or rollback fails.
|
||||
Generated key material is supplied to `wg` through stdin or stdout and is not
|
||||
placed in command arguments or diagnostic output.
|
||||
## TUI
|
||||
|
||||
## M5 inspection and export
|
||||
TUI требует пригодный `gum` и оба terminal descriptors. Main screen показывает
|
||||
interface/runtime/managed/active/warning counts; Escape возвращает на предыдущий
|
||||
экран, отмена mutation ничего не меняет. Клиент выбирается по стабильной
|
||||
trusted identity, а не по decorated name. Config/QR требуют отдельного действия;
|
||||
missing `qrencode` — безопасный fallback. Critical rollback немедленно выходит
|
||||
с `71`; обычная ошибка сохраняет диагностический текст и возвращается в меню
|
||||
ограниченное число раз. Backend functions не вызывают gum напрямую.
|
||||
|
||||
`show NAME` displays only non-secret peer details and a config path after
|
||||
identity, ownership, mode, and path validation. `config NAME` prints only that
|
||||
validated path. Full client content requires the explicit noninteractive
|
||||
`config NAME --show` form; it never reads separate `.key` or `.psk` files.
|
||||
## Offline-проверка
|
||||
|
||||
`qr NAME` feeds the validated config to `qrencode -t ansiutf8` through stdin.
|
||||
If the optional `qrencode` dependency is unavailable, it prints the safe config
|
||||
path and returns status `69`; QR failures return the operation status. Routine
|
||||
show/export/path/error output is secret-free.
|
||||
Тесты используют только temporary `TEST_ROOT`, deny-by-default mocks и placeholder
|
||||
keys. Они не читают реальный WireGuard/network/service/firewall/sysctl state:
|
||||
|
||||
The inspection commands do not require `gum` or `qrencode` for `show` and
|
||||
`config`. `qrencode` is the only optional QR dependency; when it is absent,
|
||||
`qr NAME` writes the validated configuration path to stdout and an actionable
|
||||
warning to stderr, then returns status `69`. A nonzero `qrencode` result
|
||||
returns status `70` without printing configuration content in the diagnostic.
|
||||
```bash
|
||||
bash -n wg-manager.sh add-user.sh tests/run.sh tests/helpers.sh
|
||||
bash tests/run.sh
|
||||
bash tests/run.sh m11-e2e
|
||||
bash tests/run.sh secret-scan
|
||||
bash tests/run.sh no-host-writes
|
||||
bash tests/run.sh no-host-command-bypass
|
||||
bash tests/run.sh symlink-safety
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Common command statuses are `64` for usage errors, `65` for invalid or unsafe
|
||||
client identity/path data, `69` for a missing optional dependency, and `70`
|
||||
for an operational read/export/QR failure. `config NAME --show` is the only
|
||||
inspection form that writes the complete client configuration to stdout;
|
||||
ordinary `show`, `config`, and `qr` output remains secret-safe.
|
||||
Тестовые mocks не заменяют kernel WireGuard, distribution-specific systemd,
|
||||
firewall policy или архитектурный smoke-test на disposable interface.
|
||||
|
||||
## M9–M10 TUI
|
||||
## Troubleshooting
|
||||
|
||||
Запуск без аргументов или командой `tui` открывает интерфейс `gum` в
|
||||
терминале. `gum` разрешается в порядке `WG_MANAGER_GUM`, локального `bin/gum`,
|
||||
затем `PATH`; необходимые подкоманды проверяются до вывода интерфейса. Нужны
|
||||
одновременно интерактивные stdin и stdout. Escape и отмена возвращают на
|
||||
предыдущий экран или завершают меню без изменений. Доступны status, clients/show,
|
||||
diagnose, log и backups. В M10 также доступны add/remove, создание backup и
|
||||
restore через те же backend-функции, что и CLI; подтверждение и отмена остаются
|
||||
обязательными для мутаций, а критический rollback завершает TUI с ненулевым кодом.
|
||||
Для клиента сначала показываются безопасные детали; конфигурация и QR требуют
|
||||
отдельного явного действия, при отсутствии `qrencode` показывается понятный
|
||||
fallback. CLI работает независимо от `gum`; отсутствие или неполная
|
||||
возможность `gum` даёт код `69` только для TUI, а неинтерактивный запуск TUI —
|
||||
код `64`. Восстановление требует ввода полного идентификатора выбранной копии;
|
||||
несовпадение или отмена не изменяют состояние.
|
||||
- `65`: проверьте root-owned `0600` config, абсолютные пути, отсутствие symlink
|
||||
и соответствие интерфейса.
|
||||
- `69` для TUI/QR: установите совместимый gum/qrencode самостоятельно или
|
||||
используйте CLI без capability; TUI не скачивает бинарники.
|
||||
- `70`: проверьте stderr, lock и доступность `wg-quick`/`wg`; исходный config
|
||||
должен оставаться восстановленным.
|
||||
- `71 КРИТИЧНО`: остановите mutation, сохраните backup/archive и вручную
|
||||
проверьте disk/runtime перед повтором.
|
||||
- `diagnose` `SKIP`: capability отсутствует или systemd не является active init.
|
||||
- Для gum проверьте архитектуру, `WG_MANAGER_GUM`, локальный `bin/gum` и `PATH`.
|
||||
|
||||
## M6 удаление клиента
|
||||
## M11 evidence matrix
|
||||
|
||||
`remove NAME [--yes]` удаляет только подтверждённый managed-peer после повторной проверки точного публичного ключа, адресов и блока под lock. В интерактивном терминале без `--yes` показываются имя, адреса и только последние 8 символов публичного ключа; ответ, отличный от `y`, отменяет операцию без изменений. В неинтерактивном режиме `--yes` обязателен.
|
||||
| Требование | Evidence |
|
||||
|---|---|
|
||||
| status / list | `status`, `list`, `m11-e2e` — машинный вывод и смешанные origins |
|
||||
| add и allocation | `add-client`, `tui-add`, `m11-e2e` — полная/выборочная сеть и rollback |
|
||||
| show / config export | `show-client`, `config-export`, `m11-e2e` |
|
||||
| QR и fallback | `qr`, `tui-client-actions`, `secret-scan` |
|
||||
| remove / archive | `remove-client`, `archive-client`, `tui-remove`, `m11-e2e` |
|
||||
| diagnose | `diagnostics`, `diagnostics-exit-codes`, `m11-e2e` |
|
||||
| backup / backups | `backup-list`, `runtime-file-modes`, `tui-backup` |
|
||||
| restore | `restore`, `restore-rollback`, `tui-restore`, `m11-e2e` |
|
||||
| audit / log | `audit`, `audit-no-secrets`, `secret-scan` |
|
||||
| public-key identity | `parser`, `show-client`, `remove-client`, `tui-client-actions` |
|
||||
| transaction, apply и rollback | `transaction`, `rollback`, `add-rollback`, `m11-e2e` |
|
||||
| paths, symlinks и modes | `path-safety`, `symlink-safety`, `backup-path-safety`, `runtime-file-modes` |
|
||||
| secret redaction | `no-secret-logs`, `secret-scan`, `tui-no-secret-args` |
|
||||
| host write/command isolation | `no-host-writes`, `no-host-command-bypass` |
|
||||
| exit/status semantics | `config`, `diagnostics-exit-codes`, `rollback`, CLI-focused groups |
|
||||
| docs и deployment | этот README и disposable checklist; live-server deployment остаётся ручным шагом |
|
||||
|
||||
Изменение серверной конфигурации выполняется внутри транзакции: сначала конфигурация заменяется и проверяется в WireGuard, а затем финализатор архивирует связанные `.conf`, `.meta`, `.key`, `.pub` и `.psk` (если существуют) в уникальный каталог `ARCHIVE_DIR` с режимами `0700`/`0600`. Ошибка архивирования после применения приводит к откату и оставляет исходные файлы доступными для восстановления; архивы не удаляются автоматически и служат восстановительным материалом. Журнал содержит operation, target, UTC timestamp, outcome и короткий суффикс публичного ключа; ключи и содержимое конфигураций в него не записываются.
|
||||
Это offline evidence, а не обещание, что live server уже настроен или что
|
||||
deployment checklist выполнен.
|
||||
|
||||
`remove` возвращает `64` для ошибки аргументов, `65` для неподтверждённой или небезопасной identity/path, `69` для отсутствующей обязательной зависимости и `70` для ошибки операции или архивации. Ошибка аудита после успешной мутации явно сообщается и возвращает операционный статус; отмена остаётся успешной без изменения состояния.
|
||||
`diagnose [--quiet]` выполняет только read-only проверки конфигурации и прав, наличия интерфейса, `wg` dump, forwarding, адресов/маршрутов IPv4 и IPv6, UDP-порта, firewall, systemd, runtime-vs-disk peer set и журнала. Отдельные записи `config-parse`, `duplicate-keys`, `allowedips-overlap` и `key-permissions` не скрывают независимые причины сбоя. Каждая запись имеет один из статусов `OK`, `WARN`, `FAIL` или `SKIP`; `--quiet` выводит только стабильные строки `id=STATUS`. Обязательны `wg`, `wg-quick` и `ip`; `systemctl`, `journalctl`, `nft`, `iptables` и `ss` являются optional и при отсутствии дают собственный `SKIP`. На хосте без активного systemd проверки systemd и журнала пропускаются. `firewall=OK` означает только видимое ожидаемое NAT-свидетельство, а не полную проверку политики firewall. Ненулевой код (`70`) возвращается при любом `FAIL`, `64` — при неверных аргументах. Команда не изменяет конфигурацию, runtime, маршруты, firewall или sysctl; handshake и журнал имеют ограниченную историческую видимость. Тесты используют только изолированные fixtures/mocks и не доказывают состояние реального сервера.
|
||||
## Disposable deployment checklist (не выполнен)
|
||||
|
||||
## M8 резервные копии, восстановление и аудит
|
||||
1. Создать отдельный non-production interface и disposable host.
|
||||
2. Скопировать каталог проекта, не устанавливая его в `PATH`.
|
||||
3. Проверить Bash, WireGuard tools, coreutils/util-linux и архитектуру gum.
|
||||
4. Скопировать example config и заменить только placeholder values.
|
||||
5. Проверить owner root, config `0600` и каталоги `0700`.
|
||||
6. Выполнить `help`, `version`, `status` и `list`.
|
||||
7. Выполнить `diagnose` и сохранить безопасный отчёт.
|
||||
8. Добавить disposable client и проверить modes/metadata/audit.
|
||||
9. Проверить `show`, safe `config` path и QR fallback.
|
||||
10. Удалить client с явным подтверждением и проверить archive.
|
||||
11. Создать backup и проверить newest-first listing.
|
||||
12. Восстановить backup с typed confirmation на disposable config.
|
||||
13. Проверить lock contention и rollback на тестовом runtime.
|
||||
14. Сверить SSH/noninteractive, firewall visibility и systemd limitations.
|
||||
15. Только после human review решить, допустим ли production rollout.
|
||||
|
||||
`backup` создаёт ручную резервную копию текущей серверной конфигурации под lock; `backups` показывает только проверенные прямые дочерние каталоги, в порядке от новых к старым. Повреждённые или неполные записи не исполняются и не читаются как shell-код. `restore BACKUP [--yes]` восстанавливает только серверную конфигурацию: перед заменой текущая версия также резервируется, а ошибки применения проверяют и откатывают disk/runtime. Восстановление другой WireGuard-интерфейсной конфигурации отклоняется. Без `--yes` требуется подтверждение в терминале; в неинтерактивном режиме команда возвращает ошибку использования. Клиентские metadata/key-файлы восстановлением не изменяются.
|
||||
|
||||
`log [LIMIT]` выводит только корректные структурированные audit-события в newest-first порядке; `LIMIT` находится в диапазоне 1..1000. При отсутствии журнала выводится безопасное сообщение. Записи содержат UTC timestamp, actor UID, operation, target, outcome и короткую категорию ошибки; секреты, команды и полные конфигурации не записываются. Ошибки аудита после успешной мутации сообщаются как частичный операционный результат. Основные коды: `64` — аргументы, `65` — небезопасная/невалидная backup identity, `70` — ошибка операции; критический неудачный rollback сохраняет отдельный rollback status.
|
||||
Этот список намеренно не выполняется агентом и не является разрешением менять
|
||||
реальный host state без отдельной проверки оператором.
|
||||
|
||||
Reference in New Issue
Block a user