This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 13:03:12 +02:00
1 parent feb9d91a3d
commit 1f0daa0679
25 files changed
+174 -1035

No files matched your search

-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SRC=${1:?usage: check-rustproxy-source.sh /path/to/RustProxy}
for f in Cargo.toml grafana/rustproxy-overview.json src/main/bootstrap.rs; do
[[ -f "$SRC/$f" ]] || { echo "missing: $f" >&2; exit 1; }
done
grep -q '^name = "rust-proxy"' "$SRC/Cargo.toml"
grep -q 'RUST_PROXY_CONFIG' "$SRC/src/main/bootstrap.rs"
grep -q 'Reload,' "$SRC/src/main/bootstrap.rs"
# Old RustProxy releases shipped full Debian metadata. The 0.34.x source
# archive no longer does; the builder's own nFPM fallback is authoritative.
if [[ -f "$SRC/debian/rustproxy.service" ]]; then
grep -q 'ExecStart=' "$SRC/debian/rustproxy.service"
else
[[ -f "$SRC/deploy/rustproxy.service" ]] || { echo 'no reference systemd unit found' >&2; exit 1; }
grep -q '^ExecReload=' "$SRC/deploy/rustproxy.service"
fi
echo 'RustProxy source supports --config and soft reload: OK'
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# Offline verification of the agent-specific nFPM packaging pipeline.
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
SRC=${1:-}
if [[ ! -d "${SRC:-}/docker-agent" ]]; then
SRC="$WORK/source"
mkdir -p "$SRC/docker-agent"
cat > "$SRC/docker-agent/rustproxy-docker-agent.service" <<'UNIT'
[Service]
User=rustproxy-docker-agent
Group=docker
EnvironmentFile=/etc/rustproxy/docker-agent.env
ExecStart=/usr/local/bin/rustproxy-docker-agent
StateDirectory=rustproxy-docker-agent
UNIT
cat > "$SRC/docker-agent/docker-agent.env.example" <<'ENV'
RUSTPROXY_DOCKER_AGENT_TOKEN=replace-with-at-least-24-random-characters
ENV
echo '# Docker Agent' > "$SRC/docker-agent/README.md"
fi
if [[ -f "$SRC/docker-agent/go.mod" && $(command -v go || true) ]]; then
(cd "$SRC/docker-agent" && GOWORK=off CGO_ENABLED=0 go test ./... && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o "$WORK/agent" .)
else
cat > "$WORK/agent" <<'MOCK'
#!/bin/sh
exit 0
MOCK
chmod 0755 "$WORK/agent"
fi
export BUILDER_ROOT="$ROOT" VERSION=0.34.2 PACKAGE_RELEASE=4
export MAINTAINER='Package QA <qa@example.invalid>' HOMEPAGE=https://example.invalid
export GPG_PRIVATE_KEY_FILE="$WORK/test-key.asc"
"$ROOT/scripts/prepare-agent-package.sh" "$SRC" "$WORK/package" "$WORK/agent"
PKG="$WORK/package"
test -x "$PKG/payload/usr/bin/rustproxy-docker-agent"
grep -Fqx 'ExecStart=/usr/bin/rustproxy-docker-agent' "$PKG/payload/usr/lib/systemd/system/rustproxy-docker-agent.service"
grep -Fq 'config|noreplace' "$PKG/nfpm-deb.yaml"
grep -Fq 'config|noreplace' "$PKG/nfpm-rpm.yaml"
grep -q 'type: origin' "$PKG/nfpm-deb.yaml"
grep -q 'signature:' "$PKG/nfpm-rpm.yaml"
! grep -q '\.pkg' "$PKG/nfpm-deb.yaml"
for script in "$PKG"/scripts/*; do sh -n "$script"; done
for script in "$PKG"/scripts/*postinstall.sh; do
grep -Fq '/usr/bin/rustproxy-docker-agent init' "$script"
grep -Fq 'systemctl enable rustproxy-docker-agent.service' "$script"
done
if python3 -c 'import yaml' >/dev/null 2>&1; then
python3 - "$PKG" <<'PY_YAML'
import yaml, pathlib, sys
pkg=pathlib.Path(sys.argv[1])
for kind in ('deb','rpm'):
x=yaml.safe_load((pkg/f'nfpm-{kind}.yaml').read_text())
assert x['name']=='rustproxy-docker-agent'
assert x['version']=='0.34.2'
assert x['arch']=='amd64'
assert len(x['contents'])==4
assert x['scripts']['postinstall']
assert any(c['dst']=='/etc/rustproxy/docker-agent.env' and c['type']=='config|noreplace' for c in x['contents'])
PY_YAML
fi
echo 'Docker Agent package manifests, service layout and lifecycle scripts: OK'
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
WORK=$(mktemp -d)
SLEEP_PID=""
cleanup() {
[[ -z "$SLEEP_PID" ]] || kill "$SLEEP_PID" 2>/dev/null || true
rm -rf "$WORK"
}
trap cleanup EXIT
for tool in file zstd tar python3; do
command -v "$tool" >/dev/null 2>&1 || { echo "SKIP FreeBSD native packaging: missing $tool"; exit 0; }
done
mkdir -p "$WORK/source" "$WORK/repo/FreeBSD:15:amd64" "$WORK/run"
printf '[package]\nname = "rust-proxy"\nversion = "0.34.1"\n' > "$WORK/source/Cargo.toml"
python3 - "$WORK/bin" <<'PY'
from pathlib import Path
import struct,sys
p=Path(sys.argv[1]); p.write_bytes(b'\x7fELF\x02\x01\x01\x09'+bytes(8)+struct.pack('<HHIQQQIHHHHHH',2,62,1,0,0,0,0,64,56,0,64,0,0));p.chmod(0o755)
PY
export BUILDER_ROOT="$ROOT" FREEBSD_ABI=FreeBSD:15:amd64 FREEBSD_ARCH=freebsd:15:x86:64
"$ROOT/scripts/package-freebsd.sh" "$WORK/source" "$WORK/bin" "$WORK/rustproxy-0.34.1_2.pkg" 0.34.1_2
zstd -dc "$WORK/rustproxy-0.34.1_2.pkg" | tar -xOf - +MANIFEST > "$WORK/manifest.json"
python3 - "$WORK/manifest.json" <<'PY'
import json,sys
m=json.load(open(sys.argv[1])); names=set(m['files'])
assert m['name']=='rustproxy'
assert m['origin']=='www/rustproxy'
assert m['version']=='0.34.1_2'
assert m['abi']=='FreeBSD:15:amd64'
assert '/usr/local/sbin/rust-proxy' in names
assert '/usr/local/etc/rc.d/rustproxy' in names
assert '/usr/local/etc/newsyslog.conf.d/rustproxy.conf' in names
assert len(names)==3
assert not any('/opnsense/' in n or 'syshook' in n for n in names)
assert 'post-install' in m['scripts'] and 'pre-deinstall' in m['scripts']
PY
# Test the actual rc.d reload command without a running FreeBSD machine.
cat > "$WORK/rc.subr" <<'SH'
load_rc_config() { :; }
checkyesno() { eval 'v=${'"$1"'-NO}'; [ "$v" = YES ]; }
run_rc_command() { case "$1" in reload) "$reload_cmd" ;; *) return 1 ;; esac; }
SH
cat > "$WORK/su" <<'SH'
#!/bin/sh
printf '%s\n' "$*" > "$TEST_ARGS"
SH
chmod +x "$WORK/su"
python3 - "$ROOT/packaging/freebsd/rustproxy.rc" "$WORK/rc" "$WORK" <<'PY'
from pathlib import Path
import sys
src=Path(sys.argv[1]).read_text();w=Path(sys.argv[3]);
src=src.replace('. /etc/rc.subr', f'. "{w}/rc.subr"')
src=src.replace('/usr/bin/su -m', f'"{w}/su" -m')
Path(sys.argv[2]).write_text(src)
PY
sleep 120 & SLEEP_PID=$!
printf '%s\n' "$SLEEP_PID" > "$WORK/run/rustproxy.pid"
export TEST_ARGS="$WORK/cli-args" rustproxy_run_dir="$WORK/run"
export rustproxy_config=/usr/local/etc/rustproxy/config.json rustproxy_enable=YES
sh "$WORK/rc" reload
rg_args=$(cat "$TEST_ARGS")
[[ "$rg_args" == *'--data-dir /var/db/rustproxy'* ]]
[[ "$rg_args" == *'--config /usr/local/etc/rustproxy/config.json'* ]]
[[ "$rg_args" == *'--admin-listen 127.0.0.1:9090 reload'* ]]
export rustproxy_config='/usr/local/etc/rustproxy/config.json;touch /tmp/bad'
if sh "$WORK/rc" reload >/dev/null 2>&1; then
echo 'unsafe config path was accepted' >&2; exit 1
fi
unset rustproxy_config
# Catalog generation: mock FreeBSD bsdtar on Linux using zstd + tar.
mkdir -p "$WORK/mockbin"
cat > "$WORK/mockbin/bsdtar" <<'SH'
#!/bin/sh
case "$1" in
-xOf) zstd -dc "$2" | tar -xOf - "$3" ;;
*) exit 2 ;;
esac
SH
chmod +x "$WORK/mockbin/bsdtar"
cp "$WORK/rustproxy-0.34.1_2.pkg" "$WORK/repo/FreeBSD:15:amd64/"
cp "$WORK/rustproxy-0.34.1_2.pkg" "$WORK/repo/rustproxy-latest.pkg"
PATH="$WORK/mockbin:$PATH" python3 "$ROOT/scripts/generate-opnsense-repo.py" "$WORK/repo" --retention 10
python3 - "$WORK/repo/packagesite.pkg" <<'PY'
import json,tarfile,sys
with tarfile.open(sys.argv[1]) as archive:
content=archive.extractfile('packagesite.yaml').read().decode().splitlines()
assert len(content)==1,content
pkg=json.loads(content[0]);assert pkg['name']=='rustproxy'
assert pkg['path']=='FreeBSD:15:amd64/rustproxy-0.34.1_2.pkg',pkg['path']
PY
echo 'FreeBSD native package, rc.d reload and repository catalog: OK'
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/source"
printf '#!/bin/sh\nexit 0\n' > "$WORK/rust-proxy"
chmod 0755 "$WORK/rust-proxy"
export BUILDER_ROOT="$ROOT" PACKAGE_NAME=rustproxy BINARY_NAME=rust-proxy
export VERSION=1.0.0 PACKAGE_RELEASE=1 DESCRIPTION=RustProxy LICENSE=UNKNOWN
export MAINTAINER=test@example.invalid HOMEPAGE=https://example.invalid
export GPG_PRIVATE_KEY_FILE=/nonexistent/test.gpg
check_unit() {
local unit=$1
# A read-only home does not conceal /home, unlike ProtectHome=true/tmpfs.
[[ $(grep -c '^ProtectHome=read-only$' "$unit") -eq 1 ]]
! grep -Eq '^ProtectHome=(true|yes|tmpfs|false|no)$' "$unit"
grep -Fxq 'ProtectSystem=strict' "$unit"
grep -Fxq 'User=rustproxy' "$unit"
grep -Fxq 'Group=rustproxy' "$unit"
grep -Fxq 'NoNewPrivileges=true' "$unit"
grep -Fxq 'CapabilityBoundingSet=CAP_NET_BIND_SERVICE' "$unit"
grep -Fq 'ReadWritePaths=-/etc/rustproxy' "$unit"
grep -Fxq 'ExecReload=/usr/bin/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload' "$unit"
}
# Current RustProxy archive has no debian unit; both nFPM targets use fallback.
"$ROOT/scripts/prepare-package.sh" "$WORK/source" "$WORK/fallback" "$WORK/rust-proxy"
check_unit "$WORK/fallback/payload/usr/lib/systemd/system/rustproxy.service"
for platform in deb rpm; do
grep -Fq '/usr/lib/systemd/system/rustproxy.service' "$WORK/fallback/nfpm-$platform.yaml"
grep -Fq '/usr/share/doc/rustproxy/FILESYSTEM_ACCESS.md' "$WORK/fallback/nfpm-$platform.yaml"
done
cmp "$ROOT/packaging/rustproxy-filesystem-access.md" \
"$WORK/fallback/payload/usr/share/doc/rustproxy/FILESYSTEM_ACCESS.md"
# Some source releases ship their own unit. Normalize its outdated home policy
# rather than letting it override the new package default.
mkdir -p "$WORK/source/debian"
cat > "$WORK/source/debian/rustproxy.service" <<'UNIT'
[Unit]
Description=RustProxy unit supplied with a source release
[Service]
User=rustproxy
Group=rustproxy
ExecStart=/usr/bin/rust-proxy --data-dir /var/lib/rustproxy serve
ProtectSystem=strict
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
ProtectHome=true
NoNewPrivileges=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
UNIT
python3 "$ROOT/scripts/normalize-systemd-unit.py" "$WORK/source/debian/rustproxy.service" rust-proxy
check_unit "$WORK/source/debian/rustproxy.service"
# A custom unit without ProtectHome must also get the new safe policy.
sed -i '/^ProtectHome=/d' "$WORK/source/debian/rustproxy.service"
python3 "$ROOT/scripts/normalize-systemd-unit.py" "$WORK/source/debian/rustproxy.service" rust-proxy
check_unit "$WORK/source/debian/rustproxy.service"
echo 'Linux DEB/RPM external path access policy: OK'
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
# The bundled Linux rules must match the expected user-provided settings.
diff -u "$ROOT/packaging/rustproxy.logrotate" - <<'CONFIG'
/var/log/rustproxy/*.jsonl /var/log/rustproxy/*.log /var/log/rustproxy/vhosts/*.jsonl {
daily
maxsize 512M
rotate 14
missingok
notifempty
compress
delaycompress
copytruncate
su rustproxy rustproxy
}
CONFIG
# All three log groups must also be tracked by native FreeBSD newsyslog.
OPNSENSE_CONF="$ROOT/opnsense/plugin/src/etc/newsyslog.conf.d/rustproxy.conf"
for pattern in '/var/log/rustproxy/*.jsonl' '/var/log/rustproxy/*.log' '/var/log/rustproxy/vhosts/*.jsonl'; do
grep -Fq "$pattern " "$OPNSENSE_CONF"
done
[[ $(grep -c '^/var/log/rustproxy/' "$OPNSENSE_CONF") == 3 ]]
grep -Fq '14 100M $D0 GBEZPN' "$OPNSENSE_CONF"
grep -Fq '14 100M $D0 GBEZP /var/run/rustproxy/daemon.pid SIGHUP' "$OPNSENSE_CONF"
grep -Fq ' -H -P ${pidfile}' "$ROOT/opnsense/plugin/src/etc/rc.d/rustproxy"
grep -Fq 'COPY packaging/ /opt/rust-package-builder/packaging/' "$ROOT/Dockerfile"
grep -Fq 'packaging_revision=$PACKAGING_REVISION' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'packaging_revision=$OPNSENSE_PACKAGING_REVISION' "$ROOT/scripts/build-opnsense-release.sh"
# Test staging and native PKG manifest without compiling Rust or signing RPM/DEB.
for tool in file zstd tar sha256sum python3; do
command -v "$tool" >/dev/null 2>&1 || { echo "SKIP packaging integration: missing $tool"; exit 0; }
done
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/source" "$WORK/linux"
if command -v logrotate >/dev/null 2>&1; then
# Test parser compatibility even if the local test machine has no rustproxy user.
sed 's/su rustproxy rustproxy/su root root/' "$ROOT/packaging/rustproxy.logrotate" > "$WORK/logrotate-test.conf"
logrotate -d -s "$WORK/logrotate.state" "$WORK/logrotate-test.conf" > "$WORK/logrotate-output" 2>&1
grep -Fq 'reading config file' "$WORK/logrotate-output"
fi
printf '[package]\nname = "rust-proxy"\nversion = "1.0.0"\n' > "$WORK/source/Cargo.toml"
python3 - "$WORK/bin" <<'PY'
from pathlib import Path
import struct, sys
p = Path(sys.argv[1])
# ELF header with FreeBSD OSABI is enough to check the OPNsense packaging path.
e_ident = b'\x7fELF\x02\x01\x01\x09' + bytes(8)
header = struct.pack('<HHIQQQIHHHHHH', 2, 62, 1, 0, 0, 0, 0, 64, 56, 0, 64, 0, 0)
p.write_bytes(e_ident + header)
p.chmod(0o755)
PY
export BUILDER_ROOT="$ROOT" PACKAGE_NAME=rustproxy BINARY_NAME=rust-proxy
export VERSION=1.0.0 PACKAGE_RELEASE=2 DESCRIPTION=RustProxy LICENSE=UNKNOWN
export MAINTAINER=test@example.invalid HOMEPAGE=https://example.invalid
export GPG_PRIVATE_KEY_FILE=/nonexistent/test.gpg
"$ROOT/scripts/prepare-package.sh" "$WORK/source" "$WORK/linux" "$WORK/bin"
cmp "$ROOT/packaging/rustproxy.logrotate" "$WORK/linux/payload/etc/logrotate.d/rustproxy"
UNIT="$WORK/linux/payload/usr/lib/systemd/system/rustproxy.service"
grep -Fq 'ExecReload=/usr/bin/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload' "$UNIT"
grep -Fq 'ReadWritePaths=-/etc/rustproxy' "$UNIT"
grep -Fxq 'ProtectHome=read-only' "$UNIT"
grep -Fxq 'ProtectSystem=strict' "$UNIT"
for metadata in nfpm-deb.yaml nfpm-rpm.yaml; do
grep -Fq '/etc/logrotate.d/rustproxy' "$WORK/linux/$metadata"
grep -Fq 'config|noreplace' "$WORK/linux/$metadata"
grep -Fq ' - logrotate' "$WORK/linux/$metadata"
done
"$ROOT/scripts/package-opnsense.sh" "$WORK/source" "$WORK/bin" "$WORK/os-rustproxy.pkg" '1.0.0_20'
zstd -dc "$WORK/os-rustproxy.pkg" | tar -xOf - +MANIFEST > "$WORK/manifest.json"
python3 - "$WORK/manifest.json" <<'PY'
import json, sys
with open(sys.argv[1]) as f:
manifest=json.load(f)
path='/usr/local/etc/newsyslog.conf.d/rustproxy.conf'
assert path in manifest['files'], f'{path} missing from PKG manifest'
assert path in manifest['files'] and len(manifest['files'][path]) == 64
assert '/usr/local/etc/rc.d/rustproxy' in manifest['files']
assert '/usr/local/etc/rc.syshook.d/start/50-rustproxy.start' in manifest['files']
assert '/usr/local/etc/rc.syshook.d/10-rustproxy.start' not in manifest['files']
PY
zstd -dc "$WORK/os-rustproxy.pkg" | tar -xOf - /usr/local/etc/newsyslog.conf.d/rustproxy.conf > "$WORK/packaged-newsyslog.conf"
cmp "$OPNSENSE_CONF" "$WORK/packaged-newsyslog.conf"
echo 'log rotation package integration: OK'
-151
View File
@@ -1,151 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SRC="$ROOT/opnsense/plugin/src"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
RC="$SRC/etc/rc.d/rustproxy"
HOOK="$SRC/etc/rc.syshook.d/start/50-rustproxy.start"
SERVICE="$SRC/opnsense/scripts/rustproxy/service.sh"
MODEL="$SRC/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml"
FORM="$SRC/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml"
TEMPLATE="$SRC/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf"
[[ -f "$HOOK" && -x "$HOOK" ]] || { echo 'Boot syshook must exist and be executable' >&2; exit 1; }
[[ ! -e "$SRC/etc/rc.syshook.d/10-rustproxy.start" ]] || { echo 'Old hook path still exists' >&2; exit 1; }
for script in "$RC" "$HOOK" "$SERVICE"; do
sh -n "$script"
done
php -l "$SRC/opnsense/mvc/app/models/OPNsense/RustProxy/Migrations/M1_5_0.php" >/dev/null
# Ensure GUI, model and Jinja template agree about the new setting.
python3 - "$MODEL" "$FORM" "$TEMPLATE" <<'PY'
import pathlib, sys, xml.etree.ElementTree as ET
model = ET.parse(sys.argv[1]).getroot()
form = ET.parse(sys.argv[2]).getroot()
template = pathlib.Path(sys.argv[3]).read_text()
assert model.find('version').text == '1.5.0'
field = model.find('./items/general/updateCheckIntervalSecs')
assert field is not None and field.attrib['type'] == 'IntegerField'
assert field.findtext('Default') == '3600'
assert field.findtext('MinimumValue') == '60'
assert field.findtext('MaximumValue') == '604800'
assert 'rustproxy.general.updateCheckIntervalSecs' in [e.text for e in form.iter('id')]
assert 'rustproxy_update_check_interval_secs=' in template
assert 'OPNsense.rustproxy.general.updateCheckIntervalSecs' in template
PY
# Isolate the rc.d argument composition without running FreeBSD-only commands.
cat > "$WORK/rc.subr" <<'EOF'
load_rc_config() { :; }
run_rc_command() {
if [ "$1" = reload ]; then "$reload_cmd"; else printf '%s\n' "$command_args"; fi
}
EOF
python3 - "$RC" "$WORK/rc-test" "$WORK" <<'PY'
from pathlib import Path
import sys
src = Path(sys.argv[1]).read_text()
work = Path(sys.argv[3])
src = src.replace('. /etc/rc.subr', '. "'+str(work/'rc.subr')+'"')
src = src.replace('config_file="/usr/local/etc/rustproxy/opnsense.conf"', 'config_file="'+str(work/'opnsense.conf')+'"')
Path(sys.argv[2]).write_text(src)
PY
assert_interval() {
local val=$1
printf 'enabled="1"\nrustproxy_update_check_interval_secs="%s"\n' "$val" > "$WORK/opnsense.conf"
local args
args=$(sh "$WORK/rc-test" start)
[[ "$args" == *"--update-check-interval-secs $val serve"* ]] || {
echo "Wrong update interval arguments: $args" >&2; exit 1;
}
}
assert_interval 3600
assert_interval 7200
assert_interval 60
assert_interval 604800
printf 'enabled="1"\n' > "$WORK/opnsense.conf"
args=$(sh "$WORK/rc-test" start)
[[ "$args" == *'--update-check-interval-secs 3600 serve'* ]] || exit 1
# Exercise the rc.d soft reload itself with a live mock child and CLI.
mkdir -p "$WORK/bin"
cat > "$WORK/bin/mock-cli" <<'EOF'
#!/bin/sh
printf '%s\n' "$*" > "$TEST_CLI_ARGS"
EOF
chmod +x "$WORK/bin/mock-cli"
printf 'enabled="1"\nrustproxy_config_path="/usr/local/etc/rustproxy/config.json"\n' > "$WORK/opnsense.conf"
echo "$$" > "$WORK/rustproxy.pid"
export TEST_CLI_ARGS="$WORK/cli-args"
rustproxy_run_dir="$WORK" rustproxy_command="$WORK/bin/mock-cli" sh "$WORK/rc-test" reload
args=$(cat "$TEST_CLI_ARGS")
[[ "$args" == *'--data-dir /var/db/rustproxy'* ]]
[[ "$args" == *'--config /usr/local/etc/rustproxy/config.json'* ]]
[[ "$args" == *'--admin-listen 127.0.0.1:9090 reload'* ]]
printf 'enabled="1"\nrustproxy_config_path="/usr/local/etc/rustproxy/config.json;touch /tmp/bad"\n' > "$WORK/opnsense.conf"
if rustproxy_run_dir="$WORK" rustproxy_command="$WORK/bin/mock-cli" sh "$WORK/rc-test" reload >/dev/null 2>&1; then
echo 'Unsafe OPNsense config path was accepted' >&2; exit 1
fi
for invalid in 0 59 604801 nope '1;touch /tmp/pwn'; do
printf 'enabled="1"\nrustproxy_update_check_interval_secs="%s"\n' "$invalid" > "$WORK/opnsense.conf"
if sh "$WORK/rc-test" start >/dev/null 2>&1; then
echo "Interval '$invalid' should have failed" >&2; exit 1
fi
done
# Mock configd + rc.d so we can exercise the actual hook and service wrapper.
mkdir -p "$WORK/bin"
cat > "$WORK/bin/configctl" <<'EOF'
#!/bin/sh
printf 'template %s\n' "$*" >> "$TEST_LOG"
if [ "${FAIL_RENDER:-0}" = 1 ]; then
exit 2
fi
printf 'enabled="%s"\n' "${ENABLED:-1}" > "$TEST_CONFIG"
EOF
cat > "$WORK/bin/rustproxy-rc" <<'EOF'
#!/bin/sh
case "$1" in
status) [ -f "$TEST_PID" ] ;;
onestart) printf 'start\n' >> "$TEST_LOG"; touch "$TEST_PID" ;;
onestop) printf 'stop\n' >> "$TEST_LOG"; rm -f "$TEST_PID" ;;
reload) printf 'reload\n' >> "$TEST_LOG" ;;
*) exit 1 ;;
esac
EOF
chmod +x "$WORK/bin/configctl" "$WORK/bin/rustproxy-rc"
python3 - "$HOOK" "$SERVICE" "$WORK" <<'PY'
from pathlib import Path
import sys
hook = Path(sys.argv[1]).read_text()
service = Path(sys.argv[2]).read_text()
w = Path(sys.argv[3])
hook = hook.replace('/usr/local/sbin/configctl', str(w/'bin/configctl'))
hook = hook.replace('/usr/local/opnsense/scripts/rustproxy/service.sh', str(w/'service-test'))
service = service.replace('/usr/local/etc/rustproxy/opnsense.conf', str(w/'opnsense.conf'))
service = service.replace('/usr/local/etc/rc.d/rustproxy', str(w/'bin/rustproxy-rc'))
(w/'hook-test').write_text(hook)
(w/'service-test').write_text(service)
PY
chmod +x "$WORK/hook-test" "$WORK/service-test"
export TEST_LOG="$WORK/actions.log" TEST_CONFIG="$WORK/opnsense.conf" TEST_PID="$WORK/pidfile"
: > "$TEST_LOG"
ENABLED=1 "$WORK/hook-test"
[[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Boot did not start service' >&2; exit 1; }
ENABLED=1 "$WORK/hook-test"
[[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Duplicate supervisor start' >&2; exit 1; }
ENABLED=1 "$WORK/service-test" reload
[[ $(grep -cx reload "$TEST_LOG") == 1 ]] || { echo 'Soft reload not passed through' >&2; exit 1; }
[[ $(grep -cx stop "$TEST_LOG" || true) == 0 ]] || { echo 'Soft reload restarted service' >&2; exit 1; }
rm -f "$TEST_PID"
ENABLED=0 "$WORK/hook-test"
[[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Disabled service started' >&2; exit 1; }
if FAIL_RENDER=1 ENABLED=1 "$WORK/hook-test" >/dev/null 2>&1; then
echo 'Broken configuration reload was not reported' >&2; exit 1
fi
[[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Started despite failed template rendering' >&2; exit 1; }
echo 'OPNsense boot / update interval / soft reload checks: OK'
-79
View File
@@ -1,79 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
for f in "$ROOT"/start.sh "$ROOT"/scripts/*.sh "$ROOT"/tests/*.sh; do
bash -n "$f"
done
python3 -m py_compile "$ROOT/scripts/generate-opnsense-repo.py"
# Main image/build dependencies.
grep -q 'FROM rockylinux:9' "$ROOT/Dockerfile"
grep -q 'FREEBSD_RELEASE=15.1-RELEASE' "$ROOT/Dockerfile"
grep -q 'x86_64-unknown-freebsd' "$ROOT/Dockerfile"
grep -q '/opt/freebsd-sysroot' "$ROOT/Dockerfile"
# Coordinator uses one checkout but independent branches.
grep -q 'build-linux-release.sh' "$ROOT/scripts/build-release.sh"
grep -q 'build-opnsense-release.sh' "$ROOT/scripts/build-release.sh"
grep -q 'build-freebsd-release.sh' "$ROOT/scripts/build-release.sh"
grep -q 'package-freebsd.sh' "$ROOT/scripts/build-freebsd-release.sh"
grep -q 'FREEBSD_NATIVE_PACKAGING_REVISION=1' "$ROOT/scripts/lib.sh"
grep -Fq 'FREEBSD_NATIVE_ENABLED: "${FREEBSD_NATIVE_ENABLED:-0}"' "$ROOT/docker-compose.yml"
grep -Fq 'location /freebsd/' "$ROOT/http/nginx.conf"
grep -Fq 'FREEBSD_NATIVE_ENABLED=0' "$ROOT/.env.example"
grep -Fq 'ExecReload=/usr/bin/' "$ROOT/scripts/normalize-systemd-unit.py"
grep -q 'OPNsense branch will still run' "$ROOT/scripts/build-release.sh"
# Linux tests gate Linux packaging only.
grep -Fq 'cargo "+$LINUX_RUST_TOOLCHAIN" test --release' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'CGO_ENABLED=0 go test ./...' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'prepare-agent-package.sh' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'AGENT_DEB_FILE=' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'AGENT_RPM_FILE=' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'rustproxy-docker-agent' "$ROOT/scripts/generate-index.sh"
grep -Fq 'COPY --from=go-toolchain' "$ROOT/Dockerfile"
grep -q 'PACKAGING_REVISION=5' "$ROOT/scripts/lib.sh"
grep -Fq 'ensure-rust-toolchain.sh' "$ROOT/scripts/build-linux-release.sh"
grep -Fq 'ensure-rust-toolchain.sh' "$ROOT/scripts/build-opnsense.sh"
grep -Fq 'nfpm package' "$ROOT/scripts/build-linux-release.sh"
grep -q 'aptly .*publish' "$ROOT/scripts/build-linux-release.sh"
grep -q 'createrepo_c' "$ROOT/scripts/build-linux-release.sh"
[[ -x "$ROOT/tests/linux-path-access.sh" ]]
! grep -q 'cargo test' "$ROOT/scripts/build-opnsense-release.sh"
! grep -q 'cargo test' "$ROOT/scripts/build-opnsense.sh"
# FreeBSD/OPNsense branch.
grep -q 'build-opnsense.sh' "$ROOT/scripts/build-opnsense-release.sh"
grep -q 'package-opnsense.sh' "$ROOT/scripts/build-opnsense-release.sh"
grep -q 'generate-opnsense-repo.py' "$ROOT/scripts/build-opnsense-release.sh"
grep -q 'last_opnsense_commit' "$ROOT/scripts/build-opnsense-release.sh"
test -f "$ROOT/opnsense/plugin/src/etc/rc.d/rustproxy"
test -x "$ROOT/opnsense/plugin/src/etc/rc.syshook.d/start/50-rustproxy.start"
[[ ! -e "$ROOT/opnsense/plugin/src/etc/rc.syshook.d/10-rustproxy.start" ]]
grep -q 'OPNSENSE_PACKAGING_REVISION=4' "$ROOT/scripts/lib.sh"
test -f "$ROOT/opnsense/patches/0.20.0/0001-opnsense-build.patch"
# Per-branch state/retry logic.
grep -q 'last_linux_commit' "$ROOT/scripts/check-and-build.sh"
grep -q 'last_opnsense_commit' "$ROOT/scripts/check-and-build.sh"
grep -q 'BUILD_LINUX' "$ROOT/scripts/check-and-build.sh"
grep -q 'BUILD_OPNSENSE' "$ROOT/scripts/check-and-build.sh"
grep -q 'RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}' "$ROOT/scripts/entrypoint.sh"
grep -Fq 'RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}"' "$ROOT/docker-compose.yml"
# Two public endpoints from one nginx container.
grep -Fq '${REPOSITORY_URL:?REPOSITORY_URL is required}' "$ROOT/docker-compose.yml"
grep -Fq '${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}' "$ROOT/docker-compose.yml"
grep -Fq '0.0.0.0:${HTTP_PORT:-18765}:8080' "$ROOT/docker-compose.yml"
grep -Fq '0.0.0.0:${OPNSENSE_HTTP_PORT:-18766}:8081' "$ROOT/docker-compose.yml"
grep -q 'listen 8080' "$ROOT/http/nginx.conf"
grep -q 'listen 8081' "$ROOT/http/nginx.conf"
grep -q 'root /srv/repository/opnsense' "$ROOT/http/nginx.conf"
grep -q 'DEFAULT_HTTP_PORT=18765' "$ROOT/start.sh"
grep -q 'DEFAULT_OPNSENSE_HTTP_PORT=18766' "$ROOT/start.sh"
grep -q 'OPNSENSE_REPOSITORY_URL=https://opn-rustproxy.linuxiarz.pl' "$ROOT/.env.example"
grep -q 'RUN_LINUX_TESTS=1' "$ROOT/.env.example"
echo 'static checks: OK'
-157
View File
@@ -1,157 +0,0 @@
#!/usr/bin/env python3
"""Run: python3 -m unittest discover -s tests -p 'test_dashboard.py' -v"""
from __future__ import annotations
import importlib.util
import json
import os
import tempfile
import threading
import unittest
from datetime import datetime, timezone
from http.server import ThreadingHTTPServer
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("dashboard_app", ROOT / "dashboard" / "app.py")
app = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(app)
class RepoFixture:
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.repo = Path(self.temp.name) / "repository"
self.r1 = self.repo / "releases" / "r1"
self.r1.mkdir(parents=True)
(self.repo / "current").symlink_to("releases/r1")
def make_file(self, name, *, contents="package-bytes", timestamp=1735853760):
path = self.r1 / name
path.parent.mkdir(parents=True,exist_ok=True)
path.write_text(contents,encoding="utf-8")
os.utime(path,(timestamp,timestamp))
return path
def make_catalog(self):
self.make_file("apt/pool/r/rustproxy/rustproxy_0.34.6-2_amd64.deb")
self.make_file("apt/pool/r/rustproxy/rustproxy-docker-agent_0.34.6-2_amd64.deb")
self.make_file("rpm/x86_64/rustproxy-0.34.6-2.x86_64.rpm")
self.make_file("rpm/x86_64/rustproxy-docker-agent-0.34.6-2.x86_64.rpm")
self.make_file("opnsense/26.7/amd64/os-rustproxy-0.34.6_123.pkg")
self.make_file("opnsense/packagesite.pkg",contents="index")
self.make_file("opnsense/os-rustproxy-latest.pkg",contents="duplicate")
self.make_file("freebsd/FreeBSD:15:amd64/rustproxy-0.34.6_123.pkg")
self.make_file("freebsd/rustproxy-latest.pkg",contents="duplicate")
self.make_file("LINUX-BUILD-INFO.txt",contents="app_version=0.34.6\npackage_release=2\nsource_commit=abc123\nbuilt_at=2026-10-22T08:41:00Z\n")
self.make_file("opnsense/BUILD-INFO.txt",contents="app_version=0.34.6\npackage_version=0.34.6_123\nsource_commit=abc123\nbuilt_at=2026-10-22T08:43:00Z\n")
self.make_file("freebsd/BUILD-INFO.txt",contents="app_version=0.34.6\npackage_version=0.34.6_123\nsource_commit=abc123\nbuilt_at=2026-10-22T08:45:00Z\n")
class CatalogTests(RepoFixture, unittest.TestCase):
def test_scans_real_file_layout_without_index_duplicates(self):
self.make_catalog()
catalog = app.generate_catalog(self.repo)
self.assertEqual(6, catalog["summary"]["packages_count"])
self.assertEqual(3, catalog["summary"]["channels_published"])
self.assertEqual(4, catalog["summary"]["channel_counts"]["linux"])
self.assertEqual(1, catalog["summary"]["channel_counts"]["opnsense"])
self.assertEqual(1, catalog["summary"]["channel_counts"]["freebsd"])
self.assertEqual("0.34.6-2",catalog["builds"]["linux"]["package_version"])
self.assertEqual("2026-10-22T08:45:00Z",catalog["summary"]["latest_build_at"])
linux = [p for p in catalog["packages"] if p["format"] == "RPM"]
self.assertEqual({"rustproxy","rustproxy-docker-agent"}, {p["name"] for p in linux})
self.assertEqual("2026-10-22T08:41:00Z", linux[0]["built_at"])
self.assertEqual("build",linux[0]["date_source"])
opnsense = next(p for p in catalog["packages"] if p["channel"] == "opnsense")
self.assertTrue(opnsense["download_url"].endswith("/26.7/amd64/os-rustproxy-0.34.6_123.pkg"))
freebsd = next(p for p in catalog["packages"] if p["channel"] == "freebsd")
self.assertIn("/freebsd/FreeBSD%3A15%3Aamd64/",freebsd["download_url"])
self.assertEqual("2026-10-22T08:45:00Z",freebsd["built_at"])
def test_mtime_backwards_compatibility(self):
self.make_file("LINUX-BUILD-INFO.txt",contents="app_version=0.21.0\npackage_release=5\n")
self.make_file("rpm/x86_64/rustproxy-0.21.0-5.x86_64.rpm")
catalog = app.generate_catalog(self.repo)
self.assertEqual("file", catalog["builds"]["linux"]["date_source"])
self.assertEqual("file", catalog["packages"][0]["date_source"])
self.assertEqual("0.21.0-5",catalog["packages"][0]["version"])
self.assertTrue(catalog["packages"][0]["built_at"].endswith("Z"))
def test_symlink_rotation_gives_current_packages(self):
self.make_file("rpm/x86_64/rustproxy-0.1.0-1.x86_64.rpm")
v2 = self.repo / "releases" / "r2"
v2.mkdir()
(v2 / "rpm/x86_64").mkdir(parents=True)
(v2 / "rpm/x86_64/rustproxy-0.2.0-1.x86_64.rpm").write_bytes(b"new package")
before = app.generate_catalog(self.repo)
self.assertEqual("0.1.0-1",before["packages"][0]["version"])
(self.repo / "current.new").symlink_to("releases/r2")
os.replace(self.repo / "current.new",self.repo / "current")
after = app.generate_catalog(self.repo)
self.assertEqual("0.2.0-1",after["packages"][0]["version"])
def test_empty_repo_and_untrusted_symlink(self):
first = app.generate_catalog(self.repo)
self.assertFalse(first["ready"])
self.assertEqual([],first["packages"])
(self.repo / "current").unlink()
(self.repo / "current").symlink_to(self.temp.name) # outside repo root
second = app.generate_catalog(self.repo)
self.assertFalse(second["ready"])
self.assertEqual([],second["packages"])
def test_regexes_and_metadata_parser(self):
self.assertEqual(("rustproxy-docker-agent","0.34.6-2","x86_64"),app.parse_filename("rustproxy-docker-agent-0.34.6-2.x86_64.rpm","rpm"))
self.assertEqual(("os-rustproxy","0.34.6_123","amd64"),app.parse_filename("os-rustproxy-0.34.6_123.pkg","pkg"))
self.assertIsNone(app.parse_filename("packagesite.pkg","pkg"))
self.assertIsNone(app.parse_filename("rustproxy-latest.pkg","pkg"))
self.assertIsNone(app.parse_filename("repomd.xml","rpm"))
self.assertEqual({},app.parse_metadata(self.repo/"missing-info.txt"))
class HttpTests(RepoFixture, unittest.TestCase):
def setUp(self):
super().setUp()
self.make_catalog()
original_cache = app.CACHE
app.CACHE = app.CatalogCache(self.repo,ttl=1)
self.addCleanup(lambda: setattr(app,"CACHE",original_cache))
self.server = ThreadingHTTPServer(("127.0.0.1",0),app.DashboardHandler)
self.thread = threading.Thread(target=self.server.serve_forever,daemon=True)
self.thread.start()
self.addCleanup(self.server.server_close)
self.addCleanup(self.server.shutdown)
self.base = f"http://127.0.0.1:{self.server.server_port}"
def test_http_catalog_page_assets_head(self):
with urlopen(self.base + "/api/v1/packages") as response:
self.assertIn("no-store",response.headers["Cache-Control"])
catalog = json.load(response)
self.assertEqual(6,len(catalog["packages"]))
request = Request(self.base + "/",headers={"X-Repo-Site":"opnsense"})
with urlopen(request) as response:
html = response.read().decode()
self.assertIn('data-site="opnsense"',html)
self.assertIn("Package Registry",html)
self.assertIn("Content-Security-Policy",response.headers)
with urlopen(self.base + "/freebsd/") as response:
self.assertIn('data-site="freebsd"',response.read().decode())
with urlopen(self.base + "/ui/style.css") as response:
self.assertIn("text/css",response.headers["Content-Type"])
self.assertIn("--accent:#f69b74",response.read().decode())
request = Request(self.base + "/ui/app.js", method="HEAD")
with urlopen(request) as response:
self.assertEqual(200,response.status)
self.assertEqual(b"",response.read())
with self.assertRaises(HTTPError) as raised:
urlopen(self.base + "/../something")
self.assertEqual(404,raised.exception.code)
if __name__ == "__main__":
unittest.main()
-136
View File
@@ -1,136 +0,0 @@
#!/usr/bin/env bash
# Offline simulation of rustup installation/selection; no Rust or network required.
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/bin" "$WORK/source" "$WORK/state"
cat > "$WORK/bin/rustup" <<'MOCK'
#!/usr/bin/env bash
set -Eeuo pipefail
state=${MOCK_RUSTUP_STATE:?}
case "$1" in
run)
channel=$2
[[ -r "$state/$channel.version" ]] || exit 1
printf 'rustc %s (mock)\n' "$(cat "$state/$channel.version")"
;;
toolchain)
[[ "$2" == install ]] || exit 7
channel=$3
[[ ${MOCK_INSTALL_FAIL:-0} != 1 ]] || exit 9
case "$channel" in
stable) version=${MOCK_NEW_STABLE:-1.100.0} ;;
*) case "$channel" in *.*.*) version=$channel ;; *) version=${channel}.0 ;; esac ;;
esac
echo "$version" > "$state/$channel.version"
printf 'install %s\n' "$channel" >> "$state/calls"
;;
target)
action=$2
if [[ "$action" == list ]]; then
[[ "$3" == --toolchain && "$5" == --installed ]] || exit 7
[[ -f "$state/$4.target" ]] && cat "$state/$4.target"
elif [[ "$action" == add ]]; then
[[ "$4" == --toolchain ]] || exit 7
echo "$3" > "$state/$5.target"
printf 'target %s %s\n' "$5" "$3" >> "$state/calls"
else exit 7
fi
;;
*) printf 'Unexpected rustup arguments: %s\n' "$*" >&2; exit 7 ;;
esac
MOCK
chmod +x "$WORK/bin/rustup"
export PATH="$WORK/bin:$PATH" MOCK_RUSTUP_STATE="$WORK/state" TOML_PYTHON=python3
TOOL="$ROOT/scripts/ensure-rust-toolchain.sh"
reset_state() { rm -f "$WORK/state/"*; }
manifest() { printf '[package]\nname = "rust-proxy"\nversion = "0.31.2"\nrust-version = "%s"\n' "$1" > "$WORK/source/Cargo.toml"; rm -f "$WORK/source/rust-toolchain" "$WORK/source/rust-toolchain.toml"; }
# No update or download when installed stable already satisfies the manifest.
reset_state
manifest 1.99
printf '1.100.0\n' > "$WORK/state/stable.version"
result=$($TOOL "$WORK/source" linux)
[[ "$result" == stable ]]
[[ ! -e "$WORK/state/calls" ]]
# Upgrade stable only when too old, then reuse it on subsequent checks.
reset_state
printf '1.98.1\n' > "$WORK/state/stable.version"
result=$($TOOL "$WORK/source" linux)
[[ "$result" == stable ]]
grep -Fqx 'install stable' "$WORK/state/calls"
[[ $($TOOL "$WORK/source" linux) == stable ]]
[[ $(wc -l < "$WORK/state/calls") == 1 ]]
# FreeBSD default 1.88 is auto raised to the project's 1.99 MSRV with its own target.
reset_state
result=$(FREEBSD_RUST_TOOLCHAIN=1.88.0 "$TOOL" "$WORK/source" freebsd)
[[ "$result" == 1.99 ]]
grep -Fqx 'install 1.99' "$WORK/state/calls"
grep -Fqx 'target 1.99 x86_64-unknown-freebsd' "$WORK/state/calls"
[[ $(FREEBSD_RUST_TOOLCHAIN=1.88.0 "$TOOL" "$WORK/source" freebsd) == 1.99 ]]
[[ $(wc -l < "$WORK/state/calls") == 2 ]]
# A deliberate rust-toolchain.toml pin is respected; do not silently override it.
reset_state
cat > "$WORK/source/rust-toolchain.toml" <<'PIN'
[toolchain]
channel = "1.98.0"
PIN
if "$TOOL" "$WORK/source" linux >/dev/null 2>&1; then
printf 'An incompatible explicit pin should have failed\n' >&2
exit 1
fi
[[ ! -e "$WORK/state/calls" ]] || { echo 'Should not install an incompatible pin' >&2; exit 1; }
rm "$WORK/source/rust-toolchain.toml"
# Pin that meets the project requirement is installed without changing stable.
reset_state
cat > "$WORK/source/rust-toolchain.toml" <<'PIN'
[toolchain]
channel = "1.99.0"
PIN
[[ $($TOOL "$WORK/source" linux) == 1.99.0 ]]
grep -Fqx 'install 1.99.0' "$WORK/state/calls"
rm "$WORK/source/rust-toolchain.toml"
# RustProxy-like rust-toolchain.toml (stable) also upgrades stale stable for FreeBSD.
reset_state
manifest 1.99
cat > "$WORK/source/rust-toolchain.toml" <<'PIN'
[toolchain]
channel = "stable"
profile = "minimal"
components = ["rustfmt", "clippy"]
PIN
printf '1.98.1\n' > "$WORK/state/stable.version"
[[ $(FREEBSD_RUST_TOOLCHAIN=1.88.0 "$TOOL" "$WORK/source" freebsd) == stable ]]
grep -Fqx 'install stable' "$WORK/state/calls"
grep -Fqx 'target stable x86_64-unknown-freebsd' "$WORK/state/calls"
rm "$WORK/source/rust-toolchain.toml"
# Workspace-inherited rust-version is resolved through TOML, not grep.
reset_state
cat > "$WORK/source/Cargo.toml" <<'MANIFEST'
[workspace.package]
rust-version = "1.99"
[package]
name = "rust-proxy"
version = "0.31.2"
rust-version = { workspace = true }
MANIFEST
printf '1.100.0\n' > "$WORK/state/stable.version"
[[ $($TOOL "$WORK/source" linux) == stable ]]
# An unavailable upgrade must be a hard failure.
reset_state
manifest 1.99
printf '1.98.1\n' > "$WORK/state/stable.version"
if MOCK_INSTALL_FAIL=1 "$TOOL" "$WORK/source" linux >/dev/null 2>&1; then
printf 'A failed rustup update should abort the build\n' >&2
exit 1
fi
echo 'Rust toolchain selection: 8 offline scenarios OK'