release: v5.15.6 - input validation, atomicity and a JSON status field

A hardening and quality release that finalizes two accumulated workstreams.

Code-audit hardening (manage/awg_common/install):
- modify validates DNS as real IPv4/IPv6; octet-range check on the detected public IP
- --expires validated before client creation; --psk fails closed
- atomic .vpnuri and QR writes via the shared safe-temp helper; one shared client-artifact cleanup
- content-aware expiry cron; restore guards for an empty server/clients backup
- INT/TERM abort the script with exit 130/143 instead of falling through

Machine-readable status (additive, backward-compatible):
- list --json and stats --json also emit a stable status_code enum
  (active | recent | inactive | no_handshake | key_error | no_data);
  the localized status field is kept

Docs and process:
- accurate CLI help (--endpoint FQDN/IPv4/[IPv6], --no-tweaks note), README diagnose/repair-module rows
- ARM coverage for Ubuntu 26.04 (DKMS); docs-check enforces the OS x arch x prebuilt-target matrix
- CI badges, signing-doc cleanup, clearer issue/PR templates

Release mechanics: version and date bumped to 5.15.6 across the six scripts,
helper SHA pins recomputed, CHANGELOG (RU + EN) updated, pinned raw-URL tags and
README badges bumped. Default install behavior and the support matrix are unchanged.
This commit is contained in:
Ivan Bondarev 2026-06-08 14:40:03 +03:00 committed by GitHub
parent 44a0ab0a49
commit 902f4d787b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1181 additions and 193 deletions

View file

@ -95,9 +95,26 @@ body:
- type: textarea
id: diagnostic-output
attributes:
label: Diagnostic output
description: Output from the built-in diagnostic command.
placeholder: "Paste output of: sudo bash install_amneziawg.sh --diagnostic"
label: Installer diagnostic report
description: >-
Run `sudo bash install_amneziawg.sh --diagnostic`. The command prints only
the PATH to a report file; attach the CONTENTS of that file (default
/root/awg/diag_<timestamp>.txt). Mask anything sensitive (public IPs,
ports, routes) before pasting.
placeholder: "Contents of /root/awg/diag_<timestamp>.txt"
render: text
validations:
required: false
- type: textarea
id: runtime-diagnose
attributes:
label: Runtime diagnostics
description: >-
For an already-running server, also attach the output of
`sudo bash /root/awg/manage_amneziawg.sh diagnose` (for mobile-network
problems add `--carrier=NAME`), plus `dkms status`. Mask sensitive values.
placeholder: "Output of: sudo bash /root/awg/manage_amneziawg.sh diagnose"
render: text
validations:
required: false

View file

@ -19,7 +19,7 @@
- [ ] Tested on clean Ubuntu 24.04 LTS VPS (if script changes)
- [ ] Tested on Ubuntu 25.10/26.04 (noble fallback path, if PPA/install logic changes)
- [ ] Tested on Debian 12/13 (if OS-specific changes)
- [ ] CHANGELOG.md **and** CHANGELOG.en.md updated (including `[Unreleased]` comparator link)
- [ ] CHANGELOG.md **and** CHANGELOG.en.md updated (including `[Unreleased]` comparator link), or not applicable (internal/test-only change)
- [ ] SCRIPT_VERSION updated (if releasing new version)
- [ ] SHA pins recomputed and verified (`scripts/update-sha-pins.sh --verify`) (if `awg_common*`/`manage_amneziawg*` changed)
- [ ] Version badge in README.md and README.en.md updated (if version bump)

5
.gitignore vendored
View file

@ -33,3 +33,8 @@ output/
# Secrets
.env
.env.*
# Signing: the minisign offline secret key must never be committed. The signing/
# directory itself is intentionally NOT ignored - Option B lands *.minisig there
# in the tagged commit (see docs/SIGNING_DESIGN.md).
*.key

View file

@ -442,7 +442,7 @@ Options:
--route-all Mode: All traffic (0.0.0.0/0)
--route-amnezia Mode: Amnezia List + DNS (default)
--route-custom=NETS Mode: Only specified networks
--endpoint=IP Specify external IP (for servers behind NAT)
--endpoint=ADDR External server endpoint: FQDN, IPv4 or [IPv6] (NAT)
--preset=TYPE Obfuscation parameter preset: default, mobile
mobile: Jc=3, narrow Jmax — for mobile carriers (Tele2, Yota, Megafon)
--jc=N Set Jc manually (1-128, overrides preset)
@ -450,7 +450,8 @@ Options:
--jmax=N Set Jmax manually (0-1280, overrides preset, must be >= Jmin)
-y, --yes Non-interactive mode (all confirmations auto-yes)
-f, --force Reinstall over a working AWG (ENV: AWG_FORCE_REINSTALL=1)
--no-tweaks Skip hardening/optimization (no UFW, Fail2Ban, sysctl tweaks)
--no-tweaks Skip optional hardening/optimization (UFW, Fail2Ban);
the minimal forwarding sysctl is always applied
```
<a id="manage-cli-adv"></a>
@ -595,7 +596,7 @@ Client keys are stored in `/root/awg/keys/` (permissions 600). Server keys are i
The installer downloads `awg_common.sh` and `manage_amneziawg.sh` from URLs pinned to the specific version tag:
```
https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common.sh
https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common.sh
```
This provides **supply chain pinning**: downloaded scripts match the installer version, even if `main` has already been updated.
@ -615,12 +616,12 @@ To update the management and shared library scripts **without reinstalling the s
```bash
# Russian version:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common.sh
# English version:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common_en.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common_en.sh
# Set permissions
chmod 700 /root/awg/manage_amneziawg.sh /root/awg/awg_common.sh
@ -972,11 +973,14 @@ sudo bash /root/awg/manage_amneziawg.sh stats --json
"rx": 1332477952,
"tx": 374083174,
"last_handshake": 1710312180,
"status": "active"
"status": "Active",
"status_code": "active"
}
]
```
> **Note:** the `status` field is localized (EN `Active` / `Recent` / `Inactive`, RU `Активен` / `Недавно` / `Неактивен`) and is meant for humans. For automation, use the machine-stable `status_code` field - it does not depend on the script language and takes values from a fixed set: `active` (handshake < 3 min), `recent` (< 24 h), `inactive` (stats: no or stale handshake), `no_handshake` (list: no or stale handshake), `key_error` (list: client key not found in the server config), `no_data` (list: not enough data). The `status_code` field is present in both `list --json` and `stats --json`.
---
<a id="expires-adv"></a>
@ -1175,6 +1179,8 @@ Starting with v5.9.0, the installer works on ARM systems alongside x86_64.
2. If a prebuilt `amneziawg.ko` package matching your kernel exists in the [arm-packages release](https://github.com/bivlked/amneziawg-installer/releases/tag/arm-packages), it is downloaded and installed via `dpkg`. This takes 2-3 minutes.
3. If no prebuilt package matches, the installer falls back to DKMS compilation from source. This works on any kernel but takes longer (10-30 min depending on hardware).
> **Prebuilt ARM coverage:** prebuilt packages are built for Raspberry Pi (3/4/5), Ubuntu 24.04/25.10 ARM64 and Debian 12/13 ARM64. Ubuntu 26.04 ARM64 has no prebuilt yet - the module is built from source via DKMS (slower on first install, then works normally).
**Raspberry Pi kernel detection:**
Raspberry Pi Foundation kernels have a `+rpt` suffix in their version string (e.g. `6.12.75+rpt-rpi-v8`). The installer maps this suffix to the correct headers package. Standard Debian/Ubuntu ARM64 kernels use their default headers.

View file

@ -438,7 +438,7 @@ PersistentKeepalive = 33
--route-all Режим: Весь трафик (0.0.0.0/0)
--route-amnezia Режим: Список Amnezia+DNS (умолч.)
--route-custom=СЕТИ Режим: Только указанные сети
--endpoint=IP Указать внешний IP (для серверов за NAT)
--endpoint=АДРЕС Внешний endpoint сервера: FQDN, IPv4 или [IPv6] (для NAT)
--preset=ТИП Набор параметров обфускации: default, mobile
mobile: Jc=3, узкий Jmax — для мобильных операторов (Tele2, Yota, Мегафон)
--jc=N Задать Jc вручную (1-128, поверх preset)
@ -446,7 +446,8 @@ PersistentKeepalive = 33
--jmax=N Задать Jmax вручную (0-1280, поверх preset, ≥ Jmin)
-y, --yes Неинтерактивный режим (все подтверждения auto-yes)
-f, --force Переустановка поверх работающего AWG (ENV: AWG_FORCE_REINSTALL=1)
--no-tweaks Пропустить hardening/оптимизацию (без UFW, Fail2Ban, sysctl tweaks)
--no-tweaks Пропустить необязательный hardening/оптимизацию (UFW,
Fail2Ban); минимальный forwarding-sysctl применяется всегда
```
<a id="manage-cli-adv"></a>
@ -591,7 +592,7 @@ graph TD
Инсталлятор скачивает `awg_common.sh` и `manage_amneziawg.sh` с URL, привязанных к конкретному тегу версии:
```
https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common.sh
https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common.sh
```
Это даёт **supply chain pinning**: скачиваемые скрипты соответствуют версии инсталлятора, даже если `main` уже обновлён.
@ -611,12 +612,12 @@ AWG_BRANCH=my-feature-branch sudo bash ./install_amneziawg.sh
```bash
# Русская версия:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common.sh
# Английская версия:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common_en.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common_en.sh
# Установить права
chmod 700 /root/awg/manage_amneziawg.sh /root/awg/awg_common.sh
@ -968,11 +969,14 @@ sudo bash /root/awg/manage_amneziawg.sh stats --json
"rx": 1332477952,
"tx": 374083174,
"last_handshake": 1710312180,
"status": "active"
"status": "Активен",
"status_code": "active"
}
]
```
> **Примечание:** поле `status` локализовано (RU `Активен` / `Недавно` / `Неактивен`, EN `Active` / `Recent` / `Inactive`) и предназначено для человека. Для автоматизации используйте машинно-стабильное поле `status_code` - оно не зависит от языка скрипта и принимает значения из фиксированного набора: `active` (handshake < 3 мин), `recent` (< 24 ч), `inactive` (stats: handshake не было или давно), `no_handshake` (list: handshake не было или давно), `key_error` (list: ключ клиента не найден в конфиге сервера), `no_data` (list: недостаточно данных). Поле `status_code` присутствует и в `list --json`, и в `stats --json`.
---
<a id="expires-adv"></a>
@ -1171,6 +1175,8 @@ apt-get update && apt-get install -y curl
2. Если в [arm-packages release](https://github.com/bivlked/amneziawg-installer/releases/tag/arm-packages) есть готовый пакет `amneziawg.ko` для вашего ядра, он скачивается и устанавливается через `dpkg`. Это занимает 2-3 минуты.
3. Если готовый пакет не подходит, инсталлятор переключается на DKMS-сборку из исходников. Работает с любым ядром, но занимает больше времени (10-30 мин в зависимости от оборудования).
> **Покрытие готовыми ARM-пакетами:** prebuilt собираются для Raspberry Pi (3/4/5), Ubuntu 24.04/25.10 ARM64 и Debian 12/13 ARM64. Ubuntu 26.04 ARM64 пока без prebuilt - модуль собирается из исходников через DKMS (дольше при первой установке, дальше работает штатно).
**Определение ядра Raspberry Pi:**
Ядра Raspberry Pi Foundation имеют суффикс `+rpt` в строке версии (например, `6.12.75+rpt-rpi-v8`). Инсталлятор маппит этот суффикс на правильный пакет заголовков. Стандартные ядра Debian/Ubuntu ARM64 используют свои обычные заголовки.

View file

@ -14,6 +14,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
## [5.15.6] - 2026-06-08
**v5.15.6** - input validation, atomicity and a JSON status field. This release continues the code-audit hardening cycle: it sharpens input validation in `manage`, makes client-artifact writes atomic, refines interrupt handling, and adds a stable machine-readable `status_code` to `list`/`stats --json`. The default install is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.
### Added
- `list --json` and `stats --json` now emit an extra `status_code` field with a stable, language-independent value (`active` | `recent` | `inactive` | `no_handshake` | `key_error` | `no_data`). The existing localized `status` field is kept - the change is additive and backward-compatible, and convenient for `jq` and automation (the contract is documented in `ADVANCED.en.md`)
### Improved
- `manage modify <client>` validates each DNS entry as a real IPv4/IPv6 address (via the shared `_valid_ipv4`/`_valid_ipv6` helpers) rather than a character set, so values like `abc` or `999.999.999.999` are no longer accepted. The same octet-range check (0-255) now applies to the auto-detected server public IP
- `--expires` duration is validated once before the client is created: an invalid value aborts the command up front without changing anything. A failure to write the expiry after creation is now reflected in a non-zero result and the log
- `--psk` is treated as an explicit request for a key: if the PSK cannot be generated, the command fails instead of silently degrading to a PSK-less client, and no artifacts are created
- `.vpnuri` and the temporary QR PNGs are written through the shared safe-temp mechanism with an atomic `mv`, so an interrupted write cannot leave an empty or truncated file over a working one. Client removal (manual and automatic on expiry) uses one shared helper that covers every client artifact
- The expired-client auto-removal cron is compared by content and refreshed when the working directory changes (for example after a restore), not only when the file is absent
- Restore checks for the server config before stopping the service (an interruption no longer touches a working system), and an empty clients directory is treated as a valid case
- Ctrl+C or a termination signal now aborts the script with the correct exit code (130/143) instead of continuing past the interrupted operation; an interrupted restore still rolls back
### Documentation
- Installer built-in help and the option tables in `ADVANCED.en.md`: `--endpoint` accepts an FQDN, IPv4 or `[IPv6]`; clarified that `--no-tweaks` still applies the minimal forwarding sysctl
- The command tables in `README.en.md` now include `diagnose` and `repair-module`
- ARM support for Ubuntu 26.04 (built via DKMS) is documented; the OS / architecture / prebuilt-package matrix is now checked automatically for consistency
### Tests
- Expanded automated coverage (bats): input validators, file-operation atomicity, signal handling, and the `status_code` contract
---
## [5.15.5] - 2026-06-07
**v5.15.5** - fail2ban bugfix on Ubuntu 24.04 (thanks @stereomonk).
@ -1302,6 +1332,7 @@ Major security and reliability update after several consecutive code audits. The
- Full uninstall (`--uninstall`).
[Unreleased]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.5...HEAD
[5.15.6]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.5...v5.15.6
[5.15.5]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.4...v5.15.5
[5.15.4]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.3...v5.15.4
[5.15.3]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.2...v5.15.3

View file

@ -14,6 +14,36 @@
---
## [5.15.6] - 2026-06-08
**v5.15.6** - проверки ввода, атомарность файловых операций и машиночитаемое поле статуса в JSON. Релиз продолжает цикл закрепления по итогам аудита кода: усилены проверки ввода в `manage`, запись клиентских артефактов сделана атомарной, аккуратнее обрабатываются прерывания, в `list`/`stats --json` добавлено стабильное машиночитаемое поле `status_code`. Поведение установки по умолчанию не меняется. Матрица поддержки прежняя: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.
### Добавлено
- `list --json` и `stats --json` теперь отдают дополнительное поле `status_code` со стабильным, не зависящим от языка значением (`active` | `recent` | `inactive` | `no_handshake` | `key_error` | `no_data`). Прежнее локализованное поле `status` сохранено - изменение аддитивное и обратно совместимое, удобно для `jq` и автоматизации (контракт описан в `ADVANCED.md`)
### Улучшено
- `manage modify <клиент>` проверяет каждый DNS как настоящий IPv4/IPv6-адрес (через общие `_valid_ipv4`/`_valid_ipv6`), а не только набор символов: значения вроде `abc` или `999.999.999.999` больше не принимаются. Та же проверка диапазона октетов (0-255) применяется к автоопределённому публичному IP сервера
- Длительность `--expires` проверяется один раз до создания клиента: при некорректном значении команда прекращается заранее, ничего не меняя. Сбой записи срока после создания теперь явно отражается в коде возврата и логе
- Флаг `--psk` трактуется как явный запрос на ключ: при невозможности сгенерировать PSK команда завершается с ошибкой и не создаёт артефакты вместо тихого перехода к клиенту без PSK
- Запись `.vpnuri` и временных PNG для QR-кодов идёт через общий механизм безопасных временных файлов с атомарным `mv`, поэтому прерывание в момент записи не оставит пустой или усечённый файл. Удаление клиента (ручное и автоматическое по истечении срока) использует единый помощник, охватывающий все артефакты клиента
- Cron автоудаления истёкших клиентов сверяется по содержимому и обновляется при смене рабочего каталога (например, после восстановления из бэкапа), а не только по факту наличия файла
- Восстановление из бэкапа проверяет наличие серверного конфига до остановки сервиса (прерывание не трогает рабочую систему), а пустой каталог клиентов воспринимается как корректный сценарий
- Прерывание по Ctrl+C или сигналу завершает скрипт с корректным кодом (130/143), не продолжая выполнение после прерванной операции; восстановление из бэкапа при прерывании по-прежнему откатывается
### Документация
- Встроенная справка установщика и таблицы опций в `ADVANCED.md`: `--endpoint` принимает FQDN, IPv4 или `[IPv6]`; уточнено, что `--no-tweaks` всё равно применяет минимальный forwarding-sysctl
- Таблицы команд в `README.md` дополнены `diagnose` и `repair-module`
- Описана поддержка ARM для Ubuntu 26.04 (сборка через DKMS); согласованность матрицы ОС, архитектур и готовых пакетов теперь проверяется автоматически
### Тесты
- Расширено покрытие автотестами (bats): валидаторы ввода, атомарность файловых операций, обработка сигналов, контракт поля `status_code`
---
## [5.15.5] - 2026-06-07
**v5.15.5** - багфикс fail2ban на Ubuntu 24.04 (спасибо @stereomonk).
@ -1302,6 +1332,7 @@ Hardening-фиксы надёжности и безопасности по ре
- Полная деинсталляция (`--uninstall`).
[Unreleased]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.5...HEAD
[5.15.6]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.5...v5.15.6
[5.15.5]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.4...v5.15.5
[5.15.4]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.3...v5.15.4
[5.15.3]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.2...v5.15.3

View file

@ -52,6 +52,8 @@ Before submitting a PR, ensure:
done
```
> Note: these two loops cover the six runtime scripts as a quick local shortcut. CI (`shellcheck.yml`) and `scripts/preflight-check.sh` check a wider scope - all tracked `*.sh`, including `scripts/*.sh` and `tests/`. The full local gate is `BASE_REF=origin/main bash scripts/preflight-check.sh`.
3. **Unit tests (bats-core)** pass. The suite grows every release, so there is no fixed count to match - run `bats tests/` and make sure the full suite is green before opening a PR.
```bash
bats tests/

View file

@ -41,7 +41,7 @@ sudo ufw allow <your-ssh-port>/tcp
Then:
```bash
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh
```

View file

@ -20,10 +20,12 @@
<img src="https://img.shields.io/badge/Architecture-x86__64_|_ARM64_|_ARMv7-green" alt="x86_64 | ARM64 | ARMv7">
<a href="https://github.com/bivlked/amneziawg-installer/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bivlked/amneziawg-installer" alt="License"></a>
<img src="https://img.shields.io/badge/Status-Stable-success" alt="Status">
<a href="https://github.com/bivlked/amneziawg-installer/releases"><img src="https://img.shields.io/badge/Installer_Version-5.15.5-blue" alt="Version"></a>
<a href="https://github.com/bivlked/amneziawg-installer/releases"><img src="https://img.shields.io/badge/Installer_Version-5.15.6-blue" alt="Version"></a>
<img src="https://img.shields.io/badge/AmneziaWG-2.0-blueviolet" alt="AWG 2.0">
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/shellcheck.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/shellcheck.yml/badge.svg" alt="ShellCheck"></a>
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/test.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/docs-check.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/docs-check.yml/badge.svg" alt="Docs Check"></a>
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/arm-build.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/arm-build.yml/badge.svg" alt="ARM Build"></a>
<a href="https://github.com/bivlked/amneziawg-installer/stargazers"><img src="https://img.shields.io/github/stars/bivlked/amneziawg-installer?style=flat" alt="Stars"></a>
<a href="https://github.com/bivlked/amneziawg-installer/network/members"><img src="https://img.shields.io/github/forks/bivlked/amneziawg-installer?style=flat" alt="Forks"></a>
<img src="https://img.shields.io/github/last-commit/bivlked/amneziawg-installer" alt="Last commit">
@ -119,7 +121,7 @@ This installer is the headless SSH path: minimum footprint, no web panel, kernel
> 📘 **Full deployment guide:** [Install AmneziaWG VPN server on Ubuntu/Debian VPS](INSTALL_VPS.md) - covers VPS choice, ARM, troubleshooting, and uninstall.
```bash
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh
```
@ -254,8 +256,8 @@ This installation method handles interactive prompts and colored output correctl
2. **Download the script:**
```bash
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
# or: curl -fLo install_amneziawg_en.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
# or: curl -fLo install_amneziawg_en.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
```
> Minimal Debian may not ship curl (wget is usually present) - use `wget`. The installer itself adds curl in step 1.
3. **Make it executable:**
@ -270,7 +272,7 @@ This installation method handles interactive prompts and colored output correctl
> **Russian version:** For Russian output, use `install_amneziawg.sh`:
> ```bash
> wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg.sh
> wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg.sh
> sudo bash ./install_amneziawg.sh
> ```
> The Russian version is functionally identical; only user-facing messages and logs are in Russian.
@ -373,6 +375,8 @@ sudo bash /root/awg/manage_amneziawg.sh <command> [arguments]
| `stats` | `[--json]` | Per-client traffic statistics | No |
| `show` | | Run `awg show` | No |
| `check` | | Check server status | No |
| `diagnose`| `[--carrier=NAME]` | Diagnostics (opt. per carrier) | No |
| `repair-module` | | Rebuild kernel module (DKMS) | Yes |
| `restart` | | Restart AmneziaWG service | - |
> **💡 Note:** `add` and `remove` commands auto-apply changes via `awg syncconf` — no service restart needed.
@ -381,11 +385,11 @@ sudo bash /root/awg/manage_amneziawg.sh <command> [arguments]
```bash
# Installation (English)
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh # Run (+ 2 reboots)
# Installation (Russian)
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg.sh
sudo bash ./install_amneziawg.sh # Run (+ 2 reboots)
# Client management
@ -460,13 +464,13 @@ For the roadmap and priorities, see **[docs/ROADMAP.md](docs/ROADMAP.md)**.
<b>A:</b> Download the updated scripts and replace them on the server:
<pre>
# English version:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common_en.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common_en.sh
chmod 700 /root/awg/manage_amneziawg.sh /root/awg/awg_common.sh
# Russian version:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common.sh
chmod 700 /root/awg/manage_amneziawg.sh /root/awg/awg_common.sh
</pre>
Server reinstallation is not required.

View file

@ -20,10 +20,12 @@
<img src="https://img.shields.io/badge/Architecture-x86__64_|_ARM64_|_ARMv7-green" alt="x86_64 | ARM64 | ARMv7">
<a href="https://github.com/bivlked/amneziawg-installer/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bivlked/amneziawg-installer" alt="License"></a>
<img src="https://img.shields.io/badge/Status-Stable-success" alt="Status">
<a href="https://github.com/bivlked/amneziawg-installer/releases"><img src="https://img.shields.io/badge/Installer_Version-5.15.5-blue" alt="Version"></a>
<a href="https://github.com/bivlked/amneziawg-installer/releases"><img src="https://img.shields.io/badge/Installer_Version-5.15.6-blue" alt="Version"></a>
<img src="https://img.shields.io/badge/AmneziaWG-2.0-blueviolet" alt="AWG 2.0">
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/shellcheck.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/shellcheck.yml/badge.svg" alt="ShellCheck"></a>
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/test.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/docs-check.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/docs-check.yml/badge.svg" alt="Docs Check"></a>
<a href="https://github.com/bivlked/amneziawg-installer/actions/workflows/arm-build.yml"><img src="https://github.com/bivlked/amneziawg-installer/actions/workflows/arm-build.yml/badge.svg" alt="ARM Build"></a>
<a href="https://github.com/bivlked/amneziawg-installer/stargazers"><img src="https://img.shields.io/github/stars/bivlked/amneziawg-installer?style=flat" alt="Stars"></a>
<a href="https://github.com/bivlked/amneziawg-installer/network/members"><img src="https://img.shields.io/github/forks/bivlked/amneziawg-installer?style=flat" alt="Forks"></a>
<img src="https://img.shields.io/github/last-commit/bivlked/amneziawg-installer" alt="Last commit">
@ -119,7 +121,7 @@
> 📘 **Полный гайд по развёртыванию (EN):** [Install AmneziaWG VPN server on Ubuntu/Debian VPS](INSTALL_VPS.md) - выбор VPS, ARM, troubleshooting, удаление.
```bash
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg.sh
chmod +x install_amneziawg.sh
sudo bash ./install_amneziawg.sh
```
@ -254,8 +256,8 @@ sudo bash ./install_amneziawg.sh --yes --route-all
2. **Скачайте скрипт:**
```bash
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg.sh
# или: curl -fLo install_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg.sh
# или: curl -fLo install_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg.sh
```
> На минимальном Debian curl может отсутствовать (wget обычно есть) - используйте `wget`. Сам curl установщик доставит на шаге 1.
3. **Сделайте его исполняемым:**
@ -270,7 +272,7 @@ sudo bash ./install_amneziawg.sh --yes --route-all
> **English version:** Для вывода на английском используйте `install_amneziawg_en.sh`:
> ```bash
> wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
> wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
> sudo bash ./install_amneziawg_en.sh
> ```
> Английская версия функционально идентична; только сообщения и логи на английском.
@ -373,6 +375,8 @@ sudo bash /root/awg/manage_amneziawg.sh <команда> [аргументы]
| `stats` | `[--json]` | Статистика трафика по клиентам | Нет |
| `show` | | Статус `awg show` | Нет |
| `check` | | Проверка состояния сервера | Нет |
| `diagnose`| `[--carrier=ИМЯ]` | Диагностика (опц. под оператора) | Нет |
| `repair-module` | | Пересобрать модуль ядра (DKMS) | Да |
| `restart` | | Перезапуск сервиса AmneziaWG | - |
> **💡 Примечание:** Команды `add` и `remove` автоматически применяют изменения через `awg syncconf` — перезапуск сервиса не требуется.
@ -381,11 +385,11 @@ sudo bash /root/awg/manage_amneziawg.sh <команда> [аргументы]
```bash
# Установка (русский)
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg.sh
sudo bash ./install_amneziawg.sh # Запуск (+ 2 перезагрузки)
# Установка (English)
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/install_amneziawg_en.sh
wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh # Запуск (+ 2 перезагрузки)
# Управление клиентами
@ -460,13 +464,13 @@ sudo bash /root/awg/manage_amneziawg.sh restart # Перезапу
<b>О:</b> Скачайте новый скрипт установки и замените скрипты управления на сервере:
<pre>
# Русская версия:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common.sh
chmod 700 /root/awg/manage_amneziawg.sh /root/awg/awg_common.sh
# Английская версия:
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.5/awg_common_en.sh
wget -O /root/awg/manage_amneziawg.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/manage_amneziawg_en.sh
wget -O /root/awg/awg_common.sh https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.15.6/awg_common_en.sh
chmod 700 /root/awg/manage_amneziawg.sh /root/awg/awg_common.sh
</pre>
Переустановка сервера не требуется.

View file

@ -3,8 +3,8 @@
# ==============================================================================
# Общая библиотека функций для AmneziaWG 2.0
# Автор: @bivlked
# Версия: 5.15.5
# Дата: 2026-06-07
# Версия: 5.15.6
# Дата: 2026-06-08
# Репозиторий: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
#
@ -188,7 +188,7 @@ get_server_public_ip() {
https://ipinfo.io/ip
do
ip=$(curl -4 -sf --max-time 5 "$svc" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ -n "$ip" ]] && _valid_ipv4 "$ip"; then
_CACHED_PUBLIC_IP="$ip"
# Observability: write trace to LOG_FILE directly. Never to stdout
# (the function's stdout IS the IP; any extra bytes corrupt the
@ -223,7 +223,7 @@ _try_local_ip() {
| cut -d/ -f1 \
| grep -v '^127\.' \
| head -1)
[[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
{ [[ -n "$ip" ]] && _valid_ipv4 "$ip"; } || return 1
echo "$ip"
return 0
}
@ -1437,10 +1437,12 @@ generate_qr() {
fi
# C4: генерируем во временный файл и атомарно переносим (mv) - чтобы
# прерывание qrencode не оставило частичный/битый PNG поверх рабочего
# (как уже сделано в generate_qr_vpnuri). tmp лежит в той же папке, что и
# png_file, поэтому mv - атомарный rename на одной ФС.
local tmp_png="${png_file}.tmp.$$"
# прерывание qrencode не оставило частичный/битый PNG поверх рабочего.
# awg_mktemp "$AWG_DIR" кладёт tmp в ту же папку (mv = атомарный rename на
# одной ФС) И регистрирует его в общем cleanup-реестре, поэтому SIGKILL
# между qrencode и mv не оставит осиротевший tmp.
local tmp_png
tmp_png=$(awg_mktemp "$AWG_DIR") || { log_error "Ошибка mktemp для QR '$name'"; return 1; }
if ! qrencode -t png -o "$tmp_png" < "$conf_file"; then
log_error "Ошибка генерации QR-кода для '$name'"
rm -f "$tmp_png"
@ -1618,8 +1620,17 @@ generate_vpn_uri() {
fi
rm -f "$perl_err"
echo "$vpn_uri" > "$uri_file"
chmod 600 "$uri_file"
# Пишем через tmp + atomic mv (как .conf/.png), чтобы обрыв записи не оставил
# пустой/обрезанный .vpnuri поверх рабочего.
local _uri_tmp
_uri_tmp=$(awg_mktemp "$AWG_DIR") || { log_error "Ошибка mktemp для vpn:// URI '$name'"; return 1; }
printf '%s\n' "$vpn_uri" > "$_uri_tmp" || { rm -f "$_uri_tmp"; log_error "Ошибка записи vpn:// URI для '$name'"; return 1; }
chmod 600 "$_uri_tmp"
if ! mv -f "$_uri_tmp" "$uri_file"; then
rm -f "$_uri_tmp"
log_error "Ошибка сохранения vpn:// URI для '$name'"
return 1
fi
log_debug "vpn:// URI для '$name' создан: $uri_file"
return 0
}
@ -1634,7 +1645,7 @@ generate_qr_vpnuri() {
local name="$1"
local uri_file="$AWG_DIR/${name}.vpnuri"
local png_file="$AWG_DIR/${name}.vpnuri.png"
local tmp_png="${png_file}.tmp.$$"
local tmp_png
if [[ ! -f "$uri_file" ]]; then
log_error "vpn:// URI для '$name' не найден: $uri_file"
@ -1646,6 +1657,9 @@ generate_qr_vpnuri() {
return 1
fi
# tmp через awg_mktemp (общий cleanup-реестр + atomic mv в той же ФС).
tmp_png=$(awg_mktemp "$AWG_DIR") || { log_error "Ошибка mktemp для QR vpn:// '$name'"; return 1; }
# Флаги qrencode для длинных vpn:// URI с PSK (issue #72):
# -s 6 размер модуля 6 пикселей вместо дефолтных 3 - это и есть основной фикс.
# На дефолтном масштабе модули были слишком мелкими, чтобы камера iPhone
@ -1682,6 +1696,17 @@ _rollback_client_artifacts() {
rm -f "$KEYS_DIR/$1.private" "$KEYS_DIR/$1.public" "$AWG_DIR/$1.conf"
}
# Полный набор клиентских артефактов (conf/png/vpnuri/vpnuri.png + ключи).
# Единый список для `manage remove` и автоудаления истёкших, чтобы пути не
# расходились (раньше expiry-cleanup забывал .vpnuri.png). НЕ трогает expiry-метку
# и cron - это делает вызывающий (remove_client_expiry / rm "$efile").
_remove_client_files() {
local name="$1"
rm -f "$AWG_DIR/${name}.conf" "$AWG_DIR/${name}.png" \
"$AWG_DIR/${name}.vpnuri" "$AWG_DIR/${name}.vpnuri.png" \
"$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
}
# Полный цикл создания клиента:
# keypair → next IP → client config → add peer → QR
# generate_client <name> [endpoint]
@ -1707,9 +1732,12 @@ generate_client() {
# Опциональный PresharedKey: "auto" → `awg genpsk`, иначе используем
# переданное значение как есть. Пустое/unset → без PSK.
if [[ "${CLIENT_PSK:-}" == "auto" ]]; then
# --psk запрошен явно: при сбое awg genpsk НЕ деградируем молча в клиента
# без PSK (это ослабило бы запрошенную безопасность). Fail-closed; здесь
# ещё нет созданных артефактов (ключи/конфиг создаются ниже), откат не нужен.
CLIENT_PSK=$(awg genpsk) || {
log_warn "awg genpsk не сработал — клиент будет создан без PresharedKey"
CLIENT_PSK=""
log_error "awg genpsk не сработал - клиент с PresharedKey (--psk) НЕ создан. Повторите."
return 1
}
fi
@ -2120,7 +2148,7 @@ validate_awg_config() {
# ==============================================================================
EXPIRY_DIR="${AWG_DIR}/expiry"
EXPIRY_CRON="/etc/cron.d/awg-expiry"
EXPIRY_CRON="${EXPIRY_CRON:-/etc/cron.d/awg-expiry}"
# Парсинг длительности в секунды: 1h, 12h, 1d, 7d, 30d
# parse_duration <duration_string>
@ -2250,8 +2278,7 @@ check_expired_clients() {
if [[ $now -ge $expires_at ]]; then
log "Клиент '$name' истёк. Удаление..."
if remove_peer_from_server "$name" 2>/dev/null; then
rm -f "$AWG_DIR/$name.conf" "$AWG_DIR/$name.png" "$AWG_DIR/$name.vpnuri"
rm -f "$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
_remove_client_files "$name"
rm -f "$efile"
log "Клиент '$name' удалён (истёк)."
((removed++))
@ -2273,19 +2300,38 @@ check_expired_clients() {
# Установка cron-задачи для автоудаления
install_expiry_cron() {
if [[ -f "$EXPIRY_CRON" ]]; then
log_debug "Cron-задача expiry уже установлена."
return 0
fi
cat > "$EXPIRY_CRON" << CRONEOF
# AmneziaWG client expiry check — every 5 minutes
# Идемпотентность по СОДЕРЖИМОМУ, не по факту существования файла. Раньше
# ранний выход «файл есть» оставлял stale-пути после restore/переноса/
# --conf-dir: cron продолжал смотреть в старый AWG_DIR. Генерируем ожидаемый
# текст и заменяем файл, только если он отличается.
local _cron_tmp
_cron_tmp=$(awg_mktemp "$(dirname "$EXPIRY_CRON")") || { log_error "Ошибка mktemp для cron expiry"; return 1; }
# Проверяем успех записи ДО cmp/mv: иначе сбой (диск/права) мог бы атомарно
# заменить рабочий cron пустым/частичным tmp.
if ! cat > "$_cron_tmp" << CRONEOF
# AmneziaWG client expiry check - every 5 minutes
AWG_DIR="${AWG_DIR}"
CONFIG_FILE="${CONFIG_FILE}"
SERVER_CONF_FILE="${SERVER_CONF_FILE}"
*/5 * * * * root /bin/bash -c 'source "${AWG_DIR}/awg_common.sh" || exit 1; check_expired_clients' >> "${AWG_DIR}/expiry.log" 2>&1
CRONEOF
chmod 644 "$EXPIRY_CRON"
log "Cron-задача expiry установлена: $EXPIRY_CRON"
then
rm -f "$_cron_tmp"
log_error "Ошибка записи cron-задачи expiry"
return 1
fi
if [[ -f "$EXPIRY_CRON" ]] && cmp -s "$_cron_tmp" "$EXPIRY_CRON"; then
rm -f "$_cron_tmp"
log_debug "Cron-задача expiry уже актуальна."
return 0
fi
chmod 644 "$_cron_tmp"
if ! mv -f "$_cron_tmp" "$EXPIRY_CRON"; then
rm -f "$_cron_tmp"
log_error "Ошибка установки cron-задачи expiry: $EXPIRY_CRON"
return 1
fi
log "Cron-задача expiry установлена/обновлена: $EXPIRY_CRON"
}
# Удаление expiry-данных клиента

