From 1cbee098cab3311cbe62c6f8595518f810611fae Mon Sep 17 00:00:00 2001 From: Ivan Bondarev <50226185+bivlked@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:08:36 +0300 Subject: [PATCH] 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. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/docs-check.yml | 41 ++++ .github/workflows/release.yml | 31 ++- CHANGELOG.en.md | 69 ++++++- CHANGELOG.md | 69 ++++++- CONTRIBUTING.md | 2 +- README.en.md | 5 +- README.md | 5 +- SECURITY.md | 9 +- awg_common.sh | 96 ++++++--- awg_common_en.sh | 96 ++++++--- docs/RELEASE_PROCESS.md | 118 +++++++++++ docs/SIGNING_DESIGN.md | 4 +- install_amneziawg.sh | 64 ++++-- install_amneziawg_en.sh | 65 ++++-- manage_amneziawg.sh | 85 ++++++-- manage_amneziawg_en.sh | 87 ++++++-- scripts/check-docs-consistency.sh | 195 ++++++++++++++++++ scripts/preflight-check.sh | 74 +++++-- tests/test_v5151_audit_fixes.bats | 115 +++++++++++ ...test_v5151_ipv6_split_tunnel_option_c.bats | 63 ++++++ tests/test_v515_ipv6_route_all_no_native.bats | 4 + .../test_v515_ipv6_route_all_with_native.bats | 6 +- tests/test_v515_sha_pins_lockstep.bats | 10 +- 24 files changed, 1134 insertions(+), 181 deletions(-) create mode 100644 .github/workflows/docs-check.yml create mode 100644 docs/RELEASE_PROCESS.md create mode 100644 scripts/check-docs-consistency.sh create mode 100644 tests/test_v5151_audit_fixes.bats create mode 100644 tests/test_v5151_ipv6_split_tunnel_option_c.bats diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0405e5d..7087079 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml new file mode 100644 index 0000000..6d75117 --- /dev/null +++ b/.github/workflows/docs-check.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f995ec5..5b05f34 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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" diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 4b3adf0..46010e4 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e6bb2..bc69d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cccfa1..ee27a71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/ ``` diff --git a/README.en.md b/README.en.md index 6516f36..a78bbbe 100644 --- a/README.en.md +++ b/README.en.md @@ -20,7 +20,7 @@ x86_64 | ARM64 | ARMv7 License Status - Version + Version AWG 2.0 ShellCheck Tests @@ -363,7 +363,7 @@ sudo bash /root/awg/manage_amneziawg.sh [arguments] | :-------- | :--------------------- | :----------------------------- | :------: | | `add` | ` [name2 ...] [--expires=DUR]` | Add client(s) (opt. with expiry) | No (auto) | | `remove` | ` [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` | ` ` | 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) diff --git a/README.md b/README.md index 3f311ce..8f7e901 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ x86_64 | ARM64 | ARMv7 License Status - Version + Version AWG 2.0 ShellCheck Tests @@ -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 дней) diff --git a/SECURITY.md b/SECURITY.md index a332fdd..ffaad51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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. diff --git a/awg_common.sh b/awg_common.sh index ab65363..9cf960c 100644 --- a/awg_common.sh +++ b/awg_common.sh @@ -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 = /32, /128 -# - AllowedIPs: SERVER_HAS_NATIVE_IPV6=1 -> 0.0.0.0/0, ::/0 -# SERVER_HAS_NATIVE_IPV6=0 -> 0.0.0.0/0, +# - AllowedIPs (зеркалю IPv4 routing mode в IPv6, intent-mirroring): +# full tunnel (ALLOWED_IPS=0.0.0.0/0): + ::/0 (native) или + (no-native) +# split tunnel (кастомный ALLOWED_IPS): IPv4-список БЕЗ изменений + ТОЛЬКО , +# НИКОГДА ::/0 - нет IPv6 split-list, нельзя угонять весь IPv6 (ломает split-tunnel). # Если пустой (legacy-клиент): Address = /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 [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 diff --git a/awg_common_en.sh b/awg_common_en.sh index 1685bb1..01a9b04 100644 --- a/awg_common_en.sh +++ b/awg_common_en.sh @@ -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 = /32, /128 -# - AllowedIPs: SERVER_HAS_NATIVE_IPV6=1 -> 0.0.0.0/0, ::/0 -# SERVER_HAS_NATIVE_IPV6=0 -> 0.0.0.0/0, +# - AllowedIPs (mirror the IPv4 routing mode into IPv6, intent-mirroring): +# full tunnel (ALLOWED_IPS=0.0.0.0/0): + ::/0 (native) or + (no-native) +# split tunnel (custom ALLOWED_IPS): IPv4 list UNCHANGED + ONLY , +# NEVER ::/0 - there is no IPv6 split-list, hijacking all IPv6 breaks split-tunnel. # If empty (legacy client): Address = /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 [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 ` 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 diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md new file mode 100644 index 0000000..dd9f6ad --- /dev/null +++ b/docs/RELEASE_PROCESS.md @@ -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 `..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 +`` 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`. diff --git a/docs/SIGNING_DESIGN.md b/docs/SIGNING_DESIGN.md index 1c549f9..e3185d0 100644 --- a/docs/SIGNING_DESIGN.md +++ b/docs/SIGNING_DESIGN.md @@ -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 diff --git a/install_amneziawg.sh b/install_amneziawg.sh index ab6e541..4b409c4 100644 --- a/install_amneziawg.sh +++ b/install_amneziawg.sh @@ -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 diff --git a/install_amneziawg_en.sh b/install_amneziawg_en.sh index b7f61f9..00599d6 100644 --- a/install_amneziawg_en.sh +++ b/install_amneziawg_en.sh @@ -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 diff --git a/manage_amneziawg.sh b/manage_amneziawg.sh index 60950fa..542d8c9 100644 --- a/manage_amneziawg.sh +++ b/manage_amneziawg.sh @@ -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 diff --git a/manage_amneziawg_en.sh b/manage_amneziawg_en.sh index 4f19de4..d033d21 100644 --- a/manage_amneziawg_en.sh +++ b/manage_amneziawg_en.sh @@ -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 [name2 ...] Add client(s). --expires applies to all" echo " remove [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

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 diff --git a/scripts/check-docs-consistency.sh b/scripts/check-docs-consistency.sh new file mode 100644 index 0000000..45f90b1 --- /dev/null +++ b/scripts/check-docs-consistency.sh @@ -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")" + + # Целевые якоря в файле: явные / + slug'и заголовков. + declare -A anchors=() + while IFS= read -r a; do + [[ -n "$a" ]] && anchors["$a"]=1 + done < <(printf '%s\n' "$stripped" | grep -oiP '&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 diff --git a/scripts/preflight-check.sh b/scripts/preflight-check.sh index 070c26b..e06eb43 100644 --- a/scripts/preflight-check.sh +++ b/scripts/preflight-check.sh @@ -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: ..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=)" + 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 ===" diff --git a/tests/test_v5151_audit_fixes.bats b/tests/test_v5151_audit_fixes.bats new file mode 100644 index 0000000..ca857c9 --- /dev/null +++ b/tests/test_v5151_audit_fixes.bats @@ -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"* ]] +} diff --git a/tests/test_v5151_ipv6_split_tunnel_option_c.bats b/tests/test_v5151_ipv6_split_tunnel_option_c.bats new file mode 100644 index 0000000..3d32111 --- /dev/null +++ b/tests/test_v5151_ipv6_split_tunnel_option_c.bats @@ -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" +} diff --git a/tests/test_v515_ipv6_route_all_no_native.bats b/tests/test_v515_ipv6_route_all_no_native.bats index 71d945b..5beae15 100644 --- a/tests/test_v515_ipv6_route_all_no_native.bats +++ b/tests/test_v515_ipv6_route_all_no_native.bats @@ -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 diff --git a/tests/test_v515_ipv6_route_all_with_native.bats b/tests/test_v515_ipv6_route_all_with_native.bats index a355597..a8661ad 100644 --- a/tests/test_v515_ipv6_route_all_with_native.bats +++ b/tests/test_v515_ipv6_route_all_with_native.bats @@ -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 diff --git a/tests/test_v515_sha_pins_lockstep.bats b/tests/test_v515_sha_pins_lockstep.bats index 8e13795..cb56241 100644 --- a/tests/test_v515_sha_pins_lockstep.bats +++ b/tests/test_v515_sha_pins_lockstep.bats @@ -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" ] || {