diff --git a/Cargo.lock b/Cargo.lock index 55784ea..69a3858 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "awg2-harness" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", diff --git a/README.md b/README.md index f578d42..6868a1e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # AWG2 Harness -`AWG2 Harness` — host-side orchestrator для выделенного Debian-хоста с self-hosted AmneziaWG. Пакет управляет инфраструктурным состоянием вокруг Amnezia: per-peer traffic shaping, IFB ingress shaping, nftables host firewall, anti-torrent connection guard, managed swap, диагностикой drift и плановым reboot. +`AWG2 Harness` — host-side orchestrator для выделенного Debian-хоста с self-hosted AmneziaWG. Основной target релиза 0.3.1 — AmneziaWG 3.1; AWG 3.0/2.x сохраняются как regression compatibility. Пакет управляет инфраструктурным состоянием вокруг Amnezia: per-peer traffic shaping, IFB ingress shaping, nftables host firewall, anti-torrent connection guard, managed swap, диагностикой drift и плановым reboot. -Проект разрабатывался и проверялся под AmneziaWG/AWG 2.0 и совместимый legacy-сценарий AmneziaWG/AWG 1.5. Он не заменяет AmneziaVPN, не генерирует ключи клиентов и не меняет продуктовую логику Amnezia. Amnezia сначала ставится официальным способом, после этого устанавливается AWG2 Harness. +Релиз source-validated по официальной документации и исходникам AWG 3.1; фактическая live-приёмка выполняется после production deployment. Harness не заменяет AmneziaVPN, не генерирует ключи клиентов и не мигрирует AWG 2.0 на 3.1. Amnezia сначала ставится официальным способом, после этого устанавливается AWG2 Harness. Точная upstream-база зафиксирована в [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md). ## Что ставит пакет @@ -118,37 +118,38 @@ AWG2 Harness не устанавливает Amnezia. Используйте о 1. Установите актуальное приложение AmneziaVPN на рабочую машину. 2. В приложении выберите self-hosted server. 3. Укажите публичный IPv4 сервера, SSH-порт, SSH-пользователя и приватный ключ. -4. Установите AmneziaWG/AWG. Для AWG 2.0 нужен актуальный AmneziaVPN, где протокол AmneziaWG 2.0 доступен в списке протоколов. +4. Установите AmneziaWG 3.1. Для self-hosted deployment требуется AmneziaVPN 5.0.1.5 или новее. 5. После установки убедитесь, что контейнер поднят и UDP-порт опубликован. Проверка на сервере: ```bash docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' -C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" echo "container=$C" docker port "$C" docker exec "$C" awg show ``` -Ожидаемые имена контейнера по умолчанию: `amnezia-awg2` или `amnezia-awg`. `amnezia-awg3` перечислен на будущее — в AmneziaVPN 5.0 такого типа контейнера ещё нет. Ожидаемый интерфейс внутри container netns: обычно `awg0`, fallback-кандидаты описаны в `config.example.toml`. +Ожидаемые имена контейнера по умолчанию: основной `amnezia-awg2`, затем legacy fallback `amnezia-awg`. Ожидаемый интерфейс внутри container netns — `awg0`; дополнительные fallback-кандидаты описаны в `config.example.toml`. Любое другое корректное имя можно указать вручную в config. -### Совместимость с AmneziaWG 3.0 +### Совместимость с AmneziaWG 3.1 -Проверено по исходникам на: AmneziaVPN `5.0.0.5`, `amneziawg-tools` и `amneziawg-go` ветки `master` на момент правки. Утверждения ниже относятся к этим версиям; при более новых релизах их нужно перепроверить, а не считать бессрочными. +Канонические версии, commit SHA и официальные ссылки приведены в [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md). Релиз 0.3.1 source-validated against AWG 3.1; до отдельной live-приёмки это не означает «протестировано на production AWG 3.1». -Обвязка не реализует AWG на уровне пакетов: `tc`, IFB и `nftables` работают с inner-IP уже после декапсуляции, поэтому header protection, content padding и рандомизация таймеров AWG 3.0 её не касаются. +Обвязка не реализует AWG на уровне пакетов: `tc`, IFB и `nftables` работают с inner-IP уже после декапсуляции, поэтому header protection, content padding, RandomTrailers и DisableCookies её не касаются. -Читается только peer state, всегда через `docker exec`. Реальный бинарь в официальном образе — `/usr/bin/awg`, `/usr/bin/wg` является symlink на него; порядок задаётся `amnezia.wg_binaries` (по умолчанию `["awg", "wg"]`). На хосте `wireguard-tools` не требуется и не проверяется в preflight: AmneziaWG — userspace-реализация, её UAPI-сокет лежит в mount namespace контейнера, и хостовый `wg` через `nsenter -n` до него не достаёт. +Peer state читается через `docker exec` официальным `awg` внутри контейнера; `/usr/bin/wg` в reference image является symlink на него. Это единая container-side точка доступа независимо от того, использован kernel `amneziawg` или fallback `amneziawg-go`, и она не привязывает harness к host-side tools. Порядок задаётся `amnezia.wg_binaries` (по умолчанию `["awg", "wg"]`); host `wireguard-tools` не требуется. Smoke-проверка после обновления контейнера: ```bash -C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" docker exec "$C" readlink -f /usr/bin/wg # ожидается /usr/bin/awg docker exec "$C" awg --version docker exec "$C" ip -o link show awg0 +docker exec "$C" ip -d link show awg0 docker exec "$C" awg show all allowed-ips docker port "$C" @@ -156,15 +157,15 @@ docker port "$C" docker exec "$C" awg show all dump | awk -F '\t' '{ print "row=" NR, "fields=" NF, "interface=" $1 }' ``` -Ожидаемо: `/usr/bin/wg` указывает на `/usr/bin/awg`, интерфейс `awg0` существует, peer-адреса отображаются, серверный порт опубликован по UDP. Peer-строки дают 9 полей. +Для reference tools AWG 3.1 ожидаемо: `/usr/bin/wg` указывает на `/usr/bin/awg`, интерфейс `awg0` существует, peer-адреса отображаются, серверный порт опубликован по UDP, interface-row содержит 30 полей, peer-row — 9. -Ширина interface-строки зависит от версии `amneziawg-tools`, а не от версии параметров в конфигурации: утилиты AWG 3.0 печатают расширенную строку и для туннеля, настроенного как AWG 2.0. По количеству колонок определять версию протокола нельзя, и обвязка этого не делает. +Исторически WireGuard/AWG 2.x/AWG 3.0/AWG 3.1 tools печатают interface-row шириной 5/14/28/30 полей соответственно; текущий peer-row содержит 9. Это только диагностика: ширина зависит от producer tools, а не от версии параметров туннеля. По количеству колонок harness не определяет версию и не классифицирует peer. Полный вывод `awg show all dump` передавать нельзя: во второй колонке interface-строки находится приватный ключ устройства. ### 3.1 Установить AmneziaDNS (обязательный production-шаг) -Для production-профиля AWG2 установите AmneziaDNS из приложения AmneziaVPN после установки AmneziaWG/AWG: +Для production-профиля AWG 3.1 установите AmneziaDNS из приложения AmneziaVPN после установки AmneziaWG: ```text AmneziaVPN: @@ -188,13 +189,13 @@ Security note: ```bash cd awg2_harness ./tools/build.sh --prepare-build-env --bootstrap-rust --yes -./tools/build.sh --package --version 0.3.0 +./tools/build.sh --package (cd dist && sha256sum -c SHA256SUMS) -dpkg-deb -I dist/awg2-harness_0.3.0_amd64.deb -dpkg-deb -c dist/awg2-harness_0.3.0_amd64.deb | sort +dpkg-deb -I dist/awg2-harness_0.3.1_amd64.deb +dpkg-deb -c dist/awg2-harness_0.3.1_amd64.deb | sort ``` -`--version` можно не указывать, если версия в `traffic_core/Cargo.toml` уже соответствует release. Если version override указан, он меняет версию Debian-пакета, а не версию crate в `Cargo.toml`. +Release package берёт версию из `traffic_core/Cargo.toml`. `--version` предназначен только для осознанного Debian packaging override и не меняет версию crate. ### 5. Перенести `.deb` и config на production @@ -224,7 +225,7 @@ scp -P 2323 dist/SHA256SUMS root@:/root/ ```bash cd /root sha256sum -c SHA256SUMS -chmod 644 /root/awg2-harness_0.3.0_amd64.deb +chmod 644 /root/awg2-harness_0.3.1_amd64.deb apt install ./awg2-harness_*_amd64.deb install -d -m 0755 /etc/awg2-harness install -m 0644 /root/awg2-harness.config.toml /etc/awg2-harness/config.toml @@ -237,7 +238,7 @@ install -m 0644 /root/awg2-harness.config.toml /etc/awg2-harness/config.toml ```bash cd /root sha256sum -c SHA256SUMS -chmod 644 /root/awg2-harness_0.3.0_amd64.deb +chmod 644 /root/awg2-harness_0.3.1_amd64.deb apt install ./awg2-harness_*_amd64.deb install -d -m 0755 /etc/awg2-harness cp /usr/share/doc/awg2-harness/repo/config.example.toml /etc/awg2-harness/config.toml @@ -317,7 +318,7 @@ awg2-harness --config /etc/awg2-harness/config.toml status Дополнительно можно проверить фактическую `tc` topology внутри network namespace Amnezia: ```bash -C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" PID="$(docker inspect -f '{{.State.Pid}}' "$C")" nsenter -t "$PID" -n -- tc class show dev awg0 @@ -344,7 +345,7 @@ awg2-harness --config /etc/awg2-harness/config.toml `apply-firewall` делает два reconcile: host INPUT firewall в таблице `inet awg2_host_firewall` и anti-torrent guard в выбранном namespace. Команда не управляет Docker NAT и host `FORWARD`. -`apply-limits` находит Amnezia container, заходит в его network namespace, читает peers через `wg`, строит per-peer HTB/fq_codel классы на AWG/WG интерфейсе и IFB ingress path. Если drift не найден, пишет `traffic rules up-to-date, skip apply`. +`apply-limits` находит Amnezia container, заходит в его network namespace, читает peers через container-side `awg`/`wg`, строит per-peer HTB/fq_codel классы на AWG/WG интерфейсе и IFB ingress path. Если drift не найден, пишет `traffic rules up-to-date, skip apply`. `status` печатает фактическую диагностику: сервис, контейнер, netns, интерфейс, peers, WireGuard runtime, traffic desired/actual, host firewall drift, Docker/Amnezia port path, anti-torrent counters, swap state, conntrack, recent events и planned reboot. @@ -386,6 +387,7 @@ AWG2 Harness не владеет: ## Документация - [`docs/INSTALLATION.md`](docs/INSTALLATION.md) — установка и preflight. +- [`docs/UPSTREAM_COMPATIBILITY.md`](docs/UPSTREAM_COMPATIBILITY.md) — upstream-версии и контракт AWG 3.1. - [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) — параметры `config.toml`. - [`docs/ACCEPTANCE_CHECKLIST.md`](docs/ACCEPTANCE_CHECKLIST.md) — production checklist. - [`docs/ANTI_TORRENT.md`](docs/ANTI_TORRENT.md) — connection-count guard. diff --git a/config.example.toml b/config.example.toml index 14faa00..05ec516 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,7 +1,6 @@ [amnezia] # Порядок важен: используется первый запущенный контейнер из списка. -# amnezia-awg3 указан на будущее: в AmneziaVPN 5.0 такого типа контейнера ещё нет. -container_names = ["amnezia-awg2", "amnezia-awg3", "amnezia-awg"] +container_names = ["amnezia-awg2", "amnezia-awg"] interface_candidates = ["awg0", "wg0", "amnezia0", "tun0"] interface_patterns = ["awg*", "wg*", "amnezia*", "tun*"] @@ -14,7 +13,7 @@ wg_binaries = ["awg", "wg"] # Подсеть VPN peers. # Проверка: -# C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +# C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" # docker exec "$C" ip -4 addr # docker exec "$C" awg show peer_ipv4_cidr = "10.8.1.0/24" @@ -63,7 +62,7 @@ enabled = true # Должен совпадать с реально слушающим портом sshd до apply-firewall. ssh_port = 2323 # Должен совпадать с опубликованным Docker/Amnezia UDP-портом. -# Проверка: docker port amnezia-awg2 2>/dev/null || docker port amnezia-awg3 2>/dev/null || docker port amnezia-awg +# Проверка: docker port amnezia-awg2 2>/dev/null || docker port amnezia-awg # Транспортный порт AmneziaWG/AWG, опубликованный Docker, обычно 443/udp. # Это не AmneziaDNS. Не указывайте здесь 53. amnezia_port = 443 @@ -73,7 +72,7 @@ default_input_policy = "drop" [anti_torrent] enabled = true -max_connections_per_peer = 150 +max_connections_per_peer = 500 # Обычно та же подсеть, что и [amnezia].peer_ipv4_cidr. peer_ipv4_cidr = "10.8.1.0/24" apply_in_amnezia_netns = true diff --git a/docs/ACCEPTANCE_CHECKLIST.md b/docs/ACCEPTANCE_CHECKLIST.md index e9ff49c..c129635 100644 --- a/docs/ACCEPTANCE_CHECKLIST.md +++ b/docs/ACCEPTANCE_CHECKLIST.md @@ -4,8 +4,8 @@ - [ ] Хост — Debian 13 amd64. - [ ] Сервер выделенный, без соседних сервисов. -- [ ] AmneziaWG/AWG 2.0 или AmneziaWG/AWG 1.5 установлена официальным способом. -- [ ] Контейнер `amnezia-awg2` или `amnezia-awg` запущен (`amnezia-awg3` — резерв на будущее, в AmneziaVPN 5.0 такого типа контейнера ещё нет). +- [ ] AmneziaWG 3.1 установлена официальным self-hosted способом через AmneziaVPN 5.0.1.5 или новее. +- [ ] Контейнер `amnezia-awg2` запущен; `amnezia-awg` допустим только как осознанный legacy fallback. - [ ] `docker ps` видит контейнер. - [ ] Можно получить PID контейнера. - [ ] `/proc//ns/net` существует. @@ -15,6 +15,12 @@ ## B. Обнаружение peers +- [ ] `C=amnezia-awg2` соответствует фактически запущенному контейнеру. +- [ ] `docker exec "$C" awg --version` зафиксирован в acceptance evidence. +- [ ] `docker exec "$C" ip -o link show awg0` находит основной интерфейс. +- [ ] `docker exec "$C" ip -d link show awg0` фиксирует фактический kernel/userspace runtime без требования конкретной реализации. +- [ ] `docker exec "$C" awg show all dump | awk -F '\t' '{print NR, NF, $1}'` для reference AWG 3.1 tools показывает 30 полей interface-row и 9 полей peer-row. +- [ ] Width используется только как acceptance evidence; parser не определяет по нему версию и не классифицирует peer. - [ ] `awg2-harness status` показывает найденный контейнер. - [ ] `awg2-harness status` показывает PID/netns контейнера. - [ ] `awg2-harness status` показывает найденный интерфейс. @@ -171,7 +177,8 @@ - [ ] Есть chain `forward_antitorrent` с hook `forward` и policy `accept`. - [ ] Правило ограничивает `ip saddr `, default `10.8.1.0/24`. - [ ] Лимит считается per source VPN IP через meter/set, а не глобально по всей подсети. -- [ ] Порог совпадает с `anti_torrent.max_connections_per_peer`; текущий пример config использует `150`, более строгий вариант — `100`. +- [ ] Порог совпадает с `anti_torrent.max_connections_per_peer`; shipped production baseline — `500`. +- [ ] В фактической nft rule присутствует `ct count over 500` для shipped config. - [ ] Drop counter виден в `status`. - [ ] На first-run без table `awg2-harness apply-firewall` создаёт `table inet awg2_harness`, а не падает. - [ ] При превышении порога новые flows дропаются, established flows ниже порога не ломаются. @@ -193,6 +200,7 @@ - [ ] Package собирается на отдельной Debian 13 amd64 build-machine. - [ ] Production host не требует Rust/cargo/source tree. - [ ] `./tools/build.sh --package` создаёт `dist/awg2-harness__amd64.deb`. +- [ ] Release package без `--version` получает версию `0.3.1` из `traffic_core/Cargo.toml`. - [ ] `dist/SHA256SUMS` валидирует checksum пакета. - [ ] Пакет ставит бинарь в `/usr/sbin/awg2-harness`. - [ ] Пакет ставит unit в `/lib/systemd/system/awg2-harness.service`. @@ -219,7 +227,7 @@ - [ ] AmneziaDNS установлен из приложения AmneziaVPN. - [ ] Контейнер `amnezia-dns` в состоянии healthy. - [ ] `docker inspect amnezia-dns` показывает `"53/tcp":null` и `"53/udp":null`. -- [ ] `amnezia-dns` и активный AWG-контейнер (`amnezia-awg2`, `amnezia-awg3` или `amnezia-awg`) подключены к одной Amnezia DNS Docker-сети. +- [ ] `amnezia-dns` и активный AWG-контейнер (`amnezia-awg2` или осознанный legacy `amnezia-awg`) подключены к одной Amnezia DNS Docker-сети. - [ ] Публичный DNS-запрос к `:53` не резолвит домены; допустимы `connection refused`, `timeout` или `no servers could be reached`. - [ ] DNS-резолвинг у VPN-клиента работает при активном подключении. - [ ] AWG2 host firewall не открывает `53/tcp` и `53/udp`. diff --git a/docs/ANTI_TORRENT.md b/docs/ANTI_TORRENT.md index 26fa8d3..b99f0d4 100644 --- a/docs/ANTI_TORRENT.md +++ b/docs/ANTI_TORRENT.md @@ -4,7 +4,7 @@ ## 1. Цель -Не детектить BitTorrent как протокол. Цель проще: ограничить количество одновременных conntrack flows на один VPN peer IP. Торрент-клиенты обычно пытаются держать сотни TCP/UDP peer-сессий, DHT/uTP/trackers. Лимит в диапазоне 100–250 одновременных flows заметно ухудшает P2P-поведение, но почти не затрагивает обычные сценарии при корректной настройке. +Не детектить BitTorrent как протокол. Цель проще: ограничить количество одновременных conntrack flows на один VPN peer IP. Торрент-клиенты обычно пытаются держать сотни TCP/UDP peer-сессий, DHT/uTP/trackers. Штатный baseline `500` сохраняет мягкий connection guard, не вмешиваясь в нормальный повседневный профиль по результатам эксплуатационных измерений проекта. Это мягкий технический барьер, а не abuse-система со страйками. @@ -54,7 +54,7 @@ table inet awg2_harness { type filter hook forward priority filter; policy accept; ip saddr 10.8.1.0/24 ct state new \ - meter awg2_peer_connlimit_v4 { ip saddr ct count over 100 } \ + meter awg2_peer_connlimit_v4 { ip saddr ct count over 500 } \ counter name antitorrent_dropped_v4 \ drop } @@ -70,15 +70,10 @@ table inet awg2_harness { ```toml [anti_torrent] enabled = true -max_connections_per_peer = 150 +max_connections_per_peer = 500 ``` -Практические варианты: - -- `100` — более строгий режим. -- `150` — текущий рекомендуемый стартовый baseline, мягче к обычному browsing/mobile traffic. -- `200/250` — ещё более permissive варианты. -- `70` — агрессивный режим только после наблюдения реального трафика. +`500` — production baseline, выбранный по прикладным и нагрузочным измерениям проекта. Connection guard остаётся включённым, но порог не должен мешать обычному browsing/mobile/application traffic. Любое значение ниже `500` является более агрессивной операторской политикой и требует отдельной проверки на реальных клиентах. Значение остаётся конфигурируемым и не зашито в алгоритм. ## 5. Почему это должно быть в orchestrator @@ -92,7 +87,7 @@ max_connections_per_peer = 150 ## 6. Conntrack и производительность -`ct count` использует conntrack. На текущем масштабе это нормально: даже 23 peers × 100 flows = около 2300 активных flows до ограничения. Это не выглядит опасно для production-хоста само по себе. +`ct count` использует conntrack. Для примера 23 peers × 500 flows дают до 11 500 потенциальных flow slots перед ограничением каждого peer. Само по себе это не является доказательством достаточной ёмкости конкретного хоста, поэтому фактическое состояние conntrack обязательно наблюдается. Что надо мониторить: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3c1a899..b40e148 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,6 +1,6 @@ # Конфигурация AWG2 Harness -AWG2 Harness разрабатывался для self-hosted AmneziaWG/AWG 2.0 и совместимого legacy-сценария AmneziaWG/AWG 1.5. Конфиг живёт в `/etc/awg2-harness/config.toml`. Он общий для всего пакета и должен содержать только эксплуатационно значимые параметры. +Основной target AWG2 Harness 0.3.1 — self-hosted AmneziaWG 3.1. AWG 3.0/2.x и `amnezia-awg` сохраняются как regression/legacy compatibility. Точная upstream-база описана в [`UPSTREAM_COMPATIBILITY.md`](UPSTREAM_COMPATIBILITY.md). Конфиг живёт в `/etc/awg2-harness/config.toml`, общий для всего пакета и содержит только эксплуатационно значимые параметры. ## Создание runtime config @@ -24,9 +24,9 @@ nano /etc/awg2-harness/config.toml Discovery-команды: ```bash -docker port amnezia-awg2 2>/dev/null || docker port amnezia-awg3 2>/dev/null || docker port amnezia-awg +docker port amnezia-awg2 2>/dev/null || docker port amnezia-awg -C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" docker exec "$C" ip -4 addr docker exec "$C" awg show ``` @@ -35,7 +35,7 @@ docker exec "$C" awg show ```toml [amnezia] -container_names = ["amnezia-awg2", "amnezia-awg3", "amnezia-awg"] +container_names = ["amnezia-awg2", "amnezia-awg"] interface_candidates = ["awg0", "wg0", "amnezia0", "tun0"] interface_patterns = ["awg*", "wg*", "amnezia*", "tun*"] wg_binaries = ["awg", "wg"] @@ -73,7 +73,7 @@ default_input_policy = "drop" [anti_torrent] enabled = true -max_connections_per_peer = 150 +max_connections_per_peer = 500 peer_ipv4_cidr = "10.8.1.0/24" apply_in_amnezia_netns = true @@ -92,9 +92,7 @@ swappiness = 1 ## Пояснения -`amnezia.container_names` — имена контейнеров, которые считаются Amnezia/AWG. Порядок важен, сопоставление точное. По текущему production-снимку основной вариант — `amnezia-awg2`, fallback — `amnezia-awg`. - -`amnezia-awg3` перечислен на будущее и **не является подтверждённым официальным именем**: в проверенной версии AmneziaVPN `5.0.0.5` поддержка AWG 3.0 добавлена, но варианты контейнера по-прежнему представлены как `Awg` и `Awg2`, отдельного типа `Awg3` нет. Имя оставлено в примере потому, что несовпавшее имя не стоит ничего — поиск просто идёт дальше по списку, — тогда как отсутствующее в списке имя даст молчаливое «контейнер не найден». Если Amnezia введёт другое имя, его нужно будет дописать сюда. +`amnezia.container_names` — упорядоченный список точных имён контейнеров Amnezia/AWG. Основной официальный вариант — `amnezia-awg2`, затем проверяется legacy `amnezia-awg`. Отдельного официального типа `amnezia-awg3` в зафиксированной upstream-версии нет. Другое имя можно явно добавить в runtime config; неизвестные имена автоматически не угадываются. `amnezia.interface_candidates` — точные имена интерфейса, которые пробуются первыми. @@ -102,7 +100,7 @@ swappiness = 1 `amnezia.wg_binaries` — утилита чтения peer state внутри контейнера, пробуется по порядку до первого успеха. Default — `["awg", "wg"]`. В официальном образе AmneziaWG реальный бинарь `/usr/bin/awg`, а `/usr/bin/wg` — symlink на него, поэтому `awg` идёт первым; `wg` остаётся для контейнеров с чистым WireGuard. Значения выполняются внутри контейнера и валидируются как имена команд: непустые, без пробелов и без разделителей пути. Параметр опциональный — конфиги, созданные до его появления, продолжают работать с default. -Хостовый `wireguard-tools` не нужен и не проверяется в preflight. Peer state читается только через `docker exec`: AmneziaWG — userspace-реализация, её UAPI-сокет находится в `RUNSTATEDIR/amneziawg/` внутри mount namespace контейнера, а `nsenter -n` входит только в network namespace, поэтому хостовый `wg` до него не достаёт. +Хостовый `wireguard-tools` не нужен и не проверяется в preflight. Peer state читается через `docker exec` официальным `awg` внутри контейнера. Это единая container-side точка доступа для kernel `amneziawg` и fallback `amneziawg-go`, не зависящая от версии host-side tools. `amnezia.peer_ipv4_cidr` — подсеть VPN peers. Production default — `10.8.1.0/24`; значение конфигурируемое. @@ -177,7 +175,7 @@ swappiness = 1 `host_firewall.default_input_policy` — политика INPUT chain (`drop` или `accept`). Для production рекомендуется `drop`. -`anti_torrent.max_connections_per_peer` — максимум одновременных conntrack flows на один VPN peer IP. Текущий пример использует `150`: это мягче, чем строгий `100`, и обычно безопаснее как первый production baseline. `100` можно использовать как более жёсткий режим; `200/250` — более permissive варианты. +`anti_torrent.max_connections_per_peer` — максимум одновременных conntrack flows на один VPN peer IP. Штатный production baseline — `500`, выбранный по эксплуатационным измерениям проекта так, чтобы connection guard оставался включённым и не вмешивался в нормальный повседневный профиль. Значение ниже `500` является сознательно более агрессивной политикой и требует проверки на реальных клиентах. Поле обязательное и не скрыто в Rust default. `anti_torrent.peer_ipv4_cidr` — подсеть, на которую распространяется правило. Обычно должна совпадать с `amnezia.peer_ipv4_cidr`. diff --git a/docs/IMPLEMENTATION_SPEC.md b/docs/IMPLEMENTATION_SPEC.md index b6c8727..b504d98 100644 --- a/docs/IMPLEMENTATION_SPEC.md +++ b/docs/IMPLEMENTATION_SPEC.md @@ -9,7 +9,7 @@ Production-пакет AWG2 Harness содержит два Rust-бинаря: - `awg2-harness` — host-side оркестратор traffic/firewall/status/swap; - `awg2-reboot-scheduler` — отдельный сервис планировщика перезагрузки. -`awg2-harness` остаётся основным host-side мульти-оркестратором для выделенного сервера Debian 13 amd64 с AmneziaWG/AWG 2.0. +`awg2-harness` остаётся основным host-side мульти-оркестратором для выделенного сервера Debian 13 amd64. Основной upstream target релиза 0.3.1 — self-hosted AmneziaWG 3.1; AWG 3.0/2.x сохраняются как regression compatibility. Версии и source contract зафиксированы в [`UPSTREAM_COMPATIBILITY.md`](UPSTREAM_COMPATIBILITY.md). Он владеет инфраструктурным состоянием, которое нужно VPN-сервису: @@ -236,7 +236,7 @@ struct PeerTrafficStatus { Это снижает эксплуатационный риск: если контейнер Amnezia перестанет содержать `tc` или `iproute2`, host-side утилиты всё равно применят правила внутри его netns. -Чтение состояния peers — единственное исключение, и его нельзя выполнять host-side бинарём. AmneziaWG — userspace-реализация, её UAPI-сокет находится в `RUNSTATEDIR/amneziawg/.sock` внутри mount namespace контейнера, тогда как хостовый `wireguard-tools` ищет сокет в `/var/run/wireguard/`. `nsenter -n` входит только в network namespace, поэтому хостовый `wg` не достаёт ни до сокета, ни до kernel-устройства и всегда возвращает пустой список peers. Поэтому обнаружение peers использует исключительно `docker exec`, а `wireguard-tools` не является зависимостью хоста и не проверяется в preflight. +Чтение состояния peers — единственное исключение: harness намеренно использует официальный container-side `awg` через `docker exec`. `awg-quick` сначала пробует kernel interface типа `amneziawg`, а при его отсутствии запускает `amneziawg-go`. Container-side CLI является общей точкой доступа для обеих реализаций и изолирует harness от наличия и версии host-side `awg`/`wg`. Поэтому `wireguard-tools` не является зависимостью хоста и не проверяется в preflight. ## 6. Порядок discovery @@ -290,10 +290,10 @@ Fallback-парсер разбирает текстовый вывод, извл Классификация строк `show all dump`: - строка считается peer-строкой, только если в ней не менее 9 tab-колонок, колонка 5 (allowed ips) равна `(none)` или является списком CIDR через запятую, а колонки 6–8 (handshake, rx, tx) целиком парсятся как целые; -- **количество колонок не является дискриминатором и не должно им становиться.** Ширину interface-строки определяет версия `amneziawg-tools`, которая её печатает, а не то, какие параметры протокола реально настроены в туннеле: AWG 3.0-утилиты печатают расширенную строку и для конфигурации AWG 2.0, просто с нулевыми или пустыми новыми полями. Любое правило вида «N колонок означает версию X» неверно по построению; +- **количество колонок не является дискриминатором и не должно им становиться.** Исторически WireGuard/AWG 2.x/AWG 3.0/AWG 3.1 tools печатают interface-row шириной 5/14/28/30 полей, а текущий peer-row содержит 9. Ширину определяет producer `amneziawg-tools`, а не настроенная версия параметров: новые tools могут печатать широкую строку для старой конфигурации с нулевыми или пустыми новыми полями. Любое правило вида «N колонок означает версию X» неверно по построению; - interface-строка к тому же *длиннее* peer-строки, а на позициях handshake/rx/tx у неё стоят размеры junk-пакетов, которые тоже парсятся как целые, поэтому и эти колонки не разделяют два случая; - это важно не только для корректности: колонка 2 interface-строки — приватный ключ устройства, и ошибочно классифицированная interface-строка выдала бы его как public key peer; -- AWG 3.0 печатает протокольные таймеры диапазонами (`20-30`). Колонка keepalive у peer не читается, и диапазон в ней не должен ломать разбор. +- AWG 3.x печатает протокольные таймеры диапазонами (`20-30`). AWG 3.1 tools `v3.1.20260812` дополнительно печатают `RandomTrailers` и `DisableCookies` перед `fwmark`. Колонка keepalive у peer не читается, и диапазон в ней не должен ломать разбор. Политика выполнения для production: @@ -426,7 +426,7 @@ table inet awg2_harness { Целевой namespace — тот же Amnezia network namespace, где применяется `tc` shaping. Нельзя предполагать, что начальный host namespace видит исходные source-адреса peers. Если Amnezia выполняет NAT внутри namespace контейнера, host Docker bridge может видеть только адрес контейнера, а не `10.8.1.x`. -Порог в примере конфигурации — `150` одновременных flows на peer IP. `100` — более строгий режим. `70` агрессивнее и применяется только после наблюдения реального трафика. `200`/`250` — более permissive варианты. +Порог в примере конфигурации — `500` одновременных flows на peer IP. Это production baseline по эксплуатационным измерениям проекта: guard остаётся включённым, но не должен вмешиваться в нормальный повседневный профиль. Значение ниже `500` является более агрессивной политикой и требует отдельной проверки на реальных клиентах. Порог передаётся из config и не зашит в алгоритм. Размещение правила: hook `forward` с приоритетом `filter` или любым приоритетом после conntrack lookup. Оценка ограничивается `ct state new`, чтобы установленные flows продолжали работать и дропались только новые попытки сверх порога. @@ -696,10 +696,10 @@ inode = 123456 Модульные тесты: - валидация конфига, включая форму `amnezia.wg_binaries`; -- разбор `config.example.toml` в актуальную схему; +- разбор `config.example.toml` в актуальную схему с точным default-порядком контейнеров, `awg`/`wg` и anti-torrent baseline `500`; - парсер `awg|wg show all allowed-ips`, включая несколько allowed ips через пробел; - fallback-парсер `awg|wg show`; -- классификация peer/interface строк `awg|wg show all dump` против interface-строк AWG 3.0 с включённой и с выключенной защитой заголовка, AWG 2.0, чистого WireGuard, а также конфигурации AWG 2.0, прочитанной утилитами AWG 3.0; +- классификация peer/interface строк `awg|wg show all dump` против двух 30-полевых AWG 3.1 fixtures, AWG 3.0 с включённой и выключенной защитой заголовка, AWG 2.x, чистого WireGuard, а также конфигурации AWG 2.0, прочитанной tools AWG 3.0; - разбор dump с несколькими интерфейсами, с интерфейсом без peers и с повреждёнными строками; - порядок выбора бинаря wg и ошибка при отсутствии всех кандидатов; - фильтрация по CIDR; diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index f41dded..7eb06fc 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -5,14 +5,16 @@ ## Контракт развёртывания - Целевой production-хост: Debian 13 amd64. -- Amnezia ставится до AWG2 Harness официальным self-hosted способом через приложение AmneziaVPN. -- Проект рассчитан на AmneziaWG/AWG 2.0 и compatible legacy-сценарий AmneziaWG/AWG 1.5. +- Amnezia ставится до AWG2 Harness официальным self-hosted способом через AmneziaVPN 5.0.1.5 или новее. +- Основной target релиза 0.3.1 — AmneziaWG 3.1; AWG 3.0/2.x остаются regression compatibility. - SSH должен быть заранее настроен. Production-пример: порт `2323`, вход по ключу, password login disabled. - Production-хост получает готовый Debian-пакет; Rust/cargo/source tree на нём не требуются. - `/etc/awg2-harness/config.toml` создаётся вручную. `config.example.toml` не копируется автоматически как боевой config. - Пакет не устанавливает Docker/Amnezia и не включает services автоматически. - `sysctl_tuning` не применяется в `postinst`; применение только на старте daemon или вручную через CLI. +Основной документированный сценарий — clean installation AWG 3.1. Официальный переход с AWG 2.0 требует пересоздания контейнера и перевыпуска пользователей; harness эту миграцию не выполняет. Версии и исходный контракт приведены в [`UPSTREAM_COMPATIBILITY.md`](UPSTREAM_COMPATIBILITY.md). + ## A. Сборочная машина ### A1. Подготовить сборочное окружение @@ -40,10 +42,10 @@ cargo test --locked --workspace --release --manifest-path Cargo.toml ### A3. Собрать Debian-пакет ```bash -./tools/build.sh --package --version 0.3.0 +./tools/build.sh --package (cd dist && sha256sum -c SHA256SUMS) -dpkg-deb -I dist/awg2-harness_0.3.0_amd64.deb -dpkg-deb -c dist/awg2-harness_0.3.0_amd64.deb | sort +dpkg-deb -I dist/awg2-harness_0.3.1_amd64.deb +dpkg-deb -c dist/awg2-harness_0.3.1_amd64.deb | sort ``` `--version` можно опустить, если версия в `traffic_core/Cargo.toml` уже равна release version. Version override влияет только на metadata `.deb`. @@ -127,7 +129,7 @@ Password or SSH private key: ```bash docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' -C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" echo "container=$C" docker port "$C" docker exec "$C" awg show @@ -166,7 +168,7 @@ amnezia-dns amnezia-dns Up ... (healthy) 53/tcp, 53/udp Проверка публикации портов: ```bash -AWG="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +AWG="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" echo "AWG=$AWG" docker inspect -f 'ports={{json .NetworkSettings.Ports}}' amnezia-dns @@ -195,7 +197,7 @@ Optional deep-check DNS path (если нужна углублённая про ```bash apt install -y dnsutils -AWG="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +AWG="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" DNS_IP="$(docker inspect -f '{{range $n,$v := .NetworkSettings.Networks}}{{if $v.IPAddress}}{{println $v.IPAddress}}{{end}}{{end}}' amnezia-dns | head -n1)" DNS_PID="$(docker inspect -f '{{.State.Pid}}' amnezia-dns)" AWG_PID="$(docker inspect -f '{{.State.Pid}}' "$AWG")" diff --git a/docs/PRODUCTION_EXAMPLE.md b/docs/PRODUCTION_EXAMPLE.md index 7f77f4f..7af9a03 100644 --- a/docs/PRODUCTION_EXAMPLE.md +++ b/docs/PRODUCTION_EXAMPLE.md @@ -43,7 +43,7 @@ awg2-harness --config /etc/awg2-harness/config.toml status ```bash docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' -C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)" +C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg)$' | head -n1)" docker port "$C" docker exec "$C" awg show docker inspect -f 'ports={{json .NetworkSettings.Ports}}' amnezia-dns @@ -55,11 +55,12 @@ docker inspect -f 'ports={{json .NetworkSettings.Ports}}' amnezia-dns docker exec "$C" readlink -f /usr/bin/wg # ожидается /usr/bin/awg docker exec "$C" awg --version docker exec "$C" ip -o link show awg0 +docker exec "$C" ip -d link show awg0 docker exec "$C" awg show all allowed-ips docker exec "$C" awg show all dump | awk -F '\t' '{ print "row=" NR, "fields=" NF, "interface=" $1 }' ``` -Interface-строка `awg show all dump` даёт 14 полей на AWG 2.0 и 28 на AWG 3.0, peer-строки — 9. Полный вывод `awg show all dump` передавать нельзя: во второй колонке interface-строки находится приватный ключ устройства. +Reference AWG 3.1 tools `v3.1.20260812` дают 30 полей interface-row и 9 полей peer-row. Исторически WireGuard/AWG 2.x/AWG 3.0 interface-row имели 5/14/28 полей. Width используется только как acceptance evidence: harness не определяет по нему версию и не классифицирует peer. Полный вывод `awg show all dump` передавать нельзя: во второй колонке interface-row находится приватный ключ устройства. Точная upstream-база приведена в [`UPSTREAM_COMPATIBILITY.md`](UPSTREAM_COMPATIBILITY.md). Проверка, что runtime config создан вручную (ожидаемое поведение): diff --git a/docs/README.md b/docs/README.md index 1d31694..cf1c8d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,11 +3,12 @@ Рекомендуемый порядок чтения: 1. [`../README.md`](../README.md) — общий runtime-план: SSH, официальная Amnezia, установка пакета, preflight, проверка перезагрузкой и CLI. -2. [`INSTALLATION.md`](INSTALLATION.md) — сжатый порядок установки на production. -3. [`CONFIGURATION.md`](CONFIGURATION.md) — параметры `/etc/awg2-harness/config.toml`. -4. [`ACCEPTANCE_CHECKLIST.md`](ACCEPTANCE_CHECKLIST.md) — чек-лист приёмки перед статусом production-ready. -5. [`ANTI_TORRENT.md`](ANTI_TORRENT.md) — ограничение числа соединений через nftables. -6. [`IMPLEMENTATION_SPEC.md`](IMPLEMENTATION_SPEC.md) — архитектура и reconciliation состояния. +2. [`UPSTREAM_COMPATIBILITY.md`](UPSTREAM_COMPATIBILITY.md) — канонический контракт совместимости с AmneziaWG. +3. [`INSTALLATION.md`](INSTALLATION.md) — сжатый порядок установки на production. +4. [`CONFIGURATION.md`](CONFIGURATION.md) — параметры `/etc/awg2-harness/config.toml`. +5. [`ACCEPTANCE_CHECKLIST.md`](ACCEPTANCE_CHECKLIST.md) — чек-лист приёмки перед статусом production-ready. +6. [`ANTI_TORRENT.md`](ANTI_TORRENT.md) — ограничение числа соединений через nftables. +7. [`IMPLEMENTATION_SPEC.md`](IMPLEMENTATION_SPEC.md) — архитектура и reconciliation состояния. Отдельно: diff --git a/docs/UPSTREAM_COMPATIBILITY.md b/docs/UPSTREAM_COMPATIBILITY.md new file mode 100644 index 0000000..691e2d6 --- /dev/null +++ b/docs/UPSTREAM_COMPATIBILITY.md @@ -0,0 +1,79 @@ +# Совместимость с upstream AmneziaWG + +Этот документ — канонический источник сведений о версиях AmneziaWG, с которыми сверяется AWG2 Harness. Остальная документация не должна дублировать плавающий набор upstream-версий: она ссылается сюда. + +## Статус релиза 0.3.1 + +AWG2 Harness 0.3.1 проверен по официальной документации и исходникам AmneziaWG 3.1 по состоянию на 28 августа 2026 года. Это означает **source-validated against AWG 3.1**, но не заменяет live-приёмку на production-сервере. + +Основной сценарий: + +- self-hosted AmneziaWG 3.1; +- AmneziaVPN 5.0.1.5 или новее; +- чистая установка AWG 3.1 через AmneziaVPN; +- контейнер `amnezia-awg2`; +- интерфейс `awg0`; +- чтение состояния через `awg`, с fallback на `wg`. + +AWG 3.0 и 2.x остаются regression compatibility. Контейнер `amnezia-awg` поддерживается как legacy fallback. Название AWG2 Harness является именем продукта и пакета, а не ограничением на версию протокола. + +## Зафиксированная upstream-база + +| Компонент | Версия или ревизия | Назначение | +|---|---|---| +| AmneziaVPN | `5.0.1.5`, tag `7d4f3e0f5090b74903609179653d1f669d2ad08a` | Self-hosted deployment AWG 3.1 и container contract | +| `amneziawg-tools` | `v3.1.20260812`, commit `ee0f0a9aa34ff0a0da4b3433b9512781cfe02843` | CLI, `awg-quick`, формат `show all dump` | +| `amneziawg-go` | `v3.1.20260814`, commit `1b86b2ae0e493e7ea93f8c1a0f0cb6735b1551f1` | userspace fallback и reference container image | + +Официальные источники: + +- [FAQ: установка AWG 3.1 на self-hosted-сервер](https://docs.amnezia.org/ru/faq/); +- [поддерживаемые серверные ОС и архитектуры](https://docs.amnezia.org/ru/documentation/supported-linux-os-for-vps/); +- [отображение `DockerContainer::Awg2` в `amnezia-awg2`](https://github.com/amnezia-vpn/amnezia-client/blob/7d4f3e0f5090b74903609179653d1f669d2ad08a/client/core/utils/containers/containerUtils.cpp); +- [`show all dump` в tools v3.1.20260812](https://github.com/amnezia-vpn/amneziawg-tools/blob/v3.1.20260812/src/show.c); +- [kernel-first и userspace fallback в `awg-quick`](https://github.com/amnezia-vpn/amneziawg-tools/blob/v3.1.20260812/src/wg-quick/linux.bash); +- [Dockerfile `amneziawg-go v3.1.20260814`](https://github.com/amnezia-vpn/amneziawg-go/blob/v3.1.20260814/Dockerfile). + +Клиентский Dockerfile AmneziaVPN использует `amneziavpn/amneziawg-go:latest`. Поэтому перечисленные теги являются воспроизводимой reference/source baseline, но не обещанием, что любой будущий контейнер с тегом `latest` содержит те же сборки. Фактические версии проверяются во время live-приёмки. + +## Контракт контейнера и tools + +Default discovery имеет такой порядок: + +```toml +container_names = ["amnezia-awg2", "amnezia-awg"] +wg_binaries = ["awg", "wg"] +``` + +Отдельного официального типа или имени `amnezia-awg3` в зафиксированной версии клиента нет. Оператор может вручную указать другое корректное имя контейнера в `amnezia.container_names`. + +Reference image содержит `/usr/bin/awg`, `/usr/bin/awg-quick` и symlinks `/usr/bin/wg -> /usr/bin/awg`, `/usr/bin/wg-quick -> /usr/bin/awg-quick`. + +## Kernel и userspace + +`awg-quick` сначала выполняет `ip link add type amneziawg`. Если kernel implementation недоступна, запускается `amneziawg-go`. + +Harness не выбирает реализацию и не требует конкретную из них. Peer state читается через `docker exec` официальным `awg` внутри контейнера. Это единая container-side точка доступа для kernel и userspace вариантов и не создаёт зависимости от версии host-side tools. + +## Контракт `show all dump` + +Исторические widths являются диагностикой: + +| Producer | Interface-row | Peer-row | +|---|---:|---:| +| WireGuard tools | 5 | 9 | +| AWG 2.x tools | 14 | 9 | +| AWG 3.0 tools | 28 | 9 | +| AWG 3.1 tools `v3.1.20260812` | 30 | 9 | + +В AWG 3.1 поля `RandomTrailers` и `DisableCookies` добавлены перед `fwmark`. Peer representation осталось девятиполевым. + +**Количество колонок interface-row никогда не используется harness для определения версии протокола или классификации peer.** Версия tools определяет ширину interface-row, а новые tools могут печатать широкую строку и для конфигурации со старыми параметрами. Parser классифицирует peer семантически по allowed IPs и числовым handshake/RX/TX полям. + +Полный interface-row нельзя публиковать в отчётах: во второй колонке находится приватный ключ устройства. Для диагностики выводятся только номер строки, число полей и имя интерфейса. + +## Переход с AWG 2.0 + +Официальная модель перехода на AWG 3.1 — удалить AmneziaWG с сервера, установить заново новый контейнер, повторно создать пользователей и выпустить новые connection keys/configuration files. + +Harness не выполняет эту миграцию, не читает и не меняет `awg0.conf`, не генерирует ключи и не управляет протокольными параметрами. Основной документированный путь для 0.3.1 — clean installation. Live acceptance выполняется после того, как AmneziaVPN завершила установку AWG 3.1. diff --git a/tools/README.md b/tools/README.md index 5c64757..9a4407c 100644 --- a/tools/README.md +++ b/tools/README.md @@ -24,12 +24,14 @@ reboot_module -> awg2-reboot-scheduler cd awg2_harness chmod +x tools/build.sh ./tools/build.sh --prepare-build-env --bootstrap-rust --yes -./tools/build.sh --package --version 0.3.0 +./tools/build.sh --package (cd dist && sha256sum -c SHA256SUMS) -dpkg-deb -I dist/awg2-harness_0.3.0_amd64.deb -dpkg-deb -c dist/awg2-harness_0.3.0_amd64.deb | sort +dpkg-deb -I dist/awg2-harness_0.3.1_amd64.deb +dpkg-deb -c dist/awg2-harness_0.3.1_amd64.deb | sort ``` +Release package берёт версию из `traffic_core/Cargo.toml`. `--version` нужен только для осознанного Debian packaging override, валидируется как Debian version и не меняет версию Rust crate. + ## Что делает default запуск ```bash diff --git a/tools/build.sh b/tools/build.sh index 55c86f5..4b91156 100644 --- a/tools/build.sh +++ b/tools/build.sh @@ -64,7 +64,7 @@ Flags: --build Run build gate (default when no action flags) --package Build Debian package into dist/ (runs build gate by default) --dist-dir Output directory for package artifacts (default: ./dist) - --version Override package version (default: read from Cargo.toml) + --version Override with a valid Debian package version (default: Cargo.toml) --install Install binary and systemd unit (no enable/start) --enable Enable and start awg2-harness.service --yes Confirm system mutations (apt/rustup) @@ -297,6 +297,25 @@ read_package_version_from_manifest() { awk -F '"' '/^[[:space:]]*version[[:space:]]*=[[:space:]]*"/ { print $2; exit }' "$CRATE_MANIFEST" } +validate_package_version() { + local version="$1" + + if [[ -z "$version" ]]; then + echo "Package version must not be empty" + exit 1 + fi + if [[ "$version" == *'/'* || "$version" == *'\'* ]]; then + echo "Package version must not contain path separators: $version" + exit 1 + fi + + require_cmd dpkg + if ! dpkg --validate-version "$version"; then + echo "Invalid Debian package version: $version" + exit 1 + fi +} + package_deb() { require_cmd dpkg-deb require_cmd install @@ -304,6 +323,8 @@ package_deb() { require_cmd file require_cmd ldd require_cmd mktemp + require_cmd grep + require_cmd sh if [[ "$DO_BUILD" != true ]]; then prepare_build_gate @@ -325,6 +346,7 @@ package_deb() { echo "Unable to determine package version" exit 1 fi + validate_package_version "$version" local staging staging="$(mktemp -d)" @@ -429,6 +451,9 @@ exit 0 EOF chmod 0755 "$staging/DEBIAN/preinst" "$staging/DEBIAN/postinst" "$staging/DEBIAN/postrm" + sh -n "$staging/DEBIAN/preinst" + sh -n "$staging/DEBIAN/postinst" + sh -n "$staging/DEBIAN/postrm" mkdir -p "$DIST_DIR" local pkg_name @@ -436,9 +461,33 @@ EOF local pkg_path pkg_path="$DIST_DIR/$pkg_name" - dpkg-deb --build "$staging" "$pkg_path" + dpkg-deb --build --root-owner-group "$staging" "$pkg_path" dpkg-deb --info "$pkg_path" - dpkg-deb --contents "$pkg_path" | sort + local contents + contents="$(dpkg-deb --contents "$pkg_path")" + printf '%s\n' "$contents" | sort + + local actual_package actual_version actual_arch + actual_package="$(dpkg-deb -f "$pkg_path" Package)" + actual_version="$(dpkg-deb -f "$pkg_path" Version)" + actual_arch="$(dpkg-deb -f "$pkg_path" Architecture)" + [[ "$actual_package" == "awg2-harness" ]] || { echo "Unexpected package name: $actual_package"; exit 1; } + [[ "$actual_version" == "$version" ]] || { echo "Package version mismatch: expected $version, got $actual_version"; exit 1; } + [[ "$actual_arch" == "amd64" ]] || { echo "Unexpected package architecture: $actual_arch"; exit 1; } + + local required_path + for required_path in \ + ./usr/sbin/awg2-harness \ + ./usr/sbin/awg2-reboot-scheduler \ + ./lib/systemd/system/awg2-harness.service \ + ./lib/systemd/system/awg2-reboot-scheduler.service \ + ./usr/share/doc/awg2-harness/repo/config.example.toml \ + ./usr/share/doc/awg2-harness/repo/docs/UPSTREAM_COMPATIBILITY.md; do + grep -Fq "$required_path" <<<"$contents" || { + echo "Package is missing required path: $required_path" + exit 1 + } + done ( cd "$DIST_DIR" diff --git a/traffic_core/Cargo.toml b/traffic_core/Cargo.toml index 46a0686..0c25f56 100644 --- a/traffic_core/Cargo.toml +++ b/traffic_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awg2-harness" -version = "0.3.0" +version = "0.3.1" edition = "2024" [dependencies] diff --git a/traffic_core/src/config.rs b/traffic_core/src/config.rs index 2924932..2c545f8 100644 --- a/traffic_core/src/config.rs +++ b/traffic_core/src/config.rs @@ -881,12 +881,12 @@ mod tests { let raw = include_str!("../../config.example.toml"); let cfg = toml::from_str::(raw).expect("config.example.toml parse"); cfg.validate_config().expect("config.example.toml validate"); - assert_eq!(cfg.amnezia.wg_binaries, vec!["awg", "wg"]); - assert!( - cfg.amnezia - .container_names - .contains(&"amnezia-awg2".to_string()) + assert_eq!( + cfg.amnezia.container_names, + vec!["amnezia-awg2", "amnezia-awg"] ); + assert_eq!(cfg.amnezia.wg_binaries, vec!["awg", "wg"]); + assert_eq!(cfg.anti_torrent.max_connections_per_peer, 500); } #[test] diff --git a/traffic_core/src/wg.rs b/traffic_core/src/wg.rs index d80c951..5529796 100644 --- a/traffic_core/src/wg.rs +++ b/traffic_core/src/wg.rs @@ -29,10 +29,9 @@ pub struct PeerRuntime { pub transfer_tx_bytes: Option, } -/// Peer state is only readable inside the container's mount namespace: AmneziaWG is a -/// userspace implementation and its UAPI socket lives under `RUNSTATEDIR/amneziawg/` -/// in the container filesystem. A host-side `wg` entered into the container netns via -/// `nsenter -n` cannot reach that socket, so `docker exec` is the only valid transport. +/// Peer state is read with the official container-side tools. This is the stable access +/// point for both a kernel `amneziawg` interface and the `amneziawg-go` fallback, and it +/// avoids coupling the harness to the version or presence of host-side wg/awg tools. fn container_transport(target: &RuntimeTarget) -> DockerExecTransport { DockerExecTransport { container: target.container_name.clone(), @@ -175,8 +174,8 @@ fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result = AWG3_INTERFACE_LINE.split('\t').collect(); + fn rejects_awg30_interface_line() { + let cols: Vec<&str> = AWG30_INTERFACE_LINE.split('\t').collect(); assert_eq!(cols.len(), 28); assert!(!dump_line_is_peer(&cols)); assert!( - parse_wg_show_dump_runtime_peers(AWG3_INTERFACE_LINE, "0.0.0.0/0") + parse_wg_show_dump_runtime_peers(AWG30_INTERFACE_LINE, "0.0.0.0/0") .expect("parse") .is_empty() ); } + #[test] + fn rejects_both_awg31_interface_variants() { + for (raw, random_trailers, disable_cookies) in [ + (AWG31_INTERFACE_LINE, "on", "on"), + (AWG31_INTERFACE_LINE_DISABLED, "off", "off"), + ] { + let cols: Vec<&str> = raw.split('\t').collect(); + assert_eq!(cols.len(), 30); + assert_eq!(cols[27], random_trailers); + assert_eq!(cols[28], disable_cookies); + assert_eq!(cols[29], "off"); + assert!(!dump_line_is_peer(&cols)); + assert!( + parse_wg_show_dump_runtime_peers(raw, "0.0.0.0/0") + .expect("parse") + .is_empty() + ); + } + } + + #[test] + fn awg31_interface_and_peer_yield_exactly_one_peer() { + let raw = format!("{AWG31_INTERFACE_LINE}\n{PEER_LINE}"); + let peers = parse_wg_show_dump_runtime_peers(&raw, "10.8.1.0/24").expect("parse"); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].public_key, "pubkey1"); + } + #[test] fn rejects_awg2_interface_line() { let cols: Vec<&str> = AWG2_INTERFACE_LINE.split('\t').collect(); @@ -464,8 +499,8 @@ mod tests { } #[test] - fn rejects_awg3_interface_line_without_header_protection() { - let cols: Vec<&str> = AWG3_INTERFACE_LINE_NO_HEADER_PROTECTION + fn rejects_awg30_interface_line_without_header_protection() { + let cols: Vec<&str> = AWG30_INTERFACE_LINE_NO_HEADER_PROTECTION .split('\t') .collect(); assert!(!dump_line_is_peer(&cols)); @@ -475,14 +510,14 @@ mod tests { /// read through AWG 3.0 tools still prints the wide interface row. Peers must parse /// and the interface row must be dropped, exactly as with a native AWG 3.0 tunnel. #[test] - fn awg2_configuration_read_through_awg3_tools() { - let raw = format!("{AWG2_CONFIG_VIA_AWG3_TOOLS_INTERFACE_LINE}\n{PEER_LINE}"); - let cols: Vec<&str> = AWG2_CONFIG_VIA_AWG3_TOOLS_INTERFACE_LINE + fn awg2_configuration_read_through_awg30_tools() { + let raw = format!("{AWG2_CONFIG_VIA_AWG30_TOOLS_INTERFACE_LINE}\n{PEER_LINE}"); + let cols: Vec<&str> = AWG2_CONFIG_VIA_AWG30_TOOLS_INTERFACE_LINE .split('\t') .collect(); assert_eq!( cols.len(), - AWG3_INTERFACE_LINE.split('\t').count(), + AWG30_INTERFACE_LINE.split('\t').count(), "an AWG 2.0 config through AWG 3.0 tools has the same width as AWG 3.0" ); assert!(!dump_line_is_peer(&cols)); @@ -496,7 +531,7 @@ mod tests { #[test] fn parses_multiple_interfaces_in_one_dump() { let raw = format!( - "{AWG3_INTERFACE_LINE}\n{PEER_LINE}\n{AWG2_INTERFACE_LINE}\nwg0\tpubkey2\t(none)\t5.6.7.8:51820\t10.8.1.20/32\t1715000001\t7\t8\t0" + "{AWG31_INTERFACE_LINE}\n{PEER_LINE}\n{AWG30_INTERFACE_LINE}\n{AWG2_INTERFACE_LINE}\n{WG_INTERFACE_LINE}\nwg0\tpubkey2\t(none)\t5.6.7.8:51820\t10.8.1.20/32\t1715000001\t7\t8\t0" ); let peers = parse_wg_show_dump_runtime_peers(&raw, "10.8.1.0/24").expect("parse"); assert_eq!(peers.len(), 2); @@ -506,9 +541,10 @@ mod tests { #[test] fn interface_without_peers_yields_no_peers() { - let peers = - parse_wg_show_dump_runtime_peers(AWG3_INTERFACE_LINE, "10.8.1.0/24").expect("parse"); - assert!(peers.is_empty()); + for raw in [AWG31_INTERFACE_LINE, AWG31_INTERFACE_LINE_DISABLED] { + let peers = parse_wg_show_dump_runtime_peers(raw, "10.8.1.0/24").expect("parse"); + assert!(peers.is_empty()); + } } /// Truncated or garbled rows must be skipped, not partially interpreted. @@ -536,7 +572,7 @@ mod tests { #[test] fn never_exposes_interface_private_key_as_peer() { let raw = format!( - "{AWG3_INTERFACE_LINE}\n{AWG2_INTERFACE_LINE}\n{WG_INTERFACE_LINE}\nawg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0" + "{AWG31_INTERFACE_LINE}\n{AWG31_INTERFACE_LINE_DISABLED}\n{AWG30_INTERFACE_LINE}\n{AWG2_INTERFACE_LINE}\n{WG_INTERFACE_LINE}\nawg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0" ); let peers = parse_wg_show_dump_runtime_peers(&raw, "0.0.0.0/0").expect("parse"); assert_eq!(peers.len(), 1);