Files
AWG2_harness/tools/build.sh
T
founder 6b3e5d1b2f fix(swap): never release the fallback swap during a failed rollback; release 0.3.0 [P0]
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.
2026-08-07 05:23:42 +05:00

492 lines
14 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKSPACE_MANIFEST="$ROOT_DIR/Cargo.toml"
CRATE_MANIFEST="$ROOT_DIR/traffic_core/Cargo.toml"
TARGET_BIN="$ROOT_DIR/target/release/awg2-harness"
REBOOT_BIN="$ROOT_DIR/target/release/awg2-reboot-scheduler"
RUST_TOOLCHAIN_FILE="$ROOT_DIR/rust-toolchain.toml"
RUST_TOOLCHAIN_VERSION="1.95.0"
PREPARE_BUILD_ENV=false
PREPARE_RUNTIME_ENV=false
BOOTSTRAP_RUST=false
DO_BUILD=false
PACKAGE=false
INSTALL=false
ENABLE=false
YES=false
SKIP_FMT=false
HAS_ACTION=false
DIST_DIR="$ROOT_DIR/dist"
PACKAGE_VERSION=""
RUN_AS_ROOT=()
BUILD_PACKAGES=(
ca-certificates
curl
build-essential
pkg-config
file
shellcheck
dpkg-dev
)
RUNTIME_PACKAGES=(
iproute2
nftables
kmod
util-linux
procps
systemd
)
usage() {
cat <<'EOF'
Usage:
./tools/build.sh
./tools/build.sh --prepare-build-env --yes
./tools/build.sh --prepare-runtime-env --yes
./tools/build.sh --bootstrap-rust --yes
./tools/build.sh --build
./tools/build.sh --package
./tools/build.sh --build --package
./tools/build.sh --install
./tools/build.sh --enable
Flags:
--prepare-build-env Install missing build dependencies via apt
--prepare-runtime-env Install missing runtime dependencies via apt
--bootstrap-rust Install rustup/toolchain pinned by this repo
--build Run build gate (default when no action flags)
--package Build Debian package into dist/ (runs build gate by default)
--dist-dir <path> Output directory for package artifacts (default: ./dist)
--version <version> Override package version (default: read from Cargo.toml)
--install Install binary and systemd unit (no enable/start)
--enable Enable and start awg2-harness.service
--yes Confirm system mutations (apt/rustup)
--skip-fmt Skip cargo fmt --check
--help Show this help
Notes:
--install is not the production deployment path; it is for manual source-tree installs (staging/debug).
Use generated .deb packages on production hosts: apt install ./awg2-harness_<version>_amd64.deb
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--prepare-build-env) PREPARE_BUILD_ENV=true; HAS_ACTION=true ;;
--prepare-runtime-env) PREPARE_RUNTIME_ENV=true; HAS_ACTION=true ;;
--bootstrap-rust) BOOTSTRAP_RUST=true; HAS_ACTION=true ;;
--build) DO_BUILD=true; HAS_ACTION=true ;;
--package) PACKAGE=true; HAS_ACTION=true ;;
--dist-dir)
[[ $# -ge 2 ]] || { echo "--dist-dir requires value"; exit 1; }
DIST_DIR="$2"
shift
;;
--version)
[[ $# -ge 2 ]] || { echo "--version requires value"; exit 1; }
PACKAGE_VERSION="$2"
shift
;;
--install) INSTALL=true; HAS_ACTION=true ;;
--enable) ENABLE=true; HAS_ACTION=true ;;
--yes) YES=true ;;
--skip-fmt) SKIP_FMT=true ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown arg: $1"; usage; exit 1 ;;
esac
shift
done
if [[ "$HAS_ACTION" == false ]]; then
DO_BUILD=true
fi
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing command: $1"
exit 1
}
}
setup_root_runner() {
if [[ "$(id -u)" -eq 0 ]]; then
RUN_AS_ROOT=()
else
require_cmd sudo
RUN_AS_ROOT=(sudo)
fi
}
ensure_supported_host() {
if [[ ! -f /etc/os-release ]]; then
echo "Unsupported OS: missing /etc/os-release. Only Debian 13 amd64 is supported."
exit 1
fi
# shellcheck disable=SC1091
source /etc/os-release
if [[ "${ID:-}" != "debian" || "${VERSION_ID:-}" != "13" ]]; then
echo "Unsupported OS: ${PRETTY_NAME:-unknown}. Only Debian 13 amd64 is supported."
exit 1
fi
local arch
arch="$(dpkg --print-architecture 2>/dev/null || uname -m)"
if [[ "$arch" != "amd64" && "$arch" != "x86_64" ]]; then
echo "Unsupported arch: $arch. Only amd64 is supported."
exit 1
fi
}
apt_install_missing() {
local packages=("$@")
local missing=()
require_cmd dpkg-query
require_cmd apt-get
for pkg in "${packages[@]}"; do
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
missing+=("$pkg")
fi
done
if [[ "${#missing[@]}" -eq 0 ]]; then
echo "All required packages already installed"
return 0
fi
echo "Missing packages: ${missing[*]}"
if [[ "$YES" != true ]]; then
echo "Re-run with --yes to install them"
exit 1
fi
setup_root_runner
"${RUN_AS_ROOT[@]}" apt-get update
"${RUN_AS_ROOT[@]}" apt-get install -y --no-install-recommends "${missing[@]}"
}
bootstrap_rust() {
require_cmd curl
if [[ "$YES" != true ]]; then
echo "This will install rustup and Rust toolchain $RUST_TOOLCHAIN_VERSION"
echo "Re-run with --yes to proceed"
exit 1
fi
if ! command -v rustup >/dev/null 2>&1; then
local rustup_script
rustup_script="$(mktemp)"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o "$rustup_script"
sh "$rustup_script" -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN_VERSION"
rm -f "$rustup_script"
fi
export PATH="$HOME/.cargo/bin:$PATH"
require_cmd rustup
rustup toolchain install "$RUST_TOOLCHAIN_VERSION" --profile minimal --component rustfmt --component clippy
rustup component add rustfmt clippy --toolchain "$RUST_TOOLCHAIN_VERSION"
rustc --version
}
prepare_build_gate() {
require_cmd cargo
require_cmd rustc
require_cmd gcc
require_cmd pkg-config
require_cmd file
require_cmd ldd
require_cmd shellcheck
# This script installs systemd units and writes to /usr/sbin, so it is part of the
# release surface and is linted like the Rust code is.
shellcheck "$ROOT_DIR/tools/build.sh"
if [[ ! -f "$RUST_TOOLCHAIN_FILE" ]]; then
echo "Missing rust toolchain pin file: $RUST_TOOLCHAIN_FILE"
exit 1
fi
if [[ ! -f "$ROOT_DIR/Cargo.lock" ]]; then
echo "Missing lockfile: $ROOT_DIR/Cargo.lock"
exit 1
fi
if [[ "$SKIP_FMT" != true ]]; then
cargo fmt --manifest-path "$WORKSPACE_MANIFEST" --all --check
fi
if [[ ! -f "$WORKSPACE_MANIFEST" ]]; then
echo "Missing workspace manifest: $WORKSPACE_MANIFEST"
exit 1
fi
cargo clippy --locked --workspace --all-targets --manifest-path "$WORKSPACE_MANIFEST" -- -D warnings
cargo test --locked --workspace --release --manifest-path "$WORKSPACE_MANIFEST"
cargo build --locked --workspace --release --manifest-path "$WORKSPACE_MANIFEST"
cargo metadata --locked --format-version 1 --manifest-path "$WORKSPACE_MANIFEST" >/dev/null
if command -v systemd-analyze >/dev/null 2>&1; then
local docker_unit_present=false
if [[ -e /lib/systemd/system/docker.service || -e /usr/lib/systemd/system/docker.service || -e /etc/systemd/system/docker.service ]]; then
docker_unit_present=true
fi
if [[ -x /usr/sbin/awg2-harness && -x /usr/sbin/awg2-reboot-scheduler && "$docker_unit_present" == true ]]; then
systemd-analyze verify "$ROOT_DIR/awg2-harness.service"
systemd-analyze verify "$ROOT_DIR/awg2-reboot-scheduler.service"
else
echo "Skipping source-tree systemd-analyze verify: package runtime files or docker.service are not present on this build host."; echo "This check is a post-install gate, not a build gate: on a clean builder it is expected to be skipped, and the units are verified after --install or on the target host."
fi
fi
file "$TARGET_BIN"
file "$REBOOT_BIN"
ldd "$TARGET_BIN" || true
ldd "$REBOOT_BIN" || true
"$TARGET_BIN" --help >/dev/null
"$REBOOT_BIN" --help >/dev/null
sha256sum "$TARGET_BIN"
sha256sum "$REBOOT_BIN"
ls -lh "$TARGET_BIN"
ls -lh "$REBOOT_BIN"
}
install_artifacts() {
require_cmd install
require_cmd systemctl
if [[ ! -f "$TARGET_BIN" || ! -f "$REBOOT_BIN" ]]; then
echo "Binary not found: $TARGET_BIN or $REBOOT_BIN"
echo "Run build first: ./tools/build.sh"
exit 1
fi
if [[ ! -f /etc/awg2-harness/config.toml ]]; then
echo "Missing /etc/awg2-harness/config.toml"
echo "Refusing to install example config as production config"
echo "Create config manually from $ROOT_DIR/config.example.toml and re-run with --install"
exit 1
fi
setup_root_runner
"${RUN_AS_ROOT[@]}" install -d -m 0755 /usr/sbin
"${RUN_AS_ROOT[@]}" install -m 0755 "$TARGET_BIN" /usr/sbin/awg2-harness
"${RUN_AS_ROOT[@]}" install -m 0755 "$REBOOT_BIN" /usr/sbin/awg2-reboot-scheduler
"${RUN_AS_ROOT[@]}" install -d -m 0755 /lib/systemd/system
"${RUN_AS_ROOT[@]}" install -m 0644 "$ROOT_DIR/awg2-harness.service" /lib/systemd/system/awg2-harness.service
"${RUN_AS_ROOT[@]}" install -m 0644 "$ROOT_DIR/awg2-reboot-scheduler.service" /lib/systemd/system/awg2-reboot-scheduler.service
if command -v systemd-analyze >/dev/null 2>&1; then
"${RUN_AS_ROOT[@]}" systemd-analyze verify /lib/systemd/system/awg2-harness.service
"${RUN_AS_ROOT[@]}" systemd-analyze verify /lib/systemd/system/awg2-reboot-scheduler.service
fi
"${RUN_AS_ROOT[@]}" systemctl daemon-reload
}
read_package_version_from_manifest() {
awk -F '"' '/^[[:space:]]*version[[:space:]]*=[[:space:]]*"/ { print $2; exit }' "$CRATE_MANIFEST"
}
package_deb() {
require_cmd dpkg-deb
require_cmd install
require_cmd sha256sum
require_cmd file
require_cmd ldd
require_cmd mktemp
if [[ "$DO_BUILD" != true ]]; then
prepare_build_gate
fi
if [[ ! -f "$TARGET_BIN" || ! -f "$REBOOT_BIN" ]]; then
echo "Binary not found after build: $TARGET_BIN or $REBOOT_BIN"
exit 1
fi
local version
if [[ -n "$PACKAGE_VERSION" ]]; then
version="$PACKAGE_VERSION"
else
version="$(read_package_version_from_manifest)"
fi
if [[ -z "$version" ]]; then
echo "Unable to determine package version"
exit 1
fi
local staging
staging="$(mktemp -d)"
trap 'rm -rf "$staging"' RETURN
install -d -m 0755 "$staging/DEBIAN"
install -d -m 0755 "$staging/usr/sbin"
install -d -m 0755 "$staging/lib/systemd/system"
install -d -m 0755 "$staging/etc/awg2-harness"
install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo"
install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo/docs"
install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo/tools"
install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo/reboot_module"
install -m 0755 "$TARGET_BIN" "$staging/usr/sbin/awg2-harness"
install -m 0755 "$REBOOT_BIN" "$staging/usr/sbin/awg2-reboot-scheduler"
install -m 0644 "$ROOT_DIR/awg2-harness.service" "$staging/lib/systemd/system/awg2-harness.service"
install -m 0644 "$ROOT_DIR/awg2-reboot-scheduler.service" "$staging/lib/systemd/system/awg2-reboot-scheduler.service"
install -m 0644 "$ROOT_DIR/config.example.toml" "$staging/usr/share/doc/awg2-harness/repo/config.example.toml"
install -m 0644 "$ROOT_DIR/README.md" "$staging/usr/share/doc/awg2-harness/repo/README.md"
local doc
for doc in "$ROOT_DIR"/docs/*.md; do
install -m 0644 "$doc" "$staging/usr/share/doc/awg2-harness/repo/docs/$(basename "$doc")"
done
install -m 0644 "$ROOT_DIR/tools/README.md" "$staging/usr/share/doc/awg2-harness/repo/tools/README.md"
install -m 0644 "$ROOT_DIR/reboot_module/README.md" "$staging/usr/share/doc/awg2-harness/repo/reboot_module/README.md"
cat >"$staging/DEBIAN/control" <<EOF
Package: awg2-harness
Version: $version
Section: net
Priority: optional
Architecture: amd64
Maintainer: AWG2 Harness
Depends: libc6, iproute2, nftables, kmod, util-linux, procps, systemd
Description: Host-side orchestrator for AmneziaWG traffic shaping and lifecycle recovery
AWG2 Harness manages AmneziaWG traffic shaping, IFB ingress shaping,
nftables anti-torrent guard, reboot scheduling, swap setup and daemonized
drift recovery on dedicated Debian hosts. Includes external reboot scheduler.
EOF
cat >"$staging/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload || true
fi
echo "AWG2 Harness installed."
echo "Runtime config is not created automatically by design."
echo "Create /etc/awg2-harness/config.toml manually before enabling services:"
echo " install -d -m 0755 /etc/awg2-harness"
echo " cp /usr/share/doc/awg2-harness/repo/config.example.toml /etc/awg2-harness/config.toml"
echo " nano /etc/awg2-harness/config.toml"
echo "Then run preflight before enabling services:"
echo " awg2-harness --config /etc/awg2-harness/config.toml validate-config"
echo " awg2-harness --config /etc/awg2-harness/config.toml validate-runtime"
echo " awg2-harness --config /etc/awg2-harness/config.toml status"
echo "Enable services only after SSH, Amnezia port and peer CIDR are verified."
echo " systemctl enable --now awg2-harness.service awg2-reboot-scheduler.service"
exit 0
EOF
cat >"$staging/DEBIAN/preinst" <<'EOF'
#!/bin/sh
set -e
if [ ! -r /etc/os-release ]; then
echo "awg2-harness supports only Debian 13 amd64" >&2
exit 1
fi
. /etc/os-release
if [ "${ID:-}" != "debian" ] || [ "${VERSION_ID:-}" != "13" ]; then
echo "awg2-harness supports only Debian 13 amd64; got ${PRETTY_NAME:-unknown}" >&2
exit 1
fi
if command -v dpkg >/dev/null 2>&1; then
arch="$(dpkg --print-architecture)"
if [ "$arch" != "amd64" ]; then
echo "awg2-harness supports only amd64; got $arch" >&2
exit 1
fi
fi
exit 0
EOF
cat >"$staging/DEBIAN/postrm" <<'EOF'
#!/bin/sh
set -e
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload || true
fi
exit 0
EOF
chmod 0755 "$staging/DEBIAN/preinst" "$staging/DEBIAN/postinst" "$staging/DEBIAN/postrm"
mkdir -p "$DIST_DIR"
local pkg_name
pkg_name="awg2-harness_${version}_amd64.deb"
local pkg_path
pkg_path="$DIST_DIR/$pkg_name"
dpkg-deb --build "$staging" "$pkg_path"
dpkg-deb --info "$pkg_path"
dpkg-deb --contents "$pkg_path" | sort
(
cd "$DIST_DIR"
sha256sum "$pkg_name" > SHA256SUMS
)
echo "Package created: $pkg_path"
echo "Checksums: $DIST_DIR/SHA256SUMS"
}
enable_service() {
require_cmd systemctl
setup_root_runner
"${RUN_AS_ROOT[@]}" systemctl enable --now awg2-harness.service
"${RUN_AS_ROOT[@]}" systemctl enable --now awg2-reboot-scheduler.service
}
ensure_supported_host
if [[ "$PREPARE_BUILD_ENV" == true ]]; then
apt_install_missing "${BUILD_PACKAGES[@]}"
fi
if [[ "$PREPARE_RUNTIME_ENV" == true ]]; then
apt_install_missing "${RUNTIME_PACKAGES[@]}"
fi
if [[ "$BOOTSTRAP_RUST" == true ]]; then
bootstrap_rust
fi
if [[ "$DO_BUILD" == true ]]; then
prepare_build_gate
fi
if [[ "$PACKAGE" == true ]]; then
package_deb
fi
if [[ "$INSTALL" == true ]]; then
install_artifacts
fi
if [[ "$ENABLE" == true ]]; then
enable_service
fi
if [[ "$DO_BUILD" == true ]]; then
echo "Build completed: $TARGET_BIN"
fi