platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -0,0 +1,687 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_MANIFEST="${SCRIPT_DIR}/manifest.example.json"
DEFAULT_SOURCE_POLICY="${SCRIPT_DIR}/source_policy.production.json"
MANIFEST="${DEFAULT_MANIFEST}"
BIN_ROOT="/opt/selective-vpn/bin"
TARGET=""
COMPONENTS_RAW=""
SOURCE_POLICY=""
SIGNATURE_MODE_OVERRIDE=""
ROLLOUT_STAGE="stable"
COHORT_ID=""
FORCE_ROLLOUT=0
DRY_RUN=0
usage() {
cat <<'EOF'
Usage:
update.sh [--manifest PATH] [--bin-root DIR] [--target OS-ARCH] [--component NAME[,NAME...]]
[--source-policy PATH] [--signature-mode off|optional|required]
[--rollout-stage stable|canary|any] [--cohort-id 0..99] [--force-rollout]
[--canary] [--dry-run]
Description:
Manual pinned updater for transport companion binaries in runtime_mode=exec.
Reads versions/urls/checksums from manifest and atomically switches active symlinks in BIN_ROOT.
Examples:
./scripts/transport-packaging/update.sh --manifest ./manifest.json --component singbox
./scripts/transport-packaging/update.sh --manifest ./manifest.json --target linux-amd64 --dry-run
./scripts/transport-packaging/update.sh --manifest ./manifest.production.json --source-policy ./source_policy.production.json
./scripts/transport-packaging/update.sh --manifest /path/to/manifest-with-canary.json --rollout-stage canary
EOF
}
normalize_os() {
local raw="$1"
raw="$(echo "$raw" | tr '[:upper:]' '[:lower:]')"
case "$raw" in
linux) echo "linux" ;;
darwin) echo "darwin" ;;
*) echo "$raw" ;;
esac
}
normalize_arch() {
local raw="$1"
raw="$(echo "$raw" | tr '[:upper:]' '[:lower:]')"
case "$raw" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l|armv7) echo "armv7" ;;
*) echo "$raw" ;;
esac
}
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "[transport-update] missing required command: ${cmd}" >&2
exit 1
fi
}
history_append_if_changed() {
local history_file="$1"
local line="$2"
local target="$3"
local last_target=""
if [[ -f "$history_file" ]]; then
last_target="$(tail -n1 "$history_file" | awk -F'|' '{print $4}')"
fi
if [[ "$last_target" == "$target" ]]; then
return 0
fi
mkdir -p "$(dirname "$history_file")"
echo "$line" >> "$history_file"
}
trim_spaces() {
echo "$1" | xargs
}
normalize_signature_mode() {
local raw
raw="$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)"
case "$raw" in
""|off|optional|required) echo "$raw" ;;
*)
echo "[transport-update] invalid signature mode: ${raw}" >&2
exit 1
;;
esac
}
compute_cohort_id() {
local component="$1"
local target="$2"
local seed=""
if [[ -r /etc/machine-id ]]; then
seed="$(tr -d '\n' </etc/machine-id)"
fi
if [[ -z "$seed" ]]; then
seed="$(hostname 2>/dev/null || uname -n || echo "unknown-host")"
fi
local input="${seed}:${component}:${target}"
local hash
hash="$(printf '%s' "$input" | sha256sum | awk '{print $1}')"
printf '%d' $((16#${hash:0:8} % 100))
}
evaluate_policy_and_signature_mode() {
local component="$1"
local url="$2"
local sig_url="$3"
local sig_type="$4"
python3 - "$SOURCE_POLICY" "$component" "$url" "$sig_url" "$sig_type" "$SIGNATURE_MODE_OVERRIDE" <<'PY'
import json
import sys
from urllib.parse import urlparse
policy_path, component, asset_url, sig_url, sig_type_raw, mode_override_raw = sys.argv[1:]
component = component.strip()
asset_url = asset_url.strip()
sig_url = sig_url.strip()
sig_type = sig_type_raw.strip().lower()
mode_override = mode_override_raw.strip().lower()
def as_bool(value, default=False):
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
raw = value.strip().lower()
if raw in {"1", "true", "yes", "on"}:
return True
if raw in {"0", "false", "no", "off"}:
return False
return default
def as_list(value):
if value is None:
return []
if isinstance(value, list):
return [str(v).strip() for v in value if str(v).strip()]
if isinstance(value, str):
raw = value.strip()
return [raw] if raw else []
raise SystemExit("policy list value has unsupported type")
def fail(msg):
raise SystemExit(msg)
def normalize_mode(raw):
raw = (raw or "").strip().lower()
if raw == "":
return ""
if raw not in {"off", "optional", "required"}:
fail(f"signature mode must be off|optional|required (got {raw})")
return raw
policy = {}
if not policy_path:
mode = normalize_mode(mode_override) or "off"
print(mode)
raise SystemExit(0)
if policy_path:
with open(policy_path, "r", encoding="utf-8") as f:
policy = json.load(f)
if not isinstance(policy, dict):
fail("source policy must be a JSON object")
components_cfg = policy.get("components", {})
if components_cfg is None:
components_cfg = {}
if not isinstance(components_cfg, dict):
fail("policy.components must be an object")
component_cfg = components_cfg.get(component, {})
if component_cfg is None:
component_cfg = {}
if not isinstance(component_cfg, dict):
fail(f"policy.components.{component} must be an object")
require_https = as_bool(policy.get("require_https", True), default=True)
allow_file_scheme = as_bool(policy.get("allow_file_scheme", False), default=False)
allowed_schemes = [s.lower() for s in as_list(policy.get("allowed_schemes"))]
default_hosts = set(as_list(policy.get("default_allowed_hosts")))
default_prefixes = as_list(policy.get("default_allowed_url_prefixes"))
component_hosts = set(as_list(component_cfg.get("allowed_hosts")))
component_prefixes = as_list(component_cfg.get("allowed_url_prefixes"))
hosts = component_hosts if component_hosts else default_hosts
prefixes = component_prefixes if component_prefixes else default_prefixes
def validate_url(kind, raw_url):
raw_url = raw_url.strip()
if not raw_url:
return
parsed = urlparse(raw_url)
scheme = (parsed.scheme or "").lower()
if not scheme:
fail(f"{kind} URL is missing scheme: {raw_url}")
if scheme == "file":
if not allow_file_scheme:
fail(f"{kind} URL uses file:// but policy allow_file_scheme=false: {raw_url}")
return
if allowed_schemes and scheme not in allowed_schemes:
fail(f"{kind} URL scheme {scheme} is not in policy.allowed_schemes")
if require_https and scheme != "https":
fail(f"{kind} URL must use https:// by policy: {raw_url}")
host = (parsed.hostname or "").lower()
if hosts and host not in {h.lower() for h in hosts}:
fail(f"{kind} URL host {host} is not trusted for component {component}")
if prefixes and not any(raw_url.startswith(prefix) for prefix in prefixes):
fail(f"{kind} URL is not in trusted prefixes for component {component}")
validate_url("asset", asset_url)
if sig_url:
validate_url("signature", sig_url)
sig_cfg = policy.get("signature", {})
if sig_cfg is None:
sig_cfg = {}
if not isinstance(sig_cfg, dict):
fail("policy.signature must be an object")
allowed_sig_types = [s.lower() for s in as_list(sig_cfg.get("allowed_types"))]
if sig_type and allowed_sig_types and sig_type not in allowed_sig_types:
fail(f"signature type {sig_type} is not allowed by policy")
mode = normalize_mode(mode_override)
if not mode:
mode = normalize_mode(component_cfg.get("signature_mode", ""))
if not mode:
mode = normalize_mode(sig_cfg.get("default_mode", "off")) or "off"
print(mode)
PY
}
manifest_rows() {
python3 - "$MANIFEST" "$TARGET" "$COMPONENTS_RAW" <<'PY'
import json
import sys
manifest_path, target_key, components_raw = sys.argv[1], sys.argv[2], sys.argv[3]
with open(manifest_path, "r", encoding="utf-8") as f:
doc = json.load(f)
components = doc.get("components")
if not isinstance(components, dict):
raise SystemExit("manifest.components must be an object")
wanted = set()
if components_raw.strip():
for part in components_raw.split(","):
part = part.strip()
if part:
wanted.add(part)
def fail(msg: str):
raise SystemExit(msg)
seen = set()
rows = []
for name, meta in sorted(components.items()):
if wanted and name not in wanted:
continue
if not isinstance(meta, dict):
fail(f"component {name} config must be an object")
enabled = bool(meta.get("enabled", True))
if not enabled:
continue
binary_name = str(meta.get("binary_name", "")).strip()
if not binary_name:
fail(f"component {name} missing binary_name")
targets = meta.get("targets")
if not isinstance(targets, dict):
fail(f"component {name} missing targets map")
rec = targets.get(target_key)
if rec is None:
fail(f"component {name} has no target {target_key}")
if not isinstance(rec, dict):
fail(f"component {name} target {target_key} must be object")
version = str(rec.get("version", "")).strip()
url = str(rec.get("url", "")).strip()
sha256 = str(rec.get("sha256", "")).strip().lower()
asset_type = str(rec.get("asset_type", "raw")).strip().lower()
asset_binary_path = str(rec.get("asset_binary_path", "")).strip()
rollout = rec.get("rollout")
if rollout is None:
rollout = {}
if not isinstance(rollout, dict):
fail(f"component {name} target {target_key} rollout must be object")
rollout_stage = str(rollout.get("stage", "stable")).strip().lower()
rollout_percent_raw = rollout.get("percent", 100)
try:
rollout_percent = int(rollout_percent_raw)
except Exception:
fail(f"component {name} target {target_key} rollout.percent must be int")
sig = rec.get("signature")
if sig is None:
sig = {}
if not isinstance(sig, dict):
fail(f"component {name} target {target_key} signature must be object")
sig_type = str(sig.get("type", "")).strip().lower()
sig_url = str(sig.get("url", "")).strip()
sig_sha256 = str(sig.get("sha256", "")).strip().lower()
sig_public_key_path = str(sig.get("public_key_path", "")).strip()
if not version:
fail(f"component {name} target {target_key} missing version")
if not url:
fail(f"component {name} target {target_key} missing url")
if len(sha256) != 64 or any(c not in "0123456789abcdef" for c in sha256):
fail(f"component {name} target {target_key} has invalid sha256")
if asset_type not in ("raw", "tar.gz", "zip"):
fail(f"component {name} target {target_key} has unsupported asset_type={asset_type}")
if asset_type in ("tar.gz", "zip") and not asset_binary_path:
fail(f"component {name} target {target_key} requires asset_binary_path for {asset_type}")
if rollout_stage not in ("stable", "canary"):
fail(f"component {name} target {target_key} rollout.stage must be stable|canary")
if rollout_percent < 0 or rollout_percent > 100:
fail(f"component {name} target {target_key} rollout.percent must be 0..100")
if sig_type and not sig_url:
fail(f"component {name} target {target_key} signature.url is required when signature.type is set")
if sig_sha256:
if len(sig_sha256) != 64 or any(c not in "0123456789abcdef" for c in sig_sha256):
fail(f"component {name} target {target_key} signature.sha256 is invalid")
if sig_public_key_path and not sig_type:
fail(f"component {name} target {target_key} signature.type is required when signature.public_key_path is set")
rows.append((
name, binary_name, version, url, sha256, asset_type, asset_binary_path,
rollout_stage, str(rollout_percent), sig_type, sig_url, sig_sha256, sig_public_key_path
))
seen.add(name)
if wanted:
missing = sorted(wanted - seen)
if missing:
fail("missing/enabled=false components in manifest: " + ",".join(missing))
for row in rows:
print("\x1f".join(row))
PY
}
verify_asset_signature() {
local component="$1"
local asset_path="$2"
local tmp_dir="$3"
local sig_mode="$4"
local sig_type="$5"
local sig_url="$6"
local sig_sha256="$7"
local sig_public_key_path="$8"
if [[ "$sig_mode" == "off" ]]; then
return 0
fi
if [[ -z "$sig_type" || -z "$sig_url" || -z "$sig_public_key_path" ]]; then
if [[ "$sig_mode" == "required" ]]; then
echo "[transport-update] ${component}: signature is required, but signature fields are incomplete" >&2
return 1
fi
echo "[transport-update] WARN ${component}: signature_mode=${sig_mode}, signature metadata is incomplete, skip signature check"
return 0
fi
if [[ ! -f "$sig_public_key_path" ]]; then
if [[ "$sig_mode" == "required" ]]; then
echo "[transport-update] ${component}: signature public key not found: ${sig_public_key_path}" >&2
return 1
fi
echo "[transport-update] WARN ${component}: signature public key not found: ${sig_public_key_path}, skip signature check"
return 0
fi
case "$sig_type" in
openssl-sha256)
require_cmd openssl
;;
*)
if [[ "$sig_mode" == "required" ]]; then
echo "[transport-update] ${component}: unsupported signature type: ${sig_type}" >&2
return 1
fi
echo "[transport-update] WARN ${component}: unsupported signature type: ${sig_type}, skip signature check"
return 0
;;
esac
local sig_file="${tmp_dir}/asset.sig"
echo "[transport-update] ${component}: downloading signature ${sig_url}"
curl -fsSL "$sig_url" -o "$sig_file"
if [[ -n "$sig_sha256" ]]; then
echo "${sig_sha256} ${sig_file}" | sha256sum -c - >/dev/null
echo "[transport-update] ${component}: signature checksum ok"
fi
if ! openssl dgst -sha256 -verify "$sig_public_key_path" -signature "$sig_file" "$asset_path" >/dev/null 2>&1; then
echo "[transport-update] ${component}: signature verification failed (${sig_type})" >&2
return 1
fi
echo "[transport-update] ${component}: signature verified (${sig_type})"
}
download_install_binary() {
local component="$1"
local binary_name="$2"
local version="$3"
local url="$4"
local sha256="$5"
local asset_type="$6"
local asset_binary_path="$7"
local sig_mode="$8"
local sig_type="$9"
local sig_url="${10}"
local sig_sha256="${11}"
local sig_public_key_path="${12}"
local release_dir="${13}"
local release_binary="${14}"
if [[ -x "$release_binary" ]]; then
echo "[transport-update] ${component}: release already present ${release_binary}"
return 0
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[transport-update] DRY-RUN ${component}: download ${url} -> ${release_binary}"
if [[ "$sig_mode" != "off" ]]; then
echo "[transport-update] DRY-RUN ${component}: signature_mode=${sig_mode} signature_type=${sig_type:-none}"
fi
return 0
fi
local tmp_dir
tmp_dir="$(mktemp -d)"
local asset="${tmp_dir}/asset"
local unpack="${tmp_dir}/unpack"
mkdir -p "$release_dir" "$unpack"
echo "[transport-update] ${component}: downloading ${url}"
curl -fsSL "$url" -o "$asset"
echo "${sha256} ${asset}" | sha256sum -c - >/dev/null
echo "[transport-update] ${component}: checksum ok"
verify_asset_signature "$component" "$asset" "$tmp_dir" "$sig_mode" "$sig_type" "$sig_url" "$sig_sha256" "$sig_public_key_path"
local source_binary=""
case "$asset_type" in
raw)
source_binary="$asset"
;;
tar.gz)
require_cmd tar
tar -xzf "$asset" -C "$unpack"
source_binary="${unpack}/${asset_binary_path}"
;;
zip)
require_cmd unzip
unzip -q "$asset" -d "$unpack"
source_binary="${unpack}/${asset_binary_path}"
;;
*)
rm -rf "$tmp_dir"
echo "[transport-update] ${component}: unsupported asset_type ${asset_type}" >&2
return 1
;;
esac
if [[ ! -f "$source_binary" ]]; then
rm -rf "$tmp_dir"
echo "[transport-update] ${component}: binary not found in asset: ${source_binary}" >&2
return 1
fi
install -m 0755 "$source_binary" "$release_binary"
rm -rf "$tmp_dir"
echo "[transport-update] ${component}: installed ${release_binary}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--manifest)
MANIFEST="${2:-}"
shift 2
;;
--bin-root)
BIN_ROOT="${2:-}"
shift 2
;;
--target)
TARGET="${2:-}"
shift 2
;;
--component)
COMPONENTS_RAW="${2:-}"
shift 2
;;
--source-policy)
SOURCE_POLICY="${2:-}"
shift 2
;;
--signature-mode)
SIGNATURE_MODE_OVERRIDE="${2:-}"
shift 2
;;
--rollout-stage)
ROLLOUT_STAGE="${2:-}"
shift 2
;;
--canary)
ROLLOUT_STAGE="canary"
shift
;;
--cohort-id)
COHORT_ID="${2:-}"
shift 2
;;
--force-rollout)
FORCE_ROLLOUT=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "[transport-update] unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
require_cmd curl
require_cmd sha256sum
require_cmd python3
require_cmd install
require_cmd mktemp
require_cmd readlink
require_cmd ln
require_cmd awk
require_cmd tail
if [[ ! -f "$MANIFEST" ]]; then
echo "[transport-update] manifest not found: ${MANIFEST}" >&2
exit 1
fi
if [[ -z "$SOURCE_POLICY" && "$(basename "$MANIFEST")" == "manifest.production.json" && -f "$DEFAULT_SOURCE_POLICY" ]]; then
SOURCE_POLICY="$DEFAULT_SOURCE_POLICY"
fi
if [[ -n "$SOURCE_POLICY" && ! -f "$SOURCE_POLICY" ]]; then
echo "[transport-update] source policy not found: ${SOURCE_POLICY}" >&2
exit 1
fi
SIGNATURE_MODE_OVERRIDE="$(normalize_signature_mode "$SIGNATURE_MODE_OVERRIDE")"
ROLLOUT_STAGE="$(echo "$ROLLOUT_STAGE" | tr '[:upper:]' '[:lower:]' | xargs)"
case "$ROLLOUT_STAGE" in
stable|canary|any) ;;
*)
echo "[transport-update] rollout stage must be stable|canary|any" >&2
exit 1
;;
esac
if [[ -n "$COHORT_ID" ]]; then
if [[ ! "$COHORT_ID" =~ ^[0-9]+$ ]]; then
echo "[transport-update] cohort id must be integer 0..99" >&2
exit 1
fi
if (( COHORT_ID < 0 || COHORT_ID > 99 )); then
echo "[transport-update] cohort id must be in range 0..99" >&2
exit 1
fi
fi
if [[ -z "$TARGET" ]]; then
TARGET="$(normalize_os "$(uname -s)")-$(normalize_arch "$(uname -m)")"
fi
echo "[transport-update] manifest=${MANIFEST}"
echo "[transport-update] bin_root=${BIN_ROOT}"
echo "[transport-update] target=${TARGET}"
if [[ -n "$COMPONENTS_RAW" ]]; then
echo "[transport-update] components=${COMPONENTS_RAW}"
fi
if [[ -n "$SOURCE_POLICY" ]]; then
echo "[transport-update] source_policy=${SOURCE_POLICY}"
fi
echo "[transport-update] rollout_stage=${ROLLOUT_STAGE}"
if [[ -n "$COHORT_ID" ]]; then
echo "[transport-update] cohort_id=${COHORT_ID}"
fi
if [[ "$FORCE_ROLLOUT" -eq 1 ]]; then
echo "[transport-update] force_rollout=true"
fi
if [[ -n "$SIGNATURE_MODE_OVERRIDE" ]]; then
echo "[transport-update] signature_mode_override=${SIGNATURE_MODE_OVERRIDE}"
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[transport-update] mode=dry-run"
fi
mapfile -t ROWS < <(manifest_rows)
if [[ "${#ROWS[@]}" -eq 0 ]]; then
echo "[transport-update] no enabled components selected" >&2
exit 1
fi
mkdir -p "$BIN_ROOT" "$BIN_ROOT/releases" "$BIN_ROOT/.packaging"
for row in "${ROWS[@]}"; do
IFS=$'\x1f' read -r component binary_name version url sha256 asset_type asset_binary_path rollout_stage rollout_percent sig_type sig_url sig_sha256 sig_public_key_path <<< "$row"
release_dir="${BIN_ROOT}/releases/${component}/${version}"
release_binary="${release_dir}/${binary_name}"
active_link="${BIN_ROOT}/${binary_name}"
history_file="${BIN_ROOT}/.packaging/${component}.history"
sig_mode="$(evaluate_policy_and_signature_mode "$component" "$url" "$sig_url" "$sig_type")"
sig_mode="$(trim_spaces "$sig_mode")"
sig_mode="$(normalize_signature_mode "$sig_mode")"
if [[ "$sig_mode" == "required" && ( -z "$sig_type" || -z "$sig_url" || -z "$sig_public_key_path" ) ]]; then
echo "[transport-update] ${component}: signature_mode=required but signature metadata is incomplete" >&2
exit 1
fi
if [[ "$ROLLOUT_STAGE" != "any" && "$rollout_stage" != "$ROLLOUT_STAGE" ]]; then
echo "[transport-update] ${component}: skip rollout stage=${rollout_stage} (requested ${ROLLOUT_STAGE})"
continue
fi
effective_cohort="$COHORT_ID"
if [[ -z "$effective_cohort" ]]; then
effective_cohort="$(compute_cohort_id "$component" "$TARGET")"
fi
if [[ "$FORCE_ROLLOUT" -ne 1 && "$rollout_percent" -lt 100 && "$effective_cohort" -ge "$rollout_percent" ]]; then
echo "[transport-update] ${component}: skip rollout percent=${rollout_percent}% cohort=${effective_cohort}"
continue
fi
echo "[transport-update] ${component}: version=${version} binary=${binary_name}"
download_install_binary \
"$component" "$binary_name" "$version" "$url" "$sha256" "$asset_type" "$asset_binary_path" \
"$sig_mode" "$sig_type" "$sig_url" "$sig_sha256" "$sig_public_key_path" \
"$release_dir" "$release_binary"
prev_target=""
if [[ -L "$active_link" || -e "$active_link" ]]; then
prev_target="$(readlink -f "$active_link" || true)"
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[transport-update] DRY-RUN ${component}: switch ${active_link} -> ${release_binary}"
continue
fi
if [[ ! -x "$release_binary" ]]; then
echo "[transport-update] ${component}: installed binary is not executable: ${release_binary}" >&2
exit 1
fi
if [[ -n "$prev_target" && "$prev_target" != "$release_binary" && ! -f "$history_file" ]]; then
history_append_if_changed "$history_file" "$(date -u +%Y-%m-%dT%H:%M:%SZ)|${binary_name}|preexisting|${prev_target}" "$prev_target"
fi
ln -sfn "$release_binary" "$active_link"
history_append_if_changed "$history_file" "$(date -u +%Y-%m-%dT%H:%M:%SZ)|${binary_name}|${version}|${release_binary}" "$release_binary"
echo "[transport-update] ${component}: active -> ${release_binary}"
done
echo "[transport-update] done"