# wireguard-scripts ## M1 runtime foundation 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: `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_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. M2 adds read-only `status` and `list`. They parse the configured WireGuard file and, when available, `wg show 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. 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 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. M3 tests use only isolated overrides and deny-by-default command mocks; they do not inspect or modify host WireGuard state. ## M4 client creation `add NAME` creates a client transactionally. The optional forms are `--mode full-tunnel|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`. 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. ## M5 inspection and export `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. `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. 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. 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. ## M6 удаление клиента `remove NAME [--yes]` удаляет только подтверждённый managed-peer после повторной проверки точного публичного ключа, адресов и блока под lock. В интерактивном терминале без `--yes` показываются имя, адреса и только последние 8 символов публичного ключа; ответ, отличный от `y`, отменяет операцию без изменений. В неинтерактивном режиме `--yes` обязателен. Изменение серверной конфигурации выполняется внутри транзакции: сначала конфигурация заменяется и проверяется в WireGuard, а затем финализатор архивирует связанные `.conf`, `.meta`, `.key`, `.pub` и `.psk` (если существуют) в уникальный каталог `ARCHIVE_DIR` с режимами `0700`/`0600`. Ошибка архивирования после применения приводит к откату и оставляет исходные файлы доступными для восстановления; архивы не удаляются автоматически и служат восстановительным материалом. Журнал содержит operation, target, UTC timestamp, outcome и короткий суффикс публичного ключа; ключи и содержимое конфигураций в него не записываются. `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 и не доказывают состояние реального сервера. ## M8 резервные копии, восстановление и аудит `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.