v5.15.1 - maintenance release (audit fixes, IPv6 hardening, release gate)

Maintenance release after external code and documentation audits. No new
features; hardens the v5.15.0 dual-stack IPv6 work, tightens several manage
commands, fixes correctness and robustness issues, and adds a release
preflight gate plus a docs-consistency check.

Behavior change: split tunnel + IPv6 mirrors the IPv4 intent - a custom
ALLOWED_IPS keeps that IPv4 split and adds only the tunnel ULA for IPv6
instead of forcing a full ::/0 route. A full tunnel still gets ::/0 (native
IPv6) or the ULA.

Fixed: vpn:// URI carries the server's real MTU/keepalive/DNS; stricter
native-IPv6 detection (global non-ULA address and default route) with the
host IPv6 stack re-enabled before detection; install fails fast on apt update
errors; manage modify validates Endpoint/AllowedIPs; manage add is safe
against name reuse and cleans partial artifacts; manage restore prunes stale
clients/keys and recreates the expiry dir; manage help exit codes; manage
--json keeps stdout pure JSON; log messages stop doubling percent signs.

Docs/process: subnet and OS help accuracy, list --json documented, changelog
compare-links and anchors restored, SECURITY/CONTRIBUTING refreshed, public
RELEASE_PROCESS.md, release publish gated behind a full preflight job, new
docs-consistency workflow, signing design aligned with the asset draft.

Version bumped to 5.15.1 across all six scripts; SHA pins recomputed.
This commit is contained in:
Ivan Bondarev 2026-06-02 14:08:36 +03:00 • committed by GitHub
parent 7eb5e9ebde
commit 1cbee098ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1134 additions and 181 deletions

View file

@ -51,7 +51,7 @@ body:
attributes:
label: Script Version
description: Version of the script (shown in the log header or in the script file header comment).
placeholder: "e.g., 5.10.0"
placeholder: "e.g., 5.15.0"
validations:
required: true

41
.github/workflows/docs-check.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Docs consistency
# Lightweight, fast checks for the classes of drift that slip past the
# shellcheck/test workflows (their paths cover only shell scripts): broken
# internal markdown anchors, version triple drift, stale OS matrix, changelog
# heading/reference mismatch. No build, no network, no bats.
on:
push:
branches: [main]
paths:
- '**/*.md'
- 'docs/**'
- '.github/**'
- 'install_amneziawg*.sh'
- 'manage_amneziawg*.sh'
- 'awg_common*.sh'
pull_request:
paths:
- '**/*.md'
- 'docs/**'
- '.github/**'
- 'install_amneziawg*.sh'
- 'manage_amneziawg*.sh'
- 'awg_common*.sh'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docs-consistency:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run docs consistency checks
run: bash scripts/check-docs-consistency.sh

View file

@ -12,7 +12,36 @@ concurrency:
cancel-in-progress: true
jobs:
# Gate: a tag must clear the full pre-tag checklist before anything is
# published. A fresh install verifies the helper-script SHA pins over the
# network, so a tag with stale pins would yield a broken install straight
# from the release. This job blocks that.
preflight:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Full history so the merge-base for the diff checks is present.
fetch-depth: 0
- name: Fetch main for diff base
# The tag checkout does not bring branch refs. preflight-check.sh diffs
# against origin/main, so make that ref resolvable. A hard failure here
# is intentional: without a base ref the punctuation/marker diff checks
# cannot run.
run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
- name: Install shellcheck and bats
run: sudo apt-get update && sudo apt-get install -y shellcheck bats
- name: Run preflight checks
env:
BASE_REF: origin/main
run: bash scripts/preflight-check.sh
release:
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
@ -25,7 +54,7 @@ jobs:
run: |
TAG="${GITHUB_REF#refs/tags/}"
if gh release view "$TAG" --repo "${{ github.repository }}" > /dev/null 2>&1; then
echo "Release $TAG already exists — skipping"
echo "Release $TAG already exists - skipping"
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"

View file

