6b3e5d1b2f
The rollback claimed an invariant in its own comment - restore the original, confirm it,
and only then release staging - and did not enforce it. original_back was computed and then
ignored: the staging swapoff ran unconditionally.
The failure path that mattered: staging is active, the original is already off, reactivating
the original fails (swapon can fail under the memory pressure that made the resize
necessary), and the rollback then switched off the staging swap - the last thing carrying
load. Recovering from a failed resize left the host with no swap at all, which is worse
than the failure being recovered from.
original_back now gates the release. If the original cannot be brought back the rollback
returns early with the staging swap still active, and the error carries the two commands
that finish the job by hand. An extra temporary swap is always the better end state than
none. The staging file is also only unlinked once swapoff has actually succeeded - an
active swapfile cannot be removed, and attempting it produced an error that explained
nothing.
Asserting this needs a seam, because the invariant is a sequence of calls rather than a
value: a small SwapCommands trait covers swapon/swapoff, with the real implementation
behind it. Three tests: the original cannot be restored and swapoff is never called with
the message naming the preserved fallback; the original comes back and staging is released
in that order; nothing was taken down so nothing is touched.
Also from the same audit:
- StaleOwnership advice was wrong. It suggested deleting the ownership record, which turns
the file Foreign - refused just as firmly. The message now distinguishes the two cases: a
leftover .previous next to the swapfile is the signature of a replacement killed between
installing the new file and recording its identity, so the error says nothing is lost,
points at the intact previous swapfile, and gives the exact commands for going back or
keeping the new file. Full automatic crash recovery is deliberately not added here - it
needs a transition journal, and that belongs with the Debian acceptance work.
- The acceptance checklist still described the PID-file lock ("re-acquires by the absence
of /proc/<pid>"), which stopped being true when locking moved to flock. The kernel
releases it on process exit, and the leftover file means nothing.
- shellcheck was installed by --prepare-build-env but never run. It now runs in
prepare_build_gate: the script installs systemd units and writes to /usr/sbin, so it is
part of the release surface.
- The skip message for systemd-analyze verify now says what the check actually is - a
post-install gate, expected to be skipped on a clean builder - rather than reading like a
build gate that silently did not run.
One audit item did not reproduce: there is no duplicated -j in antitorrent_drop_counter,
the flag appears once.
Version raised to 0.3.0 in traffic_core/Cargo.toml, with the packaging examples in README
and INSTALLATION updated to match. The reboot scheduler keeps its own 0.2.0.
cargo fmt --check, clippy -D warnings, cargo test --locked --workspace --release: 191 + 4
passed. build.sh not syntax-checked here - no bash on this host; the shellcheck gate now
covers it on the builder.
333 lines
12 KiB
Markdown
333 lines
12 KiB
Markdown
# Установка
|
||
|
||
Этот документ описывает production-порядок установки AWG2 Harness. Подробный runtime-план с SSH hardening и Amnezia preflight находится в корневом [`README.md`](../README.md).
|
||
|
||
## Контракт развёртывания
|
||
|
||
- Целевой production-хост: Debian 13 amd64.
|
||
- Amnezia ставится до AWG2 Harness официальным self-hosted способом через приложение AmneziaVPN.
|
||
- Проект рассчитан на AmneziaWG/AWG 2.0 и compatible legacy-сценарий AmneziaWG/AWG 1.5.
|
||
- 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.
|
||
|
||
## A. Сборочная машина
|
||
|
||
### A1. Подготовить сборочное окружение
|
||
|
||
```bash
|
||
cd awg2_harness
|
||
chmod +x tools/build.sh
|
||
./tools/build.sh --prepare-build-env --bootstrap-rust --yes
|
||
```
|
||
|
||
### A2. Проверить код
|
||
|
||
```bash
|
||
cargo fmt --manifest-path Cargo.toml --all --check
|
||
cargo clippy --locked --workspace --all-targets --manifest-path Cargo.toml -- -D warnings
|
||
cargo test --locked --workspace --release --manifest-path Cargo.toml
|
||
```
|
||
|
||
То же самое выполняет build helper:
|
||
|
||
```bash
|
||
./tools/build.sh --build
|
||
```
|
||
|
||
### A3. Собрать Debian-пакет
|
||
|
||
```bash
|
||
./tools/build.sh --package --version 0.3.0
|
||
(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
|
||
```
|
||
|
||
`--version` можно опустить, если версия в `traffic_core/Cargo.toml` уже равна release version. Version override влияет только на metadata `.deb`.
|
||
|
||
## B. Подготовить production-хост
|
||
|
||
### B1. SSH
|
||
|
||
Сначала сгенерируйте отдельную пару ключей для Amnezia на Windows (RSA PEM):
|
||
|
||
```cmd
|
||
ssh-keygen -t rsa -b 4096 -m PEM -f %USERPROFILE%\.ssh\amnezia_key
|
||
```
|
||
|
||
Дальше разделяйте ключи строго:
|
||
|
||
- на сервер в `/root/.ssh/authorized_keys` кладётся содержимое `%USERPROFILE%\.ssh\amnezia_key.pub`;
|
||
- в AmneziaVPN в поле `Password or SSH private key` вставляется содержимое `%USERPROFILE%\.ssh\amnezia_key` (без `.pub`).
|
||
|
||
`.pub` нельзя вставлять в поле private key в AmneziaVPN.
|
||
|
||
Установка `.pub` на сервер:
|
||
|
||
```bash
|
||
install -d -m 0700 /root/.ssh
|
||
nano /root/.ssh/authorized_keys
|
||
chmod 0600 /root/.ssh/authorized_keys
|
||
```
|
||
|
||
Production-пример:
|
||
|
||
```bash
|
||
install -d -m 0755 /etc/ssh/sshd_config.d
|
||
cat >/etc/ssh/sshd_config.d/99-awg2-production.conf <<'EOF'
|
||
Port 2323
|
||
PermitRootLogin prohibit-password
|
||
PasswordAuthentication no
|
||
KbdInteractiveAuthentication no
|
||
PubkeyAuthentication yes
|
||
EOF
|
||
sshd -t
|
||
systemctl reload ssh 2>/dev/null || systemctl reload sshd
|
||
ss -ltnp | grep ':2323'
|
||
```
|
||
|
||
Обязательно проверьте вход с нового терминала:
|
||
|
||
```cmd
|
||
ssh -i %USERPROFILE%\.ssh\amnezia_key -p 2323 root@<server-ip>
|
||
```
|
||
|
||
Проверить содержимое ключей в Windows:
|
||
|
||
```cmd
|
||
type %USERPROFILE%\.ssh\amnezia_key.pub
|
||
type %USERPROFILE%\.ssh\amnezia_key
|
||
```
|
||
|
||
### B2. Amnezia
|
||
|
||
Установите AmneziaWG/AWG через официальное приложение AmneziaVPN в режиме self-hosted server.
|
||
|
||
Заполнение полей в интерфейсе AmneziaVPN:
|
||
|
||
```text
|
||
Configure your server:
|
||
|
||
Server IP address [:port]:
|
||
<server-ip>:2323
|
||
|
||
SSH Username:
|
||
root
|
||
|
||
Password or SSH private key:
|
||
полное содержимое приватного ключа %USERPROFILE%\.ssh\amnezia_key
|
||
```
|
||
|
||
Важно: в это поле не вставляется `.pub`. Файл `.pub` нужен только для `/root/.ssh/authorized_keys` на сервере.
|
||
|
||
Проверка после установки:
|
||
|
||
```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)"
|
||
echo "container=$C"
|
||
docker port "$C"
|
||
docker exec "$C" awg show
|
||
```
|
||
|
||
Если контейнер называется иначе или интерфейс не `awg0`, отразите это в `/etc/awg2-harness/config.toml`.
|
||
|
||
`awg` — реальный бинарь в официальном образе AmneziaWG; `wg` внутри контейнера является symlink на него и тоже сработает. На хосте `wireguard-tools` устанавливать не нужно: обвязка читает peer state только через `docker exec`.
|
||
|
||
### B3. Установить AmneziaDNS
|
||
|
||
Установите AmneziaDNS из desktop-приложения AmneziaVPN:
|
||
|
||
```text
|
||
Servers → <server> → Services → AmneziaDNS → Install
|
||
```
|
||
|
||
Затем включите его для подключения:
|
||
|
||
```text
|
||
Settings → Connection → DNS servers → Use AmneziaDNS
|
||
```
|
||
|
||
Ожидаемое состояние Docker:
|
||
|
||
```bash
|
||
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
|
||
```
|
||
|
||
Ожидаемая строка для DNS-контейнера:
|
||
|
||
```text
|
||
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)"
|
||
echo "AWG=$AWG"
|
||
|
||
docker inspect -f 'ports={{json .NetworkSettings.Ports}}' amnezia-dns
|
||
docker inspect -f 'networks={{range $n,$v := .NetworkSettings.Networks}}{{printf "%s=%s " $n $v.IPAddress}}{{end}}' amnezia-dns
|
||
docker inspect -f 'networks={{range $n,$v := .NetworkSettings.Networks}}{{printf "%s=%s " $n $v.IPAddress}}{{end}}' "$AWG"
|
||
```
|
||
|
||
Ожидаемый результат по портам:
|
||
|
||
```text
|
||
ports={"53/tcp":null,"53/udp":null}
|
||
```
|
||
|
||
Это означает, что DNS не опубликован на публичном host-интерфейсе и остаётся доступным только через VPN/Docker path.
|
||
|
||
Negative-check публичного DNS:
|
||
|
||
```bash
|
||
nslookup example.com <server-public-ip>
|
||
```
|
||
|
||
Ожидаемое поведение: публичный DNS-запрос **не должен успешно резолвиться**. Типичные безопасные варианты ответа: `connection refused`, `timeout` или `no servers could be reached`.
|
||
|
||
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)"
|
||
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")"
|
||
|
||
echo "AWG=$AWG"
|
||
echo "DNS_IP=$DNS_IP"
|
||
nsenter -t "$DNS_PID" -n ss -lntup
|
||
nsenter -t "$AWG_PID" -n ip route
|
||
nsenter -t "$AWG_PID" -n dig @"$DNS_IP" example.com A +short
|
||
```
|
||
|
||
## C. Передача артефактов
|
||
|
||
Ниже два production-сценария установки.
|
||
|
||
### C1. Вариант A: конфиг передаётся вместе с `.deb`
|
||
|
||
```bash
|
||
scp -P 2323 dist/awg2-harness_*_amd64.deb root@<server-ip>:/root/
|
||
scp -P 2323 dist/SHA256SUMS root@<server-ip>:/root/
|
||
scp -P 2323 config.example.toml root@<server-ip>:/root/awg2-harness.config.toml
|
||
```
|
||
|
||
Перед установкой отредактируйте `/root/awg2-harness.config.toml`. Минимально проверьте:
|
||
|
||
- `[host_firewall].ssh_port` совпадает с реальным SSH-портом (`2323` в production-примере);
|
||
- `[host_firewall].amnezia_port` совпадает с published UDP port Amnezia;
|
||
- `[amnezia].peer_ipv4_cidr` совпадает с подсетью clients;
|
||
- `[anti_torrent].peer_ipv4_cidr` обычно равен `[amnezia].peer_ipv4_cidr`;
|
||
- `peer_limit` и `root_rate` отражают нужную traffic policy.
|
||
|
||
### C2. Вариант B (основной): конфиг создаётся на production после установки
|
||
|
||
```bash
|
||
scp -P 2323 dist/awg2-harness_*_amd64.deb root@<server-ip>:/root/
|
||
scp -P 2323 dist/SHA256SUMS root@<server-ip>:/root/
|
||
```
|
||
|
||
## D. Установка на production
|
||
|
||
### D1. Вариант A
|
||
|
||
```bash
|
||
cd /root
|
||
sha256sum -c SHA256SUMS
|
||
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
|
||
systemctl daemon-reload
|
||
```
|
||
|
||
### D2. Вариант B (основной)
|
||
|
||
```bash
|
||
cd /root
|
||
sha256sum -c SHA256SUMS
|
||
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
|
||
nano /etc/awg2-harness/config.toml
|
||
systemctl daemon-reload
|
||
```
|
||
|
||
Перед первым `apply-firewall` обязательно перепроверьте:
|
||
|
||
- `[host_firewall].ssh_port` совпадает с реально слушающим `sshd` портом;
|
||
- `[host_firewall].amnezia_port` совпадает с published Docker UDP port;
|
||
- `[amnezia].peer_ipv4_cidr` совпадает с peer-подсетью;
|
||
- `[anti_torrent].peer_ipv4_cidr` обычно тот же CIDR.
|
||
|
||
## E. Preflight до включения сервисов
|
||
|
||
```bash
|
||
awg2-harness --config /etc/awg2-harness/config.toml validate-config
|
||
awg2-harness --config /etc/awg2-harness/config.toml validate-runtime
|
||
awg2-harness --config /etc/awg2-harness/config.toml ensure-swap
|
||
awg2-harness --config /etc/awg2-harness/config.toml apply-firewall
|
||
awg2-harness --config /etc/awg2-harness/config.toml apply-limits
|
||
awg2-harness --config /etc/awg2-harness/config.toml status
|
||
```
|
||
|
||
Ожидаемый здоровый результат:
|
||
|
||
```text
|
||
Traffic actual:
|
||
drift: no
|
||
Host firewall:
|
||
drift: no
|
||
Anti-torrent:
|
||
drift: no
|
||
Swap:
|
||
drift: no
|
||
```
|
||
|
||
Если peers ещё нет, `apply-limits` напечатает `no peers found, skip tc apply`. Для пустого сервера это здоровое состояние.
|
||
|
||
При `swap.enabled = true` AWG2 Harness намеренно делает swap эксклюзивным: один управляемый swapfile, одна swap-запись в `/etc/fstab`, заданный `swappiness`.
|
||
|
||
## F. Включение сервисов
|
||
|
||
```bash
|
||
systemctl enable --now awg2-harness.service
|
||
systemctl enable --now awg2-reboot-scheduler.service
|
||
|
||
systemctl status awg2-harness.service --no-pager -l
|
||
systemctl status awg2-reboot-scheduler.service --no-pager -l
|
||
journalctl -u awg2-harness.service --since '5 minutes ago' --no-pager
|
||
journalctl -u awg2-reboot-scheduler.service --since '5 minutes ago' --no-pager
|
||
```
|
||
|
||
Через 30–60 секунд:
|
||
|
||
```bash
|
||
awg2-harness --config /etc/awg2-harness/config.toml status
|
||
```
|
||
|
||
## G. Проверка перезагрузкой
|
||
|
||
```bash
|
||
reboot
|
||
```
|
||
|
||
После восстановления подключения:
|
||
|
||
```bash
|
||
systemctl status docker --no-pager -l
|
||
systemctl status awg2-harness.service --no-pager -l
|
||
systemctl status awg2-reboot-scheduler.service --no-pager -l
|
||
awg2-harness --config /etc/awg2-harness/config.toml status
|
||
```
|
||
|
||
## H. Ручная установка из дерева исходников
|
||
|
||
`./tools/build.sh --install` и `./tools/build.sh --enable` существуют для staging и отладки. Это не рекомендуемый production-путь: на production используйте `.deb`.
|