#!/bin/sh set -eu lab=${1:?pass an absolute prepared lab directory} case "$lab" in /*) ;; *) echo 'absolute directory required' >&2; exit 1;; esac [ "$(uname -s)" = Linux ] || { echo 'Linux required' >&2; exit 1; } # The Docker daemon may remap container root. This directory contains only # disposable synthetic credentials, never production exports. chmod 755 "$lab" chmod 644 "$lab"/*.json "$lab"/*.pem "$lab"/*.py "$lab"/*.cfg prefix=vastora-recovery-lab-$$ network=$prefix-network xr=ghcr.io/xtls/xray-core:26.7.28@sha256:b697cda1588faca696ab7f7755dd1161f60862af3ff6026300e44cff6aedd558 py=python@sha256:399babc8b49529dabfd9c922f2b5eea81d611e4512e3ed250d75bd2e7683f4b0 ha=docker.io/library/haproxy:3.2.7-alpine@sha256:3b80483d47e1c7d1fc7eb4b9104f33d9a51259769be299eb675524dca2bc8157 # Fail before taking ownership if any target already exists. for role in witness landing entry client gateway probe; do if docker container inspect "$prefix-$role" >/dev/null 2>&1; then echo 'resource collision' >&2; exit 1; fi done if docker network inspect "$network" >/dev/null 2>&1; then echo 'network collision' >&2; exit 1; fi cleanup() { status=$? if [ "$status" -ne 0 ]; then for role in probe client gateway entry landing witness; do docker logs "$prefix-$role" >"$lab/$role.log" 2>&1 || true chmod 600 "$lab/$role.log" done fi for role in probe client gateway entry landing witness; do docker rm -f "$prefix-$role" >/dev/null 2>&1 || true; done docker network rm "$network" >/dev/null 2>&1 || true } # No host ports, host mounts outside this fixture, private-controller changes, # privileged containers or production configuration are involved. docker network create --internal --subnet 192.168.241.0/24 --label vastora.recovery-lab=true "$network" >/dev/null trap cleanup EXIT INT TERM run() { run_role=$1; run_address=$2; run_image=$3; shift 3 docker run -d --name "$prefix-$run_role" --label vastora.recovery-lab=true --network "$network" --ip "$run_address" --user 0 --cpus 1 --memory 128m --pids-limit 64 --cap-drop ALL --security-opt no-new-privileges:true --read-only --mount "type=bind,src=$lab,dst=/lab,readonly" "$run_image" "$@" >/dev/null } run witness 192.168.241.4 "$py" python /lab/witness.py run landing 192.168.241.5 "$xr" run -c /lab/landing.json for phase in initial replacement; do address=192.168.241.2 if [ "$phase" = replacement ]; then # Remove only the owned runtime. Reconstruct on a different machine address # without rewriting the saved native/fixed client credentials. docker rm -f "$prefix-gateway" "$prefix-entry" >/dev/null address=192.168.241.7 fi run entry "$address" "$xr" run -c "/lab/server-$phase.json" run gateway 192.168.241.6 "$ha" haproxy -f "/lab/gateway-$phase.cfg" for kind in native fixed invalid; do run client 192.168.241.3 "$xr" run -c "/lab/$kind.json" expected=$address if [ "$kind" = fixed ]; then expected=192.168.241.5; fi docker run --rm --name "$prefix-probe" --label vastora.recovery-lab=true --network "container:$prefix-client" --user 0 --cpus 1 --memory 128m --pids-limit 32 --cap-drop ALL --security-opt no-new-privileges:true --read-only --mount "type=bind,src=$lab,dst=/lab,readonly" "$py" python /lab/probe.py "$phase" "$kind" "$expected" docker rm -f "$prefix-client" >/dev/null done done