Compare commits

...

14 Commits

49 changed files with 10303 additions and 192 deletions
+17
View File
@@ -0,0 +1,17 @@
name = "milestone_repairer"
description = "Low-cost repair agent that fixes concrete defects found by the senior milestone reviewer."
model = "gpt-5.6-luna"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
developer_instructions = """
Repair only the concrete review findings supplied by the primary agent for the current milestone.
Read AGENTS.md, the current milestone in PLANS.md, the relevant MVP.md requirements, the current diff, and every referenced file. Reproduce each reported failure before editing when the failure is reproducible in the mock-based development environment.
For every finding, identify the cause, make the smallest safe correction, add or strengthen a regression test, and rerun the failed command plus the complete verification gate for the current milestone. Do not weaken tests, suppress errors without justification, or change acceptance criteria.
The development machine has no WireGuard server. Use only fixtures and mocks. Never touch the host's real WireGuard, networking, firewall, services, journal, or /etc/wireguard state.
Do not mark the milestone DONE, create commits, or declare the review passed. Return a concise finding-by-finding report with changed files, tests added, exact verification results, and anything that remains unresolved. The primary agent must independently review the result.
"""
+19
View File
@@ -0,0 +1,19 @@
name = "milestone_worker"
description = "Low-cost implementation agent for one bounded WireGuard Manager milestone or an explicitly isolated part of it."
model = "gpt-5.6-luna"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
developer_instructions = """
Implement only the milestone or bounded subtask assigned by the primary agent.
Before editing, read the applicable AGENTS.md rules, the assigned section of PLANS.md, and the relevant parts of MVP.md. Inspect the current files and preserve unrelated user changes.
Use fixtures and mocks for all WireGuard, networking, service, firewall, journal, qrencode, and gum behavior. The development machine has no WireGuard server. Never access or modify the host's real /etc/wireguard, interfaces, routes, firewall, services, or sysctl settings.
Keep the change within the files and acceptance criteria named in the assignment. Add or update tests with the implementation. Run every assigned verification command. If a check fails, repair the implementation and rerun it before returning.
Do not mark a milestone DONE, update the final milestone status, create commits, broaden scope, or claim that mock verification proves behavior on a real WireGuard server. The primary agent owns review, status changes, and completion decisions.
Return a concise report containing changed files, behavior implemented, verification commands and results, remaining uncertainties, and any blocker. Never include private keys, preshared keys, or full client configs in the report.
"""
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.6-sol"
model_reasoning_effort = "high"
[agents]
enabled = true
max_concurrent_threads_per_session = 3
default_subagent_model = "gpt-5.6-luna"
default_subagent_reasoning_effort = "medium"
interrupt_message = true
+19
View File
@@ -1 +1,20 @@
/.idea/ /.idea/
/.obsidian/
# Local administrator configuration and runtime data
/wg-manager.conf
/backups/
/logs/
/state/
/bin/gum
# Generated client material
*.key
*.pub
*.psk
*-client-*.conf
*.conf.tmp
# Isolated test output
/tests/tmp/
/tests/output/
+300
View File
@@ -0,0 +1,300 @@
# AGENTS.md
## Scope
- These rules apply to the entire repository.
- Build a local WireGuard management tool for Linux servers.
- Keep `wg-manager.sh` as the primary entry point.
- The project is copied as a directory and is not installed into `PATH`.
- Do not add installer or uninstaller scripts unless explicitly requested.
- Treat `MVP.md` as the product scope and acceptance reference.
- Treat `PLANS.md` as the required milestone sequence for MVP implementation.
- Complete and verify one milestone before starting the next one.
- Update the `PLANS.md` progress and execution log as milestones are completed.
- Run milestones under the Sol coordinator and Luna worker/reviewer loop defined in `PLANS.md` and `.codex/`.
- The coordinator must independently review Luna's changes and delegate explicit Luna repairs until verification passes or a genuine blocker remains.
- Use the existing `add-user.sh` only as a behavioral reference.
- New code must improve its safety rather than copy its weaknesses.
## General priorities
- Preserve a working WireGuard configuration above all else.
- Prefer correctness and recoverability over cleverness.
- Keep CLI behavior independent from the TUI.
- Make every mutating operation explicit and auditable.
- Keep secrets out of output, logs, tests, and error messages.
- Minimize required dependencies.
- Support operation over SSH and ordinary terminal emulators.
- Write user-facing text in Russian unless the surrounding interface uses another language.
- Keep identifiers, functions, CLI command names, and code comments in English.
- Avoid unrelated refactors while implementing a focused change.
## Bash baseline
- Use Bash, not POSIX `sh`.
- Start executable scripts with `#!/usr/bin/env bash`.
- Enable `set -Eeuo pipefail` in production entry points.
- Understand strict-mode exceptions before suppressing them.
- Use `if command; then` when a nonzero status is expected.
- Do not append `|| true` unless the ignored failure is intentional and documented.
- Set `umask 077` before creating configs, keys, metadata, logs, or backups.
- Require a supported Bash version at startup when using version-specific features.
- Prefer Bash builtins over unnecessary subprocesses.
- Use `printf` rather than `echo -e`.
- Use `[[ ... ]]` for Bash conditionals.
- Use `(( ... ))` for arithmetic conditions.
- Use `case` for command dispatch and finite state handling.
- Use `local` for function-local variables.
- Declare variables close to first use.
- Use `readonly` for constants after configuration is loaded.
- Use uppercase names only for exported variables and true constants.
- Use lowercase snake_case for local variables and functions.
- Give functions verb-based names such as `load_config` or `add_client`.
- Keep functions focused and normally below about 60 lines.
- Return statuses from functions; print data only when output is the contract.
- Do not mix machine-readable results with progress messages.
- Send errors and diagnostics to stderr.
- Use explicit exit codes for expected CLI failure categories.
## Quoting and data handling
- Double-quote parameter expansions unless splitting is deliberately required.
- Use `"${array[@]}"` when forwarding array elements.
- Use arrays to build commands; never construct commands as strings.
- Never use `eval`.
- Never pass user-controlled text to `bash -c`.
- Avoid command substitution for multiline or NUL-sensitive data.
- Use `mapfile -t` for line-oriented command output when appropriate.
- Use `IFS= read -r` to read literal lines.
- Use `read -r -d ''` or NUL delimiters for arbitrary file names.
- Use `--` before user-derived positional arguments where supported.
- Do not rely on locale-sensitive parsing.
- Set `LC_ALL=C` locally for command output that must be parsed predictably.
- Do not parse human-formatted output when a stable dump format exists.
- Prefer `wg show <interface> dump` for WireGuard state parsing.
- Treat names, endpoints, addresses, paths, and config values as untrusted input.
- Validate input with allowlists and semantic checks.
- Reject control characters and embedded newlines in user input.
- Never derive a filesystem path from an unchecked client name.
- Canonicalize or constrain paths before reading or writing them.
## Configuration
- Keep server-specific values in `wg-manager.conf`.
- Resolve the project directory from `${BASH_SOURCE[0]}`.
- Do not depend on the caller's current working directory.
- Do not silently overwrite administrator configuration.
- Validate every required setting at startup.
- Reject unknown enum values such as unsupported tunnel modes.
- Validate numeric ranges before arithmetic use.
- Validate IPv4, IPv6, CIDR, port, and hostname values before use.
- Do not `source` a configuration file that may be writable by untrusted users.
- If sourcing is retained, require root ownership and non-group/world-writable permissions.
- Prefer a restricted `KEY=VALUE` parser for long-term safety.
- Environment overrides must be documented and narrowly scoped.
- Never accept key material through command-line arguments.
- Avoid exporting secrets into the process environment.
## Temporary files and cleanup
- Create temporary files with `mktemp`.
- Create mutation staging files on the same filesystem as the target.
- Never use predictable temporary filenames.
- Register cleanup immediately after creating temporary resources.
- Use a cleanup function with `trap` for `EXIT`, `INT`, and `TERM`.
- Quote trap bodies safely; avoid premature expansion.
- Preserve the original exit status during cleanup.
- Do not remove a path unless this process created it or validated it exactly.
- Apply restrictive permissions before writing secret content.
- Do not leave key material in world-readable temporary directories.
## WireGuard safety
- Require root only for commands that need privileged access or mutation.
- Allow safe read-only commands without root when system permissions permit it.
- Check that the configured interface and config file agree.
- Identify peers by public key, not only by display name.
- Treat comments such as `### Client NAME` as metadata, not identity.
- Detect duplicate public keys and overlapping `AllowedIPs`.
- Detect address collisions before generating any persistent file.
- Distinguish managed peers from unmanaged peers.
- Never modify an unmanaged peer implicitly.
- Do not regenerate server keys automatically.
- Do not rotate or replace client keys without explicit confirmation.
- Never print private or preshared keys in routine output.
- Never include key values in traces or debug logs.
- Disable shell tracing around all secret-handling code.
- Store client files with mode `0600`.
- Keep directories containing client material at mode `0700`.
## Mutating operations
- Serialize mutations with `flock`.
- Acquire the lock before re-reading mutable state.
- Revalidate assumptions after the lock is acquired.
- Create a backup before every persistent change.
- Write the complete proposed config to a staging file.
- Validate the staged config before replacing the live config.
- Preserve the target owner, group, and restrictive mode.
- Replace files atomically with a same-filesystem rename.
- Apply the new configuration only after durable storage succeeds.
- Verify active state after applying the change.
- Roll back both disk and runtime state on failure.
- Report rollback failure as a critical error.
- Never append directly to the live WireGuard config.
- Never use broad text deletion to remove a peer block.
- Match an exact managed block and its public key.
- Default to archiving client files instead of permanent deletion.
- Require explicit confirmation before destructive or restorative actions.
- Make repeated operations fail safely or behave idempotently.
## Backups and audit log
- Timestamp backups in an unambiguous UTC format.
- Include the interface name in backup metadata or filenames.
- Keep a bounded number of automatic backups.
- Never prune the only known-good backup.
- Validate a backup before offering restoration.
- Back up the current state before restoring an older state.
- Record who initiated a mutation when that information is available.
- Log the operation, target, timestamp, outcome, and concise error.
- Do not log private keys, preshared keys, or full client configs.
- Avoid logging full environment dumps or command lines with secrets.
- Make logs readable only by root by default.
- Log failed mutations and rollback outcomes.
## CLI design
- TUI actions must call the same backend functions as CLI commands.
- Keep CLI commands usable when `gum` is unavailable.
- Provide `help` and useful usage errors.
- Reject unknown commands and extra positional arguments.
- Keep command names stable once documented.
- Use stdout for requested data and stderr for warnings or progress.
- Do not add decorative ANSI output when stdout is not a terminal.
- Honor `NO_COLOR` for CLI colors where colors are used.
- Return zero only when the requested operation succeeded.
- Use distinct nonzero statuses for usage, validation, dependency, and runtime errors.
- Avoid interactive prompts when a CLI command is explicitly noninteractive.
- Require a confirmation flag for destructive noninteractive commands.
- Never let localization change machine-parsed values or exit codes.
## TUI with gum
- Use `gum` as the only planned TUI framework.
- Do not add `dialog`, `whiptail`, or a second TUI framework.
- Do not download or install `gum` from the application.
- Resolve `gum` in this order: `WG_MANAGER_GUM`, local `bin/gum`, then `PATH`.
- Validate that the resolved path is executable.
- Check required `gum` subcommands or minimum version at startup.
- Fail with a clear message when TUI is requested without usable `gum`.
- Keep all `gum` invocation details in small UI wrapper functions.
- Backend functions must never call `gum` directly.
- Use `gum choose` for short finite menus.
- Use `gum filter` for searchable client selection.
- Use `gum input` for short validated text fields.
- Use `gum confirm` before deletion, restoration, or key rotation.
- Use `gum table` only for display or selection, not as a data store.
- Use `gum pager` for logs, diagnostics, and long text.
- Use `gum spin` only when the wrapped operation can safely hide routine output.
- Preserve stderr from failed commands behind a spinner.
- Never show secret content inside a spinner command line.
- Keep stable internal values separate from translated display labels.
- Treat cancellation and Escape as normal control flow, not an error.
- Return to the previous screen after cancellation.
- Do not trap users in an endless menu loop after repeated failures.
- Keep the main menu shallow and task-oriented.
- Show interface name and current status on the main screen.
- Use consistent verbs: view, add, remove, diagnose, restore, exit.
- Show the exact target before asking for destructive confirmation.
- Require stronger confirmation for restoring backups or deleting client material.
- Keep colors readable on light and dark terminals.
- Do not communicate status by color alone.
- Keep layouts usable at 80 columns.
- Avoid assumptions about mouse support.
- Do not clear valuable terminal output after an error.
- Detect noninteractive stdin or stdout before starting TUI.
- Never emit TUI control output into pipelines or redirected files.
## Error handling
- Provide one concise user-facing error plus actionable context.
- Preserve the failing command's status where meaningful.
- Do not expose secret variable values in error messages.
- Distinguish validation errors from system failures.
- Distinguish skipped diagnostic checks from successful checks.
- Diagnostic results use `OK`, `WARN`, `FAIL`, or `SKIP`.
- A critical diagnostic failure must produce a nonzero status.
- Include remediation guidance when it is reliable and specific.
- Avoid guessing firewall fixes across distributions.
- Continue independent diagnostic checks after a nonfatal failure.
## Dependencies and portability
- Assume a Linux server with Bash and WireGuard tools.
- Keep required dependencies listed in `MVP.md` and README.
- Treat `qrencode`, systemd, `nft`, and `iptables` as optional capabilities.
- Detect optional commands before using them.
- Do not fail the entire diagnostic run because one optional tool is absent.
- Prefer feature detection over distribution detection.
- Do not assume systemd is PID 1 on every supported server.
- Support at least Linux `x86_64`; document additional tested architectures.
- Do not assume the bundled `gum` binary matches the current architecture.
- Report architecture mismatch clearly.
## Tests
- Add regression tests for every fixed bug.
- Tests must not modify the host's real `/etc/wireguard`.
- Use fixtures for server configs, peer blocks, and metadata.
- Mock external commands through a controlled test `PATH`.
- Cover `wg`, `wg-quick`, `ip`, `systemctl`, `journalctl`, `nft`, and `iptables` as needed.
- Test success, expected failure, rollback, and interruption paths.
- Test client names containing spaces, newlines, glob characters, and option prefixes.
- Test duplicate keys, duplicate IPs, and overlapping networks.
- Test empty peer lists and unmanaged peers.
- Test CLI behavior without `gum`.
- Test local binary, environment override, and `PATH` resolution for `gum`.
- Test that logs do not contain generated secrets.
- Test concurrent mutation attempts.
- Test restoration failure and failed rollback reporting.
- Keep tests deterministic; do not depend on external network access.
- Use temporary test directories and remove them safely.
## Quality gates
- Run `bash -n` on every changed shell script.
- Run ShellCheck on every changed shell script when available.
- Run the relevant automated tests.
- Format consistently; use `shfmt` when the project adopts a fixed style.
- Do not silence ShellCheck globally to hide local problems.
- Add a narrow inline suppression only with an explanatory comment.
- Review diffs for accidental key material before finishing.
- Review file permissions in tests that create secrets.
- Verify that failure paths preserve the original config.
- Verify CLI output with stdout redirected.
- Verify TUI entry and cancellation in an interactive terminal.
## Documentation
- Update `MVP.md` only when product scope or architecture changes.
- Keep operational usage in README once implementation begins.
- Document every CLI command and its exit behavior.
- Document required and optional dependencies separately.
- Include examples that use placeholder keys and addresses only.
- Never commit real endpoints, private keys, preshared keys, or client configs.
- Explain backup and restore behavior before users rely on it.
- Document limitations of handshake history and traffic visibility.
## Definition of done
- The requested behavior is implemented through shared backend functions.
- Mutations are locked, validated, backed up, atomic, and reversible.
- CLI operation remains available independently of the TUI.
- TUI uses the configured `gum` resolution order.
- Errors are actionable and do not reveal secrets.
- Relevant tests pass.
- `bash -n` passes.
- ShellCheck has no unexplained findings.
- Documentation matches observable behavior.
- No unrelated files or user changes are overwritten.
+1734
View File
File diff suppressed because it is too large Load Diff
+223
View File
@@ -1,2 +1,225 @@
# wireguard-scripts # wireguard-scripts
Безопасный копируемый Bash-инструмент для управления одним WireGuard-интерфейсом
на Linux. Скрипт не устанавливается в `PATH`, не запускает установщики и не
изменяет систему вне явно настроенных путей. Операции изменения выполняются под
`flock`, с резервной копией, атомарной заменой, проверкой runtime и откатом.
## Поддерживаемая среда и ограничения
Требуются 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`, `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.
## Команды и коды
| Команда | Поведение, 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 |
Коды: `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.
Примеры используют только документационные адреса и имена:
```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
```
`add-user.sh` — source-safe compatibility wrapper: он только передаёт
`add NAME [--endpoint HOST]`, не исполняет аргументы и не содержит второй
реализации backend.
## Транзакции, backups, archive и audit
Каждая 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 своего интерфейса.
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.
## TUI
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 напрямую.
## Offline-проверка
Тесты используют только temporary `TEST_ROOT`, deny-by-default mocks и placeholder
keys. Они не читают реальный WireGuard/network/service/firewall/sysctl state:
```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
```
Тестовые mocks не заменяют kernel WireGuard, distribution-specific systemd,
firewall policy или архитектурный smoke-test на disposable interface.
## Troubleshooting
- `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`.
## M11 evidence matrix
| Требование | 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 остаётся ручным шагом |
Это offline evidence, а не обещание, что live server уже настроен или что
deployment checklist выполнен.
## Disposable deployment checklist (не выполнен)
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.
Этот список намеренно не выполняется агентом и не является разрешением менять
реальный host state без отдельной проверки оператором.
+23 -192
View File
@@ -1,200 +1,31 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -Eeuo pipefail
WG_INTERFACE="wg0"
WG_CONF="/etc/wireguard/${WG_INTERFACE}.conf"
WG_DIR="/etc/wireguard"
CLIENT_DIR="${WG_DIR}/clients"
SERVER_PORT="51820"
DEFAULT_ENDPOINT="37.26.136.232"
SERVER_VPN_IP="10.9.0.1"
VPN_SUBNET_PREFIX="10.9.0"
START_HOST="2"
END_HOST="254"
USE_IPV6="0"
IPV6_PREFIX="fd05:13:53::"
umask 077 umask 077
die() { # Compatibility entry point. Client mutation has one implementation in
echo "Ошибка: $*" >&2 # wg-manager.sh; this wrapper deliberately contains no WireGuard operations.
exit 1 ADD_USER_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly ADD_USER_SCRIPT_DIR
add_user_usage() {
printf 'Использование: %s NAME [ENDPOINT]\n' "${ADD_USER_SCRIPT_DIR}/add-user.sh" >&2
printf 'Устаревший wrapper: используйте %s add NAME [--endpoint HOST]\n' \
"${ADD_USER_SCRIPT_DIR}/wg-manager.sh" >&2
} }
need_cmd() { add_user_main() {
command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1" local -a args
} if (( $# < 1 || $# > 2 )); then
add_user_usage
usage() { return 64
cat <<EOF
Использование:
sudo $0 CLIENT_NAME [ENDPOINT]
Примеры:
sudo $0 android-phone
sudo $0 work-laptop vpn.example.com
sudo $0 user5 37.26.136.232
Что делает:
- создаёт ключи клиента
- подбирает следующий свободный IP
- добавляет peer в ${WG_CONF}
- создаёт клиентский конфиг в ${CLIENT_DIR}
- применяет конфиг WireGuard
EOF
}
[[ $# -ge 1 ]] || { usage; exit 1; }
CLIENT_NAME="$1"
ENDPOINT="${2:-$DEFAULT_ENDPOINT}"
[[ "$EUID" -eq 0 ]] || die "Скрипт нужно запускать через sudo/root"
[[ -f "$WG_CONF" ]] || die "Не найден конфиг ${WG_CONF}"
need_cmd wg
need_cmd awk
need_cmd grep
need_cmd sed
need_cmd sort
need_cmd tee
need_cmd mkdir
need_cmd chmod
mkdir -p "$CLIENT_DIR"
if [[ ! "$CLIENT_NAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then
die "Имя клиента может содержать только буквы, цифры, точку, подчёркивание и дефис"
fi
CLIENT_CONF="${CLIENT_DIR}/${WG_INTERFACE}-client-${CLIENT_NAME}.conf"
CLIENT_PRIVKEY_FILE="${CLIENT_DIR}/${CLIENT_NAME}.key"
CLIENT_PUBKEY_FILE="${CLIENT_DIR}/${CLIENT_NAME}.pub"
CLIENT_PSK_FILE="${CLIENT_DIR}/${CLIENT_NAME}.psk"
if grep -qE "^### Client ${CLIENT_NAME}$" "$WG_CONF"; then
die "Клиент с именем '${CLIENT_NAME}' уже есть в ${WG_CONF}"
fi
if [[ -e "$CLIENT_CONF" || -e "$CLIENT_PRIVKEY_FILE" || -e "$CLIENT_PUBKEY_FILE" || -e "$CLIENT_PSK_FILE" ]]; then
die "Файлы клиента '${CLIENT_NAME}' уже существуют в ${CLIENT_DIR}"
fi
find_next_ipv4() {
local used host
used="$(
grep -oE "${VPN_SUBNET_PREFIX//./\\.}\.[0-9]+/32" "$WG_CONF" \
| sed 's#/32##' \
| awk -F. '{print $4}' \
| sort -n \
| uniq
)"
for host in $(seq "$START_HOST" "$END_HOST"); do
if ! grep -qx "$host" <<<"$used"; then
echo "${VPN_SUBNET_PREFIX}.${host}"
return 0
fi
done
return 1
}
CLIENT_IPV4="$(find_next_ipv4)" || die "Не удалось найти свободный IPv4 адрес"
CLIENT_HOST_OCTET="${CLIENT_IPV4##*.}"
if [[ "$USE_IPV6" == "1" ]]; then
CLIENT_IPV6="${IPV6_PREFIX}${CLIENT_HOST_OCTET}"
fi
echo "Создаю клиента: ${CLIENT_NAME}"
echo "IPv4 клиента: ${CLIENT_IPV4}"
if [[ "$USE_IPV6" == "1" ]]; then
echo "IPv6 клиента: ${CLIENT_IPV6}"
fi
wg genkey | tee "$CLIENT_PRIVKEY_FILE" | wg pubkey > "$CLIENT_PUBKEY_FILE"
wg genpsk > "$CLIENT_PSK_FILE"
chmod 600 "$CLIENT_PRIVKEY_FILE" "$CLIENT_PUBKEY_FILE" "$CLIENT_PSK_FILE"
CLIENT_PRIVATE_KEY="$(cat "$CLIENT_PRIVKEY_FILE")"
CLIENT_PUBLIC_KEY="$(cat "$CLIENT_PUBKEY_FILE")"
CLIENT_PRESHARED_KEY="$(cat "$CLIENT_PSK_FILE")"
if command -v wg >/dev/null 2>&1 && wg show "$WG_INTERFACE" >/dev/null 2>&1; then
SERVER_PUBLIC_KEY="$(wg show "$WG_INTERFACE" public-key)"
else
SERVER_PUBLIC_KEY="$(wg pubkey < <(grep '^PrivateKey *= *' "$WG_CONF" | head -n1 | awk -F'=' '{gsub(/ /,"",$2); print $2}'))"
fi
TMP_PEER_FILE="$(mktemp)"
trap 'rm -f "$TMP_PEER_FILE"' EXIT
{
echo
echo "### Client ${CLIENT_NAME}"
echo "[Peer]"
echo "PublicKey = ${CLIENT_PUBLIC_KEY}"
echo "PresharedKey = ${CLIENT_PRESHARED_KEY}"
if [[ "$USE_IPV6" == "1" ]]; then
echo "AllowedIPs = ${CLIENT_IPV4}/32,${CLIENT_IPV6}/128"
else
echo "AllowedIPs = ${CLIENT_IPV4}/32"
fi fi
} > "$TMP_PEER_FILE" args=(add "$1")
if (( $# == 2 )); then
args+=(--endpoint "$2")
fi
exec "${ADD_USER_SCRIPT_DIR}/wg-manager.sh" "${args[@]}"
}
cat "$TMP_PEER_FILE" >> "$WG_CONF" if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
add_user_main "$@"
if [[ "$USE_IPV6" == "1" ]]; then
CLIENT_ADDRESS="${CLIENT_IPV4}/32,${CLIENT_IPV6}/128"
CLIENT_ALLOWED_IPS="0.0.0.0/0,::/0"
else
CLIENT_ADDRESS="${CLIENT_IPV4}/32"
CLIENT_ALLOWED_IPS="0.0.0.0/0"
fi fi
cat > "$CLIENT_CONF" <<EOF
[Interface]
PrivateKey = ${CLIENT_PRIVATE_KEY}
Address = ${CLIENT_ADDRESS}
DNS = ${SERVER_VPN_IP},192.168.0.200
[Peer]
PublicKey = ${SERVER_PUBLIC_KEY}
PresharedKey = ${CLIENT_PRESHARED_KEY}
Endpoint = ${ENDPOINT}:${SERVER_PORT}
AllowedIPs = ${CLIENT_ALLOWED_IPS}
PersistentKeepalive = 25
EOF
chmod 600 "$CLIENT_CONF"
echo "Применяю конфиг WireGuard..."
wg syncconf "$WG_INTERFACE" <(wg-quick strip "$WG_INTERFACE")
echo
echo "Готово."
echo "Клиент: ${CLIENT_NAME}"
echo "IP: ${CLIENT_IPV4}"
echo "Конфиг: ${CLIENT_CONF}"
echo
if command -v qrencode >/dev/null 2>&1; then
echo "QR-код для клиента:"
qrencode -t ansiutf8 < "$CLIENT_CONF"
echo
else
echo "Пакет qrencode не установлен, QR-код не напечатан."
echo "Установить можно так:"
echo " sudo apt update && sudo apt install -y qrencode"
echo
fi
echo "Проверка:"
echo " sudo wg show ${WG_INTERFACE}"
echo
echo "Передать клиенту файл:"
echo " ${CLIENT_CONF}"
+8
View File
@@ -0,0 +1,8 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32,192.0.2.2/32
+12
View File
@@ -0,0 +1,12 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.3/32
+7
View File
@@ -0,0 +1,7 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
AllowedIPs = 192.0.2.2/32
+10
View File
@@ -0,0 +1,10 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
# wg-manager-client: alice
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
Endpoint = vpn.example.invalid:51820
+16
View File
@@ -0,0 +1,16 @@
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
ListenPort = 51820
### Client first
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
### Client second
[Peer]
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32
# wg-manager-client: third
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
AllowedIPs = 192.0.2.4/32
+12
View File
@@ -0,0 +1,12 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32, 2001:db8::2/128
[Peer]
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32
+18
View File
@@ -0,0 +1,18 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
AllowedIPs = 192.0.2.4/32
[Peer]
### Client legacy
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32,2001:db8::3/128
[Peer]
# wg-manager-client: alice
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
+18
View File
@@ -0,0 +1,18 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
# wg-manager-client: alice
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
[Peer]
### Client legacy
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32,2001:db8::3/128
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
AllowedIPs = 192.0.2.4/32
+4
View File
@@ -0,0 +1,4 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+12
View File
@@ -0,0 +1,12 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 2001:db8::/64
[Peer]
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 2001:db8::2/128
+12
View File
@@ -0,0 +1,12 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.0/24
[Peer]
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32
+9
View File
@@ -0,0 +1,9 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
# wg-manager-client: alice
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32, 2001:db8::2/128
+15
View File
@@ -0,0 +1,15 @@
[Interface]
Address = 192.0.2.1/24
Address = 2001:db8::1/64
DNS = 192.0.2.53
DNS = 2001:db8::53
PreUp = true
PreUp = true
PostUp = true
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
ListenPort = 51820
FwMark = 123
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
+2
View File
@@ -0,0 +1,2 @@
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 51820 0
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= (none) vpn.example.invalid:51820 192.0.2.2/32 1700000100 100 200 off
+8
View File
@@ -0,0 +1,8 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32; do-not-execute
+2
View File
@@ -0,0 +1,2 @@
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 51820 0
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= (none) vpn.example.invalid:51820 192.0.2.2/32 1700000100 100 200 25
+10
View File
@@ -0,0 +1,10 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
# wg-manager-client: fixture
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
Endpoint = vpn.example.invalid:51820
+11
View File
@@ -0,0 +1,11 @@
[Interface]
PrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=
Address = 192.0.2.3/32
DNS = 192.0.2.53,2001:db8::53
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
PresharedKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=
Endpoint = vpn.example.invalid:51820
AllowedIPs = 0.0.0.0/0,::/0
PersistentKeepalive = 25
+11
View File
@@ -0,0 +1,11 @@
[Interface]
PrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=
Address = 192.0.2.3/32
DNS = 192.0.2.53,2001:db8::53
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
PresharedKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=
Endpoint = vpn.example.invalid:51820
AllowedIPs = 192.0.2.0/24
PersistentKeepalive = 25
+7
View File
@@ -0,0 +1,7 @@
NAME=alice
PUBLIC_KEY=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=
IPV4=192.0.2.3
CREATED_AT=1700000100
CONFIG_PATH=CLIENT_DIR/alice.conf
CLIENT_MODE=full-tunnel
MANAGED=1
+16
View File
@@ -0,0 +1,16 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
# wg-manager-client: fixture
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
Endpoint = vpn.example.invalid:51820
# wg-manager-client: alice
[Peer]
PublicKey = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=
PresharedKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=
AllowedIPs = 192.0.2.3/32
+11
View File
@@ -0,0 +1,11 @@
[Interface]
PrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=
Address = 192.0.2.3/32
DNS = 192.0.2.53,2001:db8::53
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
PresharedKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=
Endpoint = vpn.example.invalid:51820
AllowedIPs = 0.0.0.0/0,::/0
PersistentKeepalive = 25
+9
View File
@@ -0,0 +1,9 @@
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
# Non-secret fixture placeholder; not used against a live host.
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
HELPERS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly HELPERS_DIR
PROJECT_DIR="$(cd -- "${HELPERS_DIR}/.." && pwd)"
readonly PROJECT_DIR
TEST_ROOT=''
TEST_MOCK_LOG=''
TEST_FAILURES=0
TEST_ASSERTIONS=0
setup_test_environment() {
TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/wg-manager-tests.XXXXXX")"
TEST_MOCK_LOG="${TEST_ROOT}/mock-invocations.log"
: >"$TEST_MOCK_LOG"
chmod 700 "$TEST_ROOT"
cp -- "${PROJECT_DIR}/tests/fixtures/wg0.conf" "${TEST_ROOT}/wg0.conf"
export TEST_ROOT TEST_MOCK_LOG
export PATH="${PROJECT_DIR}/tests/mocks/bin:${PATH:-}"
trap cleanup_test_environment EXIT INT TERM
}
cleanup_test_environment() {
local status=$?
trap - EXIT INT TERM
if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then
rm -rf -- "$TEST_ROOT"
fi
return "$status"
}
begin_test() {
printf 'TEST %s\n' "$1"
}
record_failure() {
printf ' FAIL: %s\n' "$1" >&2
((TEST_FAILURES += 1))
}
assert_equal() {
local expected=$1 actual=$2 message=${3:-значения равны}
((TEST_ASSERTIONS += 1))
if [[ "$expected" != "$actual" ]]; then
record_failure "${message}: ожидалось [${expected}], получено [${actual}]"
fi
}
assert_status() {
local expected=$1 actual=$2 message=${3:-код завершения}
assert_equal "$expected" "$actual" "$message"
}
assert_contains() {
local needle=$1 haystack=$2 message=${3:-строка содержит ожидаемый текст}
((TEST_ASSERTIONS += 1))
if [[ "$haystack" != *"$needle"* ]]; then
record_failure "${message}: не найдено [${needle}]"
fi
}
assert_file_exists() {
local path=$1 message=${2:-файл существует}
((TEST_ASSERTIONS += 1))
if [[ ! -e "$path" ]]; then
record_failure "${message}: отсутствует [${path}]"
fi
}
assert_file_absent() {
local path=$1 message=${2:-файл отсутствует}
((TEST_ASSERTIONS += 1))
if [[ -e "$path" ]]; then
record_failure "${message}: найден [${path}]"
fi
}
assert_file_mode() {
local expected=$1 path=$2 actual message
actual=$(stat -c '%a' -- "$path")
message=${3:-режим файла}
assert_equal "$expected" "$actual" "$message"
}
assert_success() {
local status=$1 message=${2:-команда успешна}
assert_status 0 "$status" "$message"
}
assert_failure() {
local status=$1 message=${2:-команда завершилась с ошибкой}
((TEST_ASSERTIONS += 1))
if (( status == 0 )); then
record_failure "$message: ожидался ненулевой код"
fi
}
finish_tests() {
if (( TEST_FAILURES == 0 )); then
printf 'PASS: %d проверок\n' "$TEST_ASSERTIONS"
return 0
fi
printf 'FAIL: %d ошибок из %d проверок\n' "$TEST_FAILURES" "$TEST_ASSERTIONS" >&2
return 1
}
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"
for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done
printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n ${MOCK_FLOCK_CHANGE_TO:-} && -z ${MOCK_FLOCK_CHANGED_MARKER:-} && -f $MOCK_FLOCK_CHANGE_TO && -n ${WG_MANAGER_CONFIG:-} ]]; then
cp -- "$MOCK_FLOCK_CHANGE_TO" "$WG_MANAGER_CONFIG"
export MOCK_FLOCK_CHANGED_MARKER=1
fi
if [[ $# == 2 && $1 == -n && $2 =~ ^[0-9]+$ ]]; then
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
exit 0
fi
if [[ $# -ge 3 && $1 == -n && $2 =~ ^[0-9]+$ && ( $3 == printf || $3 == cat ) ]]; then
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
shift 2
command "$@"
exit $?
fi
if [[ $# == 1 && $1 =~ ^[0-9]+$ || $# == 2 && $1 == -s && $2 =~ ^[0-9]+$ ]]; then
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
real_flock=${REAL_FLOCK:-}
if [[ -z $real_flock ]]; then
for candidate in /usr/bin/flock /bin/flock; do
if [[ -x $candidate ]]; then real_flock=$candidate; break; fi
done
fi
[[ -n $real_flock ]] || exit 98
exec "$real_flock" "$@"
fi
if [[ $# -ge 2 && ( $1 == -s || $1 != -* ) && ${1#-} =~ ^[0-9]+$ || $1 == -s && ${2:-} =~ ^[0-9]+$ ]]; then
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
real_flock=${REAL_FLOCK:-}
if [[ -z $real_flock ]]; then
for candidate in /usr/bin/flock /bin/flock; do
if [[ -x $candidate ]]; then real_flock=$candidate; break; fi
done
fi
[[ -n $real_flock ]] || exit 98
exec "$real_flock" "$@"
fi
exit 99
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"
for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done
printf '\n' >>"$TEST_MOCK_LOG"
if (($# == 1)) && [[ $1 == --version ]]; then printf 'gum version 0.14.0\n'; exit 0; fi
subcommand=${1:-}
if [[ $subcommand == --help || $subcommand == '' ]]; then exit 0; fi
if (($# >= 2)) && [[ ${!#} == --help ]]; then
[[ ${GUM_FAIL_CAPABILITY:-} == "$subcommand" ]] && exit 98
exit 0
fi
case $subcommand in
style)
[[ $# -gt 1 ]] && [[ ${!#} != *'\n'* ]] || exit 98
if [[ -n ${GUM_STYLE_CAPTURE:-} ]]; then printf '%s' "${!#}" >"$GUM_STYLE_CAPTURE"; fi
printf '%s\n' "${!#}"
exit 0
;;
table|pager)
if [[ -n ${GUM_PAGER_CAPTURE:-} ]]; then cat >"$GUM_PAGER_CAPTURE"; else cat >/dev/null; fi
exit 0
;;
spin) exit 0 ;;
choose|filter|input|confirm)
if [[ -n ${GUM_CHOICES_CAPTURE:-} && ( $subcommand == choose || $subcommand == filter ) ]]; then
: >"$GUM_CHOICES_CAPTURE"
if [[ $subcommand == filter ]]; then
shift 3
else
shift
fi
printf '%s\n' "$@" >>"$GUM_CHOICES_CAPTURE"
fi
script=${GUM_SCRIPT_FILE:-}; answer=''
if [[ -n $script && -f $script ]]; then
IFS= read -r answer <"$script" || answer=''
tail -n +2 -- "$script" >"${script}.next"
mv -- "${script}.next" "$script"
fi
case $subcommand in
confirm) [[ $answer == y || $answer == Y ]] || exit 1; exit 0 ;;
input) [[ -n $answer ]] || exit 1; printf '%s\n' "$answer"; exit 0 ;;
*) [[ -n $answer ]] && printf '%s\n' "$answer" || exit 1; exit 0 ;;
esac
;;
*) printf 'Unexpected gum subcommand: %s\n' "$subcommand" >&2; exit 99 ;;
esac
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
if [[ $# == 4 && $1 == link && $2 == show && $3 == dev ]]; then
[[ $4 == ${WG_MOCK_INTERFACE:-wg0} ]] || exit 99
[[ ${DIAG_IP_LINK_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_IP_LINK_OUTPUT:-$4: <UP> mtu 1420}"; exit 0
fi
if [[ $# == 5 && $1 == -o && $2 == addr && $3 == show && $4 == dev ]]; then
[[ $5 == ${WG_MOCK_INTERFACE:-wg0} ]] || exit 99
[[ ${DIAG_IP_ADDR_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_IP_ADDR_OUTPUT:-}"; exit 0
fi
if [[ $# == 6 && $1 == -o && $2 == route && $3 == show && $5 == dev ]]; then
[[ $6 == ${WG_MOCK_INTERFACE:-wg0} ]] || exit 99
[[ ${DIAG_IP_ROUTE_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_IP_ROUTE_OUTPUT:-}"; exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
if [[ $# == 3 && $1 == -t && $2 == nat && $3 == -S ]]; then
[[ ${DIAG_IPTABLES_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_IPTABLES_OUTPUT:-}"; exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
if [[ $# == 5 && $1 == -u && $2 == wg-quick@${WG_MOCK_INTERFACE:-wg0} && $3 == --since && $4 == '24 hours ago' && $5 == --no-pager ]]; then
[[ ${DIAG_JOURNAL_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_JOURNAL_OUTPUT:-}"; exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
if [[ $# == 2 && $1 == list && $2 == ruleset ]]; then
[[ ${DIAG_NFT_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_NFT_OUTPUT:-}"; exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify isolated log}"
printf 'qrencode' >>"$TEST_MOCK_LOG"
for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done
printf '\n' >>"$TEST_MOCK_LOG"
[[ $# == 2 && $1 == -t && $2 == ansiutf8 ]] || { printf 'Unexpected qrencode arguments\n' >&2; exit 99; }
if [[ -n ${QR_MOCK_EXPECTED_FILE:-} ]]; then
[[ -n ${TEST_ROOT:-} && ${QR_MOCK_EXPECTED_FILE} == "${TEST_ROOT}"/* && -f ${QR_MOCK_EXPECTED_FILE} && ! -L ${QR_MOCK_EXPECTED_FILE} ]] || { printf 'Unexpected QR fixture path\n' >&2; exit 98; }
cmp -s -- "$QR_MOCK_EXPECTED_FILE" - || { printf 'Unexpected QR input\n' >&2; exit 98; }
else
printf 'QR expected fixture is required\n' >&2
exit 98
fi
[[ ${QR_MOCK_FAIL:-0} != 1 ]] || { printf 'Mock qrencode failure\n' >&2; exit 97; }
printf 'MOCK-QR\n'
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
if [[ $# == 1 && $1 == -lun ]]; then
[[ ${DIAG_SS_FAIL:-0} == 1 ]] && exit 98
printf '%s\n' "${DIAG_SS_OUTPUT:-}"; exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG"
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
if [[ $# == 3 && $1 == is-active && $2 == -- && $3 == wg-quick@${WG_MOCK_INTERFACE:-wg0} ]]; then
case ${SYSTEMCTL_STATE:-missing} in
active|inactive|failed|unknown) printf '%s\n' "$SYSTEMCTL_STATE"; [[ $SYSTEMCTL_STATE == active || $SYSTEMCTL_STATE == unknown ]] && exit 0 || exit 3 ;;
esac
fi
if [[ $# == 1 && $1 == is-system-running ]]; then
case ${SYSTEMCTL_INIT:-missing} in
running) printf '%s\n' "$SYSTEMCTL_INIT"; exit 0 ;;
degraded) printf '%s\n' "$SYSTEMCTL_INIT"; exit 1 ;;
*) printf '%s\n' "${SYSTEMCTL_INIT:-offline}"; exit 3 ;;
esac
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"
for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done
printf '\n' >>"$TEST_MOCK_LOG"
if [[ $# == 3 && $1 == show && -n ${WG_MOCK_INTERFACE:-} && $2 == "$WG_MOCK_INTERFACE" && $3 == public-key ]]; then
[[ ${WG_MOCK_FAIL_SERVER_PUBLIC_KEY:-0} != 1 ]] || exit 97
printf '%s\n' "${WG_MOCK_SERVER_PUBLIC_KEY_OUTPUT:-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=}"
exit 0
fi
if [[ $# == 1 && $1 == genkey ]]; then
[[ ${WG_MOCK_FAIL_GENKEY:-0} != 1 ]] || exit 97
printf '%s\n' "${WG_MOCK_PRIVATE_KEY:-EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=}"
exit 0
fi
if [[ $# == 1 && $1 == pubkey ]]; then
IFS= read -r private_key
[[ -n $private_key && $private_key =~ ^[A-Za-z0-9+/]{43}=$ ]] || exit 97
if IFS= read -r extra_key; then exit 97; fi
if [[ -n ${WG_MOCK_EXPECTED_PRIVATE_KEY:-} && $private_key != "$WG_MOCK_EXPECTED_PRIVATE_KEY" ]]; then exit 97; fi
[[ ${WG_MOCK_FAIL_PUBKEY:-0} != 1 ]] || exit 97
printf '%s\n' "${WG_MOCK_PUBLIC_KEY:-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=}"
exit 0
fi
if [[ $# == 1 && $1 == genpsk ]]; then
[[ ${WG_MOCK_FAIL_GENPSK:-0} != 1 ]] || exit 97
printf '%s\n' "${WG_MOCK_PSK:-GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=}"
exit 0
fi
if [[ $# == 3 && $1 == show && -n ${WG_MOCK_INTERFACE:-} && $2 == "$WG_MOCK_INTERFACE" && $3 == dump && -z ${WG_MOCK_DUMP_SEQUENCE_DIR:-} && -n ${WG_MOCK_DUMP_FILE:-} && -f $WG_MOCK_DUMP_FILE ]]; then
if [[ -n ${WG_MOCK_DUMP_FAIL_ON_CALL:-} ]]; then
dump_counter=${WG_MOCK_DUMP_FAIL_COUNTER_FILE:-${TEST_ROOT}/dump-counter}
dump_calls=0; [[ -f $dump_counter ]] && read -r dump_calls <"$dump_counter"; ((dump_calls += 1)); printf '%s\n' "$dump_calls" >"$dump_counter"
[[ $dump_calls != "$WG_MOCK_DUMP_FAIL_ON_CALL" ]] || exit 98
fi
cat -- "$WG_MOCK_DUMP_FILE"
exit 0
fi
if [[ $# == 3 && $1 == show && -n ${WG_MOCK_INTERFACE:-} && $2 == "$WG_MOCK_INTERFACE" && $3 == dump && -n ${WG_MOCK_DUMP_SEQUENCE_DIR:-} ]]; then
[[ ${WG_MOCK_DUMP_SEQUENCE_DIR} == "${TEST_ROOT}"/* && -d ${WG_MOCK_DUMP_SEQUENCE_DIR} && ! -L ${WG_MOCK_DUMP_SEQUENCE_DIR} ]] || exit 99
sequence_counter=${WG_MOCK_DUMP_SEQUENCE_DIR}/counter
[[ ! -L "$sequence_counter" && ( -f "$sequence_counter" || ! -e "$sequence_counter" ) ]] || exit 99
sequence_number=0
[[ -f "$sequence_counter" ]] && read -r sequence_number <"$sequence_counter"
((sequence_number += 1))
printf '%s\n' "$sequence_number" >"$sequence_counter"
sequence_file=${WG_MOCK_DUMP_SEQUENCE_DIR}/${sequence_number}
[[ -f "$sequence_file" ]] && cat -- "$sequence_file" && exit 0
printf 'Mock wg dump sequence exhausted\n' >&2
exit 98
fi
if [[ $# == 3 && $1 == syncconf ]]; then
[[ -n ${WG_MOCK_INTERFACE:-} && $2 == "$WG_MOCK_INTERFACE" ]] || { printf 'Mock wg wrong interface\n' >&2; exit 99; }
if [[ ${WG_SYNC_FAIL_ONCE_FILE:-} && ! -e ${WG_SYNC_FAIL_ONCE_FILE} ]]; then
: >"$WG_SYNC_FAIL_ONCE_FILE"
printf 'Mock wg syncconf failure once\n' >&2
exit 97
fi
if [[ -n ${WG_SYNC_FAIL_ON_CALL:-} ]]; then
sync_counter=${WG_SYNC_FAIL_COUNTER_FILE:-${TEST_ROOT}/sync-counter}
sync_calls=0; [[ -f $sync_counter ]] && read -r sync_calls <"$sync_counter"; ((sync_calls += 1)); printf '%s\n' "$sync_calls" >"$sync_counter"
[[ $sync_calls != "$WG_SYNC_FAIL_ON_CALL" ]] || { printf 'Mock wg syncconf failure\n' >&2; exit 97; }
fi
[[ -f $3 && ! -L $3 && ${WG_SYNC_FAIL:-0} != 1 && -n ${TEST_ROOT:-} && $3 == "${TEST_ROOT}"/* ]] || { printf 'Mock wg syncconf failure\n' >&2; exit 97; }
[[ $(stat -c '%a' -- "$3") == 600 ]] || { printf 'Mock wg syncconf unsafe mode\n' >&2; exit 97; }
if [[ -n ${WG_SYNC_EXPECTED_STRIP:-} ]]; then
[[ -f $WG_SYNC_EXPECTED_STRIP ]] || exit 97
cmp -s -- "$3" "$WG_SYNC_EXPECTED_STRIP" || { printf 'Mock wg syncconf unexpected stripped content\n' >&2; exit 97; }
fi
if [[ -n ${WG_SYNC_READY_FIFO:-} ]]; then
[[ ${WG_SYNC_READY_FIFO} == "${TEST_ROOT}"/* && -p ${WG_SYNC_READY_FIFO} && ${WG_SYNC_RELEASE_FIFO:-} == "${TEST_ROOT}"/* && -p ${WG_SYNC_RELEASE_FIFO} ]] || exit 99
printf 'ready\n' >"$WG_SYNC_READY_FIFO"
IFS= read -r _ <"${WG_SYNC_RELEASE_FIFO:?WG_SYNC_RELEASE_FIFO must be set}"
fi
exit 0
fi
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -Eeuo pipefail
command_name=${0##*/}
: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}"
allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-}
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"
for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done
printf '\n' >>"$TEST_MOCK_LOG"
if [[ $# == 2 && $1 == strip ]]; then
[[ -f $2 && ! -L $2 && $(stat -c '%a' -- "$2") == 600 && -n ${TEST_ROOT:-} && $2 == "${TEST_ROOT}"/* ]] || { printf 'Mock wg-quick strip failure\n' >&2; exit 97; }
case ${WG_QUICK_STRIP_FAIL:-0}:${2##*/} in
1:*|staged:*.stage.*|live:wg0.conf) printf 'Mock wg-quick strip failure\n' >&2; exit 97 ;;
esac
if [[ -n ${WG_QUICK_STRIP_OUTPUT:-} && -f $WG_QUICK_STRIP_OUTPUT ]]; then cat -- "$WG_QUICK_STRIP_OUTPUT"; fi
exit 0
fi
if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then
printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2
exit 99
fi
if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then
exit 0
fi
printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2
exit 99
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# shellcheck source=../wg-manager.sh
source "$1"
resolve_gum
Executable
+4485
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# shellcheck source=../wg-manager.sh
source "$1"
printf 'sourced'
+20
View File
@@ -0,0 +1,20 @@
# Safe example configuration. Copy to wg-manager.conf and review before use.
WG_INTERFACE="wg0"
WG_CONF="/etc/wireguard/wg0.conf"
CLIENT_DIR="/etc/wireguard/clients"
SERVER_ENDPOINT="vpn.example.invalid"
SERVER_PORT="51820"
SERVER_VPN_IP="192.0.2.1"
CLIENT_DNS="192.0.2.53"
VPN_SUBNET_PREFIX="192.0.2"
START_HOST="2"
END_HOST="254"
USE_IPV6="0"
IPV6_PREFIX="2001:db8:ffff::"
DEFAULT_CLIENT_MODE="full-tunnel"
RECENT_HANDSHAKE_SECONDS="180"
BACKUP_RETENTION="10"
Executable
+2722
View File
File diff suppressed because it is too large Load Diff