mirror of
https://github.com/bivlked/amneziawg-installer.git
synced 2026-07-09 17:29:19 +00:00
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:
parent
44a0ab0a49
commit
902f4d787b
31 changed files with 1181 additions and 193 deletions
23
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
23
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
|
@ -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
5
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
22
ADVANCED.md
22
ADVANCED.md
|
|
@ -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 используют свои обычные заголовки.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
26
README.en.md
26
README.en.md
|
|
@ -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.
|
||||
|
|
|
|||
26
README.md
26
README.md
|
|
@ -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>
|
||||
Переустановка сервера не требуется.
|
||||
|
|
|
|||
|
|
@ -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-данных клиента
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`),
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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++))
|
||||
|
|
|
|||
|
|
@ -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++))
|
||||
|
|
|
|||
|
|
@ -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 ==="
|
||||
|
|
|
|||
64
tests/test_codereview_artifacts.bats
Normal file
64
tests/test_codereview_artifacts.bats
Normal 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
|
||||
}
|
||||
74
tests/test_codereview_dns_publicip.bats
Normal file
74
tests/test_codereview_dns_publicip.bats
Normal 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 ]
|
||||
}
|
||||
119
tests/test_codereview_robustness.bats
Normal file
119
tests/test_codereview_robustness.bats
Normal 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
|
||||
}
|
||||
87
tests/test_codereview_signals.bats
Normal file
87
tests/test_codereview_signals.bats
Normal 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
|
||||
}
|
||||
69
tests/test_codereview_strict_flags.bats
Normal file
69
tests/test_codereview_strict_flags.bats
Normal 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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
117
tests/test_status_code_json.bats
Normal file
117
tests/test_status_code_json.bats
Normal 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
|
||||
}
|
||||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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"* ]]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue