Add OpenWrt package build and QEMU test scripts

Add OpenWrt SDK packaging under src/Build for console-only x86/64 builds. The build helper prepares the SDK, renders a local package recipe, builds VeraCrypt with the OpenWrt musl toolchain, uses wxWidgets 3.2.10 as static wxBase, enables FUSE3, and skips release self-tests during cross compilation.

Add a package template that installs the console binary, mount.veracrypt, and license files only. The package declares bash for mount.veracrypt and keeps runtime dependencies focused on the direct userland requirements.

Add a documented QEMU runtime test path that boots the matching OpenWrt image, installs the locally built package set with opkg, runs the VeraCrypt version and algorithm self-tests, and exercises a small filesystem=none container mount/unmount flow.

Allow wxbuild callers to pass WX_CONFIGURE_EXTRA_FLAGS so OpenWrt cross configure flags can be passed into the wxWidgets build without carrying an OpenWrt-specific source patch.
This commit is contained in:
Mounir IDRASSI
2026-05-25 17:41:07 +09:00
parent 3c771c07fc
commit 0190270f9d
7 changed files with 1554 additions and 1 deletions

4
.gitignore vendored
View File

@@ -4,6 +4,10 @@
# CLion
.idea/
# Python build/test artifacts
__pycache__/
*.py[cod]
# VC Linux build artifacts
*.o
*.o0

View File

@@ -0,0 +1,163 @@
# OpenWrt packaging
This directory contains the canonical OpenWrt package template and local test
configuration for building VeraCrypt console-only with the OpenWrt SDK.
It is a maintainer build-and-test harness for the VeraCrypt working tree, not
an OpenWrt packages-feed submission recipe.
The current supported target is `x86/64`, matching the QEMU runtime smoke test.
The build uses:
- OpenWrt 24.10.6 x86/64 SDK by default
- musl through the OpenWrt toolchain
- `NOGUI=1`
- `WITHFUSE3=1`
- `WXSTATIC=1`
- wxWidgets 3.2.10 built as static wxBase
- `NOTEST=1` during cross compilation
- `NOSTRIP=1`, with OpenWrt package stripping disabled for maintainer
diagnostics
The package installs only:
- `/usr/bin/veracrypt`
- `/sbin/mount.veracrypt`
- `/usr/share/licenses/veracrypt/License.txt`
`mount.veracrypt` uses a Bash shebang, so the OpenWrt package declares `bash`
as a runtime dependency.
## Build
From the VeraCrypt checkout root:
```sh
src/Build/build_veracrypt_openwrt.sh
```
The script downloads and verifies the OpenWrt SDK against a pinned SHA-256 for
the supported release/target, downloads and verifies wxWidgets 3.2.10, installs
the required OpenWrt feeds, renders
`package/utils/veracrypt/Makefile` inside the SDK, builds the `.ipk`, and
also builds the local userland `bash`, `fuse3`, `util-linux`, and `lvm2` feed
packages used by the QEMU runtime test. Kernel modules for the stock OpenWrt
image are resolved by the test from the official OpenWrt kmod feed for the
selected release and target.
Default output location:
```text
../openwrt-veracrypt/openwrt-sdk-24.10.6-x86-64_gcc-13.3.0_musl.Linux-x86_64/bin/packages/x86_64/base/veracrypt_<version>-r1_x86_64.ipk
```
Useful options:
```sh
src/Build/build_veracrypt_openwrt.sh --fresh-sdk
src/Build/build_veracrypt_openwrt.sh --work-dir /tmp/veracrypt-openwrt
src/Build/build_veracrypt_openwrt.sh --sdk-dir /path/to/openwrt-sdk
src/Build/build_veracrypt_openwrt.sh --sdk-url URL --sdk-sha256 HASH
src/Build/build_veracrypt_openwrt.sh --wx-version 3.2.10
```
Custom SDK URLs or unsupported OpenWrt release/target combinations must pass
`--sdk-sha256`; the build script does not trust an unsigned `sha256sums` file as
the sole integrity source for SDK archives.
If the host only has `mawk`, the build script downloads and builds GNU awk
locally under the OpenWrt work directory because OpenWrt feed scripts require
GNU awk behavior on some hosts.
## QEMU Runtime Test
Install or provide `qemu-system-x86_64`. On Debian/Ubuntu hosts:
```sh
sudo apt install qemu-system-x86
```
Then run:
```sh
python3 src/Build/test_veracrypt_openwrt_qemu.py \
--ipk ../openwrt-veracrypt/openwrt-sdk-24.10.6-x86-64_gcc-13.3.0_musl.Linux-x86_64/bin/packages/x86_64/base/veracrypt_<version>-r1_x86_64.ipk
```
The test script downloads and verifies the matching OpenWrt x86/64 ext4 image,
boots it with QEMU user networking, waits for OpenWrt network init to settle,
gets a DHCP lease on `br-lan` or `eth0`, resolves the needed dependency closure
from local SDK `.ipk` control metadata plus the official OpenWrt kmod feed,
serves those packages to the guest, and installs them with `opkg`. It then runs:
```sh
veracrypt --text --version
veracrypt --text --test
```
By default it also creates a 16 MiB AES/SHA-512 test container, opens it with
`--filesystem=none`, verifies it appears in `veracrypt --text --list`, and
unmounts it. Use `--skip-container` to run only the package install, version,
and algorithm self-test path.
If QEMU was extracted locally instead of installed system-wide, pass the binary
and firmware directory explicitly:
```sh
LD_LIBRARY_PATH=/path/to/qemu-libs \
python3 src/Build/test_veracrypt_openwrt_qemu.py \
--qemu /path/to/qemu-system-x86_64 \
--qemu-data-dir /path/to/pc-bios \
--ipk /path/to/veracrypt_<version>-r1_x86_64.ipk
```
The runner defaults to one QEMU vCPU because TCG with multiple vCPUs can
intermittently trip x86 APIC timer startup in the stock OpenWrt image.
The runner does not require external package feed access from the guest; all
runtime packages are served over the host-to-guest QEMU user-networking link.
Local userland packages come from the SDK `bin/` directory, while stock-image
kmods are downloaded by the host from the official OpenWrt kmod feed and staged
locally. If the VeraCrypt `.ipk` is not below the SDK `bin/` directory, pass
`--package-bin-dir /path/to/sdk/bin`.
For custom images whose kernel does not match the official release feed, pass
`--kmod-feed-url` for the matching kmod feed, or `--local-kmods` to resolve
kmods from `--package-bin-dir`.
The test log is written to:
```text
../openwrt-veracrypt/openwrt-qemu-test.log
```
## Runtime Packages
The VeraCrypt package itself declares the direct userland dependencies using
OpenWrt package symbols:
- `libstdcpp`
- `libfuse3`
- `bash`
OpenWrt's FUSE3 recipe defines `Package/libfuse3` with ABI version `3`, so the
binary IPK and package-index metadata are emitted as `libfuse3-3` while still
providing `libfuse3`. The QEMU test installs the seed runtime support normally
needed for useful mounts using package-index names:
- `libfuse3-3`
- `fuse3-utils`
- `kmod-fuse`
- `kmod-loop`
- `lvm2`
- `kmod-dm`
- `losetup`
- `blkid`
- `mount-utils`
- `kmod-crypto-misc`
The test resolver also stages required transitive dependencies from package
metadata, such as `libdevmapper` when pulled by `lvm2`.
Filesystem-specific mounts also need the corresponding OpenWrt filesystem
kernel modules and tools. Smart-card and EMV keyfile support should install
`libpcsclite`, `pcscd`, and the appropriate reader driver such as `ccid`; these
are optional and are not part of the base package dependency set.

View File

@@ -0,0 +1,23 @@
CONFIG_TARGET_x86=y
CONFIG_TARGET_x86_64=y
# CONFIG_TARGET_MULTI_PROFILE is not set
# CONFIG_TARGET_ALL_PROFILES is not set
CONFIG_TARGET_DEVICE_x86_64_DEVICE_generic=y
# CONFIG_ALL is not set
# CONFIG_ALL_KMODS is not set
# CONFIG_ALL_NONSHARED is not set
# CONFIG_DEVEL is not set
CONFIG_PACKAGE_veracrypt=m
CONFIG_PACKAGE_bash=m
CONFIG_PACKAGE_libfuse3=m
CONFIG_PACKAGE_fuse3-utils=m
CONFIG_PACKAGE_kmod-fuse=m
CONFIG_PACKAGE_kmod-loop=m
CONFIG_PACKAGE_kmod-dm=m
CONFIG_PACKAGE_kmod-crypto-misc=m
CONFIG_PACKAGE_lvm2=m
CONFIG_PACKAGE_losetup=m
CONFIG_PACKAGE_blkid=m
CONFIG_PACKAGE_mount-utils=m

View File

@@ -0,0 +1,91 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=veracrypt
PKG_VERSION:=@VERACRYPT_VERSION@
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0 AND LicenseRef-TrueCrypt
PKG_LICENSE_FILES:=veracrypt/src/License.txt
PKG_MAINTAINER:=Mounir IDRASSI <mounir.idrassi@amcrypto.jp>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_BUILD_PARALLEL:=1
PKG_BUILD_DEPENDS:=fuse3 pcsc-lite
WXWIDGETS_VERSION:=@WXWIDGETS_VERSION@
VERACRYPT_SOURCE_DIR:=@VERACRYPT_SOURCE_DIR@
WXWIDGETS_SOURCE_DIR:=@WXWIDGETS_SOURCE_DIR@
include $(INCLUDE_DIR)/package.mk
RSTRIP:=:
STRIP:=:
define Package/veracrypt
SECTION:=utils
CATEGORY:=Utilities
SUBMENU:=Filesystem
TITLE:=VeraCrypt console
URL:=https://www.veracrypt.fr/
DEPENDS:=+libstdcpp +libfuse3 +bash
endef
define Package/veracrypt/description
Console-only VeraCrypt build for OpenWrt using FUSE3 and a static wxBase.
endef
define Build/Prepare
rm -rf $(PKG_BUILD_DIR)
$(INSTALL_DIR) $(PKG_BUILD_DIR)
rsync -a --delete \
--exclude .git \
--exclude 'src/wxrelease' \
--exclude 'src/wxdebug' \
--exclude 'src/Main/veracrypt' \
--exclude 'src/Setup/Linux/usr' \
--exclude '*.o' \
--exclude '*.d' \
--exclude '*.a' \
$(VERACRYPT_SOURCE_DIR)/ $(PKG_BUILD_DIR)/veracrypt/
rsync -a --delete $(WXWIDGETS_SOURCE_DIR)/ $(PKG_BUILD_DIR)/wxWidgets-$(WXWIDGETS_VERSION)/
endef
define Build/Configure
endef
VC_COMMON_MAKE_FLAGS = \
AR="$(TARGET_AR)" \
CC="$(TARGET_CC)" \
CXX="$(TARGET_CXX)" \
AS="yasm" \
RANLIB="$(TARGET_RANLIB)" \
PKG_CONFIG="$(PKG_CONFIG)" \
PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" \
WX_ROOT="$(PKG_BUILD_DIR)/wxWidgets-$(WXWIDGETS_VERSION)" \
WX_BUILD_DIR="$(PKG_BUILD_DIR)/wxBuildConsole" \
WX_CONFIGURE_EXTRA_FLAGS="--target=$(GNU_TARGET_NAME) --host=$(GNU_TARGET_NAME) --build=$(GNU_HOST_NAME) --prefix=/usr --exec-prefix=/usr --disable-rpath" \
TC_EXTRA_CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS)" \
TC_EXTRA_CXXFLAGS="$(TARGET_CXXFLAGS) $(TARGET_CPPFLAGS)" \
TC_EXTRA_LFLAGS="$(TARGET_LDFLAGS)" \
NOGUI=1 \
WITHFUSE3=1 \
WXSTATIC=1 \
NOTEST=1 \
NOSTRIP=1 \
VERBOSE=1
define Build/Compile
+$(MAKE) -C $(PKG_BUILD_DIR)/veracrypt/src $(VC_COMMON_MAKE_FLAGS) clean
+$(MAKE) -C $(PKG_BUILD_DIR)/veracrypt/src $(VC_COMMON_MAKE_FLAGS) wxbuild
+$(MAKE) -C $(PKG_BUILD_DIR)/veracrypt/src $(PKG_JOBS) $(VC_COMMON_MAKE_FLAGS)
endef
define Package/veracrypt/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/veracrypt/src/Main/veracrypt $(1)/usr/bin/veracrypt
$(INSTALL_DIR) $(1)/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/veracrypt/src/Setup/Linux/mount.veracrypt $(1)/sbin/mount.veracrypt
$(INSTALL_DIR) $(1)/usr/share/licenses/veracrypt
$(INSTALL_DATA) $(PKG_BUILD_DIR)/veracrypt/src/License.txt $(1)/usr/share/licenses/veracrypt/License.txt
endef
$(eval $(call BuildPackage,veracrypt))

View File

@@ -0,0 +1,407 @@
#!/bin/sh
#
# Copyright (c) 2026 AM Crypto
# Governed by the Apache License 2.0 the full text of which is contained
# in the file License.txt included in VeraCrypt binary and source
# code distribution packages.
#
set -eu
umask 022
OPENWRT_VERSION=24.10.6
OPENWRT_TARGET=x86/64
WX_VERSION=3.2.10
WX_URL=
WX_SHA256=d66e929569947a4a5920699539089a9bda83a93e5f4917fb313a61f0c344b896
SDK_URL=
SDK_SHA256=
SDK_DIR=
FRESH_SDK=0
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
REPOROOT=$(readlink -f "$SCRIPTPATH/../..")
SOURCEPATH="$REPOROOT/src"
PARENTDIR=$(readlink -f "$SCRIPTPATH/../../..")
WORK_DIR="$PARENTDIR/openwrt-veracrypt"
JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
GAWK_VERSION=5.3.2
GAWK_URL="https://ftp.gnu.org/gnu/gawk/gawk-$GAWK_VERSION.tar.xz"
GAWK_SHA256=f8c3486509de705192138b00ef2c00bbbdd0e84c30d5c07d23fc73a9dc4cc9cc
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Build the VeraCrypt console-only OpenWrt package with the OpenWrt SDK.
Options:
--openwrt-version VERSION OpenWrt release to use (default: $OPENWRT_VERSION)
--target TARGET OpenWrt target/subtarget (default: $OPENWRT_TARGET)
--work-dir DIR Download/build workspace (default: $WORK_DIR)
--sdk-url URL SDK archive URL (auto-detected for x86/64)
--sdk-sha256 HASH SDK archive SHA-256 (required for custom SDKs)
--sdk-dir DIR Use an already extracted SDK directory
--fresh-sdk Re-extract the SDK before building
--wx-version VERSION wxWidgets version (default: $WX_VERSION)
--wx-url URL wxWidgets source archive URL
--wx-sha256 HASH wxWidgets source archive SHA-256
-j, --jobs N Parallel make jobs (default: host CPU count)
-h, --help Show this help
Only x86/64 is currently wired because it is the QEMU smoke-test target.
EOF
}
die() {
echo "Error: $*" >&2
exit 1
}
need_tool() {
command -v "$1" >/dev/null 2>&1 || die "Required tool '$1' was not found"
}
require_option_arg() {
[ $# -ge 2 ] || die "Option $1 requires an argument"
}
download_file() {
url=$1
out=$2
expected_sha=$3
tmp="$out.tmp.$$"
mkdir -p "$(dirname "$out")"
if [ -f "$out" ]; then
if [ -z "$expected_sha" ]; then
return
fi
actual_sha=$(sha256sum "$out" | awk '{print $1}')
if [ "$actual_sha" = "$expected_sha" ]; then
return
fi
echo "Checksum mismatch for existing $out; re-downloading" >&2
rm -f "$out"
fi
rm -f "$tmp"
echo "Downloading $url"
if ! wget -O "$tmp" "$url"; then
rm -f "$tmp"
die "Download failed: $url"
fi
if [ -n "$expected_sha" ]; then
actual_sha=$(sha256sum "$tmp" | awk '{print $1}')
if [ "$actual_sha" != "$expected_sha" ]; then
rm -f "$tmp"
die "SHA-256 mismatch for $out: expected $expected_sha, got $actual_sha"
fi
fi
mv "$tmp" "$out"
}
pinned_sdk_sha256() {
archive_name=$1
case "$OPENWRT_VERSION:$OPENWRT_TARGET:$archive_name" in
24.10.6:x86/64:openwrt-sdk-24.10.6-x86-64_gcc-13.3.0_musl.Linux-x86_64.tar.zst)
printf '%s\n' "9e398ea7efc098e4a986f97efff595e32d08c615fe356bcb3d885d7ad3a39ac0"
;;
esac
}
target_defaults() {
case "$OPENWRT_TARGET" in
x86/64)
OPENWRT_TARGET_SLUG=x86-64
OPENWRT_CONFIG="$REPOROOT/src/Build/Packaging/openwrt/configs/x86_64-minimal.config"
;;
*)
die "Unsupported target '$OPENWRT_TARGET'. Add a config under src/Build/Packaging/openwrt/configs first."
;;
esac
}
openwrt_base_url() {
printf 'https://downloads.openwrt.org/releases/%s/targets/%s\n' "$OPENWRT_VERSION" "$OPENWRT_TARGET"
}
resolve_sdk() {
if [ -n "$SDK_DIR" ]; then
SDK_DIR=$(readlink -f "$SDK_DIR")
[ -d "$SDK_DIR" ] || die "SDK directory does not exist: $SDK_DIR"
return
fi
base_url=$(openwrt_base_url)
mkdir -p "$WORK_DIR/downloads"
if [ -z "$SDK_URL" ]; then
index_file="$WORK_DIR/downloads/openwrt-$OPENWRT_VERSION-$OPENWRT_TARGET_SLUG-index.html"
wget -q -O "$index_file" "$base_url/"
sdk_archive=$(sed -n "s/.*href=\"\\(openwrt-sdk-$OPENWRT_VERSION-${OPENWRT_TARGET_SLUG}_[^\"]*\\.Linux-x86_64\\.tar\\.zst\\)\".*/\\1/p" "$index_file" | head -n 1)
[ -n "$sdk_archive" ] || die "Could not find an SDK archive at $base_url/"
SDK_URL="$base_url/$sdk_archive"
else
sdk_archive=$(basename "$SDK_URL")
fi
if [ -z "$SDK_SHA256" ]; then
SDK_SHA256=$(pinned_sdk_sha256 "$sdk_archive")
fi
[ -n "$SDK_SHA256" ] || die "No trusted SDK SHA-256 is available for $sdk_archive; pass --sdk-sha256 for custom SDKs"
SDK_ARCHIVE_PATH="$WORK_DIR/downloads/$sdk_archive"
download_file "$SDK_URL" "$SDK_ARCHIVE_PATH" "$SDK_SHA256"
sdk_top=$(zstd -dc "$SDK_ARCHIVE_PATH" | tar -tf - | sed -n '1{s,/.*,,;p;q;}')
[ -n "$sdk_top" ] || die "Could not determine SDK archive top-level directory"
SDK_DIR="$WORK_DIR/$sdk_top"
if [ "$FRESH_SDK" = "1" ]; then
rm -rf "$SDK_DIR"
fi
if [ ! -d "$SDK_DIR" ]; then
echo "Extracting $SDK_ARCHIVE_PATH"
zstd -dc "$SDK_ARCHIVE_PATH" | tar -xf - -C "$WORK_DIR"
fi
}
ensure_gawk() {
HOST_TOOLS="$WORK_DIR/host-tools"
if command -v gawk >/dev/null 2>&1; then
mkdir -p "$HOST_TOOLS/bin"
ln -sf "$(command -v gawk)" "$HOST_TOOLS/bin/gawk"
ln -sf "$(command -v gawk)" "$HOST_TOOLS/bin/awk"
return
fi
if [ -x "$HOST_TOOLS/prefix/bin/gawk" ]; then
mkdir -p "$HOST_TOOLS/bin"
ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/gawk"
ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/awk"
return
fi
need_tool make
if ! command -v gcc >/dev/null 2>&1 && ! command -v cc >/dev/null 2>&1; then
die "GNU awk is not installed and no C compiler was found to build it"
fi
mkdir -p "$HOST_TOOLS/src"
gawk_archive="$HOST_TOOLS/src/gawk-$GAWK_VERSION.tar.xz"
download_file "$GAWK_URL" "$gawk_archive" "$GAWK_SHA256"
rm -rf "$HOST_TOOLS/src/gawk-$GAWK_VERSION"
tar -xf "$gawk_archive" -C "$HOST_TOOLS/src"
echo "Building GNU awk $GAWK_VERSION for OpenWrt feed scripts"
(
cd "$HOST_TOOLS/src/gawk-$GAWK_VERSION"
./configure --prefix="$HOST_TOOLS/prefix" >/dev/null
make -j "$JOBS" >/dev/null
make install >/dev/null
)
mkdir -p "$HOST_TOOLS/bin"
ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/gawk"
ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/awk"
}
prepare_wxwidgets() {
if [ -z "$WX_URL" ]; then
WX_URL="https://github.com/wxWidgets/wxWidgets/releases/download/v$WX_VERSION/wxWidgets-$WX_VERSION.tar.bz2"
fi
wx_archive="$WORK_DIR/downloads/wxWidgets-$WX_VERSION.tar.bz2"
WX_SOURCE_DIR="$WORK_DIR/sources/wxWidgets-$WX_VERSION"
download_file "$WX_URL" "$wx_archive" "$WX_SHA256"
if [ ! -f "$WX_SOURCE_DIR/configure" ]; then
rm -rf "$WX_SOURCE_DIR"
mkdir -p "$WORK_DIR/sources"
echo "Extracting $wx_archive"
tar -xjf "$wx_archive" -C "$WORK_DIR/sources"
fi
}
sed_escape() {
printf '%s' "$1" | sed 's/[&|]/\\&/g'
}
render_package_makefile() {
version=$(sed -n 's/^#define[[:space:]][[:space:]]*VERSION_STRING[[:space:]][[:space:]]*"\([^"]*\)".*/\1/p' "$SOURCEPATH/Common/Tcdefs.h" | head -n 1)
[ -n "$version" ] || die "Could not determine VeraCrypt version from src/Common/Tcdefs.h"
package_dir="$SDK_DIR/package/utils/veracrypt"
template="$REPOROOT/src/Build/Packaging/openwrt/package/utils/veracrypt/Makefile.in"
rm -rf "$package_dir"
mkdir -p "$package_dir"
sed \
-e "s|@VERACRYPT_VERSION@|$(sed_escape "$version")|g" \
-e "s|@VERACRYPT_SOURCE_DIR@|$(sed_escape "$REPOROOT")|g" \
-e "s|@WXWIDGETS_VERSION@|$(sed_escape "$WX_VERSION")|g" \
-e "s|@WXWIDGETS_SOURCE_DIR@|$(sed_escape "$WX_SOURCE_DIR")|g" \
"$template" > "$package_dir/Makefile"
VERACRYPT_VERSION=$version
}
configure_sdk() {
[ -f "$OPENWRT_CONFIG" ] || die "Missing OpenWrt config seed: $OPENWRT_CONFIG"
cp "$OPENWRT_CONFIG" "$SDK_DIR/.config"
(
cd "$SDK_DIR"
if ! PATH="$HOST_TOOLS/bin:$PATH" ./scripts/feeds update packages base; then
echo "Feed update failed; removing stale feed checkouts and retrying" >&2
rm -rf feeds/base feeds/packages
PATH="$HOST_TOOLS/bin:$PATH" ./scripts/feeds update packages base
fi
PATH="$HOST_TOOLS/bin:$PATH" ./scripts/feeds install fuse3 lvm2 util-linux pcsc-lite bash
PATH="$HOST_TOOLS/bin:$PATH" make defconfig
)
}
build_package() {
(
cd "$SDK_DIR"
PATH="$HOST_TOOLS/bin:$PATH" make package/utils/veracrypt/clean V=s
PATH="$HOST_TOOLS/bin:$PATH" make package/utils/veracrypt/compile V=s -j "$JOBS"
)
IPK_PATH=$(find "$SDK_DIR/bin/packages" "$SDK_DIR/bin/targets" -name "veracrypt_${VERACRYPT_VERSION}-*.ipk" 2>/dev/null | sort | tail -n 1)
[ -n "$IPK_PATH" ] || die "Build completed but no VeraCrypt .ipk was found"
}
build_runtime_packages() {
(
cd "$SDK_DIR"
for target in \
package/feeds/packages/bash/compile \
package/feeds/packages/fuse3/compile \
package/feeds/base/util-linux/compile \
package/feeds/packages/lvm2/compile
do
echo "Building OpenWrt runtime dependency: $target"
PATH="$HOST_TOOLS/bin:$PATH" make "$target" V=s -j "$JOBS"
done
)
}
while [ $# -gt 0 ]; do
case "$1" in
--openwrt-version)
require_option_arg "$@"
OPENWRT_VERSION=$2
shift 2
;;
--target)
require_option_arg "$@"
OPENWRT_TARGET=$2
shift 2
;;
--work-dir)
require_option_arg "$@"
WORK_DIR=$(readlink -m "$2")
shift 2
;;
--sdk-url)
require_option_arg "$@"
SDK_URL=$2
shift 2
;;
--sdk-sha256)
require_option_arg "$@"
SDK_SHA256=$2
shift 2
;;
--sdk-dir)
require_option_arg "$@"
SDK_DIR=$2
shift 2
;;
--fresh-sdk)
FRESH_SDK=1
shift
;;
--wx-version)
require_option_arg "$@"
WX_VERSION=$2
shift 2
;;
--wx-url)
require_option_arg "$@"
WX_URL=$2
shift 2
;;
--wx-sha256)
require_option_arg "$@"
WX_SHA256=$2
shift 2
;;
-j|--jobs)
require_option_arg "$@"
JOBS=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
case "$JOBS" in
''|*[!0-9]*)
die "jobs must be a positive integer"
;;
esac
[ "$JOBS" -gt 0 ] || die "jobs must be a positive integer"
need_tool awk
need_tool find
need_tool make
need_tool rsync
need_tool sed
need_tool sha256sum
need_tool tar
need_tool wget
need_tool yasm
need_tool zstd
mkdir -p "$WORK_DIR"
target_defaults
resolve_sdk
ensure_gawk
prepare_wxwidgets
render_package_makefile
configure_sdk
build_package
build_runtime_packages
echo
echo "OpenWrt release: $OPENWRT_VERSION $OPENWRT_TARGET"
if [ -n "$SDK_URL" ]; then
echo "OpenWrt SDK: $SDK_URL"
else
echo "OpenWrt SDK: existing directory supplied with --sdk-dir"
fi
echo "SDK directory: $SDK_DIR"
echo "wxWidgets: $WX_URL"
echo "VeraCrypt package: $IPK_PATH"
sha256sum "$IPK_PATH"
echo
echo "Run the QEMU runtime test with:"
echo " python3 \"$SCRIPTPATH/test_veracrypt_openwrt_qemu.py\" --ipk \"$IPK_PATH\" --work-dir \"$WORK_DIR\""

View File

@@ -0,0 +1,863 @@
#!/usr/bin/env python3
#
# Copyright (c) 2026 AM Crypto
# Governed by the Apache License 2.0 the full text of which is contained
# in the file License.txt included in VeraCrypt binary and source
# code distribution packages.
#
import argparse
import gzip
import hashlib
import http.server
import io
import lzma
import os
import re
import selectors
import shutil
import socketserver
import subprocess
import sys
import tarfile
import tempfile
import threading
import time
import urllib.parse
import urllib.request
from pathlib import Path
DEFAULT_OPENWRT_VERSION = "24.10.6"
DEFAULT_TARGET = "x86/64"
DEFAULT_PASSWORD = "OpenWrt-VeraCrypt-Test-Password-123456"
SHELL_PROMPT = ":~#"
PREINSTALLED_PACKAGES = {
"base-files",
"busybox",
"kernel",
"libc",
"libgcc1",
"libpthread",
"librt",
"opkg",
}
DEFAULT_RUNTIME_PACKAGES = [
"bash",
# OpenWrt emits the FUSE3 library IPK with its ABI suffix; it still
# Provides: libfuse3.
"libfuse3-3",
"fuse3-utils",
"lvm2",
"losetup",
"blkid",
"mount-utils",
"kmod-fuse",
"kmod-loop",
"kmod-dm",
"kmod-crypto-misc",
"veracrypt",
]
class TestError(Exception):
pass
class ReusableTCPServer(socketserver.TCPServer):
allow_reuse_address = True
class PackageMetadata:
def __init__(self, path, fields, url=None, source="local"):
self.path = path
self.url = url
self.source = source
self.package = fields.get("Package", "")
self.version = fields.get("Version", "")
self.filename = fields.get("Filename", "")
self.sha256sum = fields.get("SHA256sum", "")
self.depends = parse_depends(fields.get("Depends", ""))
self.provides = parse_name_list(fields.get("Provides", ""))
def identity(self):
if self.path:
return f"local:{self.path.resolve()}"
return f"remote:{self.url}"
def display_location(self):
return str(self.path) if self.path else self.url
def package_file_name(self):
if self.path:
return self.path.name
url_path = urllib.parse.urlparse(self.url).path
return Path(url_path).name
class Console:
def __init__(self, proc, log_path):
self.proc = proc
self.selector = selectors.DefaultSelector()
self.selector.register(proc.stdout, selectors.EVENT_READ)
os.set_blocking(proc.stdout.fileno(), False)
self.buffer = ""
self.log = open(log_path, "w", encoding="utf-8", errors="replace")
self.command_index = 0
def close(self):
self.log.close()
def _record(self, data):
text = data.decode("utf-8", errors="replace")
self.buffer += text
self.log.write(text)
self.log.flush()
sys.stdout.write(text)
sys.stdout.flush()
def send(self, text):
self.proc.stdin.write(text.encode("utf-8"))
self.proc.stdin.flush()
def read_until(self, patterns, timeout, start=0):
if isinstance(patterns, str):
patterns = [patterns]
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
tail = self.buffer[start:]
for pattern in patterns:
if pattern in tail:
return pattern
if self.proc.poll() is not None:
raise TestError(f"QEMU exited before seeing {patterns}")
events = self.selector.select(0.25)
for key, _ in events:
try:
data = os.read(key.fileobj.fileno(), 8192)
except BlockingIOError:
continue
if data:
self._record(data)
raise TestError(f"Timed out waiting for {patterns}")
def run(self, command, timeout=120):
self.command_index += 1
marker = f"__VC_STATUS_{self.command_index:03d}__"
start = len(self.buffer)
wrapped = (
f"printf '\\n__VC_BEGIN_{self.command_index:03d}__\\n'\n"
"{\n"
f"{command}\n"
"}\n"
f"echo {marker}:$?\n"
)
self.send(wrapped)
deadline = time.monotonic() + timeout
status_re = re.compile(rf"{re.escape(marker)}:(\d+)")
while time.monotonic() < deadline:
tail = self.buffer[start:]
match = status_re.search(tail)
if match:
self.read_until(SHELL_PROMPT, 60, start + match.end())
tail = self.buffer[start:]
status = int(match.group(1))
if status != 0:
raise TestError(f"Command failed with status {status}: {command}")
return tail
if self.proc.poll() is not None:
raise TestError(f"QEMU exited while running: {command}")
events = self.selector.select(0.25)
for key, _ in events:
try:
data = os.read(key.fileobj.fileno(), 8192)
except BlockingIOError:
continue
if data:
self._record(data)
raise TestError(f"Timed out running: {command}")
def target_info(target, version):
if target != "x86/64":
raise TestError("Only x86/64 is currently supported by this QEMU test")
return {
"slug": "x86-64",
"image": f"openwrt-{version}-x86-64-generic-ext4-combined.img.gz",
"manifest": f"openwrt-{version}-x86-64.manifest",
"base_url": f"https://downloads.openwrt.org/releases/{version}/targets/x86/64",
}
def sha256_file(path):
digest = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def download(url, path, expected_sha256=None):
if path.exists() and expected_sha256:
actual_sha = sha256_file(path)
if actual_sha == expected_sha256:
return
print(f"Checksum mismatch for existing {path}; re-downloading", file=sys.stderr)
path.unlink()
elif path.exists():
return
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name(f"{path.name}.tmp-{os.getpid()}")
if tmp.exists():
tmp.unlink()
print(f"Downloading {url}")
try:
with urllib.request.urlopen(url) as response, open(tmp, "wb") as out:
shutil.copyfileobj(response, out)
if expected_sha256:
actual_sha = sha256_file(tmp)
if actual_sha != expected_sha256:
raise TestError(f"SHA-256 mismatch for {path}: expected {expected_sha256}, got {actual_sha}")
tmp.replace(path)
finally:
if tmp.exists():
tmp.unlink()
def read_text_archive(path):
data = path.read_bytes()
if path.name.endswith(".gz"):
return gzip.decompress(data).decode("utf-8", errors="replace")
if path.name.endswith(".xz"):
return lzma.decompress(data).decode("utf-8", errors="replace")
return data.decode("utf-8", errors="replace")
def expected_sha_from_sums(sums_path, filename):
with open(sums_path, "r", encoding="utf-8") as fh:
for line in fh:
parts = line.split()
if len(parts) >= 2 and parts[1].lstrip("*") == filename:
return parts[0]
return None
def sh_quote(text):
return "'" + str(text).replace("'", "'\"'\"'") + "'"
def read_ar_control_archive(ipk_path):
with open(ipk_path, "rb") as fh:
magic = fh.read(8)
if magic != b"!<arch>\n":
raise TestError(f"Unsupported .ipk format for {ipk_path}")
while True:
header = fh.read(60)
if not header:
break
if len(header) != 60 or header[58:60] != b"`\n":
raise TestError(f"Malformed ar archive in {ipk_path}")
name = header[:16].decode("utf-8", errors="replace").strip().rstrip("/")
size = int(header[48:58].decode("ascii").strip())
data = fh.read(size)
if size % 2:
fh.read(1)
if Path(name).name.startswith("control.tar"):
return name, data
raise TestError(f"No control archive found in {ipk_path}")
def extract_top_level_control_archive(ipk_path):
with open(ipk_path, "rb") as fh:
magic = fh.read(8)
if magic == b"!<arch>\n":
return read_ar_control_archive(ipk_path)
try:
with tarfile.open(ipk_path, "r:*") as outer:
for member in outer:
if Path(member.name).name.startswith("control.tar"):
member_file = outer.extractfile(member)
if member_file:
return member.name, member_file.read()
except tarfile.TarError as exc:
raise TestError(f"Unsupported .ipk format for {ipk_path}: {exc}") from exc
raise TestError(f"No control archive found in {ipk_path}")
def open_control_tar(name, data):
try:
return tarfile.open(fileobj=io.BytesIO(data), mode="r:*")
except tarfile.TarError:
pass
if name.endswith(".zst"):
zstd = shutil.which("zstd")
if not zstd:
raise TestError(f"{name} is zstd-compressed, but zstd was not found")
result = subprocess.run([zstd, "-dc"], input=data, stdout=subprocess.PIPE, check=False)
if result.returncode != 0:
raise TestError(f"zstd failed while reading {name}")
return tarfile.open(fileobj=io.BytesIO(result.stdout), mode="r:")
if name.endswith(".gz"):
return tarfile.open(fileobj=io.BytesIO(gzip.decompress(data)), mode="r:")
if name.endswith(".xz"):
return tarfile.open(fileobj=io.BytesIO(lzma.decompress(data)), mode="r:")
raise TestError(f"Unsupported control archive compression: {name}")
def read_control_fields(ipk_path):
control_name, control_data = extract_top_level_control_archive(ipk_path)
with open_control_tar(control_name, control_data) as control_tar:
for member in control_tar:
if member.name in ("control", "./control") or member.name.endswith("/control"):
member_file = control_tar.extractfile(member)
if member_file:
text = member_file.read().decode("utf-8", errors="replace")
return parse_control_fields(text)
raise TestError(f"No control file found in {ipk_path}")
def parse_control_fields(text):
fields = {}
current = None
for line in text.splitlines():
if not line:
current = None
continue
if line[0].isspace() and current:
fields[current] += "\n" + line.strip()
continue
key, sep, value = line.partition(":")
if not sep:
continue
current = key
fields[key] = value.strip()
return fields
def parse_control_paragraphs(text):
paragraphs = []
lines = []
for line in text.splitlines():
if line.strip():
lines.append(line)
continue
if lines:
paragraphs.append(parse_control_fields("\n".join(lines)))
lines = []
if lines:
paragraphs.append(parse_control_fields("\n".join(lines)))
return paragraphs
def parse_package_name(text):
text = re.sub(r"\s*\([^)]*\)", "", text).strip()
return text.split()[0] if text else ""
def parse_depends(value):
groups = []
for item in value.replace("\n", " ").split(","):
alternatives = [parse_package_name(part) for part in item.split("|")]
alternatives = [name for name in alternatives if name]
if alternatives:
groups.append(alternatives)
return groups
def parse_name_list(value):
names = []
for item in value.replace("\n", " ").split(","):
name = parse_package_name(item)
if name:
names.append(name)
return names
def infer_package_bin_dir(ipk_path):
for parent in [ipk_path.parent] + list(ipk_path.parents):
if parent.name == "bin":
return parent
return ipk_path.parent
def read_package_metadata(ipk_path):
meta = PackageMetadata(ipk_path, read_control_fields(ipk_path))
if not meta.package:
raise TestError(f"Package metadata in {ipk_path} has no Package field")
return meta
def add_package_metadata(index, meta, override=False):
for name in [meta.package] + meta.provides:
if name and (override or name not in index):
index[name] = meta
def build_package_index(package_bin_dir, veracrypt_ipk, skip_local_kmods=False):
if not package_bin_dir.is_dir():
raise TestError(f"Package bin directory does not exist: {package_bin_dir}")
index = {}
for ipk in sorted(package_bin_dir.rglob("*.ipk")):
if skip_local_kmods and ipk.name.startswith("kmod-"):
continue
add_package_metadata(index, read_package_metadata(ipk))
add_package_metadata(index, read_package_metadata(veracrypt_ipk), override=True)
return index
def parse_packages_index(text, feed_url, source):
index = {}
feed_url = feed_url.rstrip("/") + "/"
for fields in parse_control_paragraphs(text):
package = fields.get("Package", "")
filename = fields.get("Filename", "")
if not package or not filename:
continue
url = urllib.parse.urljoin(feed_url, filename)
meta = PackageMetadata(None, fields, url=url, source=source)
add_package_metadata(index, meta)
return index
def download_packages_index(index_url, cache_path):
download(index_url, cache_path)
return read_text_archive(cache_path)
def official_index_cache_path(work_dir, info, name):
safe_name = re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_")
return work_dir / "package-indexes" / info["slug"] / safe_name
def kmod_dir_from_kernel_version(version):
match = re.match(r"^([^~]+)~([0-9a-fA-F]+)-r([0-9A-Za-z_.+-]+)$", version)
if not match:
return None
linux_version, vermagic, release = match.groups()
return f"{linux_version}-{release}-{vermagic}"
def official_manifest_kernel_version(args, info):
manifest_url = f"{info['base_url']}/{info['manifest']}"
cache_path = official_index_cache_path(args.work_dir, info, info["manifest"])
download(manifest_url, cache_path)
manifest = read_text_archive(cache_path)
for line in manifest.splitlines():
name, sep, version = line.partition(" - ")
if sep and name == "kernel":
return version.strip()
return None
def discover_single_kmod_feed_url(info):
kmods_url = f"{info['base_url']}/kmods/"
print(f"Discovering OpenWrt kmod feed from {kmods_url}")
with urllib.request.urlopen(kmods_url) as response:
html = response.read().decode("utf-8", errors="replace")
candidates = sorted(set(re.findall(r'href="([^"/]+-[^"/]+-[0-9a-fA-F]+/)"', html)))
if len(candidates) == 1:
return urllib.parse.urljoin(kmods_url, candidates[0]).rstrip("/")
if not candidates:
raise TestError(f"Could not discover an OpenWrt kmod feed at {kmods_url}")
raise TestError(
"Multiple OpenWrt kmod feeds are available; pass --kmod-feed-url explicitly: "
+ ", ".join(urllib.parse.urljoin(kmods_url, candidate).rstrip("/") for candidate in candidates)
)
def resolve_official_kmod_feed_url(args, info):
if args.kmod_feed_url:
return args.kmod_feed_url.rstrip("/")
kernel_version = official_manifest_kernel_version(args, info)
if kernel_version:
kmod_dir = kmod_dir_from_kernel_version(kernel_version)
if kmod_dir:
return f"{info['base_url']}/kmods/{kmod_dir}"
return discover_single_kmod_feed_url(info)
def official_kmod_package_index(args):
info = target_info(args.target, args.openwrt_version)
feed_url = resolve_official_kmod_feed_url(args, info)
index_url = f"{feed_url}/Packages.gz"
cache_path = official_index_cache_path(args.work_dir, info, f"kmods-{Path(feed_url).name}-Packages.gz")
text = download_packages_index(index_url, cache_path)
index = parse_packages_index(text, feed_url, f"official kmods {feed_url}")
if not index:
raise TestError(f"No packages were found in OpenWrt kmod feed {index_url}")
return index, feed_url
def overlay_package_index(index, overlay):
seen = set()
for meta in overlay.values():
meta_key = meta.identity()
if meta_key in seen:
continue
seen.add(meta_key)
add_package_metadata(index, meta, override=True)
def resolve_runtime_packages(package_index, seed_packages):
resolved = []
resolved_paths = set()
visiting = set()
def visit(name, chain):
if name in PREINSTALLED_PACKAGES:
return
meta = package_index.get(name)
if not meta:
chain_text = " -> ".join(chain + [name])
raise TestError(f"Missing .ipk metadata for dependency '{name}' while resolving {chain_text}")
meta_key = meta.identity()
if meta_key in resolved_paths:
return
if meta_key in visiting:
return
visiting.add(meta_key)
for alternatives in meta.depends:
selected = None
for alternative in alternatives:
if alternative in PREINSTALLED_PACKAGES or alternative in package_index:
selected = alternative
break
if not selected:
chain_text = " -> ".join(chain + [meta.package])
raise TestError(
f"Missing .ipk metadata for dependency '{alternatives[0]}' required by {chain_text}"
)
visit(selected, chain + [meta.package])
visiting.remove(meta_key)
resolved_paths.add(meta_key)
resolved.append(meta)
for package in seed_packages:
visit(package, [])
return resolved
def staged_package_name(index, meta):
return f"{index:03d}-{meta.package_file_name()}"
def ensure_remote_package(meta, cache_dir):
if not meta.url:
raise TestError(f"Package {meta.package} has no local path or remote URL")
file_name = meta.package_file_name()
cache_name = f"{hashlib.sha256(meta.url.encode('utf-8')).hexdigest()[:12]}-{file_name}"
cached = cache_dir / cache_name
def verify_cached():
if meta.sha256sum and sha256_file(cached) != meta.sha256sum:
cached.unlink()
return False
return True
if cached.exists() and verify_cached():
return cached
download(meta.url, cached, meta.sha256sum)
if meta.sha256sum and sha256_file(cached) != meta.sha256sum:
raise TestError(f"SHA-256 mismatch for {cached} downloaded from {meta.url}")
return cached
def stage_packages(packages, directory, cache_dir):
directory.mkdir(parents=True, exist_ok=True)
cache_dir.mkdir(parents=True, exist_ok=True)
for index, meta in enumerate(packages):
source = meta.path if meta.path else ensure_remote_package(meta, cache_dir)
shutil.copy2(source, directory / staged_package_name(index, meta))
def package_download_command(packages, http_port):
lines = [
"set -e",
"rm -rf /tmp/veracrypt-ipks",
"mkdir -p /tmp/veracrypt-ipks",
]
for index, meta in enumerate(packages):
package_name = staged_package_name(index, meta)
url_path = urllib.parse.quote(package_name, safe="")
lines.append(
f"wget -O {sh_quote(f'/tmp/veracrypt-ipks/{package_name}')} "
f"{sh_quote(f'http://10.0.2.2:{http_port}/{url_path}')}"
)
lines.append("opkg install /tmp/veracrypt-ipks/*.ipk")
return "\n".join(lines)
def prepare_image(args):
if args.image:
image = Path(args.image).resolve()
if not image.exists():
raise TestError(f"Image does not exist: {image}")
return image
info = target_info(args.target, args.openwrt_version)
image_gz = args.work_dir / "images" / info["image"]
image = image_gz.with_suffix("")
sums = args.work_dir / "images" / f"sha256sums-{args.openwrt_version}-{info['slug']}"
download(f"{info['base_url']}/sha256sums", sums)
expected_sha = expected_sha_from_sums(sums, info["image"])
if not expected_sha:
raise TestError(f"Could not find {info['image']} in {sums}")
download(f"{info['base_url']}/{info['image']}", image_gz, expected_sha)
actual_sha = sha256_file(image_gz)
if actual_sha != expected_sha:
raise TestError(f"SHA-256 mismatch for {image_gz}: expected {expected_sha}, got {actual_sha}")
if not image.exists():
print(f"Extracting {image_gz}")
with open(image, "wb") as out:
result = subprocess.run(["gzip", "-cd", str(image_gz)], stdout=out)
if result.returncode not in (0, 2):
raise TestError(f"gzip failed while extracting {image_gz}")
return image
def start_http_server(directory, address, port):
handler = lambda *args, **kwargs: http.server.SimpleHTTPRequestHandler(
*args, directory=str(directory), **kwargs
)
server = ReusableTCPServer((address, port), handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server
def boot_qemu(args, image):
qemu = shutil.which(args.qemu) if os.sep not in args.qemu else args.qemu
if not qemu:
raise TestError("qemu-system-x86_64 was not found; install QEMU or pass --qemu")
netdev = "user,id=net0"
if args.ssh_port is not None:
netdev += f",hostfwd=tcp::{args.ssh_port}-:22"
qemu_cmd = [
qemu,
"-accel", args.accel,
"-M", "pc",
"-cpu", args.cpu,
"-m", args.memory,
"-smp", str(args.smp),
"-nographic",
"-drive", f"file={image},format=raw,if=virtio",
"-netdev", netdev,
"-device", "virtio-net-pci,netdev=net0",
]
if args.qemu_data_dir:
qemu_cmd[1:1] = ["-L", str(args.qemu_data_dir)]
print("Starting QEMU:")
print(" ".join(qemu_cmd))
return subprocess.Popen(
qemu_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
def run_guest_tests(args, console, http_port, packages):
console.read_until(["Please press Enter", SHELL_PROMPT], args.boot_timeout)
prompt_start = len(console.buffer)
console.send("\n")
console.read_until(SHELL_PROMPT, 90, prompt_start)
console.run(
"sleep 20\n"
"if ip link show br-lan >/dev/null 2>&1; then NETDEV=br-lan; else NETDEV=eth0; fi\n"
"ip addr flush dev \"$NETDEV\" || true\n"
"ip link set \"$NETDEV\" up\n"
"udhcpc -n -q -t 10 -i \"$NETDEV\"\n"
"ip -4 addr show \"$NETDEV\"\n"
"ping -c 1 10.0.2.2",
timeout=120,
)
console.run(package_download_command(packages, http_port), timeout=900)
version_output = console.run("veracrypt --text --version", timeout=120)
if "VeraCrypt " not in version_output:
raise TestError("version command did not print a VeraCrypt version")
test_output = console.run("veracrypt --text --test", timeout=240)
if "Self-tests of all algorithms passed" not in test_output:
raise TestError("algorithm self-test did not report success")
if not args.skip_container:
escaped_password = args.password.replace("'", "'\"'\"'")
console.run("dd if=/dev/urandom of=/tmp/vc-random.bin bs=1M count=1", timeout=120)
console.run(
"veracrypt --text --create /tmp/openwrt-test.hc "
f"--size={args.container_size} "
f"--password='{escaped_password}' "
"--encryption=AES --hash=SHA-512 --filesystem=none "
"--volume-type=normal --random-source=/tmp/vc-random.bin "
"--quick --force --non-interactive",
timeout=360,
)
console.run("mkdir -p /mnt/veracrypt-test", timeout=60)
console.run(
"veracrypt --text --mount /tmp/openwrt-test.hc /mnt/veracrypt-test "
f"--password='{escaped_password}' "
"--pim=0 --keyfiles='' --protect-hidden=no --filesystem=none --non-interactive",
timeout=240,
)
list_output = console.run("veracrypt --text --list", timeout=120)
if "/dev/mapper/veracrypt" not in list_output:
raise TestError("container did not appear in veracrypt --list output")
console.run("veracrypt --text --unmount /tmp/openwrt-test.hc", timeout=180)
def parse_args():
parser = argparse.ArgumentParser(description="Boot OpenWrt in QEMU and test a VeraCrypt .ipk")
parser.add_argument("--ipk", required=True, type=Path, help="Path to veracrypt_*.ipk")
parser.add_argument(
"--package-bin-dir",
type=Path,
help="SDK bin directory containing local dependency .ipk files; defaults to the nearest bin parent of --ipk",
)
parser.add_argument("--openwrt-version", default=DEFAULT_OPENWRT_VERSION)
parser.add_argument("--target", default=DEFAULT_TARGET)
parser.add_argument("--work-dir", type=Path, default=None)
parser.add_argument("--image", type=Path, help="Use an already extracted OpenWrt raw image")
parser.add_argument("--qemu", default="qemu-system-x86_64")
parser.add_argument("--qemu-data-dir", type=Path, help="QEMU pc-bios directory for locally extracted QEMU builds")
parser.add_argument("--accel", default="tcg")
parser.add_argument("--cpu", default="max")
parser.add_argument("--memory", default="512M")
parser.add_argument("--smp", default=1, type=int)
parser.add_argument(
"--ssh-port",
type=int,
metavar="PORT",
help="Forward host TCP PORT to guest SSH; disabled by default",
)
parser.add_argument("--http-port", default=0, type=int)
parser.add_argument(
"--http-bind-address",
default="127.0.0.1",
help="Host address for the temporary package server (default: 127.0.0.1)",
)
parser.add_argument(
"--kmod-feed-url",
help="OpenWrt kmod feed URL; defaults to the official feed matching --openwrt-version and --target",
)
parser.add_argument(
"--local-kmods",
action="store_true",
help="Resolve kmod-* packages from --package-bin-dir instead of the official OpenWrt kmod feed",
)
parser.add_argument("--boot-timeout", default=180, type=int)
parser.add_argument("--container-size", default="16M")
parser.add_argument("--password", default=DEFAULT_PASSWORD)
parser.add_argument("--skip-container", action="store_true")
parser.add_argument("--keep-image", action="store_true")
return parser.parse_args()
def main():
args = parse_args()
args.ipk = args.ipk.resolve()
if not args.ipk.exists():
raise TestError(f"Package does not exist: {args.ipk}")
if args.package_bin_dir is None:
args.package_bin_dir = infer_package_bin_dir(args.ipk)
args.package_bin_dir = args.package_bin_dir.resolve()
repo_root = Path(__file__).resolve().parents[2]
if args.work_dir is None:
args.work_dir = repo_root.parent / "openwrt-veracrypt"
args.work_dir = args.work_dir.resolve()
args.work_dir.mkdir(parents=True, exist_ok=True)
package_index = build_package_index(
args.package_bin_dir,
args.ipk,
skip_local_kmods=not args.local_kmods,
)
if not args.local_kmods:
kmod_index, kmod_feed_url = official_kmod_package_index(args)
overlay_package_index(package_index, kmod_index)
print(f"Using OpenWrt kmod feed: {kmod_feed_url}")
packages = resolve_runtime_packages(package_index, DEFAULT_RUNTIME_PACKAGES)
print("Resolved OpenWrt packages:")
for index, meta in enumerate(packages):
print(f" {index:02d} {meta.package}: {meta.display_location()}")
base_image = prepare_image(args)
test_image = args.work_dir / "images" / f"{base_image.stem}-veracrypt-test.img"
test_image.parent.mkdir(parents=True, exist_ok=True)
if test_image.exists():
test_image.unlink()
shutil.copyfile(base_image, test_image)
with tempfile.TemporaryDirectory(prefix="veracrypt-ipks-", dir=args.work_dir) as package_dir:
server_root = Path(package_dir)
stage_packages(packages, server_root, args.work_dir / "package-cache")
server = start_http_server(server_root, args.http_bind_address, args.http_port)
http_port = server.server_address[1]
print(f"Serving staged packages from {server_root} on http://{args.http_bind_address}:{http_port}/")
log_path = args.work_dir / "openwrt-qemu-test.log"
proc = None
console = None
try:
proc = boot_qemu(args, test_image)
console = Console(proc, log_path)
run_guest_tests(args, console, http_port, packages)
console.send("poweroff\n")
try:
proc.wait(timeout=90)
except subprocess.TimeoutExpired:
proc.terminate()
proc.wait(timeout=30)
finally:
server.shutdown()
server.server_close()
if console:
console.close()
if proc and proc.poll() is None:
proc.terminate()
if not args.keep_image and test_image.exists():
test_image.unlink()
print()
print("OpenWrt QEMU test passed")
print(f"Log: {log_path}")
if __name__ == "__main__":
try:
main()
except TestError as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)

View File

@@ -19,6 +19,8 @@
# NOTEST: Do not test release binary
# RESOURCEDIR: Run-time resource directory
# VERBOSE: Enable verbose messages
# WITHFUSE3: Build with FUSE3 support instead of FUSE2
# WX_CONFIGURE_EXTRA_FLAGS: Extra flags passed to wxWidgets configure
# WXSTATIC: Use static wxWidgets library
# SSSE3: Enable SSSE3 support in compiler
# SSE41: Enable SSE4.1 support in compiler
@@ -736,7 +738,7 @@ endif
WX_CONFIGURE_LEGACY_FLAGS="$$WX_CONFIGURE_LEGACY_FLAGS --enable-$$option"; \
fi; \
done; \
"$(WX_ROOT)/configure" $(WX_CONFIGURE_FLAGS) $$WX_CONFIGURE_LEGACY_FLAGS >/dev/null
"$(WX_ROOT)/configure" $(WX_CONFIGURE_FLAGS) $(WX_CONFIGURE_EXTRA_FLAGS) $$WX_CONFIGURE_LEGACY_FLAGS >/dev/null
@echo Building wxWidgets library...
cd "$(WX_BUILD_DIR)" && $(MAKE) -j 4