View file

@ -3,8 +3,8 @@
# ==============================================================================
# Shared function library for AmneziaWG 2.0
# Author: @bivlked
# Version: 5.15.5
# Date: 2026-06-07
# Version: 5.15.6
# Date: 2026-06-08
# Repository: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
#
@ -189,7 +189,7 @@ get_server_public_ip() {
https://ipinfo.io/ip
do
ip=$(curl -4 -sf --max-time 5 "$svc" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ -n "$ip" ]] && _valid_ipv4 "$ip"; then
_CACHED_PUBLIC_IP="$ip"
# Observability: write trace to LOG_FILE directly. Never to stdout
# (the function's stdout IS the IP; any extra bytes corrupt the
@ -224,7 +224,7 @@ _try_local_ip() {
| cut -d/ -f1 \
| grep -v '^127\.' \
| head -1)
[[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
{ [[ -n "$ip" ]] && _valid_ipv4 "$ip"; } || return 1
echo "$ip"
return 0
}
@ -1442,10 +1442,12 @@ generate_qr() {
fi
# C4: generate into a temp file and move it into place atomically, so an
# interrupted qrencode cannot leave a partial/corrupt PNG over the working
# one (as generate_qr_vpnuri already does). tmp sits in the same directory
# as png_file, so mv is an atomic rename on one filesystem.
local tmp_png="${png_file}.tmp.$$"
# interrupted qrencode cannot leave a partial/corrupt PNG over the working one.
# awg_mktemp "$AWG_DIR" puts the tmp in the same directory (mv = atomic rename
# on one filesystem) AND registers it in the shared cleanup registry, so a
# SIGKILL between qrencode and mv leaves no orphan tmp.
local tmp_png
tmp_png=$(awg_mktemp "$AWG_DIR") || { log_error "mktemp error for QR '$name'"; return 1; }
if ! qrencode -t png -o "$tmp_png" < "$conf_file"; then
log_error "Failed to generate QR code for '$name'"
rm -f "$tmp_png"
@ -1624,8 +1626,17 @@ generate_vpn_uri() {
fi
rm -f "$perl_err"
echo "$vpn_uri" > "$uri_file"
chmod 600 "$uri_file"
# Write via tmp + atomic mv (like .conf/.png) so an interrupted write never
# leaves an empty/truncated .vpnuri on top of a working one.
local _uri_tmp
_uri_tmp=$(awg_mktemp "$AWG_DIR") || { log_error "mktemp error for vpn:// URI '$name'"; return 1; }
printf '%s\n' "$vpn_uri" > "$_uri_tmp" || { rm -f "$_uri_tmp"; log_error "Error writing vpn:// URI for '$name'"; return 1; }
chmod 600 "$_uri_tmp"
if ! mv -f "$_uri_tmp" "$uri_file"; then
rm -f "$_uri_tmp"
log_error "Error saving vpn:// URI for '$name'"
return 1
fi
log_debug "vpn:// URI for '$name' created: $uri_file"
return 0
}
@ -1640,7 +1651,7 @@ generate_qr_vpnuri() {
local name="$1"
local uri_file="$AWG_DIR/${name}.vpnuri"
local png_file="$AWG_DIR/${name}.vpnuri.png"
local tmp_png="${png_file}.tmp.$$"
local tmp_png
if [[ ! -f "$uri_file" ]]; then
log_error "vpn:// URI for '$name' not found: $uri_file"
@ -1652,6 +1663,9 @@ generate_qr_vpnuri() {
return 1
fi
# tmp via awg_mktemp (shared cleanup registry + atomic mv on the same FS).
tmp_png=$(awg_mktemp "$AWG_DIR") || { log_error "mktemp error for vpn:// QR '$name'"; return 1; }
# qrencode flags for long vpn:// URIs with PSK (issue #72):
# -s 6 module size of 6 pixels instead of the default 3 - this is the real fix.
# At the default scale modules were too small for the iPhone camera to
@ -1689,6 +1703,17 @@ _rollback_client_artifacts() {
rm -f "$KEYS_DIR/$1.private" "$KEYS_DIR/$1.public" "$AWG_DIR/$1.conf"
}
# Full set of client artifacts (conf/png/vpnuri/vpnuri.png + keys). A single
# list for `manage remove` and expired-client auto-removal so the paths do not
# diverge (expiry-cleanup used to forget .vpnuri.png). Does NOT touch the expiry
# marker or cron - the caller does that (remove_client_expiry / rm "$efile").
_remove_client_files() {
local name="$1"
rm -f "$AWG_DIR/${name}.conf" "$AWG_DIR/${name}.png" \
"$AWG_DIR/${name}.vpnuri" "$AWG_DIR/${name}.vpnuri.png" \
"$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
}
# Full client creation cycle:
# keypair -> next IP -> client config -> add peer -> QR
# generate_client <name> [endpoint]
@ -1713,9 +1738,13 @@ generate_client() {
# Optional PresharedKey: "auto" -> `awg genpsk`, otherwise use the
# given value as-is. Empty/unset -> no PSK.
if [[ "${CLIENT_PSK:-}" == "auto" ]]; then
# --psk was requested explicitly: on awg genpsk failure do NOT silently
# degrade to a PSK-less client (that would weaken the requested security).
# Fail-closed; no artifacts exist yet (keys/config are created below), so
# no rollback is needed.
CLIENT_PSK=$(awg genpsk) || {
log_warn "awg genpsk failed — client will be created without PresharedKey"
CLIENT_PSK=""
log_error "awg genpsk failed - client with PresharedKey (--psk) NOT created. Please retry."
return 1
}
fi
@ -2127,7 +2156,7 @@ validate_awg_config() {
# ==============================================================================
EXPIRY_DIR="${AWG_DIR}/expiry"
EXPIRY_CRON="/etc/cron.d/awg-expiry"
EXPIRY_CRON="${EXPIRY_CRON:-/etc/cron.d/awg-expiry}"
# Parse duration string to seconds: 1h, 12h, 1d, 7d, 30d
# parse_duration <duration_string>
@ -2257,8 +2286,7 @@ check_expired_clients() {
if [[ $now -ge $expires_at ]]; then
log "Client '$name' expired. Removing..."
if remove_peer_from_server "$name" 2>/dev/null; then
rm -f "$AWG_DIR/$name.conf" "$AWG_DIR/$name.png" "$AWG_DIR/$name.vpnuri"
rm -f "$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
_remove_client_files "$name"
rm -f "$efile"
log "Client '$name' removed (expired)."
((removed++))
@ -2280,19 +2308,38 @@ check_expired_clients() {
# Install cron job for auto-removal
install_expiry_cron() {
if [[ -f "$EXPIRY_CRON" ]]; then
log_debug "Expiry cron job already installed."
return 0
fi
cat > "$EXPIRY_CRON" << CRONEOF
# AmneziaWG client expiry check — every 5 minutes
# Idempotent by CONTENT, not by file existence. The old early-out on "file
# exists" left stale paths after restore/migration/--conf-dir: the cron kept
# pointing at the old AWG_DIR. Generate the expected text and replace the file
# only when it differs.
local _cron_tmp
_cron_tmp=$(awg_mktemp "$(dirname "$EXPIRY_CRON")") || { log_error "mktemp error for expiry cron"; return 1; }
# Check the write succeeded BEFORE cmp/mv: otherwise a failure (disk/perms)
# could atomically replace a working cron with an empty/partial tmp.
if ! cat > "$_cron_tmp" << CRONEOF
# AmneziaWG client expiry check - every 5 minutes
AWG_DIR="${AWG_DIR}"
CONFIG_FILE="${CONFIG_FILE}"
SERVER_CONF_FILE="${SERVER_CONF_FILE}"
*/5 * * * * root /bin/bash -c 'source "${AWG_DIR}/awg_common.sh" || exit 1; check_expired_clients' >> "${AWG_DIR}/expiry.log" 2>&1
CRONEOF
chmod 644 "$EXPIRY_CRON"
log "Expiry cron job installed: $EXPIRY_CRON"
then
rm -f "$_cron_tmp"
log_error "Error writing expiry cron job"
return 1
fi
if [[ -f "$EXPIRY_CRON" ]] && cmp -s "$_cron_tmp" "$EXPIRY_CRON"; then
rm -f "$_cron_tmp"
log_debug "Expiry cron job already current."
return 0
fi
chmod 644 "$_cron_tmp"
if ! mv -f "$_cron_tmp" "$EXPIRY_CRON"; then
rm -f "$_cron_tmp"
log_error "Error installing expiry cron job: $EXPIRY_CRON"
return 1
fi
log "Expiry cron job installed/updated: $EXPIRY_CRON"
}
# Remove client expiry data

View file

@ -89,7 +89,12 @@ Also confirm by hand before tagging:
- The README version badge, `SCRIPT_VERSION`, and the newest changelog heading
all agree.
- Ubuntu / Debian support matrix is current everywhere it is stated (README,
installer `--help`, issue template).
installer `--help`, issue template). When adding a new OS version, decide its
ARM story explicitly: either add a prebuilt target (an `arm-build.yml` matrix
entry plus the mapping in `_try_install_prebuilt_arm`) or keep it DKMS-only and
document that in INSTALL_VPS.md / ADVANCED. `check-docs-consistency.sh` fails
if a supported Ubuntu version has no ARM prebuilt target and is not marked
DKMS-only (e.g. Ubuntu 26.04 ARM64 is DKMS-only today).
- The public roadmap issue
([#79](https://github.com/bivlked/amneziawg-installer/issues/79)) reflects the
release: shipped items move to "Recently shipped" and leave the plan sections,
@ -123,10 +128,20 @@ A tag push (`git push origin vX.Y.Z`) triggers two independent workflows:
recording the installer tag, the exact upstream commit, the kernel version and
the SHA256, so the mutable `arm-packages` assets stay auditable.
The release is not finished until both runs are green. After they are, open the
rendered README on GitHub and confirm the install and update one-liners point at
the new tag - a final guard against a stale raw-URL slipping past the automated
check.
The release is not finished until both runs are green. Then run this post-publish
health check:
- The Releases page shows the new tag as "Latest", and that tag equals
`SCRIPT_VERSION` and the README version badge.
- The `arm-packages` release was refreshed by `arm-build.yml` (its assets carry
the new installer tag in their `.manifest.json`).
- Open the rendered README on GitHub and confirm the install and update
one-liners point at the new tag - a final guard against a stale raw-URL
slipping past the automated check.
Note: `release.yml` only depends on the preflight gate, so the GitHub Release can
appear before `arm-build.yml` finishes. For a release that advertises ARM
prebuilt packages, wait for the ARM run to go green before announcing.
If the preflight gate fails on a pushed tag, the release is not published.
Delete the tag (`git push origin :refs/tags/vX.Y.Z` and `git tag -d vX.Y.Z`),

View file

@ -94,7 +94,7 @@ Pros: zero CI changes, signatures generated on the trusted maintainer machine. C
### Option B: CI uploads signatures generated locally (asymmetric)
Maintainer generates `*.minisig` files locally, commits them transiently to a `signing/` directory (gitignored elsewhere), tags. A separate, manually dispatched workflow reads them and uploads as assets. Same trust model as Option A - just automates the upload step.
Maintainer generates `*.minisig` files locally, commits them transiently to a `signing/` directory (tracked, so they land in the tagged commit - `signing/` is intentionally NOT gitignored; remove them in a follow-up commit once the dispatch workflow has uploaded them), tags. A separate, manually dispatched workflow reads them and uploads as assets. Same trust model as Option A - just automates the upload step.
The signing of the files NEVER happens in GitHub Actions. The private key is never exposed to Actions. This is intentional and the whole point.
@ -136,7 +136,7 @@ Activation steps, in order:
4. Add README section "Verifying releases" with placeholder link to this design doc. TODO - add when the public key is published as `KEYS.txt` so the section is actionable, not vapor.
5. Add the draft workflow `docs/release-sign.yml.draft` for review. DONE.
6. After keypair exists and is published as `KEYS.txt`:
a. Move `docs/release-sign.yml.draft` to `.github/workflows/release-sign.yml`.
a. Refresh all action pins in the draft to match the active workflows (e.g. `actions/checkout`), then move `docs/release-sign.yml.draft` to `.github/workflows/release-sign.yml`.
b. Test on a pre-release tag (e.g., `vX.Y.Z-rc1`).
c. Flip the README section from "planned" to "active".
7. Optional follow-up: SBOM generation via `syft` or GitHub's native dependency graph (a separate, smaller task).

View file

@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.tag }}

View file

@ -8,15 +8,15 @@ fi
# ==============================================================================
# Скрипт для установки и настройки AmneziaWG 2.0 на Ubuntu/Debian серверах
# Автор: @bivlked
# Версия: 5.15.5
# Дата: 2026-06-07
# Версия: 5.15.6
# Дата: 2026-06-08
# Репозиторий: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Безопасный режим и Константы ---
set -o pipefail
SCRIPT_VERSION="5.15.5"
SCRIPT_VERSION="5.15.6"
AWG_DIR="/root/awg"
CONFIG_FILE="$AWG_DIR/awgsetup_cfg.init"
STATE_FILE="$AWG_DIR/setup_state"
@ -33,8 +33,8 @@ MANAGE_SCRIPT_PATH="$AWG_DIR/manage_amneziawg.sh"
# Проверяются в step5_download_scripts() после curl.
# Если AWG_BRANCH переопределён (не v$SCRIPT_VERSION), проверка пропускается.
# Формат: sha256sum output (hex, 64 chars).
COMMON_SCRIPT_SHA256="e02f9b942b1586f5b11754575b67cce83136177c7891c1240f405c360833ec8a"
MANAGE_SCRIPT_SHA256="b8733d20a3a29706953133339fbb56297dab3f8bd3fd0e9dda2631661176971b"
COMMON_SCRIPT_SHA256="949f9414ead9d0855967e724d5ec7184a290d9e7df2354fcd9c19e7135ae513f"
MANAGE_SCRIPT_SHA256="550086d35fc4e3661a37af657cbeb9ff867e91a54cd3b13b3fcc859b618eac2e"
# Флаги CLI
UNINSTALL=0; HELP=0; HELP_EXIT_RC=0; DIAGNOSTIC=0; VERBOSE=0; NO_COLOR=0; AUTO_YES=0; NO_TWEAKS=0
@ -46,13 +46,27 @@ CLI_ALLOW_IPV6_TUNNEL=0
# --- Автоочистка временных файлов ---
_install_temp_files=()
_install_cleaned=0
_install_cleanup() {
# Идемпотентно: на INT/TERM зовётся из сигнального обработчика, затем ещё раз
# на EXIT - второй вызов должен быть no-op.
[[ "$_install_cleaned" -eq 1 ]] && return 0
_install_cleaned=1
local f
for f in "${_install_temp_files[@]}"; do [[ -f "$f" ]] && rm -f "$f"; done
# Очистка временных файлов из awg_common.sh (если уже подключён через source)
type _awg_cleanup &>/dev/null && _awg_cleanup
}
trap _install_cleanup EXIT INT TERM
# На INT/TERM раньше cleanup срабатывал, но скрипт НЕ завершался - выполнение
# продолжалось после прерванной команды (опасно посреди apt/dpkg/правки конфигов),
# и cleanup ещё раз шёл на EXIT. Теперь сигнал = cleanup + явный выход 130/143.
_install_on_signal() {
_install_cleanup
exit "$1"
}
trap _install_cleanup EXIT
trap '_install_on_signal 130' INT
trap '_install_on_signal 143' TERM
# --- Обработка аргументов ---
while [[ $# -gt 0 ]]; do
@ -266,12 +280,13 @@ show_help() {
--route-all Использовать режим 'Весь трафик' неинтерактивно
--route-amnezia Использовать режим 'Amnezia' неинтерактивно
--route-custom=СЕТИ Использовать режим 'Пользовательский' неинтерактивно
--endpoint=IP Указать внешний IP сервера (для серверов за NAT)
--endpoint=АДРЕС Внешний endpoint сервера: FQDN, IPv4 или [IPv6] (для NAT)
-y, --yes Автоматическое подтверждение (перезагрузки, UFW и т.д.)
-f, --force Принудительная переустановка поверх уже работающего AmneziaWG
(по умолчанию запуск на сконфигурированном сервере прерывается;
ENV: AWG_FORCE_REINSTALL=1 эквивалентен флагу)
--no-tweaks Пропустить hardening/оптимизацию (без UFW, Fail2Ban, sysctl tweaks)
--no-tweaks Пропустить необязательный hardening/оптимизацию (UFW,
Fail2Ban); минимальный forwarding-sysctl применяется всегда
--preset=ТИП Набор параметров обфускации: default, mobile
mobile: Jc=3, узкий Jmax — для мобильных операторов (Tele2, Yota, Megafon)
--jc=N Задать Jc вручную (1-128, поверх preset)

View file

@ -8,14 +8,14 @@ fi
# ==============================================================================
# AmneziaWG 2.0 installation and configuration script for Ubuntu/Debian servers
# Author: @bivlked
# Version: 5.15.5
# Date: 2026-06-07
# Version: 5.15.6
# Date: 2026-06-08
# Repository: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Safe mode and Constants ---
set -o pipefail
SCRIPT_VERSION="5.15.5"
SCRIPT_VERSION="5.15.6"
AWG_DIR="/root/awg"
CONFIG_FILE="$AWG_DIR/awgsetup_cfg.init"
@ -33,8 +33,8 @@ MANAGE_SCRIPT_PATH="$AWG_DIR/manage_amneziawg.sh"
# Verified in step5_download_scripts() after curl.
# Verification is skipped when AWG_BRANCH is overridden (test branch).
# Format: sha256sum output (hex, 64 chars).
COMMON_SCRIPT_SHA256="ee36a1449cc93252ee62f8868ec9dbd48ff3ee2885d4546ae5fcbf8102c87d5a"
MANAGE_SCRIPT_SHA256="d8be0d16a1fed5426cf1f22b47721c13d25208953ef99ad0431ad1413ae75380"
COMMON_SCRIPT_SHA256="e444e54f4156895483cb5fc8c57d505fd3d341e6d5c62b851c818cdd6c33a327"
MANAGE_SCRIPT_SHA256="6d1149ed4edfec4e3961d3a8641b14c4799adfa0109584049b28f5b7dda4757a"
# CLI flags
UNINSTALL=0; HELP=0; HELP_EXIT_RC=0; DIAGNOSTIC=0; VERBOSE=0; NO_COLOR=0; AUTO_YES=0; NO_TWEAKS=0
@ -46,13 +46,27 @@ CLI_ALLOW_IPV6_TUNNEL=0
# --- Auto-cleanup of temporary files ---
_install_temp_files=()
_install_cleaned=0
_install_cleanup() {
# Idempotent: on INT/TERM it is called from the signal handler, then again on
# EXIT - the second call must be a no-op.
[[ "$_install_cleaned" -eq 1 ]] && return 0
_install_cleaned=1
local f
for f in "${_install_temp_files[@]}"; do [[ -f "$f" ]] && rm -f "$f"; done
# Clean up temporary files from awg_common.sh (if already sourced)
type _awg_cleanup &>/dev/null && _awg_cleanup
}
trap _install_cleanup EXIT INT TERM
# On INT/TERM the cleanup used to run but the script did NOT exit - execution
# continued past the interrupted command (dangerous mid apt/dpkg/config edits)
# and cleanup ran again on EXIT. A signal now means cleanup + explicit 130/143.
_install_on_signal() {
_install_cleanup
exit "$1"
}
trap _install_cleanup EXIT
trap '_install_on_signal 130' INT
trap '_install_on_signal 143' TERM
# --- Argument processing ---
while [[ $# -gt 0 ]]; do
@ -269,12 +283,13 @@ Options:
--route-all Use 'All traffic' mode non-interactively
--route-amnezia Use 'Amnezia' mode non-interactively
--route-custom=NETS Use 'Custom' mode non-interactively
--endpoint=IP Specify external server IP (for servers behind NAT)
--endpoint=ADDR External server endpoint: FQDN, IPv4 or [IPv6] (NAT)
-y, --yes Auto-confirm (reboots, UFW, etc.)
-f, --force Force reinstall on top of an already-running AmneziaWG
(by default a run on a configured server aborts;
ENV: AWG_FORCE_REINSTALL=1 is equivalent to the flag)
--no-tweaks Skip hardening/optimization (no UFW, Fail2Ban, sysctl tweaks)
--no-tweaks Skip optional hardening/optimization (UFW, Fail2Ban);
the minimal forwarding sysctl is always applied
--preset=TYPE Obfuscation parameter preset: default, mobile
mobile: Jc=3, narrow Jmax — for mobile carriers (Tele2, Yota, Megafon)
--jc=N Set Jc manually (1-128, overrides preset)

View file

@ -8,14 +8,14 @@ fi
# ==============================================================================
# Скрипт для управления пользователями (пирами) AmneziaWG 2.0
# Автор: @bivlked
# Версия: 5.15.5
# Дата: 2026-06-07
# Версия: 5.15.6
# Дата: 2026-06-08
# Репозиторий: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Безопасный режим и Константы ---
# shellcheck disable=SC2034
SCRIPT_VERSION="5.15.5"
SCRIPT_VERSION="5.15.6"
set -o pipefail
AWG_DIR="/root/awg"
SERVER_CONF_FILE="/etc/amnezia/amneziawg/awg0.conf"
@ -44,14 +44,29 @@ manage_mktempdir() {
echo "$d"
}
_manage_cleaned=0
_manage_cleanup() {
# Идемпотентно: на INT/TERM зовётся из сигнального обработчика, затем ещё раз
# на EXIT - повтор должен быть no-op.
[[ "$_manage_cleaned" -eq 1 ]] && return 0
_manage_cleaned=1
local d
for d in "${_manage_temp_dirs[@]}"; do
[[ -d "$d" ]] && rm -rf "$d"
done
type _awg_cleanup &>/dev/null && _awg_cleanup
}
trap _manage_cleanup EXIT INT TERM
# На INT/TERM раньше cleanup срабатывал, но скрипт НЕ завершался - выполнение шло
# дальше после прерванной команды, и cleanup повторялся на EXIT. Теперь сигнал =
# cleanup + явный выход 130/143. restore_backup на время destructive-фазы ставит
# СВОЙ INT/TERM-обработчик (с откатом), затем снимает его в _restore_cleanup.
_manage_on_signal() {
_manage_cleanup
exit "$1"
}
trap _manage_cleanup EXIT
trap '_manage_on_signal 130' INT
trap '_manage_on_signal 143' TERM
# --- Обработка аргументов ---
COMMAND=""
@ -494,7 +509,12 @@ restore_backup() {
# Реентранс невозможен: `local` и `trap -` не вызывают функций,
# а после `trap - RETURN` наш trap уже снят.
local _rc=$?
# Снимаем RETURN и ВОССТАНАВЛИВАЕМ глобальные INT/TERM (локальные хуки
# restore выставлены ниже). Просто `trap -` сбросил бы их в default и
# менеджер после restore потерял бы B1-поведение signal -> cleanup+exit.
trap - RETURN
trap '_manage_on_signal 130' INT
trap '_manage_on_signal 143' TERM
if [[ $_restore_ok -eq 0 && $_destructive_ops_started -eq 1 && -n "$_rollback_snap" ]]; then
_restore_do_rollback "$_rollback_snap" || true
fi
@ -504,6 +524,13 @@ restore_backup() {
return $_rc
}
trap _restore_cleanup RETURN
# INT/TERM в ходе restore: тот же rollback+cleanup, что и на обычном return
# (_restore_cleanup видит локальные _restore_ok/_rollback_snap/td), затем выход
# с сигнальным кодом. Перекрывает глобальный _manage_on_signal, чтобы прерывание
# destructive-фазы не оставило систему без отката. _restore_cleanup сам снимет
# эти хуки (trap - INT TERM выше).
trap '_restore_cleanup; exit 130' INT
trap '_restore_cleanup; exit 143' TERM
log "Создание бэкапа текущей..."
# --no-prune: выбранный для восстановления $bf лежит в той же папке бэкапов;
@ -578,6 +605,17 @@ restore_backup() {
return 1
fi
# Проверка полноты бэкапа ДО остановки сервиса. Бэкап без серверного конфига
# бесполезен (VPN без него не поднять), а пустой server/ ронял `cp "$td/server/"*`
# уже ПОСЛЕ stop и форсил откат рабочей системы. Проверяем до destructive-фазы:
# сервис не трогаем, откат не нужен.
local _srv_base
_srv_base=$(basename "$SERVER_CONF_FILE")
if [[ ! -f "$td/server/$_srv_base" ]]; then
log_error "Бэкап неполный: отсутствует серверный конфиг ($_srv_base) - восстановление отменено."
return 1
fi
log "Остановка сервиса..."
systemctl stop awg-quick@awg0 || log_warn "Сервис не остановлен."
@ -604,15 +642,22 @@ restore_backup() {
# orphan .conf/.png/.vpnuri). Scope строго managed client-globs - НЕ
# трогаю скрипты, server-ключи, backups/, логи, .lock, awgsetup_cfg.init.
rm -f "$AWG_DIR"/*.conf "$AWG_DIR"/*.png "$AWG_DIR"/*.vpnuri 2>/dev/null || true
if ! cp -a "$td/clients/"* "$AWG_DIR/"; then
log_error "Ошибка копирования clients — восстановление прервано (запуск отката)."
return 1
# Пустой clients/ - валидный случай (сервер без клиентских конфигов):
# prune выше уже дал чистую замену, copy просто пропускаем (без compgen
# голый glob "$td/clients/"* остался бы литералом и уронил cp -> откат).
if compgen -G "$td/clients/*" > /dev/null; then
if ! cp -a "$td/clients/"* "$AWG_DIR/"; then
log_error "Ошибка копирования clients — восстановление прервано (запуск отката)."
return 1
fi
chmod 600 "$AWG_DIR"/*.conf 2>/dev/null
chmod 600 "$AWG_DIR"/*.png 2>/dev/null
chmod 600 "$AWG_DIR"/*.vpnuri 2>/dev/null
chmod 600 "$CONFIG_FILE" 2>/dev/null
log_debug "Файлы клиентов восстановлены в $AWG_DIR"
else
log_debug "Бэкап без клиентских файлов (clients/ пуст) - пропуск копирования."
fi
chmod 600 "$AWG_DIR"/*.conf 2>/dev/null
chmod 600 "$AWG_DIR"/*.png 2>/dev/null
chmod 600 "$AWG_DIR"/*.vpnuri 2>/dev/null
chmod 600 "$CONFIG_FILE" 2>/dev/null
log_debug "Файлы клиентов восстановлены в $AWG_DIR"
fi
if [[ -d "$td/keys" ]]; then
@ -712,10 +757,37 @@ modify_client() {
case "$param" in
DNS)
if ! [[ "$value" =~ ^[0-9a-fA-F.:,\ ]+$ ]]; then
log_error "Невалидный DNS: '$value' (допустимы IP-адреса через запятую)"
return 1
fi ;;
# Структурная проверка списка DNS. Старый charset-only regex
# ^[0-9a-fA-F.:,\ ]+$ пропускал мусор ('abc' - буквы a-f; '999.999.999.999' -
# вне диапазона). DNS по контракту - только IP через запятую (без FQDN),
# поэтому каждый элемент = bare IPv4 или IPv6, как у Endpoint/AllowedIPs.
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"")
log_error "Невалидный DNS: '$value'"
return 1 ;;
esac
case "$value" in
,*|*,|*,,*)
log_error "Невалидный DNS '$value': пустой элемент списка (лишняя запятая)"
return 1 ;;
esac
local _dns_tok _dns_ifs="$IFS"
IFS=','
for _dns_tok in $value; do
_dns_tok="${_dns_tok//[[:space:]]/}"
if [[ -z "$_dns_tok" ]]; then
IFS="$_dns_ifs"
log_error "Невалидный DNS '$value': пустой элемент списка (лишняя запятая)"
return 1
fi
if ! _valid_ipv4 "$_dns_tok" && ! _valid_ipv6 "$_dns_tok"; then
IFS="$_dns_ifs"
log_error "Невалидный DNS '$value': '$_dns_tok' не похож на IPv4/IPv6-адрес"
return 1
fi
done
IFS="$_dns_ifs"
;;
PersistentKeepalive)
if ! [[ "$value" =~ ^[0-9]+$ ]] || [[ "$value" -gt 65535 ]]; then
log_error "Невалидный PersistentKeepalive: '$value' (допустимо: 0-65535)"
@ -1208,7 +1280,7 @@ list_clients() {
if [[ -z "$name" ]]; then continue; fi
((tot++))
local cf="?" png="?" pk="-" ip="-" ip6="-" st="Нет данных"
local cf="?" png="?" pk="-" ip="-" ip6="-" st="Нет данных" st_code="no_data"
local color_start="" color_end=""
if [[ "$NO_COLOR" -eq 0 ]]; then
color_end="\033[0m"
@ -1249,24 +1321,24 @@ list_clients() {
if [[ "$handshake" =~ ^[0-9]+$ && "$handshake" -gt 0 ]]; then
local diff=$((now - handshake))
if [[ $diff -lt 180 ]]; then
st="Активен"
st="Активен"; st_code="active"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;32m"
((act++))
elif [[ $diff -lt 86400 ]]; then
st="Недавно"
st="Недавно"; st_code="recent"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;33m"
((act++))
else
st="Нет handshake"
st="Нет handshake"; st_code="no_handshake"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;37m"
fi
else
st="Нет handshake"
st="Нет handshake"; st_code="no_handshake"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;37m"
fi
else
pk="?"
st="Ошибка ключа"
st="Ошибка ключа"; st_code="key_error"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;31m"
fi
fi
@ -1282,7 +1354,7 @@ list_clients() {
if [[ "$JSON_OUTPUT" -eq 1 ]]; then
local _ip6_val="${ip6}"
[[ "$_ip6_val" == "-" ]] && _ip6_val=""
json_entries+=("{\"name\":\"$(json_escape "$name")\",\"ip\":\"$(json_escape "$ip")\",\"client_ipv6\":\"$(json_escape "$_ip6_val")\",\"status\":\"$(json_escape "$st")\"}")
json_entries+=("{\"name\":\"$(json_escape "$name")\",\"ip\":\"$(json_escape "$ip")\",\"client_ipv6\":\"$(json_escape "$_ip6_val")\",\"status\":\"$(json_escape "$st")\",\"status_code\":\"${st_code}\"}")
elif [[ $verbose -eq 1 ]]; then
local ip_display
if [[ "$ip6" != "-" ]]; then
@ -1384,15 +1456,15 @@ stats_clients() {
fi
local hs_str="никогда"
local status="Неактивен"
local status="Неактивен" status_code="inactive"
if [[ "$handshake" =~ ^[0-9]+$ && "$handshake" -gt 0 ]]; then
local now
now=$(date +%s)
local diff=$((now - handshake))
if [[ $diff -lt 180 ]]; then
status="Активен"
status="Активен"; status_code="active"
elif [[ $diff -lt 86400 ]]; then
status="Недавно"
status="Недавно"; status_code="recent"
fi
hs_str=$(date -d "@$handshake" '+%F %T' 2>/dev/null || echo "$handshake")
fi
@ -1401,7 +1473,7 @@ stats_clients() {
total_tx=$((total_tx + tx))
if [[ "$JSON_OUTPUT" -eq 1 ]]; then
json_entries+=("{\"name\":\"$(json_escape "$cname")\",\"ip\":\"$(json_escape "$ip")\",\"rx\":$rx,\"tx\":$tx,\"last_handshake\":$handshake,\"status\":\"$(json_escape "$status")\"}")
json_entries+=("{\"name\":\"$(json_escape "$cname")\",\"ip\":\"$(json_escape "$ip")\",\"rx\":$rx,\"tx\":$tx,\"last_handshake\":$handshake,\"status\":\"$(json_escape "$status")\",\"status_code\":\"${status_code}\"}")
else
local rx_h tx_h
rx_h=$(format_bytes "$rx")
@ -1516,6 +1588,15 @@ case $COMMAND in
log "PresharedKey будет сгенерирован для каждого нового клиента (--psk)."
fi
# --expires валидируем ОДИН раз ДО создания первого клиента. Иначе при
# неверном формате (--expires=bad) клиенты создавались permanent, а
# set_client_expiry молча падал per-client - временный клиент незаметно
# становился постоянным. Плохой формат теперь рушит команду до изменений.
if [[ -n "$EXPIRES_DURATION" ]]; then
parse_duration "$EXPIRES_DURATION" >/dev/null \
|| die "Некорректный --expires='$EXPIRES_DURATION'. Используйте: 1h, 12h, 1d, 7d, 4w."
fi
_added=0
for _cname in "${ARGS[@]}"; do
validate_client_name "$_cname" || { _cmd_rc=1; continue; }
@ -1540,7 +1621,13 @@ case $COMMAND in
fi
if [[ -n "$EXPIRES_DURATION" ]]; then
if set_client_expiry "$_cname" "$EXPIRES_DURATION"; then
install_expiry_cron
install_expiry_cron || { log_error "Клиент '$_cname' создан со сроком, но cron автоудаления НЕ установлен - истёкший клиент сам не удалится."; _cmd_rc=1; }
else
# Формат проверен выше, значит сбой записи expiry (FS/права).
# Клиент создан и рабочий, но БЕЗ авто-срока - сигналим явно,
# чтобы временный клиент не остался незаметно постоянным.
log_error "Клиент '$_cname' создан, но срок действия НЕ установлен (ошибка записи expiry). Клиент постоянный - задайте срок повторно или удалите."
_cmd_rc=1
fi
fi
((_added++))
@ -1604,9 +1691,7 @@ case $COMMAND in
for _rname in "${_valid_names[@]}"; do
log "Удаление '$_rname'..."
if remove_peer_from_server "$_rname"; then
rm -f "$AWG_DIR/$_rname.conf" "$AWG_DIR/$_rname.png" \
"$AWG_DIR/$_rname.vpnuri" "$AWG_DIR/$_rname.vpnuri.png"
rm -f "$KEYS_DIR/${_rname}.private" "$KEYS_DIR/${_rname}.public"
_remove_client_files "$_rname"
remove_client_expiry "$_rname"
log "Клиент '$_rname' удалён."
((_removed++))

View file

@ -8,14 +8,14 @@ fi
# ==============================================================================
# AmneziaWG 2.0 peer management script
# Author: @bivlked
# Version: 5.15.5
# Date: 2026-06-07
# Version: 5.15.6
# Date: 2026-06-08
# Repository: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Safe mode and Constants ---
# shellcheck disable=SC2034
SCRIPT_VERSION="5.15.5"
SCRIPT_VERSION="5.15.6"
set -o pipefail
AWG_DIR="/root/awg"
SERVER_CONF_FILE="/etc/amnezia/amneziawg/awg0.conf"
@ -44,14 +44,29 @@ manage_mktempdir() {
echo "$d"
}
_manage_cleaned=0
_manage_cleanup() {
# Idempotent: on INT/TERM it is called from the signal handler, then again on
# EXIT - the repeat must be a no-op.
[[ "$_manage_cleaned" -eq 1 ]] && return 0
_manage_cleaned=1
local d
for d in "${_manage_temp_dirs[@]}"; do
[[ -d "$d" ]] && rm -rf "$d"
done
type _awg_cleanup &>/dev/null && _awg_cleanup
}
trap _manage_cleanup EXIT INT TERM
# On INT/TERM the cleanup used to run but the script did NOT exit - execution
# continued past the interrupted command and cleanup ran again on EXIT. A signal
# now means cleanup + explicit 130/143. restore_backup installs its OWN INT/TERM
# handler (with rollback) for its destructive phase and clears it in _restore_cleanup.
_manage_on_signal() {
_manage_cleanup
exit "$1"
}
trap _manage_cleanup EXIT
trap '_manage_on_signal 130' INT
trap '_manage_on_signal 143' TERM
# --- Argument handling ---
COMMAND=""
@ -497,7 +512,12 @@ restore_backup() {
# not invoke functions, and once `trap - RETURN` runs, our
# trap is off.
local _rc=$?
# Clear RETURN and RESTORE the global INT/TERM (restore's local hooks are
# set below). A plain `trap -` would reset them to default and the manager
# would lose its B1 signal -> cleanup+exit behavior after a restore.
trap - RETURN
trap '_manage_on_signal 130' INT
trap '_manage_on_signal 143' TERM
if [[ $_restore_ok -eq 0 && $_destructive_ops_started -eq 1 && -n "$_rollback_snap" ]]; then
_restore_do_rollback "$_rollback_snap" || true
fi
@ -507,6 +527,13 @@ restore_backup() {
return $_rc
}
trap _restore_cleanup RETURN
# INT/TERM during restore: same rollback+cleanup as a normal return
# (_restore_cleanup sees the local _restore_ok/_rollback_snap/td), then exit
# with the signal code. Overrides the global _manage_on_signal so interrupting
# the destructive phase does not leave the system without a rollback.
# _restore_cleanup clears these hooks itself (trap - INT TERM above).
trap '_restore_cleanup; exit 130' INT
trap '_restore_cleanup; exit 143' TERM
log "Backing up current config..."
# --no-prune: the backup selected for restore ($bf) lives in the same
@ -581,6 +608,18 @@ restore_backup() {
return 1
fi
# Check backup completeness BEFORE stopping the service. A backup without a
# server config is useless (a VPN cannot come up without it), and an empty
# server/ used to crash `cp "$td/server/"*` AFTER the stop, forcing a rollback
# of a working system. Checking before the destructive phase leaves the
# service untouched and needs no rollback.
local _srv_base
_srv_base=$(basename "$SERVER_CONF_FILE")
if [[ ! -f "$td/server/$_srv_base" ]]; then
log_error "Incomplete backup: missing server config ($_srv_base) - restore aborted."
return 1
fi
log "Stopping service..."
systemctl stop awg-quick@awg0 || log_warn "Service not stopped."
@ -608,15 +647,23 @@ restore_backup() {
# globs - never touch scripts, server keys, backups/, logs, .lock,
# awgsetup_cfg.init.
rm -f "$AWG_DIR"/*.conf "$AWG_DIR"/*.png "$AWG_DIR"/*.vpnuri 2>/dev/null || true
if ! cp -a "$td/clients/"* "$AWG_DIR/"; then
log_error "Error copying clients — restore aborted (triggering rollback)."
return 1
# An empty clients/ is a valid case (a server with no client configs):
# the prune above already gives a clean replacement, so we just skip the
# copy (without compgen the bare glob "$td/clients/"* would stay literal
# and crash cp -> rollback).
if compgen -G "$td/clients/*" > /dev/null; then
if ! cp -a "$td/clients/"* "$AWG_DIR/"; then
log_error "Error copying clients — restore aborted (triggering rollback)."
return 1
fi
chmod 600 "$AWG_DIR"/*.conf 2>/dev/null
chmod 600 "$AWG_DIR"/*.png 2>/dev/null
chmod 600 "$AWG_DIR"/*.vpnuri 2>/dev/null
chmod 600 "$CONFIG_FILE" 2>/dev/null
log_debug "Client files restored to $AWG_DIR"
else
log_debug "Backup has no client files (clients/ empty) - skipping copy."
fi
chmod 600 "$AWG_DIR"/*.conf 2>/dev/null
chmod 600 "$AWG_DIR"/*.png 2>/dev/null
chmod 600 "$AWG_DIR"/*.vpnuri 2>/dev/null
chmod 600 "$CONFIG_FILE" 2>/dev/null
log_debug "Client files restored to $AWG_DIR"
fi
if [[ -d "$td/keys" ]]; then
@ -717,10 +764,37 @@ modify_client() {
case "$param" in
DNS)
if ! [[ "$value" =~ ^[0-9a-fA-F.:,\ ]+$ ]]; then
log_error "Invalid DNS: '$value' (expected comma-separated IPs)"
return 1
fi ;;
# Structural validation of the DNS list. The old charset-only regex
# ^[0-9a-fA-F.:,\ ]+$ let garbage through ('abc' - a-f letters;
# '999.999.999.999' - out of range). DNS is IP-only by contract (no
# FQDN), so each element must be a bare IPv4 or IPv6, like Endpoint/AllowedIPs.
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"")
log_error "Invalid DNS: '$value'"
return 1 ;;
esac
case "$value" in
,*|*,|*,,*)
log_error "Invalid DNS '$value': empty list element (stray comma)"
return 1 ;;
esac
local _dns_tok _dns_ifs="$IFS"
IFS=','
for _dns_tok in $value; do
_dns_tok="${_dns_tok//[[:space:]]/}"
if [[ -z "$_dns_tok" ]]; then
IFS="$_dns_ifs"
log_error "Invalid DNS '$value': empty list element (stray comma)"
return 1
fi
if ! _valid_ipv4 "$_dns_tok" && ! _valid_ipv6 "$_dns_tok"; then
IFS="$_dns_ifs"
log_error "Invalid DNS '$value': '$_dns_tok' is not a valid IPv4/IPv6 address"
return 1
fi
done
IFS="$_dns_ifs"
;;
PersistentKeepalive)
if ! [[ "$value" =~ ^[0-9]+$ ]] || [[ "$value" -gt 65535 ]]; then
log_error "Invalid PersistentKeepalive: '$value' (expected: 0-65535)"
@ -1212,7 +1286,7 @@ list_clients() {
if [[ -z "$name" ]]; then continue; fi
((tot++))
local cf="?" png="?" pk="-" ip="-" ip6="-" st="No data"
local cf="?" png="?" pk="-" ip="-" ip6="-" st="No data" st_code="no_data"
local color_start="" color_end=""
if [[ "$NO_COLOR" -eq 0 ]]; then
color_end="\033[0m"
@ -1253,24 +1327,24 @@ list_clients() {
if [[ "$handshake" =~ ^[0-9]+$ && "$handshake" -gt 0 ]]; then
local diff=$((now - handshake))
if [[ $diff -lt 180 ]]; then
st="Active"
st="Active"; st_code="active"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;32m"
((act++))
elif [[ $diff -lt 86400 ]]; then
st="Recent"
st="Recent"; st_code="recent"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;33m"
((act++))
else
st="No handshake"
st="No handshake"; st_code="no_handshake"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;37m"
fi
else
st="No handshake"
st="No handshake"; st_code="no_handshake"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;37m"
fi
else
pk="?"
st="Key error"
st="Key error"; st_code="key_error"
[[ "$NO_COLOR" -eq 0 ]] && color_start="\033[0;31m"
fi
fi
@ -1286,7 +1360,7 @@ list_clients() {
if [[ "$JSON_OUTPUT" -eq 1 ]]; then
local _ip6_val="${ip6}"
[[ "$_ip6_val" == "-" ]] && _ip6_val=""
json_entries+=("{\"name\":\"$(json_escape "$name")\",\"ip\":\"$(json_escape "$ip")\",\"client_ipv6\":\"$(json_escape "$_ip6_val")\",\"status\":\"$(json_escape "$st")\"}")
json_entries+=("{\"name\":\"$(json_escape "$name")\",\"ip\":\"$(json_escape "$ip")\",\"client_ipv6\":\"$(json_escape "$_ip6_val")\",\"status\":\"$(json_escape "$st")\",\"status_code\":\"${st_code}\"}")
elif [[ $verbose -eq 1 ]]; then
local ip_display
if [[ "$ip6" != "-" ]]; then
@ -1388,15 +1462,15 @@ stats_clients() {
fi
local hs_str="never"
local status="Inactive"
local status="Inactive" status_code="inactive"
if [[ "$handshake" =~ ^[0-9]+$ && "$handshake" -gt 0 ]]; then
local now
now=$(date +%s)
local diff=$((now - handshake))
if [[ $diff -lt 180 ]]; then
status="Active"
status="Active"; status_code="active"
elif [[ $diff -lt 86400 ]]; then
status="Recent"
status="Recent"; status_code="recent"
fi
hs_str=$(date -d "@$handshake" '+%F %T' 2>/dev/null || echo "$handshake")
fi
@ -1405,7 +1479,7 @@ stats_clients() {
total_tx=$((total_tx + tx))
if [[ "$JSON_OUTPUT" -eq 1 ]]; then
json_entries+=("{\"name\":\"$(json_escape "$cname")\",\"ip\":\"$(json_escape "$ip")\",\"rx\":$rx,\"tx\":$tx,\"last_handshake\":$handshake,\"status\":\"$(json_escape "$status")\"}")
json_entries+=("{\"name\":\"$(json_escape "$cname")\",\"ip\":\"$(json_escape "$ip")\",\"rx\":$rx,\"tx\":$tx,\"last_handshake\":$handshake,\"status\":\"$(json_escape "$status")\",\"status_code\":\"${status_code}\"}")
else
local rx_h tx_h
rx_h=$(format_bytes "$rx")
@ -1520,6 +1594,15 @@ case $COMMAND in
log "PresharedKey will be generated for each new client (--psk)."
fi
# Validate --expires ONCE before creating the first client. Otherwise a
# bad format (--expires=bad) created permanent clients while
# set_client_expiry failed silently per-client - a temporary client
# quietly became permanent. A bad format now aborts before any change.
if [[ -n "$EXPIRES_DURATION" ]]; then
parse_duration "$EXPIRES_DURATION" >/dev/null \
|| die "Invalid --expires='$EXPIRES_DURATION'. Use: 1h, 12h, 1d, 7d, 4w."
fi
_added=0
for _cname in "${ARGS[@]}"; do
validate_client_name "$_cname" || { _cmd_rc=1; continue; }
@ -1544,7 +1627,14 @@ case $COMMAND in
fi
if [[ -n "$EXPIRES_DURATION" ]]; then
if set_client_expiry "$_cname" "$EXPIRES_DURATION"; then
install_expiry_cron
install_expiry_cron || { log_error "Client '$_cname' created with an expiry, but the auto-removal cron was NOT installed - the expired client will not be removed automatically."; _cmd_rc=1; }
else
# Format is validated above, so this is a write failure
# (FS/permissions). The client exists and works but has NO
# auto-expiry - signal it so a temporary client does not
# silently stay permanent.
log_error "Client '$_cname' created, but expiry was NOT set (expiry write error). The client is permanent - set the expiry again or remove it."
_cmd_rc=1
fi
fi
((_added++))
@ -1607,9 +1697,7 @@ case $COMMAND in
for _rname in "${_valid_names[@]}"; do
log "Removing '$_rname'..."
if remove_peer_from_server "$_rname"; then
rm -f "$AWG_DIR/$_rname.conf" "$AWG_DIR/$_rname.png" \
"$AWG_DIR/$_rname.vpnuri" "$AWG_DIR/$_rname.vpnuri.png"
rm -f "$KEYS_DIR/${_rname}.private" "$KEYS_DIR/${_rname}.public"
_remove_client_files "$_rname"
remove_client_expiry "$_rname"
log "Client '$_rname' removed."
((_removed++))

View file

@ -28,6 +28,8 @@
# (present-tense "не поддерживается / implies full-tunnel"; past-tense
# историческая заметка разрешена).
# 8. Issue-template: placeholder версии нейтральный (не протухающий X.Y.Z).
# 9. Матрица OS×arch×prebuilt-target: supported Ubuntu-версии без ARM
# prebuilt-таргета в arm-build.yml помечены DKMS-only для ARM в INSTALL_VPS.
set -o pipefail
@ -43,14 +45,21 @@ declare -a RESULTS
_ok() { echo "PASS: $1"; RESULTS+=("PASS: $1"); PASS=$((PASS+1)); }
_bad() { echo "FAIL: $1" >&2; RESULTS+=("FAIL: $1"); FAIL=$((FAIL+1)); }
# Файлы документации с внутренними якорями.
DOC_FILES=(
README.md README.en.md
ADVANCED.md ADVANCED.en.md
CHANGELOG.md CHANGELOG.en.md
SECURITY.md CONTRIBUTING.md INSTALL_VPS.md
docs/SIGNING_DESIGN.md docs/RELEASE_PROCESS.md docs/ROADMAP.md
)
# Файлы документации с внутренними якорями. Обнаруживаются динамически: ВСЕ
# tracked *.md, чтобы новый markdown (например CODE_OF_CONDUCT.md) автоматически
# попадал под anchor-валидацию. Раньше список был захардкожен (#4 docs-audit), и
# новый MD проходил CI без проверки якорей. Спец-проверки ниже (README/CHANGELOG/
# SECURITY/CONTRIBUTING/ОС-матрица) остаются точечными по своим файлам.
mapfile -t DOC_FILES < <(git ls-files '*.md' 2>/dev/null | sort)
if [[ "${#DOC_FILES[@]}" -eq 0 ]]; then
# Fallback вне git-дерева: явный базовый набор.
DOC_FILES=(
README.md README.en.md ADVANCED.md ADVANCED.en.md
CHANGELOG.md CHANGELOG.en.md SECURITY.md CONTRIBUTING.md
CODE_OF_CONDUCT.md INSTALL_VPS.md
docs/SIGNING_DESIGN.md docs/RELEASE_PROCESS.md docs/ROADMAP.md
)
fi
echo "=== check-docs-consistency ==="
@ -270,6 +279,33 @@ if [[ -f "$bug_tmpl" ]]; then
fi
if [[ "$tmpl_fail" -eq 0 ]]; then _ok "issue-template: placeholder версии нейтральный"; else _bad "issue-template: протухающий placeholder версии"; fi
# --- 9. Матрица OS×arch×prebuilt-target: ARM prebuilt-покрытие согласовано ---
# arm-build.yml собирает prebuilt ARM .deb только для образов из своей matrix.
# Заявленные supported Ubuntu-версии без ARM prebuilt-таргета обязаны быть явно
# помечены DKMS-only для ARM, иначе матрица ОС создаёт впечатление prebuilt там,
# где его нет (docs-audit #3: docs-check проверял OS и arch как независимые
# токены, не их пересечение с prebuilt-таргетом). Источник prebuilt-набора - сам
# arm-build.yml, поэтому проверка не протухнет при добавлении/удалении таргета.
arm_yml=".github/workflows/arm-build.yml"
arm_matrix_fail=0
if [[ -f "$arm_yml" ]]; then
mapfile -t arm_ubuntu < <(grep -oP 'image:[[:space:]]*ubuntu:\K[0-9]+\.[0-9]+' "$arm_yml" | sort -u)
for os in "${EXPECTED_OS[@]}"; do
[[ "$os" =~ ^[0-9]+\.[0-9]+$ ]] || continue # только Ubuntu version-токены
has_prebuilt=0
for u in "${arm_ubuntu[@]}"; do [[ "$u" == "$os" ]] && has_prebuilt=1; done
[[ "$has_prebuilt" -eq 1 ]] && continue
os_re="${os//./\\.}"
if ! grep -qiE "${os_re} ARM64.*(DKMS|from source)" INSTALL_VPS.md 2>/dev/null; then
echo " INSTALL_VPS.md: Ubuntu $os без ARM prebuilt-таргета и не помечен DKMS-only для ARM" >&2
arm_matrix_fail=1
fi
done
else
echo " нет $arm_yml (проверка ARM prebuilt-матрицы)" >&2; arm_matrix_fail=1
fi
if [[ "$arm_matrix_fail" -eq 0 ]]; then _ok "ARM prebuilt-покрытие согласовано (OS×arch×target)"; else _bad "ARM prebuilt-покрытие рассинхронизировано"; fi
# --- Summary ---
echo ""
echo "=== docs-consistency summary: $PASS passed, $FAIL failed ==="

View file

@ -0,0 +1,64 @@
#!/usr/bin/env bats
# Code-review findings A3 (.vpnuri atomic write), A4 (QR PNG via awg_mktemp),
# B2 (expiry cleanup must drop .vpnuri.png; shared client-artifact helper).
load test_helper
@test "B2 _remove_client_files: removes all six artifacts incl .vpnuri.png" {
local n=alice
: > "$AWG_DIR/$n.conf"
: > "$AWG_DIR/$n.png"
: > "$AWG_DIR/$n.vpnuri"
: > "$AWG_DIR/$n.vpnuri.png"
: > "$KEYS_DIR/$n.private"
: > "$KEYS_DIR/$n.public"
run _remove_client_files "$n"
[ "$status" -eq 0 ]
for ext in conf png vpnuri vpnuri.png; do
[ ! -e "$AWG_DIR/$n.$ext" ] || { echo "left behind: $n.$ext"; false; }
done
[ ! -e "$KEYS_DIR/$n.private" ]
[ ! -e "$KEYS_DIR/$n.public" ]
}
@test "B2 _remove_client_files: defined in both awg_common scripts" {
for f in awg_common.sh awg_common_en.sh; do
run grep -E '^_remove_client_files\(\) \{' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing _remove_client_files"; false; }
run grep -E '\$\{name\}\.vpnuri\.png' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f helper omits .vpnuri.png"; false; }
done
}
@test "B2 expiry cleanup and manage remove both call _remove_client_files" {
for f in awg_common.sh awg_common_en.sh; do
run grep -E '_remove_client_files "\$name"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f expiry cleanup not using helper"; false; }
done
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
run grep -E '_remove_client_files "\$_rname"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f manage remove not using helper"; false; }
done
}
@test "A3 source: generate_vpn_uri writes .vpnuri via awg_mktemp + atomic mv" {
for f in awg_common.sh awg_common_en.sh; do
# No direct redirect into the final .vpnuri file anymore.
run grep -E '"\$vpn_uri" > "\$uri_file"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still writes .vpnuri non-atomically"; false; }
run grep -E 'mv -f "\$_uri_tmp" "\$uri_file"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing atomic mv for .vpnuri"; false; }
done
}
@test "A4 source: QR PNGs use awg_mktemp (no manual .tmp.\$\$)" {
for f in awg_common.sh awg_common_en.sh; do
run grep -F '${png_file}.tmp.$$' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still uses manual .tmp.\$\$ for QR"; false; }
# Both QR functions must obtain tmp_png from awg_mktemp.
run grep -cE 'tmp_png=\$\(awg_mktemp "\$AWG_DIR"\)' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ]
[ "$output" -ge 2 ] || { echo "$f: expected 2 awg_mktemp QR sites, got $output"; false; }
done
}

View file

@ -0,0 +1,74 @@
#!/usr/bin/env bats
# Code-review findings A5/B3 (DNS) and B4 (public IP octet range).
#
# A5/B3: `manage modify <client> DNS <value>` used a charset-only regex
# ^[0-9a-fA-F.:,\ ]+$ that accepted non-addresses ('abc' is all a-f letters,
# '999.999.999.999' is out of range). DNS is IP-only by contract, so each
# comma-separated element must now pass _valid_ipv4 or _valid_ipv6.
# B4: get_server_public_ip() / _try_local_ip() accepted any N.N.N.N without an
# octet-range check; they now reuse _valid_ipv4 (already in awg_common).
load test_helper
# Faithful copy of the per-element DNS validation added to modify_client.
dns_list_ok() {
local value="$1"
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"") return 1 ;;
esac
case "$value" in
,*|*,|*,,*) return 1 ;;
esac
local tok ifs="$IFS"
IFS=','
for tok in $value; do
tok="${tok//[[:space:]]/}"
if [[ -z "$tok" ]]; then IFS="$ifs"; return 1; fi
if ! _valid_ipv4 "$tok" && ! _valid_ipv6 "$tok"; then IFS="$ifs"; return 1; fi
done
IFS="$ifs"
return 0
}
@test "A5 DNS: well-formed IPv4/IPv6 lists pass" {
for ok in "1.1.1.1" "8.8.8.8, 1.0.0.1" "2606:4700:4700::1111" "1.1.1.1, 2606:4700:4700::1001"; do
run dns_list_ok "$ok"
[ "$status" -eq 0 ] || { echo "rejected valid DNS: $ok"; false; }
done
}
@test "A5 DNS: garbage that passed the old charset regex is now rejected" {
for bad in "abc" "999.999.999.999" "1.2.3" "10.0.0.x" "1.1.1.1,," ",1.1.1.1" "1.1.1.1," ""; do
run dns_list_ok "$bad"
[ "$status" -ne 0 ] || { echo "accepted invalid DNS: $bad"; false; }
done
}
@test "A5 source: both manage scripts validate DNS via _valid_ipv4/_valid_ipv6 (not charset-only)" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
# The old charset-only DNS regex must no longer be used in an active match
# (a literal inside an explanatory comment is fine).
run grep -E '=~ \^\[0-9a-fA-F' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still uses charset-only DNS regex"; false; }
# The DNS branch must reference the structural validators.
run grep -E '_valid_ipv4 "\$_dns_tok"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f DNS branch missing _valid_ipv4"; false; }
done
}
@test "B4 source: public-IP detection uses _valid_ipv4 (no loose N.N.N.N regex)" {
for f in awg_common.sh awg_common_en.sh; do
# Loose four-octet regex must no longer gate detected IPs.
run grep -F '=~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still has loose public-IP regex"; false; }
run grep -E '_valid_ipv4 "\$ip"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f public-IP path missing _valid_ipv4"; false; }
done
}
@test "B4 behavior: _valid_ipv4 rejects the out-of-range IP the old regex accepted" {
run _valid_ipv4 "999.999.999.999"
[ "$status" -ne 0 ]
run _valid_ipv4 "203.0.113.7"
[ "$status" -eq 0 ]
}

View file

@ -0,0 +1,119 @@
#!/usr/bin/env bats
# Code-review findings A6 (install_expiry_cron idempotent by content) and
# A7 (restore: server-completeness check before the service stop; empty
# clients/ is a valid no-op instead of a cp crash + rollback).
load test_helper
# --- A6: install_expiry_cron is content-aware -------------------------------
@test "A6 install_expiry_cron: writes a cron pointing at the current AWG_DIR" {
install_expiry_cron
[ -f "$EXPIRY_CRON" ]
grep -q "AWG_DIR=\"$AWG_DIR\"" "$EXPIRY_CRON"
}
@test "A6 install_expiry_cron: identical content is left untouched (idempotent)" {
install_expiry_cron
local before; before=$(cat "$EXPIRY_CRON")
run install_expiry_cron
[ "$status" -eq 0 ]
[ "$(cat "$EXPIRY_CRON")" = "$before" ]
}
@test "A6 install_expiry_cron: refreshes stale paths after AWG_DIR changes" {
install_expiry_cron
grep -q "AWG_DIR=\"$AWG_DIR\"" "$EXPIRY_CRON"
# Simulate a restore/migration to a new directory; the cron path is fixed but
# its embedded AWG_DIR must be rewritten (the old early-out left it stale).
local newdir="$TEST_DIR/moved"; mkdir -p "$newdir"
export AWG_DIR="$newdir"
run install_expiry_cron
[ "$status" -eq 0 ]
grep -q "AWG_DIR=\"$newdir\"" "$EXPIRY_CRON"
! grep -q "AWG_DIR=\"$TEST_DIR\"" "$EXPIRY_CRON"
}
@test "A6 source: install_expiry_cron compares content (no bare file-exists early-out)" {
for f in awg_common.sh awg_common_en.sh; do
run grep -E 'cmp -s "\$_cron_tmp" "\$EXPIRY_CRON"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f: install_expiry_cron not content-aware"; false; }
done
}
# --- A7: restore completeness + empty-clients no-op ------------------------
# Mirror of the A7 pre-stop server-completeness check (looks for the actual
# server config by basename, not just any file in server/).
server_complete_ok() {
local td="$1"
[[ -f "$td/server/$(basename "$SERVER_CONF_FILE")" ]]
}
# Mirror of the A7-guarded clients-restore fragment.
restore_clients_fragment() {
local td="$1"
if [[ -d "$td/clients" ]]; then
rm -f "$AWG_DIR"/*.conf "$AWG_DIR"/*.png "$AWG_DIR"/*.vpnuri 2>/dev/null || true
if compgen -G "$td/clients/*" > /dev/null; then
cp -a "$td/clients/"* "$AWG_DIR/" || return 1
fi
fi
return 0
}
@test "A7 server completeness: empty, missing, or non-config server/ is rejected" {
local td="$TEST_DIR/x"
mkdir -p "$td/server" # present but empty
run server_complete_ok "$td"
[ "$status" -ne 0 ]
echo readme > "$td/server/README" # a file, but not the server config
run server_complete_ok "$td"
[ "$status" -ne 0 ]
rm -rf "$td/server" # missing entirely
run server_complete_ok "$td"
[ "$status" -ne 0 ]
}
@test "A7 server completeness: a server/ holding the actual config passes" {
local td="$TEST_DIR/x"
mkdir -p "$td/server"
echo "[Interface]" > "$td/server/$(basename "$SERVER_CONF_FILE")"
run server_complete_ok "$td"
[ "$status" -eq 0 ]
}
@test "A7 clients: empty clients/ is a no-op (no crash, no leftover)" {
local td="$TEST_DIR/x"
mkdir -p "$td/clients"
echo stale > "$AWG_DIR/old.conf"
run restore_clients_fragment "$td"
[ "$status" -eq 0 ]
[ ! -e "$AWG_DIR/old.conf" ] # prune still happened (clean replace)
}
@test "A7 clients: populated clients/ is copied" {
local td="$TEST_DIR/x"
mkdir -p "$td/clients"
echo "cfg" > "$td/clients/bob.conf"
run restore_clients_fragment "$td"
[ "$status" -eq 0 ]
[ -f "$AWG_DIR/bob.conf" ]
}
@test "A7 source: server completeness is checked BEFORE the service stop" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
local check_line stop_line
check_line=$(grep -n '! -f "\$td/server/\$_srv_base"' "$BATS_TEST_DIRNAME/../$f" | head -1 | cut -d: -f1)
stop_line=$(grep -n 'systemctl stop awg-quick@awg0' "$BATS_TEST_DIRNAME/../$f" | head -1 | cut -d: -f1)
[ -n "$check_line" ] || { echo "$f: no server config completeness check"; false; }
[ "$check_line" -lt "$stop_line" ] || { echo "$f: completeness check not before stop"; false; }
done
}
@test "A7 source: clients copy is guarded by compgen in both scripts" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
run grep -E 'compgen -G "\$td/clients/\*"' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f: clients copy not compgen-guarded"; false; }
done
}

View file

@ -0,0 +1,87 @@
#!/usr/bin/env bats
# Code-review finding B1: INT/TERM traps must terminate the script.
#
# Both installers and managers used `trap _cleanup EXIT INT TERM`. On INT/TERM
# bash ran the cleanup but did NOT exit, so execution continued past the
# interrupted command (dangerous mid apt/dpkg/config edits) and cleanup ran a
# second time on EXIT. The fix splits the handlers: EXIT does cleanup only;
# INT/TERM do an idempotent cleanup and exit 130/143.
load test_helper
@test "B1 signal pattern: SIGTERM exits 143, cleans up once, does not continue" {
local out="$TEST_DIR/sig.out"
: > "$out"
cat > "$TEST_DIR/sig.sh" <<EOF
#!/bin/bash
cleaned=0
_cleanup() { [[ "\$cleaned" -eq 1 ]] && return 0; cleaned=1; echo CLEANUP >> "$out"; }
_on_signal() { _cleanup; exit "\$1"; }
trap _cleanup EXIT
trap '_on_signal 130' INT
trap '_on_signal 143' TERM
echo START >> "$out"
sleep 10
echo AFTER >> "$out"
EOF
bash "$TEST_DIR/sig.sh" &
local pid=$! i=0
while [[ ! -s "$out" ]] && (( i++ < 50 )); do sleep 0.1; done
kill -TERM "$pid"
local rc=0
wait "$pid" || rc=$?
[ "$rc" -eq 143 ] || { echo "exit code was $rc, expected 143"; false; }
grep -q START "$out"
grep -q CLEANUP "$out"
! grep -q AFTER "$out" || { echo "script continued past the signal"; false; }
[ "$(grep -c CLEANUP "$out")" -eq 1 ] || { echo "cleanup ran more than once"; false; }
}
@test "B1 source: installers split EXIT from INT/TERM and exit on signal" {
for f in install_amneziawg.sh install_amneziawg_en.sh; do
# The combined trap must be gone.
run grep -E 'trap _install_cleanup EXIT INT TERM' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still has combined EXIT INT TERM trap"; false; }
run grep -E 'trap _install_cleanup EXIT$' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing EXIT-only cleanup trap"; false; }
run grep -E "_install_on_signal 130' INT" "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing INT signal handler"; false; }
run grep -E "_install_on_signal 143' TERM" "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing TERM signal handler"; false; }
done
}
@test "B1 source: managers split EXIT from INT/TERM and exit on signal" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
run grep -E 'trap _manage_cleanup EXIT INT TERM' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still has combined EXIT INT TERM trap"; false; }
run grep -E "_manage_on_signal 130' INT" "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing INT signal handler"; false; }
run grep -E "_manage_on_signal 143' TERM" "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing TERM signal handler"; false; }
done
}
@test "B1 source: cleanup is idempotent (guard flag) in all four scripts" {
grep -qE '_install_cleaned' "$BATS_TEST_DIRNAME/../install_amneziawg.sh"
grep -qE '_install_cleaned' "$BATS_TEST_DIRNAME/../install_amneziawg_en.sh"
grep -qE '_manage_cleaned' "$BATS_TEST_DIRNAME/../manage_amneziawg.sh"
grep -qE '_manage_cleaned' "$BATS_TEST_DIRNAME/../manage_amneziawg_en.sh"
}
@test "B1 source: restore installs its own INT/TERM rollback hook and clears it" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
run grep -E "_restore_cleanup; exit 130' INT" "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f restore missing INT rollback hook"; false; }
run grep -E "_restore_cleanup; exit 143' TERM" "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f restore missing TERM rollback hook"; false; }
# _restore_cleanup must clear RETURN and RESTORE the global INT/TERM
# handlers (not reset them to default), so B1 survives a restore.
run grep -E 'trap - RETURN$' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f restore does not clear RETURN"; false; }
run grep -cE "_manage_on_signal 1(30|43)'" "$BATS_TEST_DIRNAME/../$f"
# 2 global installs + 2 re-arms inside _restore_cleanup = 4 occurrences.
[ "$output" -ge 4 ] || { echo "$f does not re-arm global signal handlers after restore"; false; }
done
}

View file

@ -0,0 +1,69 @@
#!/usr/bin/env bats
# Code-review findings A1 (--expires atomicity) and A2 (--psk fail-closed).
#
# A1: `manage add --expires=...` incremented the added-client counter regardless
# of set_client_expiry's result and never validated the duration up front, so
# `--expires=bad` created PERMANENT clients while expiry silently failed. The
# fix validates the format once before the loop (aborts before any change) and
# flags a non-zero return if the post-create expiry write fails.
# A2: `generate_client` with CLIENT_PSK=auto (set only by --psk) degraded to a
# PSK-less client on `awg genpsk` failure. It now fails closed (return 1).
load test_helper
@test "A1 parse_duration: rejects bad formats, accepts valid units" {
for bad in bad 1 1x 12 h "" "-1h" "1 h" "1.5h"; do
run parse_duration "$bad"
[ "$status" -ne 0 ] || { echo "accepted invalid duration: $bad"; false; }
done
for ok in 1h 12h 1d 7d 4w; do
run parse_duration "$ok"
[ "$status" -eq 0 ] || { echo "rejected valid duration: $ok"; false; }
done
}
@test "A1 source: add validates --expires before the client loop (die on bad format)" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
# The upfront guard must call parse_duration and die before the loop.
run grep -E 'parse_duration "\$EXPIRES_DURATION" >/dev/null' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing upfront --expires validation"; false; }
# That guard must sit before the add loop, not inside it.
local guard_line loop_line
guard_line=$(grep -n 'parse_duration "\$EXPIRES_DURATION" >/dev/null' "$BATS_TEST_DIRNAME/../$f" | head -1 | cut -d: -f1)
loop_line=$(grep -n '_added=0' "$BATS_TEST_DIRNAME/../$f" | head -1 | cut -d: -f1)
[ "$guard_line" -lt "$loop_line" ] || { echo "$f: --expires guard not before add loop"; false; }
done
}
@test "A1 source: post-create expiry failure sets a non-zero command result" {
# The set_client_expiry if/else block must flag _cmd_rc=1 on failure, and the
# cron-install rc inside the then-branch must also be checked (review finding #3).
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
local block
block=$(grep -A10 'if set_client_expiry "\$_cname" "\$EXPIRES_DURATION"; then' "$BATS_TEST_DIRNAME/../$f")
echo "$block" | grep -q 'else' || { echo "$f: no else on set_client_expiry"; false; }
echo "$block" | grep -q '_cmd_rc=1' || { echo "$f: expiry failure does not set _cmd_rc=1"; false; }
echo "$block" | grep -qE 'install_expiry_cron \|\|' || { echo "$f: cron-install rc not checked"; false; }
done
}
@test "A2 source: generate_client fails closed on awg genpsk failure (no silent PSK-less client)" {
for f in awg_common.sh awg_common_en.sh; do
# The old silent-degradation warning must be gone.
run grep -E 'будет создан без PresharedKey|will be created without PresharedKey' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -ne 0 ] || { echo "$f still degrades to PSK-less client"; false; }
# The auto branch must no longer reset CLIENT_PSK to empty on failure.
run grep -E 'awg genpsk\) \|\| \{' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ]
# And it must carry a fail-closed return inside generate_client.
run grep -E 'NOT created|НЕ создан' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f missing fail-closed PSK error"; false; }
done
}
@test "A2 source: auto-PSK failure path returns 1 (not CLIENT_PSK=\"\")" {
for f in awg_common.sh awg_common_en.sh; do
run grep -Pzo 'awg genpsk\) \|\| \{[^}]*return 1' "$BATS_TEST_DIRNAME/../$f"
[ "$status" -eq 0 ] || { echo "$f auto-PSK failure does not return 1"; false; }
done
}

View file

@ -145,8 +145,11 @@ SHIM
}
@test "manage remove cleans up .vpnuri.png (RU+EN scripts)" {
# Cleanup is now delegated to the shared _remove_client_files helper (which
# includes .vpnuri.png - asserted in test_codereview_artifacts.bats), so
# manage remove must call that helper rather than inline-rm the artifacts.
local RU_MGMT="${BATS_TEST_DIRNAME}/../manage_amneziawg.sh"
local EN_MGMT="${BATS_TEST_DIRNAME}/../manage_amneziawg_en.sh"
grep -qE '_rname\.vpnuri\.png' "$RU_MGMT"
grep -qE '_rname\.vpnuri\.png' "$EN_MGMT"
grep -qE '_remove_client_files "\$_rname"' "$RU_MGMT"
grep -qE '_remove_client_files "\$_rname"' "$EN_MGMT"
}

View file

@ -0,0 +1,117 @@
#!/usr/bin/env bats
# Machine-stable status_code enum in list --json and stats --json (finding 1bkk.6).
#
# The localized `status` field (Активен/Active, Недавно/Recent, ...) is kept for
# display, and a language-independent `status_code` enum is added alongside:
# active | recent | inactive | no_handshake | key_error | no_data
# Behavioral tests run list_clients with JSON output; source-level tests cover
# both list and stats emitters in both language variants.
#
# shellcheck disable=SC2034 # VERBOSE_LIST/NO_COLOR are consumed by the eval'd list_clients
load test_helper
_add_peer() {
local name="$1" ipv4="$2"
cat >> "$SERVER_CONF_FILE" << EOF
[Peer]
#_Name = ${name}
PublicKey = PK_${name}
AllowedIPs = ${ipv4}/32
EOF
}
_make_client_conf() {
local name="$1" ipv4="$2"
cat > "$AWG_DIR/${name}.conf" << EOF
[Interface]
PrivateKey = PRIV_${name}
Address = ${ipv4}/32
DNS = 1.1.1.1
MTU = 1280
[Peer]
PublicKey = SERVERPUB
AllowedIPs = 0.0.0.0/0
EOF
}
# Source-safe loader: extract list_clients with the stubs it depends on.
_load_list_clients() {
local src="$1"
JSON_OUTPUT="${JSON_OUTPUT:-1}"
VERBOSE_LIST=0
NO_COLOR=1
json_escape() { local s="$1"; s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '%s' "$s"; }
format_remaining() { echo "soon"; }
get_client_expiry() { echo ""; }
awg() { return 1; }
eval "$(awk '/^list_clients\(\)/{p=1} p{print} p && /^\}$/{exit}' "$src")"
}
# Valid enum members - any emitted status_code must be one of these.
_VALID_CODES_RE='"status_code":"(active|recent|inactive|no_handshake|key_error|no_data)"'
@test "1bkk.6: list --json emits a valid status_code enum (RU)" {
create_server_config
_add_peer "alice" "10.9.9.2"
_make_client_conf "alice" "10.9.9.2"
export JSON_OUTPUT=1
_load_list_clients "${BATS_TEST_DIRNAME}/../manage_amneziawg.sh"
run list_clients
[ "$status" -eq 0 ]
echo "$output" | grep -qE "$_VALID_CODES_RE" || { echo "no valid status_code in: $output"; false; }
}
@test "1bkk.6: list --json emits a valid status_code enum (EN)" {
create_server_config
_add_peer "bob" "10.9.9.2"
_make_client_conf "bob" "10.9.9.2"
export JSON_OUTPUT=1
_load_list_clients "${BATS_TEST_DIRNAME}/../manage_amneziawg_en.sh"
run list_clients
[ "$status" -eq 0 ]
echo "$output" | grep -qE "$_VALID_CODES_RE" || { echo "no valid status_code in: $output"; false; }
}
@test "1bkk.6: list --json keeps localized status alongside status_code (RU)" {
create_server_config
_add_peer "carol" "10.9.9.2"
_make_client_conf "carol" "10.9.9.2"
export JSON_OUTPUT=1
_load_list_clients "${BATS_TEST_DIRNAME}/../manage_amneziawg.sh"
run list_clients
[ "$status" -eq 0 ]
echo "$output" | grep -q '"status":"'
echo "$output" | grep -q '"status_code":"'
}
@test "1bkk.6 source: both list and stats JSON emitters include status_code (RU)" {
local f="${BATS_TEST_DIRNAME}/../manage_amneziawg.sh"
local n
n=$(grep -cE 'json_entries\+=.*status_code' "$f" || true)
[ "$n" -ge 2 ] || { echo "expected >=2 json emitters with status_code, got $n"; false; }
}
@test "1bkk.6 source: both list and stats JSON emitters include status_code (EN)" {
local f="${BATS_TEST_DIRNAME}/../manage_amneziawg_en.sh"
local n
n=$(grep -cE 'json_entries\+=.*status_code' "$f" || true)
[ "$n" -ge 2 ] || { echo "expected >=2 json emitters with status_code, got $n"; false; }
}
@test "1bkk.6 source: status_code enum values present in both variants" {
for f in manage_amneziawg.sh manage_amneziawg_en.sh; do
local p="${BATS_TEST_DIRNAME}/../$f"
for code in active recent inactive no_handshake key_error; do
grep -q "status_code=\"${code}\"" "$p" || grep -q "st_code=\"${code}\"" "$p" \
|| { echo "$f missing status_code value: $code"; false; }
done
done
}

View file

@ -17,8 +17,8 @@ TMPL_STALE_RE='placeholder:[[:space:]]*"e\.g\.,[[:space:]]*[0-9]+\.[0-9]+\.[0-9]
@test "T6: check-docs-consistency passes on the current repo" {
run bash "$SCRIPT"
[ "$status" -eq 0 ]
# 9 checks since v5.15.3 round-2 added the full OS + arch matrix check (D4).
[[ "$output" == *"9 passed, 0 failed"* ]]
# 10 checks since 1bkk added the OS x arch x prebuilt-target matrix check (#9).
[[ "$output" == *"10 passed, 0 failed"* ]]
}
@test "T6: script defines checks #7 and #8" {

View file

@ -52,8 +52,8 @@ setup() {
[ "$status" -eq 0 ]
}
@test ".4 integration: docs-check still passes 9/9 with ROADMAP included" {
@test ".4 integration: docs-check still passes 10/10 with ROADMAP included" {
run bash "$SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"9 passed, 0 failed"* ]]
[[ "$output" == *"10 passed, 0 failed"* ]]
}

View file

@ -88,14 +88,16 @@ SHIM
}
@test "C4: generate_qr uses a temp file and checks the mv (RU source)" {
run grep -E 'tmp_png="\$\{png_file\}\.tmp\.\$\$"' "$BATS_TEST_DIRNAME/../awg_common.sh"
# Temp now comes from awg_mktemp (shared cleanup registry) instead of a
# manual ${png_file}.tmp.$$; the atomic mv check is unchanged.
run grep -E 'tmp_png=\$\(awg_mktemp "\$AWG_DIR"\)' "$BATS_TEST_DIRNAME/../awg_common.sh"
[ "$status" -eq 0 ]
run grep -E 'if ! mv -f "\$tmp_png" "\$png_file"' "$BATS_TEST_DIRNAME/../awg_common.sh"
[ "$status" -eq 0 ]
}
@test "C4: generate_qr uses a temp file and checks the mv (EN source)" {
run grep -E 'tmp_png="\$\{png_file\}\.tmp\.\$\$"' "$BATS_TEST_DIRNAME/../awg_common_en.sh"
run grep -E 'tmp_png=\$\(awg_mktemp "\$AWG_DIR"\)' "$BATS_TEST_DIRNAME/../awg_common_en.sh"
[ "$status" -eq 0 ]
run grep -E 'if ! mv -f "\$tmp_png" "\$png_file"' "$BATS_TEST_DIRNAME/../awg_common_en.sh"
[ "$status" -eq 0 ]