@ -14,6 +14,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
## [5.15.1] - 2026-06-02
**v5.15.1** - a maintenance release after a round of external code and documentation audits. No new features: it hardens the v5.15.0 dual-stack IPv6 work, tightens several management commands, and fixes a number of correctness and robustness issues. A default install is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.
### Changed
- **Split tunnel + IPv6 now mirrors the IPv4 intent (behavior change).** When a client uses a custom `ALLOWED_IPS` (split tunnel), the dual-stack config keeps that IPv4 split list and adds only the tunnel ULA for IPv6 - it no longer forces a full `::/0` IPv6 route. A full tunnel (`ALLOWED_IPS=0.0.0.0/0`) still gets `::/0` with native IPv6, or the tunnel ULA without it. This affects only clients created with both a split tunnel and `--allow-ipv6-tunnel`. If you relied on the old behavior of IPv6 going full-tunnel while IPv4 stayed split, recreate the client to pick up the new routing.
### Fixed
- **`vpn://` URI now carries the server's real MTU, keepalive, and DNS** instead of fixed values, so a config imported from the URI matches the `.conf` even after the server settings or a `manage modify` changed them.
- **Stricter native-IPv6 detection.** The server is treated as natively IPv6-capable only when it has a global (non-ULA) IPv6 address and a default IPv6 route, so a client gets a full `::/0` route only when it will actually work.
- **The IPv6 host stack is re-enabled before detection** when the tunnel needs it, so a server that had IPv6 disabled via sysctl still gets a working dual-stack tunnel.
- **`install` fails fast if `apt update` errors out**, instead of continuing a multi-step install against a stale package cache.
- **`manage modify` validates the Endpoint and AllowedIPs** it is given (host:port, CIDR list) before writing them.
- **`manage add` is safe against name reuse:** it refuses a name that already exists and cleans up partial key or config artifacts if creation fails partway.
- **`manage restore` prunes stale clients and keys** that are absent from the backup, so a restore yields exactly the backed-up state.
- **`manage help` exits 0** for an explicit help request and 1 only for a real usage error.
- **`manage --json` keeps stdout pure JSON** by routing info and debug logging to stderr.
- **Log messages no longer double percent signs** ("95%" stays "95%").
- **`manage restore` recreates the expiry directory** before restoring expiry timestamps.
### Documentation
- `--subnet` help states the `/24`-only requirement; `install --help` lists Ubuntu 26.04.
- `manage --help` and the README document `list --json` (including the `client_ipv6` field).
- Restored the changelog compare-links for 5.11.0-5.15.0, fixed several broken in-page anchors, refreshed `SECURITY.md` and `CONTRIBUTING.md`, and added `docs/RELEASE_PROCESS.md`.
### Internal
- Tagging a release now runs a full preflight gate (syntax, shellcheck, tests, punctuation, version and SHA-pin consistency, documentation checks) before the GitHub Release is published, plus a lightweight documentation-consistency workflow. The signing design doc was aligned with the asset-upload draft.
---
## [5.15.0] - 2026-06-01
**v5.15.0** - optional dual-stack IPv6 inside the tunnel. Requested by users in [#24](https://github.com/bivlked/amneziawg-installer/issues/24): the new `--allow-ipv6-tunnel` flag hands clients an IPv6 address from the private ULA subnet `fddd:2c4:2c4:2c4::/64` alongside the usual IPv4. Off by default - without the flag the install is identical to v5.14.x. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.
@ -238,7 +272,7 @@ sudo bash ./install_amneziawg_en.sh
### Upgrading an existing server
Run the latest `install_amneziawg.sh` with `--force` (if AmneziaWG is already running) — Step 5 fetches the fresh `manage_amneziawg.sh` and `awg_common.sh` with SHA256 verification. Full commands — [ADVANCED.en.md → Updating the scripts](ADVANCED.en.md#-updating-scripts).
Run the latest `install_amneziawg.sh` with `--force` (if AmneziaWG is already running) — Step 5 fetches the fresh `manage_amneziawg.sh` and `awg_common.sh` with SHA256 verification. Full commands — [ADVANCED.en.md → Updating the scripts](ADVANCED.en.md#update-scripts-adv).
### Tests
@ -281,11 +315,11 @@ chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh
```
3 commands → ~20 minutes → a ready-to-use VPN server with traffic obfuscation. Details — [README → Install](README.en.md#install).
3 commands → ~20 minutes → a ready-to-use VPN server with traffic obfuscation. Details — [README → Install](README.en.md#installation).
### Upgrading an existing server
Run a fresh `install_amneziawg_en.sh` — at step 5, `manage_amneziawg.sh` and `awg_common.sh` are updated automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → How to update the scripts](ADVANCED.en.md#-how-to-update-the-scripts).
Run a fresh `install_amneziawg_en.sh` — at step 5, `manage_amneziawg.sh` and `awg_common.sh` are updated automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → How to update the scripts](ADVANCED.en.md#update-scripts-adv).
### Tests
@ -370,11 +404,11 @@ chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh
```
3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — [README → Install](README.en.md#install).
3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — [README → Install](README.en.md#installation).
### Upgrading an existing server
Run the fresh `install_amneziawg_en.sh` — at step 5 `manage_amneziawg.sh` and `awg_common.sh` are refreshed automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → Updating the scripts](ADVANCED.en.md#-updating-the-scripts).
Run the fresh `install_amneziawg_en.sh` — at step 5 `manage_amneziawg.sh` and `awg_common.sh` are refreshed automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → Updating the scripts](ADVANCED.en.md#update-scripts-adv).
### Tests
@ -406,11 +440,11 @@ chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh
```
3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — [README → Install](README.en.md#install).
3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — [README → Install](README.en.md#installation).
### Upgrading an existing server
Run the fresh `install_amneziawg_en.sh` — at step 5 `manage_amneziawg.sh` and `awg_common.sh` are refreshed automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → Updating the scripts](ADVANCED.en.md#-updating-the-scripts).
Run the fresh `install_amneziawg_en.sh` — at step 5 `manage_amneziawg.sh` and `awg_common.sh` are refreshed automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → Updating the scripts](ADVANCED.en.md#update-scripts-adv).
### Tests
@ -450,7 +484,7 @@ sudo bash ./install_amneziawg_en.sh
### Upgrading an existing server
Re-run the latest `install_amneziawg_en.sh` — step 5 refreshes `manage_amneziawg.sh` and `awg_common.sh` automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → How to Update Scripts](ADVANCED.en.md#-how-to-update-scripts).
Re-run the latest `install_amneziawg_en.sh` — step 5 refreshes `manage_amneziawg.sh` and `awg_common.sh` automatically (with SHA256 verification). Full commands — [ADVANCED.en.md → How to Update Scripts](ADVANCED.en.md#update-scripts-adv).
### Tests
@ -1194,7 +1228,24 @@ Major security and reliability update after several consecutive code audits. The
- Diagnostic report (`--diagnostic`).
- Full uninstall (`--uninstall`).
[Unreleased]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.2...HEAD
[Unreleased]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.1...HEAD
[5.15.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.0...v5.15.1
[5.15.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.5...v5.15.0
[5.14.5]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.4...v5.14.5
[5.14.4]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.3...v5.14.4
[5.14.3]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.2...v5.14.3
[5.14.2]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.1...v5.14.2
[5.14.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.0...v5.14.1
[5.14.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.13.0...v5.14.0
[5.13.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.12.1...v5.13.0
[5.12.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.12.0...v5.12.1
[5.12.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.5...v5.12.0
[5.11.5]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.4...v5.11.5
[5.11.4]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.3...v5.11.4
[5.11.3]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.2...v5.11.3
[5.11.2]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.1...v5.11.2
[5.11.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.0...v5.11.1
[5.11.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.2...v5.11.0
[5.10.2]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.1...v5.10.2
[5.10.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.0...v5.10.1
[5.10.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.9.0...v5.10.0

View file

@ -14,6 +14,40 @@
---
## [5.15.1] - 2026-06-02
**v5.15.1** - поддерживающий релиз после серии внешних аудитов кода и документации. Новых функций нет: укрепляет dual-stack IPv6 из v5.15.0, ужесточает несколько команд управления и устраняет ряд проблем корректности и надёжности. Установка по умолчанию не меняется. Матрица поддержки прежняя: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.
### Изменено
- **Сплит-туннель + IPv6 теперь следует логике IPv4 (изменение поведения).** Если клиент использует свой `ALLOWED_IPS` (сплит-туннель), dual-stack конфиг сохраняет этот IPv4-список и добавляет для IPv6 только ULA туннеля - больше не навязывает полный маршрут `::/0`. Полный туннель (`ALLOWED_IPS=0.0.0.0/0`) по-прежнему получает `::/0` при нативном IPv6 либо ULA туннеля без него. Затрагивает только клиентов, созданных одновременно со сплит-туннелем и `--allow-ipv6-tunnel`. Если вы полагались на прежнее поведение, когда IPv6 уходил в полный туннель при сплит-IPv4, пересоздайте клиента, чтобы применить новую маршрутизацию.
### Исправлено
- **`vpn://` URI теперь несёт реальные MTU, keepalive и DNS сервера** вместо фиксированных значений, так что импортированный из URI конфиг совпадает с `.conf` даже после смены настроек сервера или `manage modify`.
- **Более строгое определение нативного IPv6.** Сервер считается нативно IPv6-способным только при наличии глобального (не-ULA) IPv6-адреса и IPv6-маршрута по умолчанию, поэтому клиент получает полный маршрут `::/0` лишь тогда, когда он действительно будет работать.
- **Хостовый IPv6-стек включается перед детектом**, если он нужен туннелю, так что сервер с ранее отключённым через sysctl IPv6 всё равно получает рабочий dual-stack туннель.
- **`install` падает сразу при ошибке `apt update`** вместо продолжения многошаговой установки на устаревшем кэше пакетов.
- **`manage modify` проверяет переданные Endpoint и AllowedIPs** (host:port, список CIDR) перед записью.
- **`manage add` защищён от повторного имени:** отклоняет уже существующее имя и убирает частичные артефакты ключей или конфига, если создание прервалось.
- **`manage restore` убирает устаревших клиентов и ключи**, которых нет в резервной копии, так что восстановление даёт ровно состояние бэкапа.
- **`manage help` завершается с кодом 0** для явного запроса справки и 1 - только при реальной ошибке использования.
- **`manage --json` держит stdout чистым JSON**, отправляя info- и debug-логи в stderr.
- **Сообщения в логе больше не удваивают знак процента** ("95%" остаётся "95%").
- **`manage restore` пересоздаёт каталог expiry** перед восстановлением меток срока действия.
### Документация
- Справка `--subnet` указывает требование только `/24`; `install --help` перечисляет Ubuntu 26.04.
- `manage --help` и README документируют `list --json` (включая поле `client_ipv6`).
- Восстановлены compare-ссылки changelog для 5.11.0-5.15.0, исправлено несколько битых внутренних якорей, обновлены `SECURITY.md` и `CONTRIBUTING.md`, добавлен `docs/RELEASE_PROCESS.md`.
### Внутреннее
- Простановка тега релиза теперь запускает полный preflight-гейт (синтаксис, shellcheck, тесты, пунктуация, согласованность версий и SHA-пинов, проверки документации) перед публикацией GitHub Release, плюс лёгкий workflow проверки согласованности документации. Документ дизайна подписи приведён в соответствие с черновиком загрузки ассетов.
---
## [5.15.0] - 2026-06-01
**v5.15.0** - опциональный dual-stack IPv6 внутри туннеля. По просьбе пользователей в [#24](https://github.com/bivlked/amneziawg-installer/issues/24): новый флаг `--allow-ipv6-tunnel` выдаёт клиентам IPv6-адрес из приватной подсети ULA `fddd:2c4:2c4:2c4::/64` дополнительно к привычному IPv4. По умолчанию выключено - без флага установка идентична v5.14.x. Поддержка операционных систем без изменений: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.
@ -234,11 +268,11 @@ chmod +x install_amneziawg.sh
sudo bash ./install_amneziawg.sh
```
3 команды → ~20 минут → готовый VPN-сервер с обфускацией трафика. Подробнее — [README → Установка](README.md#установка).
3 команды → ~20 минут → готовый VPN-сервер с обфускацией трафика. Подробнее — [README → Установка](README.md#ustanovka).
### Обновление существующего сервера
Запустите `install_amneziawg.sh` свежей версии с флагом `--force` (если AmneziaWG уже работает) — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#-как-обновить-скрипты).
Запустите `install_amneziawg.sh` свежей версии с флагом `--force` (если AmneziaWG уже работает) — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#update-scripts-adv).
### Тесты
@ -281,11 +315,11 @@ chmod +x install_amneziawg.sh
sudo bash ./install_amneziawg.sh
```
3 команды → ~20 минут → готовый VPN-сервер с обфускацией трафика. Подробнее — [README → Установка](README.md#установка).
3 команды → ~20 минут → готовый VPN-сервер с обфускацией трафика. Подробнее — [README → Установка](README.md#ustanovka).
### Обновление существующего сервера
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#-как-обновить-скрипты).
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#update-scripts-adv).
### Тесты
@ -370,11 +404,11 @@ chmod +x install_amneziawg.sh
sudo bash ./install_amneziawg.sh
```
3 команды → ~20 минут → готовый VPN-сервер с обфускацией трафика. Подробнее — [README → Установка](README.md#установка).
3 команды → ~20 минут → готовый VPN-сервер с обфускацией трафика. Подробнее — [README → Установка](README.md#ustanovka).
### Обновление существующего сервера
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#-как-обновить-скрипты).
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#update-scripts-adv).
### Тесты
@ -410,7 +444,7 @@ sudo bash ./install_amneziawg.sh
### Обновление существующего сервера
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#-как-обновить-скрипты).
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#update-scripts-adv).
### Тесты
@ -450,7 +484,7 @@ sudo bash ./install_amneziawg.sh
### Обновление существующего сервера
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#-как-обновить-скрипты).
Запустите `install_amneziawg.sh` свежей версии — на 5-м шаге `manage_amneziawg.sh` и `awg_common.sh` обновятся автоматически (с проверкой SHA256). Полные команды — [ADVANCED.md → Как обновить скрипты](ADVANCED.md#update-scripts-adv).
### Тесты
@ -1194,7 +1228,24 @@ Hardening-фиксы надёжности и безопасности по ре
- Диагностический отчет (`--diagnostic`).
- Полная деинсталляция (`--uninstall`).
[Unreleased]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.2...HEAD
[Unreleased]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.1...HEAD
[5.15.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.15.0...v5.15.1
[5.15.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.5...v5.15.0
[5.14.5]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.4...v5.14.5
[5.14.4]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.3...v5.14.4
[5.14.3]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.2...v5.14.3
[5.14.2]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.1...v5.14.2
[5.14.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.14.0...v5.14.1
[5.14.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.13.0...v5.14.0
[5.13.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.12.1...v5.13.0
[5.12.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.12.0...v5.12.1
[5.12.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.5...v5.12.0
[5.11.5]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.4...v5.11.5
[5.11.4]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.3...v5.11.4
[5.11.3]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.2...v5.11.3
[5.11.2]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.1...v5.11.2
[5.11.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.11.0...v5.11.1
[5.11.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.2...v5.11.0
[5.10.2]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.1...v5.10.2
[5.10.1]: https://github.com/bivlked/amneziawg-installer/compare/v5.10.0...v5.10.1
[5.10.0]: https://github.com/bivlked/amneziawg-installer/compare/v5.9.0...v5.10.0

View file

@ -52,7 +52,7 @@ Before submitting a PR, ensure:
done
```
3. **Unit tests (bats-core)** pass. Current expected baseline on `v5.14.1`: **510 tests** (v5.14.1 added 18 tests for MTU resolution priority + validation).
3. **Unit tests (bats-core)** pass. The suite grows every release, so there is no fixed count to match - run `bats tests/` and make sure the full suite is green before opening a PR.
```bash
bats tests/
```

View file

@ -20,7 +20,7 @@
<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.0-blue" alt="Version"></a>
<a href="https://github.com/bivlked/amneziawg-installer/releases"><img src="https://img.shields.io/badge/Installer_Version-5.15.1-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>
@ -363,7 +363,7 @@ sudo bash /root/awg/manage_amneziawg.sh <command> [arguments]
| :-------- | :--------------------- | :----------------------------- | :------: |
| `add` | `<name> [name2 ...] [--expires=DUR]` | Add client(s) (opt. with expiry) | No (auto) |
| `remove` | `<name> [name2 ...]` | Remove client(s) | No (auto) |
| `list` | `[-v]` | List clients (`-v` for details)| No |
| `list` | `[-v] [--json]` | List clients (`-v` for details, `--json` machine-readable with `client_ipv6`)| No |
| `regen` | `[client_name]` | Regenerate files (all/one) | No |
| `modify` | `<name> <param> <val>` | Modify a client parameter | No |
| `backup` | | Create a backup | No |
@ -391,6 +391,7 @@ sudo bash /root/awg/manage_amneziawg.sh add my_phone # Add
sudo bash /root/awg/manage_amneziawg.sh add my_iphone --psk # +PresharedKey (Shadowrocket iOS/macOS)
sudo bash /root/awg/manage_amneziawg.sh remove my_phone # Remove
sudo bash /root/awg/manage_amneziawg.sh list # List
sudo bash /root/awg/manage_amneziawg.sh list --json # List as JSON (for scripts)
sudo bash /root/awg/manage_amneziawg.sh regen # Regenerate
# Temporary client (7 days)

View file

@ -20,7 +20,7 @@
<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.0-blue" alt="Version"></a>
<a href="https://github.com/bivlked/amneziawg-installer/releases"><img src="https://img.shields.io/badge/Installer_Version-5.15.1-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>
@ -363,7 +363,7 @@ sudo bash /root/awg/manage_amneziawg.sh <команда> [аргументы]
| :-------- | :--------------------- | :--------------------------- | :-----------: |
| `add` | `<имя> [имя2 ...] [--expires=ВРЕМЯ]` | Добавить клиента(ов) (опц. с истечением) | Нет (авто) |
| `remove` | `<имя> [имя2 ...]` | Удалить клиента(ов) | Нет (авто) |
| `list` | `[-v]` | Список клиентов (`-v` детали) | Нет |
| `list` | `[-v] [--json]` | Список клиентов (`-v` детали, `--json` машиночитаемый с `client_ipv6`) | Нет |
| `regen` | `[имя_клиента]` | Переген. файлы (всех/одного) | Нет |
| `modify` | `<имя> <пар> <зн>` | Изменить параметр клиента | Нет |
| `backup` | | Создать резервную копию | Нет |
@ -391,6 +391,7 @@ sudo bash /root/awg/manage_amneziawg.sh add my_phone # Добавить
sudo bash /root/awg/manage_amneziawg.sh add my_iphone --psk # +PresharedKey (Shadowrocket iOS/macOS)
sudo bash /root/awg/manage_amneziawg.sh remove my_phone # Удалить
sudo bash /root/awg/manage_amneziawg.sh list # Список
sudo bash /root/awg/manage_amneziawg.sh list --json # Список в JSON (для скриптов)
sudo bash /root/awg/manage_amneziawg.sh regen # Перегенерация
# Временный клиент (7 дней)

View file

@ -4,10 +4,9 @@
| Version | Supported |
|---------|--------------------|
| 5.13.x | :white_check_mark: |
| 5.12.x | Security fixes only |
| 5.11.x | Security fixes only |
| < 5.11 | :x: |
| 5.15.x | :white_check_mark: |
| 5.14.x | Security fixes only |
| < 5.14 | :x: |
## Reporting a Vulnerability
@ -49,6 +48,6 @@ Coordinated disclosure. 30 days for a fix before public disclosure. If a vulnera
## Release integrity (planned)
Detached `minisign` signatures for installer and management scripts are planned for v5.14+. Once the maintainer public key is published as `KEYS.txt` in the repository root, users will be able to verify a downloaded script against the maintainer's offline-held private key, independent of GitHub's TLS chain. Design and threat model: [docs/SIGNING_DESIGN.md](docs/SIGNING_DESIGN.md).
Detached `minisign` signatures for installer and management scripts are planned for a future release and are not active yet. Once the maintainer public key is published as `KEYS.txt` in the repository root, users will be able to verify a downloaded script against the maintainer's offline-held private key, independent of GitHub's TLS chain. Design and threat model: [docs/SIGNING_DESIGN.md](docs/SIGNING_DESIGN.md).
Until activation, fetch the installer over HTTPS from `raw.githubusercontent.com` (default in README) - GitHub's TLS still protects the path on the wire, the missing piece is independent maintainer-level signing.

View file

@ -3,8 +3,8 @@
# ==============================================================================
# Общая библиотека функций для AmneziaWG 2.0
# Автор: @bivlked
# Версия: 5.15.0
# Дата: 2026-06-01
# Версия: 5.15.1
# Дата: 2026-06-02
# Репозиторий: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
#
@ -924,8 +924,10 @@ _extract_mtu_from_server_conf() {
# client_ipv6 (необязательный, 7-й аргумент): IPv6-адрес клиента без префикса
# длины (например fddd:2c4:2c4:2c4::5). Если непустой и ALLOW_IPV6_TUNNEL=1:
# - Address = <ipv4>/32, <ipv6>/128
# - AllowedIPs: SERVER_HAS_NATIVE_IPV6=1 -> 0.0.0.0/0, ::/0
# SERVER_HAS_NATIVE_IPV6=0 -> 0.0.0.0/0, <IPV6_SUBNET>
# - AllowedIPs (зеркалю IPv4 routing mode в IPv6, intent-mirroring):
# full tunnel (ALLOWED_IPS=0.0.0.0/0): + ::/0 (native) или + <IPV6_SUBNET> (no-native)
# split tunnel (кастомный ALLOWED_IPS): IPv4-список БЕЗ изменений + ТОЛЬКО <IPV6_SUBNET>,
# НИКОГДА ::/0 - нет IPv6 split-list, нельзя угонять весь IPv6 (ломает split-tunnel).
# Если пустой (legacy-клиент): Address = <ipv4>/32, AllowedIPs без изменений.
render_client_config() {
local name="$1"
@ -941,11 +943,23 @@ render_client_config() {
local conf_file="$AWG_DIR/${name}.conf"
local allowed_ips
if [[ -n "$client_ipv6" ]]; then
if [[ "${SERVER_HAS_NATIVE_IPV6:-0}" == "1" ]]; then
allowed_ips="0.0.0.0/0, ::/0"
# Dual-stack: зеркалю IPv4 routing intent в IPv6.
# full tunnel (IPv4=0.0.0.0/0) -> ::/0 (native) или tunnel-ULA (no-native).
# split tunnel (кастомный ALLOWED_IPS) -> IPv4-split AS-IS + ТОЛЬКО tunnel-ULA,
# никогда ::/0 (нет IPv6 split-list, нельзя угонять весь IPv6).
local ipv4_part ipv6_part
ipv4_part="${ALLOWED_IPS:-0.0.0.0/0}"
if [[ "$ipv4_part" == "0.0.0.0/0" && "${SERVER_HAS_NATIVE_IPV6:-0}" == "1" ]]; then
ipv6_part="::/0"
else
allowed_ips="0.0.0.0/0, ${IPV6_SUBNET:-fddd:2c4:2c4:2c4::/64}"
ipv6_part="${IPV6_SUBNET:-fddd:2c4:2c4:2c4::/64}"
fi
# Защитный de-dup: ALLOWED_IPS по конструкции IPv4-only, но не дублирую
# ipv6_part если он уже присутствует токеном в списке.
case ",${ipv4_part// /}," in
*",${ipv6_part},"*) allowed_ips="$ipv4_part" ;;
*) allowed_ips="${ipv4_part}, ${ipv6_part}" ;;
esac
else
allowed_ips="${ALLOWED_IPS:-0.0.0.0/0}"
fi
@ -1391,12 +1405,25 @@ generate_vpn_uri() {
# затягивает \r в значение, что ломает JSON.allowed_ips).
allowed_ips=$(grep -oP 'AllowedIPs\s*=\s*\K.+' "$conf_file" | tr -d ' \r') || allowed_ips="0.0.0.0/0"
# MTU/PersistentKeepalive/DNS из .conf - могли быть изменены через manage modify.
# Клиент Amnezia при импорте vpn:// использует структурные поля inner JSON
# (awgConfigurator берёт mtu именно из структурного поля, не из embedded config),
# поэтому хардкод рассинхронизировал бы их с .conf - тот же класс, что issue #67
# (structured-поле psk_key было авторитетным).
local mtu keepalive dns_line dns1 dns2
mtu=$(grep -oP '^MTU\s*=\s*\K[0-9]+' "$conf_file" | head -n1); mtu="${mtu:-1280}"
keepalive=$(grep -oP '^PersistentKeepalive\s*=\s*\K[0-9]+' "$conf_file" | head -n1); keepalive="${keepalive:-33}"
dns_line=$(grep -oP '^DNS\s*=\s*\K.+' "$conf_file" | head -n1 | tr -d ' \r')
dns1="${dns_line%%,*}"; dns1="${dns1:-1.1.1.1}"
if [[ "$dns_line" == *,* ]]; then dns2="${dns_line#*,}"; dns2="${dns2%%,*}"; else dns2="$dns1"; fi
local vpn_uri perl_err
perl_err=$(awg_mktemp) || perl_err="/tmp/awg_perl_err.$$"
# shellcheck disable=SC2016
vpn_uri=$(perl -MCompress::Zlib -MMIME::Base64 -e '
my ($conf_path, $h1,$h2,$h3,$h4, $jc,$jmin,$jmax,
$s1,$s2,$s3,$s4, $i1, $port, $ep, $cip, $cipv6, $cpk, $spk, $aips, $psk) = @ARGV;
$s1,$s2,$s3,$s4, $i1, $port, $ep, $cip, $cipv6, $cpk, $spk, $aips, $psk,
$mtu, $keepalive, $dns1, $dns2) = @ARGV;
open my $fh, "<", $conf_path or die;
local $/; my $raw = <$fh>; close $fh;
@ -1430,8 +1457,8 @@ generate_vpn_uri() {
$inner .= qq("psk_key":"$epsk",);
}
$inner .= qq("config":"$eraw",);
$inner .= qq("hostName":"$ep","mtu":"1280",);
$inner .= qq("persistent_keep_alive":"33","port":$port,);
$inner .= qq("hostName":"$ep","mtu":"$mtu",);
$inner .= qq("persistent_keep_alive":"$keepalive","port":$port,);
$inner .= qq("server_pub_key":"$spk"});
my $einner = je($inner);
@ -1442,7 +1469,8 @@ generate_vpn_uri() {
$outer .= qq("transport_proto":"udp"\},"container":"amnezia-awg"\}],);
$outer .= qq("defaultContainer":"amnezia-awg",);
$outer .= qq("description":"AWG Server",);
$outer .= qq("dns1":"1.1.1.1","dns2":"1.0.0.1",);
my $ed1 = je($dns1); my $ed2 = je($dns2);
$outer .= qq("dns1":"$ed1","dns2":"$ed2",);
$outer .= qq("hostName":"$ep"});
my $compressed = compress($outer);
@ -1456,7 +1484,8 @@ generate_vpn_uri() {
"$AWG_Jc" "$AWG_Jmin" "$AWG_Jmax" \
"$AWG_S1" "$AWG_S2" "$AWG_S3" "$AWG_S4" \
"$AWG_I1" "$AWG_PORT" "$endpoint" \
"$client_ip" "$client_ipv6" "$client_privkey" "$server_pubkey" "$allowed_ips" "$client_psk" 2>"$perl_err"
"$client_ip" "$client_ipv6" "$client_privkey" "$server_pubkey" "$allowed_ips" "$client_psk" \
"$mtu" "$keepalive" "$dns1" "$dns2" 2>"$perl_err"
)
if [[ -z "$vpn_uri" ]]; then
@ -1520,6 +1549,13 @@ generate_qr_vpnuri() {
return 0
}
# Удаляет частично созданные артефакты клиента (ключи + .conf). Используется
# в early-error путях generate_client - C10: не оставлять orphan-ключи при сбое
# до коммита пира в серверный конфиг.
_rollback_client_artifacts() {
rm -f "$KEYS_DIR/$1.private" "$KEYS_DIR/$1.public" "$AWG_DIR/$1.conf"
}
# Полный цикл создания клиента:
# keypair → next IP → client config → add peer → QR
# generate_client <name> [endpoint]
@ -1561,29 +1597,40 @@ generate_client() {
return 1
fi
# Генерация ключей
generate_keypair "$name" || { exec {lock_fd}>&-; return 1; }
# C6: клиент не должен уже существовать. Проверяю ПОД локом, ДО генерации
# ключей - иначе `add <существующее_имя>` молча перезатёр бы ключи живого
# клиента (generate_keypair перезаписывает безусловно), а параллельный add
# того же имени гонялся бы за перезапись.
if [[ -e "$KEYS_DIR/${name}.private" || -e "$KEYS_DIR/${name}.public" || -e "$AWG_DIR/${name}.conf" ]]; then
log_error "Клиент '$name' уже существует. Используйте 'remove' или другое имя."
exec {lock_fd}>&-
return 1
fi
# Генерация ключей. С этого момента любой ранний сбой обязан удалить уже
# созданные ключи/conf (C10) через _rollback_client_artifacts.
generate_keypair "$name" || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# Следующий свободный IP
local client_ip
client_ip=$(get_next_client_ip) || { exec {lock_fd}>&-; return 1; }
client_ip=$(get_next_client_ip) || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# IPv6-адрес клиента (при ALLOW_IPV6_TUNNEL=1)
local client_ipv6=""
if [[ "${ALLOW_IPV6_TUNNEL:-0}" == "1" ]]; then
client_ipv6=$(get_next_client_ipv6 "$client_ip") || { exec {lock_fd}>&-; return 1; }
client_ipv6=$(get_next_client_ipv6 "$client_ip") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
log_debug "Выделен IPv6-адрес ${client_ipv6} для клиента ${name}"
fi
# Читаем ключи
local client_privkey client_pubkey server_pubkey
client_privkey=$(cat "$KEYS_DIR/${name}.private") || { exec {lock_fd}>&-; return 1; }
client_pubkey=$(cat "$KEYS_DIR/${name}.public") || { exec {lock_fd}>&-; return 1; }
client_privkey=$(cat "$KEYS_DIR/${name}.private") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
client_pubkey=$(cat "$KEYS_DIR/${name}.public") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# Пытаемся восстановить server_public.key из awg0.conf если кеша нет
# (поддержка ручных установок без installer-шага 6).
_ensure_server_public_key || { exec {lock_fd}>&-; return 1; }
server_pubkey=$(cat "$AWG_DIR/server_public.key") || { exec {lock_fd}>&-; return 1; }
_ensure_server_public_key || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
server_pubkey=$(cat "$AWG_DIR/server_public.key") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# Endpoint: из аргумента → AWG_ENDPOINT (awgsetup_cfg.init) → curl до
# внешних сервисов → локальный IP с сетевого интерфейса.
@ -1600,22 +1647,23 @@ generate_client() {
fi
if [[ -z "$endpoint" ]]; then
log_error "Не удалось определить внешний IP сервера. Используйте --endpoint=IP"
_rollback_client_artifacts "$name"
exec {lock_fd}>&-
return 1
fi
# Конфиг клиента
render_client_config "$name" "$client_ip" "$client_privkey" "$server_pubkey" "$endpoint" "${AWG_PORT}" "$client_ipv6" || {
log_error "Откат: удаление ключей '$name'"
rm -f "$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
log_error "Откат: удаление артефактов '$name'"
_rollback_client_artifacts "$name"
exec {lock_fd}>&-
return 1
}
# Добавляем пир в серверный конфиг
if ! add_peer_to_server "$name" "$client_pubkey" "$client_ip" "$client_ipv6"; then
log_error "Откат: удаление файлов '$name'"
rm -f "$AWG_DIR/${name}.conf" "$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
log_error "Откат: удаление артефактов '$name'"
_rollback_client_artifacts "$name"
exec {lock_fd}>&-
return 1
fi

View file

@ -3,8 +3,8 @@
# ==============================================================================
# Shared function library for AmneziaWG 2.0
# Author: @bivlked
# Version: 5.15.0
# Date: 2026-06-01
# Version: 5.15.1
# Date: 2026-06-02
# Repository: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
#
@ -928,8 +928,10 @@ _extract_mtu_from_server_conf() {
# client_ipv6 (optional 7th argument): client IPv6 address without prefix
# length (e.g. fddd:2c4:2c4:2c4::5). If non-empty and ALLOW_IPV6_TUNNEL=1:
# - Address = <ipv4>/32, <ipv6>/128
# - AllowedIPs: SERVER_HAS_NATIVE_IPV6=1 -> 0.0.0.0/0, ::/0
# SERVER_HAS_NATIVE_IPV6=0 -> 0.0.0.0/0, <IPV6_SUBNET>
# - AllowedIPs (mirror the IPv4 routing mode into IPv6, intent-mirroring):
# full tunnel (ALLOWED_IPS=0.0.0.0/0): + ::/0 (native) or + <IPV6_SUBNET> (no-native)
# split tunnel (custom ALLOWED_IPS): IPv4 list UNCHANGED + ONLY <IPV6_SUBNET>,
# NEVER ::/0 - there is no IPv6 split-list, hijacking all IPv6 breaks split-tunnel.
# If empty (legacy client): Address = <ipv4>/32, AllowedIPs unchanged.
render_client_config() {
local name="$1"
@ -945,11 +947,23 @@ render_client_config() {
local conf_file="$AWG_DIR/${name}.conf"
local allowed_ips
if [[ -n "$client_ipv6" ]]; then
if [[ "${SERVER_HAS_NATIVE_IPV6:-0}" == "1" ]]; then
allowed_ips="0.0.0.0/0, ::/0"
# Dual-stack: mirror the IPv4 routing intent into IPv6.
# full tunnel (IPv4=0.0.0.0/0) -> ::/0 (native) or tunnel ULA (no-native).
# split tunnel (custom ALLOWED_IPS) -> IPv4 split AS-IS + ONLY tunnel ULA,
# never ::/0 (no IPv6 split-list, must not hijack all IPv6).
local ipv4_part ipv6_part
ipv4_part="${ALLOWED_IPS:-0.0.0.0/0}"
if [[ "$ipv4_part" == "0.0.0.0/0" && "${SERVER_HAS_NATIVE_IPV6:-0}" == "1" ]]; then
ipv6_part="::/0"
else
allowed_ips="0.0.0.0/0, ${IPV6_SUBNET:-fddd:2c4:2c4:2c4::/64}"
ipv6_part="${IPV6_SUBNET:-fddd:2c4:2c4:2c4::/64}"
fi
# Defensive de-dup: ALLOWED_IPS is IPv4-only by construction, but do not
# duplicate ipv6_part if it is already present as a token in the list.
case ",${ipv4_part// /}," in
*",${ipv6_part},"*) allowed_ips="$ipv4_part" ;;
*) allowed_ips="${ipv4_part}, ${ipv6_part}" ;;
esac
else
allowed_ips="${ALLOWED_IPS:-0.0.0.0/0}"
fi
@ -1396,12 +1410,25 @@ generate_vpn_uri() {
# captures \r into the value, which breaks JSON.allowed_ips).
allowed_ips=$(grep -oP 'AllowedIPs\s*=\s*\K.+' "$conf_file" | tr -d ' \r') || allowed_ips="0.0.0.0/0"
# MTU/PersistentKeepalive/DNS from .conf - these can be changed via manage modify.
# On vpn:// import the Amnezia client uses the structured inner-JSON fields
# (awgConfigurator takes mtu from the structured field, not the embedded config),
# so hardcoding them would desync from .conf - same class as issue #67 (the
# structured psk_key field was authoritative).
local mtu keepalive dns_line dns1 dns2
mtu=$(grep -oP '^MTU\s*=\s*\K[0-9]+' "$conf_file" | head -n1); mtu="${mtu:-1280}"
keepalive=$(grep -oP '^PersistentKeepalive\s*=\s*\K[0-9]+' "$conf_file" | head -n1); keepalive="${keepalive:-33}"
dns_line=$(grep -oP '^DNS\s*=\s*\K.+' "$conf_file" | head -n1 | tr -d ' \r')
dns1="${dns_line%%,*}"; dns1="${dns1:-1.1.1.1}"
if [[ "$dns_line" == *,* ]]; then dns2="${dns_line#*,}"; dns2="${dns2%%,*}"; else dns2="$dns1"; fi
local vpn_uri perl_err
perl_err=$(awg_mktemp) || perl_err="/tmp/awg_perl_err.$$"
# shellcheck disable=SC2016
vpn_uri=$(perl -MCompress::Zlib -MMIME::Base64 -e '
my ($conf_path, $h1,$h2,$h3,$h4, $jc,$jmin,$jmax,
$s1,$s2,$s3,$s4, $i1, $port, $ep, $cip, $cipv6, $cpk, $spk, $aips, $psk) = @ARGV;
$s1,$s2,$s3,$s4, $i1, $port, $ep, $cip, $cipv6, $cpk, $spk, $aips, $psk,
$mtu, $keepalive, $dns1, $dns2) = @ARGV;
open my $fh, "<", $conf_path or die;
local $/; my $raw = <$fh>; close $fh;
@ -1435,8 +1462,8 @@ generate_vpn_uri() {
$inner .= qq("psk_key":"$epsk",);
}
$inner .= qq("config":"$eraw",);
$inner .= qq("hostName":"$ep","mtu":"1280",);
$inner .= qq("persistent_keep_alive":"33","port":$port,);
$inner .= qq("hostName":"$ep","mtu":"$mtu",);
$inner .= qq("persistent_keep_alive":"$keepalive","port":$port,);
$inner .= qq("server_pub_key":"$spk"});
my $einner = je($inner);
@ -1447,7 +1474,8 @@ generate_vpn_uri() {
$outer .= qq("transport_proto":"udp"\},"container":"amnezia-awg"\}],);
$outer .= qq("defaultContainer":"amnezia-awg",);
$outer .= qq("description":"AWG Server",);
$outer .= qq("dns1":"1.1.1.1","dns2":"1.0.0.1",);
my $ed1 = je($dns1); my $ed2 = je($dns2);
$outer .= qq("dns1":"$ed1","dns2":"$ed2",);
$outer .= qq("hostName":"$ep"});
my $compressed = compress($outer);
@ -1461,7 +1489,8 @@ generate_vpn_uri() {
"$AWG_Jc" "$AWG_Jmin" "$AWG_Jmax" \
"$AWG_S1" "$AWG_S2" "$AWG_S3" "$AWG_S4" \
"$AWG_I1" "$AWG_PORT" "$endpoint" \
"$client_ip" "$client_ipv6" "$client_privkey" "$server_pubkey" "$allowed_ips" "$client_psk" 2>"$perl_err"
"$client_ip" "$client_ipv6" "$client_privkey" "$server_pubkey" "$allowed_ips" "$client_psk" \
"$mtu" "$keepalive" "$dns1" "$dns2" 2>"$perl_err"
)
if [[ -z "$vpn_uri" ]]; then
@ -1526,6 +1555,13 @@ generate_qr_vpnuri() {
return 0
}
# Removes partially created client artifacts (keys + .conf). Used by the
# early-error paths of generate_client - C10: do not leave orphan keys when a
# step fails before the peer is committed to the server config.
_rollback_client_artifacts() {
rm -f "$KEYS_DIR/$1.private" "$KEYS_DIR/$1.public" "$AWG_DIR/$1.conf"
}
# Full client creation cycle:
# keypair -> next IP -> client config -> add peer -> QR
# generate_client <name> [endpoint]
@ -1566,29 +1602,40 @@ generate_client() {
return 1
fi
# Generate keys
generate_keypair "$name" || { exec {lock_fd}>&-; return 1; }
# C6: the client must not already exist. Check UNDER the lock, BEFORE
# generating keys - otherwise `add <existing_name>` would silently overwrite
# a live client's keys (generate_keypair overwrites unconditionally), and a
# concurrent same-name add would race to overwrite.
if [[ -e "$KEYS_DIR/${name}.private" || -e "$KEYS_DIR/${name}.public" || -e "$AWG_DIR/${name}.conf" ]]; then
log_error "Client '$name' already exists. Use 'remove' or a different name."
exec {lock_fd}>&-
return 1
fi
# Generate keys. From here on, any early failure must remove the freshly
# created keys/conf (C10) via _rollback_client_artifacts.
generate_keypair "$name" || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# Next free IP
local client_ip
client_ip=$(get_next_client_ip) || { exec {lock_fd}>&-; return 1; }
client_ip=$(get_next_client_ip) || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# IPv6 address for client (when ALLOW_IPV6_TUNNEL=1)
local client_ipv6=""
if [[ "${ALLOW_IPV6_TUNNEL:-0}" == "1" ]]; then
client_ipv6=$(get_next_client_ipv6 "$client_ip") || { exec {lock_fd}>&-; return 1; }
client_ipv6=$(get_next_client_ipv6 "$client_ip") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
log_debug "Allocated IPv6 address ${client_ipv6} for client ${name}"
fi
# Read keys
local client_privkey client_pubkey server_pubkey
client_privkey=$(cat "$KEYS_DIR/${name}.private") || { exec {lock_fd}>&-; return 1; }
client_pubkey=$(cat "$KEYS_DIR/${name}.public") || { exec {lock_fd}>&-; return 1; }
client_privkey=$(cat "$KEYS_DIR/${name}.private") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
client_pubkey=$(cat "$KEYS_DIR/${name}.public") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# Try to reconstruct server_public.key from awg0.conf when the cache
# is missing (supports manual setups without the installer step 6).
_ensure_server_public_key || { exec {lock_fd}>&-; return 1; }
server_pubkey=$(cat "$AWG_DIR/server_public.key") || { exec {lock_fd}>&-; return 1; }
_ensure_server_public_key || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
server_pubkey=$(cat "$AWG_DIR/server_public.key") || { _rollback_client_artifacts "$name"; exec {lock_fd}>&-; return 1; }
# Endpoint: argument → AWG_ENDPOINT (awgsetup_cfg.init) → curl to
# external services → local IP on a network interface.
@ -1605,22 +1652,23 @@ generate_client() {
fi
if [[ -z "$endpoint" ]]; then
log_error "Failed to determine server public IP. Use --endpoint=IP"
_rollback_client_artifacts "$name"
exec {lock_fd}>&-
return 1
fi
# Client config
render_client_config "$name" "$client_ip" "$client_privkey" "$server_pubkey" "$endpoint" "${AWG_PORT}" "$client_ipv6" || {
log_error "Rollback: deleting keys for '$name'"
rm -f "$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
log_error "Rollback: removing artifacts for '$name'"
_rollback_client_artifacts "$name"
exec {lock_fd}>&-
return 1
}
# Add peer to server config
if ! add_peer_to_server "$name" "$client_pubkey" "$client_ip" "$client_ipv6"; then
log_error "Rollback: deleting files for '$name'"
rm -f "$AWG_DIR/${name}.conf" "$KEYS_DIR/${name}.private" "$KEYS_DIR/${name}.public"
log_error "Rollback: removing artifacts for '$name'"
_rollback_client_artifacts "$name"
exec {lock_fd}>&-
return 1
fi

118
docs/RELEASE_PROCESS.md Normal file
View file

@ -0,0 +1,118 @@
# Release process
This is the maintainer runbook for cutting a tagged release of
amneziawg-installer. It is the public, self-contained reference that
`scripts/preflight-check.sh` points at. Contributors do not need to run a
release, but a green `preflight-check.sh` is the bar a release branch must
clear before it is tagged.
## Versioned files and the lockstep rule
Six scripts carry a version and must stay in lockstep:
- `install_amneziawg.sh`, `install_amneziawg_en.sh`
- `manage_amneziawg.sh`, `manage_amneziawg_en.sh`
- `awg_common.sh`, `awg_common_en.sh`
When any of them changes in a release:
1. Bump `SCRIPT_VERSION` and the `# Version:` / `# Версия:` header (plus the
date) only in the files that actually changed.
2. The installers download the helper scripts over the network and verify
their SHA256 against pinned constants. If `awg_common*.sh` or
`manage_amneziawg*.sh` changed, recompute the four pins
(`COMMON_SCRIPT_SHA256` / `MANAGE_SCRIPT_SHA256`, RU + EN) with:
```bash
bash scripts/update-sha-pins.sh # rewrite the four pins
bash scripts/update-sha-pins.sh --verify # confirm they match
```
A mismatched pin means a fresh install fails the over-the-network integrity
check, so the pins must be recomputed after the helper scripts are final and
before the tag is pushed.
## Pre-tag checklist
Run the full gate in one shot:
```bash
BASE_REF=origin/main bash scripts/preflight-check.sh
```
`preflight-check.sh` runs, in order:
1. `bash -n` on the six scripts.
2. `shellcheck -s bash -S warning` on the six scripts.
3. `bats tests/`.
4. No newly added em-dash / en-dash (U+2013 / U+2014) in the diff against
`BASE_REF`. New text is hyphen-minus only. Legacy dashes in existing lines
are kept (no mass purge); a word-diff is used so that point-editing a legacy
line does not flag a dash it already contained - only a genuinely new dash
fails. If you do touch such a line, converting its dashes to hyphens is
welcome but not required.
5. No AI / tool markers introduced in the diff or in the commit-message log.
6. No `Co-authored-by` trailers in the commit-message log.
7. `SCRIPT_VERSION` and the six version headers agree.
8. SHA pins in lockstep (`update-sha-pins.sh --verify`).
9. Documentation consistency (`scripts/check-docs-consistency.sh`): internal
anchors resolve, changelog headings have reference links and the RU/EN
version sets match, the version triple agrees, the OS matrix is current.
`BASE_REF` selects the ref the diff checks compare against. If you do not set
it, the script tries `main`, then `origin/main`. On a detached checkout with no
local `main` (for example a freshly cloned CI runner on a tag), set it
explicitly: `BASE_REF=origin/main`. `LOG_RANGE` (default `<base>..HEAD`)
selects which commits the message checks read; merge commits brought in from
`main` are outside the branch range and are not re-checked.
Also confirm by hand before tagging:
- CHANGELOG entry added to both `CHANGELOG.md` and `CHANGELOG.en.md` in the
exact heading format `## [X.Y.Z] - YYYY-MM-DD` (the release workflow parses it
with awk), plus the matching `[X.Y.Z]:` reference link, and the `[Unreleased]`
link retargeted to `vX.Y.Z...HEAD`.
- The RU and EN changelogs list the same set of versions.
- 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).
## Tagging and the two release workflows
A tag push (`git push origin vX.Y.Z`) triggers two independent workflows:
- `release.yml` (about 30 seconds): runs the preflight gate, then creates the
GitHub Release from the matching `CHANGELOG.en.md` section. The publish step
depends on the gate, so a tag that fails preflight does not produce a
release.
- `arm-build.yml` (about 20-30 minutes): builds the ARM prebuilt `.deb`
packages under QEMU and publishes them to the separate `arm-packages`
release. This is a separate, slower track and does not block the main
release.
The release is not finished until both runs are green.
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`),
fix the branch, and re-tag.
## Release notes
`release.yml` fills the English release body from `CHANGELOG.en.md`. Add the
Russian section by hand afterwards:
```bash
gh release edit vX.Y.Z --notes-file body.md
```
Follow the established bilingual template: Russian header, a
`[English version below](#english-version)` link, the Russian content, an
`<a id="english-version"></a>` anchor, then the English content.
## Release signing
Release signing (minisign detached signatures) is designed but not active. See
`docs/SIGNING_DESIGN.md`. The draft workflow `docs/release-sign.yml.draft` is
not installed under `.github/workflows/` until a maintainer public key is
published as `KEYS.txt`.

View file

@ -94,11 +94,11 @@ 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 new job in `release.yml` 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 (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.
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.
A draft of Option B is committed at `docs/release-sign.yml.draft` for review. It is NOT placed in `.github/workflows/` until the public key is published.
A draft of Option B is committed at `docs/release-sign.yml.draft` for review. It is a standalone `workflow_dispatch` workflow (not a job added to `release.yml`), and is NOT placed in `.github/workflows/` until the public key is published. When activated it uploads, for each of the six scripts, the script itself plus its `.minisig`, and also attaches `KEYS.txt` - so everything needed to verify is available from a single release. `release.yml` itself does not publish the `.sh` files as assets; that is intentionally left to the signing workflow to avoid two code paths uploading the same files. Until signing is activated, the canonical way to obtain a script is still the documented `wget` from `raw.githubusercontent.com`.
## User-side verification

View file

@ -8,15 +8,15 @@ fi
# ==============================================================================
# Скрипт для установки и настройки AmneziaWG 2.0 на Ubuntu/Debian серверах
# Автор: @bivlked
# Версия: 5.15.0
# Дата: 2026-06-01
# Версия: 5.15.1
# Дата: 2026-06-02
# Репозиторий: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Безопасный режим и Константы ---
set -o pipefail
SCRIPT_VERSION="5.15.0"
SCRIPT_VERSION="5.15.1"
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="3aad0993439f746fa0780f974026e0238b4d3b4587c8303e784d08385e15eac3"
MANAGE_SCRIPT_SHA256="64453fc8d32dc21939ce6bf85b64c243685c97f10a100d889b3422d3bbe19481"
COMMON_SCRIPT_SHA256="6e0634c89ddb69c47ca423eb8a6efd0b974368e8122fefeddb6e22c2425f4c2e"
MANAGE_SCRIPT_SHA256="6d2cbc25a86ff8577344e416613bce7357389d7fea4b3988e2449a2e8b8a54e0"
# Флаги CLI
UNINSTALL=0; HELP=0; DIAGNOSTIC=0; VERBOSE=0; NO_COLOR=0; AUTO_YES=0; NO_TWEAKS=0
@ -92,9 +92,7 @@ log_msg() {
local type="$1" msg="$2"
local ts
ts=$(date +'%F %T')
local safe_msg
safe_msg="${msg//%/%%}"
local entry="[$ts] $type: $safe_msg"
local entry="[$ts] $type: $msg"
local color_start="" color_end=""
if [[ "$NO_COLOR" -eq 0 ]]; then
@ -249,7 +247,7 @@ apt_wait_for_ppa_package() {
show_help() {
cat << 'EOF'
Использование: sudo bash install_amneziawg.sh [ОПЦИИ]
Скрипт для установки и настройки AmneziaWG 2.0 на Ubuntu (24.04 / 25.10) и Debian (12 / 13).
Скрипт для установки и настройки AmneziaWG 2.0 на Ubuntu (24.04 / 25.10 / 26.04) и Debian (12 / 13).
Опции:
-h, --help Показать эту справку и выйти
@ -261,7 +259,7 @@ show_help() {
--ssh-port=ПОРТ SSH-порт для правила UFW (по умолчанию определяется
автоматически; список через запятую). Используйте, если
SSH на нестандартном порту и автодетект недоступен
--subnet=ПОДСЕТЬ Установить подсеть туннеля (x.x.x.x/yy) неинтерактивно
--subnet=ПОДСЕТЬ Подсеть туннеля, только /24 (напр. 10.9.9.1/24) неинтерактивно
--allow-ipv6 Оставить IPv6 включенным неинтерактивно
--disallow-ipv6 Принудительно отключить IPv6 неинтерактивно
--allow-ipv6-tunnel Включить dual-stack IPv6 внутри туннеля (ULA, opt-in)
@ -455,7 +453,12 @@ install_packages() {
fi
log "Установка: ${to_install[*]}..."
if [[ "${_APT_UPDATED:-0}" -eq 0 ]]; then
apt_update_tolerant || log_warn "Не удалось обновить apt."
# C4: жёсткая ошибка apt_update_tolerant (GPG / сеть на binary-репо / OOM) -
# это НЕ source-шум, а реальный сбой; продолжать на устаревшем кэше нельзя
# (контракт стр.138, как у вызовов 1975/2108). die завершает установку, так
# что _APT_UPDATED=1 проставляется только при успехе - иначе повторный
# install_packages в этой сессии молча пропустил бы update.
apt_update_tolerant || die "Ошибка apt update."
_APT_UPDATED=1
fi
if ! DEBIAN_FRONTEND=noninteractive apt install -y "${to_install[@]}"; then
@ -510,10 +513,23 @@ configure_ipv6() {
}
# Определение наличия native IPv6 на VPS.
# Глобальный scope IPv6 (не link-local fe80::) означает выход в IPv6-интернет.
# Эхо 1 если найден global IPv6, иначе 0.
# Native IPv6 = глобально маршрутизируемый адрес (НЕ ULA fc00::/7, НЕ link-local
# fe80::) И наличие IPv6 default-маршрута. Любого из условий по отдельности мало:
# - global-адрес без default route -> нет выхода в IPv6-интернет (клиент с ::/0
# получит black-hole);
# - ULA (fddd::/...) для `ip` имеет scope global, но в интернет не маршрутизируется.
# Эхо 1 только когда выполнены оба условия, иначе 0.
detect_native_ipv6() {
if ip -6 addr show scope global 2>/dev/null | grep -q "inet6"; then
local have_addr=0 have_route=0
if ip -6 addr show scope global 2>/dev/null \
| grep -oP 'inet6\s+\K[0-9a-fA-F:]+' \
| grep -qviE '^(fc|fd)'; then
have_addr=1
fi
if ip -6 route show default 2>/dev/null | grep -q .; then
have_route=1
fi
if [[ "$have_addr" -eq 1 && "$have_route" -eq 1 ]]; then
echo 1
else
echo 0
@ -527,12 +543,22 @@ configure_ipv6_tunnel() {
ALLOW_IPV6_TUNNEL=0
fi
: "${IPV6_SUBNET:=fddd:2c4:2c4:2c4::/64}"
# Native IPv6 определяю при каждом запуске (кэширую в init для client render Phase 4).
SERVER_HAS_NATIVE_IPV6=$(detect_native_ipv6)
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 && "$DISABLE_IPV6" -eq 1 ]]; then
log_warn "--allow-ipv6-tunnel requires host IPv6 forwarding; overriding --disallow-ipv6 (DISABLE_IPV6=0)"
DISABLE_IPV6=0
# IPv6-туннель требует включённого IPv6 на хосте. Снимаю --disallow-ipv6 И
# активно включаю IPv6 в рантайме ДО detection/render: при upgrade с дефолтной
# прошлой установки (IPv6 был выключен в рантайме) ядро скрывает все IPv6-адреса,
# поэтому detect_native_ipv6 дал бы false-negative, а клиент отрендерился бы с
# IPv6 Address при выключенном в ядре IPv6 (awg-quick restart может упасть). weaq P1.
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 ]]; then
if [[ "$DISABLE_IPV6" -eq 1 ]]; then
log_warn "--allow-ipv6-tunnel requires host IPv6 forwarding; overriding --disallow-ipv6 (DISABLE_IPV6=0)"
DISABLE_IPV6=0
fi
sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.lo.disable_ipv6=0 >/dev/null 2>&1 || true
fi
# Native IPv6 определяю ПОСЛЕ runtime-включения (кэширую в init для client render Phase 4).
SERVER_HAS_NATIVE_IPV6=$(detect_native_ipv6)
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 && "$SERVER_HAS_NATIVE_IPV6" -eq 0 ]]; then
log_warn "Native IPv6 не обнаружен на VPS - туннель IPv6 будет работать peer-to-peer без выхода в IPv6-интернет."
fi

View file

@ -8,14 +8,14 @@ fi
# ==============================================================================
# AmneziaWG 2.0 installation and configuration script for Ubuntu/Debian servers
# Author: @bivlked
# Version: 5.15.0
# Date: 2026-06-01
# Version: 5.15.1
# Date: 2026-06-02
# Repository: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Safe mode and Constants ---
set -o pipefail
SCRIPT_VERSION="5.15.0"
SCRIPT_VERSION="5.15.1"
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="6e01a699f520a36b0cd65c2e0a1c357a92a06d14f68ffd35498551ae2f5e12f8"
MANAGE_SCRIPT_SHA256="baa49a273539e9d9d8e342f9c2a914161fda96b58b4f016261744effa4b028b6"
COMMON_SCRIPT_SHA256="25b741ee04b294963d082cd11afdef1fd14e18bff88a14376bef12cee13a226f"
MANAGE_SCRIPT_SHA256="ce2d392c396d08915ea24eaa51663f12c3530dd9b044314feb3f585620a8a06b"
# CLI flags
UNINSTALL=0; HELP=0; DIAGNOSTIC=0; VERBOSE=0; NO_COLOR=0; AUTO_YES=0; NO_TWEAKS=0
@ -92,9 +92,7 @@ log_msg() {
local type="$1" msg="$2"
local ts
ts=$(date +'%F %T')
local safe_msg
safe_msg="${msg//%/%%}"
local entry="[$ts] $type: $safe_msg"
local entry="[$ts] $type: $msg"
local color_start="" color_end=""
if [[ "$NO_COLOR" -eq 0 ]]; then
@ -252,7 +250,7 @@ apt_wait_for_ppa_package() {
show_help() {
cat << 'EOF'
Usage: sudo bash install_amneziawg_en.sh [OPTIONS]
Script for installation and configuration of AmneziaWG 2.0 on Ubuntu (24.04 / 25.10) and Debian (12 / 13).
Script for installation and configuration of AmneziaWG 2.0 on Ubuntu (24.04 / 25.10 / 26.04) and Debian (12 / 13).
Options:
-h, --help Show this help and exit
@ -264,7 +262,7 @@ Options:
--ssh-port=PORT SSH port for the UFW rule (auto-detected by default;
comma-separated list). Use if SSH runs on a non-standard
port and auto-detection is unavailable
--subnet=SUBNET Set tunnel subnet (x.x.x.x/yy) non-interactively
--subnet=SUBNET Tunnel subnet, /24 only (e.g. 10.9.9.1/24) non-interactively
--allow-ipv6 Keep IPv6 enabled non-interactively
--disallow-ipv6 Force-disable IPv6 non-interactively
--allow-ipv6-tunnel Enable dual-stack IPv6 inside the tunnel (ULA, opt-in)
@ -460,7 +458,12 @@ install_packages() {
fi
log "Installing: ${to_install[*]}..."
if [[ "${_APT_UPDATED:-0}" -eq 0 ]]; then
apt_update_tolerant || log_warn "Failed to update apt."
# C4: a hard apt_update_tolerant failure (GPG / binary-repo network / OOM)
# is NOT source noise but a real error; continuing on a stale cache is not
# safe (contract line ~138, same as callers 1975/2108). die aborts the
# install, so _APT_UPDATED=1 is set only on success - otherwise a later
# install_packages call in this session would silently skip the update.
apt_update_tolerant || die "apt update error."
_APT_UPDATED=1
fi
if ! DEBIAN_FRONTEND=noninteractive apt install -y "${to_install[@]}"; then
@ -515,10 +518,23 @@ configure_ipv6() {
}
# Detect whether the VPS has native IPv6.
# A global-scope IPv6 (not link-local fe80::) means egress to the IPv6 internet.
# Echo 1 if a global IPv6 is found, otherwise 0.
# Native IPv6 = a globally routable address (NOT ULA fc00::/7, NOT link-local
# fe80::) AND a default IPv6 route. Either condition alone is insufficient:
# - a global address without a default route -> no IPv6 internet egress (a client
# with ::/0 would black-hole);
# - a ULA (fddd::/...) has global scope to `ip` but is not internet-routable.
# Echo 1 only when both conditions hold, otherwise 0.
detect_native_ipv6() {
if ip -6 addr show scope global 2>/dev/null | grep -q "inet6"; then
local have_addr=0 have_route=0
if ip -6 addr show scope global 2>/dev/null \
| grep -oP 'inet6\s+\K[0-9a-fA-F:]+' \
| grep -qviE '^(fc|fd)'; then
have_addr=1
fi
if ip -6 route show default 2>/dev/null | grep -q .; then
have_route=1
fi
if [[ "$have_addr" -eq 1 && "$have_route" -eq 1 ]]; then
echo 1
else
echo 0
@ -532,12 +548,23 @@ configure_ipv6_tunnel() {
ALLOW_IPV6_TUNNEL=0
fi
: "${IPV6_SUBNET:=fddd:2c4:2c4:2c4::/64}"
# Detect native IPv6 on every run (cached in init for client render in Phase 4).
SERVER_HAS_NATIVE_IPV6=$(detect_native_ipv6)
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 && "$DISABLE_IPV6" -eq 1 ]]; then
log_warn "--allow-ipv6-tunnel requires host IPv6 forwarding; overriding --disallow-ipv6 (DISABLE_IPV6=0)"
DISABLE_IPV6=0
# The IPv6 tunnel requires host IPv6 enabled. Override --disallow-ipv6 AND
# actively re-enable IPv6 at runtime BEFORE detection/render: on an upgrade
# from a default past install (IPv6 was runtime-disabled), the kernel hides
# all IPv6 addresses, so detect_native_ipv6 would false-negative and a client
# would be rendered with an IPv6 Address while the kernel has IPv6 off
# (awg-quick restart can fail). weaq P1.
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 ]]; then
if [[ "$DISABLE_IPV6" -eq 1 ]]; then
log_warn "--allow-ipv6-tunnel requires host IPv6 forwarding; overriding --disallow-ipv6 (DISABLE_IPV6=0)"
DISABLE_IPV6=0
fi
sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.lo.disable_ipv6=0 >/dev/null 2>&1 || true
fi
# Detect native IPv6 AFTER the runtime re-enable (cached in init for client render in Phase 4).
SERVER_HAS_NATIVE_IPV6=$(detect_native_ipv6)
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 && "$SERVER_HAS_NATIVE_IPV6" -eq 0 ]]; then
log_warn "Native IPv6 not detected on VPS - the IPv6 tunnel will work peer-to-peer only, without IPv6 internet egress."
fi

View file

@ -8,14 +8,14 @@ fi
# ==============================================================================
# Скрипт для управления пользователями (пирами) AmneziaWG 2.0
# Автор: @bivlked
# Версия: 5.15.0
# Дата: 2026-06-01
# Версия: 5.15.1
# Дата: 2026-06-02
# Репозиторий: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Безопасный режим и Константы ---
# shellcheck disable=SC2034
SCRIPT_VERSION="5.15.0"
SCRIPT_VERSION="5.15.1"
set -o pipefail
AWG_DIR="/root/awg"
SERVER_CONF_FILE="/etc/amnezia/amneziawg/awg0.conf"
@ -55,10 +55,11 @@ trap _manage_cleanup EXIT INT TERM
# --- Обработка аргументов ---
COMMAND=""
HELP_EXIT_RC=0 # C1: 0 = явный help (exit 0); ставится в 1 для ошибок использования
ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) COMMAND="help"; break ;;
-h|--help) COMMAND="help"; HELP_EXIT_RC=0; break ;;
-v|--verbose) VERBOSE_LIST=1; shift ;;
--no-color) NO_COLOR=1; shift ;;
--json) JSON_OUTPUT=1; shift ;;
@ -69,7 +70,7 @@ while [[ $# -gt 0 ]]; do
--psk) CLI_ADD_PSK=1; shift ;;
--yes) CLI_YES=1; shift ;;
--carrier=*) CLI_CARRIER="${1#*=}"; shift ;;
--*) echo "Неизвестная опция: $1" >&2; COMMAND="help"; break ;;
--*) echo "Неизвестная опция: $1" >&2; COMMAND="help"; HELP_EXIT_RC=1; break ;;
*)
if [[ -z "$COMMAND" ]]; then
COMMAND=$1
@ -97,9 +98,7 @@ log_msg() {
local type="$1" msg="$2"
local ts
ts=$(date +'%F %T')
local safe_msg
safe_msg="${msg//%/%%}"
local entry="[$ts] $type: $safe_msg"
local entry="[$ts] $type: $msg"
local color_start="" color_end=""
if [[ "$NO_COLOR" -eq 0 ]]; then
@ -121,6 +120,11 @@ log_msg() {
# для CI/automation парсинга: stdout = «данные», stderr = «диагностика»).
if [[ "$type" == "ERROR" || "$type" == "WARN" ]]; then
printf "${color_start}%s${color_end}\n" "$entry" >&2
elif [[ "${JSON_OUTPUT:-0}" -eq 1 ]]; then
# weaq P2: в режиме --json stdout обязан содержать ТОЛЬКО JSON (jq/automation).
# INFO/DEBUG уводим в stderr, иначе list/show/stats --json печатают INFO-строки
# перед JSON и ломают парсинг (подтверждено на biHetzner).
printf "${color_start}%s${color_end}\n" "$entry" >&2
else
printf "${color_start}%s${color_end}\n" "$entry"
fi
@ -383,7 +387,7 @@ _restore_do_rollback() {
[[ -d "$_rtd/keys" ]] && cp -a "$_rtd/keys/"* "$KEYS_DIR/" 2>/dev/null
[[ -f "$_rtd/server_private.key" ]] && cp -a "$_rtd/server_private.key" "$AWG_DIR/" 2>/dev/null
[[ -f "$_rtd/server_public.key" ]] && cp -a "$_rtd/server_public.key" "$AWG_DIR/" 2>/dev/null
[[ -d "$_rtd/expiry" ]] && cp -a "$_rtd/expiry"/* "${EXPIRY_DIR:-$AWG_DIR/expiry}/" 2>/dev/null
[[ -d "$_rtd/expiry" ]] && { mkdir -p "${EXPIRY_DIR:-$AWG_DIR/expiry}"; cp -a "$_rtd/expiry"/* "${EXPIRY_DIR:-$AWG_DIR/expiry}/" 2>/dev/null; }
[[ -f "$_rtd/awg-expiry" ]] && cp -a "$_rtd/awg-expiry" /etc/cron.d/awg-expiry 2>/dev/null
rm -rf "$_rtd"
@ -582,6 +586,11 @@ restore_backup() {
if [[ -d "$td/clients" ]]; then
log "Восстановление файлов клиентов..."
# C11: чистая замена, не merge. Удаляю stale client-артефакты, которых
# нет в бэкапе (иначе клиент, удалённый после снятия бэкапа, остаётся
# 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
@ -596,6 +605,9 @@ restore_backup() {
if [[ -d "$td/keys" ]]; then
log "Восстановление ключей..."
mkdir -p "$KEYS_DIR"
# C11: удаляю stale client-ключи, которых нет в бэкапе (server-ключи
# лежат в AWG_DIR, не в KEYS_DIR, поэтому не затрагиваются).
rm -f "$KEYS_DIR"/* 2>/dev/null || true
if ! cp -a "$td/keys/"* "$KEYS_DIR/"; then
log_error "Ошибка копирования keys — восстановление прервано (запуск отката)."
return 1
@ -624,6 +636,10 @@ restore_backup() {
if [[ -d "$td/expiry" ]]; then
log "Восстановление данных expiry..."
mkdir -p "${EXPIRY_DIR:-$AWG_DIR/expiry}"
# C11: expiry НЕ пруним намеренно. Orphan-метки для несуществующих клиентов
# безвредны (cron-чистка их игнорирует), а prune здесь был бы небезопасен:
# и rm, и последующий cp - best-effort (|| true), так что сбой copy после
# prune молча оставил бы expiry пустым. Сами client-артефакты пруним выше.
cp -a "$td/expiry/"* "${EXPIRY_DIR:-$AWG_DIR/expiry}/" 2>/dev/null || true
chmod 600 "${EXPIRY_DIR:-$AWG_DIR/expiry}"/* 2>/dev/null
fi
@ -687,17 +703,43 @@ modify_client() {
return 1
fi ;;
Endpoint)
# C5: помимо отсечения опасных символов - позитивная проверка host:port.
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"")
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|*' '*|*$'\t'*|"")
log_error "Невалидный Endpoint: '$value'"
return 1 ;;
esac ;;
esac
local _eh _ept
if [[ "$value" == \[*\]:* ]]; then
_eh="${value%]:*}"; _eh="${_eh#\[}" # IPv6 без скобок
_ept="${value##*]:}"
[[ "$_eh" =~ ^[0-9A-Fa-f:]+$ ]] || { log_error "Невалидный Endpoint '$value': некорректный IPv6-хост"; return 1; }
else
_eh="${value%:*}"; _ept="${value##*:}"
[[ "$_eh" =~ ^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*|[0-9]{1,3}(\.[0-9]{1,3}){3})$ ]] || { log_error "Невалидный Endpoint '$value': ожидается host:port (FQDN / IPv4 / [IPv6])"; return 1; }
fi
{ [[ "$_ept" =~ ^[0-9]+$ ]] && [[ "$_ept" -ge 1 && "$_ept" -le 65535 ]]; } || { log_error "Невалидный Endpoint '$value': порт должен быть 1-65535"; return 1; }
;;
AllowedIPs)
# C5: помимо отсечения опасных символов - позитивная проверка CIDR-списка.
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"")
log_error "Невалидный AllowedIPs: '$value'"
return 1 ;;
esac ;;
esac
local _aip_tok _aip_ifs="$IFS"
IFS=','
for _aip_tok in $value; do
_aip_tok="${_aip_tok//[[:space:]]/}"
[[ -z "$_aip_tok" ]] && continue
if ! [[ "$_aip_tok" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$ || "$_aip_tok" =~ ^[0-9A-Fa-f:]+(/[0-9]{1,3})?$ ]]; then
IFS="$_aip_ifs"
log_error "Невалидный AllowedIPs '$value': '$_aip_tok' не похож на CIDR (a.b.c.d/n или IPv6/n)"
return 1
fi
done
IFS="$_aip_ifs"
;;
esac
# Блокировка перед state-проверками (защита от TOCTOU с concurrent remove)
@ -1358,7 +1400,11 @@ stats_clients() {
# ==============================================================================
usage() {
exec >&2
# C1: явный help (rc=0) -> stdout + exit 0; ошибка использования (rc!=0,
# дефолт) -> stderr + exit 1. Явные help-вызовы передают 0, error-вызовы
# опускают аргумент (получают 1).
local _rc="${1:-1}"
[[ "$_rc" -ne 0 ]] && exec >&2
echo ""
echo "Скрипт управления AmneziaWG 2.0 (v${SCRIPT_VERSION})"
echo "=============================================="
@ -1368,7 +1414,7 @@ usage() {
echo " -h, --help Показать эту справку"
echo " -v, --verbose Расширенный вывод (для команды list)"
echo " --no-color Отключить цветной вывод"
echo " --json JSON-вывод (для команды stats)"
echo " --json Машиночитаемый JSON-вывод (для команд list / show / stats)"
echo " --expires=ВРЕМЯ Срок действия при add (1h, 12h, 1d, 7d, 30d, 4w)"
echo " --conf-dir=ПУТЬ Указать директорию AWG (умолч: $AWG_DIR)"
echo " --server-conf=ПУТЬ Указать файл конфига сервера"
@ -1383,7 +1429,7 @@ usage() {
echo "Команды:"
echo " add <имя> [имя2 ...] Добавить клиента(ов). --expires применяется ко всем"
echo " remove <имя> [имя2 ...] Удалить клиента(ов)"
echo " list [-v] Показать список клиентов"
echo " list [-v] [--json] Показать список клиентов (--json: машиночитаемый, с client_ipv6)"
echo " stats [--json] Статистика трафика по клиентам"
echo " regen [имя] Перегенерировать файлы клиента(ов)"
echo " modify <имя> <пар> <зн> Изменить параметр клиента"
@ -1397,15 +1443,18 @@ usage() {
echo " (dkms autoinstall + modprobe + запуск awg-quick)"
echo " help Показать эту справку"
echo ""
exit 1
exit "$_rc"
}
# ==============================================================================
# Основная логика
# ==============================================================================
if [[ "$COMMAND" == "help" || -z "$COMMAND" ]]; then
usage
if [[ -z "$COMMAND" ]]; then
usage 1
fi
if [[ "$COMMAND" == "help" ]]; then
usage "$HELP_EXIT_RC"
fi
check_dependencies || exit 1

View file

@ -8,14 +8,14 @@ fi
# ==============================================================================
# AmneziaWG 2.0 peer management script
# Author: @bivlked
# Version: 5.15.0
# Date: 2026-06-01
# Version: 5.15.1
# Date: 2026-06-02
# Repository: https://github.com/bivlked/amneziawg-installer
# ==============================================================================
# --- Safe mode and Constants ---
# shellcheck disable=SC2034
SCRIPT_VERSION="5.15.0"
SCRIPT_VERSION="5.15.1"
set -o pipefail
AWG_DIR="/root/awg"
SERVER_CONF_FILE="/etc/amnezia/amneziawg/awg0.conf"
@ -55,10 +55,11 @@ trap _manage_cleanup EXIT INT TERM
# --- Argument handling ---
COMMAND=""
HELP_EXIT_RC=0 # C1: 0 = explicit help (exit 0); set to 1 for usage errors
ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) COMMAND="help"; break ;;
-h|--help) COMMAND="help"; HELP_EXIT_RC=0; break ;;
-v|--verbose) VERBOSE_LIST=1; shift ;;
--no-color) NO_COLOR=1; shift ;;
--json) JSON_OUTPUT=1; shift ;;
@ -69,7 +70,7 @@ while [[ $# -gt 0 ]]; do
--psk) CLI_ADD_PSK=1; shift ;;
--yes) CLI_YES=1; shift ;;
--carrier=*) CLI_CARRIER="${1#*=}"; shift ;;
--*) echo "Unknown option: $1" >&2; COMMAND="help"; break ;;
--*) echo "Unknown option: $1" >&2; COMMAND="help"; HELP_EXIT_RC=1; break ;;
*)
if [[ -z "$COMMAND" ]]; then
COMMAND=$1
@ -97,9 +98,7 @@ log_msg() {
local type="$1" msg="$2"
local ts
ts=$(date +'%F %T')
local safe_msg
safe_msg="${msg//%/%%}"
local entry="[$ts] $type: $safe_msg"
local entry="[$ts] $type: $msg"
local color_start="" color_end=""
if [[ "$NO_COLOR" -eq 0 ]]; then
@ -121,6 +120,11 @@ log_msg() {
# important for CI/automation parsing: stdout = "data", stderr = "diagnostics").
if [[ "$type" == "ERROR" || "$type" == "WARN" ]]; then
printf "${color_start}%s${color_end}\n" "$entry" >&2
elif [[ "${JSON_OUTPUT:-0}" -eq 1 ]]; then
# weaq P2: in --json mode stdout must contain ONLY JSON (jq/automation).
# Route INFO/DEBUG to stderr, otherwise list/show/stats --json print INFO
# lines before the JSON and break parsing (confirmed on biHetzner).
printf "${color_start}%s${color_end}\n" "$entry" >&2
else
printf "${color_start}%s${color_end}\n" "$entry"
fi
@ -384,7 +388,7 @@ _restore_do_rollback() {
[[ -d "$_rtd/keys" ]] && cp -a "$_rtd/keys/"* "$KEYS_DIR/" 2>/dev/null
[[ -f "$_rtd/server_private.key" ]] && cp -a "$_rtd/server_private.key" "$AWG_DIR/" 2>/dev/null
[[ -f "$_rtd/server_public.key" ]] && cp -a "$_rtd/server_public.key" "$AWG_DIR/" 2>/dev/null
[[ -d "$_rtd/expiry" ]] && cp -a "$_rtd/expiry"/* "${EXPIRY_DIR:-$AWG_DIR/expiry}/" 2>/dev/null
[[ -d "$_rtd/expiry" ]] && { mkdir -p "${EXPIRY_DIR:-$AWG_DIR/expiry}"; cp -a "$_rtd/expiry"/* "${EXPIRY_DIR:-$AWG_DIR/expiry}/" 2>/dev/null; }
[[ -f "$_rtd/awg-expiry" ]] && cp -a "$_rtd/awg-expiry" /etc/cron.d/awg-expiry 2>/dev/null
rm -rf "$_rtd"
@ -585,6 +589,12 @@ restore_backup() {
if [[ -d "$td/clients" ]]; then
log "Restoring client files..."
# C11: clean replacement, not a merge. Remove stale client artifacts that
# are absent from the backup (otherwise a client deleted since the backup
# lingers as orphan .conf/.png/.vpnuri). Scope strictly to managed client
# 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
@ -599,6 +609,9 @@ restore_backup() {
if [[ -d "$td/keys" ]]; then
log "Restoring keys..."
mkdir -p "$KEYS_DIR"
# C11: remove stale client keys absent from the backup (server keys live
# in AWG_DIR, not KEYS_DIR, so they are not affected).
rm -f "$KEYS_DIR"/* 2>/dev/null || true
if ! cp -a "$td/keys/"* "$KEYS_DIR/"; then
log_error "Error copying keys — restore aborted (triggering rollback)."
return 1
@ -627,6 +640,11 @@ restore_backup() {
if [[ -d "$td/expiry" ]]; then
log "Restoring expiry data..."
mkdir -p "${EXPIRY_DIR:-$AWG_DIR/expiry}"
# C11: expiry is intentionally NOT pruned. Orphan stamps for nonexistent
# clients are harmless (cron cleanup ignores them), and a prune here would
# be unsafe: both the rm and the following cp are best-effort (|| true), so
# a copy failure after the prune would silently leave expiry empty. The
# client artifacts themselves are pruned above.
cp -a "$td/expiry/"* "${EXPIRY_DIR:-$AWG_DIR/expiry}/" 2>/dev/null || true
chmod 600 "${EXPIRY_DIR:-$AWG_DIR/expiry}"/* 2>/dev/null
fi
@ -690,17 +708,43 @@ modify_client() {
return 1
fi ;;
Endpoint)
# C5: beyond rejecting dangerous chars - positive host:port check.
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"")
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|*' '*|*$'\t'*|"")
log_error "Invalid Endpoint: '$value'"
return 1 ;;
esac ;;
esac
local _eh _ept
if [[ "$value" == \[*\]:* ]]; then
_eh="${value%]:*}"; _eh="${_eh#\[}" # IPv6 without brackets
_ept="${value##*]:}"
[[ "$_eh" =~ ^[0-9A-Fa-f:]+$ ]] || { log_error "Invalid Endpoint '$value': malformed IPv6 host"; return 1; }
else
_eh="${value%:*}"; _ept="${value##*:}"
[[ "$_eh" =~ ^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*|[0-9]{1,3}(\.[0-9]{1,3}){3})$ ]] || { log_error "Invalid Endpoint '$value': expected host:port (FQDN / IPv4 / [IPv6])"; return 1; }
fi
{ [[ "$_ept" =~ ^[0-9]+$ ]] && [[ "$_ept" -ge 1 && "$_ept" -le 65535 ]]; } || { log_error "Invalid Endpoint '$value': port must be 1-65535"; return 1; }
;;
AllowedIPs)
# C5: beyond rejecting dangerous chars - positive CIDR-list check.
case "$value" in
*$'\n'*|*$'\r'*|*\\*|*\"*|*\'*|"")
log_error "Invalid AllowedIPs: '$value'"
return 1 ;;
esac ;;
esac
local _aip_tok _aip_ifs="$IFS"
IFS=','
for _aip_tok in $value; do
_aip_tok="${_aip_tok//[[:space:]]/}"
[[ -z "$_aip_tok" ]] && continue
if ! [[ "$_aip_tok" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$ || "$_aip_tok" =~ ^[0-9A-Fa-f:]+(/[0-9]{1,3})?$ ]]; then
IFS="$_aip_ifs"
log_error "Invalid AllowedIPs '$value': '$_aip_tok' is not a CIDR (a.b.c.d/n or IPv6/n)"
return 1
fi
done
IFS="$_aip_ifs"
;;
esac
# Lock before state checks (TOCTOU protection against concurrent remove)
@ -1360,7 +1404,11 @@ stats_clients() {
# ==============================================================================
usage() {
exec >&2
# C1: explicit help (rc=0) -> stdout + exit 0; usage error (rc!=0, default)
# -> stderr + exit 1. Explicit-help callers pass 0, error callers omit the
# argument (get 1).
local _rc="${1:-1}"
[[ "$_rc" -ne 0 ]] && exec >&2
echo ""
echo "AmneziaWG 2.0 management script (v${SCRIPT_VERSION})"
echo "=============================================="
@ -1370,7 +1418,7 @@ usage() {
echo " -h, --help Show this help"
echo " -v, --verbose Verbose output (for list command)"
echo " --no-color Disable colored output"
echo " --json JSON output (for stats command)"
echo " --json Machine-readable JSON output (for list / show / stats)"
echo " --expires=DURATION Expiry time for add (1h, 12h, 1d, 7d, 30d, 4w)"
echo " --conf-dir=PATH Specify AWG directory (default: $AWG_DIR)"
echo " --server-conf=PATH Specify server config file"
@ -1385,7 +1433,7 @@ usage() {
echo "Commands:"
echo " add <name> [name2 ...] Add client(s). --expires applies to all"
echo " remove <name> [name2 ...] Remove client(s)"
echo " list [-v] List clients"
echo " list [-v] [--json] List clients (--json: machine-readable, includes client_ipv6)"
echo " stats [--json] Client traffic statistics"
echo " regen [name] Regenerate client file(s)"
echo " modify <name> <p> <v> Modify a client parameter"
@ -1399,15 +1447,18 @@ usage() {
echo " (dkms autoinstall + modprobe + start awg-quick)"
echo " help Show this help"
echo ""
exit 1
exit "$_rc"
}
# ==============================================================================
# Main logic
# ==============================================================================
if [[ "$COMMAND" == "help" || -z "$COMMAND" ]]; then
usage
if [[ -z "$COMMAND" ]]; then
usage 1
fi
if [[ "$COMMAND" == "help" ]]; then
usage "$HELP_EXIT_RC"
fi
check_dependencies || exit 1

View file

@ -0,0 +1,195 @@
#!/bin/bash
# check-docs-consistency.sh - быстрые проверки согласованности документации
#
# Лёгкий аналог preflight для документации и метаданных: ничего не собирает,
# не качает по сети, не гоняет bats. Только дешёвые детерминированные проверки,
# которые ловят классы рассинхрона, проскакивающие мимо shellcheck/test
# (битые внутренние ссылки, рассогласование версий, протухшая матрица ОС).
#
# Запускается локально и в CI (docs-check workflow), а также включён в
# preflight-check.sh как один из шагов.
#
# Использование:
# bash scripts/check-docs-consistency.sh
#
# Проверки:
# 1. Внутренние markdown-ссылки (#anchor) резолвятся в этом же файле.
# 2. CHANGELOG: у каждого version-heading есть reference-link; набор версий
# в RU == EN; [Unreleased] присутствует в обоих.
# 3. Version triple: README badge == SCRIPT_VERSION == верхний changelog
# heading (RU и EN).
# 4. Матрица ОС: Ubuntu 26.04 присутствует во всех заявленных местах.
# 5. SECURITY/CONTRIBUTING не протухли (текущий minor в supported-таблице;
# нет захардкоженного test-count baseline).
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT" || { echo "ОШИБКА: не удалось перейти в $REPO_ROOT" >&2; exit 2; }
PASS=0
FAIL=0
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
)
echo "=== check-docs-consistency ==="
# GitHub-совместимая slug-генерация из текста заголовка:
# lowercase, убрать всё кроме букв/цифр/пробела/дефиса, пробелы -> дефисы,
# срезать ведущие/хвостовые дефисы (их даёт, например, emoji в начале heading -
# GitHub их тоже не оставляет).
_slug() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}" # ltrim
s="${s%"${s##*[![:space:]]}"}" # rtrim
printf '%s' "$s" \
| tr '[:upper:]' '[:lower:]' \
| LC_ALL=C sed -E 's/[^a-z0-9 _-]//g' \
| tr ' ' '-' \
| sed -E 's/^-+//; s/-+$//'
}
# Удалить из markdown код, чтобы примеры разметки внутри него не парсились как
# реальные ссылки/якоря: fenced-блоки (``` ... ```) целиком + inline-спаны
# (`...`). Иначе документированный в backticks пример вида `](#anchor)` дал бы
# ложный FAIL.
_strip_code() {
awk '/^```/ { infence = !infence; next } !infence { print }' "$1" \
| sed 's/`[^`]*`//g'
}
# --- 1. Внутренние anchor-ссылки резолвятся ---
anchor_fail=0
for f in "${DOC_FILES[@]}"; do
[[ -f "$f" ]] || continue
# Анализируем файл с вырезанным кодом (примеры в backticks - не разметка).
stripped="$(_strip_code "$f")"
# Целевые якоря в файле: явные <a id=..> / <a name=..> + slug'и заголовков.
declare -A anchors=()
while IFS= read -r a; do
[[ -n "$a" ]] && anchors["$a"]=1
done < <(printf '%s\n' "$stripped" | grep -oiP '<a\s+(id|name)="\K[^"]+')
while IFS= read -r h; do
# Заголовки ATX: "# ...". Текст уже без ведущих #, slug'им.
sl="$(_slug "$h")"
[[ -n "$sl" ]] && anchors["$sl"]=1
done < <(printf '%s\n' "$stripped" | grep -E '^#{1,6}[[:space:]]' | sed -E 's/^#{1,6}[[:space:]]+//')
# Внутренние ссылки вида ](#anchor) в этом файле.
while IFS= read -r ref; do
[[ -z "$ref" ]] && continue
if [[ -z "${anchors[$ref]:-}" ]]; then
echo " $f: битая внутренняя ссылка #$ref" >&2
anchor_fail=1
fi
done < <(printf '%s\n' "$stripped" | grep -oP '\]\(#\K[^)]+')
unset anchors
done
if [[ "$anchor_fail" -eq 0 ]]; then _ok "внутренние anchor-ссылки резолвятся"; else _bad "битые внутренние anchor-ссылки"; fi
# --- 2. CHANGELOG: heading <-> reference-link, RU == EN ---
changelog_fail=0
_changelog_versions() { # печатает версии из "## [X]" headings
grep -oP '^##\s+\[\K[^]]+' "$1"
}
_changelog_refs() { # печатает версии из "[X]:" reference-links
grep -oP '^\[\K[^]]+(?=\]:)' "$1"
}
for f in CHANGELOG.md CHANGELOG.en.md; do
[[ -f "$f" ]] || { _bad "нет $f"; changelog_fail=1; continue; }
while IFS= read -r v; do
[[ -z "$v" ]] && continue
if ! _changelog_refs "$f" | grep -qxF "$v"; then
echo " $f: heading [$v] без reference-link [$v]:" >&2
changelog_fail=1
fi
done < <(_changelog_versions "$f")
if ! _changelog_versions "$f" | grep -qxF "Unreleased"; then
echo " $f: нет heading [Unreleased]" >&2
changelog_fail=1
fi
done
# RU == EN набор версий (отсортированные уникальные heading-списки).
if [[ -f CHANGELOG.md && -f CHANGELOG.en.md ]]; then
ru_set="$(_changelog_versions CHANGELOG.md | sort -u)"
en_set="$(_changelog_versions CHANGELOG.en.md | sort -u)"
if [[ "$ru_set" != "$en_set" ]]; then
echo " набор версий CHANGELOG.md != CHANGELOG.en.md:" >&2
diff <(printf '%s\n' "$ru_set") <(printf '%s\n' "$en_set") >&2 || true
changelog_fail=1
fi
fi
if [[ "$changelog_fail" -eq 0 ]]; then _ok "CHANGELOG headings/refs согласованы, RU == EN"; else _bad "CHANGELOG рассинхрон"; fi
# --- 3. Version triple: badge == SCRIPT_VERSION == верхний changelog heading ---
ver_fail=0
script_ver="$(awk -F'"' '/^SCRIPT_VERSION=/{print $2; exit}' install_amneziawg.sh)"
# Верхний non-Unreleased heading в каждом changelog.
top_ru="$(grep -oP '^##\s+\[\K[0-9]+\.[0-9]+\.[0-9]+' CHANGELOG.md | head -n1)"
top_en="$(grep -oP '^##\s+\[\K[0-9]+\.[0-9]+\.[0-9]+' CHANGELOG.en.md | head -n1)"
for pair in "README.md:$script_ver" "README.en.md:$script_ver"; do
rf="${pair%%:*}"; expect="${pair##*:}"
badge="$(grep -oP 'Installer_Version-\K[0-9]+\.[0-9]+\.[0-9]+' "$rf" | head -n1)"
if [[ "$badge" != "$expect" ]]; then
echo " $rf badge='$badge' != SCRIPT_VERSION='$expect'" >&2
ver_fail=1
fi
done
if [[ "$top_ru" != "$script_ver" ]]; then echo " CHANGELOG.md top heading '$top_ru' != SCRIPT_VERSION '$script_ver'" >&2; ver_fail=1; fi
if [[ "$top_en" != "$script_ver" ]]; then echo " CHANGELOG.en.md top heading '$top_en' != SCRIPT_VERSION '$script_ver'" >&2; ver_fail=1; fi
if [[ "$ver_fail" -eq 0 ]]; then _ok "version triple согласован ($script_ver)"; else _bad "version triple рассинхрон"; fi
# --- 4. Матрица ОС: Ubuntu 26.04 во всех заявленных местах ---
os_fail=0
for f in README.md README.en.md install_amneziawg.sh install_amneziawg_en.sh .github/ISSUE_TEMPLATE/bug_report.yml; do
[[ -f "$f" ]] || { echo " нет $f (проверка 26.04)" >&2; os_fail=1; continue; }
if ! grep -q '26\.04' "$f"; then
echo " $f: нет упоминания Ubuntu 26.04 в матрице ОС" >&2
os_fail=1
fi
done
if [[ "$os_fail" -eq 0 ]]; then _ok "Ubuntu 26.04 присутствует в матрице ОС"; else _bad "Ubuntu 26.04 отсутствует где-то в матрице ОС"; fi
# --- 5. SECURITY/CONTRIBUTING не протухли ---
stale_fail=0
# Текущий minor (X.Y) должен фигурировать в SECURITY supported-таблице.
minor="$(printf '%s' "$script_ver" | grep -oP '^[0-9]+\.[0-9]+')"
if [[ -f SECURITY.md ]]; then
if ! grep -qE "${minor//./\\.}\.[x0-9]" SECURITY.md; then
echo " SECURITY.md: текущий minor $minor.x не найден в supported-таблице" >&2
stale_fail=1
fi
else
echo " нет SECURITY.md" >&2; stale_fail=1
fi
# CONTRIBUTING не должен хардкодить число тестов (хрупкий baseline).
if [[ -f CONTRIBUTING.md ]]; then
if grep -qiP '\b[0-9]{3,}\s+tests?\b' CONTRIBUTING.md; then
echo " CONTRIBUTING.md: захардкоженный счётчик тестов (хрупкий baseline)" >&2
stale_fail=1
fi
fi
if [[ "$stale_fail" -eq 0 ]]; then _ok "SECURITY/CONTRIBUTING не протухли"; else _bad "SECURITY/CONTRIBUTING протухли"; fi
# --- Summary ---
echo ""
echo "=== docs-consistency summary: $PASS passed, $FAIL failed ==="
for r in "${RESULTS[@]}"; do echo " $r"; done
[[ "$FAIL" -gt 0 ]] && exit 1
exit 0

View file

@ -1,16 +1,19 @@
#!/bin/bash
# preflight-check.sh - единый прогон всех pre-tag проверок перед релизом
#
# Запускает в одной команде полный pre-tag чеклист (см. CLAUDE.md "Release
# Process" и memory feedback_awg20_release_checklist). Каждая проверка печатает
# PASS/FAIL, в конце - сводка. Exit!=0 если хоть одна провалилась.
# Запускает в одной команде полный pre-tag чеклист (см. docs/RELEASE_PROCESS.md).
# Каждая проверка печатает PASS/FAIL, в конце - сводка. Exit!=0 если хоть одна
# провалилась.
#
# Использование:
# bash scripts/preflight-check.sh
# BASE_REF=origin/main bash scripts/preflight-check.sh # detached checkout
#
# Переменные окружения:
# BASE_REF git-ref для diff-проверок пунктуации/маркеров. Default: main.
# LOG_RANGE git-диапазон для проверки commit-сообщений. Default: main..HEAD
# BASE_REF git-ref для diff-проверок пунктуации/маркеров. Если не задан,
# берётся первый существующий из: main, origin/main. На detached
# checkout без локального main задавайте явно BASE_REF=origin/main.
# LOG_RANGE git-диапазон для проверки commit-сообщений. Default: <base>..HEAD
# (только коммиты ветки релиза; merged-from-main коммиты с legacy
# human/bot Co-authored-by трейлерами в проверку НЕ попадают).
#
@ -18,11 +21,12 @@
# 1. bash -n на 6 скриптах
# 2. shellcheck -s bash -S warning на 6 скриптах
# 3. bats tests/
# 4. em/en-dash (U+2013/U+2014) в diff BASE_REF...HEAD = 0
# 4. реально добавленных em/en-dash (U+2013/U+2014) в diff BASE_REF...HEAD = 0
# 5. AI/tool-mention в diff + commit-логе = 0
# 6. Co-authored-by в commit-логе = 0
# 7. SCRIPT_VERSION консистентен в 4 версионированных скриптах
# 8. SHA-пины синхронны (update-sha-pins.sh --verify)
# 9. Согласованность документации (check-docs-consistency.sh)
set -o pipefail
@ -30,8 +34,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT" || { echo "ОШИБКА: не удалось перейти в $REPO_ROOT" >&2; exit 2; }
BASE_REF="${BASE_REF:-main}"
LOG_RANGE="${LOG_RANGE:-main..HEAD}"
# BASE_REF fallback: уважаем явно заданный, иначе первый резолвящийся из
# main / origin/main. Это переживает detached checkout (CI на теге), где
# локального main нет, но origin/main подтянут (fetch-depth:0 + fetch main).
if [[ -z "${BASE_REF:-}" ]]; then
for _cand in main origin/main; do
if git rev-parse --verify "$_cand" >/dev/null 2>&1; then
BASE_REF="$_cand"
break
fi
done
fi
if [[ -z "${BASE_REF:-}" ]]; then
echo "ОШИБКА: не найден base-ref для diff-проверок (пробовал main, origin/main)." >&2
echo " Задайте явно, например: BASE_REF=origin/main bash scripts/preflight-check.sh" >&2
exit 2
fi
LOG_RANGE="${LOG_RANGE:-${BASE_REF}..HEAD}"
SCRIPTS=(
install_amneziawg.sh
@ -97,20 +116,23 @@ else
_bad "bats not found in PATH"
fi
# --- 4. em/en-dash in diff ---
if git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then
# Байтовые паттерны UTF-8: en-dash U+2013 = E2 80 93, em-dash U+2014 = E2 80 94.
# LC_ALL=C + \xHH работает на GNU grep 3.0 (Git Bash), в отличие от \x{2013}
# (codepoint-форма падает "character value in \x{} too large" и глоталась || true).
dash_hits=$(git diff "${BASE_REF}...HEAD" | LC_ALL=C grep -nP '^\+.*(\xe2\x80\x93|\xe2\x80\x94)' || true)
if [[ -z "$dash_hits" ]]; then
_ok "no em/en-dash in diff ${BASE_REF}...HEAD"
else
echo "$dash_hits" >&2
_bad "em/en-dash found in diff ${BASE_REF}...HEAD"
fi
# --- 4. newly added em/en-dash in diff ---
# Политика проекта (CLAUDE.md): новый текст - только hyphen-minus; legacy
# em/en-dash в существующих строках сохраняется без mass-purge, а point-edit
# легаси-строки НЕ обязывает переписывать тире на этой строке. Поэтому ловим
# ТОЛЬКО реально добавленные тире. word-diff=porcelain помечает префиксом '+'
# лишь изменённые слова, так что тире в неизменённой части правленой строки в
# '+' не попадает (legacy не даёт ложный FAIL); под флаг идёт только заново
# внесённый символ тире (новая строка/слово). Байты UTF-8: en-dash U+2013 =
# E2 80 93, em-dash U+2014 = E2 80 94 (LC_ALL=C + \xHH, codepoint-форма \x{}
# падает на GNU grep Git Bash).
dash_added=$(git diff --word-diff=porcelain "${BASE_REF}...HEAD" \
| LC_ALL=C grep -nP '^\+.*(\xe2\x80\x93|\xe2\x80\x94)' || true)
if [[ -z "$dash_added" ]]; then
_ok "no newly added em/en-dash in diff ${BASE_REF}...HEAD"
else
_bad "BASE_REF '$BASE_REF' not found (set BASE_REF=<ref>)"
echo "$dash_added" >&2
_bad "newly added em/en-dash in diff ${BASE_REF}...HEAD"
fi
# --- 5. AI/tool-mention in diff + commit log ---
@ -172,6 +194,16 @@ else
_bad "SHA pins out of sync (run: bash scripts/update-sha-pins.sh)"
fi
# --- 9. Docs consistency ---
# Тот же скрипт гоняет docs-check workflow. Здесь - часть единого pre-tag gate.
if bash "$SCRIPT_DIR/check-docs-consistency.sh" >/tmp/preflight-docs.$$ 2>&1; then
_ok "docs consistency (check-docs-consistency.sh)"
else
cat /tmp/preflight-docs.$$ >&2
_bad "docs consistency (run: bash scripts/check-docs-consistency.sh)"
fi
rm -f /tmp/preflight-docs.$$
# --- Summary ---
echo ""
echo "=== preflight summary: $PASS passed, $FAIL failed ==="

View file

@ -0,0 +1,115 @@
#!/usr/bin/env bats
# v5.15.1 audit fixes - regression locks for:
# C8 / weaq P1: detect_native_ipv6 must require a globally-routable (non-ULA)
# address AND a default IPv6 route (no false-positive -> no ::/0
# black-hole).
# E / C13: install --help OS line lists 26.04; --subnet help states /24.
# C12: log_msg must not double percent signs.
# weaq P2: log_msg must route INFO/DEBUG to stderr when JSON_OUTPUT=1.
#
# shellcheck disable=SC2034 # mock env vars (NO_COLOR/VERBOSE_LIST/LOG_FILE/JSON_OUTPUT) are consumed by the sourced functions
# shellcheck disable=SC2154 # $stderr is set by bats `run --separate-stderr`
bats_require_minimum_version 1.5.0
# ---------------------------------------------------------------------------
# detect_native_ipv6 (extracted from install_amneziawg.sh, `ip` mocked)
# ---------------------------------------------------------------------------
setup_detect() {
local installer="${BATS_TEST_DIRNAME}/../install_amneziawg.sh"
# shellcheck source=/dev/null
source <(sed -n '/^detect_native_ipv6() {$/,/^}$/p' "$installer")
ip() {
case "$*" in
*"addr show scope global"*) printf '%s\n' "${MOCK_IP6_ADDR:-}" ;;
*"route show default"*) printf '%s\n' "${MOCK_IP6_ROUTE:-}" ;;
*) : ;;
esac
}
export -f ip
}
@test "v5.15.1 C8: native = global non-ULA address + default route -> 1" {
setup_detect
MOCK_IP6_ADDR=" inet6 2a01:4f8:c17:abcd::1/64 scope global"
MOCK_IP6_ROUTE="default via fe80::1 dev eth0 proto ra"
run detect_native_ipv6
[ "$output" = "1" ]
}
@test "v5.15.1 C8: ULA-only global address (fd..) + default route -> 0" {
setup_detect
MOCK_IP6_ADDR=" inet6 fddd:2c4:2c4:2c4::1/64 scope global"
MOCK_IP6_ROUTE="default via fe80::1 dev eth0"
run detect_native_ipv6
[ "$output" = "0" ]
}
@test "v5.15.1 C8: global address but NO default route -> 0 (avoids black-hole)" {
setup_detect
MOCK_IP6_ADDR=" inet6 2a01:4f8:c17:abcd::1/64 scope global"
MOCK_IP6_ROUTE=""
run detect_native_ipv6
[ "$output" = "0" ]
}
@test "v5.15.1 C8: no IPv6 at all -> 0" {
setup_detect
MOCK_IP6_ADDR=""
MOCK_IP6_ROUTE=""
run detect_native_ipv6
[ "$output" = "0" ]
}
# ---------------------------------------------------------------------------
# install --help text (E: 26.04, C13: /24) - source-level assertions
# ---------------------------------------------------------------------------
@test "v5.15.1 E: RU installer help OS line includes 26.04" {
grep -q "Ubuntu (24.04 / 25.10 / 26.04)" "${BATS_TEST_DIRNAME}/../install_amneziawg.sh"
}
@test "v5.15.1 E: EN installer help OS line includes 26.04" {
grep -q "Ubuntu (24.04 / 25.10 / 26.04)" "${BATS_TEST_DIRNAME}/../install_amneziawg_en.sh"
}
@test "v5.15.1 C13: RU installer --subnet help states /24 only" {
grep -qP 'subnet.*/24' "${BATS_TEST_DIRNAME}/../install_amneziawg.sh"
}
# ---------------------------------------------------------------------------
# log_msg (extracted from manage_amneziawg.sh) - C12 + weaq P2
# ---------------------------------------------------------------------------
setup_logmsg() {
NO_COLOR=1
VERBOSE_LIST=0
LOG_FILE="${BATS_TEST_TMPDIR:-/tmp}/awg_test_$$.log"
JSON_OUTPUT=0
local mgr="${BATS_TEST_DIRNAME}/../manage_amneziawg.sh"
# shellcheck source=/dev/null
source <(sed -n '/^log_msg() {$/,/^}$/p' "$mgr")
}
@test "v5.15.1 C12: log_msg does not double percent signs" {
setup_logmsg
run log_msg INFO "disk is 95% full"
[[ "$output" == *"95% full"* ]]
[[ "$output" != *"95%%"* ]]
}
@test "v5.15.1 weaq P2: log_msg INFO goes to stderr (empty stdout) when JSON_OUTPUT=1" {
setup_logmsg
JSON_OUTPUT=1
run --separate-stderr log_msg INFO "informational line"
[ -z "$output" ]
[[ "$stderr" == *"informational line"* ]]
}
@test "v5.15.1 weaq P2: log_msg INFO goes to stdout when JSON_OUTPUT=0" {
setup_logmsg
JSON_OUTPUT=0
run log_msg INFO "informational line"
[[ "$output" == *"informational line"* ]]
}

View file

@ -0,0 +1,63 @@
#!/usr/bin/env bats
# v5.15.1 (audit C7) - intent-mirroring of the IPv4 routing mode into IPv6.
#
# When the client uses a SPLIT tunnel (custom ALLOWED_IPS, not 0.0.0.0/0),
# render_client_config must keep the IPv4 split list AS-IS and append ONLY the
# tunnel ULA - never ::/0. There is no IPv6 split-list, so routing all IPv6 into
# the tunnel would silently break the user's split-tunnel intent.
#
# create_init_config seeds a split ALLOWED_IPS ('0.0.0.0/5, 8.0.0.0/7'), so these
# tests exercise the split path directly without overriding it.
load test_helper
setup_split_native() {
create_server_config
create_init_config # ALLOWED_IPS='0.0.0.0/5, 8.0.0.0/7' (split)
cat >> "$CONFIG_FILE" << 'CONF'
export ALLOW_IPV6_TUNNEL=1
export IPV6_SUBNET='fddd:2c4:2c4:2c4::/64'
export SERVER_HAS_NATIVE_IPV6=1
CONF
safe_load_config "$CONFIG_FILE"
}
setup_split_no_native() {
create_server_config
create_init_config
cat >> "$CONFIG_FILE" << 'CONF'
export ALLOW_IPV6_TUNNEL=1
export IPV6_SUBNET='fddd:2c4:2c4:2c4::/64'
export SERVER_HAS_NATIVE_IPV6=0
CONF
safe_load_config "$CONFIG_FILE"
}
@test "v5.15.1 C7: split tunnel + native IPv6 keeps IPv4 split and appends only the ULA" {
setup_split_native
render_client_config "splitnat" "10.9.9.20" "FAKEPRIVKEY" "FAKEPUBKEY" "1.2.3.4" "39743" "fddd:2c4:2c4:2c4::20"
local conf="$AWG_DIR/splitnat.conf"
grep -q "AllowedIPs = 0.0.0.0/5, 8.0.0.0/7, fddd:2c4:2c4:2c4::/64" "$conf"
}
@test "v5.15.1 C7: split tunnel + native IPv6 does NOT add ::/0" {
setup_split_native
render_client_config "splitnat2" "10.9.9.21" "FAKEPRIVKEY" "FAKEPUBKEY" "1.2.3.4" "39743" "fddd:2c4:2c4:2c4::21"
local conf="$AWG_DIR/splitnat2.conf"
! grep -q "::/0" "$conf"
}
@test "v5.15.1 C7: split tunnel + no native IPv6 keeps IPv4 split and appends only the ULA" {
setup_split_no_native
render_client_config "splitno" "10.9.9.22" "FAKEPRIVKEY" "FAKEPUBKEY" "1.2.3.4" "39743" "fddd:2c4:2c4:2c4::22"
local conf="$AWG_DIR/splitno.conf"
grep -q "AllowedIPs = 0.0.0.0/5, 8.0.0.0/7, fddd:2c4:2c4:2c4::/64" "$conf"
}
@test "v5.15.1 C7: split tunnel preserves the IPv4 split (not collapsed to 0.0.0.0/0)" {
setup_split_native
render_client_config "splitnat3" "10.9.9.23" "FAKEPRIVKEY" "FAKEPUBKEY" "1.2.3.4" "39743" "fddd:2c4:2c4:2c4::23"
local conf="$AWG_DIR/splitnat3.conf"
grep -qP '^AllowedIPs = 0\.0\.0\.0/5, 8\.0\.0\.0/7,' "$conf"
! grep -qP '^AllowedIPs = 0\.0\.0\.0/0' "$conf"
}

View file

@ -8,7 +8,11 @@ load test_helper
setup_no_native() {
create_server_config
create_init_config
# "route all" (full tunnel) tests: force ALLOWED_IPS=0.0.0.0/0 (overrides the
# split default from create_init_config; safe_load_config is last-wins). With
# no native IPv6 a full tunnel maps to the tunnel ULA, never ::/0.
cat >> "$CONFIG_FILE" << 'CONF'
export ALLOWED_IPS='0.0.0.0/0'
export ALLOW_IPV6_TUNNEL=1
export IPV6_SUBNET='fddd:2c4:2c4:2c4::/64'
export SERVER_HAS_NATIVE_IPV6=0

View file

@ -8,8 +8,12 @@ load test_helper
setup_common() {
create_server_config
create_init_config
# Append IPv6 keys to the init config
# Append IPv6 keys to the init config. These are the "route all" (full
# tunnel) tests, so force ALLOWED_IPS=0.0.0.0/0 (overrides the split default
# from create_init_config; safe_load_config is last-wins). Under the
# intent-mirroring rule a full IPv4 tunnel maps to ::/0 on native IPv6.
cat >> "$CONFIG_FILE" << 'CONF'
export ALLOWED_IPS='0.0.0.0/0'
export ALLOW_IPV6_TUNNEL=1
export IPV6_SUBNET='fddd:2c4:2c4:2c4::/64'
export SERVER_HAS_NATIVE_IPV6=1

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bats
# v5.15.0 SHA256 pin lockstep test.
# SHA256 pin lockstep test (5.15.x line).
#
# The installers (install_amneziawg.sh / install_amneziawg_en.sh) download the
# helper scripts over the network and verify them against hardcoded pins
@ -14,7 +14,7 @@
# ---------- RU installer pins ----------
@test "v5.15.0: RU installer COMMON pin matches awg_common.sh" {
@test "RU installer COMMON pin matches awg_common.sh" {
actual=$(sha256sum "$BATS_TEST_DIRNAME/../awg_common.sh" | cut -d' ' -f1)
pinned=$(grep -oP 'COMMON_SCRIPT_SHA256="\K[0-9a-f]{64}' "$BATS_TEST_DIRNAME/../install_amneziawg.sh")
[ "$actual" = "$pinned" ] || {
@ -23,7 +23,7 @@
}
}
@test "v5.15.0: RU installer MANAGE pin matches manage_amneziawg.sh" {
@test "RU installer MANAGE pin matches manage_amneziawg.sh" {
actual=$(sha256sum "$BATS_TEST_DIRNAME/../manage_amneziawg.sh" | cut -d' ' -f1)
pinned=$(grep -oP 'MANAGE_SCRIPT_SHA256="\K[0-9a-f]{64}' "$BATS_TEST_DIRNAME/../install_amneziawg.sh")
[ "$actual" = "$pinned" ] || {
@ -34,7 +34,7 @@
# ---------- EN installer pins ----------
@test "v5.15.0: EN installer COMMON pin matches awg_common_en.sh" {
@test "EN installer COMMON pin matches awg_common_en.sh" {
actual=$(sha256sum "$BATS_TEST_DIRNAME/../awg_common_en.sh" | cut -d' ' -f1)
pinned=$(grep -oP 'COMMON_SCRIPT_SHA256="\K[0-9a-f]{64}' "$BATS_TEST_DIRNAME/../install_amneziawg_en.sh")
[ "$actual" = "$pinned" ] || {
@ -43,7 +43,7 @@
}
}
@test "v5.15.0: EN installer MANAGE pin matches manage_amneziawg_en.sh" {
@test "EN installer MANAGE pin matches manage_amneziawg_en.sh" {
actual=$(sha256sum "$BATS_TEST_DIRNAME/../manage_amneziawg_en.sh" | cut -d' ' -f1)
pinned=$(grep -oP 'MANAGE_SCRIPT_SHA256="\K[0-9a-f]{64}' "$BATS_TEST_DIRNAME/../install_amneziawg_en.sh")
[ "$actual" = "$pinned" ] || {