Created
January 14, 2026 15:14
-
-
Save cs224/3af61447aa358fceb77985c01e87a4e9 to your computer and use it in GitHub Desktop.
Nym Mixnet & dVPN: A Node Operator's Guide (2026) - Backup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| NODE_ID="${NODE_ID:-default-nym-node}" | |
| SRC_BASE="/root/.nym" | |
| SRC_NODE="${SRC_BASE}/nym-nodes/${NODE_ID}" | |
| BASE="/root/nym-backup" | |
| REPO="${BASE}/dotnym-repo" | |
| if [ ! -d "$SRC_NODE" ]; then | |
| echo "ERROR: Source node directory not found: $SRC_NODE" >&2 | |
| exit 1 | |
| fi | |
| if [ ! -d "${REPO}/.git" ]; then | |
| echo "ERROR: Backup repo not initialized at ${REPO}. Run backup-init.sh first." >&2 | |
| exit 1 | |
| fi | |
| mkdir -p "${REPO}/nym-nodes/${NODE_ID}" | |
| start_node() { | |
| systemctl start nym-node || true | |
| } | |
| trap start_node EXIT | |
| systemctl stop nym-node | |
| # Make sure SQLite DBs are self-contained (no WAL dependency) | |
| shopt -s nullglob | |
| for db in "${SRC_NODE}/data/"*.sqlite; do | |
| sqlite3 "$db" 'PRAGMA wal_checkpoint(TRUNCATE);' >/dev/null 2>&1 || true | |
| done | |
| shopt -u nullglob | |
| # Update target marker (optional but useful) | |
| mkdir -p "${REPO}/meta" | |
| { | |
| echo "hostname: $(hostname -f 2>/dev/null || hostname)" | |
| echo "node_id: ${NODE_ID}" | |
| echo "snapshot_utc: $(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| } > "${REPO}/meta/current_target.txt" | |
| rsync -a --delete \ | |
| --exclude 'data/replay-detection/' \ | |
| --exclude 'data/*.sqlite-wal' \ | |
| --exclude 'data/*.sqlite-shm' \ | |
| --exclude 'data/*~' \ | |
| --exclude 'data/*.bak*' \ | |
| "${SRC_NODE}/" "${REPO}/nym-nodes/${NODE_ID}/" | |
| cd "$REPO" | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "No changes detected; no commit created." | |
| else | |
| TS="$(date -u +%Y-%m-%d-%H%M%S)" | |
| git -c user.name="Anonymous" -c user.email="anon@example.com" commit -m "Snapshot ${TS}" | |
| git gc --auto || true | |
| echo "Committed snapshot ${TS}" | |
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| BASE="/root/nym-backup" | |
| REPO="${BASE}/dotnym-repo" | |
| BUNDLE_DIR="${BASE}/dotnym-bundles" | |
| if [ ! -d "${REPO}/.git" ]; then | |
| echo "ERROR: Backup repo not initialized at ${REPO}. Run backup-init.sh first." >&2 | |
| exit 1 | |
| fi | |
| mkdir -p "$BUNDLE_DIR" | |
| chmod 700 "$BUNDLE_DIR" | |
| cd "$REPO" | |
| LAST_TS="$(git log -1 --date=format:'%Y-%m-%d-%H%M%S' --format='%cd')" | |
| BUNDLE="${BUNDLE_DIR}/dotnym.${LAST_TS}.bundle" | |
| CHECKSUM="${BUNDLE}.sha256" | |
| # If bundle exists and verifies, treat as no-op. | |
| if [ -f "$BUNDLE" ]; then | |
| if git bundle verify "$BUNDLE" >/dev/null 2>&1; then | |
| echo "Bundle already exists and verifies: $BUNDLE" | |
| exit 0 | |
| else | |
| echo "Existing bundle failed verification; recreating: $BUNDLE" >&2 | |
| rm -f "$BUNDLE" "$CHECKSUM" | |
| fi | |
| fi | |
| git bundle create "$BUNDLE" --all | |
| git bundle verify "$BUNDLE" | |
| sha256sum "$BUNDLE" > "$CHECKSUM" | |
| chmod 600 "$BUNDLE" "$CHECKSUM" | |
| echo "Created and verified bundle: $BUNDLE" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| BASE="/root/nym-backup" | |
| REPO="${BASE}/dotnym-repo" | |
| BUNDLE_DIR="${BASE}/dotnym-bundles" | |
| mkdir -p "$REPO" "$BUNDLE_DIR" | |
| chmod 700 "$BASE" "$REPO" "$BUNDLE_DIR" | |
| cd "$REPO" | |
| if [ ! -d .git ]; then | |
| git init -b main | |
| fi | |
| cat > "${REPO}/.gitignore" <<'EOF' | |
| # Exclude large runtime anti-replay state | |
| nym-nodes/*/data/replay-detection/ | |
| # Exclude SQLite runtime sidecars (we snapshot DB files; WAL/SHM are noise) | |
| nym-nodes/*/data/*.sqlite-wal | |
| nym-nodes/*/data/*.sqlite-shm | |
| # Editor/backup noise | |
| **/*~ | |
| **/*.bak* | |
| EOF | |
| # Optional: store a per-snapshot "target marker" for audit/history | |
| mkdir -p "${REPO}/meta" | |
| if [ ! -f "${REPO}/meta/current_target.txt" ]; then | |
| { | |
| echo "hostname: $(hostname -f 2>/dev/null || hostname)" | |
| echo "created_utc: $(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| } > "${REPO}/meta/current_target.txt" | |
| fi | |
| git add .gitignore meta/current_target.txt || true | |
| # Only commit if this is a new repo or files changed | |
| if ! git rev-parse --verify HEAD >/dev/null 2>&1; then | |
| git -c user.name="Anonymous" -c user.email="anon@example.com" commit -m "Initialize backup repository" | |
| else | |
| if ! git diff --cached --quiet; then | |
| git -c user.name="Anonymous" -c user.email="anon@example.com" commit -m "Update backup repo metadata" | |
| fi | |
| fi | |
| echo "Init complete:" | |
| echo " Repo: $REPO" | |
| echo " Bundles: $BUNDLE_DIR" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # Usage: | |
| # backup-restore-bundle.sh /path/to/dotnym.YYYY-MM-DD-HHMMSS.bundle | |
| # | |
| # Optional environment variables: | |
| # NODE_ID=default-nym-node | |
| # SRC_BASE=/root/.nym | |
| # BACKUP_BASE=/root/nym-backup | |
| # START_SERVICE=true|false | |
| if [ $# -lt 1 ]; then | |
| echo "Usage: $0 /path/to/dotnym.<TIMESTAMP>.bundle" >&2 | |
| exit 2 | |
| fi | |
| # Resolve bundle to absolute path so later chdir/-C does not break it | |
| BUNDLE_PATH="$(readlink -f "$1" 2>/dev/null || true)" | |
| NODE_ID="${NODE_ID:-default-nym-node}" | |
| SRC_BASE="${SRC_BASE:-/root/.nym}" | |
| BACKUP_BASE="${BACKUP_BASE:-/root/nym-backup}" | |
| REPO="${BACKUP_BASE}/dotnym-repo" | |
| START_SERVICE="${START_SERVICE:-true}" | |
| TS="$(date -u +%Y-%m-%d-%H%M%S)" | |
| OLD_RUNTIME="${SRC_BASE}.old.${TS}" | |
| OLD_REPO="${REPO}.old.${TS}" | |
| COMPLETE="false" | |
| require_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || { | |
| echo "ERROR: Required command not found: $1" >&2 | |
| exit 1 | |
| } | |
| } | |
| rollback() { | |
| if [ "$COMPLETE" = "true" ]; then | |
| return 0 | |
| fi | |
| echo "ERROR occurred; attempting rollback..." >&2 | |
| # Restore repo | |
| if [ -d "$OLD_REPO" ]; then | |
| rm -rf "$REPO" 2>/dev/null || true | |
| mv "$OLD_REPO" "$REPO" 2>/dev/null || true | |
| fi | |
| # Restore runtime | |
| if [ -d "$OLD_RUNTIME" ]; then | |
| rm -rf "$SRC_BASE" 2>/dev/null || true | |
| mv "$OLD_RUNTIME" "$SRC_BASE" 2>/dev/null || true | |
| fi | |
| if [ "$START_SERVICE" = "true" ]; then | |
| systemctl start nym-node 2>/dev/null || true | |
| fi | |
| } | |
| trap rollback ERR | |
| if [ -z "$BUNDLE_PATH" ] || [ ! -f "$BUNDLE_PATH" ]; then | |
| echo "ERROR: Bundle not found: $1" >&2 | |
| exit 1 | |
| fi | |
| if [ "$(id -u)" -ne 0 ]; then | |
| echo "ERROR: Must be run as root." >&2 | |
| exit 1 | |
| fi | |
| require_cmd git | |
| require_cmd rsync | |
| mkdir -p "$BACKUP_BASE" | |
| chmod 700 "$BACKUP_BASE" | |
| echo "Restoring from bundle: $BUNDLE_PATH" | |
| echo "Node ID: $NODE_ID" | |
| echo "Runtime dir: $SRC_BASE" | |
| echo "Backup repo target: $REPO" | |
| echo "Timestamp: $TS" | |
| echo | |
| echo "[1/6] Stopping nym-node (best-effort)..." | |
| systemctl stop nym-node 2>/dev/null || true | |
| echo "[2/6] Backing up existing runtime dir (if any)..." | |
| if [ -d "$SRC_BASE" ]; then | |
| mv "$SRC_BASE" "$OLD_RUNTIME" | |
| else | |
| echo "No existing runtime dir." | |
| fi | |
| echo "[3/6] Backing up existing backup repo (if any)..." | |
| if [ -d "$REPO" ]; then | |
| mv "$REPO" "$OLD_REPO" | |
| else | |
| echo "No existing backup repo." | |
| fi | |
| echo "[4/6] Cloning bundle into backup repo..." | |
| # Note: this is the "integrity check" now; if the bundle is invalid, clone will fail. | |
| git clone "$BUNDLE_PATH" "$REPO" | |
| SNAPSHOT_NODE="${REPO}/nym-nodes/${NODE_ID}" | |
| if [ ! -d "$SNAPSHOT_NODE" ]; then | |
| echo "ERROR: Snapshot directory not found in bundle repo: $SNAPSHOT_NODE" >&2 | |
| echo "Available node IDs in bundle repo:" >&2 | |
| ls -1 "${REPO}/nym-nodes" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "[5/6] Restoring node snapshot into runtime..." | |
| mkdir -p "${SRC_BASE}/nym-nodes/${NODE_ID}" | |
| rsync -a --delete "${SNAPSHOT_NODE}/" "${SRC_BASE}/nym-nodes/${NODE_ID}/" | |
| # Restore optional /root/.nym/backup directory if present in snapshot repo | |
| if [ -d "${REPO}/backup" ]; then | |
| mkdir -p "${SRC_BASE}/backup" | |
| rsync -a --delete "${REPO}/backup/" "${SRC_BASE}/backup/" | |
| fi | |
| # Lock down perms (keys + mnemonic are sensitive) | |
| chown -R root:root "$SRC_BASE" | |
| chmod -R go-rwx "$SRC_BASE" | |
| echo "[6/6] Starting nym-node (if enabled, best-effort)..." | |
| if [ "$START_SERVICE" = "true" ]; then | |
| systemctl start nym-node 2>/dev/null || true | |
| systemctl status nym-node --no-pager 2>/dev/null || true | |
| else | |
| echo "START_SERVICE=false; leaving nym-node stopped." | |
| fi | |
| COMPLETE="true" | |
| echo | |
| echo "Restore complete." | |
| echo "Backups kept at (if they existed):" | |
| echo " $OLD_RUNTIME" | |
| echo " $OLD_REPO" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| alias git-ls='( cd /root/nym-backup/dotnym-repo && \ | |
| git log --pretty=format:%H | while read -r c; do \ | |
| date=$(git show --no-patch --format="%cd" --date=format:"%Y-%m-%d-%H%M" "$c"); \ | |
| subj=$(git show --no-patch --format="%s" "$c"); \ | |
| target=$(git show "$c:meta/current_target.txt" 2>/dev/null | tr "\n" " "); \ | |
| [ -z "$target" ] && target="N/A"; \ | |
| echo "$date $c $subj $target"; \ | |
| done )' | |
| alias nym-ls='curl --silent "https://api.github.com/repos/nymtech/nym/releases" | jq -r "(\"prerelease,draft,updated_at,url\"), (.[] | . as \$release | .assets[] | select(.name == \"nym-node\") | [\$release.prerelease,\$release.draft,.updated_at,.browser_download_url] | @csv)"' | |
| alias nym-dl='f(){ \ | |
| local tag="${1:-}"; \ | |
| if [ -z "$tag" ]; then \ | |
| echo "usage: nym-dl <nym-binaries-tag>" >&2; \ | |
| echo "example: nym-dl nym-binaries-v2025.21-mozzarella" >&2; \ | |
| return 2; \ | |
| fi; \ | |
| local dir="/root/nym-binaries"; \ | |
| local ver="$(printf "%s" "$tag" | sed -E "s/^nym-binaries-//")"; \ | |
| local url="https://github.com/nymtech/nym/releases/download/${tag}/nym-node"; \ | |
| local out="${dir}/nym-node-${ver}"; \ | |
| mkdir -p "$dir" || return 1; \ | |
| if ! curl --fail --location --show-error --silent "$url" -o "$out"; then \ | |
| echo "ERROR: download failed for tag: $tag" >&2; \ | |
| echo "URL: $url" >&2; \ | |
| rm -f "$out" 2>/dev/null || true; \ | |
| return 1; \ | |
| fi; \ | |
| chmod +x "$out" || return 1; \ | |
| echo "Downloaded: $out"; \ | |
| }; f' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| ARG="${1:-}" | |
| if [ -z "$ARG" ]; then | |
| echo "Usage: $0 <tag>" >&2 | |
| echo "Examples:" >&2 | |
| echo " $0 nym-binaries-v2025.21-mozzarella" >&2 | |
| echo " $0 v2025.21-mozzarella" >&2 | |
| exit 2 | |
| fi | |
| BIN_DIR="/root/nym-binaries" | |
| SYMLINK="${BIN_DIR}/nym-node" | |
| # Where to record the currently active binary (for your backup/audit workflow) | |
| BACKUP_REPO="/root/nym-backup/dotnym-repo" | |
| META_DIR="${BACKUP_REPO}/meta" | |
| TARGET_FILE="${META_DIR}/current_target.txt" | |
| require_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing command: $1" >&2; exit 1; } | |
| } | |
| require_cmd systemctl | |
| require_cmd readlink | |
| require_cmd ln | |
| require_cmd mv | |
| require_cmd sed | |
| require_cmd date | |
| require_cmd hostname | |
| if [ "$(id -u)" -ne 0 ]; then | |
| echo "ERROR: must be run as root." >&2 | |
| exit 1 | |
| fi | |
| # Normalize input: | |
| # - If user passed "nym-binaries-<ver>", strip the prefix. | |
| # - Otherwise accept "<ver>" as-is. | |
| # - Require it to start with "v" to avoid ambiguous input. | |
| if [[ "$ARG" == nym-binaries-* ]]; then | |
| VER="${ARG#nym-binaries-}" | |
| else | |
| VER="$ARG" | |
| fi | |
| if [[ "$VER" != v* ]]; then | |
| echo "ERROR: tag must start with 'v' (or be prefixed with 'nym-binaries-')." >&2 | |
| echo "Got: '$ARG'" >&2 | |
| exit 2 | |
| fi | |
| # The GitHub release tag is "nym-binaries-<ver>" | |
| TAG="nym-binaries-${VER}" | |
| NEW_BIN="${BIN_DIR}/nym-node-${VER}" | |
| if [ ! -d "$BIN_DIR" ]; then | |
| echo "ERROR: directory not found: $BIN_DIR" >&2 | |
| exit 1 | |
| fi | |
| if [ ! -f "$NEW_BIN" ]; then | |
| echo "ERROR: new binary not found: $NEW_BIN" >&2 | |
| echo "Hint: download it first, e.g.:" >&2 | |
| echo " nym-dl $TAG" >&2 | |
| exit 1 | |
| fi | |
| if [ ! -x "$NEW_BIN" ]; then | |
| echo "ERROR: new binary is not executable: $NEW_BIN" >&2 | |
| exit 1 | |
| fi | |
| OLD_TARGET="" | |
| if [ -L "$SYMLINK" ]; then | |
| OLD_TARGET="$(readlink -f "$SYMLINK" || true)" | |
| fi | |
| echo "Pre-flight:" | |
| echo " Input: $ARG" | |
| echo " Version: $VER" | |
| echo " Release tag: $TAG" | |
| echo " New binary: $NEW_BIN" | |
| echo " Symlink: $SYMLINK" | |
| echo " Old target: ${OLD_TARGET:-<none>}" | |
| echo | |
| echo "Checking new binary runs..." | |
| "$NEW_BIN" --version >/dev/null 2>&1 || { | |
| echo "ERROR: '$NEW_BIN --version' failed; aborting." >&2 | |
| exit 1 | |
| } | |
| echo "Stopping nym-node.service..." | |
| systemctl stop nym-node 2>/dev/null || true | |
| if systemctl is-active --quiet nym-node 2>/dev/null; then | |
| echo "ERROR: nym-node still active after stop; aborting." >&2 | |
| exit 1 | |
| fi | |
| echo "Swapping symlink atomically..." | |
| TMP_LINK="${SYMLINK}.tmp" | |
| rm -f "$TMP_LINK" 2>/dev/null || true | |
| ln -s "$NEW_BIN" "$TMP_LINK" | |
| mv -Tf "$TMP_LINK" "$SYMLINK" | |
| echo "Starting nym-node.service..." | |
| if ! systemctl start nym-node 2>/dev/null; then | |
| echo "ERROR: failed to start nym-node after swap; rolling back." >&2 | |
| if [ -n "$OLD_TARGET" ] && [ -f "$OLD_TARGET" ]; then | |
| ln -s "$OLD_TARGET" "$TMP_LINK" | |
| mv -Tf "$TMP_LINK" "$SYMLINK" | |
| systemctl start nym-node 2>/dev/null || true | |
| echo "Rollback complete. Symlink restored to: $OLD_TARGET" >&2 | |
| else | |
| echo "Rollback not possible (no previous symlink target found)." >&2 | |
| fi | |
| systemctl status nym-node --no-pager 2>/dev/null || true | |
| journalctl -u nym-node -n 50 --no-pager 2>/dev/null || true | |
| exit 1 | |
| fi | |
| systemctl is-active --quiet nym-node 2>/dev/null || { | |
| echo "ERROR: nym-node did not become active; showing logs." >&2 | |
| systemctl status nym-node --no-pager 2>/dev/null || true | |
| journalctl -u nym-node -n 80 --no-pager 2>/dev/null || true | |
| exit 1 | |
| } | |
| # Record the new active binary in your backup repo meta file (separate from snapshot metadata) | |
| NEW_TARGET="$(readlink -f "$SYMLINK" || true)" | |
| TS_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| HOST_FQDN="$(hostname -f 2>/dev/null || hostname)" | |
| UPGRADE_FILE="${META_DIR}/current_upgrade.txt" | |
| # Extract a few stable fields from --version output (avoid huge multi-line diffs) | |
| VERSION_OUT="$("$SYMLINK" --version 2>/dev/null || true)" | |
| BUILD_VERSION="$(printf "%s\n" "$VERSION_OUT" | awk -F': +' '/^Build Version:/ {print $2; exit}')" | |
| COMMIT_SHA="$(printf "%s\n" "$VERSION_OUT" | awk -F': +' '/^Commit SHA:/ {print $2; exit}')" | |
| BUILD_TS="$(printf "%s\n" "$VERSION_OUT" | awk -F': +' '/^Build Timestamp:/ {print $2; exit}')" | |
| if [ -d "$BACKUP_REPO" ]; then | |
| mkdir -p "$META_DIR" | |
| chmod 700 "$META_DIR" 2>/dev/null || true | |
| { | |
| echo "hostname: ${HOST_FQDN}" | |
| echo "updated_utc: ${TS_UTC}" | |
| echo "release_tag: ${TAG}" | |
| echo "version: ${VER}" | |
| echo "binary_symlink: ${SYMLINK}" | |
| echo "binary_target: ${NEW_TARGET:-<unresolved>}" | |
| echo "build_version: ${BUILD_VERSION:-<unknown>}" | |
| echo "build_timestamp: ${BUILD_TS:-<unknown>}" | |
| echo "commit_sha: ${COMMIT_SHA:-<unknown>}" | |
| } > "$UPGRADE_FILE" | |
| chmod 600 "$UPGRADE_FILE" 2>/dev/null || true | |
| echo "Updated: $UPGRADE_FILE" | |
| else | |
| echo "WARNING: backup repo not found at $BACKUP_REPO; skipping meta update." >&2 | |
| fi | |
| echo | |
| echo "Upgrade successful." | |
| echo "Symlink now points to:" | |
| readlink -f "$SYMLINK" || true | |
| echo "Running version:" | |
| "$SYMLINK" --version 2>/dev/null || true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment