feat: harden docker runtime validation

This commit is contained in:
thePR0M3TH3AN committed 2026-03-19 22:30:28 -04:00
1 parent 004cce2a4f
commit 83e9e3d511
32 files changed
+1360 -40

No files matched your search

+4
View File
@@ -21,6 +21,10 @@ python3 -m voxvera.cli --lang en check
if [[ "$(uname -s)" == "Linux" ]]; then
echo "==> platform smoke"
bash scripts/platform-smoke.sh linux-cli
if command -v docker >/dev/null 2>&1; then
echo "==> docker runtime smoke"
bash scripts/docker-runtime-smoke.sh
fi
fi
echo "==> build docs"
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
VOXVERA_DIR="${VOXVERA_DIR:-/flyers}"
VOXVERA_CONFIG_PATH="${VOXVERA_CONFIG_PATH:-$VOXVERA_DIR/config.json}"
VOXVERA_START_INTERVAL="${VOXVERA_START_INTERVAL:-300}"
VOXVERA_BOOTSTRAP_DEFAULT_SITE="${VOXVERA_BOOTSTRAP_DEFAULT_SITE:-1}"
TOR_SOCKS_PORT="${TOR_SOCKS_PORT:-9050}"
TOR_CONTROL_PORT="${TOR_CONTROL_PORT:-9051}"
VOXVERA_TOR_DIR="${VOXVERA_TOR_DIR:-/tmp/voxvera-tor}"
TOR_PID=""
SLEEP_PID=""
log() {
printf '[voxvera-docker] %s\n' "$*"
}
cleanup() {
if [[ -n "$SLEEP_PID" ]] && kill -0 "$SLEEP_PID" 2>/dev/null; then
kill "$SLEEP_PID" 2>/dev/null || true
fi
voxvera stop-all >/dev/null 2>&1 || true
if [[ -n "$TOR_PID" ]] && kill -0 "$TOR_PID" 2>/dev/null; then
kill "$TOR_PID" 2>/dev/null || true
wait "$TOR_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
wait_for_port() {
local host="$1"
local port="$2"
python3 - "$host" "$port" <<'PY'
import socket
import sys
import time
host = sys.argv[1]
port = int(sys.argv[2])
deadline = time.time() + 60
while time.time() < deadline:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
if sock.connect_ex((host, port)) == 0:
sys.exit(0)
time.sleep(1)
sys.exit(1)
PY
}
has_site_configs() {
find "$VOXVERA_DIR/host" -mindepth 2 -maxdepth 2 -name config.json -print -quit 2>/dev/null | grep -q .
}
seed_default_config() {
mkdir -p "$VOXVERA_DIR"
if [[ ! -f "$VOXVERA_CONFIG_PATH" ]]; then
cp /opt/voxvera/voxvera/src/config.json "$VOXVERA_CONFIG_PATH"
log "Seeded default config at $VOXVERA_CONFIG_PATH"
fi
}
bootstrap_default_site_if_needed() {
if [[ "$VOXVERA_BOOTSTRAP_DEFAULT_SITE" != "1" ]]; then
return
fi
if has_site_configs; then
return
fi
log "No sites found under $VOXVERA_DIR/host; bootstrapping default site"
voxvera --config "$VOXVERA_CONFIG_PATH" init --non-interactive
}
start_tor() {
mkdir -p "$VOXVERA_TOR_DIR"
chmod 700 "$VOXVERA_TOR_DIR"
cat >"$VOXVERA_TOR_DIR/torrc" <<EOF
DataDirectory $VOXVERA_TOR_DIR/data
SocksPort 127.0.0.1:$TOR_SOCKS_PORT
ControlPort 127.0.0.1:$TOR_CONTROL_PORT
CookieAuthentication 0
AvoidDiskWrites 1
Log notice stdout
EOF
mkdir -p "$VOXVERA_TOR_DIR/data"
log "Starting Tor on 127.0.0.1:$TOR_SOCKS_PORT / $TOR_CONTROL_PORT"
tor -f "$VOXVERA_TOR_DIR/torrc" &
TOR_PID="$!"
wait_for_port 127.0.0.1 "$TOR_SOCKS_PORT"
wait_for_port 127.0.0.1 "$TOR_CONTROL_PORT"
log "Tor ports are reachable"
}
start_loop() {
export TOR_SOCKS_PORT TOR_CONTROL_PORT
while true; do
if ! kill -0 "$TOR_PID" 2>/dev/null; then
log "Tor exited unexpectedly"
wait "$TOR_PID"
exit 1
fi
log "Running voxvera start-all"
voxvera start-all || true
sleep "$VOXVERA_START_INTERVAL" &
SLEEP_PID="$!"
wait "$SLEEP_PID"
SLEEP_PID=""
done
}
seed_default_config
start_tor
bootstrap_default_site_if_needed
start_loop
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
VOXVERA_DIR="${VOXVERA_DIR:-/flyers}"
TMP_JSON="$(mktemp)"
voxvera doctor --json >"$TMP_JSON"
python3 - "$TMP_JSON" "$VOXVERA_DIR" <<'PY'
import json
import sys
from pathlib import Path
doctor = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
voxvera_dir = Path(sys.argv[2])
checks = {check["name"]: check for check in doctor.get("checks", [])}
required = ("voxvera_cli", "onionshare_cli", "tor_socks_reachable", "host_root")
missing = []
failing = []
for name in required:
check = checks.get(name)
if not check:
missing.append(name)
continue
if not check.get("ok"):
failing.append(f"{name}: {check.get('detail', '')}")
site_configs = list((voxvera_dir / "host").glob("*/config.json"))
running_sites = [cfg.parent for cfg in site_configs if (cfg.parent / "server.pid").exists()]
if missing or failing or not site_configs or not running_sites:
if missing:
print("Missing checks: " + ", ".join(missing), file=sys.stderr)
if failing:
print("Failing checks: " + "; ".join(failing), file=sys.stderr)
if not site_configs:
print("No configured sites found under host/", file=sys.stderr)
if site_configs and not running_sites:
print("Configured sites exist but no server.pid files are present", file=sys.stderr)
sys.exit(1)
print("healthy")
PY
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
if ! command -v docker >/dev/null 2>&1; then
echo "docker is not installed; skipping Docker runtime smoke"
exit 0
fi
IMAGE_TAG="${VOXVERA_DOCKER_SMOKE_TAG:-voxvera:smoke}"
CONTAINER_NAME="${VOXVERA_DOCKER_SMOKE_CONTAINER:-voxvera-smoke}"
TMP_DIR="${VOXVERA_DOCKER_SMOKE_TMPDIR:-$(mktemp -d /tmp/voxvera-docker-smoke-XXXXXX)}"
FLYERS_DIR="$TMP_DIR/flyers"
mkdir -p "$FLYERS_DIR"
cleanup() {
docker logs "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
if [[ -z "${VOXVERA_DOCKER_SMOKE_TMPDIR:-}" ]]; then
rm -rf "$TMP_DIR"
fi
}
trap cleanup EXIT
echo "==> building Docker image"
docker build -t "$IMAGE_TAG" .
echo "==> starting Docker container"
docker run -d \
--name "$CONTAINER_NAME" \
--restart no \
-e VOXVERA_START_INTERVAL=15 \
-v "$FLYERS_DIR:/flyers" \
"$IMAGE_TAG" >/dev/null
echo "==> waiting for healthy container"
deadline=$((SECONDS + 240))
while true; do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$CONTAINER_NAME")"
case "$status" in
healthy)
break
;;
unhealthy)
echo "Container became unhealthy" >&2
docker logs "$CONTAINER_NAME" >&2 || true
exit 1
;;
esac
if (( SECONDS >= deadline )); then
echo "Timed out waiting for healthy Docker container" >&2
docker logs "$CONTAINER_NAME" >&2 || true
exit 1
fi
sleep 5
done
PLATFORM_JSON="$TMP_DIR/platform-status.json"
DOCTOR_JSON="$TMP_DIR/doctor.json"
docker exec "$CONTAINER_NAME" voxvera platform-status --json >"$PLATFORM_JSON"
docker exec "$CONTAINER_NAME" voxvera doctor --json >"$DOCTOR_JSON"
PLATFORM_JSON="$PLATFORM_JSON" \
DOCTOR_JSON="$DOCTOR_JSON" \
FLYERS_DIR="$FLYERS_DIR" \
python3 - <<'PY'
import json
import os
import sys
from pathlib import Path
platform_status = json.loads(Path(os.environ["PLATFORM_JSON"]).read_text(encoding="utf-8"))
doctor = json.loads(Path(os.environ["DOCTOR_JSON"]).read_text(encoding="utf-8"))
flyers_dir = Path(os.environ["FLYERS_DIR"])
errors = []
if platform_status.get("platform_id") != "docker_cli":
errors.append(f"platform_id mismatch: {platform_status.get('platform_id')}")
if platform_status.get("tier") != "experimental":
errors.append(f"tier mismatch: {platform_status.get('tier')}")
checks = {check["name"]: check for check in doctor.get("checks", [])}
for required in ("container_marker", "docker_entrypoint", "docker_healthcheck", "tor_socks_reachable", "host_root"):
check = checks.get(required)
if not check:
errors.append(f"missing doctor check: {required}")
elif not check.get("ok"):
errors.append(f"doctor check failed: {required} ({check.get('detail')})")
site_dir = flyers_dir / "host" / "voxvera"
for path in (flyers_dir / "config.json", site_dir / "config.json", site_dir / "index.html", site_dir / ".onionshare-session", site_dir / "server.pid"):
if not path.exists():
errors.append(f"missing expected Docker runtime artifact: {path}")
if errors:
print("Docker runtime smoke failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
sys.exit(1)
print("Docker runtime smoke OK")
print(f"Platform: {platform_status['label']} [{platform_status['tier']}]")
print(f"Hosting model: {platform_status.get('hosting_model', '')}")
PY
+2 -2
View File
@@ -36,8 +36,8 @@ case "$TARGET" in
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
;;
docker-cli)
EXPECTED_PLATFORM_ID="linux_cli_systemd"
EXPECTED_TIER="${VOXVERA_EXPECTED_TIER_OVERRIDE:-supported}"
EXPECTED_PLATFORM_ID="docker_cli"
EXPECTED_TIER="${VOXVERA_EXPECTED_TIER_OVERRIDE:-experimental}"
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
;;
list)